Memory Augmentation › Memory Writing and Updating

Michael BrenndoerferUpdated July 8, 20257 min read
Language AI HandbookAll chapters
Part 18 of 67Long Context
Chapter 131Memory Augmentation › Memory Writing and Updating
Reading section 5 of 12 · Memory Writing and Updating · 7 minSection 5 of 12 · 7 min

Book contents

Section 5 of 12 · 7 min read

Memory Augmentation › Memory Writing and UpdatingOne section · Sections in this chapter5 / 12 ↓
Reading tools Definitions & inline links

Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.

Article links

Make inline references clickable

Reading is only half the story. The model must also decide what to write to memory and when to update existing entries. Several strategies exist.

Append-Only Memory

The simplest approach appends new key-value pairs without modifying existing entries. This preserves all historical information but causes unbounded memory growth.

In[17]:
Code
class AppendOnlyMemory:
    """
    Memory that grows by appending new entries.
    Simple but unbounded in size.
    """

    def __init__(self, key_dim, value_dim):
        self.key_dim = key_dim
        self.value_dim = value_dim
        self.keys = []
        self.values = []

    def write(self, key, value):
        """Add a new entry to memory."""
        self.keys.append(key)
        self.values.append(value)

    def read(self, query, k=10):
        """Read from memory using top-k retrieval."""
        if not self.keys:
            return None, None

        keys_tensor = torch.stack(self.keys)
        values_tensor = torch.stack(self.values)

        # Compute similarities
        scores = torch.matmul(query, keys_tensor.T)
        k = min(k, len(self.keys))
        topk_scores, topk_indices = torch.topk(scores.squeeze(), k)

        # Retrieve
        topk_values = values_tensor[topk_indices]
        attention = F.softmax(topk_scores, dim=-1)
        retrieved = torch.sum(attention.unsqueeze(-1) * topk_values, dim=0)

        return retrieved, topk_indices

    def size(self):
        return len(self.keys)


# Simulate memory growth
memory = AppendOnlyMemory(key_dim=64, value_dim=128)
tokens_processed = [100, 500, 1000, 5000, 10000]

for n in tokens_processed:
    # Simulate adding entries
    for _ in range(n - memory.size()):
        key = torch.randn(64)
        value = torch.randn(128)
        memory.write(key, value)
Out[18]:
Console
Append-Only Memory Growth:

  Tokens Processed     Memory Size  Memory (MB, est.)
-------------------------------------------------------
               100             100              0.07
               500             500              0.37
             1,000           1,000              0.73
             5,000           5,000              3.66
            10,000          10,000              7.32

Memory usage grows linearly with tokens processed. After processing 10,000 tokens, the memory consumes approximately 0.73 MB. While this seems manageable, extrapolating to 1 million tokens would require 73 MB, and to 100 million tokens would require 7.3 GB. For long-running applications like continuous conversation or document streaming, this unbounded growth eventually exhausts available memory.

Out[19]:
Visualization
Line plot comparing memory size growth over tokens processed for append-only, FIFO, and LRU strategies.
Memory consumption over time for different storage strategies. Append-only grows without bound, eventually exhausting resources. Bounded strategies (FIFO, LRU) plateau at the configured limit, enabling indefinite processing at fixed memory cost.

Bounded Memory with Eviction

To bound memory size, older or less-used entries must be evicted. Common strategies include FIFO (first-in, first-out), LRU (least recently used), and importance-weighted eviction.

In[20]:
Code
class BoundedMemory:
    """
    Memory with fixed maximum size.
    Evicts entries when capacity is reached.
    """

    def __init__(self, max_size, key_dim, value_dim, eviction="fifo"):
        self.max_size = max_size
        self.key_dim = key_dim
        self.value_dim = value_dim
        self.eviction = eviction

        # Storage
        self.keys = torch.zeros(max_size, key_dim)
        self.values = torch.zeros(max_size, value_dim)
        self.write_ptr = 0
        self.filled = 0

        # For LRU tracking
        self.access_times = torch.zeros(max_size)
        self.current_time = 0

    def write(self, key, value):
        """Write to memory, evicting if necessary."""
        if self.eviction == "fifo":
            # Write at current pointer, wrapping around
            idx = self.write_ptr
            self.write_ptr = (self.write_ptr + 1) % self.max_size
        elif self.eviction == "lru":
            if self.filled < self.max_size:
                idx = self.filled
            else:
                # Find least recently accessed
                idx = torch.argmin(self.access_times[: self.filled]).item()
        else:
            raise ValueError(f"Unknown eviction policy: {self.eviction}")

        self.keys[idx] = key
        self.values[idx] = value
        self.filled = min(self.filled + 1, self.max_size)
        self.access_times[idx] = self.current_time
        self.current_time += 1

    def read(self, query, k=10):
        """Read with top-k retrieval, updating access times for LRU."""
        if self.filled == 0:
            return None, None

        active_keys = self.keys[: self.filled]
        active_values = self.values[: self.filled]

        scores = torch.matmul(query, active_keys.T)
        k = min(k, self.filled)
        topk_scores, topk_indices = torch.topk(scores.squeeze(), k)

        # Update access times for LRU
        self.access_times[topk_indices] = self.current_time
        self.current_time += 1

        topk_values = active_values[topk_indices]
        attention = F.softmax(topk_scores, dim=-1)
        retrieved = torch.sum(attention.unsqueeze(-1) * topk_values, dim=0)

        return retrieved, topk_indices


