"""Integrate dynamic memory into a local Qwen3.5 checkpoint. The original Hugging Face checkpoint is loaded unchanged. Decoder layers are wrapped after loading, and only the separate memory module is trainable by default. """ from __future__ import annotations from dataclasses import asdict, dataclass from pathlib import Path from typing import Any, Optional import json import os import re import time import weakref import torch import torch.nn.functional as F from torch import Tensor, nn from .memory_os_v2 import ( KVBudgetManagerV2, MemoryOSV2, MemoryRouterV2, PagedMemoryBankV2, memory_record_to_dict, ) from .tiered_memory_store_v2 import TieredMemoryStoreV2 # Qwen's local tokenizer already contains this unused FIM control token. It # gives us a reset signal without changing the base vocabulary or resizing a # quantized embedding table. Callers may still pass any other token id. DEFAULT_MEMORY_RESET_TOKEN = "<|fim_prefix|>" def split_memory_candidates(text: str, *, max_candidates: int = 8) -> list[str]: """Split one user turn into bounded fact-sized write candidates. This is only a write-side segmentation aid. The model's internal policy still decides whether each candidate is durable; the read path remains entirely inside the model-owned memory bank. """ text = " ".join(str(text).strip().split()) if not text: return [] # A comma is not a safe write boundary. In natural conversation it is # common for the second half of a sentence to negate or qualify the # first half (for example, "mentioned X, but it is not important"). # Splitting there would make the policy see a misleading fragment and can # permanently store noise. Keep clauses intact and only split at hard # sentence boundaries. parts = [ part.strip() for part in re.split(r"(?<=[。!?!?;;])\s*", text) if part.strip() ] if len(parts) <= 1: return [text] if len(parts) <= max_candidates: return parts # Preserve all text when a long paragraph exceeds the safety bound. The # exact sequence can still be stored in one slot without silent loss. return [text] def looks_like_question(text: str) -> bool: """Reject question-shaped turns from the automatic durable-write path.""" normalized = " ".join(str(text).strip().split()) if not normalized: return False if any(mark in normalized for mark in ("?", "?")): return True return normalized.startswith( ( "请问", "什么", "为什么", "怎么", "如何", "能不能", "是否", "有没有", "如果", "帮我", "解释", ) ) @dataclass class QwenMemoryConfig: memory_slots: int = 16 memory_dim: int = 512 layer_indices: Optional[tuple[int, ...]] = None read_scale: float = 1.0 write_scale: float = 1.0 mode: str = "residual" blend_init: float = 0.0 direct_logit_scale: float = 0.0 write_token_offset: Optional[int] = None broadcast_write: bool = False raw_token_write: bool = False raw_logit_scale: float = 0.0 native_mode: bool = False persistent_memory: bool = False reset_token_id: Optional[int] = None summary_pooling: bool = True # Natural-language episodic memory keeps the exact token sequence of a # selected write inside the model-owned memory state. The model retrieves # and injects these tokens internally; callers never need to replay chat # history or assemble a prompt by hand. natural_language_memory: bool = False text_memory_tokens: int = 256 text_memory_top_k: int = 2 text_memory_threshold: float = 0.40 text_memory_write_threshold: float = 0.5 text_memory_replace_threshold: float = 0.35 # A slightly more permissive boundary for recognizing a same-attribute # update. Unrelated fragments still remain independent V2 records. text_memory_update_overlap_threshold: float = 0.30 text_memory_key_tokens: int = 128 text_memory_overlap_threshold: float = 0.22 automatic_memory: bool = False auto_memory_threshold: float = 0.35 # v2 adds a separately trained natural-language forget decision. Version # 1 keeps the old one-logit policy byte-compatible with existing adapters. automatic_memory_policy_version: int = 1 auto_forget_threshold: float = 0.50 # Natural Memory v2: compact addressed pages sit beside the v1 hot text # bank. V2 is opt-in so existing checkpoints remain byte-compatible. memory_version: int = 1 hierarchical_memory: bool = False memory_router_dim: int = 128 memory_router_heads: int = 8 memory_page_capacity: int = 32 # 32K pages x 32 records/page gives a one-million-record address space. # Pages are allocated lazily; this is a capacity, not a startup tensor. memory_max_pages: int = 32768 memory_hot_pages: int = 8 memory_top_k_pages: int = 4 memory_top_k_records: int = 8 memory_max_hops: int = 3 memory_coarse_index_bits: int = 20 memory_v2_read_threshold: float = 0.65 memory_v2_write_threshold: float = 0.50 # ``embedded`` keeps the portable user snapshot inside the safetensors # package. ``tiered`` additionally opens a durable page store so cold # records can leave RAM while the same model-owned reader remains active. memory_storage_mode: str = "embedded" memory_storage_path: Optional[str] = None memory_resident_pages: int = 256 # Embedded mode keeps every record in process RAM. Only this bounded hot # cache is copied to the execution device; it is not a second store. memory_gpu_cache_records: int = 256 memory_gpu_cache_tokens: int = 131072 # Keep a large VRAM safety margin for the model weights, activations and # KV cache. Memory records that do not fit remain in system RAM. memory_gpu_cache_reserve_mb: int = 2048 memory_gpu_cache_adaptive: bool = True # HF generation can keep the active cache on CPU while the current layer # is executing. This is opt-in because it trades GPU memory for PCIe # traffic and is therefore not always faster on short prompts. kv_offload: bool = False kv_cache_implementation: Optional[str] = None kv_offload_only_non_sliding: bool = True # When a prompt exceeds the hot-window budget, archive the old prefix as # V2 context records and run Qwen only on the recent window. The reader # remains model-owned; callers do not have to assemble a retrieval prompt. auto_compact_context: bool = True context_chunk_tokens: int = 512 context_archive_max_records: int = 1_048_576 kv_budget_tokens: int = 32768 kv_hard_max_tokens: int = 131072 kv_compaction_trigger: float = 0.90 kv_keep_recent_tokens: int = 8192 def __post_init__(self) -> None: if self.mode not in {"residual", "blend", "replace"}: raise ValueError("mode must be one of: residual, blend, replace") if not 0.0 <= self.blend_init <= 1.0: raise ValueError("blend_init must be between 0 and 1") if self.direct_logit_scale < 0.0: raise ValueError("direct_logit_scale must be non-negative") if self.write_token_offset is not None and self.write_token_offset < 1: raise ValueError("write_token_offset must be positive") if self.raw_logit_scale < 0.0: raise ValueError("raw_logit_scale must be non-negative") if self.text_memory_tokens < 1: raise ValueError("text_memory_tokens must be positive") if self.text_memory_top_k < 1: raise ValueError("text_memory_top_k must be positive") if not 0.0 <= self.text_memory_threshold <= 1.0: raise ValueError("text_memory_threshold must be between 0 and 1") if not 0.0 <= self.text_memory_write_threshold <= 1.0: raise ValueError("text_memory_write_threshold must be between 0 and 1") if not -1.0 <= self.text_memory_replace_threshold <= 1.0: raise ValueError("text_memory_replace_threshold must be between -1 and 1") if not -1.0 <= self.text_memory_update_overlap_threshold <= 1.0: raise ValueError("text_memory_update_overlap_threshold must be between -1 and 1") if self.text_memory_key_tokens < 1: raise ValueError("text_memory_key_tokens must be positive") if not 0.0 <= self.text_memory_overlap_threshold <= 1.0: raise ValueError("text_memory_overlap_threshold must be between 0 and 1") if not 0.0 <= self.auto_memory_threshold <= 1.0: raise ValueError("auto_memory_threshold must be between 0 and 1") if self.automatic_memory_policy_version < 1: raise ValueError("automatic_memory_policy_version must be positive") if not 0.0 <= self.auto_forget_threshold <= 1.0: raise ValueError("auto_forget_threshold must be between 0 and 1") if self.memory_version < 1: raise ValueError("memory_version must be positive") if self.memory_router_heads < 1: raise ValueError("memory_router_heads must be positive") if self.memory_router_dim < 8 or self.memory_router_dim % self.memory_router_heads != 0: raise ValueError("memory_router_dim must be divisible by memory_router_heads") if self.memory_page_capacity < 1 or self.memory_max_pages < 1: raise ValueError("memory page limits must be positive") if self.memory_top_k_pages < 1 or self.memory_top_k_records < 1: raise ValueError("memory top-k limits must be positive") if self.memory_max_hops < 1: raise ValueError("memory_max_hops must be positive") if not 0.0 <= self.memory_v2_read_threshold <= 1.0: raise ValueError("memory_v2_read_threshold must be between 0 and 1") if not 0.0 <= self.memory_v2_write_threshold <= 1.0: raise ValueError("memory_v2_write_threshold must be between 0 and 1") if self.memory_storage_mode not in {"embedded", "tiered"}: raise ValueError("memory_storage_mode must be embedded or tiered") if self.memory_resident_pages < self.memory_hot_pages: raise ValueError("memory_resident_pages must be >= memory_hot_pages") if self.memory_gpu_cache_records < 0 or self.memory_gpu_cache_tokens < 0: raise ValueError("memory GPU cache limits must be non-negative") if self.memory_gpu_cache_reserve_mb < 0: raise ValueError("memory_gpu_cache_reserve_mb must be non-negative") if self.kv_cache_implementation is not None and not str(self.kv_cache_implementation).strip(): raise ValueError("kv_cache_implementation must be non-empty when provided") if self.context_chunk_tokens < 1: raise ValueError("context_chunk_tokens must be positive") if self.context_archive_max_records < 1: raise ValueError("context_archive_max_records must be positive") if not 1 <= self.kv_budget_tokens <= self.kv_hard_max_tokens: raise ValueError("kv_budget_tokens must be inside [1, kv_hard_max_tokens]") def resolved_layers(self, num_hidden_layers: int) -> tuple[int, ...]: if self.layer_indices is not None: layers = tuple(sorted(set(self.layer_indices))) else: stride = max(1, num_hidden_layers // 4) layers = tuple(min(num_hidden_layers - 1, stride * i - 1) for i in range(1, 5)) layers = tuple(sorted(set(layers))) if not layers or min(layers) < 0 or max(layers) >= num_hidden_layers: raise ValueError(f"layer_indices must be inside [0, {num_hidden_layers})") return layers def load_memory_config(adapter_dir: str | Path) -> QwenMemoryConfig: """Load the architecture/configuration metadata saved with an adapter.""" metadata_path = Path(adapter_dir) / "memory_config.json" metadata = json.loads(metadata_path.read_text(encoding="utf-8")) saved = metadata.get("memory_config", {}) saved_layers = metadata.get("layer_indices", saved.get("layer_indices")) return QwenMemoryConfig( memory_slots=int(saved.get("memory_slots", 16)), memory_dim=int(saved.get("memory_dim", 512)), layer_indices=tuple(saved_layers) if saved_layers is not None else None, read_scale=float(saved.get("read_scale", 1.0)), write_scale=float(saved.get("write_scale", 1.0)), mode=str(saved.get("mode", "residual")), blend_init=float(saved.get("blend_init", 0.0)), direct_logit_scale=float(saved.get("direct_logit_scale", 0.0)), write_token_offset=( int(saved["write_token_offset"]) if saved.get("write_token_offset") is not None else None ), broadcast_write=bool(saved.get("broadcast_write", False)), raw_token_write=bool(saved.get("raw_token_write", False)), raw_logit_scale=float(saved.get("raw_logit_scale", 0.0)), native_mode=bool(saved.get("native_mode", False)), persistent_memory=bool(saved.get("persistent_memory", False)), reset_token_id=(int(saved["reset_token_id"]) if saved.get("reset_token_id") is not None else None), summary_pooling=bool(saved.get("summary_pooling", True)), natural_language_memory=bool(saved.get("natural_language_memory", False)), text_memory_tokens=int(saved.get("text_memory_tokens", 256)), text_memory_top_k=int(saved.get("text_memory_top_k", 2)), text_memory_threshold=float(saved.get("text_memory_threshold", 0.40)), text_memory_write_threshold=float(saved.get("text_memory_write_threshold", 0.5)), text_memory_replace_threshold=float(saved.get("text_memory_replace_threshold", 0.35)), text_memory_update_overlap_threshold=float( saved.get("text_memory_update_overlap_threshold", 0.30) ), text_memory_key_tokens=int(saved.get("text_memory_key_tokens", 128)), text_memory_overlap_threshold=float(saved.get("text_memory_overlap_threshold", 0.22)), automatic_memory=bool(saved.get("automatic_memory", False)), auto_memory_threshold=float(saved.get("auto_memory_threshold", 0.35)), automatic_memory_policy_version=int(saved.get("automatic_memory_policy_version", 1)), auto_forget_threshold=float(saved.get("auto_forget_threshold", 0.50)), memory_version=int(saved.get("memory_version", 1)), hierarchical_memory=bool(saved.get("hierarchical_memory", False)), memory_router_dim=int(saved.get("memory_router_dim", 128)), memory_router_heads=int(saved.get("memory_router_heads", 8)), memory_page_capacity=int(saved.get("memory_page_capacity", 32)), memory_max_pages=int(saved.get("memory_max_pages", 32768)), memory_hot_pages=int(saved.get("memory_hot_pages", 8)), memory_top_k_pages=int(saved.get("memory_top_k_pages", 4)), memory_top_k_records=int(saved.get("memory_top_k_records", 8)), memory_max_hops=int(saved.get("memory_max_hops", 3)), memory_coarse_index_bits=int(saved.get("memory_coarse_index_bits", 20)), memory_v2_read_threshold=float(saved.get("memory_v2_read_threshold", 0.50)), memory_v2_write_threshold=float(saved.get("memory_v2_write_threshold", 0.50)), memory_storage_mode=str(saved.get("memory_storage_mode", "embedded")), memory_storage_path=(str(saved["memory_storage_path"]) if saved.get("memory_storage_path") else None), memory_resident_pages=int(saved.get("memory_resident_pages", 256)), memory_gpu_cache_records=int(saved.get("memory_gpu_cache_records", 256)), memory_gpu_cache_tokens=int(saved.get("memory_gpu_cache_tokens", 131072)), memory_gpu_cache_reserve_mb=int(saved.get("memory_gpu_cache_reserve_mb", 2048)), memory_gpu_cache_adaptive=bool(saved.get("memory_gpu_cache_adaptive", True)), kv_offload=bool(saved.get("kv_offload", False)), kv_cache_implementation=( str(saved["kv_cache_implementation"]) if saved.get("kv_cache_implementation") is not None else None ), kv_offload_only_non_sliding=bool(saved.get("kv_offload_only_non_sliding", True)), auto_compact_context=bool(saved.get("auto_compact_context", True)), context_chunk_tokens=int(saved.get("context_chunk_tokens", 512)), context_archive_max_records=int(saved.get("context_archive_max_records", 1_048_576)), kv_budget_tokens=int(saved.get("kv_budget_tokens", 32768)), kv_hard_max_tokens=int(saved.get("kv_hard_max_tokens", 131072)), kv_compaction_trigger=float(saved.get("kv_compaction_trigger", 0.90)), kv_keep_recent_tokens=int(saved.get("kv_keep_recent_tokens", 8192)), ) def resolve_memory_reset_token(tokenizer: Any, token: str = DEFAULT_MEMORY_RESET_TOKEN) -> int: """Resolve a tokenizer control token suitable for clearing memory.""" token_id = tokenizer.convert_tokens_to_ids(token) unknown_id = getattr(tokenizer, "unk_token_id", None) if token_id is None or (unknown_id is not None and int(token_id) == int(unknown_id)): raise ValueError( f"reset token {token!r} is not present in this tokenizer; pass --reset-token-id instead" ) return int(token_id) class QwenDynamicMemory(nn.Module): """A compact float32 key-value memory connected to Qwen hidden states.""" def __init__(self, hidden_size: int, config: QwenMemoryConfig) -> None: super().__init__() self.hidden_size = hidden_size self.config = config dim = config.memory_dim self.query = nn.Linear(hidden_size, dim, bias=False) self.key = nn.Linear(dim, dim, bias=False) self.value = nn.Linear(dim, dim, bias=False) self.read_out = nn.Linear(dim, hidden_size, bias=False) self.read_gate = nn.Linear(hidden_size, 1) self.slot_keys = nn.Parameter(torch.randn(config.memory_slots, dim) / dim**0.5) self.write_key = nn.Linear(hidden_size, dim, bias=False) self.write_value = nn.Linear(hidden_size, dim, bias=False) self.write_gate = nn.Linear(hidden_size, config.memory_slots) self.apply(self._init_weights) self.last_read_address: Optional[Tensor] = None self.last_read_relevance: Optional[Tensor] = None self.last_write_address: Optional[Tensor] = None self.last_write_probability: Optional[Tensor] = None @staticmethod def _init_weights(module: nn.Module) -> None: if isinstance(module, nn.Linear): nn.init.normal_(module.weight, mean=0.0, std=0.02) def initial_state(self, batch_size: int, *, device: torch.device) -> Tensor: return torch.zeros( batch_size, self.config.memory_slots, self.config.memory_dim, device=device, dtype=self.slot_keys.dtype, ) def read(self, hidden_states: Tensor, memory: Tensor) -> Tensor: work_dtype = self.slot_keys.dtype hidden = hidden_states.to(work_dtype) memory = memory.to(device=hidden.device, dtype=work_dtype) q = self.query(hidden) k = self.key(memory) v = self.value(memory) scores = torch.matmul(q, k.transpose(-1, -2)) / self.config.memory_dim**0.5 address = scores.softmax(dim=-1) retrieved = torch.matmul(address, v) retrieved = self.read_out(retrieved) gate = torch.sigmoid(self.read_gate(hidden)) self.last_read_address = address self.last_read_relevance = gate * address.max(dim=-1, keepdim=True).values return (self.config.read_scale * gate * retrieved).to(hidden_states.dtype) def update( self, hidden_states: Tensor, memory: Tensor, attention_mask: Optional[Tensor] = None, ) -> Tensor: work_dtype = self.slot_keys.dtype hidden = hidden_states.to(work_dtype) memory = memory.to(device=hidden.device, dtype=work_dtype) if self.config.write_token_offset is None: summary = hidden[:, -1] else: if hidden.shape[1] < self.config.write_token_offset: raise ValueError("write_token_offset exceeds the memory sequence length") summary = hidden[:, -self.config.write_token_offset] proposal = self.write_value(summary) if self.config.broadcast_write: strength = torch.ones( hidden.shape[0], self.config.memory_slots, device=hidden.device, dtype=work_dtype, ) address = strength / float(self.config.memory_slots) else: address = (self.write_key(summary) @ self.slot_keys.t()).softmax(dim=-1) strength = torch.sigmoid(self.write_gate(summary)) * address self.last_write_address = address self.last_write_probability = strength.sum(dim=-1, keepdim=True) strength = (self.config.write_scale * strength).unsqueeze(-1) proposal = proposal[:, None, :].expand(-1, self.config.memory_slots, -1) return memory + strength * (proposal - memory) class NaturalLanguageRetriever(nn.Module): """Trainable pair scorer for query-to-episodic-memory retrieval.""" def __init__(self, hidden_size: int, projection_size: int = 256) -> None: super().__init__() self.query_projection = nn.Linear(hidden_size, projection_size, bias=False) self.key_projection = nn.Linear(hidden_size, projection_size, bias=False) self.pair_scorer = nn.Sequential( nn.Linear(projection_size * 4, projection_size), nn.SiLU(), nn.Linear(projection_size, 1), ) self.apply(self._init_weights) @staticmethod def _init_weights(module: nn.Module) -> None: if isinstance(module, nn.Linear): nn.init.normal_(module.weight, mean=0.0, std=0.02) def forward(self, query: Tensor, keys: Tensor) -> Tensor: query = F.normalize(query.float(), dim=-1) keys = F.normalize(keys.float(), dim=-1) query_projection = self.query_projection(query) key_projection = self.key_projection(keys) if key_projection.ndim == 3: query_projection = query_projection.unsqueeze(1) features = torch.cat( ( query_projection.expand_as(key_projection), key_projection, query_projection.expand_as(key_projection) * key_projection, (query_projection.expand_as(key_projection) - key_projection).abs(), ), dim=-1, ) return self.pair_scorer(features).squeeze(-1) class AutomaticMemoryPolicy(nn.Module): """High-recall controller for deciding whether a user turn is durable.""" def __init__(self, hidden_size: int, projection_size: int = 256) -> None: super().__init__() self.encoder = nn.Sequential( nn.Linear(hidden_size, projection_size), nn.LayerNorm(projection_size), nn.SiLU(), nn.Linear(projection_size, projection_size // 2), nn.SiLU(), ) self.importance = nn.Linear(projection_size // 2, 1) self.apply(self._init_weights) @staticmethod def _init_weights(module: nn.Module) -> None: if isinstance(module, nn.Linear): nn.init.normal_(module.weight, mean=0.0, std=0.02) if module.bias is not None: nn.init.zeros_(module.bias) def forward(self, hidden: Tensor) -> Tensor: return self.importance(self.encoder(hidden.float())).squeeze(-1) class AutomaticMemoryPolicyV2(AutomaticMemoryPolicy): """Write/forget policy trained on the same natural-language turn state.""" def __init__(self, hidden_size: int, projection_size: int = 256) -> None: super().__init__(hidden_size, projection_size) self.forget = nn.Linear(projection_size // 2, 1) nn.init.normal_(self.forget.weight, mean=0.0, std=0.02) nn.init.zeros_(self.forget.bias) def _features(self, hidden: Tensor) -> Tensor: return self.encoder(hidden.float()) def forward(self, hidden: Tensor) -> Tensor: return self.importance(self._features(hidden)).squeeze(-1) def forget_logits(self, hidden: Tensor) -> Tensor: return self.forget(self._features(hidden)).squeeze(-1) class NativeQwenDynamicMemory(QwenDynamicMemory): """Learned write/forget controller for model-owned persistent memory.""" def __init__(self, hidden_size: int, config: QwenMemoryConfig) -> None: super().__init__(hidden_size, config) dim = config.memory_dim self.summary_score = nn.Linear(hidden_size, 1, bias=False) self.write_decision = nn.Linear(hidden_size, 1) # Forgetting must depend on both the incoming candidate and what is # already stored. A hidden-only gate can learn "this sentence looks # like a fact", but it cannot learn "this fact conflicts with the # value in the addressed slot". self.forget_gate = nn.Sequential( nn.Linear(hidden_size + (2 * dim), dim), nn.SiLU(), nn.Linear(dim, config.memory_slots), ) nn.init.normal_(self.summary_score.weight, mean=0.0, std=0.02) nn.init.normal_(self.write_decision.weight, mean=0.0, std=0.02) for module in self.forget_gate: if isinstance(module, nn.Linear): nn.init.normal_(module.weight, mean=0.0, std=0.02) nn.init.constant_(self.write_decision.bias, -1.5) nn.init.constant_(self.forget_gate[-1].bias, -2.0) self.last_write_probability: Optional[Tensor] = None self.last_forget_probability: Optional[Tensor] = None # Keep the semantic summary available to the automatic policy. The # older ``last_write_representation`` is optimized for the memory # value path and may discard distinctions such as negation or a # question-shaped turn. self.last_write_summary: Optional[Tensor] = None self.last_write_representation: Optional[Tensor] = None def _summary(self, hidden_states: Tensor, attention_mask: Optional[Tensor]) -> Tensor: work_dtype = self.slot_keys.dtype hidden = hidden_states.to(work_dtype) if self.config.write_token_offset is not None: if hidden.shape[1] < self.config.write_token_offset: raise ValueError("write_token_offset exceeds the memory sequence length") return hidden[:, -self.config.write_token_offset] if not self.config.summary_pooling: return hidden[:, -1] scores = self.summary_score(hidden).squeeze(-1) if attention_mask is not None and attention_mask.ndim == 2: # During cached generation the decoder may expose only the newest # token while the attention mask still covers the full sequence. # Align the mask to the score sequence instead of assuming both # lengths are identical. score_length = scores.shape[1] mask = attention_mask.to(device=hidden.device, dtype=torch.bool) if mask.shape[1] > score_length: mask = mask[:, -score_length:] elif mask.shape[1] < score_length: pad = torch.ones( mask.shape[0], score_length - mask.shape[1], device=mask.device, dtype=mask.dtype, ) mask = torch.cat((mask, pad), dim=1) scores = scores.masked_fill(~mask, torch.finfo(scores.dtype).min) weights = scores.softmax(dim=-1) return (weights.unsqueeze(-1) * hidden).sum(dim=1) def update( self, hidden_states: Tensor, memory: Tensor, attention_mask: Optional[Tensor] = None, ) -> Tensor: work_dtype = self.slot_keys.dtype memory = memory.to(device=hidden_states.device, dtype=work_dtype) summary = self._summary(hidden_states, attention_mask) self.last_write_summary = summary proposal = self.write_value(summary) self.last_write_representation = self.read_out(self.value(proposal)) address = (self.write_key(summary) @ self.slot_keys.t()).softmax(dim=-1) slot_probability = torch.sigmoid(self.write_gate(summary)) write_probability = torch.sigmoid(self.write_decision(summary)) self.last_write_address = address matched_memory = torch.sum(address.unsqueeze(-1) * memory, dim=1) forget_features = torch.cat((summary, proposal, matched_memory), dim=-1) forget_probability = torch.sigmoid(self.forget_gate(forget_features)) strength = self.config.write_scale * write_probability * slot_probability * address erase = strength * forget_probability proposal = proposal[:, None, :] updated = memory * (1.0 - erase.unsqueeze(-1)) updated = updated + strength.unsqueeze(-1) * (proposal - updated) self.last_write_probability = write_probability self.last_forget_probability = forget_probability return updated class _MemoryRuntime: def __init__(self, memory: QwenDynamicMemory) -> None: self.memory = memory self.state: Optional[Tensor] = None self.read_enabled = True self.update_enabled = True self.last_read: Optional[Tensor] = None self.raw_memory: Optional[Tensor] = None self.input_ids: Optional[Tensor] = None self.attention_mask: Optional[Tensor] = None self.output_embeddings: Optional[nn.Module] = None self.reset_mask: Optional[Tensor] = None self.text_token_ids: Optional[Tensor] = None self.text_token_mask: Optional[Tensor] = None self.text_slot_valid: Optional[Tensor] = None self.text_slot_keys: Optional[Tensor] = None self.text_slot_age: Optional[Tensor] = None self.text_write_counter: Optional[Tensor] = None self.text_key_token_ids: Optional[Tensor] = None self.text_key_token_mask: Optional[Tensor] = None self.text_last_written_slot: Optional[Tensor] = None self.text_read_slots: Optional[Tensor] = None self.text_read_relevance: Optional[Tensor] = None self.text_read_overlap: Optional[Tensor] = None self.v2_query_key: Optional[Tensor] = None self.v2_last_decisions: list[Any] = [] self.text_prefix_used: bool = False self.auto_memory_probability: Optional[Tensor] = None self.auto_memory_forget_probability: Optional[Tensor] = None self.context_compaction: Optional[dict[str, Any]] = None # Persistent checkpoints are the durable user state. Evaluation, # branch execution and a new temporary conversation must be able to # reset only their working state without mutating that checkpoint. self.use_persistent_state: bool = True class MemoryLayerAdapter(nn.Module): """Wrap one Qwen decoder layer and optionally replace its token mixer.""" def __init__( self, inner: nn.Module, runtime: _MemoryRuntime, *, read: bool, write: bool, mode: str = "residual", blend_init: float = 0.0, ) -> None: super().__init__() if mode not in {"residual", "blend", "replace"}: raise ValueError("mode must be one of: residual, blend, replace") self.inner = inner self._runtime_ref = weakref.ref(runtime) self.read_enabled = read self.write_enabled = write self.mode = mode if mode == "blend": blend_init = min(max(float(blend_init), 1e-4), 1.0 - 1e-4) logit = torch.logit(torch.tensor(blend_init, dtype=torch.float32)) inner_device = next( (parameter.device for parameter in inner.parameters() if parameter.device.type != "meta"), None, ) if inner_device is not None: logit = logit.to(inner_device) self.blend_logit = nn.Parameter(logit) def _original_token_mixer( self, normalized_hidden: Tensor, position_embeddings: Any, attention_mask: Optional[Tensor], position_ids: Optional[Tensor], past_key_values: Any, kwargs: dict[str, Any], ) -> Tensor: layer_type = getattr(self.inner, "layer_type", None) if layer_type == "linear_attention": output = self.inner.linear_attn( hidden_states=normalized_hidden, cache_params=past_key_values, attention_mask=attention_mask, **kwargs, ) return output[0] if isinstance(output, tuple) else output if layer_type == "full_attention": output = self.inner.self_attn( hidden_states=normalized_hidden, attention_mask=attention_mask, position_ids=position_ids, past_key_values=past_key_values, position_embeddings=position_embeddings, **kwargs, ) return output[0] if isinstance(output, tuple) else output raise TypeError(f"unsupported Qwen layer_type for surgery: {layer_type!r}") def _prefetch_cache_layer(self, past_key_values: Any) -> None: """Make an offloaded Qwen hybrid-cache layer ready before it is read.""" if past_key_values is None or not getattr(past_key_values, "offloading", False): return component = getattr(self.inner, "self_attn", None) if component is None: component = getattr(self.inner, "linear_attn", None) layer_index = getattr(component, "layer_idx", None) if layer_index is None or not hasattr(past_key_values, "prefetch"): return only_non_sliding = bool(getattr(past_key_values, "only_non_sliding", True)) parameter = next(component.parameters(), None) device = parameter.device if parameter is not None else torch.device("cpu") past_key_values.prefetch(int(layer_index), only_non_sliding) # The stock offload helper uses a separate CUDA stream. Qwen3.5 # reads linear-attention state before it calls ``update_*``; make the # current layer's ownership synchronous here so a CPU state can never # reach a CUDA convolution or attention kernel. if 0 <= int(layer_index) < len(past_key_values.layers): cache_layer = past_key_values.layers[int(layer_index)] for name in ("keys", "values", "conv_states", "recurrent_states"): value = getattr(cache_layer, name, None) if isinstance(value, Tensor) and value.device != device: setattr(cache_layer, name, value.to(device=device, non_blocking=False)) def _memory_read(self, hidden_states: Tensor, runtime: _MemoryRuntime) -> Tensor: if runtime.read_enabled and self.read_enabled and runtime.state is not None: output = runtime.memory.read(hidden_states, runtime.state) else: output = torch.zeros_like(hidden_states) if self.write_enabled: runtime.last_read = output return output def _surgical_forward( self, hidden_states: Tensor, position_embeddings: Any, attention_mask: Optional[Tensor], position_ids: Optional[Tensor], past_key_values: Any, kwargs: dict[str, Any], runtime: _MemoryRuntime, ) -> Tensor: residual = hidden_states normalized_hidden = self.inner.input_layernorm(hidden_states) memory_output = self._memory_read(normalized_hidden, runtime) if self.mode == "replace": token_mixer = memory_output else: original_output = self._original_token_mixer( normalized_hidden, position_embeddings, attention_mask, position_ids, past_key_values, kwargs, ) mix = torch.sigmoid(self.blend_logit).to(dtype=original_output.dtype) # A zero/empty memory must be an exact no-op. This prevents a # trained blend coefficient from attenuating the original Qwen # token mixer on ordinary prompts that have no user memory. if runtime.state is None or not runtime.read_enabled: # Exact zero in the forward pass, but retain a surrogate # gradient so the blend parameter remains trainable in unit # tests and future calibration runs. mix = mix - mix.detach() else: memory_activity = runtime.state.detach().abs().mean() if float(memory_activity) <= 1e-6: mix = mix - mix.detach() token_mixer = (1.0 - mix) * original_output + mix * memory_output hidden_states = residual + token_mixer residual = hidden_states hidden_states = self.inner.post_attention_layernorm(hidden_states) hidden_states = self.inner.mlp(hidden_states) return residual + hidden_states def forward( self, hidden_states: Tensor, position_embeddings: Any = None, attention_mask: Optional[Tensor] = None, position_ids: Optional[Tensor] = None, past_key_values: Any = None, **kwargs: Any, ) -> Any: runtime = self._runtime_ref() if runtime is None: return self.inner( hidden_states, position_embeddings, attention_mask=attention_mask, position_ids=position_ids, past_key_values=past_key_values, **kwargs, ) self._prefetch_cache_layer(past_key_values) if self.mode == "residual": hidden_states = hidden_states + self._memory_read(hidden_states, runtime) output = self.inner( hidden_states, position_embeddings, attention_mask=attention_mask, position_ids=position_ids, past_key_values=past_key_values, **kwargs, ) else: output = self._surgical_forward( hidden_states, position_embeddings, attention_mask, position_ids, past_key_values, kwargs, runtime, ) hidden_output = output[0] if isinstance(output, tuple) else output if runtime.update_enabled and self.write_enabled and runtime.state is not None: updated_state = runtime.memory.update( hidden_output, runtime.state, attention_mask=runtime.attention_mask, ) if runtime.reset_mask is not None: reset_mask = runtime.reset_mask.to(device=updated_state.device, dtype=torch.bool) updated_state = torch.where( reset_mask[:, None, None], torch.zeros_like(updated_state), updated_state, ) runtime.reset_mask = None runtime.state = updated_state if ( runtime.memory.config.raw_token_write and not runtime.read_enabled and runtime.input_ids is not None ): if runtime.output_embeddings is None: raise RuntimeError("raw_token_write requires output embeddings") offset = runtime.memory.config.write_token_offset or 1 if runtime.attention_mask is not None and runtime.attention_mask.ndim == 2: last_valid = runtime.attention_mask.sum(dim=-1).to(torch.long) - 1 positions = (last_valid - (offset - 1)).clamp_min(0) token_ids = runtime.input_ids.gather(1, positions[:, None]).squeeze(1) else: token_ids = runtime.input_ids[:, -offset] # Store the output-projection row, not the input embedding row. # Qwen checkpoints are allowed to untie these matrices, and the # generation-time pointer is consumed through output_embeddings. runtime.raw_memory = runtime.output_embeddings.weight[token_ids].detach() return output class QwenDynamicMemoryOutput: """Proxy for a normal HF output with one added field: ``memory``.""" def __init__(self, base_output: Any, memory: Optional[Tensor]) -> None: self.base_output = base_output self.memory = memory def __getattr__(self, name: str) -> Any: return getattr(self.base_output, name) def __getitem__(self, key: Any) -> Any: return self.base_output[key] class QwenDynamicMemoryModel(nn.Module): """Qwen3.5 with caller-owned persistent dynamic memory state.""" def __init__( self, base_model: nn.Module, memory_config: Optional[QwenMemoryConfig] = None, *, freeze_backbone: bool = True, ) -> None: super().__init__() self.base_model = base_model text_config = base_model.config.text_config self.memory_config = memory_config or QwenMemoryConfig() self.layer_indices = self.memory_config.resolved_layers(text_config.num_hidden_layers) memory_type = NativeQwenDynamicMemory if self.memory_config.native_mode else QwenDynamicMemory self.memory = memory_type(text_config.hidden_size, self.memory_config) self.text_retriever = ( NaturalLanguageRetriever(text_config.hidden_size) if self.memory_config.natural_language_memory else None ) policy_type = ( AutomaticMemoryPolicyV2 if self.memory_config.automatic_memory_policy_version >= 2 else AutomaticMemoryPolicy ) self.memory_policy = ( policy_type(text_config.hidden_size) if self.memory_config.natural_language_memory and self.memory_config.automatic_memory else None ) self.memory_router_v2: Optional[MemoryRouterV2] = None self.memory_os_v2: Optional[MemoryOSV2] = None self._memory_router_v2_ready = False if self.memory_config.hierarchical_memory or self.memory_config.memory_version >= 2: self.memory_router_v2 = MemoryRouterV2( text_config.hidden_size, router_dim=self.memory_config.memory_router_dim, num_heads=self.memory_config.memory_router_heads, max_hops=self.memory_config.memory_max_hops, ) self.memory_os_v2 = self._new_memory_os_v2(text_config.hidden_size) self._text_retriever_ready = False self._memory_policy_ready = False self.runtime = _MemoryRuntime(self.memory) self.runtime.output_embeddings = self.base_model.get_output_embeddings() self._persistent_memory: Optional[Tensor] = None self.register_buffer("persistent_memory", torch.empty(0), persistent=True) self.register_buffer( "persistent_text_token_ids", torch.empty(0, dtype=torch.long), persistent=True, ) self.register_buffer( "persistent_text_token_mask", torch.empty(0, dtype=torch.bool), persistent=True, ) self.register_buffer( "persistent_text_slot_valid", torch.empty(0, dtype=torch.bool), persistent=True, ) self.register_buffer( "persistent_text_slot_keys", torch.empty(0), persistent=True, ) self.register_buffer( "persistent_text_slot_age", torch.empty(0, dtype=torch.long), persistent=True, ) self.register_buffer( "persistent_text_write_counter", torch.empty(0, dtype=torch.long), persistent=True, ) self.register_buffer( "persistent_text_key_token_ids", torch.empty(0, dtype=torch.long), persistent=True, ) self.register_buffer( "persistent_text_key_token_mask", torch.empty(0, dtype=torch.bool), persistent=True, ) if freeze_backbone: for parameter in self.base_model.parameters(): parameter.requires_grad_(False) language_model = self.base_model.model.language_model self._memory_adapters: list[MemoryLayerAdapter] = [] for layer_index in self.layer_indices: layer = language_model.layers[layer_index] adapter = MemoryLayerAdapter( layer, self.runtime, read=True, write=layer_index == self.layer_indices[-1], mode=self.memory_config.mode, blend_init=self.memory_config.blend_init, ) language_model.layers[layer_index] = adapter self._memory_adapters.append(adapter) self.memory.to(self._find_layer_device()) if self.memory_router_v2 is not None: self.memory_router_v2.to(self._find_layer_device()) if self.text_retriever is not None: self.text_retriever.to(self._find_layer_device()) if self.memory_policy is not None: self.memory_policy.to(self._find_layer_device()) if self.memory_config.persistent_memory: self.persistent_memory = self.memory.initial_state( 1, device=self._find_layer_device(), ) self._persistent_memory = self.persistent_memory.detach() if hasattr(self.base_model.config, "use_cache"): self.base_model.config.use_cache = False if hasattr(self.base_model.config, "text_config"): self.base_model.config.text_config.use_cache = False def _new_memory_os_v2(self, hidden_size: int) -> MemoryOSV2: """Create a V2 store from the configured page and candidate limits.""" if self.memory_router_v2 is None: raise RuntimeError("cannot create V2 memory without a router") tier_store = None if self.memory_config.memory_storage_mode == "tiered": if not self.memory_config.memory_storage_path: raise ValueError("tiered memory requires memory_storage_path") tier_store = TieredMemoryStoreV2( self.memory_config.memory_storage_path, key_dim=self.memory_config.memory_router_dim, page_capacity=self.memory_config.memory_page_capacity, ) bank = PagedMemoryBankV2( hidden_size, page_capacity=self.memory_config.memory_page_capacity, max_pages=self.memory_config.memory_max_pages, hot_pages=self.memory_config.memory_hot_pages, top_k_pages=self.memory_config.memory_top_k_pages, top_k_records=self.memory_config.memory_top_k_records, max_hops=self.memory_config.memory_max_hops, router=self.memory_router_v2, key_dim=self.memory_config.memory_router_dim, coarse_index_bits=self.memory_config.memory_coarse_index_bits, tier_store=tier_store, max_resident_pages=self.memory_config.memory_resident_pages, runtime_device=self._find_layer_device(), gpu_cache_records=self.memory_config.memory_gpu_cache_records, gpu_cache_tokens=self.memory_config.memory_gpu_cache_tokens, gpu_cache_reserve_mb=self.memory_config.memory_gpu_cache_reserve_mb, gpu_cache_adaptive=self.memory_config.memory_gpu_cache_adaptive, ) return MemoryOSV2( hidden_size, router=self.memory_router_v2, bank=bank, kv_budget=KVBudgetManagerV2( max_tokens=self.memory_config.kv_budget_tokens, hard_max_tokens=self.memory_config.kv_hard_max_tokens, compaction_trigger=self.memory_config.kv_compaction_trigger, keep_recent_tokens=self.memory_config.kv_keep_recent_tokens, ), read_threshold=self.memory_config.memory_v2_read_threshold, write_threshold=self.memory_config.memory_v2_write_threshold, ) def _ensure_text_memory(self, batch_size: int, *, device: torch.device) -> None: """Create the fixed-size model-owned natural-language memory bank.""" if not self.memory_config.natural_language_memory: return shape = ( batch_size, self.memory_config.memory_slots, self.memory_config.text_memory_tokens, ) if tuple(self.persistent_text_token_ids.shape) != shape: self.persistent_text_token_ids = torch.zeros( shape, dtype=torch.long, device=device, ) self.persistent_text_token_mask = torch.zeros( shape, dtype=torch.bool, device=device, ) self.persistent_text_slot_valid = torch.zeros( batch_size, self.memory_config.memory_slots, dtype=torch.bool, device=device, ) hidden_size = self.memory.hidden_size self.persistent_text_slot_keys = torch.zeros( batch_size, self.memory_config.memory_slots, hidden_size, dtype=torch.float32, device=device, ) self.persistent_text_slot_age = torch.full( (batch_size, self.memory_config.memory_slots), -1, dtype=torch.long, device=device, ) self.persistent_text_write_counter = torch.zeros( batch_size, dtype=torch.long, device=device, ) key_shape = ( batch_size, self.memory_config.memory_slots, self.memory_config.text_memory_key_tokens, ) self.persistent_text_key_token_ids = torch.zeros( key_shape, dtype=torch.long, device=device, ) self.persistent_text_key_token_mask = torch.zeros( key_shape, dtype=torch.bool, device=device, ) def _bind_text_memory(self, batch_size: int, *, device: torch.device) -> None: """Bind the current text bank to the per-call runtime.""" if not self.memory_config.natural_language_memory: self.runtime.text_token_ids = None self.runtime.text_token_mask = None self.runtime.text_slot_valid = None return if self.memory_config.persistent_memory and self.runtime.use_persistent_state: self._ensure_text_memory(1, device=device) else: self._ensure_text_memory(batch_size, device=device) if not (self.memory_config.persistent_memory and self.runtime.use_persistent_state): shape = ( batch_size, self.memory_config.memory_slots, self.memory_config.text_memory_tokens, ) key_shape = ( batch_size, self.memory_config.memory_slots, self.memory_config.text_memory_key_tokens, ) if ( self.runtime.text_token_ids is None or tuple(self.runtime.text_token_ids.shape) != shape or self.runtime.text_key_token_ids is None or tuple(self.runtime.text_key_token_ids.shape) != key_shape ): self.runtime.text_token_ids = torch.zeros(shape, dtype=torch.long, device=device) self.runtime.text_token_mask = torch.zeros(shape, dtype=torch.bool, device=device) self.runtime.text_slot_valid = torch.zeros( batch_size, self.memory_config.memory_slots, dtype=torch.bool, device=device ) self.runtime.text_slot_keys = torch.zeros( batch_size, self.memory_config.memory_slots, self.memory.hidden_size, dtype=torch.float32, device=device ) self.runtime.text_slot_age = torch.full( (batch_size, self.memory_config.memory_slots), -1, dtype=torch.long, device=device ) self.runtime.text_write_counter = torch.zeros(batch_size, dtype=torch.long, device=device) self.runtime.text_key_token_ids = torch.zeros(key_shape, dtype=torch.long, device=device) self.runtime.text_key_token_mask = torch.zeros(key_shape, dtype=torch.bool, device=device) return if self.persistent_text_token_ids.shape[0] == batch_size: self.runtime.text_token_ids = self.persistent_text_token_ids self.runtime.text_token_mask = self.persistent_text_token_mask self.runtime.text_slot_valid = self.persistent_text_slot_valid self.runtime.text_slot_keys = self.persistent_text_slot_keys self.runtime.text_slot_age = self.persistent_text_slot_age self.runtime.text_write_counter = self.persistent_text_write_counter self.runtime.text_key_token_ids = self.persistent_text_key_token_ids self.runtime.text_key_token_mask = self.persistent_text_key_token_mask return if self.persistent_text_token_ids.shape[0] == 1: self.runtime.text_token_ids = self.persistent_text_token_ids.expand( batch_size, -1, -1 ).clone() self.runtime.text_token_mask = self.persistent_text_token_mask.expand( batch_size, -1, -1 ).clone() self.runtime.text_slot_valid = self.persistent_text_slot_valid.expand( batch_size, -1 ).clone() self.runtime.text_slot_keys = self.persistent_text_slot_keys.expand( batch_size, -1, -1 ).clone() self.runtime.text_slot_age = self.persistent_text_slot_age.expand( batch_size, -1 ).clone() self.runtime.text_write_counter = self.persistent_text_write_counter.expand( batch_size ).clone() self.runtime.text_key_token_ids = self.persistent_text_key_token_ids.expand( batch_size, -1, -1 ).clone() self.runtime.text_key_token_mask = self.persistent_text_key_token_mask.expand( batch_size, -1, -1 ).clone() return raise ValueError( "natural-language memory batch does not match the loaded persistent bank" ) def _clear_text_memory(self, mask: Optional[Tensor] = None) -> None: if self.runtime.text_token_ids is None: return if mask is None: self.runtime.text_token_ids.zero_() self.runtime.text_token_mask.zero_() self.runtime.text_slot_valid.zero_() self.runtime.text_slot_keys.zero_() self.runtime.text_slot_age.fill_(-1) self.runtime.text_write_counter.zero_() self.runtime.text_key_token_ids.zero_() self.runtime.text_key_token_mask.zero_() self.runtime.text_last_written_slot = None self.runtime.text_prefix_used = False return mask = mask.to(device=self.runtime.text_token_ids.device, dtype=torch.bool) self.runtime.text_token_ids[mask] = 0 self.runtime.text_token_mask[mask] = False self.runtime.text_slot_valid[mask] = False self.runtime.text_slot_keys[mask] = 0 self.runtime.text_slot_age[mask] = -1 self.runtime.text_write_counter[mask] = 0 self.runtime.text_key_token_ids[mask] = 0 self.runtime.text_key_token_mask[mask] = False self.runtime.text_last_written_slot = None @torch.no_grad() def _encode_model_key(self, input_ids: Tensor, attention_mask: Tensor) -> Tensor: """Encode a memory key with the frozen original Qwen representation.""" previous_read = self.runtime.read_enabled previous_update = self.runtime.update_enabled self.runtime.read_enabled = False self.runtime.update_enabled = False try: embedding_layer = self.base_model.get_input_embeddings() input_ids = input_ids.to(embedding_layer.weight.device) attention_mask = attention_mask.to(input_ids.device) output = self.base_model( input_ids=input_ids, attention_mask=attention_mask, use_cache=False, output_hidden_states=True, ) hidden = output.hidden_states[-1].float() weights = attention_mask.to(device=hidden.device, dtype=hidden.dtype).unsqueeze(-1) key = (hidden * weights).sum(dim=1) / weights.sum(dim=1).clamp_min(1.0) return F.normalize(key, dim=-1) finally: self.runtime.read_enabled = previous_read self.runtime.update_enabled = previous_update @torch.no_grad() def compact_context_for_kv( self, input_ids: Tensor, attention_mask: Optional[Tensor] = None, *, archive: bool = True, chunk_tokens: Optional[int] = None, ) -> tuple[Tensor, Tensor, dict[str, Any]]: """Archive the old prefix and return a bounded hot-context window. This is the model-side boundary between the exact working KV and the lossy long-term memory path. It never sends the current token to all pages: each archived chunk gets one address, and future reads still use the V2 coarse-page -> exact-record -> Top-K route. The method accepts already-tokenized input because it is also used by generation. The archived token sequence is kept losslessly inside a V2 record; its semantic address is produced by the frozen Qwen hidden representation. A caller may disable ``archive`` for measurement, in which case the method only reports the retention plan. """ if input_ids.ndim != 2: raise ValueError("input_ids must have shape [batch, sequence]") if attention_mask is None: attention_mask = torch.ones_like(input_ids) if attention_mask.shape != input_ids.shape: raise ValueError("attention_mask must have the same shape as input_ids") budget = self.memory_os_v2.kv_budget if self.memory_os_v2 is not None else None if ( self.memory_os_v2 is None or not self.memory_config.auto_compact_context or budget is None ): return input_ids, attention_mask, { "compacted": False, "reason": "disabled", "archived_records": 0, "original_tokens": [int(item) for item in attention_mask.sum(dim=-1).tolist()], "retained_tokens": [int(item) for item in attention_mask.sum(dim=-1).tolist()], } chunk_size = int(chunk_tokens or self.memory_config.context_chunk_tokens) if chunk_size < 1: raise ValueError("chunk_tokens must be positive") hot_limit = int(budget.max_tokens) pending: list[dict[str, Any]] = [] pending_previous: list[Optional[int]] = [] retained: list[Tensor] = [] original_lengths: list[int] = [] retained_lengths: list[int] = [] for batch_index in range(input_ids.shape[0]): valid = input_ids[batch_index][attention_mask[batch_index].to(dtype=torch.bool)] original_lengths.append(int(valid.numel())) if valid.numel() <= hot_limit: retained.append(valid) retained_lengths.append(int(valid.numel())) continue archived_prefix = valid[:-hot_limit] retained_window = valid[-hot_limit:] retained.append(retained_window) retained_lengths.append(int(retained_window.numel())) previous_index: Optional[int] = None for start in range(0, int(archived_prefix.numel()), chunk_size): end = min(start + chunk_size, int(archived_prefix.numel())) if len(pending) >= self.memory_config.context_archive_max_records: raise RuntimeError( "context archive record limit reached; increase " "context_archive_max_records before compacting this input" ) chunk = archived_prefix[start:end].detach() chunk_mask = torch.ones_like(chunk, dtype=torch.bool) key = self._encode_model_key(chunk.unsqueeze(0), chunk_mask.unsqueeze(0))[0] pending.append( { "text": f"context_chunk:{batch_index}:{start}:{end}", "key": key, "summary": key, "memory_type": "context_chunk", # Context chunks are ordered evidence, not competing # values of one fact. Leaving the fact-conflict # fields empty prevents ordinary version resolution # from superseding every earlier chunk in a document. "entity": "", "attribute": "", "value": "", "importance": 0.55, "confidence": 0.80, "source": "kv_compaction", "evidence": [ f"batch:{batch_index}", f"token_range:{start}:{end}", f"token_count:{end - start}", ], "token_ids": chunk, "token_mask": chunk_mask, "trusted": True, "force": True, } ) pending_previous.append(previous_index) previous_index = len(pending) - 1 results: list[tuple[Any, str]] = [] if pending and archive: results = self.memory_os_v2.write_batch(pending) # Add a forward chain after the atomic insert. This gives the # multi-hop reader a deterministic path through adjacent chunks # without making page allocation depend on future record IDs. for result_index, previous_index in enumerate(pending_previous): if previous_index is None: continue record = results[result_index][0] previous_record = results[previous_index][0] if previous_record.record_id not in record.related_ids: record.related_ids.append(previous_record.record_id) self.memory_os_v2.bank._store_record(record) max_retained = max(retained_lengths, default=0) compacted_ids = torch.zeros( (input_ids.shape[0], max_retained), dtype=input_ids.dtype, device=input_ids.device ) compacted_mask = torch.zeros( (input_ids.shape[0], max_retained), dtype=attention_mask.dtype, device=input_ids.device ) for batch_index, values in enumerate(retained): if values.numel() == 0: continue compacted_ids[batch_index, : values.numel()] = values.to(input_ids.device) compacted_mask[batch_index, : values.numel()] = 1 return compacted_ids, compacted_mask, { "compacted": any(original > retained for original, retained in zip(original_lengths, retained_lengths)), "archived_records": len(results), "archived_tokens": sum(original - retained for original, retained in zip(original_lengths, retained_lengths)), "original_tokens": original_lengths, "retained_tokens": retained_lengths, "hot_limit": hot_limit, "archive_requested": bool(archive), } @torch.no_grad() def _rank_v2_text_matches( self, query_key: Tensor, query_token_ids: Tensor, ) -> list[tuple[float, int, Any]]: """Rank active V2 text records without materializing their payloads.""" if self.memory_os_v2 is None: return [] active = [ record for record in self.memory_os_v2.bank.records.values() if record.status == "active" ] if not active: return [] keys = torch.stack([record.key for record in active], dim=0) scores = self.memory_os_v2.bank._score_candidates(query_key, keys).reshape(-1).cpu() query_unique = torch.unique(query_token_ids.detach().reshape(-1).cpu()) ranked: list[tuple[float, int, Any]] = [] for index, record in enumerate(active): shared = 0 if record.token_ids is not None and query_unique.numel() > 0: own = torch.unique(record.token_ids.detach().reshape(-1).cpu()) shared = int(torch.isin(query_unique, own).sum().item()) ranked.append((float(scores[index].item()), shared, record)) ranked.sort(key=lambda item: (item[0], item[1], item[2].last_access), reverse=True) return ranked @torch.no_grad() def _forget_text_memory_by_key( self, key_input_ids: Tensor, key_attention_mask: Tensor, ) -> Tensor: """Erase the addressed natural-language record for a learned forget turn.""" erased = torch.zeros( key_input_ids.shape[0], dtype=torch.bool, device=key_input_ids.device ) if ( self.runtime.text_slot_valid is None or self.runtime.text_key_token_ids is None or self.runtime.text_key_token_mask is None or not bool(self.runtime.text_slot_valid.any()) ): return erased query_keys = self._encode_model_key(key_input_ids, key_attention_mask) for batch_index in range(key_input_ids.shape[0]): valid_slots = self.runtime.text_slot_valid[batch_index] if not bool(valid_slots.any()): continue query_ids = key_input_ids[batch_index][key_attention_mask[batch_index].bool()] query_ids = query_ids[: self.memory_config.text_memory_key_tokens] old_ids = self.runtime.text_key_token_ids[batch_index] old_mask = self.runtime.text_key_token_mask[batch_index] shared = ( (old_ids[:, :, None] == query_ids[None, None, :]) & old_mask[:, :, None] & valid_slots[:, None, None] ).any(dim=-1).sum(dim=-1).float() old_lengths = old_mask.sum(dim=-1).float() overlap = shared / torch.sqrt( old_lengths * float(max(1, query_ids.numel())) ).clamp_min(1.0) overlap = overlap.masked_fill(~valid_slots, -1.0) best_overlap, best_slot = overlap.max(dim=-1) best_score = best_overlap if self.text_retriever is not None and self._text_retriever_ready: slot_keys = F.normalize( self.runtime.text_slot_keys[batch_index].to(query_keys.device, dtype=query_keys.dtype), dim=-1, ) learned = torch.sigmoid( self.text_retriever(query_keys[batch_index].unsqueeze(0), slot_keys.unsqueeze(0))[0] ).masked_fill(~valid_slots.to(query_keys.device), -1.0) learned_score, learned_slot = learned.max(dim=-1) if float(learned_score) >= float(best_score): best_score = learned_score best_slot = learned_slot if float(best_score) < self.memory_config.text_memory_overlap_threshold and not ( self.text_retriever is not None and self._text_retriever_ready and float(best_score) >= 0.65 ): continue slot = int(best_slot.item()) self._clear_legacy_memory_slot(slot) if self.memory_os_v2 is not None and self.runtime.use_persistent_state: ranked = self._rank_v2_text_matches( query_keys[batch_index], query_ids, ) for score, shared, record in ranked: # The hot-bank matcher has already located the target. # V2 records created as independent fragments have no # slot index, so use the frozen-Qwen address plus token # evidence to retract only the matching old record. same_hot_slot = record.slot_index == slot if same_hot_slot or (score >= 0.60 and shared >= 2): self.memory_os_v2.retract_record(record.record_id) if not same_hot_slot and score < 0.78: break erased[batch_index] = True return erased def _write_text_memory( self, input_ids: Optional[Tensor], attention_mask: Optional[Tensor], text_input_ids: Optional[Tensor] = None, text_attention_mask: Optional[Tensor] = None, key_input_ids: Optional[Tensor] = None, key_attention_mask: Optional[Tensor] = None, storage_input_ids: Optional[Tensor] = None, storage_attention_mask: Optional[Tensor] = None, force_write: bool = False, memory_text: Optional[str] = None, ) -> None: """Commit one conversational write into the selected internal slot. The learned controller decides whether a turn is a durable fact and which slot it belongs to. The exact token sequence is then copied into the model-owned bank so multi-token names, values and punctuation are not lossy-compressed into a single output token. """ # The bank is consumed as an internal prefix before the next Qwen # chat turn. Store the valid system-message form for generation; # keep the exact user fact separately in ``text_key_token_ids`` for # duplicate detection and retrieval overlap. A raw fact placed # before ``<|im_start|>user`` is not a reliable conditioning channel. storage_ids = ( text_input_ids if text_input_ids is not None else (storage_input_ids if storage_input_ids is not None else input_ids) ) storage_mask = ( text_attention_mask if text_input_ids is not None else (storage_attention_mask if storage_input_ids is not None else attention_mask) ) key_ids = key_input_ids if key_input_ids is not None else storage_ids key_mask = key_attention_mask if key_input_ids is not None else storage_mask if ( not self.memory_config.natural_language_memory or storage_ids is None or self.runtime.text_token_ids is None or self.runtime.text_slot_keys is None or self.runtime.text_key_token_ids is None ): return self.runtime.text_last_written_slot = torch.full( (storage_ids.shape[0],), -1, dtype=torch.long, device=self.runtime.text_slot_valid.device, ) address = self.memory.last_write_address write_probability = self.memory.last_write_probability if address is None or key_ids is None: return if write_probability is None: write_probability = address.max(dim=-1, keepdim=True).values if force_write: should_write = torch.ones( storage_ids.shape[0], dtype=torch.bool, device=storage_ids.device ) elif ( self.memory_config.automatic_memory and self._memory_policy_ready and self.runtime.auto_memory_probability is not None ): # High recall is intentional: the policy is trained to recognize # durable user information. It is the final automatic decision; # the older native write gate is not OR-ed here because it was # trained for continuous-state updates and can be over-eager on # question-shaped inputs. should_write = self.runtime.auto_memory_probability >= self.memory_config.auto_memory_threshold else: should_write = write_probability.squeeze(-1) >= self.memory_config.text_memory_write_threshold forget_probability = self.runtime.auto_memory_forget_probability if ( not force_write and forget_probability is not None and bool((forget_probability >= self.memory_config.auto_forget_threshold).any()) ): forget_mask = forget_probability >= self.memory_config.auto_forget_threshold forget_key_mask = ( key_mask if key_mask is not None else torch.ones_like(key_ids, dtype=torch.bool) ) erased = self._forget_text_memory_by_key(key_ids, forget_key_mask) should_write = should_write & ~(forget_mask & erased) if not force_write and memory_text: if looks_like_question(memory_text): should_write = torch.zeros_like(should_write, dtype=torch.bool) if storage_mask is None: storage_mask = torch.ones_like(storage_ids, dtype=torch.bool) else: storage_mask = storage_mask.to(device=storage_ids.device, dtype=torch.bool) if key_mask is None: key_mask = torch.ones_like(key_ids, dtype=torch.bool) else: key_mask = key_mask.to(device=key_ids.device, dtype=torch.bool) text_keys = self._encode_model_key(key_ids, key_mask) max_tokens = self.memory_config.text_memory_tokens for batch_index in range(storage_ids.shape[0]): if not bool(should_write[batch_index]): continue valid_ids = storage_ids[batch_index][storage_mask[batch_index]] if valid_ids.numel() == 0: continue valid_key_ids = key_ids[batch_index][key_mask[batch_index]] valid_key_ids = valid_key_ids[: self.memory_config.text_memory_key_tokens].detach().to( device=self.runtime.text_key_token_ids.device, dtype=torch.long, ) valid_ids = valid_ids[:max_tokens].detach().to( device=self.runtime.text_token_ids.device, dtype=torch.long, ) key = text_keys[batch_index] valid_slots = self.runtime.text_slot_valid[batch_index] if bool(valid_slots.any()): old_key_ids = self.runtime.text_key_token_ids[batch_index] old_key_mask = self.runtime.text_key_token_mask[batch_index] equal = old_key_ids[:, :, None] == valid_key_ids[None, None, :] overlap_count = ( equal & old_key_mask[:, :, None] & torch.ones( 1, 1, valid_key_ids.numel(), dtype=torch.bool, device=old_key_ids.device, ) ).any(dim=-1).sum(dim=-1).float() old_lengths = old_key_mask.sum(dim=-1).float() similarities = overlap_count / torch.sqrt( old_lengths * float(max(1, valid_key_ids.numel())) ).clamp_min(1.0) masked_similarities = similarities.masked_fill( ~valid_slots, torch.finfo(similarities.dtype).min, ) best_similarity, best_slot = masked_similarities.max(dim=-1) # Exact token equality is a stronger duplicate signal than # any learned semantic score. It makes repeated natural # language facts idempotent, even when the retriever has not # seen that exact name/value during training. key_length = valid_key_ids.numel() exact_match = ( (old_key_ids[:, :key_length] == valid_key_ids[None, :]) | (~old_key_mask[:, :key_length]) ).all(dim=-1) & valid_slots & ( old_key_mask.sum(dim=-1) == valid_key_ids.numel() ) exact_slots = exact_match.nonzero(as_tuple=False).flatten() if exact_slots.numel() > 0: best_similarity = torch.tensor(1.0, device=key.device) best_slot = exact_slots[0] if self.text_retriever is not None and self._text_retriever_ready: if exact_slots.numel() == 0: learned_similarity = torch.sigmoid( self.text_retriever( key.unsqueeze(0), self.runtime.text_slot_keys[batch_index].unsqueeze(0), )[0] ) learned_similarity = learned_similarity.masked_fill( ~valid_slots, torch.finfo(learned_similarity.dtype).min, ) learned_best, learned_slot = learned_similarity.max(dim=-1) # Shared words such as "我" and "请记住" are common in # many facts. Once the retriever is trained, use its # semantic same-attribute score for replacement so a new # fruit fact cannot evict a work-location fact merely # because both contain the same pronouns. if float(learned_best) >= 0.65: best_similarity = learned_best best_slot = learned_slot # Keep the exact token-overlap score when the learned # retriever is uncertain. Overwriting it with -1 # made obvious same-attribute updates look unrelated # merely because the paraphrase was outside the # retriever's training distribution. else: best_similarity = torch.tensor(-1.0, device=key.device) best_slot = torch.tensor(0, dtype=torch.long, device=key.device) if float(best_similarity) >= self.memory_config.text_memory_update_overlap_threshold: slot = int(best_slot.item()) else: free_slots = (~valid_slots).nonzero(as_tuple=False).flatten() if free_slots.numel() > 0: slot = int(free_slots[0].item()) else: slot = int(self.runtime.text_slot_age[batch_index].argmin().item()) self.runtime.text_token_ids[batch_index, slot].zero_() self.runtime.text_token_mask[batch_index, slot].zero_() self.runtime.text_token_ids[batch_index, slot, : valid_ids.numel()].copy_(valid_ids) self.runtime.text_token_mask[batch_index, slot, : valid_ids.numel()] = True self.runtime.text_slot_valid[batch_index, slot] = True self.runtime.text_slot_keys[batch_index, slot].copy_(key.to(self.runtime.text_slot_keys.device)) self.runtime.text_key_token_ids[batch_index, slot].zero_() self.runtime.text_key_token_mask[batch_index, slot].zero_() self.runtime.text_key_token_ids[batch_index, slot, : valid_key_ids.numel()].copy_(valid_key_ids) self.runtime.text_key_token_mask[batch_index, slot, : valid_key_ids.numel()] = True self.runtime.text_write_counter[batch_index] += 1 self.runtime.text_slot_age[batch_index, slot] = self.runtime.text_write_counter[batch_index] self.runtime.text_last_written_slot[batch_index] = slot if self.memory_os_v2 is not None and self.runtime.use_persistent_state: confidence = float(write_probability[batch_index].reshape(-1)[0].item()) if force_write: confidence = 1.0 # The 16-slot bank is only the hot cache. New, unrelated # conversational fragments must remain independent V2 # records instead of superseding whatever happened to be in # the same hot slot. Reuse a V2 slot index only when the # hot-bank matcher already established that this is a # duplicate/update of an existing fact. v2_slot_index = ( slot if float(best_similarity) >= self.memory_config.text_memory_update_overlap_threshold else -1 ) if v2_slot_index >= 0 and self.memory_os_v2 is not None: # A semantic replacement should retire the matching # active record, while unrelated fragments remain active # even if they once occupied the same hot-cache slot. for score, shared, record in self._rank_v2_text_matches( key, valid_key_ids, ): if record.slot_index == slot or (score >= 0.60 and shared >= 2): self.memory_os_v2.retract_record(record.record_id) if record.slot_index != slot and score < 0.78: break self.memory_os_v2.write( text=memory_text or f"memory_slot:{slot}", key=key, summary=key, memory_type="episodic_text", importance=max(confidence, 0.5), confidence=confidence, source="explicit" if force_write else "automatic", evidence=[f"token_count:{int(valid_ids.numel())}"], slot_index=v2_slot_index, token_ids=valid_ids, token_mask=torch.ones_like(valid_ids, dtype=torch.bool), trusted=force_write or confidence >= self.memory_config.memory_v2_write_threshold, force=force_write, ) def _probe_text_retrieval( self, input_ids: Tensor, attention_mask: Tensor, ) -> tuple[Optional[Tensor], Optional[Tensor]]: """Compute query-dependent similarity against internal text keys.""" if ( not self.memory_config.natural_language_memory or self.runtime.text_slot_valid is None or self.runtime.text_slot_keys is None or self.runtime.text_key_token_ids is None or self.runtime.text_key_token_mask is None or not bool(self.runtime.text_slot_valid.any()) ): return None, None query_key = self._encode_model_key(input_ids, attention_mask) self.runtime.v2_query_key = query_key.detach() slot_keys = F.normalize( self.runtime.text_slot_keys.to(device=query_key.device, dtype=query_key.dtype), dim=-1, ) if self.text_retriever is not None and self._text_retriever_ready: learned_logits = self.text_retriever(query_key, slot_keys) dense_similarity = torch.sigmoid(learned_logits) else: dense_similarity = torch.einsum("bh,bsh->bs", query_key, slot_keys) # Dense similarity alone is too permissive for short Chinese queries: # common tokens such as "我" and "的" can make unrelated memories # look relevant. Combine it with exact token overlap from the # internally stored key, yielding a conservative hybrid score. key_token_ids = self.runtime.text_key_token_ids.to(device=input_ids.device) key_token_mask = self.runtime.text_key_token_mask.to(device=input_ids.device, dtype=torch.bool) query_token_ids = input_ids.to(device=key_token_ids.device) query_token_mask = attention_mask.to(device=key_token_ids.device, dtype=torch.bool) equal = query_token_ids[:, None, :, None] == key_token_ids[:, :, None, :] equal &= query_token_mask[:, None, :, None] & key_token_mask[:, :, None, :] shared_tokens = equal.any(dim=-1).sum(dim=-1).float() query_lengths = query_token_mask.sum(dim=-1, keepdim=True).float() key_lengths = key_token_mask.sum(dim=-1).float() overlap = shared_tokens / torch.sqrt(query_lengths * key_lengths).clamp_min(1.0) if self.text_retriever is not None and self._text_retriever_ready: scores = 0.85 * dense_similarity + 0.15 * overlap.clamp(0.0, 1.0) else: dense_score = (dense_similarity + 1.0).clamp(0.0, 2.0) * 0.5 scores = 0.4 * dense_score + 0.6 * overlap.clamp(0.0, 1.0) self.runtime.text_read_overlap = overlap scores = scores.masked_fill( ~self.runtime.text_slot_valid.to(device=scores.device), torch.finfo(scores.dtype).min, ) return scores, scores.max(dim=-1).values def _build_text_prefix( self, input_ids: Tensor, attention_mask: Tensor, query_text: str = "", ) -> tuple[Optional[Tensor], Optional[Tensor], int]: """Retrieve text memory and build an internal prefix for Qwen.""" address, relevance = self._probe_text_retrieval(input_ids, attention_mask) self.runtime.text_read_slots = None self.runtime.v2_last_decisions = [] self.runtime.text_read_relevance = relevance self.runtime.text_prefix_used = False if ( self.memory_os_v2 is not None and self._memory_router_v2_ready and self.runtime.v2_query_key is None ): self.runtime.v2_query_key = self._encode_model_key(input_ids, attention_mask).detach() # V2 is the scalable address path. It first selects a bounded set of # pages through the coarse index, then reranks only records in those # pages. The legacy fixed bank remains a compatibility fallback for # old checkpoints that do not contain a trained V2 router. if ( self.memory_os_v2 is not None and self._memory_router_v2_ready and self.runtime.v2_query_key is not None ): v2_prefix_parts: list[Tensor] = [] v2_prefix_masks: list[Tensor] = [] v2_slots: list[list[int]] = [] for batch_index in range(input_ids.shape[0]): records, decision = self.memory_os_v2.read( query_key=self.runtime.v2_query_key[batch_index], query_text=query_text, query_token_ids=input_ids[batch_index], top_k_pages=self.memory_config.memory_top_k_pages, top_k_records=self.memory_config.memory_top_k_records, max_hops=self.memory_config.memory_max_hops, ) self.runtime.v2_last_decisions.append(asdict(decision)) ids_parts: list[Tensor] = [] slot_list: list[int] = [] for record in records: ids: Optional[Tensor] = None if record.token_ids is not None: cached_ids, cached_mask = self.memory_os_v2.bank.gpu_record_payload(record) if cached_ids is not None: if cached_mask is not None and cached_mask.numel() == cached_ids.numel(): ids = cached_ids[cached_mask] else: ids = cached_ids elif ( self.runtime.text_token_ids is not None and self.runtime.text_token_mask is not None and 0 <= record.slot_index < self.runtime.text_token_ids.shape[1] ): slot = record.slot_index slot_mask = self.runtime.text_token_mask[batch_index, slot] ids = self.runtime.text_token_ids[batch_index, slot][slot_mask].detach().cpu() if ids is not None and ids.numel() > 0: ids_parts.append(ids) if record.slot_index >= 0: slot_list.append(record.slot_index) if ids_parts: v2_prefix_parts.append(torch.cat(ids_parts, dim=0)) v2_prefix_masks.append(torch.ones_like(v2_prefix_parts[-1], dtype=torch.bool)) else: v2_prefix_parts.append(torch.zeros(0, dtype=torch.long, device=input_ids.device)) v2_prefix_masks.append(torch.zeros(0, dtype=torch.bool, device=input_ids.device)) v2_slots.append(slot_list) v2_prefix_length = max((part.numel() for part in v2_prefix_parts), default=0) if v2_prefix_length > 0: prefix_ids = torch.zeros( input_ids.shape[0], v2_prefix_length, dtype=input_ids.dtype, device=input_ids.device ) prefix_mask = torch.zeros( input_ids.shape[0], v2_prefix_length, dtype=attention_mask.dtype, device=input_ids.device ) for batch_index, (ids, mask) in enumerate(zip(v2_prefix_parts, v2_prefix_masks)): prefix_ids[batch_index, : ids.numel()] = ids.to(input_ids.device) prefix_mask[batch_index, : mask.numel()] = mask.to(attention_mask.dtype) self.runtime.text_read_slots = torch.tensor( [slots + [-1] * max(0, self.memory_config.memory_top_k_records - len(slots)) for slots in v2_slots], dtype=torch.long, device=input_ids.device, ) self.runtime.text_prefix_used = True return prefix_ids, prefix_mask, v2_prefix_length if address is None or self.runtime.text_slot_valid is None: return None, None, 0 valid = self.runtime.text_slot_valid scores = address.masked_fill(~valid, torch.finfo(address.dtype).min) top_k = min(self.memory_config.text_memory_top_k, scores.shape[-1]) top_scores, top_slots = scores.topk(top_k, dim=-1) selected = top_scores >= self.memory_config.text_memory_threshold # If exactly one stored memory exists, its relevance still has to pass # the threshold; this prevents unrelated questions from receiving an # arbitrary memory snippet and reduces hallucinated personal facts. if relevance is not None: selected &= relevance[:, None] >= self.memory_config.text_memory_threshold if ( self.runtime.text_read_overlap is not None and not (self.text_retriever is not None and self._text_retriever_ready) ): selected_overlap = self.runtime.text_read_overlap.gather(1, top_slots) selected &= selected_overlap >= self.memory_config.text_memory_overlap_threshold self.runtime.text_read_slots = top_slots if not bool(selected.any()): return None, None, 0 self.runtime.text_prefix_used = True bank_ids = self.runtime.text_token_ids bank_mask = self.runtime.text_token_mask if bank_ids is None or bank_mask is None: return None, None, 0 prefix_parts: list[Tensor] = [] prefix_masks: list[Tensor] = [] for batch_index in range(input_ids.shape[0]): ids_parts: list[Tensor] = [] mask_parts: list[Tensor] = [] for rank in range(top_k): if not bool(selected[batch_index, rank]): continue slot = int(top_slots[batch_index, rank].item()) slot_mask = bank_mask[batch_index, slot] ids_parts.append(bank_ids[batch_index, slot][slot_mask]) mask_parts.append(torch.ones_like(ids_parts[-1], dtype=torch.bool)) if ids_parts: prefix_parts.append(torch.cat(ids_parts, dim=0)) prefix_masks.append(torch.cat(mask_parts, dim=0)) else: prefix_parts.append(torch.zeros(0, dtype=torch.long, device=input_ids.device)) prefix_masks.append(torch.zeros(0, dtype=torch.bool, device=input_ids.device)) prefix_length = max((part.numel() for part in prefix_parts), default=0) if prefix_length == 0: return None, None, 0 prefix_ids = torch.zeros( input_ids.shape[0], prefix_length, dtype=input_ids.dtype, device=input_ids.device ) prefix_mask = torch.zeros( input_ids.shape[0], prefix_length, dtype=attention_mask.dtype, device=input_ids.device ) for batch_index, (ids, mask) in enumerate(zip(prefix_parts, prefix_masks)): prefix_ids[batch_index, : ids.numel()] = ids.to(input_ids.device) prefix_mask[batch_index, : mask.numel()] = mask.to(attention_mask.dtype) return prefix_ids, prefix_mask, prefix_length def _find_layer_device(self) -> torch.device: for index in self.layer_indices: layer = self.base_model.model.language_model.layers[index] # In blend mode the adapter owns a trainable scalar. Inspect the # original layer first so a CPU-created scalar cannot mislead the # device choice for input_ids and the memory module. layer_for_device = getattr(layer, "inner", layer) for parameter in layer_for_device.parameters(): if parameter.device.type != "meta": return parameter.device return self.base_model.get_input_embeddings().weight.device @property def trainable_parameters(self): parameters = [parameter for parameter in self.memory.parameters() if parameter.requires_grad] if self.text_retriever is not None: parameters.extend( parameter for parameter in self.text_retriever.parameters() if parameter.requires_grad ) if self.memory_policy is not None: parameters.extend( parameter for parameter in self.memory_policy.parameters() if parameter.requires_grad ) if self.memory_router_v2 is not None: parameters.extend( parameter for parameter in self.memory_router_v2.parameters() if parameter.requires_grad ) for adapter in self._memory_adapters: blend_logit = getattr(adapter, "blend_logit", None) if blend_logit is not None and blend_logit.requires_grad: parameters.append(blend_logit) return iter(parameters) @torch.no_grad() def read_hierarchical_memory( self, query_key: Tensor, *, query_text: str = "", query_token_ids: Optional[Tensor] = None, top_k_pages: Optional[int] = None, top_k_records: Optional[int] = None, max_hops: Optional[int] = None, ) -> tuple[list[Any], Any]: """Read V2 memory through its bounded page and record router.""" if self.memory_os_v2 is None: raise RuntimeError("hierarchical memory is disabled in memory_config") return self.memory_os_v2.read( query_key=query_key, query_text=query_text, query_token_ids=query_token_ids, top_k_pages=top_k_pages, top_k_records=top_k_records, max_hops=max_hops, ) @torch.no_grad() def write_hierarchical_memory(self, **kwargs: Any) -> tuple[Any, str]: """Write one versioned V2 record owned by the model checkpoint.""" if self.memory_os_v2 is None: raise RuntimeError("hierarchical memory is disabled in memory_config") return self.memory_os_v2.write(**kwargs) def memory_v2_stats(self) -> dict[str, Any]: if self.memory_os_v2 is None: return {"enabled": False} output = dict(self.memory_os_v2.stats()) output["enabled"] = True output["router_ready"] = self._memory_router_v2_ready return output def list_memory_records( self, *, query_text: str = "", status: str = "active", limit: int = 100, offset: int = 0, ) -> list[dict[str, Any]]: """Return a safe, tensor-free view of model-owned memory records.""" if self.memory_os_v2 is None: return [] return [ memory_record_to_dict(record) for record in self.memory_os_v2.list_records( query_text=query_text, status=status, limit=limit, offset=offset, ) ] def get_memory_record(self, record_id: str) -> dict[str, Any]: if self.memory_os_v2 is None: raise KeyError(record_id) record = self.memory_os_v2.bank._resolve_record(record_id) return memory_record_to_dict(record) def _clear_legacy_memory_slot(self, slot_index: int) -> None: """Remove a V1 compatibility copy when a V2 record is edited/retracted.""" if slot_index < 0: return fields = ( ("persistent_text_token_ids", "text_token_ids", 0), ("persistent_text_token_mask", "text_token_mask", 0), ("persistent_text_slot_valid", "text_slot_valid", 0), ("persistent_text_slot_keys", "text_slot_keys", 0), ("persistent_text_slot_age", "text_slot_age", -1), ("persistent_text_key_token_ids", "text_key_token_ids", 0), ("persistent_text_key_token_mask", "text_key_token_mask", 0), ) for persistent_name, runtime_name, fill_value in fields: persistent = getattr(self, persistent_name, None) if isinstance(persistent, Tensor) and persistent.ndim >= 2 and slot_index < persistent.shape[1]: persistent[:, slot_index].fill_(fill_value) runtime = getattr(self.runtime, runtime_name, None) if isinstance(runtime, Tensor) and runtime.ndim >= 2 and slot_index < runtime.shape[1]: runtime[:, slot_index].fill_(fill_value) @torch.no_grad() def edit_memory_record( self, record_id: str, *, text: Optional[str] = None, entity: Optional[str] = None, attribute: Optional[str] = None, value: Optional[str] = None, importance: Optional[float] = None, confidence: Optional[float] = None, evidence: Optional[list[str]] = None, token_ids: Optional[Tensor] = None, token_mask: Optional[Tensor] = None, ) -> dict[str, Any]: """Edit memory by creating a versioned successor, never overwriting history.""" if self.memory_os_v2 is None: raise KeyError(record_id) old = self.memory_os_v2.bank._resolve_record(record_id) key = None if token_ids is not None: ids = token_ids.detach().reshape(1, -1).to(self._find_layer_device()) mask = ( token_mask.detach().reshape(1, -1).to(self._find_layer_device(), dtype=torch.long) if token_mask is not None else torch.ones_like(ids) ) key = self._encode_model_key(ids, mask)[0] self._clear_legacy_memory_slot(old.slot_index) record = self.memory_os_v2.edit_record( record_id, text=text, key=key, summary=key, entity=entity, attribute=attribute, value=value, importance=importance, confidence=confidence, evidence=evidence, token_ids=token_ids, token_mask=token_mask, ) return memory_record_to_dict(record) @torch.no_grad() def retract_memory_record(self, record_id: str) -> dict[str, Any]: """Retract a record and its compatibility-slot copy without erasing history.""" if self.memory_os_v2 is None: raise KeyError(record_id) record = self.memory_os_v2.bank._resolve_record(record_id) self._clear_legacy_memory_slot(record.slot_index) self.memory_os_v2.retract_record(record_id) return memory_record_to_dict(record) def audit_memory(self) -> dict[str, Any]: if self.memory_os_v2 is None: return {"healthy": True, "issues": [], "issue_count": 0, "stats": {"enabled": False}} return self.memory_os_v2.audit() def export_memory_records(self, *, limit: int = 10000, offset: int = 0) -> dict[str, Any]: records = self.list_memory_records(status="all", limit=limit, offset=offset) return { "format_version": 1, "records": records, "offset": offset, "limit": limit, "returned": len(records), "stats": self.memory_v2_stats(), } def flush_memory_storage(self) -> None: """Flush the optional warm/cold tier without rewriting model weights.""" if self.memory_os_v2 is not None: self.memory_os_v2.flush_storage() def close_memory_storage(self) -> None: """Close the optional durable page store before process shutdown.""" if self.memory_os_v2 is not None: self.memory_os_v2.close_storage() def clear_hierarchical_memory(self) -> None: """Clear the durable V2 page store while keeping router weights.""" if self.memory_router_v2 is None: return if self.memory_os_v2 is not None and self.memory_os_v2.bank.tier_store is not None: old_store = self.memory_os_v2.bank.tier_store old_store.clear() old_store.close() self.memory_os_v2 = self._new_memory_os_v2(self.memory.hidden_size) def reset_memory(self, batch_size: Optional[int] = None, *, device: Optional[torch.device] = None) -> None: """Explicitly clear the durable memory state. Use :meth:`reset_runtime_memory` for a temporary conversation or an evaluation case. Keeping these operations separate prevents a test or a branch from silently deleting a user's embedded checkpoint. """ self.runtime.use_persistent_state = True self.clear_hierarchical_memory() if self.memory_config.persistent_memory: device = device or self._find_layer_device() batch_size = batch_size or 1 self.persistent_memory = self.memory.initial_state(batch_size, device=device) self._persistent_memory = self.persistent_memory.detach() if self.memory_config.natural_language_memory: self._ensure_text_memory(batch_size, device=device) self._clear_text_memory() self.runtime.state = self._persistent_memory self.runtime.last_read = None self.runtime.auto_memory_probability = None self.runtime.raw_memory = None self.runtime.reset_mask = None self.runtime.text_token_ids = self.persistent_text_token_ids self.runtime.text_token_mask = self.persistent_text_token_mask self.runtime.text_slot_valid = self.persistent_text_slot_valid self.runtime.text_slot_keys = self.persistent_text_slot_keys self.runtime.text_slot_age = self.persistent_text_slot_age self.runtime.text_write_counter = self.persistent_text_write_counter self.runtime.text_key_token_ids = self.persistent_text_key_token_ids self.runtime.text_key_token_mask = self.persistent_text_key_token_mask self.runtime.text_last_written_slot = None return if batch_size is None: self._persistent_memory = None self.runtime.state = None self.runtime.last_read = None self.runtime.auto_memory_probability = None self.runtime.raw_memory = None self.runtime.text_token_ids = None self.runtime.text_token_mask = None self.runtime.text_slot_valid = None self.runtime.text_slot_keys = None self.runtime.text_slot_age = None self.runtime.text_write_counter = None self.runtime.text_key_token_ids = None self.runtime.text_key_token_mask = None self.runtime.text_last_written_slot = None return device = device or self._find_layer_device() self._persistent_memory = self.memory.initial_state(batch_size, device=device) if self.memory_config.natural_language_memory: self._ensure_text_memory(batch_size, device=device) self._clear_text_memory() self.runtime.state = self._persistent_memory self.runtime.last_read = None self.runtime.auto_memory_probability = None self.runtime.raw_memory = None self.runtime.text_token_ids = self.persistent_text_token_ids self.runtime.text_token_mask = self.persistent_text_token_mask self.runtime.text_slot_valid = self.persistent_text_slot_valid self.runtime.text_slot_keys = self.persistent_text_slot_keys self.runtime.text_slot_age = self.persistent_text_slot_age self.runtime.text_write_counter = self.persistent_text_write_counter self.runtime.text_key_token_ids = self.persistent_text_key_token_ids self.runtime.text_key_token_mask = self.persistent_text_key_token_mask self.runtime.text_last_written_slot = None def reset_runtime_memory( self, batch_size: Optional[int] = None, *, device: Optional[torch.device] = None, ) -> None: """Reset only the active conversation, preserving durable state. In persistent mode this creates an ephemeral empty bank and prevents the generation path from rebinding the embedded user checkpoint. It is used by evaluation, memory branches and temporary sessions. """ device = device or self._find_layer_device() batch_size = batch_size or 1 self.runtime.use_persistent_state = False self.runtime.state = self.memory.initial_state(batch_size, device=device) self._persistent_memory = self.runtime.state.detach() self.runtime.last_read = None self.runtime.auto_memory_probability = None self.runtime.raw_memory = None self.runtime.v2_query_key = None self.runtime.v2_last_decisions = [] self.runtime.reset_mask = None self.runtime.text_last_written_slot = None if self.memory_config.natural_language_memory: self._bind_text_memory(batch_size, device=device) self._clear_text_memory() def _reset_mask(self, input_ids: Optional[Tensor]) -> Optional[Tensor]: token_id = self.memory_config.reset_token_id if token_id is None or input_ids is None: return None mask = (input_ids == token_id).any(dim=-1) return mask if bool(mask.any()) else None def save_runtime_memory( self, path: str | Path, memory_state: Optional[Tensor] = None, ) -> Path: """Persist one user's runtime memory, without saving model weights.""" state = memory_state if memory_state is not None else self.runtime.state if state is None: state = self._persistent_memory if state is None: raise ValueError("no runtime memory state is available to save") if state.ndim != 3 or tuple(state.shape[1:]) != ( self.memory_config.memory_slots, self.memory_config.memory_dim, ): raise ValueError(f"unexpected runtime memory shape: {tuple(state.shape)}") raw_memory = self.runtime.raw_memory if raw_memory is not None: raw_memory = raw_memory.detach().cpu() payload = { "format_version": 1, "memory_state": state.detach().cpu(), "raw_memory": raw_memory, "model_hidden_size": self.memory.hidden_size, "memory_config": asdict(self.memory_config), "layer_indices": list(self.layer_indices), } if self.memory_config.natural_language_memory and self.runtime.text_token_ids is not None: payload.update( { "text_token_ids": self.runtime.text_token_ids.detach().cpu(), "text_token_mask": self.runtime.text_token_mask.detach().cpu(), "text_slot_valid": self.runtime.text_slot_valid.detach().cpu(), "text_slot_keys": self.runtime.text_slot_keys.detach().cpu(), "text_slot_age": self.runtime.text_slot_age.detach().cpu(), "text_write_counter": self.runtime.text_write_counter.detach().cpu(), "text_key_token_ids": self.runtime.text_key_token_ids.detach().cpu(), "text_key_token_mask": self.runtime.text_key_token_mask.detach().cpu(), } ) output_path = Path(path) output_path.parent.mkdir(parents=True, exist_ok=True) torch.save(payload, output_path) return output_path def load_runtime_memory( self, path: str | Path, *, device: Optional[torch.device] = None, ) -> Tensor: """Load a user's runtime memory into this freshly created model.""" payload = torch.load(Path(path), map_location="cpu", weights_only=True) if isinstance(payload, Tensor): state = payload raw_memory = None elif isinstance(payload, dict): state = payload.get("memory_state", payload.get("state")) raw_memory = payload.get("raw_memory") else: state = None raw_memory = None if not isinstance(state, Tensor): raise ValueError("runtime memory file does not contain a tensor memory_state") expected_shape = ( self.memory_config.memory_slots, self.memory_config.memory_dim, ) if state.ndim != 3 or tuple(state.shape[1:]) != expected_shape: raise ValueError( f"runtime memory shape {tuple(state.shape)} does not match {expected_shape}" ) saved_hidden_size = payload.get("model_hidden_size") if isinstance(payload, dict) else None if saved_hidden_size is not None and int(saved_hidden_size) != self.memory.hidden_size: raise ValueError( f"runtime memory hidden size {saved_hidden_size} does not match {self.memory.hidden_size}" ) target_device = device or self._find_layer_device() state = state.to(device=target_device, dtype=self.memory.slot_keys.dtype) self._persistent_memory = state.detach() if self.memory_config.persistent_memory: self.persistent_memory = self._persistent_memory self.runtime.state = self._persistent_memory self.runtime.last_read = None self.runtime.raw_memory = None if self.memory_config.natural_language_memory: token_ids = payload.get("text_token_ids") if isinstance(payload, dict) else None token_mask = payload.get("text_token_mask") if isinstance(payload, dict) else None slot_valid = payload.get("text_slot_valid") if isinstance(payload, dict) else None slot_keys = payload.get("text_slot_keys") if isinstance(payload, dict) else None slot_age = payload.get("text_slot_age") if isinstance(payload, dict) else None write_counter = payload.get("text_write_counter") if isinstance(payload, dict) else None key_token_ids = payload.get("text_key_token_ids") if isinstance(payload, dict) else None key_token_mask = payload.get("text_key_token_mask") if isinstance(payload, dict) else None if not all( isinstance(item, Tensor) for item in ( token_ids, token_mask, slot_valid, slot_keys, slot_age, write_counter, key_token_ids, key_token_mask, ) ): raise ValueError("natural-language memory file is missing its indexed text bank") expected_text_shape = ( state.shape[0], self.memory_config.memory_slots, self.memory_config.text_memory_tokens, ) if tuple(token_ids.shape) != expected_text_shape or tuple(token_mask.shape) != expected_text_shape: raise ValueError("natural-language text bank shape does not match memory config") if tuple(slot_valid.shape) != expected_text_shape[:2]: raise ValueError("natural-language slot validity shape does not match memory config") if tuple(slot_keys.shape) != (state.shape[0], self.memory_config.memory_slots, self.memory.hidden_size): raise ValueError("natural-language slot key shape does not match memory config") if tuple(slot_age.shape) != expected_text_shape[:2] or tuple(write_counter.shape) != (state.shape[0],): raise ValueError("natural-language slot age shape does not match memory config") expected_key_shape = ( state.shape[0], self.memory_config.memory_slots, self.memory_config.text_memory_key_tokens, ) if tuple(key_token_ids.shape) != expected_key_shape or tuple(key_token_mask.shape) != expected_key_shape: raise ValueError("natural-language key token shape does not match memory config") self.persistent_text_token_ids = token_ids.to(device=target_device, dtype=torch.long) self.persistent_text_token_mask = token_mask.to(device=target_device, dtype=torch.bool) self.persistent_text_slot_valid = slot_valid.to(device=target_device, dtype=torch.bool) self.persistent_text_slot_keys = slot_keys.to(device=target_device, dtype=torch.float32) self.persistent_text_slot_age = slot_age.to(device=target_device, dtype=torch.long) self.persistent_text_write_counter = write_counter.to(device=target_device, dtype=torch.long) self.persistent_text_key_token_ids = key_token_ids.to(device=target_device, dtype=torch.long) self.persistent_text_key_token_mask = key_token_mask.to(device=target_device, dtype=torch.bool) self.runtime.text_token_ids = self.persistent_text_token_ids self.runtime.text_token_mask = self.persistent_text_token_mask self.runtime.text_slot_valid = self.persistent_text_slot_valid self.runtime.text_slot_keys = self.persistent_text_slot_keys self.runtime.text_slot_age = self.persistent_text_slot_age self.runtime.text_write_counter = self.persistent_text_write_counter self.runtime.text_key_token_ids = self.persistent_text_key_token_ids self.runtime.text_key_token_mask = self.persistent_text_key_token_mask if raw_memory is not None: if not isinstance(raw_memory, Tensor): raise ValueError("runtime memory raw_memory must be a tensor or null") if raw_memory.ndim == 1: raw_memory = raw_memory.unsqueeze(0) if tuple(raw_memory.shape) != (state.shape[0], self.memory.hidden_size): raise ValueError(f"unexpected raw_memory shape: {tuple(raw_memory.shape)}") output_weight = self.base_model.get_output_embeddings().weight self.runtime.raw_memory = raw_memory.to( device=output_weight.device, dtype=output_weight.dtype, ).detach() return self._persistent_memory def save_memory_adapter(self, output_dir: str | Path) -> None: path = Path(output_dir) path.mkdir(parents=True, exist_ok=True) torch.save(self.memory.state_dict(), path / "memory.pt") if self.text_retriever is not None and self._text_retriever_ready: torch.save(self.text_retriever.state_dict(), path / "text_retriever.pt") if self.memory_policy is not None and self._memory_policy_ready: torch.save(self.memory_policy.state_dict(), path / "memory_policy.pt") metadata = { "hidden_size": self.memory.hidden_size, "memory_config": asdict(self.memory_config), "layer_indices": list(self.layer_indices), "text_retriever_ready": self._text_retriever_ready, "memory_policy_ready": self._memory_policy_ready, } (path / "memory_config.json").write_text(json.dumps(metadata, indent=2), encoding="utf-8") surgery_state = { str(index): adapter.blend_logit.detach().cpu() for index, adapter in zip(self.layer_indices, self._memory_adapters) if hasattr(adapter, "blend_logit") } torch.save({"blend_logits": surgery_state}, path / "surgery.pt") if self.memory_config.persistent_memory and self.persistent_memory.numel() > 0: payload = { "format_version": 1, "memory_state": self.persistent_memory.detach().cpu(), } if self.memory_config.natural_language_memory: payload.update( { "text_token_ids": self.persistent_text_token_ids.detach().cpu(), "text_token_mask": self.persistent_text_token_mask.detach().cpu(), "text_slot_valid": self.persistent_text_slot_valid.detach().cpu(), "text_slot_keys": self.persistent_text_slot_keys.detach().cpu(), "text_slot_age": self.persistent_text_slot_age.detach().cpu(), "text_write_counter": self.persistent_text_write_counter.detach().cpu(), "text_key_token_ids": self.persistent_text_key_token_ids.detach().cpu(), "text_key_token_mask": self.persistent_text_key_token_mask.detach().cpu(), } ) torch.save( payload, path / "persistent_memory.pt", ) def save_persistent_memory_checkpoint(self, output_dir: str | Path) -> None: """Save controller weights and the current user memory together. This produces a compact model-owned memory checkpoint. The large frozen Qwen shards remain untouched; the learned reader/controller and the user's current memory are stored in the adapter package. """ state = self.runtime.state if self.runtime.state is not None else self._persistent_memory if state is None: raise ValueError("no runtime memory state is available to checkpoint") was_persistent = self.memory_config.persistent_memory self.memory_config.persistent_memory = True self.persistent_memory = state.detach().clone() self._persistent_memory = self.persistent_memory.detach() if self.memory_config.natural_language_memory: text_fields = ( self.runtime.text_token_ids, self.runtime.text_token_mask, self.runtime.text_slot_valid, self.runtime.text_slot_keys, self.runtime.text_slot_age, self.runtime.text_write_counter, self.runtime.text_key_token_ids, self.runtime.text_key_token_mask, ) if not all(isinstance(item, Tensor) for item in text_fields): raise ValueError("natural-language memory bank is not initialized") target_device = self._find_layer_device() self.persistent_text_token_ids = text_fields[0].detach().clone().to(target_device, dtype=torch.long) self.persistent_text_token_mask = text_fields[1].detach().clone().to(target_device, dtype=torch.bool) self.persistent_text_slot_valid = text_fields[2].detach().clone().to(target_device, dtype=torch.bool) self.persistent_text_slot_keys = text_fields[3].detach().clone().to(target_device, dtype=torch.float32) self.persistent_text_slot_age = text_fields[4].detach().clone().to(target_device, dtype=torch.long) self.persistent_text_write_counter = text_fields[5].detach().clone().to(target_device, dtype=torch.long) self.persistent_text_key_token_ids = text_fields[6].detach().clone().to(target_device, dtype=torch.long) self.persistent_text_key_token_mask = text_fields[7].detach().clone().to(target_device, dtype=torch.bool) try: self.save_memory_adapter(output_dir) path = Path(output_dir) metadata_path = path / "memory_config.json" metadata = json.loads(metadata_path.read_text(encoding="utf-8")) metadata["memory_config"]["persistent_memory"] = True metadata["memory_config"]["checkpoint_contains_user_memory"] = True metadata_path.write_text(json.dumps(metadata, indent=2), encoding="utf-8") finally: self.memory_config.persistent_memory = was_persistent def load_memory_adapter(self, adapter_dir: str | Path, *, strict: bool = True) -> None: path = Path(adapter_dir) state = torch.load(path / "memory.pt", map_location=self._find_layer_device(), weights_only=True) self.memory.load_state_dict(state, strict=strict) metadata_ready = None policy_ready = None metadata_path = path / "memory_config.json" if metadata_path.exists(): metadata = json.loads(metadata_path.read_text(encoding="utf-8")) if isinstance(metadata, dict) and "text_retriever_ready" in metadata: metadata_ready = bool(metadata["text_retriever_ready"]) if isinstance(metadata, dict) and "memory_policy_ready" in metadata: policy_ready = bool(metadata["memory_policy_ready"]) if metadata_ready and self.text_retriever is None: raise ValueError( "adapter contains a trained text retriever but natural_language_memory is disabled" ) retriever_path = path / "text_retriever.pt" if self.text_retriever is not None and metadata_ready and not retriever_path.exists(): raise FileNotFoundError( f"adapter declares a trained text retriever but is missing {retriever_path}" ) should_load_retriever = ( self.text_retriever is not None and retriever_path.exists() and metadata_ready is not False ) if should_load_retriever: retriever_state = torch.load( retriever_path, map_location=self._find_layer_device(), weights_only=True, ) self.text_retriever.load_state_dict(retriever_state, strict=True) self._text_retriever_ready = True policy_path = path / "memory_policy.pt" if policy_ready and self.memory_policy is None: raise ValueError( "adapter contains an automatic memory policy but automatic_memory is disabled" ) if self.memory_policy is not None and policy_ready is not None: if policy_ready and not policy_path.exists(): raise FileNotFoundError( f"adapter declares an automatic memory policy but is missing {policy_path}" ) if policy_ready: policy_state = torch.load( policy_path, map_location=self._find_layer_device(), weights_only=True, ) self.memory_policy.load_state_dict(policy_state, strict=strict) self._memory_policy_ready = True surgery_path = path / "surgery.pt" if surgery_path.exists(): surgery_state = torch.load(surgery_path, map_location="cpu", weights_only=True) blend_logits = surgery_state.get("blend_logits", {}) for index, adapter in zip(self.layer_indices, self._memory_adapters): if hasattr(adapter, "blend_logit") and str(index) in blend_logits: adapter.blend_logit.data.copy_(blend_logits[str(index)].to(adapter.blend_logit.device)) persistent_path = path / "persistent_memory.pt" if persistent_path.exists(): payload = torch.load(persistent_path, map_location="cpu", weights_only=True) state = payload.get("memory_state", payload.get("state")) if isinstance(payload, dict) else payload if not isinstance(state, Tensor): raise ValueError("persistent_memory.pt does not contain a tensor memory_state") expected_shape = (self.memory_config.memory_slots, self.memory_config.memory_dim) if state.ndim != 3 or tuple(state.shape[1:]) != expected_shape: raise ValueError( f"persistent memory shape {tuple(state.shape)} does not match {expected_shape}" ) target_device = self._find_layer_device() self.memory_config.persistent_memory = True self.persistent_memory = state.to( device=target_device, dtype=self.memory.slot_keys.dtype, ) self._persistent_memory = self.persistent_memory.detach() self.runtime.state = self._persistent_memory if self.memory_config.natural_language_memory: token_ids = payload.get("text_token_ids") if isinstance(payload, dict) else None token_mask = payload.get("text_token_mask") if isinstance(payload, dict) else None slot_valid = payload.get("text_slot_valid") if isinstance(payload, dict) else None slot_keys = payload.get("text_slot_keys") if isinstance(payload, dict) else None slot_age = payload.get("text_slot_age") if isinstance(payload, dict) else None write_counter = payload.get("text_write_counter") if isinstance(payload, dict) else None key_token_ids = payload.get("text_key_token_ids") if isinstance(payload, dict) else None key_token_mask = payload.get("text_key_token_mask") if isinstance(payload, dict) else None if not all( isinstance(item, Tensor) for item in ( token_ids, token_mask, slot_valid, slot_keys, slot_age, write_counter, key_token_ids, key_token_mask, ) ): raise ValueError("persistent adapter is missing its indexed natural-language bank") expected_text_shape = ( state.shape[0], self.memory_config.memory_slots, self.memory_config.text_memory_tokens, ) if tuple(token_ids.shape) != expected_text_shape or tuple(token_mask.shape) != expected_text_shape: raise ValueError("persistent text bank shape does not match memory config") if tuple(slot_valid.shape) != expected_text_shape[:2]: raise ValueError("persistent slot validity shape does not match memory config") if tuple(slot_keys.shape) != (state.shape[0], self.memory_config.memory_slots, self.memory.hidden_size): raise ValueError("persistent slot key shape does not match memory config") if tuple(slot_age.shape) != expected_text_shape[:2] or tuple(write_counter.shape) != (state.shape[0],): raise ValueError("persistent slot age shape does not match memory config") expected_key_shape = ( state.shape[0], self.memory_config.memory_slots, self.memory_config.text_memory_key_tokens, ) if tuple(key_token_ids.shape) != expected_key_shape or tuple(key_token_mask.shape) != expected_key_shape: raise ValueError("persistent key token shape does not match memory config") self.persistent_text_token_ids = token_ids.to(device=target_device, dtype=torch.long) self.persistent_text_token_mask = token_mask.to(device=target_device, dtype=torch.bool) self.persistent_text_slot_valid = slot_valid.to(device=target_device, dtype=torch.bool) self.persistent_text_slot_keys = slot_keys.to(device=target_device, dtype=torch.float32) self.persistent_text_slot_age = slot_age.to(device=target_device, dtype=torch.long) self.persistent_text_write_counter = write_counter.to(device=target_device, dtype=torch.long) self.persistent_text_key_token_ids = key_token_ids.to(device=target_device, dtype=torch.long) self.persistent_text_key_token_mask = key_token_mask.to(device=target_device, dtype=torch.bool) self.runtime.text_token_ids = self.persistent_text_token_ids self.runtime.text_token_mask = self.persistent_text_token_mask self.runtime.text_slot_valid = self.persistent_text_slot_valid self.runtime.text_slot_keys = self.persistent_text_slot_keys self.runtime.text_slot_age = self.persistent_text_slot_age self.runtime.text_write_counter = self.persistent_text_write_counter self.runtime.text_key_token_ids = self.persistent_text_key_token_ids self.runtime.text_key_token_mask = self.persistent_text_key_token_mask @torch.no_grad() def _load_persistent_memory_payload(self, payload: dict[str, Any]) -> None: """Load a runtime/persistent memory payload shared by .pt and safetensors.""" state = payload.get("memory_state", payload.get("state")) if not isinstance(state, Tensor): raise ValueError("persistent memory payload does not contain a tensor memory_state") expected_shape = (self.memory_config.memory_slots, self.memory_config.memory_dim) if state.ndim != 3 or tuple(state.shape[1:]) != expected_shape: raise ValueError( f"persistent memory shape {tuple(state.shape)} does not match {expected_shape}" ) target_device = self._find_layer_device() self.memory_config.persistent_memory = True self.runtime.use_persistent_state = True self.persistent_memory = state.to(device=target_device, dtype=self.memory.slot_keys.dtype) self._persistent_memory = self.persistent_memory.detach() self.runtime.state = self._persistent_memory if not self.memory_config.natural_language_memory: return fields = { "persistent_text_token_ids": ("text_token_ids", torch.long), "persistent_text_token_mask": ("text_token_mask", torch.bool), "persistent_text_slot_valid": ("text_slot_valid", torch.bool), "persistent_text_slot_keys": ("text_slot_keys", torch.float32), "persistent_text_slot_age": ("text_slot_age", torch.long), "persistent_text_write_counter": ("text_write_counter", torch.long), "persistent_text_key_token_ids": ("text_key_token_ids", torch.long), "persistent_text_key_token_mask": ("text_key_token_mask", torch.bool), } if not all(isinstance(payload.get(key), Tensor) for key, _ in fields.values()): raise ValueError("persistent memory payload is missing its indexed natural-language bank") expected_text_shape = ( state.shape[0], self.memory_config.memory_slots, self.memory_config.text_memory_tokens, ) expected_key_shape = ( state.shape[0], self.memory_config.memory_slots, self.memory_config.text_memory_key_tokens, ) expected_shapes = { "text_token_ids": expected_text_shape, "text_token_mask": expected_text_shape, "text_slot_valid": expected_text_shape[:2], "text_slot_keys": (state.shape[0], self.memory_config.memory_slots, self.memory.hidden_size), "text_slot_age": expected_text_shape[:2], "text_write_counter": (state.shape[0],), "text_key_token_ids": expected_key_shape, "text_key_token_mask": expected_key_shape, } for key, _ in fields.values(): if tuple(payload[key].shape) != expected_shapes[key]: raise ValueError(f"persistent field {key} shape does not match memory config") for attribute, (key, dtype) in fields.items(): setattr(self, attribute, payload[key].to(device=target_device, dtype=dtype)) runtime_fields = { "text_token_ids": self.persistent_text_token_ids, "text_token_mask": self.persistent_text_token_mask, "text_slot_valid": self.persistent_text_slot_valid, "text_slot_keys": self.persistent_text_slot_keys, "text_slot_age": self.persistent_text_slot_age, "text_write_counter": self.persistent_text_write_counter, "text_key_token_ids": self.persistent_text_key_token_ids, "text_key_token_mask": self.persistent_text_key_token_mask, } for attribute, value in runtime_fields.items(): setattr(self.runtime, attribute, value) def _export_memory_os_v2_checkpoint(self) -> tuple[dict[str, Tensor], dict[str, Any]]: """Split a V2 bank into tensors plus JSON-safe checkpoint metadata.""" if self.memory_os_v2 is None: return {}, {} payload = self.memory_os_v2.export_payload() tensors: dict[str, Tensor] = {} metadata = dict(payload) def pack_record(item: dict[str, Any], prefix: str) -> dict[str, Any]: item = dict(item) for field in ("key", "summary", "token_ids", "token_mask"): value = item.pop(field, None) if isinstance(value, Tensor): name = f"dynamic_memory.v2.{prefix}.{field}" tensors[name] = value.detach().cpu().contiguous() item[f"{field}_ref"] = name return item metadata["records"] = [ pack_record(item, f"records.{index}") for index, item in enumerate(payload.get("records", [])) ] metadata["quarantine"] = [ pack_record(item, f"quarantine.{index}") for index, item in enumerate(payload.get("quarantine", [])) ] metadata["pages"] = [] for index, item in enumerate(payload.get("pages", [])): item = dict(item) for field in ("key", "summary"): value = item.pop(field, None) if isinstance(value, Tensor): name = f"dynamic_memory.v2.pages.{index}.{field}" tensors[name] = value.detach().cpu().contiguous() item[f"{field}_ref"] = name metadata["pages"].append(item) return tensors, metadata def _load_memory_os_v2_checkpoint( self, metadata_text: str, weights: dict[str, Tensor], ) -> None: """Rehydrate the V2 page store from safetensors metadata and tensors.""" if self.memory_router_v2 is None: return payload = json.loads(metadata_text) def unpack_record(item: dict[str, Any]) -> dict[str, Any]: item = dict(item) for field in ("key", "summary", "token_ids", "token_mask"): ref = item.pop(f"{field}_ref", None) item[field] = weights.get(ref) if ref else None return item payload["records"] = [unpack_record(item) for item in payload.get("records", [])] payload["quarantine"] = [unpack_record(item) for item in payload.get("quarantine", [])] unpacked_pages = [] for item in payload.get("pages", []): item = dict(item) for field in ("key", "summary"): ref = item.pop(f"{field}_ref", None) item[field] = weights.get(ref) if ref else None unpacked_pages.append(item) payload["pages"] = unpacked_pages tier_store = None if self.memory_config.memory_storage_mode == "tiered": if not self.memory_config.memory_storage_path: raise ValueError("tiered memory requires memory_storage_path") tier_store = TieredMemoryStoreV2( self.memory_config.memory_storage_path, key_dim=self.memory_config.memory_router_dim, page_capacity=self.memory_config.memory_page_capacity, ) self.memory_os_v2 = MemoryOSV2.from_payload( payload, router=self.memory_router_v2, tier_store=tier_store, max_resident_pages=self.memory_config.memory_resident_pages, runtime_device=self._find_layer_device(), gpu_cache_records=self.memory_config.memory_gpu_cache_records, gpu_cache_tokens=self.memory_config.memory_gpu_cache_tokens, gpu_cache_reserve_mb=self.memory_config.memory_gpu_cache_reserve_mb, gpu_cache_adaptive=self.memory_config.memory_gpu_cache_adaptive, ) # The package configuration is authoritative for runtime safety and # capacity gates. Older snapshots may contain looser values. self.memory_os_v2.read_threshold = self.memory_config.memory_v2_read_threshold self.memory_os_v2.write_threshold = self.memory_config.memory_v2_write_threshold bank = self.memory_os_v2.bank bank.page_capacity = self.memory_config.memory_page_capacity bank.max_pages = self.memory_config.memory_max_pages bank.hot_pages = self.memory_config.memory_hot_pages bank.top_k_pages = self.memory_config.memory_top_k_pages bank.top_k_records = self.memory_config.memory_top_k_records bank.max_hops = self.memory_config.memory_max_hops bank._rebuild_coarse_index(self.memory_config.memory_coarse_index_bits) # The package configuration is authoritative for runtime safety gates. # Older snapshots may contain a looser threshold in their serialized # payload; do not let that silently re-enable noisy recalls after an # upgrade. self.memory_os_v2.read_threshold = self.memory_config.memory_v2_read_threshold self.memory_os_v2.write_threshold = self.memory_config.memory_v2_write_threshold @torch.no_grad() def load_embedded_memory_weights(self, merged_dir: str | Path) -> None: """Load memory modules and optional user state from one safetensors package.""" path = Path(merged_dir) manifest = json.loads((path / "memory_merge.json").read_text(encoding="utf-8")) from safetensors import safe_open weights_path = path / str(manifest.get("memory_weights", "memory.safetensors")) tensor_prefix = str(manifest.get("tensor_prefix", "dynamic_memory.")) with safe_open(str(weights_path), framework="pt", device="cpu") as handle: file_metadata = handle.metadata() or {} weights = { key: handle.get_tensor(key) for key in handle.keys() if key.startswith(tensor_prefix) } memory_prefix = f"{tensor_prefix}memory." memory_state = { key.removeprefix(memory_prefix): value for key, value in weights.items() if key.startswith(memory_prefix) } if not memory_state: raise ValueError(f"merged memory file has no {memory_prefix}* tensors: {weights_path}") self.memory.load_state_dict(memory_state, strict=True) retriever_prefix = f"{tensor_prefix}text_retriever." retriever_state = { key.removeprefix(retriever_prefix): value for key, value in weights.items() if key.startswith(retriever_prefix) } if retriever_state: if self.text_retriever is None: raise ValueError("merged package contains a text retriever but it is disabled") self.text_retriever.load_state_dict(retriever_state, strict=True) self._text_retriever_ready = True policy_prefix = f"{tensor_prefix}memory_policy." policy_state = { key.removeprefix(policy_prefix): value for key, value in weights.items() if key.startswith(policy_prefix) } if policy_state: if self.memory_policy is None: raise ValueError("merged package contains a memory policy but it is disabled") # A v2 candidate may be loaded on top of an older embedded package: # the old shard has only the write head, while the candidate adds # a trained forget head. The adapter load immediately afterwards # is strict and supplies the complete v2 policy. self.memory_policy.load_state_dict( policy_state, strict=self.memory_config.automatic_memory_policy_version < 2, ) self._memory_policy_ready = True router_prefix = f"{tensor_prefix}memory_router_v2." router_state = { key.removeprefix(router_prefix): value for key, value in weights.items() if key.startswith(router_prefix) } if router_state: if self.memory_router_v2 is None: raise ValueError("merged package contains a V2 router but hierarchical memory is disabled") self.memory_router_v2.load_state_dict(router_state, strict=True) self._memory_router_v2_ready = True for index, adapter in zip(self.layer_indices, self._memory_adapters): key = f"{tensor_prefix}blend_logits.{index}" if hasattr(adapter, "blend_logit") and key in weights: adapter.blend_logit.data.copy_(weights[key].to(adapter.blend_logit.device)) persistent_prefix = f"{tensor_prefix}persistent." persistent_payload = { key.removeprefix(persistent_prefix): value for key, value in weights.items() if key.startswith(persistent_prefix) } if persistent_payload: self._load_persistent_memory_payload(persistent_payload) v2_payload = file_metadata.get("memory_os_v2_payload") if v2_payload and self.memory_os_v2 is not None: self._load_memory_os_v2_checkpoint(v2_payload, weights) @torch.no_grad() def save_embedded_memory_weights(self, merged_dir: str | Path) -> None: """Atomically write the current memory snapshot back into the main shard. The merged package keeps the original Qwen shard as the base payload and appends ``dynamic_memory.*`` tensors to the same safetensors file. This intentionally rewrites the package's second shard, so callers should use it as a deliberate weight-backed persistence mode rather than on every token. """ path = Path(merged_dir) manifest = json.loads((path / "memory_merge.json").read_text(encoding="utf-8")) from safetensors.torch import save_file from .merge_memory_weights import _write_merged_shard memory_weights_name = str( manifest.get("memory_weights", "model.safetensors-00002-of-00002.safetensors") ) target_shard = path / memory_weights_name if not target_shard.exists(): raise FileNotFoundError(f"merged package target shard is missing: {target_shard}") tensors: dict[str, Tensor] = {} tensors.update( { f"dynamic_memory.memory.{key}": value.detach().cpu().contiguous() for key, value in self.memory.state_dict().items() } ) if self.text_retriever is not None and self._text_retriever_ready: tensors.update( { f"dynamic_memory.text_retriever.{key}": value.detach().cpu().contiguous() for key, value in self.text_retriever.state_dict().items() } ) if self.memory_router_v2 is not None: tensors.update( { f"dynamic_memory.memory_router_v2.{key}": value.detach().cpu().contiguous() for key, value in self.memory_router_v2.state_dict().items() } ) if self.memory_policy is not None and self._memory_policy_ready: tensors.update( { f"dynamic_memory.memory_policy.{key}": value.detach().cpu().contiguous() for key, value in self.memory_policy.state_dict().items() } ) for index, adapter in zip(self.layer_indices, self._memory_adapters): if hasattr(adapter, "blend_logit"): tensors[f"dynamic_memory.blend_logits.{index}"] = ( adapter.blend_logit.detach().cpu().contiguous() ) if self.memory_config.persistent_memory and self.persistent_memory.numel() > 0: state = self.persistent_memory else: state = self.runtime.state if self.runtime.state is not None else self._persistent_memory if state is None: raise ValueError("cannot write embedded memory without a runtime memory state") tensors["dynamic_memory.persistent.memory_state"] = state.detach().cpu().contiguous() if self.memory_config.natural_language_memory: if self.memory_config.persistent_memory and self.persistent_text_token_ids.numel() > 0: runtime_fields = { "text_token_ids": self.persistent_text_token_ids, "text_token_mask": self.persistent_text_token_mask, "text_slot_valid": self.persistent_text_slot_valid, "text_slot_keys": self.persistent_text_slot_keys, "text_slot_age": self.persistent_text_slot_age, "text_write_counter": self.persistent_text_write_counter, "text_key_token_ids": self.persistent_text_key_token_ids, "text_key_token_mask": self.persistent_text_key_token_mask, } else: runtime_fields = { "text_token_ids": self.runtime.text_token_ids, "text_token_mask": self.runtime.text_token_mask, "text_slot_valid": self.runtime.text_slot_valid, "text_slot_keys": self.runtime.text_slot_keys, "text_slot_age": self.runtime.text_slot_age, "text_write_counter": self.runtime.text_write_counter, "text_key_token_ids": self.runtime.text_key_token_ids, "text_key_token_mask": self.runtime.text_key_token_mask, } if not all(isinstance(value, Tensor) for value in runtime_fields.values()): raise ValueError("cannot write embedded memory without a complete text memory bank") tensors.update( { f"dynamic_memory.persistent.{key}": value.detach().cpu().contiguous() for key, value in runtime_fields.items() } ) v2_metadata: dict[str, Any] = {} if self.memory_os_v2 is not None: v2_tensors, v2_metadata = self._export_memory_os_v2_checkpoint() tensors.update(v2_tensors) extra_path = path / f".{memory_weights_name}.memory-update.tmp" staging_path = path / f".{memory_weights_name}.merged-update.tmp" def _sync_model_index(shard_name: str) -> None: """Keep the HF shard index aligned with the embedded memory shard.""" index_path = path / "model.safetensors.index.json" if not index_path.exists(): return index = json.loads(index_path.read_text(encoding="utf-8")) weight_map = index.get("weight_map") if not isinstance(weight_map, dict): return changed = False for key, value in list(weight_map.items()): if str(key).startswith("dynamic_memory.") and value != shard_name: weight_map[key] = shard_name changed = True if changed: index_path.write_text( json.dumps(index, indent=2, ensure_ascii=False), encoding="utf-8" ) try: metadata = {"format": "qwen_dynamic_memory_embedded_v2"} if v2_metadata: metadata["memory_os_v2_payload"] = json.dumps( v2_metadata, ensure_ascii=False, separators=(",", ":") ) save_file(tensors, str(extra_path), metadata=metadata) if str(manifest.get("shard_mode", "combined")) == "extra_only": # The extra-only layout keeps the official Qwen shards # untouched. Windows may keep the currently loaded shard # open, so rotate to a new small shard when replacement is # denied instead of requiring the chat process to exit. try: os.replace(extra_path, target_shard) except PermissionError: rotated_name = f"{target_shard.stem}.runtime-{time.time_ns()}.safetensors" rotated_path = path / rotated_name os.replace(extra_path, rotated_path) manifest["memory_weights"] = rotated_name manifest["checkpoint_contains_user_memory"] = True (path / "memory_merge.json").write_text( json.dumps(manifest, indent=2, ensure_ascii=False), encoding="utf-8" ) _sync_model_index(str(manifest.get("memory_weights", memory_weights_name))) return base_dir = Path(str(manifest["base_model"])) base_shard = base_dir / memory_weights_name if not base_shard.exists(): raise FileNotFoundError(f"merged package base shard is missing: {base_shard}") _write_merged_shard(base_shard, extra_path, staging_path) os.replace(staging_path, target_shard) _sync_model_index(memory_weights_name) finally: if extra_path.exists(): extra_path.unlink() if staging_path.exists(): staging_path.unlink() def forward( self, *args: Any, memory_state: Optional[Tensor] = None, update_memory: bool = True, read_memory: bool = True, detach_memory: bool = False, return_memory: bool = True, memory_text_input_ids: Optional[Tensor] = None, memory_text_attention_mask: Optional[Tensor] = None, memory_key_input_ids: Optional[Tensor] = None, memory_key_attention_mask: Optional[Tensor] = None, memory_query_input_ids: Optional[Tensor] = None, memory_query_attention_mask: Optional[Tensor] = None, memory_storage_input_ids: Optional[Tensor] = None, memory_storage_attention_mask: Optional[Tensor] = None, force_memory_write: bool = False, memory_text: Optional[str] = None, **kwargs: Any, ) -> Any: input_ids = kwargs.get("input_ids") if input_ids is None and args: input_ids = args[0] batch_size = input_ids.shape[0] if input_ids is not None else None reset_mask = self._reset_mask(input_ids) if reset_mask is not None and self.memory_os_v2 is not None: # The reset token is a model-level durable-memory clear signal, # not merely a transient recurrent-state reset. self.clear_hierarchical_memory() if memory_state is not None: state = memory_state elif self.memory_config.persistent_memory and self.persistent_memory.numel() > 0: state = self.persistent_memory elif self._persistent_memory is not None: state = self._persistent_memory elif batch_size is not None: state = self.memory.initial_state(batch_size, device=self._find_layer_device()) else: state = None if state is not None and batch_size is not None and state.shape[0] != batch_size: if self.memory_config.persistent_memory and state.shape[0] == 1: state = state.expand(batch_size, -1, -1).clone() else: raise ValueError(f"memory batch {state.shape[0]} does not match input batch {batch_size}") if reset_mask is not None and state is not None: state = state.clone() state[reset_mask] = 0 if batch_size is not None: self._bind_text_memory(batch_size, device=self._find_layer_device()) if reset_mask is not None: self._clear_text_memory(reset_mask) self.runtime.state = state self.runtime.read_enabled = read_memory self.runtime.update_enabled = update_memory self.runtime.last_read = None self.runtime.reset_mask = reset_mask self.runtime.input_ids = input_ids self.runtime.attention_mask = kwargs.get("attention_mask") kwargs.setdefault("use_cache", False) base_output = self.base_model(*args, **kwargs) if ( not self.memory_config.natural_language_memory and self.memory_config.direct_logit_scale > 0.0 and read_memory and self.runtime.last_read is not None and hasattr(base_output, "logits") ): read = self.runtime.last_read output_embeddings = self.base_model.get_output_embeddings() memory_logits = output_embeddings(read.to(output_embeddings.weight.device)) logits = base_output.logits + self.memory_config.direct_logit_scale * memory_logits.to( base_output.logits.device ) base_output.logits = logits labels = kwargs.get("labels") if labels is not None: base_output.loss = F.cross_entropy( logits[..., :-1, :].contiguous().view(-1, logits.shape[-1]), labels[..., 1:].contiguous().view(-1), ignore_index=-100, ) if ( not self.memory_config.natural_language_memory and self.memory_config.raw_logit_scale > 0.0 and read_memory and self.runtime.raw_memory is not None and hasattr(base_output, "logits") ): output_embeddings = self.base_model.get_output_embeddings() raw_logits = output_embeddings(self.runtime.raw_memory.to(output_embeddings.weight.device)) raw_logits = raw_logits[:, None, :].expand(-1, base_output.logits.shape[1], -1) base_output.logits = base_output.logits + self.memory_config.raw_logit_scale * raw_logits.to( base_output.logits.device ) labels = kwargs.get("labels") if labels is not None: base_output.loss = F.cross_entropy( base_output.logits[..., :-1, :].contiguous().view(-1, base_output.logits.shape[-1]), labels[..., 1:].contiguous().view(-1), ignore_index=-100, ) new_state = self.runtime.state if new_state is not None and detach_memory: new_state = new_state.detach() self._persistent_memory = new_state.detach() if new_state is not None else None if self.memory_config.persistent_memory and self.runtime.use_persistent_state and new_state is not None: self.persistent_memory = new_state.detach() if self.memory_config.natural_language_memory and update_memory: self.runtime.auto_memory_probability = None self.runtime.auto_memory_forget_probability = None if ( self.memory_policy is not None and self.memory_config.automatic_memory and self._memory_policy_ready and isinstance( getattr( self.memory, "last_write_summary", getattr(self.memory, "last_write_representation", None), ), Tensor, ) ): policy_input = getattr( self.memory, "last_write_summary", self.memory.last_write_representation, ) policy_logits = self.memory_policy(policy_input) self.runtime.auto_memory_probability = torch.sigmoid(policy_logits).detach() if hasattr(self.memory_policy, "forget_logits"): forget_logits = self.memory_policy.forget_logits(policy_input) self.runtime.auto_memory_forget_probability = torch.sigmoid(forget_logits).detach() self._write_text_memory( input_ids, kwargs.get("attention_mask"), memory_text_input_ids, memory_text_attention_mask, memory_key_input_ids, memory_key_attention_mask, memory_storage_input_ids, memory_storage_attention_mask, force_memory_write, memory_text, ) self.runtime.state = new_state return QwenDynamicMemoryOutput(base_output, new_state) if return_memory else base_output @torch.no_grad() def generate( self, *args: Any, memory_state: Optional[Tensor] = None, update_memory: bool = True, memory_query_input_ids: Optional[Tensor] = None, memory_query_attention_mask: Optional[Tensor] = None, memory_query_text: Optional[str] = None, **kwargs: Any, ) -> Any: """Use the native HF generation loop with memory-aware decoder layers.""" input_ids = kwargs.get("input_ids") if input_ids is None and args: input_ids = args[0] self.runtime.context_compaction = None reset_mask = self._reset_mask(input_ids) if reset_mask is not None and self.memory_os_v2 is not None: # Clear before any context compaction so the reset marker can # never cause the old bank to be archived again in this call. self.clear_hierarchical_memory() if input_ids is not None and kwargs.get("attention_mask") is None: kwargs["attention_mask"] = torch.ones_like(input_ids) if input_ids is not None and reset_mask is None: compacted_ids, compacted_mask, compaction_info = self.compact_context_for_kv( input_ids, kwargs.get("attention_mask"), ) input_ids = compacted_ids kwargs["input_ids"] = compacted_ids kwargs["attention_mask"] = compacted_mask self.runtime.context_compaction = compaction_info elif input_ids is not None: self.runtime.context_compaction = { "compacted": False, "reason": "reset_token", "archived_records": 0, } # This is the public prompt returned by generate. When compaction # occurred it intentionally contains the bounded hot window, not a # second copy of the potentially enormous source prompt. original_input_ids = input_ids original_prompt_length = int(input_ids.shape[1]) if input_ids is not None else 0 if memory_state is not None: self.runtime.state = memory_state elif ( self.memory_config.persistent_memory and self.runtime.use_persistent_state and self.persistent_memory.numel() > 0 ): self.runtime.state = self.persistent_memory elif self._persistent_memory is not None and input_ids is not None and self._persistent_memory.shape[0] == input_ids.shape[0]: self.runtime.state = self._persistent_memory elif input_ids is not None: self.runtime.state = self.memory.initial_state(input_ids.shape[0], device=self._find_layer_device()) if reset_mask is not None and self.runtime.state is not None: state = self.runtime.state.clone() if state.shape[0] == 1 and reset_mask.shape[0] > 1: state = state.expand(reset_mask.shape[0], -1, -1).clone() state[reset_mask] = 0 self.runtime.state = state if input_ids is not None: if self.memory_config.natural_language_memory: self._bind_text_memory(input_ids.shape[0], device=self._find_layer_device()) if reset_mask is not None: self._clear_text_memory(reset_mask) if "attention_mask" not in kwargs or kwargs["attention_mask"] is None: kwargs["attention_mask"] = torch.ones_like(input_ids) prefix_ids: Optional[Tensor] = None prefix_mask: Optional[Tensor] = None prefix_length = 0 retrieval_ids = memory_query_input_ids if memory_query_input_ids is not None else input_ids retrieval_mask = ( memory_query_attention_mask if memory_query_attention_mask is not None else kwargs["attention_mask"] ) if ( self.memory_config.natural_language_memory and self.runtime.text_slot_valid is not None and retrieval_ids is not None and retrieval_mask is not None ): prefix_ids, prefix_mask, prefix_length = self._build_text_prefix( retrieval_ids.to(device=input_ids.device), retrieval_mask.to(device=input_ids.device), query_text=memory_query_text or "", ) if prefix_ids is not None and prefix_mask is not None and prefix_length > 0: input_ids = torch.cat((prefix_ids, input_ids), dim=1) kwargs["input_ids"] = input_ids kwargs["attention_mask"] = torch.cat( (prefix_mask, kwargs["attention_mask"]), dim=1 ) # Internal memory tokens are hidden from the public wrapper # API, but remain visible to the actual Qwen forward pass. self.runtime.input_ids = input_ids # In natural-language mode, the retrieved text prefix is the complete # read path. Running the older continuous residual at the same time # would distort Qwen's native chat control tokens. With no prefix the # memory path is an exact no-op and the base model remains untouched. self.runtime.read_enabled = not self.memory_config.natural_language_memory self.runtime.update_enabled = update_memory self.runtime.last_read = None self.runtime.reset_mask = reset_mask self.runtime.input_ids = input_ids if self.memory_config.kv_offload or self.memory_config.kv_cache_implementation is not None: # Transformers 5.x owns the offload lifecycle. Supplying an # explicit DynamicCache lets us honor the non-sliding-layer # setting, which the shorthand ``cache_implementation`` does not # expose for dynamic caches. kwargs["use_cache"] = True cache_implementation = self.memory_config.kv_cache_implementation if cache_implementation is None: cache_implementation = "offloaded" if ( cache_implementation == "offloaded" and kwargs.get("past_key_values") is None ): try: from transformers.cache_utils import DynamicCache, LinearAttentionCacheLayerMixin class _NaturalMemoryOffloadedCache(DynamicCache): """Prefetch the current layer before every cache update. Transformers 5.9 prefetches the next layer as part of ``Cache.update``. Qwen3.5's hybrid linear/full stack can enter the same layer again on the next decoding step before that next-layer prefetch runs, leaving a CPU tensor to concatenate with CUDA states. The current-layer prefetch makes the ownership explicit for both attention and recurrent cache updates. """ def update(self, key_states, value_states, layer_idx, *args, **extra): if self.offloading: self.prefetch(layer_idx, self.only_non_sliding) return super().update(key_states, value_states, layer_idx, *args, **extra) def update_conv_state(self, conv_states, layer_idx, **extra): if self.offloading: self.prefetch(layer_idx, self.only_non_sliding) value = super().update_conv_state(conv_states, layer_idx, **extra) if self.offloading: self.offload(layer_idx, self.only_non_sliding) return value def update_recurrent_state(self, recurrent_states, layer_idx, **extra): if self.offloading: self.prefetch(layer_idx, self.only_non_sliding) value = super().update_recurrent_state(recurrent_states, layer_idx, **extra) if self.offloading: self.offload(layer_idx, self.only_non_sliding) return value def offload(self, layer_idx, only_non_sliding=True): # Qwen3.5 linear-attention state is tiny and is # read before update. Keep it on the execution # device; offload only the full-attention KV that # dominates memory usage. if isinstance(self.layers[layer_idx], LinearAttentionCacheLayerMixin): return return super().offload(layer_idx, only_non_sliding) decoder_config = ( self.base_model.config.get_text_config(decoder=True) if hasattr(self.base_model.config, "get_text_config") else getattr(self.base_model.config, "text_config", self.base_model.config) ) kwargs["past_key_values"] = _NaturalMemoryOffloadedCache( config=decoder_config, offloading=True, offload_only_non_sliding=self.memory_config.kv_offload_only_non_sliding, ) except (ImportError, TypeError, AttributeError): # Older Transformers releases can still understand the # public generation option even if their DynamicCache # constructor has a different signature. kwargs["cache_implementation"] = cache_implementation elif kwargs.get("past_key_values") is None: kwargs["cache_implementation"] = cache_implementation # Natural-language memory is consumed as an internal text prefix. Do # not also add a constant token-level logit bias, because that would # repeat the first value token at every generation position. if ( not self.memory_config.natural_language_memory and (self.memory_config.direct_logit_scale > 0.0 or self.memory_config.raw_logit_scale > 0.0) ): from transformers import LogitsProcessor, LogitsProcessorList runtime = self.runtime output_embeddings = self.base_model.get_output_embeddings() prompt_length = int(input_ids.shape[1]) if input_ids is not None else None class MemoryLogitsProcessor(LogitsProcessor): def __call__(self, input_ids: Tensor, scores: Tensor) -> Tensor: first_generated_token = prompt_length is None or input_ids.shape[1] == prompt_length if first_generated_token and runtime.raw_memory is not None and raw_scale > 0.0: raw = runtime.raw_memory.to(output_embeddings.weight.device) scores = scores + raw_scale * output_embeddings(raw).to(scores.device) if runtime.last_read is None: return scores last_read = runtime.last_read[:, -1].to(output_embeddings.weight.device) memory_logits = output_embeddings(last_read).to(scores.device) return scores + self_scale * memory_logits self_scale = self.memory_config.direct_logit_scale raw_scale = self.memory_config.raw_logit_scale existing = kwargs.get("logits_processor") if existing is None: kwargs["logits_processor"] = LogitsProcessorList([MemoryLogitsProcessor()]) else: kwargs["logits_processor"] = LogitsProcessorList( list(existing) + [MemoryLogitsProcessor()] ) if input_ids is not None and args: args = () generated = self.base_model.generate(*args, **kwargs) if ( original_input_ids is not None and input_ids is not None and input_ids.shape[1] != original_prompt_length ): generated = torch.cat( ( original_input_ids, generated[:, input_ids.shape[1] :], ), dim=1, ) if self.runtime.state is not None: self._persistent_memory = self.runtime.state.detach() if self.memory_config.persistent_memory and self.runtime.use_persistent_state: self.persistent_memory = self._persistent_memory return generated def train(self, mode: bool = True) -> "QwenDynamicMemoryModel": self.memory.train(mode) if self.memory_router_v2 is not None: self.memory_router_v2.train(mode) self.base_model.eval() return self def load_qwen_base( model_path: str | Path, *, load_in_4bit: bool = True, device_map: str | dict[str, Any] = "auto", ): """Load the unmodified local Qwen checkpoint with the benchmark defaults.""" import transformers load_kwargs: dict[str, Any] = { "device_map": device_map, "low_cpu_mem_usage": True, "dtype": torch.bfloat16, } if load_in_4bit: load_kwargs["quantization_config"] = transformers.BitsAndBytesConfig( load_in_4bit=True, bnb_4bit_quant_type="nf4", bnb_4bit_use_double_quant=True, bnb_4bit_compute_dtype=torch.bfloat16, ) return transformers.AutoModelForImageTextToText.from_pretrained(str(model_path), **load_kwargs) def load_qwen_dynamic( model_path: str | Path, *, memory_config: Optional[QwenMemoryConfig] = None, load_in_4bit: bool = True, device_map: str | dict[str, Any] = "auto", freeze_backbone: bool = True, ) -> QwenDynamicMemoryModel: """Load Qwen and attach either an explicit adapter or an embedded merge package.""" model_path = Path(model_path) embedded_manifest = model_path / "memory_merge.json" if memory_config is None and embedded_manifest.exists(): embedded_config = model_path / "memory_config.json" if not embedded_config.exists(): raise FileNotFoundError( f"embedded memory package is missing {embedded_config}" ) memory_config = load_memory_config(model_path) if memory_config is not None and memory_config.memory_storage_mode == "tiered": storage_path = Path(memory_config.memory_storage_path) if memory_config.memory_storage_path else None if storage_path is None: storage_path = model_path / "memory_pages.sqlite" elif not storage_path.is_absolute(): storage_path = model_path / storage_path memory_config.memory_storage_path = str(storage_path) base_model = load_qwen_base( model_path, load_in_4bit=load_in_4bit, device_map=device_map, ) model = QwenDynamicMemoryModel( base_model, memory_config=memory_config, freeze_backbone=freeze_backbone, ) if embedded_manifest.exists(): model.load_embedded_memory_weights(model_path) return model def load_tokenizer(model_path: str | Path): import transformers tokenizer = transformers.AutoTokenizer.from_pretrained(str(model_path), use_fast=True) if tokenizer.pad_token is None: tokenizer.pad_token = tokenizer.eos_token return tokenizer