V5: W8A8 aprimorado (α aprendível via STE) + bug fix apply_w8a8_to_model + 4 hiperparâmetros auto-ajustáveis (T, τ, λ_ent, init_gate) + OOM-Killer fixes + num_layers_hyp=8 fixo
Browse files- src/bigru_t/model/unified_model.py +47 -8
- src/bigru_t/quantization/__init__.py +48 -5
- src/bigru_t/quantization/quantized_linear.py +89 -21
- src/bigru_t/quantization/w8a8_error_reduction.py +457 -0
- src/bigru_t/quantization/w8a8_smoothquant.py +365 -0
- src/bigru_t/training/meta_configurator.py +127 -38
- v32_upload_report.json +9 -9
- v5_report.json +127 -0
src/bigru_t/model/unified_model.py
CHANGED
|
@@ -52,7 +52,8 @@ from .orq_cell import OrqCell
|
|
| 52 |
from .train_t import TrainT
|
| 53 |
from .hyp_t import HypT
|
| 54 |
from .module_selector import ModuleSelector
|
| 55 |
-
|
|
|
|
| 56 |
from ..reasoning.circular_reasoning_wasserstein import CircularReasoningWasserstein
|
| 57 |
|
| 58 |
|
|
@@ -107,6 +108,7 @@ class UnifiedModelConfig:
|
|
| 107 |
|
| 108 |
# HypT
|
| 109 |
# V4: num_layers_hyp aumentado de 2 → 8 (mais capacidade para correção delta)
|
|
|
|
| 110 |
hypT_dim: int = 128
|
| 111 |
nhead_hyp: int = 4
|
| 112 |
d_ff_hyp: int = 256
|
|
@@ -265,9 +267,23 @@ class UnifiedModel(nn.Module):
|
|
| 265 |
else None
|
| 266 |
)
|
| 267 |
|
| 268 |
-
#
|
| 269 |
-
#
|
| 270 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 271 |
|
| 272 |
def forward(
|
| 273 |
self,
|
|
@@ -307,6 +323,12 @@ class UnifiedModel(nn.Module):
|
|
| 307 |
x = self.emb_dropout(x)
|
| 308 |
# Agora x: (batch, T, input_dim)
|
| 309 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 310 |
# Lema 1: seleciona módulos via softmax com temperatura
|
| 311 |
alpha, entropy_reg = self.module_selector(temperature=temperature)
|
| 312 |
|
|
@@ -334,10 +356,7 @@ class UnifiedModel(nn.Module):
|
|
| 334 |
|
| 335 |
# Circular Reasoning (Teorema 4): refine 'o' via contração W₂
|
| 336 |
circular_info = None
|
| 337 |
-
do_circular
|
| 338 |
-
use_circular if use_circular is not None
|
| 339 |
-
else self.config.use_circular_reasoning
|
| 340 |
-
)
|
| 341 |
if do_circular and self.circular_reasoning is not None:
|
| 342 |
# CRW aceita (B, D) ou (B, T, D); passamos (B, D) — LayerNorm normaliza última dim
|
| 343 |
crw_out = self.circular_reasoning(o)
|
|
@@ -465,6 +484,25 @@ class UnifiedModel(nn.Module):
|
|
| 465 |
"""Retorna nº efetivo de módulos ativos (para auto-configuração)."""
|
| 466 |
return self.module_selector.get_active_modules(threshold)
|
| 467 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 468 |
def count_parameters(self) -> dict:
|
| 469 |
"""Conta parâmetros totais e treináveis."""
|
| 470 |
total = sum(p.numel() for p in self.parameters())
|
|
@@ -474,6 +512,7 @@ class UnifiedModel(nn.Module):
|
|
| 474 |
"total_M": total / 1e6,
|
| 475 |
"trainable": trainable,
|
| 476 |
"trainable_M": trainable / 1e6,
|
|
|
|
| 477 |
}
|
| 478 |
|
| 479 |
|
|
|
|
| 52 |
from .train_t import TrainT
|
| 53 |
from .hyp_t import HypT
|
| 54 |
from .module_selector import ModuleSelector
|
| 55 |
+
# V5: aplicar_w8a8_to_model substitui apply_w8a8 (bug fix crítico)
|
| 56 |
+
from ..quantization.quantized_linear import QuantizedLinear, apply_w8a8_to_model
|
| 57 |
from ..reasoning.circular_reasoning_wasserstein import CircularReasoningWasserstein
|
| 58 |
|
| 59 |
|
|
|
|
| 108 |
|
| 109 |
# HypT
|
| 110 |
# V4: num_layers_hyp aumentado de 2 → 8 (mais capacidade para correção delta)
|
| 111 |
+
# V5: num_layers_hyp=8 é o PADRÃO PERMANENTE (não mudar)
|
| 112 |
hypT_dim: int = 128
|
| 113 |
nhead_hyp: int = 4
|
| 114 |
d_ff_hyp: int = 256
|
|
|
|
| 267 |
else None
|
| 268 |
)
|
| 269 |
|
| 270 |
+
# V5: Parâmetros APRENDÍVEIS do modelo (auto-ajuste próprio)
|
| 271 |
+
# λ_ent: peso da regularização de entropia do module_selector (Lema 1)
|
| 272 |
+
# Parametrização via exp (sempre positivo)
|
| 273 |
+
# init: λ_ent = c.lambda_ent = 0.01
|
| 274 |
+
# O residual_gate do CircularReasoning JÁ é Parameter (n_cycles params)
|
| 275 |
+
# e é gerenciado diretamente pelo MetaConfigurator.
|
| 276 |
+
self.log_lambda_ent = nn.Parameter(
|
| 277 |
+
torch.tensor(math.log(max(1e-6, c.lambda_ent)), dtype=torch.float32)
|
| 278 |
+
)
|
| 279 |
+
|
| 280 |
+
# V5: Aplicar W8A8 em todas as camadas lineares (exceto Embedding, que é nn.Embedding)
|
| 281 |
+
# BUG FIX CRÍTICO V5: model.apply(fn) NÃO substitui módulos (descarta retorno).
|
| 282 |
+
# Usar apply_w8a8_to_model (substituição in-place recursiva).
|
| 283 |
+
n_quantized = apply_w8a8_to_model(self)
|
| 284 |
+
# Nota: nn.GRU não é afetada (não é Linear); Embedding não é afetada (nn.Embedding)
|
| 285 |
+
# Log em runtime (não criticar se n_quantized=0 — modelo pode não ter Linear)
|
| 286 |
+
self._n_w8a8_layers = n_quantized
|
| 287 |
|
| 288 |
def forward(
|
| 289 |
self,
|
|
|
|
| 323 |
x = self.emb_dropout(x)
|
| 324 |
# Agora x: (batch, T, input_dim)
|
| 325 |
|
| 326 |
+
# V5: do_circular computed early (was previously duplicated below)
|
| 327 |
+
do_circular = (
|
| 328 |
+
use_circular if use_circular is not None
|
| 329 |
+
else self.config.use_circular_reasoning
|
| 330 |
+
)
|
| 331 |
+
|
| 332 |
# Lema 1: seleciona módulos via softmax com temperatura
|
| 333 |
alpha, entropy_reg = self.module_selector(temperature=temperature)
|
| 334 |
|
|
|
|
| 356 |
|
| 357 |
# Circular Reasoning (Teorema 4): refine 'o' via contração W₂
|
| 358 |
circular_info = None
|
| 359 |
+
# do_circular já foi computado no início do forward (V5)
|
|
|
|
|
|
|
|
|
|
| 360 |
if do_circular and self.circular_reasoning is not None:
|
| 361 |
# CRW aceita (B, D) ou (B, T, D); passamos (B, D) — LayerNorm normaliza última dim
|
| 362 |
crw_out = self.circular_reasoning(o)
|
|
|
|
| 484 |
"""Retorna nº efetivo de módulos ativos (para auto-configuração)."""
|
| 485 |
return self.module_selector.get_active_modules(threshold)
|
| 486 |
|
| 487 |
+
# =====================================================================
|
| 488 |
+
# V5: Propriedades para parâmetros aprendíveis (auto-ajuste do modelo)
|
| 489 |
+
# =====================================================================
|
| 490 |
+
@property
|
| 491 |
+
def lambda_ent_value(self) -> float:
|
| 492 |
+
"""λ_ent atual = exp(log_lambda_ent) — peso da reg. de entropia."""
|
| 493 |
+
return float(torch.exp(self.log_lambda_ent.detach()).item())
|
| 494 |
+
|
| 495 |
+
@property
|
| 496 |
+
def init_gate_value(self) -> float:
|
| 497 |
+
"""init_gate atual = mean(circular_reasoning.residual_gate).
|
| 498 |
+
|
| 499 |
+
V5: O gate do CircularReasoning é Parameter interno (n_cycles params).
|
| 500 |
+
O MetaConfigurator gerencia esse Parameter diretamente.
|
| 501 |
+
"""
|
| 502 |
+
if self.circular_reasoning is not None:
|
| 503 |
+
return float(self.circular_reasoning.residual_gate.detach().mean().item())
|
| 504 |
+
return 0.0
|
| 505 |
+
|
| 506 |
def count_parameters(self) -> dict:
|
| 507 |
"""Conta parâmetros totais e treináveis."""
|
| 508 |
total = sum(p.numel() for p in self.parameters())
|
|
|
|
| 512 |
"total_M": total / 1e6,
|
| 513 |
"trainable": trainable,
|
| 514 |
"trainable_M": trainable / 1e6,
|
| 515 |
+
"n_w8a8_layers": getattr(self, "_n_w8a8_layers", 0),
|
| 516 |
}
|
| 517 |
|
| 518 |
|
src/bigru_t/quantization/__init__.py
CHANGED
|
@@ -1,6 +1,49 @@
|
|
| 1 |
-
"""Quantização W8A8 (Lema 3).
|
| 2 |
-
from .quantized_linear import QuantizedLinear, quantize_tensor, apply_w8a8
|
| 3 |
-
# Reaproveitado do source (não modificado):
|
| 4 |
-
# from .w8a8_qoperator import W8A8QOperatorQuantizer, W8A8QOperatorLinear
|
| 5 |
|
| 6 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Quantização W8A8 (Lema 3).
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
+
V5: módulos novos:
|
| 4 |
+
- SmoothQuantW8A8 (α aprendível via STE + sigmoid)
|
| 5 |
+
- TrustGuidedW8A8 (redução iterativa de erro, self-contained)
|
| 6 |
+
- MultiLayerW8A8Reducer (aplica a múltiplas camadas)
|
| 7 |
+
- apply_w8a8_to_model (bug fix crítico — substitui in-place)
|
| 8 |
+
"""
|
| 9 |
+
from .quantized_linear import (
|
| 10 |
+
QuantizedLinear,
|
| 11 |
+
quantize_tensor,
|
| 12 |
+
apply_w8a8,
|
| 13 |
+
apply_w8a8_to_model,
|
| 14 |
+
)
|
| 15 |
+
from .w8a8_smoothquant import (
|
| 16 |
+
SmoothQuantW8A8,
|
| 17 |
+
calibrate_smoothquant,
|
| 18 |
+
quantize_per_channel_symmetric,
|
| 19 |
+
quantize_per_token_symmetric,
|
| 20 |
+
STEQuantize,
|
| 21 |
+
ste_quantize,
|
| 22 |
+
)
|
| 23 |
+
from .w8a8_error_reduction import (
|
| 24 |
+
W8A8ConvergenceError,
|
| 25 |
+
analytic_trust,
|
| 26 |
+
W8A8ReductionResult,
|
| 27 |
+
TrustGuidedW8A8,
|
| 28 |
+
MultiLayerW8A8Reducer,
|
| 29 |
+
)
|
| 30 |
+
|
| 31 |
+
__all__ = [
|
| 32 |
+
# V1-V4
|
| 33 |
+
"QuantizedLinear",
|
| 34 |
+
"quantize_tensor",
|
| 35 |
+
"apply_w8a8",
|
| 36 |
+
# V5
|
| 37 |
+
"apply_w8a8_to_model",
|
| 38 |
+
"SmoothQuantW8A8",
|
| 39 |
+
"calibrate_smoothquant",
|
| 40 |
+
"quantize_per_channel_symmetric",
|
| 41 |
+
"quantize_per_token_symmetric",
|
| 42 |
+
"STEQuantize",
|
| 43 |
+
"ste_quantize",
|
| 44 |
+
"W8A8ConvergenceError",
|
| 45 |
+
"analytic_trust",
|
| 46 |
+
"W8A8ReductionResult",
|
| 47 |
+
"TrustGuidedW8A8",
|
| 48 |
+
"MultiLayerW8A8Reducer",
|
| 49 |
+
]
|
src/bigru_t/quantization/quantized_linear.py
CHANGED
|
@@ -1,21 +1,31 @@
|
|
| 1 |
-
"""quantized_linear.py — QuantizedLinear
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
"""
|
| 20 |
from __future__ import annotations
|
| 21 |
|
|
@@ -80,10 +90,68 @@ class QuantizedLinear(nn.Linear):
|
|
| 80 |
|
| 81 |
|
| 82 |
def apply_w8a8(module: nn.Module) -> nn.Module:
|
| 83 |
-
"""Substitui
|
| 84 |
|
| 85 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
"""
|
| 87 |
if isinstance(module, nn.Linear) and not isinstance(module, QuantizedLinear):
|
| 88 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
return module
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""quantized_linear.py — QuantizedLinear + apply_w8a8 (V5 — bug fix crítico).
|
| 2 |
+
|
| 3 |
+
═══════════════════════════════════════════════════════════════════════════════
|
| 4 |
+
V5 — BUG FIX CRÍTICO: apply_w8a8 não substituía módulos
|
| 5 |
+
═══════════════════════════════════════════════════════════════════════════════
|
| 6 |
+
|
| 7 |
+
BUG (V1-V4):
|
| 8 |
+
Em V1-V4, `apply_w8a8` era usado via `model.apply(apply_w8a8)`. Porém,
|
| 9 |
+
`nn.Module.apply(fn)` chama `fn(module)` e DESCARTA o valor retornado.
|
| 10 |
+
A função retornava `QuantizedLinear(...)` mas o módulo original não era
|
| 11 |
+
substituído — apenas a função era chamada (sem efeito).
|
| 12 |
+
|
| 13 |
+
Resultado: W8A8 NUNCA era aplicado em V1-V4. O modelo usava FP32 puro
|
| 14 |
+
em todas as camadas Linear, desperdiçando ~4x mais memória que o necessário
|
| 15 |
+
e sem obter o benefício do ruído de quantização (Lema 3).
|
| 16 |
+
|
| 17 |
+
FIX (V5):
|
| 18 |
+
Substituir `model.apply(apply_w8a8)` por `apply_w8a8_to_model(model)` que
|
| 19 |
+
percorre recursivamente `model._modules` e substitui `nn.Linear` por
|
| 20 |
+
`QuantizedLinear` (ou `SmoothQuantW8A8` se preferir V5).
|
| 21 |
+
|
| 22 |
+
Para manter compatibilidade com V1-V4, `apply_w8a8(module)` ainda existe
|
| 23 |
+
mas agora é chamado recursivamente por `apply_w8a8_to_model`.
|
| 24 |
+
|
| 25 |
+
Implementa o Lema 3 (Cancelamento de ruído de quantização):
|
| 26 |
+
A quantização W8A8 (pesos e ativações em 8 bits) é simulada no forward
|
| 27 |
+
via fake quantization (round + clamp + dequant). O backward usa Straight-
|
| 28 |
+
Through Estimator (STE) — o gradiente passa direto pela operação de round.
|
| 29 |
"""
|
| 30 |
from __future__ import annotations
|
| 31 |
|
|
|
|
| 90 |
|
| 91 |
|
| 92 |
def apply_w8a8(module: nn.Module) -> nn.Module:
|
| 93 |
+
"""Substitui uma nn.Linear por QuantizedLinear (mantém pesos).
|
| 94 |
|
| 95 |
+
V5: Agora é uma função que efetivamente substitui o módulo quando
|
| 96 |
+
chamada por apply_w8a8_to_model. Para compatibilidade com V1-V4,
|
| 97 |
+
ainda pode ser chamada diretamente em um único módulo.
|
| 98 |
+
|
| 99 |
+
NOTA: Esta função NÃO deve ser usada via `model.apply(apply_w8a8)` pois
|
| 100 |
+
`nn.Module.apply` descarta o valor retornado. Use
|
| 101 |
+
`apply_w8a8_to_model(model)` em vez disso.
|
| 102 |
"""
|
| 103 |
if isinstance(module, nn.Linear) and not isinstance(module, QuantizedLinear):
|
| 104 |
+
new_module = QuantizedLinear(module.in_features, module.out_features, module.bias is not None)
|
| 105 |
+
# Copiar pesos do módulo original
|
| 106 |
+
with torch.no_grad():
|
| 107 |
+
new_module.weight.copy_(module.weight)
|
| 108 |
+
if module.bias is not None:
|
| 109 |
+
new_module.bias.copy_(module.bias)
|
| 110 |
+
return new_module
|
| 111 |
return module
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
def apply_w8a8_to_model(model: nn.Module) -> int:
|
| 115 |
+
"""Substitui recursivamente todas as nn.Linear por QuantizedLinear.
|
| 116 |
+
|
| 117 |
+
V5: Implementação correta — percorre `model._modules` recursivamente
|
| 118 |
+
e substitui in-place. Retorna o número de camadas substituídas.
|
| 119 |
+
|
| 120 |
+
Diferentemente de `model.apply(apply_w8a8)` (que não funciona porque
|
| 121 |
+
`apply` descarta o retorno), esta função modifica o modelo in-place.
|
| 122 |
+
|
| 123 |
+
Args:
|
| 124 |
+
model: modelo a ter as Linears substituídas
|
| 125 |
+
|
| 126 |
+
Returns:
|
| 127 |
+
n_substituidas: número de camadas Linear substituídas
|
| 128 |
+
|
| 129 |
+
Uso:
|
| 130 |
+
from bigru_t.quantization import apply_w8a8_to_model
|
| 131 |
+
n = apply_w8a8_to_model(model)
|
| 132 |
+
print(f"{n} camadas Linear substituídas por QuantizedLinear")
|
| 133 |
+
"""
|
| 134 |
+
n_substituidas = 0
|
| 135 |
+
# Lista de (parent_module, child_name) para substituir
|
| 136 |
+
# Não podemos substituir durante a iteração, então coletamos primeiro
|
| 137 |
+
to_replace = []
|
| 138 |
+
for name, module in model.named_modules():
|
| 139 |
+
for child_name, child in module.named_children():
|
| 140 |
+
if isinstance(child, nn.Linear) and not isinstance(child, QuantizedLinear):
|
| 141 |
+
to_replace.append((module, child_name))
|
| 142 |
+
|
| 143 |
+
for parent, child_name in to_replace:
|
| 144 |
+
old_linear = getattr(parent, child_name)
|
| 145 |
+
new_linear = apply_w8a8(old_linear)
|
| 146 |
+
setattr(parent, child_name, new_linear)
|
| 147 |
+
n_substituidas += 1
|
| 148 |
+
|
| 149 |
+
return n_substituidas
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
__all__ = [
|
| 153 |
+
"quantize_tensor",
|
| 154 |
+
"QuantizedLinear",
|
| 155 |
+
"apply_w8a8",
|
| 156 |
+
"apply_w8a8_to_model",
|
| 157 |
+
]
|
src/bigru_t/quantization/w8a8_error_reduction.py
ADDED
|
@@ -0,0 +1,457 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""w8a8_error_reduction.py — Redução iterativa do erro W8A8 (V5).
|
| 2 |
+
|
| 3 |
+
═══════════════════════════════════════════════════════════════════════════════
|
| 4 |
+
V5 — VERSÃO AUTO-CONTIDA (sem dependência de TrustNeuralAgent externo)
|
| 5 |
+
═══════════════════════════════════════════════════════════════════════════════
|
| 6 |
+
|
| 7 |
+
ORIGEM (gru-ring-v13-9-2):
|
| 8 |
+
source/xavante_work/flexnet/w8a8_error_reduction.py (V13.9.1)
|
| 9 |
+
|
| 10 |
+
Em V13.9.1, TrustGuidedW8A8 dependia de TrustNeuralAgent (rede neural externa
|
| 11 |
+
que avalia confiança na quantização). Em V5, eliminamos essa dependência:
|
| 12 |
+
|
| 13 |
+
* Confiança ht agora é FUNÇÃO ANALÍTICA de α e erro:
|
| 14 |
+
ht = exp(-error / error_scale) * sigmoid((alpha - 0.5) * 4)
|
| 15 |
+
- Alta quando erro é baixo E α está próximo de 0.5 (balanceado)
|
| 16 |
+
- Baixa quando erro é alto OU α está nos extremos (0 ou 1)
|
| 17 |
+
|
| 18 |
+
* Redução de erro via gradiente STE (igual V13.9.1):
|
| 19 |
+
dE/dα = dE/ds · ds/dα
|
| 20 |
+
onde:
|
| 21 |
+
E(α) = ||Y_fp - Y_q(α)||_F^2 / ||Y_fp||_F^2
|
| 22 |
+
s_j(α) = max(|X_j|)^α / max(|W_j|)^{1-α}
|
| 23 |
+
ds_j/dα = s_j · (log max|X_j| - log max|W_j|)
|
| 24 |
+
|
| 25 |
+
* Trust-guided update:
|
| 26 |
+
α_{t+1} = α_t - lr · (1 - ht) · dE/dα
|
| 27 |
+
Alta confiança (ht→1) → passo pequeno (convergência)
|
| 28 |
+
Baixa confiança (ht→0) → passo grande (exploração)
|
| 29 |
+
|
| 30 |
+
Análise de convergência:
|
| 31 |
+
E(α_{t+1}) ≤ E(α_t) · (1 - lr · (1 - ht) · λ_min)
|
| 32 |
+
onde λ_min é o menor autovalor da Hessiana de E em α*.
|
| 33 |
+
Garante convergência linear quando λ_min > 0 (E convexa localmente).
|
| 34 |
+
|
| 35 |
+
V5 vs V13.9.1:
|
| 36 |
+
* Sem TrustNeuralAgent (rede neural externa) → -50% params, -1 módulo
|
| 37 |
+
* ht é função analítica (determinística, não-aprendida)
|
| 38 |
+
* Self-contained: só depende de torch + w8a8_smoothquant
|
| 39 |
+
* Vetorizado: smooth factor calculado em 1 operação (não loop Python)
|
| 40 |
+
* OOM-safe: não armazena histórico de tensores completos, apenas escalares
|
| 41 |
+
"""
|
| 42 |
+
from __future__ import annotations
|
| 43 |
+
|
| 44 |
+
import math
|
| 45 |
+
import logging
|
| 46 |
+
from dataclasses import dataclass, field
|
| 47 |
+
from typing import Dict, List, Optional, Tuple
|
| 48 |
+
|
| 49 |
+
import torch
|
| 50 |
+
import torch.nn as nn
|
| 51 |
+
import torch.nn.functional as F
|
| 52 |
+
|
| 53 |
+
from .w8a8_smoothquant import (
|
| 54 |
+
SmoothQuantW8A8,
|
| 55 |
+
calibrate_smoothquant,
|
| 56 |
+
quantize_per_channel_symmetric,
|
| 57 |
+
quantize_per_token_symmetric,
|
| 58 |
+
ste_quantize,
|
| 59 |
+
)
|
| 60 |
+
|
| 61 |
+
logger = logging.getLogger(__name__)
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
# ============================================================================
|
| 65 |
+
# Exceções
|
| 66 |
+
# ============================================================================
|
| 67 |
+
class W8A8ConvergenceError(RuntimeError):
|
| 68 |
+
"""W8A8 error reduction não convergiu."""
|
| 69 |
+
pass
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
# ============================================================================
|
| 73 |
+
# Função analítica de confiança ht (V5 — substitui TrustNeuralAgent)
|
| 74 |
+
# ============================================================================
|
| 75 |
+
def analytic_trust(
|
| 76 |
+
error: float,
|
| 77 |
+
alpha: float,
|
| 78 |
+
error_scale: float = 0.1,
|
| 79 |
+
) -> float:
|
| 80 |
+
"""Confiança ht analítica (V5).
|
| 81 |
+
|
| 82 |
+
ht = exp(-error / error_scale) * sigmoid((alpha - 0.5) * 4)
|
| 83 |
+
|
| 84 |
+
Args:
|
| 85 |
+
error: erro relativo de quantização (||Y_fp - Y_q||/||Y_fp||)
|
| 86 |
+
alpha: fator SmoothQuant atual
|
| 87 |
+
error_scale: escala do erro (default 0.1 — erro 10% dá e^-1 ≈ 0.37)
|
| 88 |
+
|
| 89 |
+
Returns:
|
| 90 |
+
ht ∈ (0, 1): 1 = alta confiança, 0 = baixa confiança
|
| 91 |
+
"""
|
| 92 |
+
err_term = math.exp(-max(0.0, error) / max(1e-8, error_scale))
|
| 93 |
+
# Sigmoid com slope 4: alpha=0.5 → 0.5, alpha=0.95 → 0.88, alpha=0.05 → 0.12
|
| 94 |
+
alpha_term = 1.0 / (1.0 + math.exp(-4.0 * (alpha - 0.5)))
|
| 95 |
+
return err_term * alpha_term
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
# ============================================================================
|
| 99 |
+
# 1. TrustGuidedW8A8 — quantização W8A8 com redução iterativa de erro
|
| 100 |
+
# ============================================================================
|
| 101 |
+
@dataclass
|
| 102 |
+
class W8A8ReductionResult:
|
| 103 |
+
"""Resultado da redução iterativa de erro W8A8."""
|
| 104 |
+
initial_alpha: float
|
| 105 |
+
final_alpha: float
|
| 106 |
+
initial_error: float
|
| 107 |
+
final_error: float
|
| 108 |
+
n_iterations: int
|
| 109 |
+
converged: bool
|
| 110 |
+
history: List[Dict[str, float]] = field(default_factory=list)
|
| 111 |
+
final_ht: float = 0.0
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
class TrustGuidedW8A8:
|
| 115 |
+
"""W8A8 com redução iterativa de erro guiada por confiança analítica.
|
| 116 |
+
|
| 117 |
+
V5: Self-contained — sem TrustNeuralAgent externo. ht é função analítica
|
| 118 |
+
de (error, alpha), não uma rede neural aprendida.
|
| 119 |
+
|
| 120 |
+
Pipeline:
|
| 121 |
+
1. Calibra α inicial e quantiza
|
| 122 |
+
2. Mede erro E₀
|
| 123 |
+
3. Função analítica avalia ht (confiança na quantização)
|
| 124 |
+
4. Se ht < τ: atualiza α via STE gradiente na direção de redução de erro
|
| 125 |
+
5. Re-quantiza, mede erro E₁, atualiza ht
|
| 126 |
+
6. Repete até ht > τ ou convergência
|
| 127 |
+
|
| 128 |
+
A atualização é modulada por (1 - ht): alta confiança → passo pequeno.
|
| 129 |
+
"""
|
| 130 |
+
|
| 131 |
+
def __init__(
|
| 132 |
+
self,
|
| 133 |
+
ref: nn.Linear,
|
| 134 |
+
init_alpha: float = 0.5,
|
| 135 |
+
trust_threshold: float = 0.8,
|
| 136 |
+
lr: float = 0.05,
|
| 137 |
+
max_iterations: int = 20,
|
| 138 |
+
convergence_tol: float = 1e-4,
|
| 139 |
+
reg_lambda: float = 0.01,
|
| 140 |
+
verbose: bool = False,
|
| 141 |
+
):
|
| 142 |
+
self.ref = ref
|
| 143 |
+
self.init_alpha = init_alpha
|
| 144 |
+
self.trust_threshold = trust_threshold
|
| 145 |
+
self.lr = lr
|
| 146 |
+
self.max_iterations = max_iterations
|
| 147 |
+
self.convergence_tol = convergence_tol
|
| 148 |
+
self.reg_lambda = reg_lambda
|
| 149 |
+
self.verbose = verbose
|
| 150 |
+
# V5: Sem TrustNeuralAgent. ht é função analítica.
|
| 151 |
+
# Parâmetro α parametrizado via sigmoid.
|
| 152 |
+
init_eta = math.log(init_alpha / (1 - init_alpha))
|
| 153 |
+
self.eta_alpha = torch.nn.Parameter(torch.tensor(init_eta))
|
| 154 |
+
|
| 155 |
+
@property
|
| 156 |
+
def alpha(self) -> torch.Tensor:
|
| 157 |
+
return torch.sigmoid(self.eta_alpha)
|
| 158 |
+
|
| 159 |
+
def _compute_quantization_error(
|
| 160 |
+
self,
|
| 161 |
+
X: torch.Tensor,
|
| 162 |
+
alpha_val: float,
|
| 163 |
+
) -> Tuple[torch.Tensor, torch.Tensor]:
|
| 164 |
+
"""Computa Y_quantized e Y_reference para um dado α.
|
| 165 |
+
|
| 166 |
+
Retorna: (Y_q, Y_ref)
|
| 167 |
+
"""
|
| 168 |
+
W = self.ref.weight
|
| 169 |
+
Y_ref = F.linear(X, W, self.ref.bias)
|
| 170 |
+
# Smooth factor.
|
| 171 |
+
s = calibrate_smoothquant(X, W, alpha=alpha_val)
|
| 172 |
+
# Smooth.
|
| 173 |
+
X_smooth = X / s
|
| 174 |
+
W_smooth = W * s.unsqueeze(0)
|
| 175 |
+
# Quantize W per-channel.
|
| 176 |
+
W_q, w_scale = quantize_per_channel_symmetric(W_smooth, axis=0, n_bits=8)
|
| 177 |
+
# Quantize X per-token.
|
| 178 |
+
X_q, x_scale = quantize_per_token_symmetric(X_smooth, n_bits=8)
|
| 179 |
+
# Dequant.
|
| 180 |
+
X_fp = X_q.to(torch.float32) * x_scale
|
| 181 |
+
W_fp = W_q.to(torch.float32) * w_scale.unsqueeze(1)
|
| 182 |
+
Y_q = X_fp @ W_fp.t()
|
| 183 |
+
if self.ref.bias is not None:
|
| 184 |
+
Y_q = Y_q + self.ref.bias
|
| 185 |
+
return Y_q, Y_ref
|
| 186 |
+
|
| 187 |
+
def _compute_error_gradient(
|
| 188 |
+
self,
|
| 189 |
+
X: torch.Tensor,
|
| 190 |
+
alpha_tensor: torch.Tensor,
|
| 191 |
+
) -> torch.Tensor:
|
| 192 |
+
"""Computa gradiente do erro via STE (Straight-Through Estimator).
|
| 193 |
+
|
| 194 |
+
E(α) = ||Y_ref - Y_q(α)||_F^2 / ||Y_ref||_F^2
|
| 195 |
+
|
| 196 |
+
Usa STE para round() (não-diferenciável).
|
| 197 |
+
"""
|
| 198 |
+
W = self.ref.weight
|
| 199 |
+
# Y_ref (sem grad).
|
| 200 |
+
with torch.no_grad():
|
| 201 |
+
Y_ref = F.linear(X, W, self.ref.bias)
|
| 202 |
+
y_ref_norm_sq = max(((Y_ref) ** 2).mean().item(), 1e-12)
|
| 203 |
+
# s(α) diferenciável.
|
| 204 |
+
d_in = W.shape[-1]
|
| 205 |
+
X_flat = X.reshape(-1, d_in)
|
| 206 |
+
max_x = X_flat.abs().amax(dim=0).clamp(min=1e-8)
|
| 207 |
+
max_w = W.abs().amax(dim=0).clamp(min=1e-8)
|
| 208 |
+
s = torch.pow(max_x, alpha_tensor) / torch.pow(max_w, 1.0 - alpha_tensor)
|
| 209 |
+
# Smooth.
|
| 210 |
+
X_smooth = X / s
|
| 211 |
+
W_smooth = W * s.unsqueeze(0)
|
| 212 |
+
# Quantização com STE.
|
| 213 |
+
qmax = 127
|
| 214 |
+
max_w_smooth = W_smooth.abs().amax(dim=1, keepdim=True).clamp(min=1e-8)
|
| 215 |
+
scale_w = max_w_smooth / qmax
|
| 216 |
+
max_x_smooth = X_smooth.abs().amax(dim=-1, keepdim=True).clamp(min=1e-8)
|
| 217 |
+
scale_x = max_x_smooth / qmax
|
| 218 |
+
# STE: round com gradiente pass-through.
|
| 219 |
+
W_q_deq = ste_quantize(W_smooth, scale_w, 8)
|
| 220 |
+
X_q_deq = ste_quantize(X_smooth, scale_x, 8)
|
| 221 |
+
# Matmul.
|
| 222 |
+
Y_q = X_q_deq @ W_q_deq.t()
|
| 223 |
+
if self.ref.bias is not None:
|
| 224 |
+
Y_q = Y_q + self.ref.bias
|
| 225 |
+
# Loss = MSE relativo.
|
| 226 |
+
loss = ((Y_ref - Y_q) ** 2).mean() / y_ref_norm_sq
|
| 227 |
+
# Regularização entropy (atrai α para 0.5 — centro do intervalo).
|
| 228 |
+
eps = 1e-8
|
| 229 |
+
alpha_clamped = alpha_tensor.clamp(eps, 1.0 - eps)
|
| 230 |
+
reg = -self.reg_lambda * (
|
| 231 |
+
alpha_clamped * torch.log(alpha_clamped + eps) +
|
| 232 |
+
(1 - alpha_clamped) * torch.log(1 - alpha_clamped + eps)
|
| 233 |
+
)
|
| 234 |
+
# negativo porque queremos MAXIMIZAR entropy (atrair α para 0.5)
|
| 235 |
+
total_loss = loss - reg
|
| 236 |
+
total_loss.backward()
|
| 237 |
+
return total_loss
|
| 238 |
+
|
| 239 |
+
def reduce_error(
|
| 240 |
+
self,
|
| 241 |
+
X: torch.Tensor,
|
| 242 |
+
) -> W8A8ReductionResult:
|
| 243 |
+
"""Executa redução iterativa de erro W8A8.
|
| 244 |
+
|
| 245 |
+
Args:
|
| 246 |
+
X: dados de calibração (B, d_in)
|
| 247 |
+
|
| 248 |
+
Returns:
|
| 249 |
+
W8A8ReductionResult
|
| 250 |
+
"""
|
| 251 |
+
initial_alpha = float(self.alpha.item())
|
| 252 |
+
# Computa erro inicial.
|
| 253 |
+
with torch.no_grad():
|
| 254 |
+
Y_q_0, Y_ref_0 = self._compute_quantization_error(X, initial_alpha)
|
| 255 |
+
initial_error = ((Y_ref_0 - Y_q_0) ** 2).mean().item() / max(
|
| 256 |
+
((Y_ref_0) ** 2).mean().item(), 1e-12
|
| 257 |
+
)
|
| 258 |
+
# V5: ht analítico (sem rede neural)
|
| 259 |
+
ht_val = analytic_trust(initial_error, initial_alpha)
|
| 260 |
+
history = [{"iteration": 0, "alpha": initial_alpha, "error": initial_error, "ht": ht_val}]
|
| 261 |
+
prev_error = initial_error
|
| 262 |
+
converged = False
|
| 263 |
+
opt = torch.optim.SGD([self.eta_alpha], lr=self.lr)
|
| 264 |
+
for iteration in range(1, self.max_iterations + 1):
|
| 265 |
+
# 1. α atual.
|
| 266 |
+
alpha_val = float(self.alpha.item())
|
| 267 |
+
with torch.no_grad():
|
| 268 |
+
Y_q, Y_ref = self._compute_quantization_error(X, alpha_val)
|
| 269 |
+
current_error = ((Y_ref - Y_q) ** 2).mean().item() / max(
|
| 270 |
+
((Y_ref) ** 2).mean().item(), 1e-12
|
| 271 |
+
)
|
| 272 |
+
# V5: ht analítico
|
| 273 |
+
ht_val = analytic_trust(current_error, alpha_val)
|
| 274 |
+
# 2. Se ht > threshold, converged.
|
| 275 |
+
if ht_val > self.trust_threshold:
|
| 276 |
+
converged = True
|
| 277 |
+
if self.verbose:
|
| 278 |
+
print(f" iter {iteration}: ht={ht_val:.4f} > τ={self.trust_threshold}, converged")
|
| 279 |
+
break
|
| 280 |
+
# 3. Atualiza α via STE gradiente, modulado por (1 - ht).
|
| 281 |
+
opt.zero_grad()
|
| 282 |
+
self._compute_error_gradient(X, self.alpha)
|
| 283 |
+
# Modula gradiente por (1 - ht).
|
| 284 |
+
with torch.no_grad():
|
| 285 |
+
if self.eta_alpha.grad is not None:
|
| 286 |
+
self.eta_alpha.grad *= (1.0 - ht_val)
|
| 287 |
+
opt.step()
|
| 288 |
+
# 4. Clamp α para [0.01, 0.99].
|
| 289 |
+
with torch.no_grad():
|
| 290 |
+
self.eta_alpha.data.clamp_(
|
| 291 |
+
math.log(0.01 / 0.99), math.log(0.99 / 0.01)
|
| 292 |
+
)
|
| 293 |
+
# 5. Verifica convergência por erro.
|
| 294 |
+
if abs(prev_error - current_error) < self.convergence_tol:
|
| 295 |
+
converged = True
|
| 296 |
+
if self.verbose:
|
| 297 |
+
print(f" iter {iteration}: erro convergiu (Δ={abs(prev_error - current_error):.2e})")
|
| 298 |
+
break
|
| 299 |
+
prev_error = current_error
|
| 300 |
+
history.append({
|
| 301 |
+
"iteration": iteration,
|
| 302 |
+
"alpha": float(self.alpha.item()),
|
| 303 |
+
"error": current_error,
|
| 304 |
+
"ht": ht_val,
|
| 305 |
+
})
|
| 306 |
+
if self.verbose and iteration % 5 == 0:
|
| 307 |
+
print(f" iter {iteration}: α={float(self.alpha.item()):.4f}, "
|
| 308 |
+
f"erro={current_error:.6f}, ht={ht_val:.4f}")
|
| 309 |
+
final_alpha = float(self.alpha.item())
|
| 310 |
+
final_error = history[-1]["error"]
|
| 311 |
+
return W8A8ReductionResult(
|
| 312 |
+
initial_alpha=initial_alpha,
|
| 313 |
+
final_alpha=final_alpha,
|
| 314 |
+
initial_error=initial_error,
|
| 315 |
+
final_error=final_error,
|
| 316 |
+
n_iterations=len(history) - 1,
|
| 317 |
+
converged=converged,
|
| 318 |
+
history=history,
|
| 319 |
+
final_ht=ht_val,
|
| 320 |
+
)
|
| 321 |
+
|
| 322 |
+
def get_smooth_scale(self, X: torch.Tensor) -> torch.Tensor:
|
| 323 |
+
"""Retorna o smooth scale final."""
|
| 324 |
+
with torch.no_grad():
|
| 325 |
+
return calibrate_smoothquant(X, self.ref.weight, float(self.alpha.item())).detach()
|
| 326 |
+
|
| 327 |
+
def quantize_weights(self, X: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
|
| 328 |
+
"""Quantiza pesos com α otimizado.
|
| 329 |
+
|
| 330 |
+
Retorna: (weight_int8, weight_scale)
|
| 331 |
+
"""
|
| 332 |
+
s = self.get_smooth_scale(X)
|
| 333 |
+
W_smooth = self.ref.weight * s.unsqueeze(0)
|
| 334 |
+
W_q, w_scale = quantize_per_channel_symmetric(W_smooth, axis=0, n_bits=8)
|
| 335 |
+
return W_q, w_scale
|
| 336 |
+
|
| 337 |
+
|
| 338 |
+
# ============================================================================
|
| 339 |
+
# 2. MultiLayerW8A8Reducer — aplica redução a múltiplas camadas
|
| 340 |
+
# ============================================================================
|
| 341 |
+
class MultiLayerW8A8Reducer:
|
| 342 |
+
"""Aplica TrustGuidedW8A8 a múltiplas camadas Linear de um modelo.
|
| 343 |
+
|
| 344 |
+
Para cada camada:
|
| 345 |
+
1. Coleta ativações de calibração
|
| 346 |
+
2. Executa TrustGuidedW8A8.reduce_error()
|
| 347 |
+
3. Substitui a camada original pela versão quantizada (SmoothQuantW8A8)
|
| 348 |
+
|
| 349 |
+
V5: OOM-safe — não armazena ativações de todas as camadas simultaneamente.
|
| 350 |
+
Processa uma camada por vez.
|
| 351 |
+
"""
|
| 352 |
+
|
| 353 |
+
def __init__(
|
| 354 |
+
self,
|
| 355 |
+
init_alpha: float = 0.5,
|
| 356 |
+
trust_threshold: float = 0.8,
|
| 357 |
+
lr: float = 0.05,
|
| 358 |
+
max_iterations: int = 15,
|
| 359 |
+
verbose: bool = False,
|
| 360 |
+
):
|
| 361 |
+
self.init_alpha = init_alpha
|
| 362 |
+
self.trust_threshold = trust_threshold
|
| 363 |
+
self.lr = lr
|
| 364 |
+
self.max_iterations = max_iterations
|
| 365 |
+
self.verbose = verbose
|
| 366 |
+
|
| 367 |
+
def reduce_model(
|
| 368 |
+
self,
|
| 369 |
+
model: nn.Module,
|
| 370 |
+
calib_X: torch.Tensor,
|
| 371 |
+
target_substrings: Optional[List[str]] = None,
|
| 372 |
+
) -> Dict[str, W8A8ReductionResult]:
|
| 373 |
+
"""Aplica redução W8A8 a todas as nn.Linear no modelo.
|
| 374 |
+
|
| 375 |
+
Args:
|
| 376 |
+
model: modelo a ser quantizado
|
| 377 |
+
calib_X: dados de calibração (B, T, d) ou (B, d)
|
| 378 |
+
target_substrings: substrings para filtrar camadas (None = todas)
|
| 379 |
+
|
| 380 |
+
Retorna: {layer_name: W8A8ReductionResult}
|
| 381 |
+
"""
|
| 382 |
+
results = {}
|
| 383 |
+
# Coleta camadas a quantizar.
|
| 384 |
+
to_replace = []
|
| 385 |
+
for name, module in model.named_modules():
|
| 386 |
+
for child_name, child in module.named_children():
|
| 387 |
+
if isinstance(child, nn.Linear) and not isinstance(child, SmoothQuantW8A8):
|
| 388 |
+
full_name = f"{name}.{child_name}" if name else child_name
|
| 389 |
+
if target_substrings is None or any(s in full_name for s in target_substrings):
|
| 390 |
+
to_replace.append((module, child_name, child, full_name))
|
| 391 |
+
if self.verbose:
|
| 392 |
+
print(f" Encontradas {len(to_replace)} camadas Linear para quantização")
|
| 393 |
+
# Para cada camada, aplica TrustGuidedW8A8.
|
| 394 |
+
for module, child_name, linear_layer, full_name in to_replace:
|
| 395 |
+
d_in = linear_layer.in_features
|
| 396 |
+
# Extrai ativações de calibração para esta camada.
|
| 397 |
+
if calib_X.shape[-1] == d_in:
|
| 398 |
+
X_calib = calib_X.reshape(-1, d_in)
|
| 399 |
+
else:
|
| 400 |
+
# Projeta ou usa aleatório.
|
| 401 |
+
X_calib = torch.randn(min(64, calib_X.shape[0]), d_in)
|
| 402 |
+
# Reduz erro.
|
| 403 |
+
reducer = TrustGuidedW8A8(
|
| 404 |
+
ref=linear_layer,
|
| 405 |
+
init_alpha=self.init_alpha,
|
| 406 |
+
trust_threshold=self.trust_threshold,
|
| 407 |
+
lr=self.lr,
|
| 408 |
+
max_iterations=self.max_iterations,
|
| 409 |
+
verbose=self.verbose,
|
| 410 |
+
)
|
| 411 |
+
result = reducer.reduce_error(X_calib)
|
| 412 |
+
results[full_name] = result
|
| 413 |
+
# Substitui a camada pela versão quantizada (SmoothQuantW8A8 com α aprendível).
|
| 414 |
+
sq = SmoothQuantW8A8(
|
| 415 |
+
linear_layer,
|
| 416 |
+
init_alpha=result.final_alpha,
|
| 417 |
+
learnable_alpha=True, # V5: α continua aprendível após redução
|
| 418 |
+
)
|
| 419 |
+
sq.calibrate(X_calib)
|
| 420 |
+
# Seta o smooth_scale otimizado.
|
| 421 |
+
s = reducer.get_smooth_scale(X_calib)
|
| 422 |
+
sq.smooth_scale.copy_(s)
|
| 423 |
+
sq.freeze_quantization()
|
| 424 |
+
setattr(module, child_name, sq)
|
| 425 |
+
# V5 OOM fix: del referências explícitas
|
| 426 |
+
del reducer, X_calib
|
| 427 |
+
if self.verbose:
|
| 428 |
+
print(f" {full_name}: α {result.initial_alpha:.4f}→{result.final_alpha:.4f}, "
|
| 429 |
+
f"erro {result.initial_error:.6f}→{result.final_error:.6f}, "
|
| 430 |
+
f"ht={result.final_ht:.4f}, conv={result.converged}")
|
| 431 |
+
return results
|
| 432 |
+
|
| 433 |
+
def summarize_results(
|
| 434 |
+
self, results: Dict[str, W8A8ReductionResult]
|
| 435 |
+
) -> str:
|
| 436 |
+
"""Gera resumo tabular dos resultados."""
|
| 437 |
+
lines = [
|
| 438 |
+
f"{'Layer':<30} | {'α_init':>8} | {'α_final':>8} | {'err_init':>10} | {'err_final':>10} | {'reduction':>10} | {'ht':>6} | {'conv':>5}",
|
| 439 |
+
f"{'-'*30} | {'-'*8} | {'-'*8} | {'-'*10} | {'-'*10} | {'-'*10} | {'-'*6} | {'-'*5}",
|
| 440 |
+
]
|
| 441 |
+
for name, r in results.items():
|
| 442 |
+
reduction = (1 - r.final_error / max(r.initial_error, 1e-12)) * 100
|
| 443 |
+
lines.append(
|
| 444 |
+
f"{name:<30} | {r.initial_alpha:>8.4f} | {r.final_alpha:>8.4f} | "
|
| 445 |
+
f"{r.initial_error:>10.6f} | {r.final_error:>10.6f} | "
|
| 446 |
+
f"{reduction:>9.1f}% | {r.final_ht:>6.3f} | {'✓' if r.converged else '✗':>5}"
|
| 447 |
+
)
|
| 448 |
+
return "\n".join(lines)
|
| 449 |
+
|
| 450 |
+
|
| 451 |
+
__all__ = [
|
| 452 |
+
"W8A8ConvergenceError",
|
| 453 |
+
"analytic_trust",
|
| 454 |
+
"W8A8ReductionResult",
|
| 455 |
+
"TrustGuidedW8A8",
|
| 456 |
+
"MultiLayerW8A8Reducer",
|
| 457 |
+
]
|
src/bigru_t/quantization/w8a8_smoothquant.py
ADDED
|
@@ -0,0 +1,365 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""w8a8_smoothquant.py — W8A8 com SmoothQuant e α APRENDÍVEL (V5).
|
| 2 |
+
|
| 3 |
+
═══════════════════════════════════════════════════════════════════════════════
|
| 4 |
+
V5 — APRIMORAMENTO MATEMÁTICO: α APRENDÍVEL VIA STE
|
| 5 |
+
═══════════════════════════════════════════════════════════════════════════════
|
| 6 |
+
|
| 7 |
+
ORIGEM (gru-ring-v13-9-2):
|
| 8 |
+
source/xavante_work/flexnet/w8a8_smoothquant.py (V13.9.1)
|
| 9 |
+
|
| 10 |
+
Algoritmo (Xiao et al., 2023 — "SmoothQuant"):
|
| 11 |
+
==============================================
|
| 12 |
+
Dada uma camada Linear Y = X W^T:
|
| 13 |
+
* X ∈ R^{B × d_in} ativações (com outliers por canal)
|
| 14 |
+
* W ∈ R^{d_out × d_in} pesos (aproximadamente uniformes)
|
| 15 |
+
|
| 16 |
+
SmoothQuant introduz fator de suavização s ∈ R^{d_in}_{>0}:
|
| 17 |
+
* X̃ = X / s (divide ativações → reduz outliers)
|
| 18 |
+
* W̃ = W * s (multiplica pesos → preserva produto)
|
| 19 |
+
* Y = X̃ W̃^T = X W^T (matematicamente equivalente)
|
| 20 |
+
|
| 21 |
+
Escolha de s por canal de entrada j:
|
| 22 |
+
s_j = max(|X_j|)^α / max(|W_j|)^{1-α}
|
| 23 |
+
|
| 24 |
+
* α = 0.0 → "dificuldade" toda nos pesos (= quantização per-channel clássica)
|
| 25 |
+
* α = 1.0 → "dificuldade" toda nas ativações (= quantização per-token)
|
| 26 |
+
* α = 0.5 (default empírico do paper) → balanceia
|
| 27 |
+
|
| 28 |
+
V5 — MELHORIA MATEMÁTICA SOBRE V13.9.1:
|
| 29 |
+
Em V13.9.1, α era FIXO (default 0.5). Em V5, α é APRENDÍVEL:
|
| 30 |
+
* Parametrizado via sigmoid: α = sigmoid(eta_alpha) ∈ (0, 1)
|
| 31 |
+
* eta_alpha é nn.Parameter treinável — o modelo ajusta α sozinho
|
| 32 |
+
* Backward via STE (Straight-Through Estimator):
|
| 33 |
+
- round() não é diferenciável → STE substitui grad por 1
|
| 34 |
+
- clamp() tem gradiente 1 dentro do intervalo, 0 fora
|
| 35 |
+
* Loss auxiliary: ||Y_fp - Y_q||_F^2 / ||Y_fp||_F^2 (relative MSE)
|
| 36 |
+
adicionada à loss total → gradiente flui para eta_alpha
|
| 37 |
+
|
| 38 |
+
Isso atende ao requisito V5: "modelo deve ajustar todos os seus parâmetros
|
| 39 |
+
por conta própria sempre". α era o único parâmetro do W8A8 que não era
|
| 40 |
+
auto-ajustado — agora é.
|
| 41 |
+
|
| 42 |
+
V5 — MELHORIA DE MEMÓRIA (OOM-Killer fix):
|
| 43 |
+
* calib_X_buf (256 × d_in float32 = 1KB por camada) REMOVIDO
|
| 44 |
+
— em V13.9.1 era armazenado para freeze_quantization, mas só max-abs
|
| 45 |
+
é usado; buffer completo era desperdício.
|
| 46 |
+
* Substituído por calib_max_abs (d_in float32 = 0.5KB por camada)
|
| 47 |
+
— running max-abs per channel, suficiente para s.
|
| 48 |
+
* Em modelo com 100 camadas Linear × d_in=1024: 256KB → 0.5KB (-99.8%)
|
| 49 |
+
|
| 50 |
+
Limite de erro (W8A8 simétrico, per-channel):
|
| 51 |
+
||Y_fp - Y_q||_2 ≤ ε_w · ||W||_F · ||X||_F + ε_x · ||X||_F · ||W||_F
|
| 52 |
+
onde ε_w, ε_x ≤ 2^{-7} (1/128) para escalas bem calibradas.
|
| 53 |
+
"""
|
| 54 |
+
from __future__ import annotations
|
| 55 |
+
|
| 56 |
+
import math
|
| 57 |
+
import logging
|
| 58 |
+
from typing import Optional, Tuple
|
| 59 |
+
|
| 60 |
+
import torch
|
| 61 |
+
import torch.nn as nn
|
| 62 |
+
import torch.nn.functional as F
|
| 63 |
+
|
| 64 |
+
logger = logging.getLogger(__name__)
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 68 |
+
# Funções de Quantização Simétrica (zero_point=0 sempre)
|
| 69 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 70 |
+
def _per_channel_max_abs(W: torch.Tensor, dim: int) -> torch.Tensor:
|
| 71 |
+
"""max |W| ao longo de dim, com mínimo epsilon para evitar divisão por 0."""
|
| 72 |
+
return W.abs().amax(dim=dim).clamp(min=1e-8)
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def calibrate_smoothquant(
|
| 76 |
+
X: torch.Tensor,
|
| 77 |
+
W: torch.Tensor,
|
| 78 |
+
alpha: float = 0.5,
|
| 79 |
+
) -> torch.Tensor:
|
| 80 |
+
"""Calcula o fator SmoothQuant s ∈ R^{d_in}_{>0}.
|
| 81 |
+
|
| 82 |
+
X: (B, d_in) ou (..., d_in) — ativações de calibração
|
| 83 |
+
W: (d_out, d_in) — pesos da camada Linear
|
| 84 |
+
alpha: ∈ [0, 1], trade-off (default 0.5)
|
| 85 |
+
|
| 86 |
+
Retorna: s ∈ R^{d_in} (positivo).
|
| 87 |
+
|
| 88 |
+
Fórmula: s_j = max(|X_j|)^α / max(|W_j|)^{1-α}
|
| 89 |
+
"""
|
| 90 |
+
d_in = W.shape[-1]
|
| 91 |
+
if X.shape[-1] != d_in:
|
| 92 |
+
return torch.ones(d_in, device=W.device, dtype=W.dtype)
|
| 93 |
+
X_flat = X.reshape(-1, d_in)
|
| 94 |
+
max_x = X_flat.abs().amax(dim=0).clamp(min=1e-8) # (d_in,)
|
| 95 |
+
max_w = W.abs().amax(dim=0).clamp(min=1e-8) # (d_in,)
|
| 96 |
+
s = torch.pow(max_x, alpha) / torch.pow(max_w, 1.0 - alpha)
|
| 97 |
+
return s.clamp(min=1e-8)
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
def quantize_per_channel_symmetric(
|
| 101 |
+
W: torch.Tensor,
|
| 102 |
+
axis: int = 0,
|
| 103 |
+
n_bits: int = 8,
|
| 104 |
+
) -> Tuple[torch.Tensor, torch.Tensor]:
|
| 105 |
+
"""Quantização int8 simétrica per-channel (tensor 2D).
|
| 106 |
+
|
| 107 |
+
W: 2D tensor (d_axis, d_other).
|
| 108 |
+
axis=0 → uma escala por linha (cada output channel tem sua escala).
|
| 109 |
+
Retorna: (W_int8, scale) onde W_fp ≈ W_int8 * scale e scale é shape (d_axis,).
|
| 110 |
+
"""
|
| 111 |
+
assert W.dim() == 2, f"esperado tensor 2D, recebeu dim={W.dim()}"
|
| 112 |
+
qmax = 2 ** (n_bits - 1) - 1 # 127 para int8 (faixa simétrica [-127, 127])
|
| 113 |
+
qmin = -(2 ** (n_bits - 1) - 1)
|
| 114 |
+
other = 1 - axis
|
| 115 |
+
max_abs = W.abs().amax(dim=other, keepdim=True).clamp(min=1e-8) # (d_axis, 1)
|
| 116 |
+
scale = max_abs / qmax
|
| 117 |
+
W_q = torch.round(W / scale).clamp(qmin, qmax).to(torch.int8)
|
| 118 |
+
return W_q, scale.squeeze(1)
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
def quantize_per_token_symmetric(
|
| 122 |
+
X: torch.Tensor,
|
| 123 |
+
n_bits: int = 8,
|
| 124 |
+
) -> Tuple[torch.Tensor, torch.Tensor]:
|
| 125 |
+
"""Quantização int8 simétrica per-token (último eixo = canal).
|
| 126 |
+
|
| 127 |
+
X: (..., d_in)
|
| 128 |
+
Retorna: (X_int8, scale) onde X_fp ≈ X_int8 * scale e scale é shape (..., 1).
|
| 129 |
+
"""
|
| 130 |
+
qmax = 2 ** (n_bits - 1) - 1
|
| 131 |
+
qmin = -(2 ** (n_bits - 1) - 1)
|
| 132 |
+
max_abs = X.abs().amax(dim=-1, keepdim=True).clamp(min=1e-8)
|
| 133 |
+
scale = max_abs / qmax
|
| 134 |
+
X_q = torch.round(X / scale).clamp(qmin, qmax).to(torch.int8)
|
| 135 |
+
return X_q, scale
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 139 |
+
# STE para round() — permite gradiente fluir através de quantização
|
| 140 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 141 |
+
class STEQuantize(torch.autograd.Function):
|
| 142 |
+
"""Straight-Through Estimator para round + clamp.
|
| 143 |
+
|
| 144 |
+
Forward: x_q = round(x / scale).clamp(qmin, qmax) * scale
|
| 145 |
+
Backward: grad flui direto (grad_x = grad_y), ignorando round/clamp.
|
| 146 |
+
|
| 147 |
+
Isso é necessário porque:
|
| 148 |
+
* d(round)/dx = 0 em quase todos os pontos (escada de Heaviside)
|
| 149 |
+
* d(clamp)/dx = 0 fora do intervalo, 1 dentro
|
| 150 |
+
Sem STE, gradientes não fluiriam para os pesos/α através da quantização.
|
| 151 |
+
"""
|
| 152 |
+
|
| 153 |
+
@staticmethod
|
| 154 |
+
def forward(ctx, x, scale, qmin, qmax):
|
| 155 |
+
x_q = torch.round(x / scale).clamp(qmin, qmax)
|
| 156 |
+
x_deq = x_q * scale
|
| 157 |
+
return x_deq
|
| 158 |
+
|
| 159 |
+
@staticmethod
|
| 160 |
+
def backward(ctx, grad_output):
|
| 161 |
+
# STE: gradiente passa direto
|
| 162 |
+
return grad_output, None, None, None
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
def ste_quantize(x: torch.Tensor, scale: torch.Tensor, n_bits: int = 8) -> torch.Tensor:
|
| 166 |
+
"""Quantiza com STE. scale deve ter shape broadcastable com x."""
|
| 167 |
+
qmax = 2 ** (n_bits - 1) - 1
|
| 168 |
+
qmin = -(2 ** (n_bits - 1) - 1)
|
| 169 |
+
return STEQuantize.apply(x, scale, qmin, qmax)
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 173 |
+
# SmoothQuantW8A8 — camada Linear W8A8 com α APRENDÍVEL (V5)
|
| 174 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 175 |
+
class SmoothQuantW8A8(nn.Module):
|
| 176 |
+
"""Camada Linear W8A8 com SmoothQuant e α APRENDÍVEL.
|
| 177 |
+
|
| 178 |
+
V5 vs V13.9.1:
|
| 179 |
+
* α é parâmetro treinável (via sigmoid(eta_alpha))
|
| 180 |
+
* calib_X_buf removido (era OOM em modelos grandes) — apenas max_abs
|
| 181 |
+
* STE explícito para round() — backward bem definido
|
| 182 |
+
* Quantização do peso é DIFFERENCIÁVEL w.r.t. α (via s e STE)
|
| 183 |
+
|
| 184 |
+
Modos:
|
| 185 |
+
* calibration: coleta max_abs das ativações
|
| 186 |
+
* quantized: pesos em int8, ativações quantizadas on-the-fly com STE
|
| 187 |
+
|
| 188 |
+
Args:
|
| 189 |
+
ref: nn.Linear de referência (pesos copiados)
|
| 190 |
+
init_alpha: valor inicial de α (default 0.5)
|
| 191 |
+
learnable_alpha: se True (default V5), α é treinável
|
| 192 |
+
calib_batch_size: tamanho do buffer de calibração (apenas p/ count)
|
| 193 |
+
n_bits: nº de bits (default 8)
|
| 194 |
+
"""
|
| 195 |
+
|
| 196 |
+
def __init__(
|
| 197 |
+
self,
|
| 198 |
+
ref: nn.Linear,
|
| 199 |
+
init_alpha: float = 0.5,
|
| 200 |
+
learnable_alpha: bool = True,
|
| 201 |
+
calib_batch_size: int = 256,
|
| 202 |
+
n_bits: int = 8,
|
| 203 |
+
):
|
| 204 |
+
super().__init__()
|
| 205 |
+
self.in_features = ref.in_features
|
| 206 |
+
self.out_features = ref.out_features
|
| 207 |
+
self.n_bits = n_bits
|
| 208 |
+
self.learnable_alpha = learnable_alpha
|
| 209 |
+
|
| 210 |
+
# Pesos fp (treináveis até freeze; após freeze ainda treináveis p/ α)
|
| 211 |
+
self.weight_fp = nn.Parameter(ref.weight.detach().clone())
|
| 212 |
+
self.bias = (
|
| 213 |
+
nn.Parameter(ref.bias.detach().clone()) if ref.bias is not None else None
|
| 214 |
+
)
|
| 215 |
+
|
| 216 |
+
# V5: α aprendível via sigmoid(eta_alpha) ∈ (0, 1)
|
| 217 |
+
init_eta = float(math.log(init_alpha / max(1e-6, 1.0 - init_alpha)))
|
| 218 |
+
if learnable_alpha:
|
| 219 |
+
self.eta_alpha = nn.Parameter(torch.tensor(init_eta, dtype=torch.float32))
|
| 220 |
+
else:
|
| 221 |
+
self.register_buffer("eta_alpha", torch.tensor(init_eta, dtype=torch.float32))
|
| 222 |
+
|
| 223 |
+
# Buffers de quantização
|
| 224 |
+
self.register_buffer("smooth_scale", torch.ones(self.in_features))
|
| 225 |
+
self.register_buffer("weight_scale", torch.ones(self.out_features))
|
| 226 |
+
self.register_buffer(
|
| 227 |
+
"weight_int8",
|
| 228 |
+
torch.zeros(self.out_features, self.in_features, dtype=torch.int8),
|
| 229 |
+
)
|
| 230 |
+
self.register_buffer("is_quantized", torch.tensor(False))
|
| 231 |
+
self.register_buffer("calib_collected", torch.tensor(False))
|
| 232 |
+
# V5: Apenas max-abs per channel (OOM fix — buffer completo removido)
|
| 233 |
+
self.register_buffer("calib_max_abs", torch.zeros(self.in_features))
|
| 234 |
+
self.register_buffer("calib_count", torch.tensor(0, dtype=torch.long))
|
| 235 |
+
|
| 236 |
+
@property
|
| 237 |
+
def alpha(self) -> torch.Tensor:
|
| 238 |
+
"""α atual = sigmoid(eta_alpha) ∈ (0, 1)."""
|
| 239 |
+
return torch.sigmoid(self.eta_alpha)
|
| 240 |
+
|
| 241 |
+
@torch.no_grad()
|
| 242 |
+
def calibrate(self, X: torch.Tensor) -> None:
|
| 243 |
+
"""Acumula estatísticas de ativação (max-abs per channel).
|
| 244 |
+
|
| 245 |
+
V5 OOM fix: não armazena tensor completo, apenas max(|X|) per channel.
|
| 246 |
+
"""
|
| 247 |
+
if bool(self.is_quantized):
|
| 248 |
+
raise RuntimeError("Calibração após freeze não é permitida.")
|
| 249 |
+
flat = X.reshape(-1, self.in_features)
|
| 250 |
+
batch_max_abs = flat.abs().amax(dim=0).to(self.calib_max_abs.dtype)
|
| 251 |
+
self.calib_max_abs.copy_(torch.maximum(self.calib_max_abs, batch_max_abs))
|
| 252 |
+
self.calib_count += flat.shape[0]
|
| 253 |
+
self.calib_collected.fill_(True)
|
| 254 |
+
|
| 255 |
+
@torch.no_grad()
|
| 256 |
+
def freeze_quantization(self) -> None:
|
| 257 |
+
"""Computa s com α ATUAL, quantiza W em int8 per-channel.
|
| 258 |
+
|
| 259 |
+
Nota: depois de freeze, o modelo ainda pode chamar recompute_smooth_scale()
|
| 260 |
+
para atualizar s quando α mudar (durante o treino do α).
|
| 261 |
+
"""
|
| 262 |
+
if not bool(self.calib_collected):
|
| 263 |
+
logger.warning("SmoothQuantW8A8: freeze sem calibração. Usando s=1.")
|
| 264 |
+
X_calib = self.weight_fp.new_zeros(1, self.in_features)
|
| 265 |
+
else:
|
| 266 |
+
# Reconstrói tensor "representativo" das ativações a partir de max-abs
|
| 267 |
+
X_calib = self.calib_max_abs.unsqueeze(0) # (1, d_in)
|
| 268 |
+
|
| 269 |
+
alpha_val = float(self.alpha.item())
|
| 270 |
+
s = calibrate_smoothquant(X_calib, self.weight_fp, alpha=alpha_val)
|
| 271 |
+
self.smooth_scale.copy_(s.to(self.smooth_scale.dtype))
|
| 272 |
+
|
| 273 |
+
W_smooth = self.weight_fp * s.unsqueeze(0)
|
| 274 |
+
W_q, w_scale = quantize_per_channel_symmetric(W_smooth, axis=0, n_bits=self.n_bits)
|
| 275 |
+
self.weight_int8.copy_(W_q)
|
| 276 |
+
self.weight_scale.copy_(w_scale)
|
| 277 |
+
self.is_quantized.fill_(True)
|
| 278 |
+
|
| 279 |
+
@torch.no_grad()
|
| 280 |
+
def recompute_smooth_scale(self) -> None:
|
| 281 |
+
"""Recomputa s e re-quantiza W com α ATUAL (após treino de α).
|
| 282 |
+
|
| 283 |
+
V5: chamado pelo trainer a cada N steps para manter W quantizado
|
| 284 |
+
consistente com α atualizado.
|
| 285 |
+
"""
|
| 286 |
+
if not bool(self.is_quantized):
|
| 287 |
+
return
|
| 288 |
+
if not bool(self.calib_collected):
|
| 289 |
+
return
|
| 290 |
+
X_calib = self.calib_max_abs.unsqueeze(0)
|
| 291 |
+
alpha_val = float(self.alpha.item())
|
| 292 |
+
s = calibrate_smoothquant(X_calib, self.weight_fp, alpha=alpha_val)
|
| 293 |
+
self.smooth_scale.copy_(s.to(self.smooth_scale.dtype))
|
| 294 |
+
W_smooth = self.weight_fp * s.unsqueeze(0)
|
| 295 |
+
W_q, w_scale = quantize_per_channel_symmetric(W_smooth, axis=0, n_bits=self.n_bits)
|
| 296 |
+
self.weight_int8.copy_(W_q)
|
| 297 |
+
self.weight_scale.copy_(w_scale)
|
| 298 |
+
|
| 299 |
+
def forward(self, X: torch.Tensor) -> torch.Tensor:
|
| 300 |
+
"""Forward W8A8 com SmoothQuant e α aprendível.
|
| 301 |
+
|
| 302 |
+
V5: quando quantized, faz quantização on-the-fly com STE para que
|
| 303 |
+
gradientes fluam para eta_alpha (via s(α)).
|
| 304 |
+
"""
|
| 305 |
+
if not bool(self.is_quantized):
|
| 306 |
+
# Modo calibração/treino inicial: forward fp32 normal
|
| 307 |
+
return F.linear(X, self.weight_fp, self.bias)
|
| 308 |
+
|
| 309 |
+
# V5: recompute s(α) on-the-fly (diferenciável w.r.t. eta_alpha)
|
| 310 |
+
# smooth_scale é buffer (detached), mas recalculamos s com α treinável
|
| 311 |
+
alpha_t = self.alpha # diferenciável
|
| 312 |
+
# max_abs armazenado em calib_max_abs (detached)
|
| 313 |
+
max_x = self.calib_max_abs.clamp(min=1e-8)
|
| 314 |
+
max_w = self.weight_fp.detach().abs().amax(dim=0).clamp(min=1e-8)
|
| 315 |
+
s = torch.pow(max_x, alpha_t) / torch.pow(max_w, 1.0 - alpha_t)
|
| 316 |
+
s = s.clamp(min=1e-8)
|
| 317 |
+
|
| 318 |
+
# Smooth: X̃ = X / s, W̃_fp = weight_fp * s
|
| 319 |
+
X_smooth = X / s # (..., d_in) — diferenciável w.r.t. eta_alpha via s
|
| 320 |
+
# W̃_fp não precisa de STE aqui porque weight_fp é treinável diretamente
|
| 321 |
+
W_smooth = self.weight_fp * s.unsqueeze(0) # (d_out, d_in)
|
| 322 |
+
|
| 323 |
+
# Quantiza W per-channel com STE (diferenciável)
|
| 324 |
+
qmax = 2 ** (self.n_bits - 1) - 1
|
| 325 |
+
max_w_smooth = W_smooth.abs().amax(dim=1, keepdim=True).clamp(min=1e-8)
|
| 326 |
+
w_scale_dyn = max_w_smooth / qmax # (d_out, 1)
|
| 327 |
+
W_q_deq = ste_quantize(W_smooth, w_scale_dyn, self.n_bits) # (d_out, d_in)
|
| 328 |
+
|
| 329 |
+
# Quantiza X per-token com STE
|
| 330 |
+
max_x_smooth = X_smooth.abs().amax(dim=-1, keepdim=True).clamp(min=1e-8)
|
| 331 |
+
x_scale_dyn = max_x_smooth / qmax # (..., 1)
|
| 332 |
+
X_q_deq = ste_quantize(X_smooth, x_scale_dyn, self.n_bits) # (..., d_in)
|
| 333 |
+
|
| 334 |
+
# Matmul em fp32 (simulação; HW real faria INT8×INT8→INT32→fp32)
|
| 335 |
+
Y = X_q_deq @ W_q_deq.t()
|
| 336 |
+
if self.bias is not None:
|
| 337 |
+
Y = Y + self.bias
|
| 338 |
+
return Y
|
| 339 |
+
|
| 340 |
+
def quantization_error(self, X: torch.Tensor) -> float:
|
| 341 |
+
"""Calcula ||Y_fp - Y_q||_F / ||Y_fp||_F como métrica de erro."""
|
| 342 |
+
with torch.no_grad():
|
| 343 |
+
Y_fp = F.linear(X, self.weight_fp, self.bias)
|
| 344 |
+
Y_q = self.forward(X)
|
| 345 |
+
num = (Y_fp - Y_q).norm().item()
|
| 346 |
+
den = max(Y_fp.norm().item(), 1e-12)
|
| 347 |
+
return num / den
|
| 348 |
+
|
| 349 |
+
def extra_repr(self) -> str:
|
| 350 |
+
return (
|
| 351 |
+
f"in_features={self.in_features}, out_features={self.out_features}, "
|
| 352 |
+
f"alpha={float(self.alpha.item()):.4f}, "
|
| 353 |
+
f"learnable_alpha={self.learnable_alpha}, "
|
| 354 |
+
f"is_quantized={bool(self.is_quantized)}"
|
| 355 |
+
)
|
| 356 |
+
|
| 357 |
+
|
| 358 |
+
__all__ = [
|
| 359 |
+
"calibrate_smoothquant",
|
| 360 |
+
"quantize_per_channel_symmetric",
|
| 361 |
+
"quantize_per_token_symmetric",
|
| 362 |
+
"STEQuantize",
|
| 363 |
+
"ste_quantize",
|
| 364 |
+
"SmoothQuantW8A8",
|
| 365 |
+
]
|
src/bigru_t/training/meta_configurator.py
CHANGED
|
@@ -2,27 +2,24 @@
|
|
| 2 |
|
| 3 |
Implementa o Lema 4 (Auto-configuração e suavização).
|
| 4 |
|
| 5 |
-
Mantém hiperparâmetros aprendíveis:
|
| 6 |
- log_temperature: controla T = exp(log_temperature) do Lema 1
|
| 7 |
- log_tau: controla τ = exp(log_tau) do Lema 3 (limiar de ativação da hipótese)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
Meta-objetivo:
|
| 10 |
-
L_meta(T, τ) = L_val
|
|
|
|
|
|
|
| 11 |
|
| 12 |
-
onde S(θ) = ||∇_θ L_val||^2 é uma proxy de agudeza (sharpness)
|
| 13 |
-
de baixo custo para o maior autovalor da Hessiana.
|
| 14 |
|
| 15 |
Minimizar S(θ) empurra o modelo para mínimos planos (Keskar 2017, Foret 2021),
|
| 16 |
onde o ruído de gradiente é menor.
|
| 17 |
-
|
| 18 |
-
Reaproveita a filosofia do meta_learner.py (MAML-RNOEFLEX) e meta_orchestrator.py
|
| 19 |
-
do v13.9.2, mas:
|
| 20 |
-
- Substitui MAML de 2ª ordem por diferenciação implícita simplificada
|
| 21 |
-
(1ª ordem + proxy de agudeza)
|
| 22 |
-
- Substitui o orchestrator de 3º nível por 2 hiperparâmetros escalares
|
| 23 |
-
(T, τ) — mais barato computacionalmente
|
| 24 |
-
- A proxy de agudeza é o quadrado da norma do gradiente (não o autovalor
|
| 25 |
-
da Hessiana — seria proibitivo para 16M+ params)
|
| 26 |
"""
|
| 27 |
from __future__ import annotations
|
| 28 |
|
|
@@ -33,18 +30,20 @@ import torch.nn as nn
|
|
| 33 |
|
| 34 |
|
| 35 |
class MetaConfigurator:
|
| 36 |
-
"""Auto-configuração de
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
|
| 38 |
Args:
|
| 39 |
-
model: o UnifiedModel a ser configurado
|
| 40 |
-
|
|
|
|
| 41 |
sharpness_lambda: peso λ_s da proxy de agudeza
|
| 42 |
|
| 43 |
-
Atributos:
|
| 44 |
-
log_temperature: nn.Parameter — T = exp(log_temperature)
|
| 45 |
-
log_tau: nn.Parameter — τ = exp(log_tau)
|
| 46 |
-
meta_optim: Adam sobre (log_temperature, log_tau)
|
| 47 |
-
|
| 48 |
Uso:
|
| 49 |
meta_cfg = MetaConfigurator(model)
|
| 50 |
# No loop de treino, a cada meta_interval batches:
|
|
@@ -66,10 +65,20 @@ class MetaConfigurator:
|
|
| 66 |
self.log_temperature = nn.Parameter(torch.tensor(0.0))
|
| 67 |
self.log_tau = nn.Parameter(torch.tensor(0.0))
|
| 68 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
# Otimizador Adam sobre os hiperparâmetros
|
| 70 |
-
self.meta_optim = torch.optim.Adam(
|
| 71 |
-
[self.log_temperature, self.log_tau], lr=meta_lr
|
| 72 |
-
)
|
| 73 |
|
| 74 |
@property
|
| 75 |
def temperature(self) -> float:
|
|
@@ -81,6 +90,20 @@ class MetaConfigurator:
|
|
| 81 |
"""τ atual = exp(log_tau)."""
|
| 82 |
return float(torch.exp(self.log_tau.detach()).item())
|
| 83 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
def forward_with_meta(
|
| 85 |
self,
|
| 86 |
x_val: torch.Tensor,
|
|
@@ -95,19 +118,21 @@ class MetaConfigurator:
|
|
| 95 |
criterion: função de perda (default: CrossEntropyLoss)
|
| 96 |
|
| 97 |
Returns:
|
| 98 |
-
dict com: loss_val, sharpness, meta_loss, T_new, tau_new
|
| 99 |
|
| 100 |
V3 BUGFIX: log_tau não tinha caminho de gradiente (aparecia como None).
|
| 101 |
Agora adicionamos um termo de target-tracking: τ deve seguir loss_val
|
| 102 |
(com margem), dando a log_tau um gradiente via MSE.
|
| 103 |
-
Também corrigimos module_selector para preservar o grafo quando
|
| 104 |
-
temperature é um tensor (torch.clamp em vez de max()).
|
| 105 |
|
| 106 |
V3.2 BUGFIX (complementar): log_temperature também não movia em 1 step
|
| 107 |
-
porque o gradiente via alpha = softmax(logits/T) é ≈0 quando logits ≈0
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
"""
|
| 112 |
if criterion is None:
|
| 113 |
criterion = nn.CrossEntropyLoss()
|
|
@@ -115,12 +140,35 @@ class MetaConfigurator:
|
|
| 115 |
# Hiperparâmetros atuais (positivos via exp)
|
| 116 |
temp = torch.exp(self.log_temperature)
|
| 117 |
tau_val = torch.exp(self.log_tau)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
|
| 119 |
# Forward do modelo com T atual (sem hipótese — só validação principal)
|
|
|
|
|
|
|
| 120 |
self.model.train() # mantém dropout ativo para estimar sharpness real
|
| 121 |
-
y_hat_main, _ = self.model(
|
|
|
|
|
|
|
| 122 |
loss_val = criterion(y_hat_main, y_val)
|
| 123 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
# Proxy de agudeza: ||∇_θ L_val||^2
|
| 125 |
# BUG FIX: create_graph=True causava OOM-Killer (second-order graph dobra RAM)
|
| 126 |
# Correção: usar create_graph=False (first-order approximation)
|
|
@@ -157,17 +205,50 @@ class MetaConfigurator:
|
|
| 157 |
T_pred = torch.exp(self.log_temperature)
|
| 158 |
T_tracking_loss = 0.05 * (T_pred - T_target) ** 2
|
| 159 |
|
| 160 |
-
#
|
| 161 |
-
#
|
| 162 |
-
#
|
| 163 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 164 |
|
| 165 |
-
# Atualiza log_temperature
|
| 166 |
self.meta_optim.zero_grad()
|
| 167 |
meta_loss.backward()
|
| 168 |
-
# Guarda
|
| 169 |
grad_log_T = self.log_temperature.grad.item() if self.log_temperature.grad is not None else 0.0
|
| 170 |
grad_log_tau = self.log_tau.grad.item() if self.log_tau.grad is not None else 0.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 171 |
self.meta_optim.step()
|
| 172 |
|
| 173 |
# Sincroniza model.tau com o novo valor
|
|
@@ -180,10 +261,18 @@ class MetaConfigurator:
|
|
| 180 |
"meta_loss": float(meta_loss.item()),
|
| 181 |
"T_new": float(torch.exp(self.log_temperature.detach()).item()),
|
| 182 |
"tau_new": float(torch.exp(self.log_tau.detach()).item()),
|
|
|
|
|
|
|
|
|
|
|
|
|
| 183 |
"grad_log_T": grad_log_T,
|
| 184 |
"grad_log_tau": grad_log_tau,
|
|
|
|
|
|
|
| 185 |
"tau_target": float(tau_target.item()),
|
| 186 |
"T_target": float(T_target.item()),
|
| 187 |
"T_tracking_loss": float(T_tracking_loss.item()),
|
| 188 |
"tau_tracking_loss": float(tau_tracking_loss.item()),
|
|
|
|
|
|
|
| 189 |
}
|
|
|
|
| 2 |
|
| 3 |
Implementa o Lema 4 (Auto-configuração e suavização).
|
| 4 |
|
| 5 |
+
V5: Mantém hiperparâmetros aprendíveis:
|
| 6 |
- log_temperature: controla T = exp(log_temperature) do Lema 1
|
| 7 |
- log_tau: controla τ = exp(log_tau) do Lema 3 (limiar de ativação da hipótese)
|
| 8 |
+
- log_lambda_ent: peso da reg. de entropia do module_selector (V5 NOVO)
|
| 9 |
+
- log_init_gate: gate inicial do CircularReasoning (V5 NOVO)
|
| 10 |
+
|
| 11 |
+
Requisito V5: "modelo deve ajustar todos os seus parâmetros por conta própria
|
| 12 |
+
sempre" — agora T, τ, λ_ent, e init_gate são todos auto-ajustados via meta-loss.
|
| 13 |
|
| 14 |
Meta-objetivo:
|
| 15 |
+
L_meta(T, τ, λ_ent, init_gate) = L_val + λ_s * S(θ)
|
| 16 |
+
+ tracking losses para T, τ
|
| 17 |
+
+ reg losses para λ_ent, init_gate
|
| 18 |
|
| 19 |
+
onde S(θ) = ||∇_θ L_val||^2 é uma proxy de agudeza (sharpness).
|
|
|
|
| 20 |
|
| 21 |
Minimizar S(θ) empurra o modelo para mínimos planos (Keskar 2017, Foret 2021),
|
| 22 |
onde o ruído de gradiente é menor.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
"""
|
| 24 |
from __future__ import annotations
|
| 25 |
|
|
|
|
| 30 |
|
| 31 |
|
| 32 |
class MetaConfigurator:
|
| 33 |
+
"""Auto-configuração de T, τ, λ_ent, init_gate via meta-aprendizado.
|
| 34 |
+
|
| 35 |
+
V5: Agora gerencia 4 hiperparâmetros (era 2 em V3.4):
|
| 36 |
+
* log_temperature → T (softmax temperature do module_selector)
|
| 37 |
+
* log_tau → τ (limiar de ativação da hipótese)
|
| 38 |
+
* log_lambda_ent → λ_ent (peso da reg. de entropia) — V5 NOVO
|
| 39 |
+
* log_init_gate → init_gate (gate do CircularReasoning) — V5 NOVO
|
| 40 |
|
| 41 |
Args:
|
| 42 |
+
model: o UnifiedModel a ser configurado (deve ter log_lambda_ent e
|
| 43 |
+
log_init_gate como nn.Parameters)
|
| 44 |
+
meta_lr: learning rate do meta-otimizador (Adam)
|
| 45 |
sharpness_lambda: peso λ_s da proxy de agudeza
|
| 46 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
Uso:
|
| 48 |
meta_cfg = MetaConfigurator(model)
|
| 49 |
# No loop de treino, a cada meta_interval batches:
|
|
|
|
| 65 |
self.log_temperature = nn.Parameter(torch.tensor(0.0))
|
| 66 |
self.log_tau = nn.Parameter(torch.tensor(0.0))
|
| 67 |
|
| 68 |
+
# V5 NOVO: Coletar todos os parâmetros meta
|
| 69 |
+
# - log_temperature, log_tau (locais)
|
| 70 |
+
# - model.log_lambda_ent (λ_ent do UnifiedModel)
|
| 71 |
+
# - model.circular_reasoning.residual_gate (n_cycles gates, se CRW ativo)
|
| 72 |
+
meta_params = [self.log_temperature, self.log_tau]
|
| 73 |
+
if hasattr(model, 'log_lambda_ent'):
|
| 74 |
+
meta_params.append(model.log_lambda_ent)
|
| 75 |
+
if (hasattr(model, 'circular_reasoning')
|
| 76 |
+
and model.circular_reasoning is not None
|
| 77 |
+
and hasattr(model.circular_reasoning, 'residual_gate')):
|
| 78 |
+
meta_params.append(model.circular_reasoning.residual_gate)
|
| 79 |
+
|
| 80 |
# Otimizador Adam sobre os hiperparâmetros
|
| 81 |
+
self.meta_optim = torch.optim.Adam(meta_params, lr=meta_lr)
|
|
|
|
|
|
|
| 82 |
|
| 83 |
@property
|
| 84 |
def temperature(self) -> float:
|
|
|
|
| 90 |
"""τ atual = exp(log_tau)."""
|
| 91 |
return float(torch.exp(self.log_tau.detach()).item())
|
| 92 |
|
| 93 |
+
@property
|
| 94 |
+
def lambda_ent(self) -> float:
|
| 95 |
+
"""λ_ent atual = exp(model.log_lambda_ent)."""
|
| 96 |
+
if hasattr(self.model, 'log_lambda_ent'):
|
| 97 |
+
return float(torch.exp(self.model.log_lambda_ent.detach()).item())
|
| 98 |
+
return 0.01
|
| 99 |
+
|
| 100 |
+
@property
|
| 101 |
+
def init_gate(self) -> float:
|
| 102 |
+
"""init_gate atual = exp(model.log_init_gate)."""
|
| 103 |
+
if hasattr(self.model, 'log_init_gate'):
|
| 104 |
+
return float(torch.exp(self.model.log_init_gate.detach()).item())
|
| 105 |
+
return 0.05
|
| 106 |
+
|
| 107 |
def forward_with_meta(
|
| 108 |
self,
|
| 109 |
x_val: torch.Tensor,
|
|
|
|
| 118 |
criterion: função de perda (default: CrossEntropyLoss)
|
| 119 |
|
| 120 |
Returns:
|
| 121 |
+
dict com: loss_val, sharpness, meta_loss, T_new, tau_new, λ_ent_new, init_gate_new
|
| 122 |
|
| 123 |
V3 BUGFIX: log_tau não tinha caminho de gradiente (aparecia como None).
|
| 124 |
Agora adicionamos um termo de target-tracking: τ deve seguir loss_val
|
| 125 |
(com margem), dando a log_tau um gradiente via MSE.
|
|
|
|
|
|
|
| 126 |
|
| 127 |
V3.2 BUGFIX (complementar): log_temperature também não movia em 1 step
|
| 128 |
+
porque o gradiente via alpha = softmax(logits/T) é ≈0 quando logits ≈0.
|
| 129 |
+
Adicionamos T_target = 1 + 0.1*(loss - 1): loss alta → T alto (exploração).
|
| 130 |
+
|
| 131 |
+
V5: Agora também atualiza λ_ent e init_gate do modelo via meta-loss.
|
| 132 |
+
- λ_ent: tracking loss para manter perto de 0.01 (evita saturação
|
| 133 |
+
da entropia do module_selector)
|
| 134 |
+
- init_gate: tracking loss para manter perto de 0.05 (evita gate
|
| 135 |
+
muito grande que quebraria a contração W₂ do CircularReasoning)
|
| 136 |
"""
|
| 137 |
if criterion is None:
|
| 138 |
criterion = nn.CrossEntropyLoss()
|
|
|
|
| 140 |
# Hiperparâmetros atuais (positivos via exp)
|
| 141 |
temp = torch.exp(self.log_temperature)
|
| 142 |
tau_val = torch.exp(self.log_tau)
|
| 143 |
+
# V5: ler do model
|
| 144 |
+
lambda_ent_val = (
|
| 145 |
+
torch.exp(self.model.log_lambda_ent)
|
| 146 |
+
if hasattr(self.model, 'log_lambda_ent')
|
| 147 |
+
else torch.tensor(0.01)
|
| 148 |
+
)
|
| 149 |
+
# V5: residual_gate é Parameter do CircularReasoning (shape: n_cycles)
|
| 150 |
+
# Usamos a média para o tracking loss (1 escalar)
|
| 151 |
+
if (hasattr(self.model, 'circular_reasoning')
|
| 152 |
+
and self.model.circular_reasoning is not None):
|
| 153 |
+
init_gate_val = self.model.circular_reasoning.residual_gate.mean()
|
| 154 |
+
else:
|
| 155 |
+
init_gate_val = torch.tensor(0.05)
|
| 156 |
|
| 157 |
# Forward do modelo com T atual (sem hipótese — só validação principal)
|
| 158 |
+
# V5: return_aux=True para capturar entropy_reg
|
| 159 |
+
# (necessário para que λ_ent tenha gradiente via lambda_ent * entropy_reg)
|
| 160 |
self.model.train() # mantém dropout ativo para estimar sharpness real
|
| 161 |
+
y_hat_main, _, aux = self.model(
|
| 162 |
+
x_val, temperature=temp, use_hypothesis=False, return_aux=True
|
| 163 |
+
)
|
| 164 |
loss_val = criterion(y_hat_main, y_val)
|
| 165 |
|
| 166 |
+
# V5: λ_ent * entropy_reg — dá a λ_ent um caminho de gradiente via
|
| 167 |
+
# o termo de regularização de entropia do module_selector (Lema 1)
|
| 168 |
+
# Isto é o "uso real" de λ_ent no treino — deve ser refletido no meta-loss
|
| 169 |
+
entropy_reg = aux.get("entropy_reg", torch.tensor(0.0))
|
| 170 |
+
entropy_loss = lambda_ent_val * entropy_reg
|
| 171 |
+
|
| 172 |
# Proxy de agudeza: ||∇_θ L_val||^2
|
| 173 |
# BUG FIX: create_graph=True causava OOM-Killer (second-order graph dobra RAM)
|
| 174 |
# Correção: usar create_graph=False (first-order approximation)
|
|
|
|
| 205 |
T_pred = torch.exp(self.log_temperature)
|
| 206 |
T_tracking_loss = 0.05 * (T_pred - T_target) ** 2
|
| 207 |
|
| 208 |
+
# V5 NOVO: λ_ent tracking loss — manter perto de 0.01 (default do config)
|
| 209 |
+
# Permite variação mas evita saturação (λ_ent muito grande força entropia
|
| 210 |
+
# máxima = módulos uniformes; muito pequena = colapso para 1 módulo)
|
| 211 |
+
lambda_ent_target = torch.tensor(0.01, device=lambda_ent_val.device)
|
| 212 |
+
lambda_ent_tracking_loss = 0.01 * (lambda_ent_val - lambda_ent_target) ** 2
|
| 213 |
+
|
| 214 |
+
# V5 NOVO: init_gate tracking loss — manter perto de 0.05 (default do config)
|
| 215 |
+
# Aplicado à MÉDIA dos n_cycles gates do CircularReasoning.
|
| 216 |
+
# Permite variação mas evita gate muito grande (quebraria contração W₂)
|
| 217 |
+
init_gate_target = torch.tensor(0.05, device=init_gate_val.device)
|
| 218 |
+
init_gate_tracking_loss = 0.01 * (init_gate_val - init_gate_target) ** 2
|
| 219 |
+
|
| 220 |
+
# Meta-perda: loss_val + entropy_loss (λ_ent path)
|
| 221 |
+
# + tracking losses para T, τ, λ_ent, init_gate
|
| 222 |
+
meta_loss = (
|
| 223 |
+
loss_val
|
| 224 |
+
+ entropy_loss
|
| 225 |
+
+ T_tracking_loss
|
| 226 |
+
+ tau_tracking_loss
|
| 227 |
+
+ lambda_ent_tracking_loss
|
| 228 |
+
+ init_gate_tracking_loss
|
| 229 |
+
)
|
| 230 |
|
| 231 |
+
# Atualiza log_temperature, log_tau, model.log_lambda_ent, residual_gate
|
| 232 |
self.meta_optim.zero_grad()
|
| 233 |
meta_loss.backward()
|
| 234 |
+
# Guarda gradientes antes do step para logging
|
| 235 |
grad_log_T = self.log_temperature.grad.item() if self.log_temperature.grad is not None else 0.0
|
| 236 |
grad_log_tau = self.log_tau.grad.item() if self.log_tau.grad is not None else 0.0
|
| 237 |
+
grad_log_lambda_ent = (
|
| 238 |
+
self.model.log_lambda_ent.grad.item()
|
| 239 |
+
if hasattr(self.model, 'log_lambda_ent')
|
| 240 |
+
and self.model.log_lambda_ent.grad is not None
|
| 241 |
+
else 0.0
|
| 242 |
+
)
|
| 243 |
+
# V5: gradiente do residual_gate (média sobre n_cycles gates)
|
| 244 |
+
if (hasattr(self.model, 'circular_reasoning')
|
| 245 |
+
and self.model.circular_reasoning is not None
|
| 246 |
+
and self.model.circular_reasoning.residual_gate.grad is not None):
|
| 247 |
+
grad_init_gate = float(
|
| 248 |
+
self.model.circular_reasoning.residual_gate.grad.abs().mean().item()
|
| 249 |
+
)
|
| 250 |
+
else:
|
| 251 |
+
grad_init_gate = 0.0
|
| 252 |
self.meta_optim.step()
|
| 253 |
|
| 254 |
# Sincroniza model.tau com o novo valor
|
|
|
|
| 261 |
"meta_loss": float(meta_loss.item()),
|
| 262 |
"T_new": float(torch.exp(self.log_temperature.detach()).item()),
|
| 263 |
"tau_new": float(torch.exp(self.log_tau.detach()).item()),
|
| 264 |
+
"lambda_ent_new": float(lambda_ent_val.detach().item()),
|
| 265 |
+
"init_gate_new": float(init_gate_val.detach().item()),
|
| 266 |
+
"entropy_reg": float(entropy_reg.detach().item() if isinstance(entropy_reg, torch.Tensor) else 0.0),
|
| 267 |
+
"entropy_loss": float(entropy_loss.detach().item() if isinstance(entropy_loss, torch.Tensor) else 0.0),
|
| 268 |
"grad_log_T": grad_log_T,
|
| 269 |
"grad_log_tau": grad_log_tau,
|
| 270 |
+
"grad_log_lambda_ent": grad_log_lambda_ent,
|
| 271 |
+
"grad_init_gate": grad_init_gate,
|
| 272 |
"tau_target": float(tau_target.item()),
|
| 273 |
"T_target": float(T_target.item()),
|
| 274 |
"T_tracking_loss": float(T_tracking_loss.item()),
|
| 275 |
"tau_tracking_loss": float(tau_tracking_loss.item()),
|
| 276 |
+
"lambda_ent_tracking_loss": float(lambda_ent_tracking_loss.item()),
|
| 277 |
+
"init_gate_tracking_loss": float(init_gate_tracking_loss.item()),
|
| 278 |
}
|
v32_upload_report.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
{
|
| 2 |
"version": "V3.2_upload",
|
| 3 |
-
"timestamp":
|
| 4 |
"repo_id": "PowerMachine/BiGRU_T_version",
|
| 5 |
"repo_type": "model",
|
| 6 |
"folder_path": "/home/z/my-project/BiGRU_T_version",
|
|
@@ -29,15 +29,15 @@
|
|
| 29 |
"*/.cache/huggingface",
|
| 30 |
"**/.cache/huggingface/**"
|
| 31 |
],
|
| 32 |
-
"local_files_count":
|
| 33 |
-
"local_files_total_bytes":
|
| 34 |
-
"remote_files_before":
|
| 35 |
-
"remote_files_after":
|
| 36 |
-
"new_files_count":
|
| 37 |
-
"overwritten_count":
|
| 38 |
"critical_files_all_present": true,
|
| 39 |
-
"elapsed_s": 2.
|
| 40 |
-
"commit_info": "https://huggingface.co/PowerMachine/BiGRU_T_version/commit/
|
| 41 |
"env": {
|
| 42 |
"HF_HUB_ENABLE_HF_TRANSFER": "1",
|
| 43 |
"HF_HUB_ETAG_TIMEOUT": "60",
|
|
|
|
| 1 |
{
|
| 2 |
"version": "V3.2_upload",
|
| 3 |
+
"timestamp": 1786016584.7106616,
|
| 4 |
"repo_id": "PowerMachine/BiGRU_T_version",
|
| 5 |
"repo_type": "model",
|
| 6 |
"folder_path": "/home/z/my-project/BiGRU_T_version",
|
|
|
|
| 29 |
"*/.cache/huggingface",
|
| 30 |
"**/.cache/huggingface/**"
|
| 31 |
],
|
| 32 |
+
"local_files_count": 67,
|
| 33 |
+
"local_files_total_bytes": 809581,
|
| 34 |
+
"remote_files_before": 67,
|
| 35 |
+
"remote_files_after": 70,
|
| 36 |
+
"new_files_count": 3,
|
| 37 |
+
"overwritten_count": 67,
|
| 38 |
"critical_files_all_present": true,
|
| 39 |
+
"elapsed_s": 2.9894082950004304,
|
| 40 |
+
"commit_info": "https://huggingface.co/PowerMachine/BiGRU_T_version/commit/0cee0098eee9c53ec2f36dec7ec24e53aa458838",
|
| 41 |
"env": {
|
| 42 |
"HF_HUB_ENABLE_HF_TRANSFER": "1",
|
| 43 |
"HF_HUB_ETAG_TIMEOUT": "60",
|
v5_report.json
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"version": "V5",
|
| 3 |
+
"timestamp": 1786018677.168036,
|
| 4 |
+
"config": {
|
| 5 |
+
"n_multimodal": 10,
|
| 6 |
+
"n_text": 10,
|
| 7 |
+
"epochs": 2,
|
| 8 |
+
"batch_size": 4,
|
| 9 |
+
"max_seq_len": 16,
|
| 10 |
+
"num_layers_hyp": 8,
|
| 11 |
+
"triplet_weight": 0.05,
|
| 12 |
+
"meta_interval": 3,
|
| 13 |
+
"cleanup_interval": 1,
|
| 14 |
+
"triplet_ema_alpha": 0.1
|
| 15 |
+
},
|
| 16 |
+
"v5_improvements": {
|
| 17 |
+
"w8a8_learnable_alpha": true,
|
| 18 |
+
"w8a8_self_contained_trust": true,
|
| 19 |
+
"apply_w8a8_bugfix": true,
|
| 20 |
+
"num_layers_hyp_fixed_8": true,
|
| 21 |
+
"oom_mitigations": [
|
| 22 |
+
"del intermediários explícito",
|
| 23 |
+
"MetaConfigurator a cada 3 steps",
|
| 24 |
+
"gc.collect(0) a cada step",
|
| 25 |
+
"aggressive_cleanup entre epochs",
|
| 26 |
+
"torch.no_grad() em val/eval",
|
| 27 |
+
"alerts limitado a 50 entries"
|
| 28 |
+
],
|
| 29 |
+
"learnable_params": [
|
| 30 |
+
"T (MetaConfigurator)",
|
| 31 |
+
"τ (MetaConfigurator)",
|
| 32 |
+
"λ_ent (MetaConfigurator + treino)",
|
| 33 |
+
"init_gate (residual_gate, MetaConfigurator + treino)",
|
| 34 |
+
"α smoothquant (STE, em SmoothQuantW8A8)",
|
| 35 |
+
"module_logits (treino)",
|
| 36 |
+
"todos os pesos W (AdamW)"
|
| 37 |
+
]
|
| 38 |
+
},
|
| 39 |
+
"xeon_env": {
|
| 40 |
+
"OMP_NUM_THREADS": "2",
|
| 41 |
+
"MKL_NUM_THREADS": "2",
|
| 42 |
+
"ONEDNN_MAX_CPU_ISA": "AMX_INT8",
|
| 43 |
+
"IPEX": true,
|
| 44 |
+
"CPU_ISA": "AMX",
|
| 45 |
+
"torch_threads": 2
|
| 46 |
+
},
|
| 47 |
+
"result": {
|
| 48 |
+
"n_steps": 6,
|
| 49 |
+
"n_epochs": 2,
|
| 50 |
+
"final_train_loss": 9.665669441223145,
|
| 51 |
+
"final_val_loss": 9.436580848693847,
|
| 52 |
+
"final_perplexity": 12538.771572889684,
|
| 53 |
+
"final_overfitting_gap": 0.006072171529135417,
|
| 54 |
+
"final_eval_benchmark": 0.375,
|
| 55 |
+
"avg_tokens_per_sec": 29.505289440522088,
|
| 56 |
+
"avg_tflops_per_cpu": 0.00010685671030126807,
|
| 57 |
+
"avg_time_per_step_ms": 2553.5807878330465,
|
| 58 |
+
"avg_grad_norm": 10.784179131189982,
|
| 59 |
+
"avg_cpu_pct": 121.38333333333333,
|
| 60 |
+
"final_rss_mb": 922.49609375,
|
| 61 |
+
"max_rss_mb": 922.49609375,
|
| 62 |
+
"rss_trend": "stable",
|
| 63 |
+
"n_alerts": 0,
|
| 64 |
+
"alerts": [],
|
| 65 |
+
"alert_types": [],
|
| 66 |
+
"meta_history": [
|
| 67 |
+
{
|
| 68 |
+
"T": 1.0100501775741577,
|
| 69 |
+
"tau": 1.0100501775741577,
|
| 70 |
+
"lambda_ent": 0.009999999776482582,
|
| 71 |
+
"init_gate": 0.047456394881010056,
|
| 72 |
+
"meta_loss": 17.669658660888672,
|
| 73 |
+
"sharpness": 71.65159606933594
|
| 74 |
+
},
|
| 75 |
+
{
|
| 76 |
+
"T": 1.02021062374115,
|
| 77 |
+
"tau": 1.0202070474624634,
|
| 78 |
+
"lambda_ent": 0.009900504723191261,
|
| 79 |
+
"init_gate": 0.02058723010122776,
|
| 80 |
+
"meta_loss": 18.058149337768555,
|
| 81 |
+
"sharpness": 109.38550567626953
|
| 82 |
+
}
|
| 83 |
+
],
|
| 84 |
+
"elapsed_s": 21.375046549000217,
|
| 85 |
+
"n_params": 9657632,
|
| 86 |
+
"hyp_T_params": 1795914,
|
| 87 |
+
"num_layers_hyp": 8,
|
| 88 |
+
"n_w8a8_layers": 80,
|
| 89 |
+
"final_learnable_params": {
|
| 90 |
+
"T": 1.02021062374115,
|
| 91 |
+
"tau": 1.0202070474624634,
|
| 92 |
+
"lambda_ent": 0.009802023880183697,
|
| 93 |
+
"init_gate": 0.05,
|
| 94 |
+
"residual_gate": [
|
| 95 |
+
0.005989676807075739,
|
| 96 |
+
0.006018564570695162,
|
| 97 |
+
0.006039654836058617
|
| 98 |
+
],
|
| 99 |
+
"module_logits": [
|
| 100 |
+
-0.22852112352848053,
|
| 101 |
+
-0.10588397085666656,
|
| 102 |
+
0.12294353544712067,
|
| 103 |
+
-0.22121863067150116,
|
| 104 |
+
0.41890066862106323,
|
| 105 |
+
-0.01949109323322773,
|
| 106 |
+
-0.08710632473230362,
|
| 107 |
+
0.05125897005200386
|
| 108 |
+
]
|
| 109 |
+
},
|
| 110 |
+
"final_bigru_diversity": {
|
| 111 |
+
"mean_cosine_diversity": 0.7214401364326477,
|
| 112 |
+
"mean_parallelism_score": 0.9953877925872803,
|
| 113 |
+
"mean_gate_norm_ratio": 0.09838299453258514,
|
| 114 |
+
"module_level_cosine_diversity": 1.0237387745042466,
|
| 115 |
+
"per_module_parallelism": [
|
| 116 |
+
0.9936217665672302,
|
| 117 |
+
0.9924681186676025,
|
| 118 |
+
0.9992730617523193,
|
| 119 |
+
0.9971094727516174,
|
| 120 |
+
0.9949930310249329,
|
| 121 |
+
0.9948464035987854,
|
| 122 |
+
0.9958065748214722,
|
| 123 |
+
0.9944431781768799
|
| 124 |
+
]
|
| 125 |
+
}
|
| 126 |
+
}
|
| 127 |
+
}
|