athul020's picture
Upload folder using huggingface_hub
97eb24b verified
Raw
History Blame Contribute Delete
7.2 kB
"""
Local Test-Time Training (TTT) engine.
Orchestrates the full pipeline: model loading → chunking → LoRA injection →
reading phase (training) → answering phase (inference) → state management.
"""
import os
import torch
from typing import Any
from .chunk_manager import ChunkManager
from .ttt_module import InPlaceTTT
__all__ = ["LocalEngine"]
# Lazy import guard — resolved once at first use, not at import time.
_transformers_available: bool | None = None
def _ensure_transformers():
"""Import transformers on first use and raise a clear error if missing."""
global _transformers_available
if _transformers_available is None:
try:
import transformers # noqa: F401
_transformers_available = True
except ImportError:
_transformers_available = False
if not _transformers_available:
raise ImportError(
"Transformers is required for the local engine. "
"Install via: pip install infinite_context[local]"
)
class LocalEngine:
"""Orchestrates the Local Test-Time Training (TTT) engine.
Loads the model in constrained memory (4-bit) and manages the Reading
and Answering phases.
"""
def __init__(
self,
model_id: str,
device: str = "cuda",
load_in_4bit: bool = True,
**kwargs: Any,
):
_ensure_transformers()
self.device = device if torch.cuda.is_available() else "cpu"
self.model_id = model_id
print(f"Loading base model {model_id} via Transformers into {self.device}...")
from transformers import AutoModelForCausalLM, AutoTokenizer
quantization_kwargs: dict = {}
if load_in_4bit and self.device == "cuda":
try:
from transformers import BitsAndBytesConfig
quantization_kwargs["quantization_config"] = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_compute_dtype=torch.float16,
bnb_4bit_use_double_quant=True,
)
except ImportError:
pass
self.tokenizer = AutoTokenizer.from_pretrained(self.model_id)
self.model = AutoModelForCausalLM.from_pretrained(
self.model_id,
device_map="auto" if self.device == "cuda" else None,
**quantization_kwargs,
)
self.chunker = ChunkManager(chunk_size=1024, overlap=128)
# ------------------------------------------------------------------
# Core pipeline
# ------------------------------------------------------------------
def generate(
self,
question: str,
context: str,
epochs_per_chunk: int = 15,
target_loss: float = 0.05,
keep_state: bool = False,
) -> str:
"""Run the full In-Place TTT pipeline.
1. Break massive context into chunks.
2. Inject Fast Weights (LoRA) into the model.
3. Reading Phase — train the Fast Weights on the chunks.
4. Answering Phase — generate the response to the question.
5. State Management — optionally retain or destroy Fast Weights.
"""
# 1. Chunking
tokens = self.tokenizer.encode(context, add_special_tokens=False)
chunks = self.chunker.chunk_tokens(tokens)
print(f"Bypassing KV-Cache: Splitting {len(tokens)} tokens into {len(chunks)} chunks.")
# 2. Attach TTT Fast Weights (skip if a checkpoint was loaded)
is_peft = hasattr(self.model, "peft_config")
ttt: InPlaceTTT | None = None
if not is_peft:
print("Injecting TTT Fast Weights into down_proj layers...")
ttt = InPlaceTTT(self.model)
else:
print("Model already has Fast Weights loaded from disk. Reusing them...")
# 3. The 'Reading' Phase
if chunks:
print("Starting Reading Phase (Test-Time Training)...")
if ttt is None:
print("Warning: Cannot train loaded Fast Weights in-place. Skipping training.")
else:
for i, chunk in enumerate(chunks):
# Hoist tensor creation outside the epoch loop.
input_ids = torch.tensor([chunk], device=self.device)
for epoch in range(epochs_per_chunk):
loss = ttt.train_on_chunk(input_ids)
if loss < target_loss:
break
print(f" Processed Chunk {i + 1}/{len(chunks)} - Epochs: {epoch + 1} - Loss: {loss:.4f}")
# KV Cache is implicitly cleared here because we are not passing
# past_key_values!
# 4. The 'Answering' Phase
print("Starting Answering Phase...")
self.model.eval()
prompt = f"Based on the codebase I just showed you, answer this question:\n{question}\nAnswer:"
prompt_ids = self.tokenizer.encode(prompt, return_tensors="pt").to(self.device)
device_type = self.device if self.device in ("cuda", "cpu") else "cpu"
with torch.no_grad():
output_ids = self.model.generate(
prompt_ids,
max_new_tokens=75,
do_sample=False,
repetition_penalty=1.1,
)
# Extract just the newly generated tokens.
response_tokens = output_ids[0][prompt_ids.shape[1]:]
response = self.tokenizer.decode(response_tokens, skip_special_tokens=True)
# 5. State Management
if not keep_state:
if ttt is not None:
print("Resetting model state (deleting Fast Weights)...")
ttt.remove_fast_weights()
else:
print("Cannot reset loaded Fast Weights. Retaining state.")
else:
print("Keeping model state (Fast Weights retained)...")
if ttt is not None:
# Update the reference so save_pretrained saves the adapter.
self.model = ttt.model
return response
# ------------------------------------------------------------------
# Persistence
# ------------------------------------------------------------------
def save_state(self, path: str) -> None:
"""Save the current Fast Weights (LoRA adapter) to disk."""
if hasattr(self.model, "peft_config"):
self.model.save_pretrained(path)
print(f"State saved to {path}")
else:
raise RuntimeError(
"No active Fast Weights to save. "
"Did you forget keep_state=True when calling generate()?"
)
def load_state(self, path: str) -> None:
"""Load Fast Weights (LoRA adapter) from disk."""
from peft import PeftModel
adapter_path = os.path.join(path, "ttt_fast_weights")
if not os.path.exists(adapter_path):
# Fallback for standard PEFT saves.
adapter_path = path
self.model = PeftModel.from_pretrained(
self.model, adapter_path, is_trainable=True,
)
print(f"State loaded from {adapter_path}")