BiGRU_T_version / src /bigru_t /quantization /quantized_linear.py
PowerMachine's picture
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
cbf308e verified
Raw
History Blame Contribute Delete
6.36 kB
"""quantized_linear.py — QuantizedLinear + apply_w8a8 (V5 — bug fix crítico).
═══════════════════════════════════════════════════════════════════════════════
V5 — BUG FIX CRÍTICO: apply_w8a8 não substituía módulos
═══════════════════════════════════════════════════════════════════════════════
BUG (V1-V4):
Em V1-V4, `apply_w8a8` era usado via `model.apply(apply_w8a8)`. Porém,
`nn.Module.apply(fn)` chama `fn(module)` e DESCARTA o valor retornado.
A função retornava `QuantizedLinear(...)` mas o módulo original não era
substituído — apenas a função era chamada (sem efeito).
Resultado: W8A8 NUNCA era aplicado em V1-V4. O modelo usava FP32 puro
em todas as camadas Linear, desperdiçando ~4x mais memória que o necessário
e sem obter o benefício do ruído de quantização (Lema 3).
FIX (V5):
Substituir `model.apply(apply_w8a8)` por `apply_w8a8_to_model(model)` que
percorre recursivamente `model._modules` e substitui `nn.Linear` por
`QuantizedLinear` (ou `SmoothQuantW8A8` se preferir V5).
Para manter compatibilidade com V1-V4, `apply_w8a8(module)` ainda existe
mas agora é chamado recursivamente por `apply_w8a8_to_model`.
Implementa o Lema 3 (Cancelamento de ruído de quantização):
A quantização W8A8 (pesos e ativações em 8 bits) é simulada no forward
via fake quantization (round + clamp + dequant). O backward usa Straight-
Through Estimator (STE) — o gradiente passa direto pela operação de round.
"""
from __future__ import annotations
import torch
import torch.nn as nn
import torch.nn.functional as F
def quantize_tensor(x: torch.Tensor, num_bits: int = 8) -> torch.Tensor:
"""Quantização simétrica uniforme (fake quant).
Args:
x: tensor a quantizar
num_bits: nº de bits (default 8 → range [-128, 127])
Returns:
x_deq: tensor dequantizado (mesmo shape, mesmo dtype)
"""
if x is None:
return None
qmin = -(2 ** (num_bits - 1))
qmax = 2 ** (num_bits - 1) - 1
# Scale por tensor (não por canal) — simplificação
scale = x.abs().max() / qmax
if scale < 1e-8:
scale = torch.tensor(1.0, dtype=x.dtype, device=x.device)
# Round + clamp (STE backward — grad passa direto)
x_q = torch.round(x / scale).clamp(qmin, qmax)
x_deq = x_q * scale
return x_deq
class QuantizedLinear(nn.Linear):
"""Camada Linear com quantização W8A8 (fake quant durante treino).
No forward:
q_weight = quantize_tensor(self.weight, 8) # W8
q_input = quantize_tensor(input, 8) # A8
output = F.linear(q_input, q_weight, self.bias)
O gradiente flui através do STE (round é não-diferenciável, mas o
autograd do PyTorch propaga o gradiente via x_deq = x_q * scale onde
x_q depende de round(x/scale) que tem gradiente zero — o STE substitui
esse gradiente por 1).
Args:
in_features, out_features, bias: iguais ao nn.Linear
num_bits: nº de bits (default 8)
"""
def __init__(self, in_features: int, out_features: int, bias: bool = True, num_bits: int = 8):
super().__init__(in_features, out_features, bias)
self.num_bits = num_bits
def forward(self, input: torch.Tensor) -> torch.Tensor:
# Quantização dos pesos (W8)
q_weight = quantize_tensor(self.weight, self.num_bits)
# Quantização da ativação de entrada (A8)
q_input = quantize_tensor(input, self.num_bits)
output = F.linear(q_input, q_weight, self.bias)
return output
def apply_w8a8(module: nn.Module) -> nn.Module:
"""Substitui uma nn.Linear por QuantizedLinear (mantém pesos).
V5: Agora é uma função que efetivamente substitui o módulo quando
chamada por apply_w8a8_to_model. Para compatibilidade com V1-V4,
ainda pode ser chamada diretamente em um único módulo.
NOTA: Esta função NÃO deve ser usada via `model.apply(apply_w8a8)` pois
`nn.Module.apply` descarta o valor retornado. Use
`apply_w8a8_to_model(model)` em vez disso.
"""
if isinstance(module, nn.Linear) and not isinstance(module, QuantizedLinear):
new_module = QuantizedLinear(module.in_features, module.out_features, module.bias is not None)
# Copiar pesos do módulo original
with torch.no_grad():
new_module.weight.copy_(module.weight)
if module.bias is not None:
new_module.bias.copy_(module.bias)
return new_module
return module
def apply_w8a8_to_model(model: nn.Module) -> int:
"""Substitui recursivamente todas as nn.Linear por QuantizedLinear.
V5: Implementação correta — percorre `model._modules` recursivamente
e substitui in-place. Retorna o número de camadas substituídas.
Diferentemente de `model.apply(apply_w8a8)` (que não funciona porque
`apply` descarta o retorno), esta função modifica o modelo in-place.
Args:
model: modelo a ter as Linears substituídas
Returns:
n_substituidas: número de camadas Linear substituídas
Uso:
from bigru_t.quantization import apply_w8a8_to_model
n = apply_w8a8_to_model(model)
print(f"{n} camadas Linear substituídas por QuantizedLinear")
"""
n_substituidas = 0
# Lista de (parent_module, child_name) para substituir
# Não podemos substituir durante a iteração, então coletamos primeiro
to_replace = []
for name, module in model.named_modules():
for child_name, child in module.named_children():
if isinstance(child, nn.Linear) and not isinstance(child, QuantizedLinear):
to_replace.append((module, child_name))
for parent, child_name in to_replace:
old_linear = getattr(parent, child_name)
new_linear = apply_w8a8(old_linear)
setattr(parent, child_name, new_linear)
n_substituidas += 1
return n_substituidas
__all__ = [
"quantize_tensor",
"QuantizedLinear",
"apply_w8a8",
"apply_w8a8_to_model",
]