PowerMachine commited on
Commit
268754b
·
verified ·
1 Parent(s): 53eff53

Delete models/context_window.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. models/context_window.py +0 -593
models/context_window.py DELETED
@@ -1,593 +0,0 @@
1
- """
2
- Context Window — Janela deslizante com cache KV para sequências longas.
3
-
4
- Implementa três estratégias (ver docs/MATH_ANALYSIS.md seção 3.4):
5
- - Sliding Window Pura
6
- - Sink + Sliding (StreamingLLM) — RECOMENDADO
7
- - Attention Recomputation (sem cache, máxima precisão)
8
-
9
- Para CNN-BiGRU cooperativo, o contexto é gerenciado em modo:
10
- - Encoder (treino/classificação): processa janela inteira, BiGRU bidirecional
11
- - Decoder (geração): cache KV para CausalSelfAttention + GRU unidirecional
12
-
13
- Autor: CNN-BiGRU Project
14
- """
15
- from __future__ import annotations
16
-
17
- import logging
18
- from dataclasses import dataclass, field
19
- from typing import Dict, List, Optional, Tuple
20
-
21
- import torch
22
- import torch.nn as nn
23
-
24
- logger = logging.getLogger(__name__)
25
-
26
-
27
- # ============================================================================
28
- # Configuração
29
- # ============================================================================
30
-
31
- @dataclass
32
- class ContextWindowConfig:
33
- """Configuração da janela de contexto."""
34
- # Comprimento máximo da janela (em tokens)
35
- max_window: int = 512
36
- # Estratégia de evicção: "sliding" | "sink_sliding" | "recompute"
37
- eviction_strategy: str = "sink_sliding"
38
- # Número de tokens "sink" (apenas para sink_sliding) — tipicamente BOS + system prompt
39
- n_sink_tokens: int = 4
40
- # Dimensão do embedding (para alocar cache KV)
41
- embed_dim: int = 256
42
- # Número de cabeças (para cache KV em multi-head attention)
43
- n_heads: int = 4
44
- # Dimensão por cabeça
45
- head_dim: Optional[int] = None # default: embed_dim // n_heads
46
- # Número de camadas (para cache KV multicamada)
47
- n_layers: int = 2
48
- # Device padrão
49
- device: str = "cpu"
50
- # Dtype do cache (None = manter fp32)
51
- dtype: Optional[torch.dtype] = None
52
-
53
-
54
- # ============================================================================
55
- # KV Cache
56
- # ============================================================================
57
-
58
- class KVCache:
59
- """
60
- Cache de pares (Key, Value) para CausalSelfAttention.
61
-
62
- Shape por camada:
63
- K: [n_heads, seq_cached, head_dim]
64
- V: [n_heads, seq_cached, head_dim]
65
-
66
- Em modo batch:
67
- K: [batch, n_heads, seq_cached, head_dim]
68
- V: [batch, n_heads, seq_cached, head_dim]
69
- """
70
-
71
- def __init__(self, n_layers: int, batch_size: int, n_heads: int,
72
- head_dim: int, device: torch.device, dtype: torch.dtype):
73
- self.n_layers = n_layers
74
- self.batch_size = batch_size
75
- self.n_heads = n_heads
76
- self.head_dim = head_dim
77
- self.device = device
78
- self.dtype = dtype
79
- # Pre-alocar listas vazias; preencher lazy no primeiro update
80
- self.keys: List[Optional[torch.Tensor]] = [None] * n_layers
81
- self.values: List[Optional[torch.Tensor]] = [None] * n_layers
82
-
83
- def update(self, layer_idx: int, new_k: torch.Tensor, new_v: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
84
- """
85
- Atualiza o cache da camada layer_idx com novos K, V.
86
-
87
- Args:
88
- layer_idx: índice da camada [0, n_layers)
89
- new_k: [batch, n_heads, seq_new, head_dim]
90
- new_v: [batch, n_heads, seq_new, head_dim]
91
-
92
- Returns:
93
- (cached_k, cached_v): [batch, n_heads, seq_total, head_dim]
94
- """
95
- if not (0 <= layer_idx < self.n_layers):
96
- raise IndexError(f"layer_idx {layer_idx} fora de range [0, {self.n_layers})")
97
-
98
- # Validar shapes
99
- if new_k.dim() != 4 or new_v.dim() != 4:
100
- raise ValueError(
101
- f"new_k e new_v devem ser 4D [batch, n_heads, seq, head_dim]; "
102
- f"recebido {new_k.shape}"
103
- )
104
-
105
- # Converter dtype/device
106
- new_k = new_k.to(device=self.device, dtype=self.dtype)
107
- new_v = new_v.to(device=self.device, dtype=self.dtype)
108
-
109
- if self.keys[layer_idx] is None:
110
- self.keys[layer_idx] = new_k
111
- self.values[layer_idx] = new_v
112
- else:
113
- self.keys[layer_idx] = torch.cat([self.keys[layer_idx], new_k], dim=2)
114
- self.values[layer_idx] = torch.cat([self.values[layer_idx], new_v], dim=2)
115
-
116
- return self.keys[layer_idx], self.values[layer_idx]
117
-
118
- def evict_sliding(self, max_keep: int) -> None:
119
- """
120
- Estratégia sliding window pura: mantém apenas os últimos max_keep tokens.
121
- """
122
- for layer_idx in range(self.n_layers):
123
- if self.keys[layer_idx] is None:
124
- continue
125
- seq = self.keys[layer_idx].size(2)
126
- if seq > max_keep:
127
- start = seq - max_keep
128
- self.keys[layer_idx] = self.keys[layer_idx][:, :, start:, :].contiguous()
129
- self.values[layer_idx] = self.values[layer_idx][:, :, start:, :].contiguous()
130
-
131
- def evict_sink_sliding(self, max_keep: int, n_sink: int) -> None:
132
- """
133
- Estratégia sink + sliding: mantém os primeiros n_sink + últimos (max_keep - n_sink).
134
- """
135
- if n_sink >= max_keep:
136
- logger.warning(
137
- f"n_sink ({n_sink}) >= max_keep ({max_keep}); usando sliding puro"
138
- )
139
- self.evict_sliding(max_keep)
140
- return
141
-
142
- n_sliding = max_keep - n_sink
143
- for layer_idx in range(self.n_layers):
144
- if self.keys[layer_idx] is None:
145
- continue
146
- seq = self.keys[layer_idx].size(2)
147
- if seq > max_keep:
148
- sink_k = self.keys[layer_idx][:, :, :n_sink, :]
149
- sink_v = self.values[layer_idx][:, :, :n_sink, :]
150
- sliding_k = self.keys[layer_idx][:, :, -n_sliding:, :]
151
- sliding_v = self.values[layer_idx][:, :, -n_sliding:, :]
152
- self.keys[layer_idx] = torch.cat([sink_k, sliding_k], dim=2).contiguous()
153
- self.values[layer_idx] = torch.cat([sink_v, sliding_v], dim=2).contiguous()
154
-
155
- def reset(self) -> None:
156
- """Limpa o cache completamente."""
157
- self.keys = [None] * self.n_layers
158
- self.values = [None] * self.n_layers
159
-
160
- def get_seq_len(self, layer_idx: int = 0) -> int:
161
- if self.keys[layer_idx] is None:
162
- return 0
163
- return self.keys[layer_idx].size(2)
164
-
165
- def total_tokens(self) -> int:
166
- return max(self.get_seq_len(i) for i in range(self.n_layers))
167
-
168
-
169
- # ============================================================================
170
- # Context Window Manager
171
- # ============================================================================
172
-
173
- class ContextWindowManager:
174
- """
175
- Gerencia a janela de contexto aplicando a política de evicção configurada.
176
- """
177
-
178
- def __init__(self, config: ContextWindowConfig):
179
- self.config = config
180
- self.head_dim = config.head_dim or (config.embed_dim // config.n_heads)
181
- if config.embed_dim % config.n_heads != 0:
182
- raise ValueError(
183
- f"embed_dim ({config.embed_dim}) deve ser divisível por "
184
- f"n_heads ({config.n_heads})"
185
- )
186
- self.kv_cache: Optional[KVCache] = None
187
- # Sequência de tokens brutos (para recomputação se necessário)
188
- self.token_history: List[torch.Tensor] = []
189
-
190
- # ----------------------------------------------------------------------
191
- # Inicialização
192
- # ----------------------------------------------------------------------
193
-
194
- def init_cache(self, batch_size: int, device: torch.device,
195
- dtype: Optional[torch.dtype] = None) -> KVCache:
196
- """Cria novo cache KV para uma sessão de geração."""
197
- dtype = dtype or self.config.dtype or torch.float32
198
- self.kv_cache = KVCache(
199
- n_layers=self.config.n_layers,
200
- batch_size=batch_size,
201
- n_heads=self.config.n_heads,
202
- head_dim=self.head_dim,
203
- device=device,
204
- dtype=dtype,
205
- )
206
- self.token_history = []
207
- return self.kv_cache
208
-
209
- # ----------------------------------------------------------------------
210
- # Adicionar tokens
211
- # ----------------------------------------------------------------------
212
-
213
- def append_tokens(self, token_ids: torch.Tensor) -> torch.Tensor:
214
- """
215
- Adiciona tokens ao histórico e retorna a janela ativa.
216
-
217
- Args:
218
- token_ids: [batch, seq_new]
219
-
220
- Returns:
221
- window: [batch, seq_window] — tokens na janela ativa após evicção
222
- """
223
- if token_ids.dim() != 2:
224
- raise ValueError(f"token_ids deve ser [batch, seq]; recebido {token_ids.shape}")
225
-
226
- # Para simplificação, mantemos histórico apenas do batch[0]
227
- # (geração autoregressiva tipicamente batch=1)
228
- if token_ids.size(0) > 1:
229
- logger.warning("ContextWindowManager.append_tokens: batch>1, histórico rastreia apenas batch[0]")
230
-
231
- self.token_history.append(token_ids)
232
-
233
- # Concatenar todo o histórico
234
- full = torch.cat(self.token_history, dim=1)
235
-
236
- # Aplicar evicção se necessário
237
- return self._evict_tokens(full)
238
-
239
- def _evict_tokens(self, full_seq: torch.Tensor) -> torch.Tensor:
240
- """Aplica política de evicção à sequência completa."""
241
- seq_len = full_seq.size(1)
242
- max_keep = self.config.max_window
243
-
244
- if seq_len <= max_keep:
245
- return full_seq
246
-
247
- strategy = self.config.eviction_strategy
248
- if strategy == "sliding":
249
- return full_seq[:, -max_keep:]
250
- elif strategy == "sink_sliding":
251
- n_sink = self.config.n_sink_tokens
252
- n_sliding = max_keep - n_sink
253
- sink = full_seq[:, :n_sink]
254
- sliding = full_seq[:, -n_sliding:]
255
- return torch.cat([sink, sliding], dim=1)
256
- elif strategy == "recompute":
257
- # Manter tudo (atenção recomputada)
258
- return full_seq
259
- else:
260
- logger.warning(f"Estratégia de evicção desconhecida: {strategy}; usando sliding")
261
- return full_seq[:, -max_keep:]
262
-
263
- # ----------------------------------------------------------------------
264
- # Evict cache KV
265
- # ----------------------------------------------------------------------
266
-
267
- def evict_cache(self) -> None:
268
- """Aplica a política de evicção ao cache KV."""
269
- if self.kv_cache is None:
270
- return
271
- strategy = self.config.eviction_strategy
272
- max_keep = self.config.max_window
273
- if strategy == "sliding":
274
- self.kv_cache.evict_sliding(max_keep)
275
- elif strategy == "sink_sliding":
276
- self.kv_cache.evict_sink_sliding(max_keep, self.config.n_sink_tokens)
277
- elif strategy == "recompute":
278
- self.kv_cache.reset()
279
- else:
280
- self.kv_cache.evict_sliding(max_keep)
281
-
282
- # ----------------------------------------------------------------------
283
- # Reset
284
- # ----------------------------------------------------------------------
285
-
286
- def reset(self) -> None:
287
- """Reseta todo o estado (cache + histórico)."""
288
- if self.kv_cache is not None:
289
- self.kv_cache.reset()
290
- self.kv_cache = None
291
- self.token_history = []
292
-
293
- # ----------------------------------------------------------------------
294
- # Info
295
- # ----------------------------------------------------------------------
296
-
297
- def get_info(self) -> Dict:
298
- """Retorna informações sobre o estado atual."""
299
- cache_len = self.kv_cache.total_tokens() if self.kv_cache else 0
300
- history_len = sum(t.size(1) for t in self.token_history)
301
- return {
302
- "max_window": self.config.max_window,
303
- "strategy": self.config.eviction_strategy,
304
- "n_sink": self.config.n_sink_tokens,
305
- "cache_tokens": cache_len,
306
- "history_tokens": history_len,
307
- "active": self.kv_cache is not None,
308
- }
309
-
310
-
311
- # ============================================================================
312
- # Helpers
313
- # ============================================================================
314
-
315
- def make_context_window(
316
- max_window: int = 512,
317
- strategy: str = "sink_sliding",
318
- n_sink: int = 4,
319
- embed_dim: int = 256,
320
- n_heads: int = 4,
321
- n_layers: int = 2,
322
- device: str = "cpu",
323
- ) -> ContextWindowManager:
324
- """Factory para criar ContextWindowManager com defaults sensatos."""
325
- config = ContextWindowConfig(
326
- max_window=max_window,
327
- eviction_strategy=strategy,
328
- n_sink_tokens=n_sink,
329
- embed_dim=embed_dim,
330
- n_heads=n_heads,
331
- head_dim=embed_dim // n_heads,
332
- n_layers=n_layers,
333
- device=device,
334
- )
335
- return ContextWindowManager(config)
336
-
337
-
338
- # ============================================================================
339
- # Contexto de 1M Tokens — Chunked / Ring Attention
340
- # ============================================================================
341
-
342
- @dataclass
343
- class LongContextConfig:
344
- """Configuração para contexto de até 1M tokens.
345
-
346
- Estratégias suportadas:
347
- - "chunked": divide a sequência em chunks de tamanho chunk_size,
348
- processa cada chunk separadamente, mantém um cache KV global.
349
- - "ring": Ring Attention — distribui chunks entre dispositivos/GPUs
350
- (apenas para multi-GPU; em CPU faz fallback para chunked).
351
- - "sink_sliding_long": sink + sliding com window grande (até max_window).
352
- - "hybrid": usa chunked para encoder, sink_sliding para decoder.
353
-
354
- Para 1M tokens:
355
- - Memória necessária para cache KV FP32:
356
- n_layers * n_heads * head_dim * 1e6 * 2 (K+V) * 4 bytes
357
- = n_layers * d_model * 1e6 * 8 bytes
358
- Ex: 6 layers * 256 d * 1e6 * 8 = ~12 GB (inviável em CPU)
359
- - Com chunked attention (chunk_size=8192), a memória por chunk é:
360
- n_layers * d_model * 8192 * 8 = ~16 MB (viável)
361
- - Ring Attention distribui entre N dispositivos: memória / N
362
- """
363
- max_window: int = 1_000_000 # 1M tokens
364
- chunk_size: int = 8192 # tamanho do chunk para processamento
365
- strategy: str = "chunked" # "chunked" | "ring" | "sink_sliding_long" | "hybrid"
366
- n_sink_tokens: int = 16 # tokens sink (BOS + system)
367
- n_sliding_tokens: int = 8192 # sliding window para sink_sliding_long
368
- embed_dim: int = 256
369
- n_heads: int = 4
370
- head_dim: Optional[int] = None
371
- n_layers: int = 2
372
- device: str = "cpu"
373
- dtype: Optional[torch.dtype] = None
374
- # Ring attention
375
- n_ring_devices: int = 1 # 1 = CPU single, >1 = multi-GPU
376
- # Overlap communication (apenas para ring)
377
- overlap_comm: bool = True
378
- # Cache para chunks processados (evita recomputação)
379
- use_chunk_cache: bool = True
380
- chunk_cache_max: int = 128 # máximo de chunks em cache
381
-
382
-
383
- class LongContextManager:
384
- """Gerencia contexto de até 1M tokens via chunked / ring attention.
385
-
386
- Para 1M tokens, a estratégia padrão é:
387
- 1. Dividir a sequência em chunks de `chunk_size` tokens
388
- 2. Processar cada chunk com atenção local (intra-chunk)
389
- 3. Comunicar informações entre chunks via:
390
- a. Tokens de "boundary" (compartilhados entre chunks adjacentes)
391
- b. Sumarização hierárquica (chunk summary -> global context)
392
- 4. Manter cache KV apenas para o chunk atual + sink tokens
393
-
394
- Em modo "ring" (multi-GPU), cada device processa um chunk e os resultados
395
- são propagados em anel (P2P communication).
396
-
397
- Para CNN-BiGRU:
398
- - O BiGRU bidirecional não funciona bem em modo chunked puro (perde
399
- dependências backward entre chunks). Solução: processar cada chunk
400
- em ambas as direções, comunicar estado hidden entre chunks.
401
- - Em modo decoder (geração), usa sink + sliding puro (cache KV).
402
- """
403
-
404
- def __init__(self, config: LongContextConfig):
405
- self.config = config
406
- self.head_dim = config.head_dim or (config.embed_dim // config.n_heads)
407
- if config.embed_dim % config.n_heads != 0:
408
- raise ValueError(
409
- f"embed_dim ({config.embed_dim}) deve ser divisível por "
410
- f"n_heads ({config.n_heads})"
411
- )
412
- self.kv_cache: Optional[KVCache] = None
413
- self.token_history: List[torch.Tensor] = []
414
- # Chunk cache: cache de sumários de chunks processados
415
- self.chunk_summaries: List[torch.Tensor] = []
416
- # Posições dos tokens para positional encoding
417
- self._global_position_offset = 0
418
-
419
- def init_cache(
420
- self,
421
- batch_size: int,
422
- device: torch.device,
423
- dtype: Optional[torch.dtype] = None,
424
- ) -> KVCache:
425
- """Cria novo cache KV para sessão de geração longa."""
426
- dtype = dtype or self.config.dtype or torch.float32
427
- self.kv_cache = KVCache(
428
- n_layers=self.config.n_layers,
429
- batch_size=batch_size,
430
- n_heads=self.config.n_heads,
431
- head_dim=self.head_dim,
432
- device=device,
433
- dtype=dtype,
434
- )
435
- self.token_history = []
436
- self.chunk_summaries = []
437
- self._global_position_offset = 0
438
- return self.kv_cache
439
-
440
- def append_tokens_chunked(
441
- self,
442
- token_ids: torch.Tensor,
443
- ) -> Dict[str, torch.Tensor]:
444
- """Adiciona tokens usando estratégia chunked.
445
-
446
- Args:
447
- token_ids: [batch, seq_new]
448
-
449
- Returns:
450
- dict com:
451
- active_window: [batch, seq_active] tokens ativos
452
- chunk_index: int — índice do chunk atual
453
- is_new_chunk: bool — True se iniciou novo chunk
454
- n_chunks_total: int — total de chunks processados
455
- """
456
- if token_ids.dim() != 2:
457
- raise ValueError(f"token_ids deve ser [batch, seq]; recebido {token_ids.shape}")
458
-
459
- self.token_history.append(token_ids)
460
- full = torch.cat(self.token_history, dim=1)
461
- total_len = full.size(1)
462
-
463
- # Verificar se ultrapassou chunk boundary
464
- chunk_size = self.config.chunk_size
465
- n_chunks = (total_len + chunk_size - 1) // chunk_size
466
- is_new_chunk = len(self.chunk_summaries) < n_chunks
467
-
468
- # Janela ativa: último chunk + sink tokens
469
- n_sink = self.config.n_sink_tokens
470
- n_sliding = self.config.n_sliding_tokens or chunk_size
471
-
472
- # Sempre manter sink + últimos n_sliding tokens
473
- if total_len > n_sink + n_sliding:
474
- sink = full[:, :n_sink]
475
- sliding = full[:, -n_sliding:]
476
- active = torch.cat([sink, sliding], dim=1)
477
- else:
478
- active = full
479
-
480
- return {
481
- "active_window": active,
482
- "chunk_index": n_chunks - 1,
483
- "is_new_chunk": is_new_chunk,
484
- "n_chunks_total": n_chunks,
485
- "total_tokens": total_len,
486
- }
487
-
488
- def append_tokens(
489
- self,
490
- token_ids: torch.Tensor,
491
- ) -> torch.Tensor:
492
- """Alias para compatibilidade — retorna apenas a janela ativa."""
493
- result = self.append_tokens_chunked(token_ids)
494
- return result["active_window"]
495
-
496
- def add_chunk_summary(self, summary: torch.Tensor) -> None:
497
- """Adiciona um sumário de chunk (para uso em atenção hierárquica).
498
-
499
- Args:
500
- summary: [batch, d] sumário do chunk processado
501
- """
502
- if self.config.use_chunk_cache:
503
- self.chunk_summaries.append(summary.detach())
504
- # Limitar tamanho do cache
505
- if len(self.chunk_summaries) > self.config.chunk_cache_max:
506
- # Remover o mais antigo (FIFO)
507
- self.chunk_summaries.pop(0)
508
-
509
- def get_global_context(self) -> Optional[torch.Tensor]:
510
- """Retorna o contexto global agregado dos chunk summaries.
511
-
512
- Returns:
513
- [batch, d] ou None se não houver summaries
514
- """
515
- if not self.chunk_summaries:
516
- return None
517
- # Mean pooling sobre os summaries
518
- stacked = torch.stack(self.chunk_summaries, dim=1) # [B, n_chunks, d]
519
- return stacked.mean(dim=1)
520
-
521
- def evict_cache(self) -> None:
522
- """Aplica política de evicção ao cache KV."""
523
- if self.kv_cache is None:
524
- return
525
- strategy = self.config.strategy
526
- max_keep = self.config.n_sliding_tokens or self.config.chunk_size
527
- if strategy in ("chunked", "ring", "hybrid"):
528
- # Manter sink + últimos n_sliding_tokens
529
- self.kv_cache.evict_sink_sliding(max_keep, self.config.n_sink_tokens)
530
- elif strategy == "sink_sliding_long":
531
- self.kv_cache.evict_sink_sliding(max_keep, self.config.n_sink_tokens)
532
- else:
533
- self.kv_cache.evict_sliding(max_keep)
534
-
535
- def reset(self) -> None:
536
- """Reseta todo o estado."""
537
- if self.kv_cache is not None:
538
- self.kv_cache.reset()
539
- self.kv_cache = None
540
- self.token_history = []
541
- self.chunk_summaries = []
542
- self._global_position_offset = 0
543
-
544
- def get_info(self) -> Dict:
545
- """Retorna informações sobre o estado atual."""
546
- cache_len = self.kv_cache.total_tokens() if self.kv_cache else 0
547
- history_len = sum(t.size(1) for t in self.token_history)
548
- return {
549
- "max_window": self.config.max_window,
550
- "strategy": self.config.strategy,
551
- "chunk_size": self.config.chunk_size,
552
- "n_sink": self.config.n_sink_tokens,
553
- "n_sliding": self.config.n_sliding_tokens,
554
- "cache_tokens": cache_len,
555
- "history_tokens": history_len,
556
- "n_chunks_processed": len(self.chunk_summaries),
557
- "active": self.kv_cache is not None,
558
- "supports_1m_tokens": self.config.max_window >= 1_000_000,
559
- }
560
-
561
-
562
- def make_long_context_window(
563
- max_window: int = 1_000_000,
564
- strategy: str = "chunked",
565
- chunk_size: int = 8192,
566
- embed_dim: int = 256,
567
- n_heads: int = 4,
568
- n_layers: int = 2,
569
- device: str = "cpu",
570
- ) -> LongContextManager:
571
- """Factory para LongContextManager com suporte a 1M tokens."""
572
- config = LongContextConfig(
573
- max_window=max_window,
574
- strategy=strategy,
575
- chunk_size=chunk_size,
576
- embed_dim=embed_dim,
577
- n_heads=n_heads,
578
- head_dim=embed_dim // n_heads,
579
- n_layers=n_layers,
580
- device=device,
581
- )
582
- return LongContextManager(config)
583
-
584
-
585
- __all__ = [
586
- "ContextWindowConfig",
587
- "KVCache",
588
- "ContextWindowManager",
589
- "make_context_window",
590
- "LongContextConfig",
591
- "LongContextManager",
592
- "make_long_context_window",
593
- ]