# Compare eviction policies
fifo_mem = BoundedMemory(
    max_size=100, key_dim=64, value_dim=128, eviction="fifo"
)
lru_mem = BoundedMemory(max_size=100, key_dim=64, value_dim=128, eviction="lru")
Out[21]:
Console
Bounded Memory with Different Eviction Policies:

Maximum size: 100 slots

FIFO (First-In, First-Out):
  - Evicts oldest entries regardless of usage
  - Simple, predictable behavior
  - Good when all information has equal importance

LRU (Least Recently Used):
  - Evicts entries not accessed recently
  - Keeps frequently-needed information
  - Better for repeated retrieval patterns

The choice of eviction policy depends on the access patterns of your application. FIFO works well when information has a natural temporal ordering and older entries become less relevant over time. LRU is better suited for applications with "hot" entries that are accessed repeatedly, as it keeps frequently-used information available even if it was written long ago.

Learnable Writing Decisions

More sophisticated approaches learn when and what to write. A gating mechanism can control write operations based on content importance.

In[22]:
Code
class GatedMemoryWriter(nn.Module):
    """
    Learns to gate write operations based on content importance.

    A write gate determines whether new content should be stored.
    This prevents memory from filling with redundant information.
    """

    def __init__(self, input_dim, key_dim, value_dim, memory_size):
        super().__init__()
        self.memory_size = memory_size

        # Projections for key and value
        self.key_proj = nn.Linear(input_dim, key_dim)
        self.value_proj = nn.Linear(input_dim, value_dim)

        # Write gate: determines importance
        self.write_gate = nn.Sequential(
            nn.Linear(input_dim, 64), nn.ReLU(), nn.Linear(64, 1), nn.Sigmoid()
        )

        # Memory storage
        self.keys = nn.Parameter(
            torch.zeros(memory_size, key_dim), requires_grad=False
        )
        self.values = nn.Parameter(
            torch.zeros(memory_size, value_dim), requires_grad=False
        )
        self.write_ptr = 0

    def forward(self, x, threshold=0.5):
        """
        Conditionally write to memory based on learned gate.

        Args:
            x: Input representation to potentially store
            threshold: Write gate threshold

        Returns:
            wrote: Whether the write occurred
            gate_value: The gate's output (importance score)
        """
        # Compute write gate
        gate_value = self.write_gate(x).squeeze()

        if gate_value > threshold:
            key = self.key_proj(x)
            value = self.value_proj(x)

            # Write to memory
            idx = self.write_ptr % self.memory_size
            self.keys.data[idx] = key.detach()
            self.values.data[idx] = value.detach()
            self.write_ptr += 1

            return True, gate_value.item()

        return False, gate_value.item()


# Demonstrate gated writing
writer = GatedMemoryWriter(
    input_dim=256, key_dim=64, value_dim=128, memory_size=100
)

# Simulate different input "importance" levels
inputs = [
    torch.randn(256) * 0.5,  # Low magnitude (likely unimportant)
    torch.randn(256) * 2.0,  # High magnitude (likely important)
    torch.randn(256) * 1.0,  # Medium magnitude
]

write_results = []
for i, inp in enumerate(inputs):
    wrote, gate = writer(inp)
    write_results.append({"input_idx": i, "gate_value": gate, "wrote": wrote})
Out[23]:
Console
Gated Memory Writing:

   Input   Gate Value    Wrote to Memory
------------------------------------------
       0        0.451                 No
       1        0.426                 No
       2        0.477                 No

The gate values demonstrate learned selectivity. With the default threshold of 0.5, inputs with gate values above this threshold get written to memory while others are discarded. In this example with random initialization, the gate produces varying importance scores. After training, the gate would learn to assign high scores to information that proves useful for downstream tasks and low scores to redundant or irrelevant content.

The write gate learns to recognize important information that's worth storing. During training, the model receives gradients based on whether stored information proved useful for downstream tasks. This naturally filters out redundant or irrelevant content.