Packages
Tensor library for Gleam/BEAM with a pure Gleam API, zero-copy views, and optional native acceleration
Retired package: Release invalid
Current section
Files
Jump to
Current section
Files
src/viva_tensor/awq.gleam
//// AWQ (Activation-aware Weight Quantization)
////
//// MLSys 2024 BEST PAPER AWARD!
//// https://arxiv.org/abs/2306.00978
////
//// INSIGHT PRINCIPAL:
//// Apenas ~1% dos pesos são "salientes" - identificados pela
//// magnitude das ATIVAÇÕES, não dos pesos!
////
//// ALGORITMO:
//// 1. Coletar estatísticas de ativação (calibration)
//// 2. Identificar canais salientes (alta ativação média)
//// 3. Escalar canais salientes PARA CIMA antes de quantizar
//// 4. Escalar ativações de entrada PARA BAIXO (matematicamente equivalente)
////
//// RESULTADO: Mesma compressão, MUITO menos erro!
////
//// Implementação: MIT-HAN Lab + AutoAWQ
import gleam/float
import gleam/int
import gleam/io
import gleam/list
import viva_tensor/tensor.{type Tensor, Tensor}
// ============================================================================
// TIPOS
// ============================================================================
/// Configuração AWQ
pub type AWQConfig {
AWQConfig(
/// Bits de quantização (4 é padrão)
bits: Int,
/// Tamanho do grupo para scales
group_size: Int,
/// Expoente alpha para scaling (0.5 é típico)
alpha: Float,
/// Usar zero-point (assimétrico)
zero_point: Bool,
)
}
/// Scales AWQ computados
pub type AWQScales {
AWQScales(
/// Scales por canal (multiplicador de pesos)
weight_scales: List(Float),
/// Estatísticas de ativação usadas
activation_stats: List(Float),
/// Alpha usado
alpha: Float,
)
}
/// Tensor quantizado com AWQ
pub type AWQTensor {
AWQTensor(
/// Pesos quantizados (INT4)
quantized_weights: List(Int),
/// Scales AWQ por canal
awq_scales: AWQScales,
/// Scales de quantização por grupo
quant_scales: List(Float),
/// Zero-points (se assimétrico)
zero_points: List(Int),
/// Shape original
shape: List(Int),
/// Memória em bytes
memory_bytes: Int,
)
}
/// Configuração padrão AWQ
pub fn default_config() -> AWQConfig {
AWQConfig(bits: 4, group_size: 128, alpha: 0.5, zero_point: False)
}
// ============================================================================
// CALIBRAÇÃO - Coleta Estatísticas de Ativação
// ============================================================================
/// Coleta estatísticas de ativação de um batch de calibração
/// Retorna média absoluta por canal
pub fn collect_activation_stats(
activations_batch: List(List(Float)),
) -> List(Float) {
case activations_batch {
[] -> []
[first, ..] -> {
let num_channels = list.length(first)
// Inicializa somas
let initial = list.repeat(0.0, num_channels)
// Acumula abs(activation) por canal
let sums =
list.fold(activations_batch, initial, fn(acc, activation) {
list.map2(acc, activation, fn(sum, act) {
sum +. float.absolute_value(act)
})
})
// Divide pelo número de amostras
let num_samples = int.to_float(list.length(activations_batch))
list.map(sums, fn(sum) { sum /. num_samples })
}
}
}
// ============================================================================
// AWQ SCALING - O Algoritmo Principal
// ============================================================================
/// Computa scales AWQ baseado nas estatísticas de ativação
/// scale[i] = activation_stat[i] ^ alpha
pub fn compute_awq_scales(
activation_stats: List(Float),
alpha: Float,
) -> AWQScales {
let weight_scales =
list.map(activation_stats, fn(stat) {
// Evita scale zero
let safe_stat = case stat >. 0.0 {
True -> stat
False -> 1.0
}
float_power(safe_stat, alpha)
})
AWQScales(
weight_scales: weight_scales,
activation_stats: activation_stats,
alpha: alpha,
)
}
/// Aplica transformação equivalente aos pesos
/// W' = W * diag(s)
/// Isso escala canais salientes PARA CIMA
pub fn apply_weight_transform(
weights: List(List(Float)),
scales: AWQScales,
) -> List(List(Float)) {
list.map(weights, fn(row) {
list.map2(row, scales.weight_scales, fn(w, s) { w *. s })
})
}
/// Aplica transformação inversa às ativações
/// X' = X * diag(1/s)
/// Isso compensa o scaling dos pesos
pub fn apply_activation_transform(
activations: List(Float),
scales: AWQScales,
) -> List(Float) {
list.map2(activations, scales.weight_scales, fn(x, s) {
case s >. 0.0 {
True -> x /. s
False -> x
}
})
}
// ============================================================================
// QUANTIZAÇÃO COM AWQ
// ============================================================================
/// Quantiza pesos usando AWQ (pipeline completo)
pub fn quantize_awq(
weights: Tensor,
calibration_data: List(List(Float)),
config: AWQConfig,
) -> AWQTensor {
let weight_data = tensor.to_list(weights)
let shape = get_tensor_shape(weights)
// Assume weights é [out_features, in_features]
let #(_out_features, in_features) = case shape {
[o, i] -> #(o, i)
_ -> #(1, list.length(weight_data))
}
// Reshape weights para matriz
let weight_matrix = list.sized_chunk(weight_data, in_features)
// Step 1: Coleta estatísticas de ativação
let activation_stats = collect_activation_stats(calibration_data)
// Step 2: Computa scales AWQ
let awq_scales = compute_awq_scales(activation_stats, config.alpha)
// Step 3: Aplica transformação aos pesos (escala canais salientes)
let transformed_weights = apply_weight_transform(weight_matrix, awq_scales)
// Step 4: Quantização simétrica dos pesos transformados
let flat_transformed = list.flatten(transformed_weights)
let #(quantized, quant_scales) =
symmetric_group_quantize(flat_transformed, config.bits, config.group_size)
// Calcula memória
// - bits/valor para dados
// - 16 bits para cada scale
let num_elements = list.length(flat_transformed)
let num_groups = { num_elements + config.group_size - 1 } / config.group_size
let data_bytes = { num_elements * config.bits + 7 } / 8
let scale_bytes = num_groups * 2
// FP16
let awq_scale_bytes = in_features * 2
// FP16 para AWQ scales
let memory = data_bytes + scale_bytes + awq_scale_bytes
AWQTensor(
quantized_weights: quantized,
awq_scales: awq_scales,
quant_scales: quant_scales,
zero_points: [],
shape: shape,
memory_bytes: memory,
)
}
/// Quantização simétrica por grupos
fn symmetric_group_quantize(
values: List(Float),
bits: Int,
group_size: Int,
) -> #(List(Int), List(Float)) {
let qmax =
float.power(2.0, int.to_float(bits - 1))
|> float_result_to_float(128.0)
|> fn(x) { x -. 1.0 }
let groups = list.sized_chunk(values, group_size)
let #(quantized_groups, scales) =
list.fold(groups, #([], []), fn(acc, group) {
let #(q_acc, s_acc) = acc
// Encontra max abs no grupo
let max_abs =
group
|> list.map(float.absolute_value)
|> list.fold(0.0, float.max)
let scale = case max_abs >. 0.0 {
True -> qmax /. max_abs
False -> 1.0
}
// Quantiza
let quantized =
list.map(group, fn(v) {
let scaled = v *. scale
let clamped = float.clamp(scaled, -1.0 *. qmax, qmax)
float.round(clamped)
})
#(list.append(q_acc, quantized), [scale, ..s_acc])
})
#(quantized_groups, list.reverse(scales))
}
// ============================================================================
// DEQUANTIZAÇÃO
// ============================================================================
/// Dequantiza tensor AWQ
pub fn dequantize_awq(awq: AWQTensor) -> Tensor {
let group_size = case awq.quant_scales {
[] -> list.length(awq.quantized_weights)
_ -> list.length(awq.quantized_weights) / list.length(awq.quant_scales)
}
let groups = list.sized_chunk(awq.quantized_weights, group_size)
// Dequantiza por grupo
let dequantized =
list.index_map(groups, fn(group, idx) {
let scale = get_at_index_float(awq.quant_scales, idx, 1.0)
list.map(group, fn(q) { int.to_float(q) /. scale })
})
|> list.flatten
// Desfaz AWQ transform (divide por scales)
let in_features = case awq.shape {
[_, i] -> i
_ -> 1
}
let weight_matrix = list.sized_chunk(dequantized, in_features)
let restored =
list.map(weight_matrix, fn(row) {
list.map2(row, awq.awq_scales.weight_scales, fn(w, s) {
case s >. 0.0 {
True -> w /. s
False -> w
}
})
})
|> list.flatten
Tensor(data: restored, shape: awq.shape)
}
// ============================================================================
// ANÁLISE DE SALIÊNCIA
// ============================================================================
/// Identifica canais salientes (top-k por ativação)
pub fn identify_salient_channels(
activation_stats: List(Float),
top_percent: Float,
) -> List(Int) {
let n = list.length(activation_stats)
let k =
float.round(int.to_float(n) *. top_percent /. 100.0)
|> int.max(1)
// Ordena por magnitude e pega top-k índices
activation_stats
|> list.index_map(fn(stat, idx) { #(idx, stat) })
|> list.sort(fn(a, b) { float.compare(b.1, a.1) })
// Descending
|> list.take(k)
|> list.map(fn(pair) { pair.0 })
}
// ============================================================================
// BENCHMARK
// ============================================================================
pub fn main() {
benchmark_awq()
}
pub fn benchmark_awq() {
io.println(
"╔══════════════════════════════════════════════════════════════════╗",
)
io.println(
"║ AWQ (Activation-aware Weight Quantization) ║",
)
io.println(
"║ MLSys 2024 BEST PAPER AWARD! ║",
)
io.println(
"╚══════════════════════════════════════════════════════════════════╝\n",
)
io.println("CONCEITO:")
io.println(" - Apenas ~1% dos pesos são 'salientes'")
io.println(" - Identificados pela magnitude das ATIVAÇÕES, não dos pesos!")
io.println(" - Escalar canais salientes PARA CIMA antes de quantizar")
io.println(" - Matematicamente equivalente: W*X = (sW)*(X/s)")
io.println("")
// Simula pesos de uma camada linear [512, 256]
let weights = tensor.random_uniform([512, 256])
// Simula dados de calibração (100 amostras x 256 features)
let calibration_data =
list.range(1, 100)
|> list.map(fn(_) {
tensor.random_uniform([256])
|> tensor.to_list
})
let config = default_config()
io.println("━━━ CALIBRAÇÃO ━━━")
let activation_stats = collect_activation_stats(calibration_data)
io.println(" Amostras de calibração: 100")
io.println(" Features por amostra: 256")
// Analisa saliência
let salient_channels = identify_salient_channels(activation_stats, 1.0)
io.println(
" Canais salientes (top 1%): "
<> int.to_string(list.length(salient_channels)),
)
// Mostra top 5 canais salientes
io.println(" Top 5 canais mais salientes:")
salient_channels
|> list.take(5)
|> list.each(fn(idx) {
let stat = get_at_index_float(activation_stats, idx, 0.0)
io.println(
" Canal " <> int.to_string(idx) <> ": " <> float_to_string(stat),
)
})
io.println("\n━━━ AWQ QUANTIZATION ━━━")
let #(time_awq, awq_tensor) =
timer_tc(fn() { quantize_awq(weights, calibration_data, config) })
let original_bytes = 512 * 256 * 4
// FP32
let ratio =
int.to_float(original_bytes) /. int.to_float(awq_tensor.memory_bytes)
io.println(" Tempo: " <> int.to_string(time_awq / 1000) <> "ms")
io.println(" Original: " <> int.to_string(original_bytes / 1024) <> " KB")
io.println(
" Comprimido: " <> int.to_string(awq_tensor.memory_bytes / 1024) <> " KB",
)
io.println(" Compressão: " <> float_to_string(ratio) <> "x")
// Verifica erro
io.println("\n━━━ VERIFICAÇÃO DE ERRO ━━━")
let decompressed = dequantize_awq(awq_tensor)
let orig_data = tensor.to_list(weights)
let decomp_data = tensor.to_list(decompressed)
let errors =
list.map2(orig_data, decomp_data, fn(o, d) { float.absolute_value(o -. d) })
let mean_error = case errors {
[] -> 0.0
_ -> list.fold(errors, 0.0, float.add) /. int.to_float(list.length(errors))
}
let max_error = list.fold(errors, 0.0, float.max)
io.println(" Erro médio: " <> float_to_string(mean_error))
io.println(" Erro máx: " <> float_to_string(max_error))
// Comparação com quantização sem AWQ
io.println("\n━━━ COMPARAÇÃO: AWQ vs Quantização Normal ━━━")
io.println(" AWQ reduz erro em canais salientes (~1% dos pesos)")
io.println(" Esses canais têm MAIOR impacto na saída")
io.println(" Resultado: mesma compressão, MUITO menos perda de qualidade")
io.println(
"\n╔══════════════════════════════════════════════════════════════════╗",
)
io.println(
"║ POR QUE AWQ VENCEU O MLSYS 2024: ║",
)
io.println(
"║ ║",
)
io.println(
"║ 1. Insight simples mas poderoso: foca nas ativações ║",
)
io.println(
"║ 2. Zero custo em runtime (transformação pré-computada) ║",
)
io.println(
"║ 3. Funciona com qualquer quantização (INT4, INT8, NF4) ║",
)
io.println(
"║ 4. Estado da arte em LLMs quantizados ║",
)
io.println(
"║ ║",
)
io.println(
"║ viva_tensor + AWQ = Máxima precisão com 8x compressão! ║",
)
io.println(
"╚══════════════════════════════════════════════════════════════════╝",
)
}
// ============================================================================
// HELPERS
// ============================================================================
fn get_tensor_shape(t: Tensor) -> List(Int) {
case t {
Tensor(_, shape) -> shape
tensor.StridedTensor(_, shape, _, _) -> shape
}
}
fn get_at_index_float(lst: List(Float), idx: Int, default: Float) -> Float {
case list.drop(lst, idx) {
[first, ..] -> first
[] -> default
}
}
fn float_power(base: Float, exp: Float) -> Float {
case float.power(base, exp) {
Ok(result) -> result
Error(_) -> 1.0
}
}
fn float_result_to_float(r: Result(Float, a), default: Float) -> Float {
case r {
Ok(v) -> v
Error(_) -> default
}
}
fn float_to_string(f: Float) -> String {
let rounded = int.to_float(float.round(f *. 10_000.0)) /. 10_000.0
float.to_string(rounded)
}
@external(erlang, "timer", "tc")
fn timer_tc(f: fn() -> a) -> #(Int, a)