eigencore 0.4.0__py3-none-any.whl

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.
Files changed (49) hide show
  1. eigencore/__init__.py +82 -0
  2. eigencore/agent/__init__.py +45 -0
  3. eigencore/agent/executor.py +297 -0
  4. eigencore/agent/grammar.py +121 -0
  5. eigencore/agent/policy.py +262 -0
  6. eigencore/agent/react.py +261 -0
  7. eigencore/agent/tool.py +149 -0
  8. eigencore/analysis/__init__.py +3 -0
  9. eigencore/analysis/sparsity.py +184 -0
  10. eigencore/benchmark/__init__.py +33 -0
  11. eigencore/benchmark/harness.py +342 -0
  12. eigencore/cli/__init__.py +0 -0
  13. eigencore/cli/main.py +856 -0
  14. eigencore/context/__init__.py +3 -0
  15. eigencore/context/manager.py +318 -0
  16. eigencore/engine/__init__.py +3 -0
  17. eigencore/engine/inference.py +378 -0
  18. eigencore/engine/layer_skip.py +268 -0
  19. eigencore/engine/prompt_lookup.py +39 -0
  20. eigencore/engine/pruning.py +116 -0
  21. eigencore/engine/sparse_inference.py +218 -0
  22. eigencore/engine/sparsity_experiment.py +109 -0
  23. eigencore/engine/speculative.py +238 -0
  24. eigencore/fit/__init__.py +34 -0
  25. eigencore/fit/estimator.py +310 -0
  26. eigencore/fit/gguf_meta.py +130 -0
  27. eigencore/forge.py +216 -0
  28. eigencore/hal/__init__.py +3 -0
  29. eigencore/hal/_cpuid_win.py +119 -0
  30. eigencore/hal/profiler.py +349 -0
  31. eigencore/kernels/__init__.py +28 -0
  32. eigencore/kernels/q4k.py +76 -0
  33. eigencore/kernels/reference.py +168 -0
  34. eigencore/models/__init__.py +3 -0
  35. eigencore/models/registry.py +193 -0
  36. eigencore/router/__init__.py +3 -0
  37. eigencore/router/classifier.py +176 -0
  38. eigencore/training/__init__.py +3 -0
  39. eigencore/training/convergence.py +112 -0
  40. eigencore/training/trainer.py +401 -0
  41. eigencore/tui/__init__.py +5 -0
  42. eigencore/tui/chat.py +265 -0
  43. eigencore/tui/onboarding.py +134 -0
  44. eigencore/tui/theme.py +61 -0
  45. eigencore-0.4.0.dist-info/METADATA +144 -0
  46. eigencore-0.4.0.dist-info/RECORD +49 -0
  47. eigencore-0.4.0.dist-info/WHEEL +4 -0
  48. eigencore-0.4.0.dist-info/entry_points.txt +2 -0
  49. eigencore-0.4.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,268 @@
