V6.5: 1000 samples (500+500), MTP val+entropy, EWC+W8A8 eval investigation, integrate gru-ring modules
c2992d0 verified Download src/bigru_t/model/token_compress.py from PowerMachine/BiGRU_T_version: direct link, hf CLI and curl.
- Browser
- Download file 14.1 kB
-
https://huggingface.co/PowerMachine/BiGRU_T_version/resolve/main/src/bigru_t/model/token_compress.py
- Command line
-
hf download hf://PowerMachine/BiGRU_T_version/src/bigru_t/model/token_compress.py
-
curl -L -o token_compress.py https://huggingface.co/PowerMachine/BiGRU_T_version/resolve/main/src/bigru_t/model/token_compress.py
14.1 kB
| """ | |
| token_compress — Compressores e indexadores para atenção de 2M tokens. | |
| Pipeline (3 estágios): | |
| 1. ToMe (Token Merging) — Bolya et al., 2023 | |
| Funde tokens similares via matching bipartido soft. Reduz N → N' com | |
| N' = N - r·T (r tokens por camada, T camadas). Custo O(N log N). | |
| 2. KNN Indexer (IVF-like) | |
| Mantém centroides via k-means. Para cada query, busca top-k tokens | |
| no índice (k << N'). Custo O(N' log N') para construir, O(k log N') | |
| por query. | |
| 3. Per-Layer Attention Pattern | |
| Cada camada tem seu próprio padrão de sparsidade, selecionável: | |
| - "full": dense attention clássica (O(N²)) — apenas para N' pequeno | |
| - "window": atenção local em janela w | |
| - "global": tokens especiais (CLS, registradores) attendem a tudo | |
| - "block": atenção em blocos (BLOCKWISE) | |
| O padrão é configurável por camada via lista `patterns`. | |
| Complexidade total: O(N log N + T · k · N') comparado a O(T · N²) do | |
| Transformer denso. Para N=2M, T=24, k=256: ganho ~ 50×. | |
| """ | |
| from __future__ import annotations | |
| import math | |
| from typing import List, Optional, Tuple | |
| import torch | |
| import torch.nn as nn | |
| import torch.nn.functional as F | |
| # --------------------------------------------------------------------------- | |
| # 1. ToMeCompressor | |
| # --------------------------------------------------------------------------- | |
| def _bipartite_soft_matching( | |
| metric: torch.Tensor, | |
| r: int, | |
| ) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]: | |
| """Matching bipartido soft (Bolya et al., 2023 — algoritmo original). | |
| Passos: | |
| 1. Ordena tokens por score (norma do embedding — proxy para PCA[0]). | |
| 2. Separa alternadamente em dois conjuntos A (índices pares) e B | |
| (índices ímpares) — disjuntos, |A| = |B| = N//2. | |
| 3. Para cada i ∈ [0, N//2), forma par candidato (a_i, b_i). | |
| 4. Calcula similaridade s_i = ⟨a_i, b_i⟩. | |
| 5. Seleciona os r pares com maior similaridade para fundir. | |
| Os demais pares (N//2 - r) mantêm ambos os tokens. | |
| metric: (B, N, C) | |
| r: nº de pares a fundir | |
| Retorna: | |
| a_idx: (B, r) — índices de A nos pares fundidos (âncoras) | |
| b_idx: (B, r) — índices de B nos pares fundidos | |
| ungated_idx: (B, N - 2r) — índices dos tokens não fundidos | |
| """ | |
| B, N, C = metric.shape | |
| M = N // 2 # número de pares | |
| if r <= 0 or M < r: | |
| all_idx = torch.arange(N, device=metric.device).unsqueeze(0).expand(B, -1) | |
| return all_idx[:, :0], all_idx[:, :0], all_idx | |
| with torch.no_grad(): | |
| scores = metric.norm(dim=-1) # (B, N) | |
| sorted_idx = scores.argsort(dim=-1, descending=True) # (B, N) | |
| # Alterna pares/ímpares: A = sorted_idx[:, 0::2], B = sorted_idx[:, 1::2]. | |
| a_idx_all = sorted_idx[:, 0:2 * M:2] # (B, M) | |
| b_idx_all = sorted_idx[:, 1:2 * M:2] # (B, M) | |
| # Similaridade entre cada par (a_i, b_i). | |
| a_set = metric.gather(1, a_idx_all.unsqueeze(-1).expand(-1, -1, C)) # (B, M, C) | |
| b_set = metric.gather(1, b_idx_all.unsqueeze(-1).expand(-1, -1, C)) # (B, M, C) | |
| sim = (a_set * b_set).sum(dim=-1) # (B, M) | |
| # Top-r pares mais similares para fundir. | |
| top_r = sim.topk(r, dim=-1).indices # (B, r) | |
| # Máscara de pares fundidos. | |
| merged_mask = torch.zeros(B, M, dtype=torch.bool, device=metric.device) | |
| merged_mask.scatter_(1, top_r, True) | |
| # Índices de pares não-fundidos (constantes no batch). | |
| pair_idx = torch.arange(M, device=metric.device).unsqueeze(0).expand(B, -1) | |
| keep_pair = pair_idx[~merged_mask] # (B*(M-r),) | |
| keep_pair = keep_pair.reshape(B, M - r) | |
| # Token indices. | |
| a_merge = a_idx_all.gather(1, top_r) # (B, r) | |
| b_merge = b_idx_all.gather(1, top_r) # (B, r) | |
| a_keep = a_idx_all.gather(1, keep_pair) # (B, M-r) | |
| b_keep = b_idx_all.gather(1, keep_pair) # (B, M-r) | |
| # Ungated = a_keep ∪ b_keep (total = 2*(M-r) = N - 2r se N par). | |
| ungated = torch.cat([a_keep, b_keep], dim=1) # (B, 2(M-r)) | |
| return a_merge, b_merge, ungated | |
| class ToMeCompressor(nn.Module): | |
| """Token Merging (ToMe) — funde r tokens similares a cada forward. | |
| Reduz N → N - r tokens (mantém dimensão de feature). | |
| """ | |
| def __init__(self, r: int = 64, use_merge_bias: bool = False): | |
| super().__init__() | |
| self.r = r | |
| # Opcional: pequeno gate para mistura ponderada entre par (a, b). | |
| # Por padrão usamos média simples (estilo ToMe original). | |
| self.merge_gate = nn.Parameter(torch.tensor(0.5)) if use_merge_bias else None | |
| def forward(self, X: torch.Tensor, return_aux: bool = False) -> torch.Tensor: | |
| """X: (B, N, D) -> (B, N - r, D). | |
| Quando return_aux=True, retorna também os índices (para debug). | |
| """ | |
| B, N, D = X.shape | |
| M = N // 2 | |
| # Se não há pares suficientes para fundir, retorna X inalterado. | |
| if self.r <= 0 or M < self.r: | |
| return X | |
| a_pair, b_idx, ungated = _bipartite_soft_matching(X, self.r) | |
| # Merge: média do par (a, b). | |
| a_set = X.gather(1, a_pair.unsqueeze(-1).expand(-1, -1, D)) # (B, r, D) | |
| b_set = X.gather(1, b_idx.unsqueeze(-1).expand(-1, -1, D)) # (B, r, D) | |
| if self.merge_gate is not None: | |
| w = torch.sigmoid(self.merge_gate) | |
| merged = w * a_set + (1.0 - w) * b_set | |
| else: | |
| merged = 0.5 * (a_set + b_set) | |
| # Ungated tokens (2*(M-r)). | |
| ungated_set = X.gather(1, ungated.unsqueeze(-1).expand(-1, -1, D)) # (B, 2(M-r), D) | |
| # Concatena: ungated + merged = 2(M-r) + r = 2M - r = N - r (se N par). | |
| out = torch.cat([ungated_set, merged], dim=1) # (B, N - r, D) | |
| if return_aux: | |
| return out, (a_pair, b_idx, ungated) | |
| return out | |
| # --------------------------------------------------------------------------- | |
| # 2. KNNIndexer (IVF-like, sem dependência do FAISS) | |
| # --------------------------------------------------------------------------- | |
| class KNNIndexer(nn.Module): | |
| """Indexador k-NN baseado em k-means (estilo IVF). | |
| Mantém uma lista invertida de tokens por centroide. Query busca | |
| apenas nos centroides mais próximos (n_probe) e retorna top-k tokens | |
| globais. Custo por query: O(n_probe · avg_list_size + n_centroids). | |
| Funciona em batch: para cada query, retorna índices dos top-k tokens | |
| e os pesos de similaridade (cosseno). | |
| """ | |
| def __init__( | |
| self, | |
| dim: int, | |
| n_centroids: int = 256, | |
| n_probe: int = 8, | |
| k: int = 64, | |
| n_iter: int = 5, | |
| ): | |
| super().__init__() | |
| self.dim = dim | |
| self.n_centroids = n_centroids | |
| self.n_probe = n_probe | |
| self.k = k | |
| self.n_iter = n_iter | |
| # Centroides — buffer treinável em modo "build". | |
| self.register_buffer("centroids", torch.randn(n_centroids, dim) * 0.02) | |
| # Listas invertidas (chave = centroide idx, valor = índices de tokens). | |
| # Em runtime mantemos como listas Python — não persistimos em state_dict. | |
| self._lists: List[torch.Tensor] = [] | |
| self._tokens: Optional[torch.Tensor] = None # (N, D) | |
| self._built = False | |
| def build(self, tokens: torch.Tensor) -> None: | |
| """Constrói o índice IVF sobre o banco de tokens. | |
| tokens: (N, D) | |
| """ | |
| N, D = tokens.shape | |
| assert D == self.dim | |
| device = tokens.device | |
| # 1. K-means rápido (n_iter iterações de Lloyd). | |
| if N <= self.n_centroids: | |
| self.centroids = tokens.clone() | |
| self._lists = [torch.tensor([i], device=device) for i in range(N)] | |
| else: | |
| # Inicialização: amostragem aleatória. | |
| perm = torch.randperm(N, device=device)[:self.n_centroids] | |
| self.centroids = tokens[perm].clone() | |
| for _ in range(self.n_iter): | |
| # Atribui cada token ao centroide mais próximo. | |
| d2 = (tokens.unsqueeze(1) - self.centroids.unsqueeze(0)).pow(2).sum(-1) # (N, K) | |
| assign = d2.argmin(dim=-1) # (N,) | |
| # Atualiza centroides. | |
| for k in range(self.n_centroids): | |
| mask = assign == k | |
| if mask.any(): | |
| self.centroids[k] = tokens[mask].mean(dim=0) | |
| # Constói listas invertidas. | |
| d2 = (tokens.unsqueeze(1) - self.centroids.unsqueeze(0)).pow(2).sum(-1) | |
| assign = d2.argmin(dim=-1) | |
| self._lists = [ | |
| (assign == k).nonzero(as_tuple=True)[0].to(device) | |
| for k in range(self.n_centroids) | |
| ] | |
| self._tokens = tokens | |
| self._built = True | |
| def search(self, queries: torch.Tensor, k: Optional[int] = None) -> Tuple[torch.Tensor, torch.Tensor]: | |
| """Busca top-k tokens para cada query. | |
| queries: (B, D) | |
| Retorna: | |
| indices: (B, k) — índices no banco _tokens | |
| weights: (B, k) — similaridade cosseno (em [-1, 1]) | |
| """ | |
| if not self._built: | |
| raise RuntimeError("Índice não construído. Chame build() antes.") | |
| k = k or self.k | |
| B = queries.shape[0] | |
| device = queries.device | |
| # 1. Encontra n_probe centroides mais próximos por query. | |
| q_norm = F.normalize(queries, dim=-1) # (B, D) | |
| c_norm = F.normalize(self.centroids, dim=-1) # (K, D) | |
| sim_c = q_norm @ c_norm.t() # (B, K) | |
| n_probe = min(self.n_probe, self.n_centroids) | |
| top_c = sim_c.topk(n_probe, dim=-1).indices # (B, n_probe) | |
| # 2. Para cada query, agrega tokens das listas invertidas correspondentes. | |
| all_idx_list = [] | |
| all_w_list = [] | |
| for b in range(B): | |
| cand_indices = torch.cat([self._lists[c.item()] for c in top_c[b] if len(self._lists[c.item()]) > 0]) | |
| if cand_indices.numel() == 0: | |
| # Fallback: busca global. | |
| cand_indices = torch.arange(self._tokens.shape[0], device=device) | |
| cand_tokens = self._tokens[cand_indices] # (M, D) | |
| q_b = q_norm[b] # (D,) | |
| sims = (cand_tokens / cand_tokens.norm(dim=-1, keepdim=True).clamp(min=1e-8)) @ q_b # (M,) | |
| kk = min(k, sims.numel()) | |
| topk = sims.topk(kk) | |
| idx = cand_indices[topk.indices] # (kk,) | |
| w = topk.values # (kk,) | |
| # Pad com zeros se necessário. | |
| if kk < k: | |
| pad_idx = torch.zeros(k - kk, dtype=torch.long, device=device) | |
| pad_w = torch.zeros(k - kk, device=device) | |
| idx = torch.cat([idx, pad_idx]) | |
| w = torch.cat([w, pad_w]) | |
| all_idx_list.append(idx) | |
| all_w_list.append(w) | |
| indices = torch.stack(all_idx_list, dim=0) # (B, k) | |
| weights = torch.stack(all_w_list, dim=0) # (B, k) | |
| return indices, weights | |
| # --------------------------------------------------------------------------- | |
| # 3. CompressionPipeline (ToMe + KNN index) | |
| # --------------------------------------------------------------------------- | |
| class CompressionPipeline(nn.Module): | |
| """Pipeline de compressão: ToMe → KNN index → retorna subconjunto + pesos.""" | |
| def __init__( | |
| self, | |
| dim: int, | |
| tome_r: int = 64, | |
| n_centroids: int = 256, | |
| n_probe: int = 8, | |
| k: int = 64, | |
| ): | |
| super().__init__() | |
| self.tome = ToMeCompressor(r=tome_r) | |
| self.indexer = KNNIndexer(dim=dim, n_centroids=n_centroids, n_probe=n_probe, k=k) | |
| self._built = False | |
| def build(self, tokens: torch.Tensor) -> None: | |
| """Constrói índice KNN sobre tokens pré-comprimidos por ToMe.""" | |
| compressed = self.tome(tokens) | |
| self.indexer.build(compressed) | |
| self._built = True | |
| def forward( | |
| self, | |
| queries: torch.Tensor, | |
| keys: torch.Tensor, | |
| ) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]: | |
| """Para cada query, retorna top-k keys + pesos. | |
| queries: (B, N_q, D) | |
| keys: (B, N_k, D) — índice construído para o batch atual. | |
| Em modo "global" (índice precompilado via build), apenas queries são | |
| processadas e keys são ignoradas. | |
| Retorna: | |
| indices: (B, N_q, k) — índices das top-k keys | |
| weights: (B, N_q, k) — similaridades (cosseno) | |
| compressed_keys: (B, N_k - r, D) — keys pós-ToMe (se r > 0) | |
| """ | |
| # Comprime keys via ToMe. | |
| compressed_keys = self.tome(keys) | |
| # Para cada batch element, faz busca local (não usa índice global pré-construído). | |
| B, Nq, D = queries.shape | |
| Nk_c = compressed_keys.shape[1] | |
| k = self.indexer.k | |
| # Similaridade cosseno entre queries e compressed_keys. | |
| q_norm = F.normalize(queries, dim=-1) # (B, Nq, D) | |
| k_norm = F.normalize(compressed_keys, dim=-1) # (B, Nk_c, D) | |
| sim = q_norm @ k_norm.transpose(-1, -2) # (B, Nq, Nk_c) | |
| topk = sim.topk(min(k, Nk_c), dim=-1) # (B, Nq, k) | |
| indices = topk.indices # (B, Nq, k) | |
| weights = topk.values # (B, Nq, k) | |
| return indices, weights, compressed_keys | |