entroplain 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DEPLOY.md +41 -0
- package/README.md +478 -476
- package/dist/entroplain-0.2.2-py3-none-any.whl +0 -0
- package/dist/entroplain-0.2.2.tar.gz +0 -0
- package/dist/entroplain-0.2.3-py3-none-any.whl +0 -0
- package/dist/entroplain-0.2.3.tar.gz +0 -0
- package/docs/AGENT_USAGE.md +178 -178
- package/docs/USAGE.md +302 -302
- package/entroplain/__init__.py +5 -3
- package/entroplain/cost_tracker.py +231 -231
- package/entroplain/dashboard.py +480 -368
- package/entroplain/monitor.py +390 -390
- package/entroplain/providers.py +626 -626
- package/entroplain/proxy.py +561 -349
- package/entroplain/shared_state.py +72 -0
- package/package.json +47 -46
- package/pyproject.toml +1 -1
- package/scripts/setup.bat +89 -0
- package/scripts/setup.sh +98 -0
- package/test_nvidia.py +56 -56
- package/test_proxy.py +16 -16
- package/vercel.json +6 -0
- package/website/README.md +14 -0
- package/website/app/globals.css +88 -0
- package/website/app/layout.tsx +34 -0
- package/website/app/page.tsx +537 -0
- package/website/package-lock.json +520 -0
- package/website/package.json +25 -0
- package/website/tsconfig.json +40 -0
- package/website/vercel.json +3 -0
- package/dist/entroplain-0.2.0-py3-none-any.whl +0 -0
- package/dist/entroplain-0.2.0.tar.gz +0 -0
package/entroplain/monitor.py
CHANGED
|
@@ -1,390 +1,390 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Entropy Monitor — Core entropy tracking and early exit logic.
|
|
3
|
-
|
|
4
|
-
Supports multiple exit strategies:
|
|
5
|
-
- Valleys plateau: Exit when reasoning milestones stabilize
|
|
6
|
-
- Entropy drop: Exit when model confidence is high
|
|
7
|
-
- Velocity zero: Exit when entropy stops changing
|
|
8
|
-
- Combined: Multiple conditions with AND/OR logic
|
|
9
|
-
- Repetition: Exit when model starts repeating
|
|
10
|
-
- Confidence: Exit when top token probability > threshold for N tokens
|
|
11
|
-
"""
|
|
12
|
-
|
|
13
|
-
import math
|
|
14
|
-
from typing import List, Tuple, Optional, Dict, Any, Callable
|
|
15
|
-
from dataclasses import dataclass, field
|
|
16
|
-
from enum import Enum
|
|
17
|
-
from collections import Counter
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
class ExitCondition(Enum):
|
|
21
|
-
VALLEYS_PLATEAU = "valleys_plateau"
|
|
22
|
-
ENTROPY_DROP = "entropy_drop"
|
|
23
|
-
VELOCITY_ZERO = "velocity_zero"
|
|
24
|
-
COMBINED = "combined"
|
|
25
|
-
# New strategies
|
|
26
|
-
REPETITION = "repetition"
|
|
27
|
-
CONFIDENCE = "confidence"
|
|
28
|
-
SEMANTIC = "semantic"
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
@dataclass
|
|
32
|
-
class EntropyPoint:
|
|
33
|
-
"""A single point in the entropy trajectory."""
|
|
34
|
-
index: int
|
|
35
|
-
token: str
|
|
36
|
-
entropy: float
|
|
37
|
-
is_valley: bool = False
|
|
38
|
-
velocity: float = 0.0
|
|
39
|
-
confidence: float = 0.0 # Top token probability
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
@dataclass
|
|
43
|
-
class MonitorConfig:
|
|
44
|
-
"""Configuration for the entropy monitor."""
|
|
45
|
-
entropy_threshold: float = 0.15
|
|
46
|
-
min_valleys: int = 2
|
|
47
|
-
velocity_threshold: float = 0.05
|
|
48
|
-
min_tokens: int = 50
|
|
49
|
-
valley_window: int = 5
|
|
50
|
-
plateau_threshold: int = 3
|
|
51
|
-
exit_condition: ExitCondition = ExitCondition.COMBINED
|
|
52
|
-
# New config options
|
|
53
|
-
repetition_window: int = 20 # Window to check for repetition
|
|
54
|
-
repetition_threshold: float = 0.3 # 30% repetition = exit
|
|
55
|
-
confidence_threshold: float = 0.95 # 95% confidence = exit
|
|
56
|
-
confidence_min_tokens: int = 5 # Min tokens at high confidence
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
class EntropyMonitor:
|
|
60
|
-
"""
|
|
61
|
-
Monitor entropy trajectory and detect reasoning convergence.
|
|
62
|
-
|
|
63
|
-
Usage:
|
|
64
|
-
monitor = EntropyMonitor()
|
|
65
|
-
for token, entropy in stream:
|
|
66
|
-
monitor.track(token, entropy)
|
|
67
|
-
if monitor.should_exit():
|
|
68
|
-
break
|
|
69
|
-
"""
|
|
70
|
-
|
|
71
|
-
def __init__(
|
|
72
|
-
self,
|
|
73
|
-
entropy_threshold: float = 0.15,
|
|
74
|
-
min_valleys: int = 2,
|
|
75
|
-
velocity_threshold: float = 0.05,
|
|
76
|
-
min_tokens: int = 50,
|
|
77
|
-
valley_window: int = 5,
|
|
78
|
-
plateau_threshold: int = 3,
|
|
79
|
-
exit_condition: str = "combined",
|
|
80
|
-
# New parameters
|
|
81
|
-
repetition_window: int = 20,
|
|
82
|
-
repetition_threshold: float = 0.3,
|
|
83
|
-
confidence_threshold: float = 0.95,
|
|
84
|
-
confidence_min_tokens: int = 5,
|
|
85
|
-
):
|
|
86
|
-
self.config = MonitorConfig(
|
|
87
|
-
entropy_threshold=entropy_threshold,
|
|
88
|
-
min_valleys=min_valleys,
|
|
89
|
-
velocity_threshold=velocity_threshold,
|
|
90
|
-
min_tokens=min_tokens,
|
|
91
|
-
valley_window=valley_window,
|
|
92
|
-
plateau_threshold=plateau_threshold,
|
|
93
|
-
exit_condition=ExitCondition(exit_condition),
|
|
94
|
-
repetition_window=repetition_window,
|
|
95
|
-
repetition_threshold=repetition_threshold,
|
|
96
|
-
confidence_threshold=confidence_threshold,
|
|
97
|
-
confidence_min_tokens=confidence_min_tokens,
|
|
98
|
-
)
|
|
99
|
-
self._trajectory: List[EntropyPoint] = []
|
|
100
|
-
self._valleys: List[EntropyPoint] = []
|
|
101
|
-
self._index = 0
|
|
102
|
-
self._high_confidence_count = 0 # Track consecutive high confidence
|
|
103
|
-
|
|
104
|
-
def calculate_entropy(self, logprobs: List[float], from_probs: bool = False) -> float:
|
|
105
|
-
"""
|
|
106
|
-
Calculate Shannon entropy from log probabilities or probabilities.
|
|
107
|
-
|
|
108
|
-
Args:
|
|
109
|
-
logprobs: List of log probabilities (natural log) or probabilities
|
|
110
|
-
from_probs: If True, treat input as probabilities (will convert)
|
|
111
|
-
|
|
112
|
-
Returns:
|
|
113
|
-
Shannon entropy in bits
|
|
114
|
-
"""
|
|
115
|
-
if not logprobs:
|
|
116
|
-
return 0.0
|
|
117
|
-
|
|
118
|
-
entropy = 0.0
|
|
119
|
-
for lp in logprobs:
|
|
120
|
-
if from_probs:
|
|
121
|
-
prob = lp
|
|
122
|
-
else:
|
|
123
|
-
prob = math.exp(lp)
|
|
124
|
-
if prob > 0:
|
|
125
|
-
entropy -= prob * math.log2(prob + 1e-10)
|
|
126
|
-
|
|
127
|
-
return entropy
|
|
128
|
-
|
|
129
|
-
def track(self, token: str, entropy: float, confidence: float = 0.0) -> EntropyPoint:
|
|
130
|
-
"""
|
|
131
|
-
Track a token and its entropy value.
|
|
132
|
-
|
|
133
|
-
Args:
|
|
134
|
-
token: The generated token
|
|
135
|
-
entropy: Calculated entropy for this token
|
|
136
|
-
confidence: Top token probability (optional, for confidence strategy)
|
|
137
|
-
|
|
138
|
-
Returns:
|
|
139
|
-
EntropyPoint with valley detection
|
|
140
|
-
"""
|
|
141
|
-
point = EntropyPoint(
|
|
142
|
-
index=self._index,
|
|
143
|
-
token=token,
|
|
144
|
-
entropy=entropy,
|
|
145
|
-
confidence=confidence
|
|
146
|
-
)
|
|
147
|
-
|
|
148
|
-
# Calculate velocity
|
|
149
|
-
if len(self._trajectory) > 0:
|
|
150
|
-
prev = self._trajectory[-1]
|
|
151
|
-
point.velocity = abs(entropy - prev.entropy)
|
|
152
|
-
|
|
153
|
-
# Detect valley (local minimum)
|
|
154
|
-
if len(self._trajectory) >= 2:
|
|
155
|
-
prev2 = self._trajectory[-2]
|
|
156
|
-
prev1 = self._trajectory[-1]
|
|
157
|
-
if prev1.entropy < prev2.entropy and prev1.entropy < entropy:
|
|
158
|
-
prev1.is_valley = True
|
|
159
|
-
self._valleys.append(prev1)
|
|
160
|
-
|
|
161
|
-
self._trajectory.append(point)
|
|
162
|
-
self._index += 1
|
|
163
|
-
|
|
164
|
-
# Track high confidence
|
|
165
|
-
if confidence >= self.config.confidence_threshold:
|
|
166
|
-
self._high_confidence_count += 1
|
|
167
|
-
else:
|
|
168
|
-
self._high_confidence_count = 0
|
|
169
|
-
|
|
170
|
-
return point
|
|
171
|
-
|
|
172
|
-
def get_valleys(self) -> List[Tuple[int, float]]:
|
|
173
|
-
"""Get all entropy valleys (local minima) as (index, entropy) tuples."""
|
|
174
|
-
return [(v.index, v.entropy) for v in self._valleys]
|
|
175
|
-
|
|
176
|
-
def get_velocity(self) -> float:
|
|
177
|
-
"""Get current entropy velocity (rate of change)."""
|
|
178
|
-
if len(self._trajectory) < 2:
|
|
179
|
-
return 0.0
|
|
180
|
-
return self._trajectory[-1].velocity
|
|
181
|
-
|
|
182
|
-
def get_mean_entropy(self) -> float:
|
|
183
|
-
"""Get mean entropy over the trajectory."""
|
|
184
|
-
if not self._trajectory:
|
|
185
|
-
return 0.0
|
|
186
|
-
return sum(p.entropy for p in self._trajectory) / len(self._trajectory)
|
|
187
|
-
|
|
188
|
-
def get_valley_count(self) -> int:
|
|
189
|
-
"""Get the number of detected valleys."""
|
|
190
|
-
return len(self._valleys)
|
|
191
|
-
|
|
192
|
-
def is_valleys_plateau(self) -> bool:
|
|
193
|
-
"""Check if valley count has plateaued."""
|
|
194
|
-
if len(self._valleys) < self.config.min_valleys:
|
|
195
|
-
return False
|
|
196
|
-
|
|
197
|
-
# Check if last N valleys have similar spacing
|
|
198
|
-
recent = self._valleys[-self.config.plateau_threshold:]
|
|
199
|
-
if len(recent) < self.config.plateau_threshold:
|
|
200
|
-
return False
|
|
201
|
-
|
|
202
|
-
# Calculate spacing between recent valleys
|
|
203
|
-
spacings = [
|
|
204
|
-
recent[i + 1].index - recent[i].index
|
|
205
|
-
for i in range(len(recent) - 1)
|
|
206
|
-
]
|
|
207
|
-
if not spacings:
|
|
208
|
-
return False
|
|
209
|
-
|
|
210
|
-
mean_spacing = sum(spacings) / len(spacings)
|
|
211
|
-
variance = sum((s - mean_spacing) ** 2 for s in spacings) / len(spacings)
|
|
212
|
-
|
|
213
|
-
# Low variance in spacing = plateau
|
|
214
|
-
return variance < 10 # Threshold tuned empirically
|
|
215
|
-
|
|
216
|
-
def is_entropy_low(self) -> bool:
|
|
217
|
-
"""Check if current entropy is below threshold."""
|
|
218
|
-
if not self._trajectory:
|
|
219
|
-
return False
|
|
220
|
-
return self._trajectory[-1].entropy < self.config.entropy_threshold
|
|
221
|
-
|
|
222
|
-
def is_velocity_stable(self) -> bool:
|
|
223
|
-
"""Check if velocity is below threshold."""
|
|
224
|
-
return self.get_velocity() < self.config.velocity_threshold
|
|
225
|
-
|
|
226
|
-
def is_repeating(self) -> bool:
|
|
227
|
-
"""
|
|
228
|
-
Check if the model is repeating itself.
|
|
229
|
-
|
|
230
|
-
Returns True if the repetition ratio in the recent window
|
|
231
|
-
exceeds the threshold.
|
|
232
|
-
"""
|
|
233
|
-
if len(self._trajectory) < self.config.repetition_window:
|
|
234
|
-
return False
|
|
235
|
-
|
|
236
|
-
# Get recent tokens
|
|
237
|
-
recent_tokens = [
|
|
238
|
-
p.token for p in self._trajectory[-self.config.repetition_window :]
|
|
239
|
-
]
|
|
240
|
-
|
|
241
|
-
# Count unique vs total
|
|
242
|
-
counter = Counter(recent_tokens)
|
|
243
|
-
unique_count = len(counter)
|
|
244
|
-
total_count = len(recent_tokens)
|
|
245
|
-
|
|
246
|
-
# Calculate repetition ratio
|
|
247
|
-
repetition_ratio = 1.0 - (unique_count / total_count)
|
|
248
|
-
|
|
249
|
-
return repetition_ratio >= self.config.repetition_threshold
|
|
250
|
-
|
|
251
|
-
def is_confident(self) -> bool:
|
|
252
|
-
"""
|
|
253
|
-
Check if model has been highly confident for consecutive tokens.
|
|
254
|
-
|
|
255
|
-
Returns True if the last N tokens had confidence >= threshold.
|
|
256
|
-
"""
|
|
257
|
-
return self._high_confidence_count >= self.config.confidence_min_tokens
|
|
258
|
-
|
|
259
|
-
def should_exit(self) -> bool:
|
|
260
|
-
"""
|
|
261
|
-
Determine if reasoning has converged and we should exit.
|
|
262
|
-
|
|
263
|
-
Uses the configured exit condition:
|
|
264
|
-
- valleys_plateau: Exit when valley count plateaus
|
|
265
|
-
- entropy_drop: Exit when entropy drops below threshold
|
|
266
|
-
- velocity_zero: Exit when velocity stabilizes
|
|
267
|
-
- combined: Use all conditions with AND logic
|
|
268
|
-
- repetition: Exit when model starts repeating
|
|
269
|
-
- confidence: Exit when confidence is high for N tokens
|
|
270
|
-
"""
|
|
271
|
-
# Always require minimum tokens
|
|
272
|
-
if len(self._trajectory) < self.config.min_tokens:
|
|
273
|
-
return False
|
|
274
|
-
|
|
275
|
-
# Always require minimum valleys (for most strategies)
|
|
276
|
-
condition = self.config.exit_condition
|
|
277
|
-
|
|
278
|
-
if condition == ExitCondition.REPETITION:
|
|
279
|
-
# Repetition doesn't require valleys
|
|
280
|
-
return self.is_repeating()
|
|
281
|
-
|
|
282
|
-
if condition == ExitCondition.CONFIDENCE:
|
|
283
|
-
# Confidence doesn't require valleys
|
|
284
|
-
return self.is_confident()
|
|
285
|
-
|
|
286
|
-
# For other strategies, require minimum valleys
|
|
287
|
-
if len(self._valleys) < self.config.min_valleys:
|
|
288
|
-
return False
|
|
289
|
-
|
|
290
|
-
if condition == ExitCondition.VALLEYS_PLATEAU:
|
|
291
|
-
return self.is_valleys_plateau()
|
|
292
|
-
|
|
293
|
-
if condition == ExitCondition.ENTROPY_DROP:
|
|
294
|
-
return self.is_entropy_low()
|
|
295
|
-
|
|
296
|
-
if condition == ExitCondition.VELOCITY_ZERO:
|
|
297
|
-
return self.is_velocity_stable()
|
|
298
|
-
|
|
299
|
-
if condition == ExitCondition.COMBINED:
|
|
300
|
-
# Combined: require entropy low OR valleys plateau, AND velocity stable
|
|
301
|
-
return (self.is_entropy_low() or self.is_valleys_plateau()) and self.is_velocity_stable()
|
|
302
|
-
|
|
303
|
-
if condition == ExitCondition.SEMANTIC:
|
|
304
|
-
# Placeholder for future semantic convergence detection
|
|
305
|
-
# Would use embeddings to detect when output stabilizes semantically
|
|
306
|
-
return False
|
|
307
|
-
|
|
308
|
-
return False
|
|
309
|
-
|
|
310
|
-
def is_converged(self) -> bool:
|
|
311
|
-
"""Alias for should_exit()."""
|
|
312
|
-
return self.should_exit()
|
|
313
|
-
|
|
314
|
-
def get_trajectory(self) -> List[float]:
|
|
315
|
-
"""Get full entropy trajectory as list of floats."""
|
|
316
|
-
return [p.entropy for p in self._trajectory]
|
|
317
|
-
|
|
318
|
-
def get_tokens(self) -> List[str]:
|
|
319
|
-
"""Get all tracked tokens."""
|
|
320
|
-
return [p.token for p in self._trajectory]
|
|
321
|
-
|
|
322
|
-
def get_stats(self) -> Dict[str, Any]:
|
|
323
|
-
"""Get summary statistics."""
|
|
324
|
-
if not self._trajectory:
|
|
325
|
-
return {}
|
|
326
|
-
|
|
327
|
-
entropies = [p.entropy for p in self._trajectory]
|
|
328
|
-
return {
|
|
329
|
-
"token_count": len(self._trajectory),
|
|
330
|
-
"valley_count": len(self._valleys),
|
|
331
|
-
"mean_entropy": sum(entropies) / len(entropies),
|
|
332
|
-
"min_entropy": min(entropies),
|
|
333
|
-
"max_entropy": max(entropies),
|
|
334
|
-
"current_entropy": entropies[-1],
|
|
335
|
-
"current_velocity": self.get_velocity(),
|
|
336
|
-
"is_converged": self.should_exit(),
|
|
337
|
-
"exit_reason": self._get_exit_reason(),
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
def _get_exit_reason(self) -> Optional[str]:
|
|
341
|
-
"""Get the reason for early exit (if triggered)."""
|
|
342
|
-
if not self.should_exit():
|
|
343
|
-
return None
|
|
344
|
-
|
|
345
|
-
condition = self.config.exit_condition
|
|
346
|
-
|
|
347
|
-
if condition == ExitCondition.REPETITION:
|
|
348
|
-
return "repetition_detected"
|
|
349
|
-
if condition == ExitCondition.CONFIDENCE:
|
|
350
|
-
return "high_confidence"
|
|
351
|
-
if condition == ExitCondition.ENTROPY_DROP:
|
|
352
|
-
return "entropy_below_threshold"
|
|
353
|
-
if condition == ExitCondition.VELOCITY_ZERO:
|
|
354
|
-
return "velocity_stable"
|
|
355
|
-
if condition == ExitCondition.VALLEYS_PLATEAU:
|
|
356
|
-
return "valleys_plateau"
|
|
357
|
-
if condition == ExitCondition.COMBINED:
|
|
358
|
-
if self.is_entropy_low() and self.is_velocity_stable():
|
|
359
|
-
return "entropy_low_velocity_stable"
|
|
360
|
-
if self.is_valleys_plateau() and self.is_velocity_stable():
|
|
361
|
-
return "valleys_plateau_velocity_stable"
|
|
362
|
-
return "combined"
|
|
363
|
-
|
|
364
|
-
return "unknown"
|
|
365
|
-
|
|
366
|
-
def reset(self) -> None:
|
|
367
|
-
"""Clear all tracked data."""
|
|
368
|
-
self._trajectory.clear()
|
|
369
|
-
self._valleys.clear()
|
|
370
|
-
self._index = 0
|
|
371
|
-
self._high_confidence_count = 0
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
# Convenience function for one-shot entropy calculation
|
|
375
|
-
def calculate_entropy_from_logprobs(logprobs: List[float]) -> float:
|
|
376
|
-
"""
|
|
377
|
-
Calculate Shannon entropy from log probabilities.
|
|
378
|
-
|
|
379
|
-
Args:
|
|
380
|
-
logprobs: List of log probabilities (natural log)
|
|
381
|
-
|
|
382
|
-
Returns:
|
|
383
|
-
Shannon entropy in bits
|
|
384
|
-
"""
|
|
385
|
-
entropy = 0.0
|
|
386
|
-
for lp in logprobs:
|
|
387
|
-
prob = math.exp(lp)
|
|
388
|
-
if prob > 0:
|
|
389
|
-
entropy -= prob * math.log2(prob + 1e-10)
|
|
390
|
-
return entropy
|
|
1
|
+
"""
|
|
2
|
+
Entropy Monitor — Core entropy tracking and early exit logic.
|
|
3
|
+
|
|
4
|
+
Supports multiple exit strategies:
|
|
5
|
+
- Valleys plateau: Exit when reasoning milestones stabilize
|
|
6
|
+
- Entropy drop: Exit when model confidence is high
|
|
7
|
+
- Velocity zero: Exit when entropy stops changing
|
|
8
|
+
- Combined: Multiple conditions with AND/OR logic
|
|
9
|
+
- Repetition: Exit when model starts repeating
|
|
10
|
+
- Confidence: Exit when top token probability > threshold for N tokens
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import math
|
|
14
|
+
from typing import List, Tuple, Optional, Dict, Any, Callable
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from enum import Enum
|
|
17
|
+
from collections import Counter
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ExitCondition(Enum):
|
|
21
|
+
VALLEYS_PLATEAU = "valleys_plateau"
|
|
22
|
+
ENTROPY_DROP = "entropy_drop"
|
|
23
|
+
VELOCITY_ZERO = "velocity_zero"
|
|
24
|
+
COMBINED = "combined"
|
|
25
|
+
# New strategies
|
|
26
|
+
REPETITION = "repetition"
|
|
27
|
+
CONFIDENCE = "confidence"
|
|
28
|
+
SEMANTIC = "semantic"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class EntropyPoint:
|
|
33
|
+
"""A single point in the entropy trajectory."""
|
|
34
|
+
index: int
|
|
35
|
+
token: str
|
|
36
|
+
entropy: float
|
|
37
|
+
is_valley: bool = False
|
|
38
|
+
velocity: float = 0.0
|
|
39
|
+
confidence: float = 0.0 # Top token probability
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class MonitorConfig:
|
|
44
|
+
"""Configuration for the entropy monitor."""
|
|
45
|
+
entropy_threshold: float = 0.15
|
|
46
|
+
min_valleys: int = 2
|
|
47
|
+
velocity_threshold: float = 0.05
|
|
48
|
+
min_tokens: int = 50
|
|
49
|
+
valley_window: int = 5
|
|
50
|
+
plateau_threshold: int = 3
|
|
51
|
+
exit_condition: ExitCondition = ExitCondition.COMBINED
|
|
52
|
+
# New config options
|
|
53
|
+
repetition_window: int = 20 # Window to check for repetition
|
|
54
|
+
repetition_threshold: float = 0.3 # 30% repetition = exit
|
|
55
|
+
confidence_threshold: float = 0.95 # 95% confidence = exit
|
|
56
|
+
confidence_min_tokens: int = 5 # Min tokens at high confidence
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class EntropyMonitor:
|
|
60
|
+
"""
|
|
61
|
+
Monitor entropy trajectory and detect reasoning convergence.
|
|
62
|
+
|
|
63
|
+
Usage:
|
|
64
|
+
monitor = EntropyMonitor()
|
|
65
|
+
for token, entropy in stream:
|
|
66
|
+
monitor.track(token, entropy)
|
|
67
|
+
if monitor.should_exit():
|
|
68
|
+
break
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
def __init__(
|
|
72
|
+
self,
|
|
73
|
+
entropy_threshold: float = 0.15,
|
|
74
|
+
min_valleys: int = 2,
|
|
75
|
+
velocity_threshold: float = 0.05,
|
|
76
|
+
min_tokens: int = 50,
|
|
77
|
+
valley_window: int = 5,
|
|
78
|
+
plateau_threshold: int = 3,
|
|
79
|
+
exit_condition: str = "combined",
|
|
80
|
+
# New parameters
|
|
81
|
+
repetition_window: int = 20,
|
|
82
|
+
repetition_threshold: float = 0.3,
|
|
83
|
+
confidence_threshold: float = 0.95,
|
|
84
|
+
confidence_min_tokens: int = 5,
|
|
85
|
+
):
|
|
86
|
+
self.config = MonitorConfig(
|
|
87
|
+
entropy_threshold=entropy_threshold,
|
|
88
|
+
min_valleys=min_valleys,
|
|
89
|
+
velocity_threshold=velocity_threshold,
|
|
90
|
+
min_tokens=min_tokens,
|
|
91
|
+
valley_window=valley_window,
|
|
92
|
+
plateau_threshold=plateau_threshold,
|
|
93
|
+
exit_condition=ExitCondition(exit_condition),
|
|
94
|
+
repetition_window=repetition_window,
|
|
95
|
+
repetition_threshold=repetition_threshold,
|
|
96
|
+
confidence_threshold=confidence_threshold,
|
|
97
|
+
confidence_min_tokens=confidence_min_tokens,
|
|
98
|
+
)
|
|
99
|
+
self._trajectory: List[EntropyPoint] = []
|
|
100
|
+
self._valleys: List[EntropyPoint] = []
|
|
101
|
+
self._index = 0
|
|
102
|
+
self._high_confidence_count = 0 # Track consecutive high confidence
|
|
103
|
+
|
|
104
|
+
def calculate_entropy(self, logprobs: List[float], from_probs: bool = False) -> float:
|
|
105
|
+
"""
|
|
106
|
+
Calculate Shannon entropy from log probabilities or probabilities.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
logprobs: List of log probabilities (natural log) or probabilities
|
|
110
|
+
from_probs: If True, treat input as probabilities (will convert)
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
Shannon entropy in bits
|
|
114
|
+
"""
|
|
115
|
+
if not logprobs:
|
|
116
|
+
return 0.0
|
|
117
|
+
|
|
118
|
+
entropy = 0.0
|
|
119
|
+
for lp in logprobs:
|
|
120
|
+
if from_probs:
|
|
121
|
+
prob = lp
|
|
122
|
+
else:
|
|
123
|
+
prob = math.exp(lp)
|
|
124
|
+
if prob > 0:
|
|
125
|
+
entropy -= prob * math.log2(prob + 1e-10)
|
|
126
|
+
|
|
127
|
+
return entropy
|
|
128
|
+
|
|
129
|
+
def track(self, token: str, entropy: float, confidence: float = 0.0) -> EntropyPoint:
|
|
130
|
+
"""
|
|
131
|
+
Track a token and its entropy value.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
token: The generated token
|
|
135
|
+
entropy: Calculated entropy for this token
|
|
136
|
+
confidence: Top token probability (optional, for confidence strategy)
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
EntropyPoint with valley detection
|
|
140
|
+
"""
|
|
141
|
+
point = EntropyPoint(
|
|
142
|
+
index=self._index,
|
|
143
|
+
token=token,
|
|
144
|
+
entropy=entropy,
|
|
145
|
+
confidence=confidence
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
# Calculate velocity
|
|
149
|
+
if len(self._trajectory) > 0:
|
|
150
|
+
prev = self._trajectory[-1]
|
|
151
|
+
point.velocity = abs(entropy - prev.entropy)
|
|
152
|
+
|
|
153
|
+
# Detect valley (local minimum)
|
|
154
|
+
if len(self._trajectory) >= 2:
|
|
155
|
+
prev2 = self._trajectory[-2]
|
|
156
|
+
prev1 = self._trajectory[-1]
|
|
157
|
+
if prev1.entropy < prev2.entropy and prev1.entropy < entropy:
|
|
158
|
+
prev1.is_valley = True
|
|
159
|
+
self._valleys.append(prev1)
|
|
160
|
+
|
|
161
|
+
self._trajectory.append(point)
|
|
162
|
+
self._index += 1
|
|
163
|
+
|
|
164
|
+
# Track high confidence
|
|
165
|
+
if confidence >= self.config.confidence_threshold:
|
|
166
|
+
self._high_confidence_count += 1
|
|
167
|
+
else:
|
|
168
|
+
self._high_confidence_count = 0
|
|
169
|
+
|
|
170
|
+
return point
|
|
171
|
+
|
|
172
|
+
def get_valleys(self) -> List[Tuple[int, float]]:
|
|
173
|
+
"""Get all entropy valleys (local minima) as (index, entropy) tuples."""
|
|
174
|
+
return [(v.index, v.entropy) for v in self._valleys]
|
|
175
|
+
|
|
176
|
+
def get_velocity(self) -> float:
|
|
177
|
+
"""Get current entropy velocity (rate of change)."""
|
|
178
|
+
if len(self._trajectory) < 2:
|
|
179
|
+
return 0.0
|
|
180
|
+
return self._trajectory[-1].velocity
|
|
181
|
+
|
|
182
|
+
def get_mean_entropy(self) -> float:
|
|
183
|
+
"""Get mean entropy over the trajectory."""
|
|
184
|
+
if not self._trajectory:
|
|
185
|
+
return 0.0
|
|
186
|
+
return sum(p.entropy for p in self._trajectory) / len(self._trajectory)
|
|
187
|
+
|
|
188
|
+
def get_valley_count(self) -> int:
|
|
189
|
+
"""Get the number of detected valleys."""
|
|
190
|
+
return len(self._valleys)
|
|
191
|
+
|
|
192
|
+
def is_valleys_plateau(self) -> bool:
|
|
193
|
+
"""Check if valley count has plateaued."""
|
|
194
|
+
if len(self._valleys) < self.config.min_valleys:
|
|
195
|
+
return False
|
|
196
|
+
|
|
197
|
+
# Check if last N valleys have similar spacing
|
|
198
|
+
recent = self._valleys[-self.config.plateau_threshold:]
|
|
199
|
+
if len(recent) < self.config.plateau_threshold:
|
|
200
|
+
return False
|
|
201
|
+
|
|
202
|
+
# Calculate spacing between recent valleys
|
|
203
|
+
spacings = [
|
|
204
|
+
recent[i + 1].index - recent[i].index
|
|
205
|
+
for i in range(len(recent) - 1)
|
|
206
|
+
]
|
|
207
|
+
if not spacings:
|
|
208
|
+
return False
|
|
209
|
+
|
|
210
|
+
mean_spacing = sum(spacings) / len(spacings)
|
|
211
|
+
variance = sum((s - mean_spacing) ** 2 for s in spacings) / len(spacings)
|
|
212
|
+
|
|
213
|
+
# Low variance in spacing = plateau
|
|
214
|
+
return variance < 10 # Threshold tuned empirically
|
|
215
|
+
|
|
216
|
+
def is_entropy_low(self) -> bool:
|
|
217
|
+
"""Check if current entropy is below threshold."""
|
|
218
|
+
if not self._trajectory:
|
|
219
|
+
return False
|
|
220
|
+
return self._trajectory[-1].entropy < self.config.entropy_threshold
|
|
221
|
+
|
|
222
|
+
def is_velocity_stable(self) -> bool:
|
|
223
|
+
"""Check if velocity is below threshold."""
|
|
224
|
+
return self.get_velocity() < self.config.velocity_threshold
|
|
225
|
+
|
|
226
|
+
def is_repeating(self) -> bool:
|
|
227
|
+
"""
|
|
228
|
+
Check if the model is repeating itself.
|
|
229
|
+
|
|
230
|
+
Returns True if the repetition ratio in the recent window
|
|
231
|
+
exceeds the threshold.
|
|
232
|
+
"""
|
|
233
|
+
if len(self._trajectory) < self.config.repetition_window:
|
|
234
|
+
return False
|
|
235
|
+
|
|
236
|
+
# Get recent tokens
|
|
237
|
+
recent_tokens = [
|
|
238
|
+
p.token for p in self._trajectory[-self.config.repetition_window :]
|
|
239
|
+
]
|
|
240
|
+
|
|
241
|
+
# Count unique vs total
|
|
242
|
+
counter = Counter(recent_tokens)
|
|
243
|
+
unique_count = len(counter)
|
|
244
|
+
total_count = len(recent_tokens)
|
|
245
|
+
|
|
246
|
+
# Calculate repetition ratio
|
|
247
|
+
repetition_ratio = 1.0 - (unique_count / total_count)
|
|
248
|
+
|
|
249
|
+
return repetition_ratio >= self.config.repetition_threshold
|
|
250
|
+
|
|
251
|
+
def is_confident(self) -> bool:
|
|
252
|
+
"""
|
|
253
|
+
Check if model has been highly confident for consecutive tokens.
|
|
254
|
+
|
|
255
|
+
Returns True if the last N tokens had confidence >= threshold.
|
|
256
|
+
"""
|
|
257
|
+
return self._high_confidence_count >= self.config.confidence_min_tokens
|
|
258
|
+
|
|
259
|
+
def should_exit(self) -> bool:
|
|
260
|
+
"""
|
|
261
|
+
Determine if reasoning has converged and we should exit.
|
|
262
|
+
|
|
263
|
+
Uses the configured exit condition:
|
|
264
|
+
- valleys_plateau: Exit when valley count plateaus
|
|
265
|
+
- entropy_drop: Exit when entropy drops below threshold
|
|
266
|
+
- velocity_zero: Exit when velocity stabilizes
|
|
267
|
+
- combined: Use all conditions with AND logic
|
|
268
|
+
- repetition: Exit when model starts repeating
|
|
269
|
+
- confidence: Exit when confidence is high for N tokens
|
|
270
|
+
"""
|
|
271
|
+
# Always require minimum tokens
|
|
272
|
+
if len(self._trajectory) < self.config.min_tokens:
|
|
273
|
+
return False
|
|
274
|
+
|
|
275
|
+
# Always require minimum valleys (for most strategies)
|
|
276
|
+
condition = self.config.exit_condition
|
|
277
|
+
|
|
278
|
+
if condition == ExitCondition.REPETITION:
|
|
279
|
+
# Repetition doesn't require valleys
|
|
280
|
+
return self.is_repeating()
|
|
281
|
+
|
|
282
|
+
if condition == ExitCondition.CONFIDENCE:
|
|
283
|
+
# Confidence doesn't require valleys
|
|
284
|
+
return self.is_confident()
|
|
285
|
+
|
|
286
|
+
# For other strategies, require minimum valleys
|
|
287
|
+
if len(self._valleys) < self.config.min_valleys:
|
|
288
|
+
return False
|
|
289
|
+
|
|
290
|
+
if condition == ExitCondition.VALLEYS_PLATEAU:
|
|
291
|
+
return self.is_valleys_plateau()
|
|
292
|
+
|
|
293
|
+
if condition == ExitCondition.ENTROPY_DROP:
|
|
294
|
+
return self.is_entropy_low()
|
|
295
|
+
|
|
296
|
+
if condition == ExitCondition.VELOCITY_ZERO:
|
|
297
|
+
return self.is_velocity_stable()
|
|
298
|
+
|
|
299
|
+
if condition == ExitCondition.COMBINED:
|
|
300
|
+
# Combined: require entropy low OR valleys plateau, AND velocity stable
|
|
301
|
+
return (self.is_entropy_low() or self.is_valleys_plateau()) and self.is_velocity_stable()
|
|
302
|
+
|
|
303
|
+
if condition == ExitCondition.SEMANTIC:
|
|
304
|
+
# Placeholder for future semantic convergence detection
|
|
305
|
+
# Would use embeddings to detect when output stabilizes semantically
|
|
306
|
+
return False
|
|
307
|
+
|
|
308
|
+
return False
|
|
309
|
+
|
|
310
|
+
def is_converged(self) -> bool:
|
|
311
|
+
"""Alias for should_exit()."""
|
|
312
|
+
return self.should_exit()
|
|
313
|
+
|
|
314
|
+
def get_trajectory(self) -> List[float]:
|
|
315
|
+
"""Get full entropy trajectory as list of floats."""
|
|
316
|
+
return [p.entropy for p in self._trajectory]
|
|
317
|
+
|
|
318
|
+
def get_tokens(self) -> List[str]:
|
|
319
|
+
"""Get all tracked tokens."""
|
|
320
|
+
return [p.token for p in self._trajectory]
|
|
321
|
+
|
|
322
|
+
def get_stats(self) -> Dict[str, Any]:
|
|
323
|
+
"""Get summary statistics."""
|
|
324
|
+
if not self._trajectory:
|
|
325
|
+
return {}
|
|
326
|
+
|
|
327
|
+
entropies = [p.entropy for p in self._trajectory]
|
|
328
|
+
return {
|
|
329
|
+
"token_count": len(self._trajectory),
|
|
330
|
+
"valley_count": len(self._valleys),
|
|
331
|
+
"mean_entropy": sum(entropies) / len(entropies),
|
|
332
|
+
"min_entropy": min(entropies),
|
|
333
|
+
"max_entropy": max(entropies),
|
|
334
|
+
"current_entropy": entropies[-1],
|
|
335
|
+
"current_velocity": self.get_velocity(),
|
|
336
|
+
"is_converged": self.should_exit(),
|
|
337
|
+
"exit_reason": self._get_exit_reason(),
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
def _get_exit_reason(self) -> Optional[str]:
|
|
341
|
+
"""Get the reason for early exit (if triggered)."""
|
|
342
|
+
if not self.should_exit():
|
|
343
|
+
return None
|
|
344
|
+
|
|
345
|
+
condition = self.config.exit_condition
|
|
346
|
+
|
|
347
|
+
if condition == ExitCondition.REPETITION:
|
|
348
|
+
return "repetition_detected"
|
|
349
|
+
if condition == ExitCondition.CONFIDENCE:
|
|
350
|
+
return "high_confidence"
|
|
351
|
+
if condition == ExitCondition.ENTROPY_DROP:
|
|
352
|
+
return "entropy_below_threshold"
|
|
353
|
+
if condition == ExitCondition.VELOCITY_ZERO:
|
|
354
|
+
return "velocity_stable"
|
|
355
|
+
if condition == ExitCondition.VALLEYS_PLATEAU:
|
|
356
|
+
return "valleys_plateau"
|
|
357
|
+
if condition == ExitCondition.COMBINED:
|
|
358
|
+
if self.is_entropy_low() and self.is_velocity_stable():
|
|
359
|
+
return "entropy_low_velocity_stable"
|
|
360
|
+
if self.is_valleys_plateau() and self.is_velocity_stable():
|
|
361
|
+
return "valleys_plateau_velocity_stable"
|
|
362
|
+
return "combined"
|
|
363
|
+
|
|
364
|
+
return "unknown"
|
|
365
|
+
|
|
366
|
+
def reset(self) -> None:
|
|
367
|
+
"""Clear all tracked data."""
|
|
368
|
+
self._trajectory.clear()
|
|
369
|
+
self._valleys.clear()
|
|
370
|
+
self._index = 0
|
|
371
|
+
self._high_confidence_count = 0
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
# Convenience function for one-shot entropy calculation
|
|
375
|
+
def calculate_entropy_from_logprobs(logprobs: List[float]) -> float:
|
|
376
|
+
"""
|
|
377
|
+
Calculate Shannon entropy from log probabilities.
|
|
378
|
+
|
|
379
|
+
Args:
|
|
380
|
+
logprobs: List of log probabilities (natural log)
|
|
381
|
+
|
|
382
|
+
Returns:
|
|
383
|
+
Shannon entropy in bits
|
|
384
|
+
"""
|
|
385
|
+
entropy = 0.0
|
|
386
|
+
for lp in logprobs:
|
|
387
|
+
prob = math.exp(lp)
|
|
388
|
+
if prob > 0:
|
|
389
|
+
entropy -= prob * math.log2(prob + 1e-10)
|
|
390
|
+
return entropy
|