theapemachine commited on
Commit
36be9a7
·
verified ·
1 Parent(s): aa619cb

Add cortex/pause_and_think.py

Browse files
Files changed (1) hide show
  1. cortex/pause_and_think.py +153 -0
cortex/pause_and_think.py ADDED
@@ -0,0 +1,153 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ PauseAndThink: Learnable latent computation tokens for extended reasoning.
3
+
4
+ Inspired by "Think before you speak: Training LMs with Pause Tokens" (Goyal et al. 2023)
5
+ and "Thoughtbubbles" (2025).
6
+
7
+ Architecture:
8
+ - Injects K learnable "thinking" embeddings into the residual stream
9
+ - These embeddings participate in self-attention (attend to all real tokens)
10
+ - But their outputs are NOT projected to vocabulary — they are computation-only
11
+ - After processing, the thinking tokens' representations are compressed
12
+ back into the real token positions via cross-attention
13
+ - Effectively gives the model K extra "thinking steps" per layer
14
+
15
+ Failure mode addressed:
16
+ - Limited compute per token: standard transformers do fixed computation per token.
17
+ Some tokens (e.g., the final answer token) need more computation.
18
+ - Shallow reasoning: complex multi-step reasoning requires iterating over information.
19
+ Thinking tokens provide extra "scratch space" for intermediate computation.
20
+ - CoT without output tokens: like chain-of-thought, but in latent space —
21
+ no extra output tokens are needed.
22
+
23
+ Injection point: RESIDUAL_STREAM (wraps full block)
24
+ - Rationale: The thinking tokens need to participate in attention.
25
+ By operating at the residual stream level, they can read from and write back
26
+ to the main representation pathway.
27
+ """
28
+
29
+ import torch
30
+ import torch.nn as nn
31
+ import torch.nn.functional as F
32
+ import math
33
+ from typing import Optional, Union, List
34
+ from cortex.core import CortexModule, InjectionPoint
35
+
36
+
37
+ class PauseAndThink(CortexModule):
38
+ """
39
+ Injects learnable computation tokens into the hidden state sequence.
40
+
41
+ These tokens expand the sequence temporarily, participate in attention,
42
+ then their information is compressed back into the original positions.
43
+
44
+ Args:
45
+ hidden_dim: Model hidden dimension
46
+ num_think_tokens: How many thinking tokens to inject
47
+ compress_method: How to fold thinking token info back ("cross_attn" or "gate")
48
+ init_scale: Scale for thinking token initialization
49
+ """
50
+
51
+ def __init__(
52
+ self,
53
+ hidden_dim: int,
54
+ num_think_tokens: int = 8,
55
+ compress_method: str = "gate",
56
+ init_scale: float = 0.01,
57
+ target_layers: Union[List[int], str] = "middle",
58
+ ):
59
+ super().__init__(InjectionPoint.RESIDUAL_STREAM, target_layers)
60
+
61
+ self.hidden_dim = hidden_dim
62
+ self.num_think_tokens = num_think_tokens
63
+ self.compress_method = compress_method
64
+
65
+ # Learnable thinking token embeddings
66
+ self.think_tokens = nn.Parameter(
67
+ torch.randn(1, num_think_tokens, hidden_dim) * init_scale
68
+ )
69
+
70
+ # Projection to adapt thinking tokens to the current context
71
+ self.context_proj = nn.Linear(hidden_dim, hidden_dim, bias=False)
72
+ nn.init.eye_(self.context_proj.weight) # Initialize as identity
73
+
74
+ if compress_method == "cross_attn":
75
+ # Cross-attention to compress thinking tokens back
76
+ self.compress_q = nn.Linear(hidden_dim, hidden_dim, bias=False)
77
+ self.compress_k = nn.Linear(hidden_dim, hidden_dim, bias=False)
78
+ self.compress_v = nn.Linear(hidden_dim, hidden_dim, bias=False)
79
+ self.compress_out = nn.Linear(hidden_dim, hidden_dim, bias=False)
80
+ nn.init.zeros_(self.compress_out.weight) # Start as no-op
81
+ elif compress_method == "gate":
82
+ # Simpler: gated addition from mean-pooled thinking tokens
83
+ self.compress_gate = nn.Sequential(
84
+ nn.Linear(hidden_dim * 2, hidden_dim),
85
+ nn.Sigmoid()
86
+ )
87
+ self.compress_proj = nn.Linear(hidden_dim, hidden_dim, bias=False)
88
+ nn.init.zeros_(self.compress_proj.weight) # Start as no-op
89
+ # Initialize gate bias to make it ~0 initially (conservative)
90
+ nn.init.constant_(self.compress_gate[0].bias, -3.0)
91
+
92
+ # Layer norm
93
+ self.think_norm = nn.LayerNorm(hidden_dim)
94
+
95
+ def forward(
96
+ self,
97
+ hidden_states: torch.Tensor,
98
+ layer_idx: int,
99
+ attention_mask: Optional[torch.Tensor] = None,
100
+ **kwargs
101
+ ) -> torch.Tensor:
102
+ """
103
+ Inject thinking tokens, let them process via the original layer,
104
+ then compress their information back.
105
+
106
+ Since we operate at RESIDUAL_STREAM level (post full block), we use the
107
+ thinking tokens as a read-process-write cycle on the hidden states.
108
+ """
109
+ batch_size, seq_len, _ = hidden_states.shape
110
+
111
+ # === GENERATE CONTEXT-AWARE THINKING TOKENS ===
112
+ context = hidden_states.mean(dim=1, keepdim=True) # [B, 1, D]
113
+
114
+ think = self.think_tokens.expand(batch_size, -1, -1) # [B, K, D]
115
+ think = think + self.context_proj(context) # Context-conditioned
116
+ think = self.think_norm(think)
117
+
118
+ # === THINK: Let thinking tokens interact with hidden states ===
119
+ Q = think # [B, K, D]
120
+ K = hidden_states # [B, T, D]
121
+ V = hidden_states # [B, T, D]
122
+
123
+ attn_scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(self.hidden_dim)
124
+ attn_weights = F.softmax(attn_scores, dim=-1)
125
+ think_output = torch.matmul(attn_weights, V) # [B, K, D]
126
+
127
+ # === COMPRESS: Fold thinking token info back into hidden states ===
128
+ if self.compress_method == "cross_attn":
129
+ cQ = self.compress_q(hidden_states) # [B, T, D]
130
+ cK = self.compress_k(think_output) # [B, K, D]
131
+ cV = self.compress_v(think_output) # [B, K, D]
132
+
133
+ c_attn = torch.matmul(cQ, cK.transpose(-2, -1)) / math.sqrt(self.hidden_dim)
134
+ c_weights = F.softmax(c_attn, dim=-1)
135
+ compressed = torch.matmul(c_weights, cV) # [B, T, D]
136
+ compressed = self.compress_out(compressed)
137
+
138
+ return hidden_states + compressed
139
+
140
+ elif self.compress_method == "gate":
141
+ think_summary = think_output.mean(dim=1, keepdim=True) # [B, 1, D]
142
+ think_summary = think_summary.expand_as(hidden_states) # [B, T, D]
143
+
144
+ gate_input = torch.cat([hidden_states, think_summary], dim=-1)
145
+ gate = self.compress_gate(gate_input) # [B, T, D]
146
+
147
+ projected = self.compress_proj(think_summary)
148
+
149
+ return hidden_states + gate * projected
150
+
151
+ def extra_repr(self):
152
+ return (f"hidden_dim={self.hidden_dim}, num_think_tokens={self.num_think_tokens}, "
153
+ f"compress={self.compress_method}, {super().extra_repr()}")