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

1040 lines
41 KiB
Python

import torch
import gc
import functools
import json
import inspect
from tqdm import tqdm
from collections import defaultdict
from torch import optim
from .smooth_quantizer import ACIQ, SmoothQuantizer
import torch.nn.functional as F
class OmniQuantizer:
def __init__(
self,
model,
max_calib_samples=32,
max_calib_seq_len=128,
act_bit=8,
act_sym=True,
generate_for_npu=False,
epochs=20,
lr=5e-3,
wd=0.0
) -> None:
self.model = model
self.tokenizer = model.tokenizer
self.act_bit = act_bit
self.act_sym = act_sym
self.generate_for_npu = generate_for_npu
self.epochs = epochs
self.lr = lr
self.wd = wd
self.max_calib_samples = max_calib_samples
self.max_calib_seq_len = max_calib_seq_len
self.calib_data = 'wikitext' if model.args.calib_data is None else model.args.calib_data
self.split = 'train'
self.best_device = self.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)):
self.to_device(self.modules[idx], "cpu")
self.act_dict = [defaultdict(dict) for _ in range(len(self.modules))]
@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
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 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 clear_memory(weight=None):
if weight is not None:
del weight
gc.collect()
torch.cuda.empty_cache()
@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
with open(data, 'r', encoding='utf-8') as f:
dataset = f.read().splitlines()
else:
raise NotImplementedError("Data loading error")
samples = []
if custom_calib_data == False:
dataset = dataset.shuffle(seed=42)
count = 0
idx = 0
while count < n_samples and idx < len(dataset):
try:
text = dataset[idx]["text"]
# skip empty lines
if not text.strip():
idx += 1
continue
input_ids = tokenizer(
text, return_tensors="pt", max_length=max_seq_len, truncation=True
).input_ids
# skip empty tokenized inputs
if input_ids.numel() > 0:
samples.append(input_ids)
count += 1
except:
pass
idx += 1
else:
for i in range(min(n_samples, len(dataset))):
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
if input_ids.numel() > 0:
samples.append(input_ids)
print(f"Collected {len(samples)} valid calibration samples.")
return samples
def init_quant(self, n_samples=128, max_seq_len=512):
samples = self.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):
sample = sample.long()
layer_kwargs = {}
seq_len = sample.numel()
new_tokens = 0
try:
inps = self.model.embedding(sample)
except RuntimeError:
sample = sample.to(self.best_device)
inps = self.model.embedding(sample)
inps = inps.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)
layer_kwargs["position_ids"] = position_ids.to(self.best_device)
del sample
self.clear_memory()
return layer_kwargs, inps
def _sanitize_kwargs(self, inputs_kwargs, module):
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):
"""Select per-layer kwargs for mixed attention models."""
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
def _clear_block_kv_cache(self, block):
"""Clear KV cache on the block's attention so each calibration sample is independent."""
if hasattr(block, "self_attn") and block.self_attn is not None:
if hasattr(block.self_attn, "past_key_value"):
block.self_attn.past_key_value = None
if hasattr(block.self_attn, "conv_state"):
block.self_attn.conv_state = None
if hasattr(block.self_attn, "rnn_state"):
block.self_attn.rnn_state = None
def _safe_forward(self, x, module, module_kwargs):
try:
target_dtype = next(module.parameters()).dtype
target_device = next(module.parameters()).device
except StopIteration:
target_dtype = torch.float32
target_device = x.device
x = x.to(device=target_device, dtype=target_dtype)
if "cuda" in str(target_device):
with torch.cuda.amp.autocast(enabled=True, dtype=target_dtype):
out = module(x, **module_kwargs)
else:
out = module(x, **module_kwargs)
if isinstance(out, tuple):
out = out[0]
return out
def _run_optimization(self, x_in, fcs, ln, act_max):
device = self.best_device
target_dtype = list(fcs[0].parameters())[0].dtype
# Increase micro_batch_size for better GPU utilization
micro_batch_size = 64
# Pre-move weights to GPU and keep there
weights = torch.cat([fc.weight for fc in fcs], dim=0).to(device)
act_max = act_max.to(device=device, dtype=target_dtype)
weight_max_per_channel = torch.cat([fc.weight.abs().max(dim=0, keepdim=True)[0] for fc in fcs], dim=0)
weight_max_per_channel = weight_max_per_channel.max(dim=0)[0].clamp(min=1e-5).to(device)
scales_init = (act_max.pow(0.5) / weight_max_per_channel.pow(0.5)).clamp(min=1e-5)
scales_init = scales_init.to(device=device, dtype=target_dtype)
log_scale = torch.nn.Parameter(torch.log(scales_init))
with torch.no_grad():
w_init_smooth = weights * scales_init.view(1, -1)
clip_init = w_init_smooth.abs().max(dim=1, keepdim=True)[0]
clip_val = torch.nn.Parameter(clip_init)
optimizer = optim.AdamW([
{'params': [log_scale], 'lr': self.lr},
{'params': [clip_val], 'lr': self.lr}
], weight_decay=self.wd)
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(
optimizer, T_max=self.epochs, eta_min=self.lr * 0.1
)
# Pre-compute constants for quantization
act_bit = self.act_bit
act_sym = self.act_sym
q_max_w = 2 ** (act_bit - 1) - 1
# Inline quantize functions to reduce function call overhead
if act_sym:
q_max_act = 2 ** (act_bit - 1) - 1
q_min_act = -q_max_act
else:
q_max_act = 2 ** act_bit - 1
q_min_act = 0
N = x_in.shape[0]
num_steps = (N + micro_batch_size - 1) // micro_batch_size
# Pre-load all data to GPU if it fits, otherwise use pinned memory
try:
# Try to fit all data on GPU
x_in_gpu = x_in.to(device, dtype=target_dtype)
use_gpu_data = True
except RuntimeError:
# Fall back to CPU with pinned memory for faster transfer
x_in_gpu = x_in.pin_memory() if x_in.device.type == 'cpu' else x_in
use_gpu_data = False
# Pre-compute target outputs for all batches (computed once, not every epoch)
with torch.no_grad():
y_targets = []
for i in range(0, N, micro_batch_size):
if use_gpu_data:
x_batch = x_in_gpu[i : i + micro_batch_size]
else:
x_batch = x_in_gpu[i : i + micro_batch_size].to(device, dtype=target_dtype, non_blocking=True)
with torch.cuda.amp.autocast(enabled=True, dtype=target_dtype):
y_target = F.linear(x_batch, weights)
y_targets.append(y_target.float())
if not use_gpu_data:
del x_batch
for epoch in range(self.epochs):
optimizer.zero_grad(set_to_none=True) # More efficient than zero_grad()
total_loss = 0.0
for batch_idx, i in enumerate(range(0, N, micro_batch_size)):
if use_gpu_data:
x_micro = x_in_gpu[i : i + micro_batch_size]
else:
x_micro = x_in_gpu[i : i + micro_batch_size].to(device, dtype=target_dtype, non_blocking=True)
y_micro_target = y_targets[batch_idx]
scale = torch.exp(log_scale)
s_view = scale.view(1, -1)
x_sim = x_micro / s_view
w_sim = weights * s_view
if act_sym:
act_scale = x_sim.abs().max() / q_max_act
act_scale = torch.clamp(act_scale, min=1e-5)
x_q = torch.round(x_sim / act_scale)
x_q = torch.clamp(x_q, q_min_act, q_max_act)
x_q = x_q * act_scale
x_q = (x_q - x_sim).detach() + x_sim
else:
t_min, t_max = x_sim.min(), x_sim.max()
act_scale = (t_max - t_min) / q_max_act
act_scale = torch.clamp(act_scale, min=1e-5)
zero = -t_min / act_scale
x_q = torch.round(x_sim / act_scale + zero)
x_q = torch.clamp(x_q, q_min_act, q_max_act)
x_q = (x_q - zero) * act_scale
x_q = (x_q - x_sim).detach() + x_sim
# Inline quantize_weight_with_clip
clip_v = F.relu(clip_val) + 1e-5
w_clamped = torch.clamp(w_sim, -clip_v, clip_v)
w_scale = clip_v / q_max_w
w_q = torch.round(w_clamped / w_scale) * w_scale
w_q = (w_q - w_clamped).detach() + w_clamped
x_q = x_q.to(dtype=target_dtype)
w_q = w_q.to(dtype=target_dtype)
with torch.cuda.amp.autocast(enabled=True, dtype=target_dtype):
y_pred = F.linear(x_q, w_q)
loss = F.mse_loss(y_pred.float(), y_micro_target)
loss = loss / num_steps
loss.backward()
total_loss += loss.item()
optimizer.step()
scheduler.step()
# Cleanup y_targets
del y_targets
with torch.no_grad():
final_scale = torch.exp(log_scale).detach().view(-1)
if self._is_offset_rmsnorm(ln):
ln.weight += 1
ln.weight.div_(final_scale)
ln.weight -= 1
else:
ln.weight.div_(final_scale)
if hasattr(ln, "bias") and ln.bias is not None:
ln.bias.div_(final_scale)
final_clip = F.relu(clip_val).detach()
current_idx = 0
for fc in fcs:
num_out = fc.weight.shape[0]
layer_clip = final_clip[current_idx : current_idx + num_out]
fc.weight.mul_(final_scale.view(1, -1))
fc.weight.data = torch.clamp(fc.weight.data, -layer_clip, layer_clip)
current_idx += num_out
del x_in_gpu, weights, weight_max_per_channel, scales_init, log_scale, clip_val, optimizer, scheduler
torch.cuda.empty_cache()
def _get_robust_act_max(self, x):
try:
x_flat = x.reshape(-1, x.shape[-1])
if x_flat.shape[0] > 2048:
if x_flat.shape[0] > 10000:
indices = torch.randperm(x_flat.shape[0])[:10000]
x_sample = x_flat[indices]
else:
x_sample = x_flat
robust_max = torch.quantile(x_sample.abs().float(), 0.999, dim=0)
return robust_max
else:
return x_flat.abs().max(dim=0)[0]
except:
return x.reshape(-1, x.shape[-1]).abs().max(dim=0)[0]
def _extract_static_scales(self):
print("OmniQuant: Extracting final JSON 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
return [scale, 0.0]
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()
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 else (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)
return [scale, zero]
func = compute_scale_sym if self.act_sym else compute_scale_zero_asym
for idx in range(len(self.act_dict)):
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 _get_all_static_scales_safe(self, idx, layer, named_linears, x_in, module_kwargs):
def stat_io_hook(m, x, y, name):
if isinstance(x, tuple): x = x[0]
if isinstance(y, tuple): y = y[0]
inp_max_min = self.act_quanter.get_max_min(x.detach().float().to("cpu"))
out_max_min = self.act_quanter.get_max_min(y.detach().float().to("cpu"))
if name not in self.act_dict[idx] or "input" not in self.act_dict[idx][name]:
self.act_dict[idx][name]["input"] = inp_max_min
else:
self.act_dict[idx][name]["input"] = inp_max_min * (1-self.moment) + self.moment * self.act_dict[idx][name]["input"]
if name not in self.act_dict[idx] or "output" not in self.act_dict[idx][name]:
self.act_dict[idx][name]["output"] = out_max_min
else:
self.act_dict[idx][name]["output"] = out_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, module_kwargs)
sanitized_kwargs = self._sanitize_kwargs(layer_kwargs, layer)
with torch.no_grad():
self._safe_forward(x_in, layer, sanitized_kwargs)
for h in handles:
h.remove()
def _prepare_calibration_data(self):
"""Prepare calibration samples and compute initial embeddings.
Returns:
layer_inputs: List of (input_tensor, kwargs) tuples for each sample
"""
# Check if we already have prepared data
if hasattr(self, '_cached_layer_inputs') and self._cached_layer_inputs is not None:
print("OmniQuant: Using cached calibration data...")
return self._cached_layer_inputs
print("OmniQuant: Initializing...")
self.samples = self.init_quant(
n_samples=self.max_calib_samples,
max_seq_len=self.max_calib_seq_len,
)
print(f"OmniQuant: Pre-computing embeddings for {len(self.samples)} samples...")
layer_inputs = []
for sample in self.samples:
# skip empty sample
if sample.numel() == 0:
continue
kw, inp = self._get_first_input(sample)
cpu_kw = {}
for k, v in kw.items():
if isinstance(v, torch.Tensor):
cpu_kw[k] = v.to("cpu")
else:
cpu_kw[k] = v
layer_inputs.append((inp.to("cpu"), cpu_kw))
self.clear_memory()
# Cache for potential reuse
self._cached_layer_inputs = layer_inputs
return layer_inputs
def optimize_weights(self, collect_feature_map=False):
"""Phase 1: Optimize weights by adjusting LayerNorm and Linear layer weights.
This phase applies smooth quantization optimization to reduce quantization error.
Args:
collect_feature_map: If True, also collect feature map info during this pass
to avoid a second traversal.
"""
layer_inputs = self._prepare_calibration_data()
if collect_feature_map:
# Re-initialize act_dict for fresh collection
self.act_dict = [defaultdict(dict) for _ in range(len(self.modules))]
print(f"OmniQuant: Starting weight optimization (Epochs={self.epochs})...")
for idx in tqdm(range(len(self.modules)), desc="OmniQuant: Optimize Weights"):
block = self.modules[idx]
self.to_device(block, self.best_device)
attn_inputs_list = []
mlp_inputs_list = []
next_layer_outputs = []
def hook_attn_input(m, i, o):
if isinstance(i, tuple) and len(i) > 0:
inp = i[0]
else:
inp = i
attn_inputs_list.append(inp.detach().view(-1, inp.shape[-1]))
def hook_mlp_input(m, i, o):
if isinstance(i, tuple) and len(i) > 0:
inp = i[0]
else:
inp = i
mlp_inputs_list.append(inp.detach().view(-1, inp.shape[-1]))
attn_module = block.self_attn
attn_linears = []
attn_hook_target = None
if all(hasattr(attn_module, name) for name in ("q_proj", "k_proj", "v_proj")):
attn_linears = [attn_module.q_proj, attn_module.k_proj, attn_module.v_proj]
attn_hook_target = attn_module.q_proj
elif hasattr(attn_module, "in_proj_qkv"):
attn_linears = [attn_module.in_proj_qkv]
for optional_name in ("in_proj_a", "in_proj_b", "in_proj_z"):
optional_proj = getattr(attn_module, optional_name, None)
if optional_proj is not None:
attn_linears.append(optional_proj)
attn_hook_target = attn_module.in_proj_qkv
h1 = None
if attn_hook_target is not None:
h1 = attn_hook_target.register_forward_hook(hook_attn_input)
h2 = block.mlp.gate_proj.register_forward_hook(hook_mlp_input)
# Pre-compute sanitized kwargs once for this block
sample_kw_gpu = {}
for k, v in layer_inputs[0][1].items():
if isinstance(v, torch.Tensor):
sample_kw_gpu[k] = v.to(self.best_device)
else:
sample_kw_gpu[k] = v
sample_kw_gpu = self._select_layer_kwargs(block, sample_kw_gpu)
sanitized_kw_template = self._sanitize_kwargs(sample_kw_gpu, block)
# Single forward pass: collect hooks AND compute outputs
with torch.no_grad():
for inp, kw in layer_inputs:
# Clear KV cache so each sample is processed independently (no past_key_value from previous iteration)
self._clear_block_kv_cache(block)
inp_gpu = inp.to(self.best_device)
kw = self._select_layer_kwargs(block, kw)
# Reuse sanitized keys, only update tensor values
kw_gpu = {}
for k, v in kw.items():
if k in sanitized_kw_template:
if isinstance(v, torch.Tensor):
kw_gpu[k] = v.to(self.best_device)
else:
kw_gpu[k] = v
out = self._safe_forward(inp_gpu, block, kw_gpu)
# Store output for next layer
next_layer_outputs.append((out.detach().to("cpu"), kw))
del inp_gpu, kw_gpu, out
if h1 is not None:
h1.remove()
h2.remove()
# Process collected attention inputs
optimize_attn = len(attn_inputs_list) > 0
if optimize_attn:
# Concatenate on GPU, then move to CPU once
total_attn_in = torch.cat(attn_inputs_list, dim=0).to("cpu")
del attn_inputs_list
# Mixed-attention models such as Qwen3.5 are highly sensitive to
# attention-side rescaling. Keep the generic optimization path for
# other architectures only.
ln_attn = block.input_layernorm
robust_max_attn = self._get_robust_act_max(total_attn_in)
self._run_optimization(total_attn_in, attn_linears, ln_attn, robust_max_attn)
del robust_max_attn
del total_attn_in
else:
del attn_inputs_list
# Process collected MLP inputs
optimize_mlp = len(mlp_inputs_list) > 0
if optimize_mlp:
# Concatenate on GPU, then move to CPU once
total_mlp_in = torch.cat(mlp_inputs_list, dim=0).to("cpu")
del mlp_inputs_list
fcs_mlp = [block.mlp.gate_proj, block.mlp.up_proj]
ln_mlp = block.post_attention_layernorm
robust_max_mlp = self._get_robust_act_max(total_mlp_in)
self._run_optimization(total_mlp_in, fcs_mlp, ln_mlp, robust_max_mlp)
del total_mlp_in, robust_max_mlp
else:
del mlp_inputs_list
self.clear_memory()
# Outputs already computed in the single forward pass above
layer_inputs = next_layer_outputs
del next_layer_outputs
if "cpu" != self.best_device:
self.to_device(block, "cpu")
self.clear_memory()
print("OmniQuant: Weight optimization completed.")
# Save final layer outputs for potential reuse by collect_feature_map_info
self._final_layer_outputs = layer_inputs
for idx in range(len(self.modules)):
self.to_device(self.modules[idx], "cpu")
self.clear_memory()
# If collect_feature_map is requested, do it now using optimized weights
if collect_feature_map:
self._collect_feature_map_optimized()
def _collect_lm_head_info(self, calib_inputs):
"""Collect lm_head layer activation info for NPU."""
if not self.generate_for_npu:
return
lm_head_idx = len(self.modules)
self.act_dict.append(defaultdict(dict))
if hasattr(self.model, 'lm') and hasattr(self.model.lm, 'lm'):
lm_head = self.model.lm.lm
elif hasattr(self.model, 'lm_head'):
lm_head = self.model.lm_head
else:
lm_head = None
print("Warning: lm_head not found in model, skipping lm_head calibration.")
if lm_head is not None:
lm_head.to(self.best_device)
if hasattr(self.model, 'final_layernorm'):
self.model.final_layernorm.to(self.best_device)
lm_head_ops = {'lm_head': lm_head}
for inp, kw in calib_inputs:
inp_gpu = inp.to(self.best_device)
with torch.no_grad():
if hasattr(self.model, 'final_layernorm'):
hidden_states = self.model.final_layernorm(inp_gpu)
else:
hidden_states = inp_gpu
self._get_all_static_scales_safe(lm_head_idx, lm_head, lm_head_ops, hidden_states, {})
del inp_gpu, hidden_states
lm_head.to("cpu")
if hasattr(self.model, 'final_layernorm'):
self.model.final_layernorm.to("cpu")
self.clear_memory()
def _collect_feature_map_optimized(self):
"""Optimized feature map collection that reuses embedding computation.
This uses cached layer outputs from optimize_weights() to avoid
re-computing embeddings through all layers.
"""
print("OmniQuant: Collecting static activation scales (optimized)...")
gc.collect()
torch.cuda.empty_cache()
# Re-initialize act_dict
self.act_dict = [defaultdict(dict) for _ in range(len(self.modules))]
# Use cached initial inputs
calib_inputs = self._prepare_calibration_data()
for idx in tqdm(range(len(self.modules)), desc="Collecting Feature Map Info"):
block = self.modules[idx]
self.to_device(block, self.best_device)
target_ops = SmoothQuantizer.get_all_leaf_modules(block)
next_calib_inputs = []
# Batch process samples for better GPU utilization
batch_size = min(8, len(calib_inputs)) # Process multiple samples together
for batch_start in range(0, len(calib_inputs), batch_size):
batch_end = min(batch_start + batch_size, len(calib_inputs))
batch_items = calib_inputs[batch_start:batch_end]
for inp, kw in batch_items:
# Process each calibration sample independently to avoid KV cache from the previous sample causing dimension mismatch between attn_weights and attention_mask
self._clear_block_kv_cache(block)
inp_gpu = inp.to(self.best_device)
kw = self._select_layer_kwargs(block, kw)
kw_gpu = {k: (v.to(self.best_device) if isinstance(v, torch.Tensor) else v) for k, v in kw.items()}
# First forward: collect activation scales
self._get_all_static_scales_safe(idx, block, target_ops, inp_gpu, kw_gpu)
sanitized_kw = self._sanitize_kwargs(kw_gpu, block)
# Clear again before the second forward: _get_all_static_scales_safe has already run one forward and written past_key_value; if not cleared, key concatenation in this forward would cause dimension error (256 vs 128)
self._clear_block_kv_cache(block)
with torch.no_grad():
out = self._safe_forward(inp_gpu, block, sanitized_kw)
next_calib_inputs.append((out.cpu(), kw))
del inp_gpu, kw_gpu, out
calib_inputs = next_calib_inputs
if "cpu" != self.best_device:
self.to_device(block, "cpu")
self.clear_memory()
# Collect lm_head info if needed
self._collect_lm_head_info(calib_inputs)
del calib_inputs
self.clear_memory()
self._extract_static_scales()
for idx in range(len(self.modules)):
self.to_device(self.modules[idx], "cpu")
self.clear_memory()
print("OmniQuant: Feature map info collection completed.")
def quantize(self, collect_feature_map=False):
"""Run the full OmniQuant quantization pipeline.
Args:
collect_feature_map: If True, collect feature map info after weight optimization.
If False, only perform weight optimization.
"""
# Run weight optimization, optionally collecting feature map info in the same pass
self.optimize_weights(collect_feature_map=collect_feature_map)
def clear_cache(self):
"""Clear cached calibration data to free memory."""
if hasattr(self, '_cached_layer_inputs'):
del self._cached_layer_inputs
self._cached_layer_inputs = None
if hasattr(self, '_final_layer_outputs'):
del self._final_layer_outputs
self._final_layer_outputs = None
self.clear_memory()
def _find_match_in_dict(self, mnn_op_name, layer_act_dict):
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):
import copy
PASS_THROUGH_OPS = [
'Reshape', 'Squeeze', 'Unsqueeze', 'Flatten',
'Transpose', 'Permute', 'ConvertTensor', 'Cast',
'Slice', 'StridedSlice', 'Split', 'Concat', 'Pack'
]
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
if op_type in PASS_THROUGH_OPS:
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
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
elif op_type in DATA_SELECT_OPS:
data_idx = inputs[0]
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
elif op_type == 'BinaryOp':
out_idx = outputs[0]
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
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:
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', '')
should_process = False
if not self.generate_for_npu:
should_process = (op_type == 'Convolution')
else:
should_process = (op_type not in npu_ignore_types)
# Handle lm_head separately using the dedicated index
if 'lm_head' in op_name:
if self.generate_for_npu and should_process:
lm_head_idx = len(self.modules) # lm_head is stored at this index
if lm_head_idx < len(self.act_dict) and len(self.act_dict[lm_head_idx]) > 0:
# lm_head stats are stored with key 'lm_head'
if 'lm_head' in self.act_dict[lm_head_idx]:
stats = self.act_dict[lm_head_idx]['lm_head']
print("Quantize lm head for QNN")
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
}
}
continue
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