theapemachine commited on
Commit
036ce13
·
verified ·
1 Parent(s): 0c9628d

Add cortex/memory_bank.py

Browse files
Files changed (1) hide show
  1. cortex/memory_bank.py +170 -0
cortex/memory_bank.py ADDED
@@ -0,0 +1,170 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ MemoryBank: Persistent episodic memory via gated cross-attention.
3
+
4
+ Inspired by LM2 (Large Memory Models, Kang et al. 2025) and WISE (Xia et al. 2024).
5
+
6
+ Architecture:
7
+ - A learnable memory matrix M ∈ R^{N×D} per injection layer
8
+ - Cross-attention: query = hidden_states, key/value = memory
9
+ - LSTM-style gating: forget gate, input gate, output gate
10
+ - Memory is updated per forward pass (recurrent across tokens)
11
+
12
+ Failure mode addressed:
13
+ - Short context window / lost long-range dependencies
14
+ - Catastrophic forgetting during conversation
15
+ - Inability to maintain working memory for multi-step reasoning
16
+
17
+ Injection point: POST_ATTENTION (between attention and FFN)
18
+ - Rationale: Memory should augment what attention retrieved, before
19
+ the FFN processes it for next-token prediction
20
+ """
21
+
22
+ import torch
23
+ import torch.nn as nn
24
+ import torch.nn.functional as F
25
+ import math
26
+ from typing import Optional, Union, List
27
+ from cortex.core import CortexModule, InjectionPoint
28
+
29
+
30
+ class MemoryBank(CortexModule):
31
+ """
32
+ Gated cross-attention memory bank.
33
+
34
+ The memory persists across forward passes (useful for multi-turn conversations
35
+ or long documents processed in chunks). It learns to read and write information
36
+ via gated cross-attention.
37
+
38
+ Args:
39
+ hidden_dim: Model's hidden dimension
40
+ num_slots: Number of memory slots (like "memory addresses")
41
+ num_heads: Number of attention heads for cross-attention
42
+ dropout: Dropout rate
43
+ gate_bias_init: Initial bias for the output gate (negative = conservative writing)
44
+ """
45
+
46
+ def __init__(
47
+ self,
48
+ hidden_dim: int,
49
+ num_slots: int = 64,
50
+ num_heads: int = 8,
51
+ dropout: float = 0.0,
52
+ gate_bias_init: float = -2.0,
53
+ target_layers: Union[List[int], str] = "middle",
54
+ ):
55
+ super().__init__(InjectionPoint.POST_ATTENTION, target_layers)
56
+
57
+ self.hidden_dim = hidden_dim
58
+ self.num_slots = num_slots
59
+ self.num_heads = num_heads
60
+ self.head_dim = hidden_dim // num_heads
61
+
62
+ # Memory bank: learnable initial state
63
+ self.memory_init = nn.Parameter(torch.randn(1, num_slots, hidden_dim) * 0.02)
64
+
65
+ # Cross-attention: query from hidden states, key/value from memory
66
+ self.q_proj = nn.Linear(hidden_dim, hidden_dim, bias=False)
67
+ self.k_proj = nn.Linear(hidden_dim, hidden_dim, bias=False)
68
+ self.v_proj = nn.Linear(hidden_dim, hidden_dim, bias=False)
69
+ self.o_proj = nn.Linear(hidden_dim, hidden_dim, bias=False)
70
+
71
+ # Gating mechanism (LSTM-style)
72
+ self.forget_gate = nn.Linear(hidden_dim, hidden_dim)
73
+ self.input_gate = nn.Linear(hidden_dim, hidden_dim)
74
+ self.output_gate = nn.Linear(hidden_dim, hidden_dim)
75
+
76
+ # Output gate bias: negative means "default to not modifying hidden states"
77
+ # This is critical for stable injection into a pretrained model
78
+ nn.init.constant_(self.output_gate.bias, gate_bias_init)
79
+
80
+ # Layer norm for memory read output
81
+ self.mem_norm = nn.LayerNorm(hidden_dim)
82
+
83
+ # Dropout
84
+ self.dropout = nn.Dropout(dropout)
85
+
86
+ # Running memory state (not a parameter — persistent buffer)
87
+ self.register_buffer("_memory_state", None, persistent=False)
88
+
89
+ @property
90
+ def memory(self) -> torch.Tensor:
91
+ """Current memory state. Initialized from memory_init if not set."""
92
+ if self._memory_state is None:
93
+ return self.memory_init
94
+ return self._memory_state
95
+
96
+ def reset_memory(self, batch_size: int = 1):
97
+ """Reset memory to initial state."""
98
+ self._memory_state = self.memory_init.expand(batch_size, -1, -1).clone()
99
+
100
+ def forward(
101
+ self,
102
+ hidden_states: torch.Tensor,
103
+ layer_idx: int,
104
+ attention_mask: Optional[torch.Tensor] = None,
105
+ **kwargs
106
+ ) -> torch.Tensor:
107
+ """
108
+ Read from and write to memory.
109
+
110
+ Args:
111
+ hidden_states: [batch, seq_len, hidden_dim]
112
+ layer_idx: Current layer index
113
+
114
+ Returns:
115
+ Modified hidden_states with memory-augmented information
116
+ """
117
+ batch_size, seq_len, _ = hidden_states.shape
118
+
119
+ # Ensure memory is batch-expanded
120
+ memory = self.memory
121
+ if memory.shape[0] == 1 and batch_size > 1:
122
+ memory = memory.expand(batch_size, -1, -1)
123
+ if memory.shape[0] != batch_size:
124
+ memory = self.memory_init.expand(batch_size, -1, -1)
125
+
126
+ memory = memory.to(hidden_states.device)
127
+
128
+ # === MEMORY READ (cross-attention) ===
129
+ Q = self.q_proj(hidden_states) # [B, T, D]
130
+ K = self.k_proj(memory) # [B, N, D]
131
+ V = self.v_proj(memory) # [B, N, D]
132
+
133
+ # Reshape for multi-head attention
134
+ Q = Q.view(batch_size, seq_len, self.num_heads, self.head_dim).transpose(1, 2)
135
+ K = K.view(batch_size, self.num_slots, self.num_heads, self.head_dim).transpose(1, 2)
136
+ V = V.view(batch_size, self.num_slots, self.num_heads, self.head_dim).transpose(1, 2)
137
+
138
+ # Scaled dot-product attention
139
+ attn_weights = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(self.head_dim)
140
+ attn_weights = F.softmax(attn_weights, dim=-1)
141
+ attn_weights = self.dropout(attn_weights)
142
+
143
+ mem_read = torch.matmul(attn_weights, V) # [B, H, T, D_h]
144
+ mem_read = mem_read.transpose(1, 2).contiguous().view(batch_size, seq_len, self.hidden_dim)
145
+ mem_read = self.o_proj(mem_read)
146
+ mem_read = self.mem_norm(mem_read)
147
+
148
+ # === OUTPUT GATING ===
149
+ g_out = torch.sigmoid(self.output_gate(mem_read))
150
+ enhanced = hidden_states + g_out * mem_read
151
+
152
+ # === MEMORY WRITE (update memory state) ===
153
+ write_signal = hidden_states[:, -1:, :] # [B, 1, D]
154
+ write_signal_expanded = write_signal.expand_as(memory)
155
+
156
+ g_forget = torch.sigmoid(self.forget_gate(write_signal_expanded))
157
+ g_input = torch.sigmoid(self.input_gate(write_signal_expanded))
158
+
159
+ new_memory = g_forget * memory + g_input * torch.tanh(write_signal_expanded)
160
+ self._memory_state = new_memory.detach()
161
+
162
+ return enhanced
163
+
164
+ def on_inject(self, model: nn.Module, layer_idx: int, hidden_dim: int):
165
+ """Initialize projection weights to be near-zero for stable injection."""
166
+ nn.init.zeros_(self.o_proj.weight)
167
+
168
+ def extra_repr(self):
169
+ return (f"hidden_dim={self.hidden_dim}, num_slots={self.num_slots}, "
170
+ f"num_heads={self.num_heads}, {super().extra_repr()}")