mirror of
https://github.com/hanzoai/gpt.git
synced 2026-10-30 18:35:47 +00:00
469 lines
18 KiB
Python
469 lines
18 KiB
Python
"""
|
|
Dynamic Context Manager for unlimited context length with intelligent token management.
|
|
This implementation allows for theoretically unlimited context with middle token eviction
|
|
when memory constraints are reached.
|
|
"""
|
|
|
|
import torch
|
|
import numpy as np
|
|
import time
|
|
from dataclasses import dataclass
|
|
from typing import List, Dict, Optional, Tuple, Any
|
|
|
|
@dataclass
|
|
class DynamicContextConfig:
|
|
"""Configuration for the dynamic context manager."""
|
|
# Memory management
|
|
max_memory_mb: int = 0 # 0 = auto-detect (80% of available system memory)
|
|
check_memory_every_n_tokens: int = 100 # How often to check memory usage
|
|
|
|
# Token preservation
|
|
begin_tokens_to_preserve: int = 0 # 0 = auto (10% of context)
|
|
end_tokens_to_preserve: int = 0 # 0 = auto (20% of context)
|
|
min_middle_tokens_to_keep: int = 0 # 0 = auto (max(100, 10% of context))
|
|
|
|
# Importance scoring
|
|
recency_weight: float = 1.0 # Weight for token recency in importance scoring
|
|
attention_weight: float = 2.0 # Weight for attention patterns
|
|
content_weight: float = 1.5 # Weight for content-based importance
|
|
min_importance_threshold: float = 0.2 # Minimum threshold to keep a token
|
|
|
|
# Initial buffer sizes
|
|
initial_buffer_size: int = 4096 # Initial size of token buffer
|
|
buffer_growth_factor: float = 2.0 # How much to grow buffer when needed
|
|
|
|
# Experimental
|
|
use_token_compression: bool = False # Use compression instead of eviction
|
|
semantic_landmark_detection: bool = False # Detect and preserve semantic landmarks
|
|
|
|
def __post_init__(self):
|
|
"""Auto-detect settings if needed."""
|
|
if self.max_memory_mb == 0:
|
|
try:
|
|
import psutil
|
|
mem = psutil.virtual_memory()
|
|
self.max_memory_mb = int(mem.available * 0.8 / (1024 * 1024))
|
|
print(f"Auto-configured max_memory_mb to {self.max_memory_mb}")
|
|
except ImportError:
|
|
self.max_memory_mb = 4096 # Default to 4GB if psutil not available
|
|
print(f"psutil not available, defaulting max_memory_mb to {self.max_memory_mb}")
|
|
|
|
|
|
class CircularTokenBuffer:
|
|
"""
|
|
Circular buffer for token storage with efficient access to beginning, middle and end.
|
|
"""
|
|
|
|
def __init__(self, initial_size: int):
|
|
"""Initialize the buffer with given size."""
|
|
self.buffer = [None] * initial_size
|
|
self.head = 0
|
|
self.tail = 0
|
|
self.size = 0
|
|
self.capacity = initial_size
|
|
self.token_metadata = {} # Additional token information
|
|
|
|
def append(self, tokens: List[Any]) -> None:
|
|
"""Add tokens to the buffer, resizing if needed."""
|
|
if not tokens:
|
|
return
|
|
|
|
# Resize if needed
|
|
if self.size + len(tokens) > self.capacity:
|
|
new_capacity = max(self.capacity * 2, self.size + len(tokens))
|
|
self._resize(new_capacity)
|
|
|
|
# Add tokens
|
|
for token in tokens:
|
|
self.buffer[self.tail] = token
|
|
self.tail = (self.tail + 1) % self.capacity
|
|
self.size += 1
|
|
|
|
def get_first(self, n: int) -> List[Any]:
|
|
"""Get first n tokens from buffer."""
|
|
result = []
|
|
idx = self.head
|
|
count = min(n, self.size)
|
|
|
|
for _ in range(count):
|
|
result.append(self.buffer[idx])
|
|
idx = (idx + 1) % self.capacity
|
|
|
|
return result
|
|
|
|
def get_last(self, n: int) -> List[Any]:
|
|
"""Get last n tokens from buffer."""
|
|
result = []
|
|
count = min(n, self.size)
|
|
|
|
if count == 0:
|
|
return result
|
|
|
|
idx = (self.tail - 1) % self.capacity # Start from the last element
|
|
for _ in range(count):
|
|
result.append(self.buffer[idx])
|
|
idx = (idx - 1) % self.capacity
|
|
|
|
return result[::-1] # Reverse to maintain order
|
|
|
|
def get_middle(self, begin_count: int, end_count: int) -> List[Any]:
|
|
"""Get all tokens between first begin_count and last end_count tokens."""
|
|
begin = min(begin_count, self.size)
|
|
end = min(end_count, self.size - begin)
|
|
middle_count = self.size - begin - end
|
|
|
|
if middle_count <= 0:
|
|
return []
|
|
|
|
result = []
|
|
idx = (self.head + begin) % self.capacity
|
|
for _ in range(middle_count):
|
|
result.append(self.buffer[idx])
|
|
idx = (idx + 1) % self.capacity
|
|
|
|
return result
|
|
|
|
def get_all(self) -> List[Any]:
|
|
"""Get all tokens in order."""
|
|
if self.size == 0:
|
|
return []
|
|
|
|
result = []
|
|
idx = self.head
|
|
for _ in range(self.size):
|
|
result.append(self.buffer[idx])
|
|
idx = (idx + 1) % self.capacity
|
|
|
|
return result
|
|
|
|
def reset(self) -> None:
|
|
"""Clear the buffer."""
|
|
self.buffer = [None] * self.capacity
|
|
self.head = 0
|
|
self.tail = 0
|
|
self.size = 0
|
|
self.token_metadata = {}
|
|
|
|
def _resize(self, new_capacity: int) -> None:
|
|
"""Resize the buffer to new capacity."""
|
|
new_buffer = [None] * new_capacity
|
|
|
|
# Copy existing elements
|
|
idx = self.head
|
|
for i in range(self.size):
|
|
new_buffer[i] = self.buffer[idx]
|
|
idx = (idx + 1) % self.capacity
|
|
|
|
self.buffer = new_buffer
|
|
self.head = 0
|
|
self.tail = self.size
|
|
self.capacity = new_capacity
|
|
|
|
def reconstruct(self, begin_tokens: List[Any], middle_tokens: List[Any], end_tokens: List[Any]) -> None:
|
|
"""Reconstruct buffer with specified tokens."""
|
|
total_size = len(begin_tokens) + len(middle_tokens) + len(end_tokens)
|
|
|
|
# Ensure capacity is sufficient
|
|
if total_size > self.capacity:
|
|
self._resize(max(self.capacity * 2, total_size))
|
|
|
|
# Reset and refill
|
|
self.reset()
|
|
|
|
# Add all tokens
|
|
all_tokens = begin_tokens + middle_tokens + end_tokens
|
|
self.append(all_tokens)
|
|
|
|
|
|
class TokenImportanceTracker:
|
|
"""
|
|
Tracks token importance based on multiple factors: recency, attention, and content.
|
|
"""
|
|
|
|
def __init__(self, config: DynamicContextConfig):
|
|
"""Initialize the importance tracker."""
|
|
self.config = config
|
|
self.token_scores = {} # Map token IDs to importance scores
|
|
self.token_metadata = {} # Additional token information
|
|
self.attention_history = [] # Track recent attention patterns
|
|
self.position_map = {} # Map token to its position
|
|
self.age_map = {} # Map token to its age (in steps)
|
|
self.current_step = 0
|
|
|
|
def update(self, tokens: List[Any], positions: List[int], attention_weights: Optional[torch.Tensor] = None) -> None:
|
|
"""
|
|
Update importance scores based on new information.
|
|
|
|
Args:
|
|
tokens: List of token IDs
|
|
positions: Positions of tokens in the sequence
|
|
attention_weights: Optional tensor of attention weights
|
|
"""
|
|
self.current_step += 1
|
|
|
|
# Update position and age information
|
|
for token, pos in zip(tokens, positions):
|
|
self.position_map[token] = pos
|
|
self.age_map[token] = self.current_step
|
|
|
|
# Update importance scores based on multiple factors
|
|
for i, token in enumerate(tokens):
|
|
# 1. Recency score (newer tokens tend to be more important)
|
|
recency_score = self.config.recency_weight * (1.0 - i / max(1, len(tokens)))
|
|
|
|
# 2. Attention score (tokens that receive more attention are important)
|
|
attention_score = 0.0
|
|
if attention_weights is not None:
|
|
attention_score = self._calculate_attention_score(token, attention_weights)
|
|
attention_score *= self.config.attention_weight
|
|
|
|
# 3. Content score (some tokens are inherently more important)
|
|
content_score = self._calculate_content_score(token) * self.config.content_weight
|
|
|
|
# Combine scores
|
|
total_score = recency_score + attention_score + content_score
|
|
self.token_scores[token] = total_score
|
|
|
|
def _calculate_attention_score(self, token, attention_weights):
|
|
"""Calculate importance based on attention patterns."""
|
|
# This would use actual attention weights from the model
|
|
# For now, use a simple placeholder that favors tokens with mid-range positions
|
|
token_pos = self.position_map.get(token, 0)
|
|
# Simplified attention score - middle positions get higher scores
|
|
# In a real implementation, this would use actual attention statistics
|
|
return 0.5 # Placeholder
|
|
|
|
def _calculate_content_score(self, token):
|
|
"""
|
|
Calculate importance based on token content.
|
|
Some tokens are inherently more important (questions, entities, etc.)
|
|
"""
|
|
# This would analyze the token itself for importance
|
|
# Placeholder implementation - would be more sophisticated in practice
|
|
# Special tokens, entities, rare tokens might get higher scores
|
|
return 0.3 # Placeholder
|
|
|
|
def rank_tokens(self, tokens: List[Any]) -> List[Tuple[Any, float]]:
|
|
"""Rank tokens by importance, returning (token, score) pairs."""
|
|
scored_tokens = [(token, self.token_scores.get(token, 0.0)) for token in tokens]
|
|
return sorted(scored_tokens, key=lambda x: x[1], reverse=True)
|
|
|
|
def filter_by_importance(self, tokens: List[Any], threshold: float) -> List[Any]:
|
|
"""Filter tokens by importance threshold."""
|
|
return [t for t in tokens if self.token_scores.get(t, 0.0) >= threshold]
|
|
|
|
def get_top_k(self, tokens: List[Any], k: int) -> List[Any]:
|
|
"""Get top k tokens by importance."""
|
|
ranked = self.rank_tokens(tokens)
|
|
return [t for t, _ in ranked[:k]]
|
|
|
|
|
|
class MemoryMonitor:
|
|
"""
|
|
Monitors memory usage and signals when compression/eviction is needed.
|
|
"""
|
|
|
|
def __init__(self, config: DynamicContextConfig):
|
|
"""Initialize the memory monitor."""
|
|
self.config = config
|
|
self.check_frequency = config.check_memory_every_n_tokens
|
|
self.last_check = 0
|
|
self.memory_limit_mb = config.max_memory_mb
|
|
self.memory_usage_history = []
|
|
|
|
def is_memory_constrained(self) -> bool:
|
|
"""Check if memory usage is approaching limits."""
|
|
self.last_check += 1
|
|
|
|
# Only check periodically to reduce overhead
|
|
if self.last_check % self.check_frequency != 0:
|
|
return False
|
|
|
|
current_usage_mb = self._get_current_memory_usage_mb()
|
|
self.memory_usage_history.append((time.time(), current_usage_mb))
|
|
|
|
# If using more than 90% of allocated memory, signal constraint
|
|
return current_usage_mb > self.memory_limit_mb * 0.9
|
|
|
|
def _get_current_memory_usage_mb(self) -> float:
|
|
"""Get current process memory usage in MB."""
|
|
try:
|
|
import psutil
|
|
process = psutil.Process()
|
|
mem_info = process.memory_info()
|
|
return mem_info.rss / (1024 * 1024) # Convert bytes to MB
|
|
except ImportError:
|
|
# If psutil not available, return a percentage of max memory
|
|
# This is not accurate but allows the code to run
|
|
return self.memory_limit_mb * 0.5 # Assume 50% usage
|
|
|
|
def get_memory_pressure(self) -> float:
|
|
"""Get memory pressure level between 0.0 and 1.0."""
|
|
current_usage = self._get_current_memory_usage_mb()
|
|
return min(1.0, current_usage / max(1.0, self.memory_limit_mb))
|
|
|
|
def get_available_memory_mb(self) -> float:
|
|
"""Get available memory in MB."""
|
|
current_usage = self._get_current_memory_usage_mb()
|
|
return max(0.0, self.memory_limit_mb - current_usage)
|
|
|
|
|
|
class DynamicContextManager:
|
|
"""
|
|
Manages context with unlimited length, dropping middle tokens when memory is constrained.
|
|
"""
|
|
|
|
def __init__(self, config: DynamicContextConfig):
|
|
"""Initialize the context manager."""
|
|
self.config = config
|
|
|
|
# Set up token storage
|
|
self.token_buffer = CircularTokenBuffer(config.initial_buffer_size)
|
|
self.importance_tracker = TokenImportanceTracker(config)
|
|
self.memory_monitor = MemoryMonitor(config)
|
|
|
|
# Statistics and monitoring
|
|
self.total_tokens_seen = 0
|
|
self.total_tokens_evicted = 0
|
|
self.eviction_events = []
|
|
|
|
def add_tokens(self, new_tokens: List[Any], positions: Optional[List[int]] = None,
|
|
attention_weights: Optional[torch.Tensor] = None) -> None:
|
|
"""
|
|
Add new tokens to the context, evicting middle tokens if needed.
|
|
|
|
Args:
|
|
new_tokens: Tokens to add
|
|
positions: Optional positions of tokens
|
|
attention_weights: Optional attention weights
|
|
"""
|
|
if not new_tokens:
|
|
return
|
|
|
|
# Generate positions if not provided
|
|
if positions is None:
|
|
start_pos = self.total_tokens_seen
|
|
positions = list(range(start_pos, start_pos + len(new_tokens)))
|
|
|
|
# Update token importance
|
|
self.importance_tracker.update(new_tokens, positions, attention_weights)
|
|
|
|
# Check if memory is constrained and evict if needed
|
|
if self.memory_monitor.is_memory_constrained():
|
|
self._evict_middle_tokens()
|
|
|
|
# Add new tokens to buffer
|
|
self.token_buffer.append(new_tokens)
|
|
self.total_tokens_seen += len(new_tokens)
|
|
|
|
def _evict_middle_tokens(self) -> int:
|
|
"""
|
|
Evict middle tokens based on importance, preserving beginning and end.
|
|
Returns number of tokens evicted.
|
|
"""
|
|
# Determine how many tokens to preserve at beginning and end
|
|
begin_count = self.config.begin_tokens_to_preserve
|
|
if begin_count == 0:
|
|
begin_count = max(100, int(self.token_buffer.size * 0.1))
|
|
|
|
end_count = self.config.end_tokens_to_preserve
|
|
if end_count == 0:
|
|
end_count = max(100, int(self.token_buffer.size * 0.2))
|
|
|
|
# Make sure we're not trying to preserve more than we have
|
|
total_to_preserve = begin_count + end_count
|
|
if total_to_preserve >= self.token_buffer.size:
|
|
scaling_factor = self.token_buffer.size / total_to_preserve * 0.9
|
|
begin_count = int(begin_count * scaling_factor)
|
|
end_count = int(end_count * scaling_factor)
|
|
|
|
# Get beginning, middle, and end sections
|
|
begin_tokens = self.token_buffer.get_first(begin_count)
|
|
end_tokens = self.token_buffer.get_last(end_count)
|
|
middle_tokens = self.token_buffer.get_middle(begin_count, end_count)
|
|
|
|
# No middle tokens to evict
|
|
if not middle_tokens:
|
|
return 0
|
|
|
|
# Determine how many middle tokens to keep based on importance and memory
|
|
memory_pressure = self.memory_monitor.get_memory_pressure()
|
|
middle_retention_ratio = max(0.1, 1.0 - memory_pressure) # Keep at least 10%
|
|
|
|
# Sort middle by importance
|
|
ranked_tokens = self.importance_tracker.rank_tokens(middle_tokens)
|
|
|
|
# Calculate how many to keep
|
|
tokens_to_keep_count = max(
|
|
self.config.min_middle_tokens_to_keep,
|
|
int(len(middle_tokens) * middle_retention_ratio)
|
|
)
|
|
|
|
# Select top tokens to keep
|
|
tokens_to_keep = [token for token, score in ranked_tokens[:tokens_to_keep_count]]
|
|
|
|
# Calculate how many were evicted
|
|
evicted_count = len(middle_tokens) - len(tokens_to_keep)
|
|
|
|
# Record eviction event
|
|
if evicted_count > 0:
|
|
self.eviction_events.append({
|
|
'timestamp': time.time(),
|
|
'context_size': self.token_buffer.size,
|
|
'evicted_count': evicted_count,
|
|
'memory_pressure': memory_pressure,
|
|
})
|
|
self.total_tokens_evicted += evicted_count
|
|
|
|
print(f"Evicted {evicted_count} middle tokens, keeping {begin_count} beginning, "
|
|
f"{len(tokens_to_keep)} middle, and {end_count} end tokens.")
|
|
|
|
# Reconstruct buffer with kept tokens
|
|
self.token_buffer.reconstruct(begin_tokens, tokens_to_keep, end_tokens)
|
|
|
|
return evicted_count
|
|
|
|
def get_all_tokens(self) -> List[Any]:
|
|
"""Get all tokens in the context."""
|
|
return self.token_buffer.get_all()
|
|
|
|
def get_context_size(self) -> int:
|
|
"""Get current context size."""
|
|
return self.token_buffer.size
|
|
|
|
def get_stats(self) -> Dict[str, Any]:
|
|
"""Get statistics about the context manager."""
|
|
return {
|
|
'total_tokens_seen': self.total_tokens_seen,
|
|
'total_tokens_evicted': self.total_tokens_evicted,
|
|
'current_context_size': self.token_buffer.size,
|
|
'eviction_events': len(self.eviction_events),
|
|
'memory_pressure': self.memory_monitor.get_memory_pressure(),
|
|
'available_memory_mb': self.memory_monitor.get_available_memory_mb(),
|
|
}
|
|
|
|
|
|
def create_context_manager(max_memory_mb=0, begin_ratio=0.1, end_ratio=0.2):
|
|
"""
|
|
Create a dynamic context manager with simple configuration.
|
|
|
|
Args:
|
|
max_memory_mb: Maximum memory to use (0 = auto-detect)
|
|
begin_ratio: Ratio of context to preserve at beginning
|
|
end_ratio: Ratio of context to preserve at end
|
|
|
|
Returns:
|
|
Configured DynamicContextManager
|
|
"""
|
|
config = DynamicContextConfig(max_memory_mb=max_memory_mb)
|
|
|
|
# Set preservation ratios instead of absolute counts
|
|
# Actual token counts will be calculated dynamically
|
|
config.begin_tokens_to_preserve = 0 # Auto calculated as begin_ratio * context_size
|
|
config.end_tokens_to_preserve = 0 # Auto calculated as end_ratio * context_size
|
|
|
|
# Create manager with custom importance weights
|
|
manager = DynamicContextManager(config)
|
|
|
|
return manager
|