Model Pruning: Comprimindo LLMs
Quando Usar Esta Skill
Use Model Pruning quando você precisar:
- Reduzir tamanho do modelo de 40-60% com <1% de perda de acurácia
- Acelerar a inferência usando esparsidade amigável ao hardware (speedup 2-4×)
- Fazer deploy em hardware limitado (dispositivos móveis, edge)
- Comprimir sem retreinamento usando métodos one-shot
- Habilitar serving eficiente com pegada de memória reduzida
Técnicas-chave: Wanda (weights × activations), SparseGPT (segunda ordem), pruning estruturado, esparsidade N:M
Papers: Wanda ICLR 2024 (arXiv 2306.11695), SparseGPT (arXiv 2301.00774)
Instalação
# Implementação Wanda
git clone https://github.com/locuslab/wanda
cd wanda
pip install -r requirements.txt
# Opcional: SparseGPT
git clone https://github.com/IST-DASLab/sparsegpt
cd sparsegpt
pip install -e .
# Dependências
pip install torch transformers accelerate
Quick Start
Wanda Pruning (One-Shot, Sem Retreinamento)
Fonte: ICLR 2024 (arXiv 2306.11695)
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
# Carrega modelo
model = AutoModelForCausalLM.from_pretrained(
"meta-llama/Llama-2-7b-hf",
torch_dtype=torch.float16,
device_map="cuda"
)
tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-2-7b-hf")
# Dados de calibração (pequeno dataset para estatísticas de ativação)
calib_data = [
"The quick brown fox jumps over the lazy dog.",
"Machine learning is transforming the world.",
"Artificial intelligence powers modern applications.",
]
# Função de pruning Wanda
def wanda_prune(model, calib_data, sparsity=0.5):
"""
Wanda: Prune por magnitude de peso × ativação de entrada.
Args:
sparsity: Fração de pesos a fazer prune (0.5 = 50%)
"""
# 1. Coleta estatísticas de ativação
activations = {}
def hook_fn(name):
def hook(module, input, output):
# Armazena nomas de ativação de entrada
activations[name] = input[0].detach().abs().mean(dim=0)
return hook
# Registra hooks para todas as camadas lineares
hooks = []
for name, module in model.named_modules():
if isinstance(module, torch.nn.Linear):
hooks.append(module.register_forward_hook(hook_fn(name)))
# Executa dados de calibração
model.eval()
with torch.no_grad():
for text in calib_data:
inputs = tokenizer(text, return_tensors="pt").to(model.device)
model(**inputs)
# Remove hooks
for hook in hooks:
hook.remove()
# 2. Prune pesos baseado em |weight| × ativação
for name, module in model.named_modules():
if isinstance(module, torch.nn.Linear) and name in activations:
W = module.weight.data
act = activations[name]
# Computa importância: |weight| × ativação
importance = W.abs() * act.unsqueeze(0)
# Achata e encontra threshold
threshold = torch.quantile(importance.flatten(), sparsity)
# Cria máscara
mask = importance >= threshold
# Aplica máscara (prune)
W *= mask.float()
return model
# Aplica pruning Wanda (50% sparsidade, one-shot, sem retreinamento)
pruned_model = wanda_prune(model, calib_data, sparsity=0.5)
# Salva
pruned_model.save_pretrained("./llama-2-7b-wanda-50")
SparseGPT (Pruning de Segunda Ordem)
Fonte: arXiv 2301.00774
from sparsegpt import SparseGPT
# Carrega modelo
model = AutoModelForCausalLM.from_pretrained("meta-llama/Llama-2-7b-hf")
# Inicializa SparseGPT
pruner = SparseGPT(model)
# Dados de calibração
calib_data = load_calibration_data() # ~128 amostras
# Prune (one-shot, reconstrução layer-wise)
pruned_model = pruner.prune(
calib_data=calib_data,
sparsity=0.5, # 50% sparsidade
prunen=0, # Não estruturado (0) ou N:M estruturado
prunem=0,
percdamp=0.01, # Damping para inversa de Hessian
)
# Resultados: Pruning quasi-sem perda em 50% sparsidade
N:M Structured Pruning (Acelerador de Hardware)
def nm_prune(weight, n=2, m=4):
"""
Pruning N:M: Mantém N pesos a cada M pesos consecutivos.
Exemplo: 2:4 = manter 2 a cada 4 pesos.
Compatível com sparse tensor cores NVIDIA (2:4, 4:8).
"""
# Reshape peso em grupos de M
shape = weight.shape
weight_flat = weight.flatten()
# Padding para múltiplo de M
pad_size = (m - weight_flat.numel() % m) % m
weight_padded = F.pad(weight_flat, (0, pad_size))
# Reshape em (num_groups, m)
weight_grouped = weight_padded.reshape(-1, m)
# Encontra top-N em cada grupo
_, indices = torch.topk(weight_grouped.abs(), n, dim=-1)
# Cria máscara
mask = torch.zeros_like(weight_grouped)
mask.scatter_(1, indices, 1.0)
# Aplica máscara
weight_pruned = weight_grouped * mask
# Reshape de volta
weight_pruned = weight_pruned.flatten()[:weight_flat.numel()]
return weight_pruned.reshape(shape)
# Aplica sparsidade 2:4 (hardware NVIDIA)
for name, module in model.named_modules():
if isinstance(module, torch.nn.Linear):
module.weight.data = nm_prune(module.weight.data, n=2, m=4)
# 50% sparsidade, 2× speedup em A100 com sparse tensor cores
Conceitos Principais
1. Critérios de Pruning
Pruning por Magnitude (baseline):
# Prune pesos com os menores valores absolutos
importance = weight.abs()
threshold = torch.quantile(importance, sparsity)
mask = importance >= threshold
Wanda (weights × activations):
# Importância = |weight| × input_activation
importance = weight.abs() * activation
# Melhor que magnitude pura (considera uso)
SparseGPT (segunda ordem):
# Usa Hessian (segunda derivada) para importância
# Mais acurado mas computacionalmente caro
importance = weight^2 / diag(Hessian)
2. Estruturado vs Não Estruturado
Não estruturado (granular fino):
- Prune pesos individuais
- Qualidade superior (melhor acurácia)
- Sem speedup de hardware (esparsidade irregular)
Estruturado (granular grosso):
- Prune neurônios, heads ou camadas inteiras
- Qualidade inferior (mais perda de acurácia)
- Speedup de hardware (esparsidade regular)
Semi-estruturado (N:M):
- O melhor dos dois mundos
- 50% sparsidade (2:4) → 2× speedup em GPUs NVIDIA
- Perda mínima de acurácia
3. Padrões de Esparsidade
# Não estruturado (aleatório)
# [1, 0, 1, 0, 1, 1, 0, 0]
# Pros: Flexível, alta qualidade
# Cons: Sem speedup
# Estruturado (bloco)
# [1, 1, 0, 0, 1, 1, 0, 0]
# Pros: Amigável ao hardware
# Cons: Mais perda de acurácia
# N:M (semi-estruturado)
# [1, 0, 1, 0] [1, 1, 0, 0] (padrão 2:4)
# Pros: Speedup de hardware + boa qualidade
# Cons: Requer hardware específico (NVIDIA)
Estratégias de Pruning
Estratégia 1: Pruning Gradual por Magnitude
def gradual_prune(model, initial_sparsity=0.0, final_sparsity=0.5, num_steps=100):
"""Aumenta gradualmente sparsidade durante o treinamento."""
for step in range(num_steps):
# Sparsidade atual
current_sparsity = initial_sparsity + (final_sparsity - initial_sparsity) * (step / num_steps)
# Prune na sparsidade atual
for module in model.modules():
if isinstance(module, torch.nn.Linear):
weight = module.weight.data
threshold = torch.quantile(weight.abs().flatten(), current_sparsity)
mask = weight.abs() >= threshold
weight *= mask.float()
# Treina um passo
train_step(model)
return model
Estratégia 2: Pruning Layer-wise
def layer_wise_prune(model, sparsity_per_layer):
"""Sparsidade diferente para camadas diferentes."""
# Camadas iniciais: Menos pruning (mais importantes)
# Camadas finais: Mais pruning (menos críticas)
sparsity_schedule = {
"layer.0": 0.3, # 30% sparsidade
"layer.1": 0.4,
"layer.2": 0.5,
"layer.3": 0.6, # 60% sparsidade
}
for name, module in model.named_modules():
if isinstance(module, torch.nn.Linear):
# Encontra índice da camada
for layer_name, sparsity in sparsity_schedule.items():
if layer_name in name:
# Prune em sparsidade específica da camada
prune_layer(module, sparsity)
break
return model
Estratégia 3: Pruning Iterativo + Fine-tuning
def iterative_prune_finetune(model, target_sparsity=0.5, iterations=5):
"""Prune gradualmente com fine-tuning entre iterações."""
current_sparsity = 0.0
sparsity_increment = target_sparsity / iterations
for i in range(iterations):
# Aumenta sparsidade
current_sparsity += sparsity_increment
# Prune
prune_model(model, sparsity=current_sparsity)
# Fine-tune (recupera acurácia)
fine_tune(model, epochs=2, lr=1e-5)
return model
# Resultados: Melhor acurácia que one-shot em alta sparsidade
Deployment em Produção
Pipeline Completo de Pruning
from transformers import Trainer, TrainingArguments
def production_pruning_pipeline(
model_name="meta-llama/Llama-2-7b-hf",
target_sparsity=0.5,
method="wanda", # ou "sparsegpt"
):
# 1. Carrega modelo
model = AutoModelForCausalLM.from_pretrained(model_name, torch_dtype=torch.float16)
tokenizer = AutoTokenizer.from_pretrained(model_name)
# 2. Carrega dados de calibração
calib_dataset = load_dataset("wikitext", "wikitext-2-raw-v1", split="train[:1000]")
# 3. Aplica pruning
if method == "wanda":
pruned_model = wanda_prune(model, calib_dataset, sparsity=target_sparsity)
elif method == "sparsegpt":
pruner = SparseGPT(model)
pruned_model = pruner.prune(calib_dataset, sparsity=target_sparsity)
# 4. (Opcional) Fine-tune para recuperar acurácia
training_args = TrainingArguments(
output_dir="./pruned-model",
num_train_epochs=1,
per_device_train_batch_size=4,
learning_rate=1e-5,
bf16=True,
)
trainer = Trainer(
model=pruned_model,
args=training_args,
train_dataset=finetune_dataset,
)
trainer.train()
# 5. Salva
pruned_model.save_pretrained("./pruned-llama-7b-50")
tokenizer.save_pretrained("./pruned-llama-7b-50")
return pruned_model
# Uso
pruned_model = production_pruning_pipeline(
model_name="meta-llama/Llama-2-7b-hf",
target_sparsity=0.5,
method="wanda"
)
Avaliação
from lm_eval import evaluator
# Avalia modelo pruned vs original
original_results = evaluator.simple_evaluate(
model="hf",
model_args="pretrained=meta-llama/Llama-2-7b-hf",
tasks=["arc_easy", "hellaswag", "winogrande"],
)
pruned_results = evaluator.simple_evaluate(
model="hf",
model_args="pretrained=./pruned-llama-7b-50",
tasks=["arc_easy", "hellaswag", "winogrande"],
)
# Compara
print(f"Original: {original_results['results']['arc_easy']['acc']:.3f}")
print(f"Pruned: {pruned_results['results']['arc_easy']['acc']:.3f}")
print(f"Degradation: {(original_results - pruned_results):.3f}")
# Resultados típicos em 50% sparsidade:
# - Wanda: <1% perda de acurácia
# - SparseGPT: <0.5% perda de acurácia
# - Magnitude: 2-3% perda de acurácia
Melhores Práticas
1. Seleção de Sparsidade
# Conservador (seguro)
sparsity = 0.3 # 30%, <0.5% perda
# Balanceado (recomendado)
sparsity = 0.5 # 50%, ~1% perda
# Agressivo (arriscado)
sparsity = 0.7 # 70%, 2-5% perda
# Extremo (depende do modelo)
sparsity = 0.9 # 90%, degradação significativa
2. Seleção de Método
# One-shot, sem retreinamento → Wanda ou SparseGPT
if no_retraining_budget:
use_method = "wanda" # Mais rápido
# Melhor qualidade → SparseGPT
if need_best_quality:
use_method = "sparsegpt" # Mais acurado
# Speedup de hardware → N:M estruturado
if need_speedup:
use_method = "nm_prune" # 2:4 ou 4:8
3. Evite Armadilhas Comuns
# ❌ Ruim: Prune sem dados de calibração
prune_random(model) # Sem estatísticas de ativação
# ✅ Bom: Use dados de calibração
prune_wanda(model, calib_data)
# ❌ Ruim: Sparsidade muito alta de uma vez
prune(model, sparsity=0.9) # Perda maciça de acurácia
# ✅ Bom: Gradual ou iterativo
iterative_prune(model, target=0.9, steps=10)
Comparação de Performance
Métodos de pruning em 50% sparsidade (LLaMA-7B):
| Método | Perda de Acurácia | Velocidade | Memória | Retreinamento Necessário |
|---|---|---|---|---|
| Magnitude | -2.5% | 1.0× | -50% | Não |
| Wanda | -0.8% | 1.0× | -50% | Não |
| SparseGPT | -0.4% | 1.0× | -50% | Não |
| N:M (2:4) | -1.0% | 2.0× | -50% | Não |
| Structured | -3.0% | 2.0× | -50% | Não |
Fonte: Paper Wanda (ICLR 2024), Paper SparseGPT
Recursos
- Paper Wanda (ICLR 2024): https://arxiv.org/abs/2306.11695
- GitHub Wanda: https://github.com/locuslab/wanda
- Paper SparseGPT: https://arxiv.org/abs/2301.00774
- GitHub SparseGPT: https://github.com/IST-DASLab/sparsegpt
- NVIDIA Sparse Tensor Cores: https://developer.nvidia.com/blog/accelerating-inference-with-sparsity-using-ampere-and-tensorrt/