PowerMachine commited on
Commit
f28a799
·
verified ·
1 Parent(s): 6816cc6

Delete utils/quantization.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. utils/quantization.py +0 -585
utils/quantization.py DELETED
@@ -1,585 +0,0 @@
1
- """quantization.py — W8A8 Quantization via SmoothQuant para CNN-BiGRU.
2
-
3
- Implementa quantização weight-only-8-bit + activation-8-bit (W8A8) usando
4
- a técnica SmoothQuant (Xiao et al., 2023) para migrar a variância das
5
- ativações para os pesos, reduzindo a perda de precisão.
6
-
7
- ==============================================================================
8
- ANÁLISE MATEMÁTICA E LÓGICA — SmoothQuant + W8A8
9
- ==============================================================================
10
-
11
- PROBLEMA:
12
- Em modelos LLM, as ativações têm outliers em alguns canais que tornam
13
- a quantização INT8 difícil. Se quantizarmos diretamente, os outliers
14
- saturam os outros canais, levando a grandes erros.
15
-
16
- SOLUÇÃO SmoothQuant:
17
- Seja Y = X * W, onde X ∈ R^{B×T×d_in} e W ∈ R^{d_in×d_out}.
18
-
19
- 1. Para cada canal de entrada i, computa o máximo absoluto:
20
- s_i = max|X_i| / max|W_i| (em batch, suavizado por alpha)
21
-
22
- Ou mais precisamente:
23
- s_i = (max|X_i|^alpha) / (max|W_i|^(1-alpha))
24
- com alpha ∈ [0, 1] tipicamente 0.5.
25
-
26
- 2. Migra a variância: dividir X por s e multiplicar W por s:
27
- X' = X / s (ativações suavizadas — outliers reduzidos)
28
- W' = W * s (pesos absorvem a escala)
29
-
30
- Como Y = X * W = (X/s) * (s*W) = X' * W', a operação matricial
31
- é matematicamente equivalente.
32
-
33
- 3. Quantiza ambos para INT8 com escala por tensor ou por canal:
34
- X_q = round(X' / scale_x) * scale_x
35
- W_q = round(W' / scale_w) * scale_w
36
-
37
- 4. Y ≈ X_q * W_q (com erro de quantização reduzido)
38
-
39
- W8A8:
40
- - W: pesos em INT8 (8-bit weights)
41
- - A: ativações em INT8 (8-bit activations)
42
- - Redução de memória: ~4x (FP32 -> INT8)
43
- - Speedup: 2-4x em hardware com suporte INT8 (AMX, AVX512-VNNI)
44
-
45
- INTEGRAÇÃO COM CNN-BiGRU:
46
- - Aplicado após o treino (post-training quantization)
47
- - Aplicável a: Linear (atenção, FFN, classificador), Conv1d
48
- - Não aplicado a: Embedding (mantém FP32 para precisão)
49
- - Em CPU sem AMX, ainda economiza memória (sem speedup significativo)
50
-
51
- ==============================================================================
52
- USO
53
- ==============================================================================
54
-
55
- from cnn_bigru.utils.quantization import (
56
- SmoothQuantizer, W8A8Config, quantize_model_w8a8
57
- )
58
-
59
- # Após treino:
60
- quantizer = SmoothQuantizer(W8A8Config(alpha=0.5, n_calibration_batches=5))
61
- quantized_model = quantizer.quantize(model, calibration_dataloader)
62
-
63
- Autor: CNN-BiGRU Project
64
- """
65
- from __future__ import annotations
66
-
67
- import logging
68
- import math
69
- from dataclasses import dataclass, field
70
- from typing import Dict, List, Optional, Tuple, Union
71
-
72
- import torch
73
- import torch.nn as nn
74
- import torch.nn.functional as F
75
-
76
- logger = logging.getLogger(__name__)
77
-
78
-
79
- # ============================================================================
80
- # Configuração
81
- # ============================================================================
82
-
83
- @dataclass
84
- class W8A8Config:
85
- """Configuração da quantização W8A8 com SmoothQuant."""
86
- # Alpha de suavização (0=só pesos, 1=só ativações, 0.5=balanceado)
87
- alpha: float = 0.5
88
- # Número de batches para calibração
89
- n_calibration_batches: int = 5
90
- # Tipo de escala: "per_tensor" ou "per_channel"
91
- scale_type: str = "per_channel"
92
- # Manter权重 em FP32 para quantização dinâmica (default: False = INT8 estático)
93
- dynamic_weight: bool = False
94
- # Quantizar embeddings (default: False)
95
- quantize_embeddings: bool = False
96
- # Quantizar Conv1d (default: True)
97
- quantize_conv1d: bool = True
98
- # Quantizar LayerNorm (default: False — sensível)
99
- quantize_layernorm: bool = False
100
- # Clip threshold para outliers (em desvios-padrão; None = sem clip)
101
- outlier_clip_std: Optional[float] = 4.0
102
- # Device para calibração
103
- device: str = "cpu"
104
- # Simular quantização (mantém FP32 mas aplica ruído de quantização)
105
- simulate: bool = False
106
-
107
-
108
- # ============================================================================
109
- # SmoothQuant Calibrator
110
- # ============================================================================
111
-
112
- class SmoothQuantCalibrator:
113
- """Coleta estatísticas de ativações e pesos para SmoothQuant.
114
-
115
- Per-corre o modelo com dados de calibração e registra max|X| e max|W|
116
- para cada camada Linear/Conv1d.
117
- """
118
-
119
- def __init__(self, config: W8A8Config):
120
- self.config = config
121
- self.stats: Dict[str, Dict[str, torch.Tensor]] = {}
122
-
123
- def _hook_factory(self, name: str):
124
- """Cria um hook forward para coletar max|X|."""
125
- def hook(module, input, output):
126
- # input é uma tupla; input[0] é o tensor principal
127
- if not isinstance(input, tuple) or len(input) == 0:
128
- return
129
- x = input[0]
130
- if not isinstance(x, torch.Tensor):
131
- return
132
- # Reduzir para [d_in] (assumindo última dim = features)
133
- with torch.no_grad():
134
- if x.dim() >= 2:
135
- # max sobre todas as dims exceto a última
136
- x_flat = x.reshape(-1, x.size(-1))
137
- max_x = x_flat.abs().max(dim=0).values # [d_in]
138
- else:
139
- max_x = x.abs() # [d_in]
140
- # Acumular max (não é média — pegamos o máximo global)
141
- if name not in self.stats:
142
- self.stats[name] = {"max_x": max_x.clone()}
143
- else:
144
- self.stats[name]["max_x"] = torch.maximum(
145
- self.stats[name]["max_x"], max_x
146
- )
147
- return hook
148
-
149
- def calibrate(
150
- self,
151
- model: nn.Module,
152
- dataloader,
153
- n_batches: Optional[int] = None,
154
- ) -> Dict[str, Dict[str, torch.Tensor]]:
155
- """Coleta estatísticas via forward hooks.
156
-
157
- Args:
158
- model: modelo a calibrar
159
- dataloader: iterador de batches
160
- n_batches: número de batches (default: config.n_calibration_batches)
161
-
162
- Returns:
163
- dict {layer_name: {"max_x": [d_in], "max_w": [d_out]}}
164
- """
165
- n_batches = n_batches or self.config.n_calibration_batches
166
- device = self.config.device
167
-
168
- # Registrar hooks em todas as camadas Linear e Conv1d
169
- hooks = []
170
- layer_modules = []
171
- for name, module in model.named_modules():
172
- if isinstance(module, (nn.Linear, nn.Conv1d)):
173
- # Skip embeddings
174
- if "embedding" in name.lower() and not self.config.quantize_embeddings:
175
- continue
176
- if isinstance(module, nn.Conv1d) and not self.config.quantize_conv1d:
177
- continue
178
- hook = module.register_forward_hook(self._hook_factory(name))
179
- hooks.append(hook)
180
- layer_modules.append((name, module))
181
-
182
- # Forward pass em modo eval (sem gradientes)
183
- model.eval()
184
- model.to(device)
185
- was_training = model.training
186
- model.eval()
187
-
188
- try:
189
- with torch.no_grad():
190
- for i, batch in enumerate(dataloader):
191
- if i >= n_batches:
192
- break
193
- try:
194
- # Tentar diferentes formatos de batch
195
- if isinstance(batch, dict):
196
- input_ids_a = batch.get("input_ids_a")
197
- input_ids_b = batch.get("input_ids_b")
198
- images = batch.get("images")
199
- audios = batch.get("audios")
200
- if input_ids_a is not None and input_ids_b is not None:
201
- # Tentar forward do modelo multimodal
202
- try:
203
- model(
204
- input_ids_a.to(device),
205
- input_ids_b.to(device),
206
- images=images.to(device) if images is not None else None,
207
- audios=audios.to(device) if audios is not None else None,
208
- mode="classify",
209
- )
210
- except Exception:
211
- # Fallback: forward sem imagens/áudios
212
- model(input_ids_a.to(device), input_ids_b.to(device))
213
- elif isinstance(batch, (list, tuple)) and len(batch) >= 2:
214
- model(batch[0].to(device), batch[1].to(device))
215
- else:
216
- logger.debug(f"Batch format não reconhecido: {type(batch)}")
217
- except Exception as e:
218
- logger.debug(f"Calibração batch {i} falhou: {e}")
219
- continue
220
- finally:
221
- # Remover hooks
222
- for hook in hooks:
223
- hook.remove()
224
- if was_training:
225
- model.train()
226
-
227
- # Coletar max|W| para cada camada
228
- for name, module in layer_modules:
229
- if name not in self.stats:
230
- continue
231
- w = module.weight.data
232
- if isinstance(module, nn.Linear):
233
- # w: [d_out, d_in] -> max sobre d_out
234
- max_w = w.abs().amax(dim=0) # [d_in]
235
- elif isinstance(module, nn.Conv1d):
236
- # w: [out_channels, in_channels//groups, kernel_size]
237
- # max sobre out_channels e kernel_size
238
- max_w = w.abs().amax(dim=(0, 2)) # [in_channels]
239
- else:
240
- continue
241
- self.stats[name]["max_w"] = max_w.clone()
242
-
243
- logger.info(
244
- "SmoothQuant calibrado: %d camadas, %d batches",
245
- len(self.stats), n_batches,
246
- )
247
- return self.stats
248
-
249
-
250
- # ============================================================================
251
- # SmoothQuantizer
252
- # ============================================================================
253
-
254
- class SmoothQuantizer:
255
- """Aplica quantização W8A8 com SmoothQuant a um modelo.
256
-
257
- Args:
258
- config: configuração W8A8
259
- """
260
-
261
- def __init__(self, config: W8A8Config):
262
- self.config = config
263
- self.calibrator = SmoothQuantCalibrator(config)
264
-
265
- def compute_scales(
266
- self,
267
- stats: Dict[str, Dict[str, torch.Tensor]],
268
- ) -> Dict[str, torch.Tensor]:
269
- """Computa fatores de escala s_i = (max_x^alpha) / (max_w^(1-alpha)).
270
-
271
- Args:
272
- stats: dict {layer_name: {"max_x": [d_in], "max_w": [d_in]}}
273
-
274
- Returns:
275
- dict {layer_name: scale [d_in]}
276
- """
277
- scales = {}
278
- alpha = self.config.alpha
279
- eps = 1e-8
280
-
281
- for name, s in stats.items():
282
- max_x = s["max_x"].float()
283
- max_w = s.get("max_w")
284
- if max_w is None:
285
- # Sem max_w (não calibrado), usa só max_x
286
- scale = torch.ones_like(max_x)
287
- else:
288
- max_w = max_w.float().to(max_x.device)
289
- # s = (max_x^alpha) / (max_w^(1-alpha))
290
- # Adiciona eps para evitar divisão por zero
291
- scale = (max_x.clamp(min=eps).pow(alpha) /
292
- max_w.clamp(min=eps).pow(1 - alpha))
293
- # Clipa outliers se configurado
294
- if self.config.outlier_clip_std is not None:
295
- mean = scale.mean()
296
- std = scale.std()
297
- threshold = self.config.outlier_clip_std * std
298
- scale = scale.clamp(min=mean - threshold, max=mean + threshold)
299
- # Normaliza para média 1 (preserva escala global)
300
- scale = scale * (scale.numel() / scale.sum().clamp(min=eps))
301
- scales[name] = scale
302
-
303
- return scales
304
-
305
- def quantize_tensor_per_channel(
306
- self,
307
- tensor: torch.Tensor,
308
- scale: Optional[torch.Tensor] = None,
309
- n_bits: int = 8,
310
- axis: int = -1,
311
- ) -> torch.Tensor:
312
- """Quantiza um tensor para INT8 (per-channel ou per-tensor).
313
-
314
- Args:
315
- tensor: tensor a quantizar
316
- scale: escala por canal (se None, calcula automaticamente)
317
- Se fornecido, deve ter shape broadcastable com `tensor`.
318
- Para per-row em [d_out, d_in], use scale shape [d_out, 1].
319
- n_bits: número de bits (default 8)
320
- axis: eixo para per-channel (apenas quando scale=None e é 1D)
321
-
322
- Returns:
323
- tensor quantizado (dequantizado para FP32 para uso em forward)
324
- """
325
- qmax = 2 ** (n_bits - 1) - 1 # 127 para INT8 simétrico
326
- qmin = -qmax
327
-
328
- if scale is None:
329
- # Per-tensor
330
- max_abs = tensor.abs().max()
331
- scale = max_abs.clamp(min=1e-8) / qmax
332
- # Quantize-dequantize
333
- q = torch.round(tensor / scale).clamp(qmin, qmax)
334
- return q * scale
335
- else:
336
- # Per-channel: scale deve ser broadcastable com tensor
337
- scale = scale.to(tensor.device)
338
- # Se scale é 1D, expandimos para o eixo
339
- if scale.dim() == 1:
340
- shape = [1] * tensor.dim()
341
- shape[axis] = scale.size(0)
342
- scale_b = scale.view(shape)
343
- else:
344
- # scale já tem shape broadcastable
345
- scale_b = scale
346
- q = torch.round(tensor / scale_b).clamp(qmin, qmax)
347
- return q * scale_b
348
-
349
- def quantize_model(
350
- self,
351
- model: nn.Module,
352
- dataloader=None,
353
- ) -> nn.Module:
354
- """Aplica quantização W8A8 ao modelo.
355
-
356
- Args:
357
- model: modelo a quantizar
358
- dataloader: dados de calibração (necessário para SmoothQuant estático)
359
-
360
- Returns:
361
- modelo quantizado (parâmetros substituídos por versões INT8 simuladas)
362
- """
363
- config = self.config
364
- device = config.device
365
- model.to(device)
366
-
367
- # 1. Calibrar se dataloader fornecido
368
- if dataloader is not None:
369
- stats = self.calibrator.calibrate(model, dataloader)
370
- scales = self.compute_scales(stats)
371
- else:
372
- stats = {}
373
- scales = {}
374
-
375
- # 2. Aplicar SmoothQuant + quantização W8A8 a cada camada
376
- n_quantized = 0
377
- n_skipped = 0
378
- with torch.no_grad():
379
- for name, module in model.named_modules():
380
- if not isinstance(module, (nn.Linear, nn.Conv1d)):
381
- continue
382
- if "embedding" in name.lower() and not config.quantize_embeddings:
383
- n_skipped += 1
384
- continue
385
- if isinstance(module, nn.Conv1d) and not config.quantize_conv1d:
386
- n_skipped += 1
387
- continue
388
-
389
- # Aplicar SmoothQuant: W' = W * s (migrar escala dos pesos)
390
- scale = scales.get(name)
391
- w = module.weight.data
392
- if scale is not None:
393
- # Suavizar pesos: multiplicar pela escala
394
- if isinstance(module, nn.Linear):
395
- # w: [d_out, d_in], scale: [d_in]
396
- w_smoothed = w * scale.to(w.device).unsqueeze(0)
397
- elif isinstance(module, nn.Conv1d):
398
- # w: [out_ch, in_ch//groups, kernel_size], scale: [in_ch]
399
- w_smoothed = w * scale.to(w.device).view(1, -1, 1)
400
- else:
401
- w_smoothed = w
402
- else:
403
- w_smoothed = w
404
-
405
- # Quantizar pesos para INT8 (simulado — dequantiza de volta)
406
- if config.scale_type == "per_channel":
407
- if isinstance(module, nn.Linear):
408
- # Per-output-channel para pesos
409
- w_scale = w_smoothed.abs().amax(dim=1) / 127.0
410
- w_scale = w_scale.clamp(min=1e-8)
411
- w_q = self.quantize_tensor_per_channel(
412
- w_smoothed, scale=w_scale.unsqueeze(1), axis=1,
413
- )
414
- elif isinstance(module, nn.Conv1d):
415
- w_scale = w_smoothed.abs().amax(dim=(1, 2)) / 127.0
416
- w_scale = w_scale.clamp(min=1e-8)
417
- w_q = self.quantize_tensor_per_channel(
418
- w_smoothed, scale=w_scale.view(-1, 1, 1), axis=0,
419
- )
420
- else:
421
- # Per-tensor
422
- w_q = self.quantize_tensor_per_channel(w_smoothed)
423
-
424
- module.weight.data = w_q.to(module.weight.dtype)
425
- n_quantized += 1
426
-
427
- logger.info(
428
- "SmoothQuant W8A8 aplicado: %d camadas quantizadas, %d ignoradas",
429
- n_quantized, n_skipped,
430
- )
431
-
432
- # Marcar modelo como quantizado (para uso futuro)
433
- model._is_quantized_w8a8 = True # type: ignore
434
- model._quantization_config = config # type: ignore
435
-
436
- return model
437
-
438
- @staticmethod
439
- def is_quantized(model: nn.Module) -> bool:
440
- """Verifica se um modelo foi quantizado."""
441
- return getattr(model, "_is_quantized_w8a8", False)
442
-
443
-
444
- # ============================================================================
445
- # Helper: quantize modelo inteiro
446
- # ============================================================================
447
-
448
- def quantize_model_w8a8(
449
- model: nn.Module,
450
- dataloader=None,
451
- alpha: float = 0.5,
452
- n_calibration_batches: int = 5,
453
- device: str = "cpu",
454
- **kwargs,
455
- ) -> nn.Module:
456
- """Atalho para quantizar um modelo W8A8.
457
-
458
- Args:
459
- model: modelo a quantizar
460
- dataloader: dados de calibração (None para quantização dinâmica)
461
- alpha: fator SmoothQuant (0.5 default)
462
- n_calibration_batches: batches para calibração
463
- device: device para calibração
464
- **kwargs: outros parâmetros de W8A8Config
465
-
466
- Returns:
467
- modelo quantizado
468
- """
469
- config = W8A8Config(
470
- alpha=alpha,
471
- n_calibration_batches=n_calibration_batches,
472
- device=device,
473
- **kwargs,
474
- )
475
- quantizer = SmoothQuantizer(config)
476
- return quantizer.quantize_model(model, dataloader=dataloader)
477
-
478
-
479
- # ============================================================================
480
- # Helper: estimar redução de memória
481
- # ============================================================================
482
-
483
- def estimate_memory_savings(model: nn.Module) -> Dict[str, float]:
484
- """Estima redução de memória após quantização W8A8.
485
-
486
- Como a quantização é SIMULADA (dequantiza de volta para FP32 para uso em
487
- forward), a memória real não muda. Esta função reporta a redução POTENCIAL
488
- se os pesos fossem armazenados como INT8 real.
489
-
490
- Args:
491
- model: modelo (preferencialmente quantizado)
492
-
493
- Returns:
494
- dict com tamanhos em MB
495
- """
496
- is_quantized = getattr(model, "_is_quantized_w8a8", False)
497
- fp32_bytes = 0 # embeddings + biases (sempre FP32)
498
- quantizable_bytes = 0 # pesos que seriam INT8
499
-
500
- for name, param in model.named_parameters():
501
- n = param.numel()
502
- if "embedding" in name.lower():
503
- # Embeddings mantêm FP32
504
- fp32_bytes += n * 4
505
- elif "bias" in name.lower():
506
- # Biases mantêm FP32 (típico em W8A8)
507
- fp32_bytes += n * 4
508
- elif param.dim() >= 2:
509
- # Pesos de matrizes (Linear, Conv) — quantizáveis
510
- quantizable_bytes += n
511
- else:
512
- # Outros 1D — mantêm FP32
513
- fp32_bytes += n * 4
514
-
515
- # Se quantizado: pesos seriam INT8 (1 byte cada)
516
- # Se não quantizado: pesos seriam FP32 (4 bytes cada)
517
- if is_quantized:
518
- int8_bytes = quantizable_bytes * 1 # INT8
519
- else:
520
- int8_bytes = quantizable_bytes * 4 # FP32
521
-
522
- fp32_mb = fp32_bytes / (1024 ** 2)
523
- int8_mb = int8_bytes / (1024 ** 2)
524
- total_mb = fp32_mb + int8_mb
525
- # Sem quantização, tudo seria FP32
526
- no_quant_mb = (fp32_bytes + quantizable_bytes * 4) / (1024 ** 2)
527
- reduction = (1 - total_mb / no_quant_mb) * 100 if no_quant_mb > 0 else 0
528
-
529
- return {
530
- "fp32_mb": fp32_mb,
531
- "int8_mb": int8_mb,
532
- "total_mb": total_mb,
533
- "no_quant_mb": no_quant_mb,
534
- "reduction_pct": reduction,
535
- "is_quantized": is_quantized,
536
- }
537
-
538
-
539
- # ============================================================================
540
- # Self-test
541
- # ============================================================================
542
-
543
- def _self_test():
544
- """Teste rápido da quantização W8A8."""
545
- torch.manual_seed(42)
546
-
547
- # Modelo simples para teste
548
- class TinyModel(nn.Module):
549
- def __init__(self):
550
- super().__init__()
551
- self.fc1 = nn.Linear(32, 64)
552
- self.fc2 = nn.Linear(64, 32)
553
- self.embedding = nn.Embedding(100, 32)
554
-
555
- def forward(self, x):
556
- return self.fc2(torch.relu(self.fc1(x)))
557
-
558
- model = TinyModel()
559
- print(f"Antes: {sum(p.numel() for p in model.parameters())} params")
560
-
561
- # Quantizar sem dataloader (dinâmico)
562
- quantized = quantize_model_w8a8(model, dataloader=None, alpha=0.5)
563
- print(f"Quantizado: {SmoothQuantizer.is_quantized(quantized)}")
564
-
565
- # Verificar forward ainda funciona
566
- x = torch.randn(2, 32)
567
- out = quantized(x)
568
- print(f"Output shape: {out.shape}")
569
-
570
- # Estimar savings
571
- savings = estimate_memory_savings(quantized)
572
- print(f"Memory savings: {savings}")
573
-
574
-
575
- if __name__ == "__main__":
576
- _self_test()
577
-
578
-
579
- __all__ = [
580
- "W8A8Config",
581
- "SmoothQuantCalibrator",
582
- "SmoothQuantizer",
583
- "quantize_model_w8a8",
584
- "estimate_memory_savings",
585
- ]