1
+ """
2
+ Dynamic layer skipping — decides which transformer layers to skip based on
3
+ per-layer sparsity profiles and input complexity.
4
+
5
+ Transformer models have high layer redundancy: many middle layers produce
6
+ near-identical residual stream updates for routine tokens. Skipping them
7
+ on CPU saves wall-clock time proportional to the skip rate with minimal
8
+ quality loss (typically < 2% perplexity increase at 20-30% skip rate).
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from dataclasses import dataclass
14
+ from enum import Enum, auto
15
+
16
+ import numpy as np
17
+
18
+
19
+ class SkipStrategy(Enum):
20
+ NONE = auto()
21
+ STATIC = auto()
22
+ ADAPTIVE = auto()
23
+ CALIBRATED = auto()
24
+
25
+
26
+ @dataclass
27
+ class LayerProfile:
28
+ index: int
29
+ importance: float
30
+ avg_sparsity: float
31
+ skip_safe: bool
32
+ samples_seen: int = 0
33
+
34
+
35
+ @dataclass
36
+ class SkipPlan:
37
+ total_layers: int
38
+ layers_to_run: list[int]
39
+ layers_to_skip: list[int]
40
+ strategy: SkipStrategy
41
+ expected_speedup: float
42
+ expected_quality_loss: float
43
+
44
+ @property
45
+ def skip_rate(self) -> float:
46
+ if self.total_layers == 0:
47
+ return 0.0
48
+ return len(self.layers_to_skip) / self.total_layers
49
+
50
+ def summary(self) -> str:
51
+ return (
52
+ f"Skip {len(self.layers_to_skip)}/{self.total_layers} layers "
53
+ f"({self.skip_rate:.0%}) | ~{self.expected_speedup:.1f}x speedup | "
54
+ f"~{self.expected_quality_loss:.1%} quality loss"
55
+ )
56
+
57
+
58
+ class LayerSkipScheduler:
59
+ """
60
+ Determines which transformer layers to skip during inference.
61
+
62
+ Three strategies:
63
+ 1. STATIC — skip fixed layers based on position heuristics (no calibration needed)
64
+ 2. ADAPTIVE — adjust skip decisions per-token based on running sparsity stats
65
+ 3. CALIBRATED — use pre-computed per-layer importance scores from a calibration run
66
+
67
+ The key insight: in most transformer models, the first ~15% and last ~15% of
68
+ layers are critical (embedding projection + output head). The middle layers
69
+ show high redundancy, especially for simple/repetitive tokens.
70
+ """
71
+
72
+ CRITICAL_HEAD_RATIO = 0.15
73
+ CRITICAL_TAIL_RATIO = 0.15
74
+
75
+ def __init__(
76
+ self,
77
+ num_layers: int,
78
+ strategy: SkipStrategy = SkipStrategy.STATIC,
79
+ max_skip_rate: float = 0.3,
80
+ quality_threshold: float = 0.02,
81
+ ):
82
+ self.num_layers = num_layers
83
+ self.strategy = strategy
84
+ self.max_skip_rate = max_skip_rate
85
+ self.quality_threshold = quality_threshold
86
+ self._layer_profiles: list[LayerProfile] = []
87
+ self._calibrated = False
88
+
89
+ def plan(self, complexity: float = 0.5) -> SkipPlan:
90
+ """
91
+ Generate a skip plan for the current inference pass.
92
+
93
+ complexity: 0.0 (trivial repetition) to 1.0 (novel/complex content).
94
+ Lower complexity → more aggressive skipping.
95
+ """
96
+ if self.num_layers <= 4:
97
+ return self._no_skip_plan()
98
+
99
+ if self.strategy == SkipStrategy.NONE:
100
+ return self._no_skip_plan()
101
+ elif self.strategy == SkipStrategy.STATIC:
102
+ return self._static_plan(complexity)
103
+ elif self.strategy == SkipStrategy.ADAPTIVE:
104
+ return self._adaptive_plan(complexity)
105
+ elif self.strategy == SkipStrategy.CALIBRATED:
106
+ if not self._calibrated:
107
+ return self._static_plan(complexity)
108
+ return self._calibrated_plan(complexity)
109
+
110
+ return self._no_skip_plan()
111
+
112
+ def calibrate(self, layer_importances: list[float]) -> None:
113
+ """
114
+ Load per-layer importance scores from a calibration run.
115
+ Importance is typically measured as the mean L2 norm of each layer's
116
+ residual stream contribution over a calibration dataset.
117
+ """
118
+ if len(layer_importances) != self.num_layers:
119
+ raise ValueError(
120
+ f"Expected {self.num_layers} importance scores, got {len(layer_importances)}"
121
+ )
122
+
123
+ self._layer_profiles = [
124
+ LayerProfile(
125
+ index=i,
126
+ importance=imp,
127
+ avg_sparsity=0.0,
128
+ skip_safe=(imp < np.median(layer_importances)),
129
+ )
130
+ for i, imp in enumerate(layer_importances)
131
+ ]
132
+ self._calibrated = True
133
+
134
+ def update_sparsity(self, layer_index: int, sparsity: float) -> None:
135
+ """Update running sparsity stats for adaptive strategy."""
136
+ if layer_index >= len(self._layer_profiles):
137
+ while len(self._layer_profiles) <= layer_index:
138
+ self._layer_profiles.append(
139
+ LayerProfile(
140
+ index=len(self._layer_profiles),
141
+ importance=1.0,
142
+ avg_sparsity=0.0,
143
+ skip_safe=False,
144
+ )
145
+ )
146
+
147
+ profile = self._layer_profiles[layer_index]
148
+ n = profile.samples_seen
149
+ profile.avg_sparsity = (profile.avg_sparsity * n + sparsity) / (n + 1)
150
+ profile.samples_seen = n + 1
151
+
152
+ if profile.avg_sparsity > 0.7 and profile.samples_seen >= 10:
153
+ profile.skip_safe = True
154
+
155
+ def _no_skip_plan(self) -> SkipPlan:
156
+ return SkipPlan(
157
+ total_layers=self.num_layers,
158
+ layers_to_run=list(range(self.num_layers)),
159
+ layers_to_skip=[],
160
+ strategy=SkipStrategy.NONE,
161
+ expected_speedup=1.0,
162
+ expected_quality_loss=0.0,
163
+ )
164
+
165
+ def _static_plan(self, complexity: float) -> SkipPlan:
166
+ """Skip middle layers at fixed intervals based on complexity."""
167
+ head = max(1, int(self.num_layers * self.CRITICAL_HEAD_RATIO))
168
+ tail = max(1, int(self.num_layers * self.CRITICAL_TAIL_RATIO))
169
+ tail_start = self.num_layers - tail
170
+
171
+ middle_layers = list(range(head, tail_start))
172
+ if not middle_layers:
173
+ return self._no_skip_plan()
174
+
175
+ adjusted_skip = self.max_skip_rate * (1.0 - complexity)
176
+ max_skippable = max(1, int(len(middle_layers) * adjusted_skip))
177
+
178
+ step = max(1, len(middle_layers) // max_skippable) if max_skippable > 0 else 1
179
+ skip_set = set(middle_layers[::step][:max_skippable])
180
+
181
+ layers_to_run = [i for i in range(self.num_layers) if i not in skip_set]
182
+ layers_to_skip = sorted(skip_set)
183
+
184
+ skip_rate = len(layers_to_skip) / self.num_layers
185
+ speedup = 1.0 / (1.0 - skip_rate) if skip_rate < 1.0 else 1.0
186
+ quality_loss = skip_rate * 0.06
187
+
188
+ return SkipPlan(
189
+ total_layers=self.num_layers,
190
+ layers_to_run=layers_to_run,
191
+ layers_to_skip=layers_to_skip,
192
+ strategy=SkipStrategy.STATIC,
193
+ expected_speedup=speedup,
194
+ expected_quality_loss=quality_loss,
195
+ )
196
+
197
+ def _adaptive_plan(self, complexity: float) -> SkipPlan:
198
+ """Skip layers with high observed sparsity."""
199
+ if not self._layer_profiles:
200
+ return self._static_plan(complexity)
201
+
202
+ head = max(1, int(self.num_layers * self.CRITICAL_HEAD_RATIO))
203
+ tail = max(1, int(self.num_layers * self.CRITICAL_TAIL_RATIO))
204
+ tail_start = self.num_layers - tail
205
+
206
+ skip_set = set()
207
+ max_total_skip = int(self.num_layers * self.max_skip_rate * (1.0 - complexity))
208
+
209
+ candidates = [
210
+ p for p in self._layer_profiles if p.skip_safe and head <= p.index < tail_start
211
+ ]
212
+ candidates.sort(key=lambda p: p.avg_sparsity, reverse=True)
213
+
214
+ for profile in candidates[:max_total_skip]:
215
+ skip_set.add(profile.index)
216
+
217
+ layers_to_run = [i for i in range(self.num_layers) if i not in skip_set]
218
+ layers_to_skip = sorted(skip_set)
219
+
220
+ skip_rate = len(layers_to_skip) / self.num_layers
221
+ speedup = 1.0 / (1.0 - skip_rate) if skip_rate < 1.0 else 1.0
222
+ quality_loss = skip_rate * 0.04
223
+
224
+ return SkipPlan(
225
+ total_layers=self.num_layers,
226
+ layers_to_run=layers_to_run,
227
+ layers_to_skip=layers_to_skip,
228
+ strategy=SkipStrategy.ADAPTIVE,
229
+ expected_speedup=speedup,
230
+ expected_quality_loss=quality_loss,
231
+ )
232
+
233
+ def _calibrated_plan(self, complexity: float) -> SkipPlan:
234
+ """Skip layers ranked by calibrated importance scores."""
235
+ head = max(1, int(self.num_layers * self.CRITICAL_HEAD_RATIO))
236
+ tail = max(1, int(self.num_layers * self.CRITICAL_TAIL_RATIO))
237
+ tail_start = self.num_layers - tail
238
+
239
+ middle_profiles = [p for p in self._layer_profiles if head <= p.index < tail_start]
240
+ middle_profiles.sort(key=lambda p: p.importance)
241
+
242
+ max_total_skip = int(self.num_layers * self.max_skip_rate * (1.0 - complexity))
243
+ skip_set = set()
244
+ cumulative_loss = 0.0
245
+
246
+ for profile in middle_profiles:
247
+ estimated_loss = (1.0 - profile.importance) * 0.01
248
+ if cumulative_loss + estimated_loss > self.quality_threshold:
249
+ break
250
+ if len(skip_set) >= max_total_skip:
251
+ break
252
+ skip_set.add(profile.index)
253
+ cumulative_loss += estimated_loss
254
+
255
+ layers_to_run = [i for i in range(self.num_layers) if i not in skip_set]
256
+ layers_to_skip = sorted(skip_set)
257
+
258
+ skip_rate = len(layers_to_skip) / self.num_layers
259
+ speedup = 1.0 / (1.0 - skip_rate) if skip_rate < 1.0 else 1.0
260
+
261
+ return SkipPlan(
262
+ total_layers=self.num_layers,
263
+ layers_to_run=layers_to_run,
264
+ layers_to_skip=layers_to_skip,
265
+ strategy=SkipStrategy.CALIBRATED,
266
+ expected_speedup=speedup,
267
+ expected_quality_loss=cumulative_loss,
268
+ )
@@ -0,0 +1,39 @@
1
+ """
2
+ Prompt-lookup decoding (PLD) - Stage C, behind a flag, default OFF.
3
+
4
+ PLD (prompt lookup / SpecInfer-style): when the model is about to emit a span
5
+ that already appears verbatim in the prompt (common in code, quotes,
6
+ boilerplate), copy it directly from the prompt instead of running the model -
7
+ zero backend work for those tokens.
8
+
9
+ This module is a pure, backend-independent matcher: given the prompt text and
10
+ the tokens generated so far, find the longest prompt substring continuing the
11
+ generation. The engine uses it only when prompt_lookup=True.
12
+
13
+ Correctness rule: a copied span must be byte-identical to the prompt span -
14
+ no fuzzy matching, no paraphrase - so output quality is unaffected by
15
+ construction. Speedup is measured, never assumed.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+
21
+ def longest_prompt_continuation(prompt: str, generated: str, max_span: int = 512) -> str:
22
+ """Longest prompt text that continues the current generation.
23
+
24
+ Finds the longest suffix of `generated` that occurs in `prompt` with at
25
+ least one more prompt character after it, and returns the prompt text
26
+ from that point (up to max_span chars). Empty string when nothing matches.
27
+ """
28
+ if not prompt or not generated:
29
+ return ""
30
+ gen = generated[-max_span:]
31
+ for size in range(min(len(gen), max_span), 0, -1):
32
+ suffix = gen[-size:]
33
+ idx = prompt.find(suffix)
34
+ while idx != -1:
35
+ end = idx + size
36
+ if end < len(prompt):
37
+ return prompt[end : end + max_span]
38
+ idx = prompt.find(suffix, idx + 1)
39
+ return ""
@@ -0,0 +1,116 @@
1
+ """
2
+ Layer-pruning experiment — Stage E slice 2 (ShortGPT-style), REPORTED-ONLY.
3
+
4
+ ShortGPT (Men et al., 2024) scores each transformer layer by Block Influence:
5
+ the 1 - cosine similarity between the layer's input and output hidden states,
6
+ averaged over calibration samples. Low-BI layers are candidates for removal.
7
+
8
+ This module computes BI scores, builds a prune plan, and applies the KILL
9
+ CRITERION from the experiment plan: if the measured quality delta (e.g.
10
+ perplexity increase) exceeds `max_quality_delta`, the experiment is aborted
11
+ and NO layers are pruned. Nothing here is wired into inference; the backend
12
+ always runs all layers today.
13
+
14
+ No performance or quality claim is made without the owner's calibration +
15
+ perplexity runs (FINAL_REPORT §9 item 10).
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ from dataclasses import dataclass, field
21
+
22
+ import numpy as np
23
+
24
+
25
+ def block_influence(layer_in: np.ndarray, layer_out: np.ndarray, eps: float = 1e-8) -> float:
26
+ """BI for one layer on one sample: 1 - cos(x_in, x_out).
27
+
28
+ `layer_in`/`layer_out`: (hidden,) vectors. Returns 0 when either is ~zero.
29
+ """
30
+ x = np.asarray(layer_in, dtype=np.float64).reshape(-1)
31
+ y = np.asarray(layer_out, dtype=np.float64).reshape(-1)
32
+ if x.shape != y.shape or x.size == 0:
33
+ raise ValueError("layer_in/layer_out must be same-shape non-empty vectors")
34
+ nx = float(np.linalg.norm(x))
35
+ ny = float(np.linalg.norm(y))
36
+ if nx < eps or ny < eps:
37
+ return 0.0
38
+ cos = float(np.dot(x, y) / (nx * ny))
39
+ return 1.0 - max(-1.0, min(1.0, cos))
40
+
41
+
42
+ def layer_bi_scores(
43
+ hidden_states: np.ndarray,
44
+ ) -> np.ndarray:
45
+ """Mean BI per layer over samples.
46
+
47
+ `hidden_states`: (num_layers + 1, num_samples, hidden) — activations at each
48
+ layer boundary (0 = model input, L = model output). Layer i's BI uses
49
+ boundary i (input) and i+1 (output). Returns (num_layers,) float64.
50
+ """
51
+ hs = np.asarray(hidden_states, dtype=np.float64)
52
+ if hs.ndim != 3 or hs.shape[0] < 2:
53
+ raise ValueError("expected (layers+1, samples, hidden)")
54
+ num_layers = hs.shape[0] - 1
55
+ scores = np.zeros(num_layers)
56
+ for i in range(num_layers):
57
+ x = hs[i] # (samples, hidden)
58
+ y = hs[i + 1]
59
+ nx = np.linalg.norm(x, axis=1)
60
+ ny = np.linalg.norm(y, axis=1)
61
+ denom = nx * ny
62
+ cos = np.where(denom > 1e-16, (x * y).sum(axis=1) / np.maximum(denom, 1e-16), 0.0)
63
+ cos = np.clip(cos, -1.0, 1.0)
64
+ scores[i] = float(np.mean(1.0 - cos))
65
+ return scores
66
+
67
+
68
+ @dataclass
69
+ class PrunePlan:
70
+ layers_to_prune: list[int]
71
+ bi_scores: np.ndarray
72
+ max_quality_delta_allowed: float
73
+ killed: bool = False
74
+ kill_reason: str = ""
75
+ notes: list[str] = field(default_factory=list)
76
+
77
+
78
+ def build_prune_plan(
79
+ bi_scores: np.ndarray,
80
+ num_layers: int,
81
+ max_prune_fraction: float = 0.3,
82
+ protect_head: float = 0.15,
83
+ protect_tail: float = 0.15,
84
+ max_quality_delta_allowed: float = 0.02,
85
+ measured_quality_delta: float | None = None,
86
+ ) -> PrunePlan:
87
+ """Rank layers by BI ascending and pick the lowest-BI middle layers.
88
+
89
+ First/last `protect_*` fraction of layers are never pruned (embedding/head
90
+ criticality). Kill criterion: if `measured_quality_delta` is provided and
91
+ exceeds `max_quality_delta_allowed`, the plan is killed (no pruning).
92
+ """
93
+ scores = np.asarray(bi_scores, dtype=np.float64)
94
+ if scores.shape[0] != num_layers:
95
+ raise ValueError("bi_scores length must equal num_layers")
96
+ if measured_quality_delta is not None and measured_quality_delta > max_quality_delta_allowed:
97
+ return PrunePlan(
98
+ layers_to_prune=[],
99
+ bi_scores=scores,
100
+ max_quality_delta_allowed=max_quality_delta_allowed,
101
+ killed=True,
102
+ kill_reason=(
103
+ f"quality delta {measured_quality_delta:.4f} > allowed "
104
+ f"{max_quality_delta_allowed:.4f} — kill criterion hit, no pruning"
105
+ ),
106
+ )
107
+ n_protect_head = int(num_layers * protect_head)
108
+ n_protect_tail = int(num_layers * protect_tail)
109
+ candidates = list(range(n_protect_head, num_layers - n_protect_tail))
110
+ max_prune = int(num_layers * max_prune_fraction)
111
+ candidates.sort(key=lambda i: scores[i])
112
+ return PrunePlan(
113
+ layers_to_prune=sorted(candidates[:max_prune]),
114
+ bi_scores=scores,
115
+ max_quality_delta_allowed=max_quality_delta_allowed,
116
+ )
@@ -0,0 +1,218 @@
1
+ """
2
+ Sparsity-aware inference — exploits the 70-90% activation sparsity in dense
3
+ transformer models to skip near-zero neuron computations on CPU.
4
+
5
+ GPU warp divergence makes sparse execution inefficient on GPUs (all 32 threads
6
+ in a warp must execute the same instruction). CPUs have fine-grained branch
7
+ prediction — skipping a zero multiply is a correctly-predicted branch that
8
+ costs nearly nothing. This is the core CPU advantage EigenCore exploits.
9
+
10
+ This module provides:
11
+ 1. SparsityPredictor: learns per-layer sparsity patterns from observation
12
+ 2. SparseExecutionPlan: decides which neurons to skip for a given input
13
+ 3. SparsityCache: caches sparsity masks across similar inputs for reuse
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import hashlib
19
+ from collections import OrderedDict
20
+ from dataclasses import dataclass
21
+
22
+ import numpy as np
23
+
24
+
25
+ @dataclass
26
+ class NeuronMask:
27
+ layer_index: int
28
+ total_neurons: int
29
+ active_indices: np.ndarray
30
+ sparsity: float
31
+
32
+ @property
33
+ def active_count(self) -> int:
34
+ return len(self.active_indices)
35
+
36
+ @property
37
+ def skip_count(self) -> int:
38
+ return self.total_neurons - self.active_count
39
+
40
+
41
+ @dataclass
42
+ class SparseExecutionPlan:
43
+ masks: list[NeuronMask]
44
+ total_neurons: int
45
+ total_active: int
46
+ overall_sparsity: float
47
+ estimated_flop_savings: float
48
+ estimated_speedup: float
49
+
50
+ def summary(self) -> str:
51
+ return (
52
+ f"Sparse plan: {self.overall_sparsity:.0%} sparsity across "
53
+ f"{len(self.masks)} layers | {self.estimated_speedup:.1f}x speedup | "
54
+ f"{self.estimated_flop_savings:.0%} FLOPs saved"
55
+ )
56
+
57
+
58
+ class SparsityPredictor:
59
+ """
60
+ Learns and predicts per-layer activation sparsity patterns.
61
+
62
+ Tracks running statistics of which neurons activate (|value| > threshold)
63
+ across multiple inputs. After enough observations, it can predict which
64
+ neurons are likely inactive for a new input, enabling preemptive skipping.
65
+ """
66
+
67
+ def __init__(
68
+ self,
69
+ num_layers: int,
70
+ neurons_per_layer: int,
71
+ threshold: float = 0.01,
72
+ warmup_samples: int = 16,
73
+ ):
74
+ self.num_layers = num_layers
75
+ self.neurons_per_layer = neurons_per_layer
76
+ self.threshold = threshold
77
+ self.warmup_samples = warmup_samples
78
+ self._activation_counts = np.zeros((num_layers, neurons_per_layer), dtype=np.int32)
79
+ self._total_samples = 0
80
+ self._layer_sparsity_history: list[list[float]] = [[] for _ in range(num_layers)]
81
+
82
+ @property
83
+ def is_warmed_up(self) -> bool:
84
+ return self._total_samples >= self.warmup_samples
85
+
86
+ def observe(self, layer_index: int, activations: np.ndarray) -> float:
87
+ """
88
+ Record which neurons activated for this input.
89
+ Returns the observed sparsity for this layer.
90
+ """
91
+ if layer_index >= self.num_layers:
92
+ raise ValueError(f"Layer {layer_index} >= num_layers {self.num_layers}")
93
+
94
+ abs_acts = np.abs(activations)
95
+ active_mask = abs_acts >= self.threshold
96
+ active_count = int(np.sum(active_mask))
97
+ sparsity = 1.0 - (active_count / len(activations)) if len(activations) > 0 else 0.0
98
+
99
+ n = min(len(activations), self.neurons_per_layer)
100
+ self._activation_counts[layer_index, :n] += active_mask[:n].astype(np.int32)
101
+ self._total_samples += 1
102
+ self._layer_sparsity_history[layer_index].append(sparsity)
103
+
104
+ return sparsity
105
+
106
+ def predict_mask(self, layer_index: int, aggressiveness: float = 0.5) -> NeuronMask:
107
+ """
108
+ Predict which neurons will be active for the next input.
109
+
110
+ aggressiveness: 0.0 (conservative, skip only always-zero neurons)
111
+ to 1.0 (aggressive, skip anything below median activation rate)
112
+ """
113
+ if not self.is_warmed_up:
114
+ return NeuronMask(
115
+ layer_index=layer_index,
116
+ total_neurons=self.neurons_per_layer,
117
+ active_indices=np.arange(self.neurons_per_layer),
118
+ sparsity=0.0,
119
+ )
120
+
121
+ activation_rates = self._activation_counts[layer_index] / self._total_samples
122
+ cutoff = aggressiveness * np.median(activation_rates)
123
+ active_indices = np.where(activation_rates > cutoff)[0]
124
+
125
+ sparsity = 1.0 - (len(active_indices) / self.neurons_per_layer)
126
+
127
+ return NeuronMask(
128
+ layer_index=layer_index,
129
+ total_neurons=self.neurons_per_layer,
130
+ active_indices=active_indices,
131
+ sparsity=sparsity,
132
+ )
133
+
134
+ def create_execution_plan(self, aggressiveness: float = 0.5) -> SparseExecutionPlan:
135
+ """Generate a full execution plan across all layers."""
136
+ masks = []
137
+ total_neurons = 0
138
+ total_active = 0
139
+
140
+ for layer_idx in range(self.num_layers):
141
+ mask = self.predict_mask(layer_idx, aggressiveness)
142
+ masks.append(mask)
143
+ total_neurons += mask.total_neurons
144
+ total_active += mask.active_count
145
+
146
+ overall_sparsity = 1.0 - (total_active / total_neurons) if total_neurons > 0 else 0.0
147
+ flop_savings = overall_sparsity
148
+ speedup = 1.0 / (1.0 - overall_sparsity) if overall_sparsity < 1.0 else 1.0
149
+ speedup = min(speedup, 5.0)
150
+
151
+ return SparseExecutionPlan(
152
+ masks=masks,
153
+ total_neurons=total_neurons,
154
+ total_active=total_active,
155
+ overall_sparsity=overall_sparsity,
156
+ estimated_flop_savings=flop_savings,
157
+ estimated_speedup=speedup,
158
+ )
159
+
160
+ def layer_stats(self) -> list[dict]:
161
+ """Per-layer statistics summary."""
162
+ stats = []
163
+ for i in range(self.num_layers):
164
+ history = self._layer_sparsity_history[i]
165
+ stats.append(
166
+ {
167
+ "layer": i,
168
+ "avg_sparsity": float(np.mean(history)) if history else 0.0,
169
+ "std_sparsity": float(np.std(history)) if history else 0.0,
170
+ "samples": len(history),
171
+ }
172
+ )
173
+ return stats
174
+
175
+
176
+ class SparsityCache:
177
+ """
178
+ Caches sparsity masks for similar inputs to avoid recomputation.
179
+
180
+ Uses a hash of the input token sequence to look up previously computed
181
+ masks. LRU eviction keeps memory bounded.
182
+ """
183
+
184
+ def __init__(self, max_entries: int = 256):
185
+ self.max_entries = max_entries
186
+ self._cache: OrderedDict[str, SparseExecutionPlan] = OrderedDict()
187
+ self.hits = 0
188
+ self.misses = 0
189
+
190
+ def _hash_input(self, token_ids: list[int]) -> str:
191
+ key = ",".join(str(t) for t in token_ids[-32:])
192
+ return hashlib.md5(key.encode()).hexdigest()
193
+
194
+ def get(self, token_ids: list[int]) -> SparseExecutionPlan | None:
195
+ key = self._hash_input(token_ids)
196
+ if key in self._cache:
197
+ self.hits += 1
198
+ self._cache.move_to_end(key)
199
+ return self._cache[key]
200
+ self.misses += 1
201
+ return None
202
+
203
+ def put(self, token_ids: list[int], plan: SparseExecutionPlan) -> None:
204
+ key = self._hash_input(token_ids)
205
+ self._cache[key] = plan
206
+ self._cache.move_to_end(key)
207
+ while len(self._cache) > self.max_entries:
208
+ self._cache.popitem(last=False)
209
+
210
+ @property
211
+ def hit_rate(self) -> float:
212
+ total = self.hits + self.misses
213
+ return self.hits / total if total > 0 else 0.0
214
+
215
+ def clear(self) -> None:
216
+ self._cache.clear()
217
+ self.hits = 0
218
+ self.misses = 0