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,184 @@
1
+ """
2
+ Activation sparsity analyzer — measures the emergent sparsity in dense LLM layers.
3
+
4
+ This is the first "gap exploitation" measurement tool. It hooks into the model's
5
+ forward pass and counts near-zero activations per layer, producing a sparsity
6
+ profile that quantifies how many FLOPs could be skipped on CPU inference.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ from dataclasses import dataclass, field
13
+ from pathlib import Path
14
+
15
+ import numpy as np
16
+
17
+
18
+ @dataclass
19
+ class LayerSparsity:
20
+ layer_index: int
21
+ layer_name: str
22
+ total_activations: int
23
+ near_zero_count: int
24
+ sparsity_ratio: float
25
+ mean_magnitude: float
26
+ median_magnitude: float
27
+ threshold_used: float
28
+
29
+
30
+ @dataclass
31
+ class SparsityReport:
32
+ model_name: str
33
+ prompt: str
34
+ threshold: float
35
+ layers: list[LayerSparsity] = field(default_factory=list)
36
+ overall_sparsity: float = 0.0
37
+ potential_speedup: float = 0.0
38
+
39
+ def summary(self) -> str:
40
+ lines = [
41
+ f"Sparsity Report: {self.model_name}",
42
+ f"Prompt: {self.prompt[:80]}{'...' if len(self.prompt) > 80 else ''}",
43
+ f"Threshold: {self.threshold}",
44
+ f"Overall sparsity: {self.overall_sparsity:.1%}",
45
+ f"Potential CPU speedup: {self.potential_speedup:.1f}x",
46
+ "",
47
+ f"{'Layer':<8} {'Name':<30} {'Sparsity':>10} {'Mean |act|':>12} {'Skip':>8}",
48
+ "-" * 72,
49
+ ]
50
+ for layer in self.layers:
51
+ lines.append(
52
+ f"{layer.layer_index:<8} "
53
+ f"{layer.layer_name:<30} "
54
+ f"{layer.sparsity_ratio:>9.1%} "
55
+ f"{layer.mean_magnitude:>11.6f} "
56
+ f"{layer.near_zero_count:>8}"
57
+ )
58
+ return "\n".join(lines)
59
+
60
+ def to_dict(self) -> dict:
61
+ return {
62
+ "model_name": self.model_name,
63
+ "prompt": self.prompt,
64
+ "threshold": self.threshold,
65
+ "overall_sparsity": self.overall_sparsity,
66
+ "potential_speedup": self.potential_speedup,
67
+ "layers": [
68
+ {
69
+ "layer_index": layer.layer_index,
70
+ "layer_name": layer.layer_name,
71
+ "total_activations": layer.total_activations,
72
+ "near_zero_count": layer.near_zero_count,
73
+ "sparsity_ratio": layer.sparsity_ratio,
74
+ "mean_magnitude": layer.mean_magnitude,
75
+ "median_magnitude": layer.median_magnitude,
76
+ }
77
+ for layer in self.layers
78
+ ],
79
+ }
80
+
81
+ def save(self, path: Path) -> None:
82
+ path.write_text(json.dumps(self.to_dict(), indent=2))
83
+
84
+
85
+ class SparsityAnalyzer:
86
+ """
87
+ Analyzes activation sparsity by intercepting llama.cpp's internal state.
88
+
89
+ Since llama-cpp-python doesn't expose per-layer activations directly,
90
+ this uses the logits output and token embeddings as proxy measurements.
91
+ For full per-layer analysis, a custom ggml hook would be needed (Phase 2).
92
+
93
+ Current implementation: analyzes the output logit distribution to estimate
94
+ how sparse the model's internal representations are for a given input.
95
+ """
96
+
97
+ def __init__(self, threshold: float = 0.01):
98
+ self.threshold = threshold
99
+
100
+ def analyze_logits(
101
+ self,
102
+ logits: list[float] | np.ndarray,
103
+ model_name: str = "unknown",
104
+ prompt: str = "",
105
+ ) -> SparsityReport:
106
+ """Analyze sparsity of the output logit vector."""
107
+ arr = np.array(logits, dtype=np.float32)
108
+ abs_arr = np.abs(arr)
109
+
110
+ near_zero = int(np.sum(abs_arr < self.threshold))
111
+ total = len(arr)
112
+ sparsity = near_zero / total if total > 0 else 0.0
113
+
114
+ layer = LayerSparsity(
115
+ layer_index=0,
116
+ layer_name="output_logits",
117
+ total_activations=total,
118
+ near_zero_count=near_zero,
119
+ sparsity_ratio=sparsity,
120
+ mean_magnitude=float(np.mean(abs_arr)),
121
+ median_magnitude=float(np.median(abs_arr)),
122
+ threshold_used=self.threshold,
123
+ )
124
+
125
+ # potential speedup: 1 / (1 - sparsity), capped at 5x
126
+ speedup = min(1.0 / (1.0 - sparsity) if sparsity < 1.0 else 5.0, 5.0)
127
+
128
+ return SparsityReport(
129
+ model_name=model_name,
130
+ prompt=prompt,
131
+ threshold=self.threshold,
132
+ layers=[layer],
133
+ overall_sparsity=sparsity,
134
+ potential_speedup=speedup,
135
+ )
136
+
137
+ def analyze_token_probabilities(
138
+ self,
139
+ logits: list[float] | np.ndarray,
140
+ model_name: str = "unknown",
141
+ prompt: str = "",
142
+ ) -> SparsityReport:
143
+ """
144
+ Analyze how concentrated the probability mass is across the vocabulary.
145
+
146
+ A highly concentrated distribution (most probability on few tokens) indicates
147
+ the model's internal pathways are sparse for this input — only a small number
148
+ of semantic directions were strongly activated.
149
+ """
150
+ arr = np.array(logits, dtype=np.float32)
151
+
152
+ # softmax
153
+ exp_arr = np.exp(arr - np.max(arr))
154
+ probs = exp_arr / np.sum(exp_arr)
155
+
156
+ # concentration: what fraction of tokens carry 95% of the probability mass
157
+ sorted_probs = np.sort(probs)[::-1]
158
+ cumsum = np.cumsum(sorted_probs)
159
+ tokens_for_95 = int(np.searchsorted(cumsum, 0.95)) + 1
160
+ concentration = 1.0 - (tokens_for_95 / len(probs))
161
+
162
+ near_zero_probs = int(np.sum(probs < 1e-6))
163
+
164
+ layer = LayerSparsity(
165
+ layer_index=0,
166
+ layer_name="probability_distribution",
167
+ total_activations=len(probs),
168
+ near_zero_count=near_zero_probs,
169
+ sparsity_ratio=near_zero_probs / len(probs) if len(probs) > 0 else 0.0,
170
+ mean_magnitude=float(np.mean(probs)),
171
+ median_magnitude=float(np.median(probs)),
172
+ threshold_used=1e-6,
173
+ )
174
+
175
+ report = SparsityReport(
176
+ model_name=model_name,
177
+ prompt=prompt,
178
+ threshold=1e-6,
179
+ layers=[layer],
180
+ overall_sparsity=concentration,
181
+ potential_speedup=min(1.0 / (1.0 - concentration) if concentration < 1.0 else 5.0, 5.0),
182
+ )
183
+
184
+ return report
@@ -0,0 +1,33 @@
1
+ """Phase 1 benchmark/evaluation harness."""
2
+
3
+ from eigencore.benchmark.harness import (
4
+ DEFAULT_CASES,
5
+ SCHEMA_VERSION,
6
+ BenchmarkReport,
7
+ CaseResult,
8
+ QualityCase,
9
+ build_report,
10
+ check_quality,
11
+ hardware_snapshot,
12
+ measure_case,
13
+ model_snapshot,
14
+ run_cases,
15
+ sha256_file,
16
+ summarize,
17
+ )
18
+
19
+ __all__ = [
20
+ "DEFAULT_CASES",
21
+ "SCHEMA_VERSION",
22
+ "BenchmarkReport",
23
+ "CaseResult",
24
+ "QualityCase",
25
+ "build_report",
26
+ "check_quality",
27
+ "hardware_snapshot",
28
+ "measure_case",
29
+ "model_snapshot",
30
+ "run_cases",
31
+ "sha256_file",
32
+ "summarize",
33
+ ]
@@ -0,0 +1,342 @@
1
+ """
2
+ Benchmark and evaluation harness — Phase 1 (trustworthy baseline).
3
+
4
+ Produces reproducible JSON evidence artifacts: hardware snapshot, model file
5
+ identity (SHA-256), engine configuration, per-case prefill/decode timing,
6
+ decode throughput, and simple quality checks. No result from this harness is
7
+ a performance claim until the artifact (with its settings) is shared alongside it.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import hashlib
13
+ import json
14
+ import platform
15
+ import statistics
16
+ import subprocess
17
+ import time
18
+ from dataclasses import asdict, dataclass, field
19
+ from datetime import datetime, timezone
20
+ from pathlib import Path
21
+ from typing import Any
22
+
23
+ from eigencore.engine.inference import GenerationConfig, InferenceEngine
24
+ from eigencore.hal.profiler import HardwareProfile
25
+ from eigencore.models.registry import ModelSpec
26
+
27
+ SCHEMA_VERSION = 1
28
+
29
+
30
+ @dataclass
31
+ class QualityCase:
32
+ """One evaluation case: a prompt plus cheap, checkable expectations."""
33
+
34
+ id: str
35
+ prompt: str
36
+ expected_contains: list[str] = field(default_factory=list)
37
+ min_tokens: int = 1
38
+ max_tokens: int = 256
39
+ note: str = ""
40
+
41
+
42
+ DEFAULT_CASES: list[QualityCase] = [
43
+ QualityCase(
44
+ id="explain-cpu",
45
+ prompt="Explain how a CPU executes instructions, step by step, in one short paragraph.",
46
+ expected_contains=["fetch"],
47
+ max_tokens=128,
48
+ note="basic technical explanation",
49
+ ),
50
+ QualityCase(
51
+ id="quicksort",
52
+ prompt="Write a Python function that implements quicksort. Reply with code only.",
53
+ expected_contains=["def ", "quicksort", "return"],
54
+ max_tokens=192,
55
+ note="code generation sanity check",
56
+ ),
57
+ QualityCase(
58
+ id="tcp-udp",
59
+ prompt="List two key differences between TCP and UDP in one sentence each.",
60
+ expected_contains=["TCP", "UDP"],
61
+ max_tokens=96,
62
+ note="factual recall",
63
+ ),
64
+ QualityCase(
65
+ id="json-format",
66
+ prompt='Reply with exactly this JSON and nothing else: {"ok": true}',
67
+ expected_contains=['{"ok": true}'],
68
+ max_tokens=32,
69
+ note="instruction following / format adherence",
70
+ ),
71
+ ]
72
+
73
+
74
+ @dataclass
75
+ class CaseResult:
76
+ case_id: str
77
+ prompt: str
78
+ output: str
79
+ prompt_tokens_estimated: int
80
+ completion_tokens: int
81
+ time_to_first_token_s: float
82
+ decode_time_s: float
83
+ total_time_s: float
84
+ decode_tokens_per_second: float
85
+ quality_pass: bool
86
+ quality_detail: str
87
+ run_index: int = 0
88
+ warm: bool = False
89
+
90
+
91
+ @dataclass
92
+ class BenchmarkReport:
93
+ schema_version: int
94
+ created_at: str
95
+ git_commit: str | None
96
+ eigencore_version: str
97
+ hardware: dict[str, Any]
98
+ model: dict[str, Any]
99
+ engine: dict[str, Any]
100
+ results: list[dict[str, Any]]
101
+ summary: dict[str, Any]
102
+
103
+ def to_dict(self) -> dict[str, Any]:
104
+ return asdict(self)
105
+
106
+ def save(self, path: str | Path) -> Path:
107
+ path = Path(path)
108
+ path.parent.mkdir(parents=True, exist_ok=True)
109
+ path.write_text(json.dumps(self.to_dict(), indent=2) + "\n")
110
+ return path
111
+
112
+
113
+ def sha256_file(path: str | Path, chunk_size: int = 1 << 20) -> str:
114
+ """SHA-256 of a model file — ties results to exact model content."""
115
+ h = hashlib.sha256()
116
+ with open(path, "rb") as f:
117
+ for chunk in iter(lambda: f.read(chunk_size), b""):
118
+ h.update(chunk)
119
+ return h.hexdigest()
120
+
121
+
122
+ def _git_commit() -> str | None:
123
+ try:
124
+ out = subprocess.run(
125
+ ["git", "rev-parse", "HEAD"],
126
+ capture_output=True,
127
+ text=True,
128
+ timeout=5,
129
+ check=False,
130
+ )
131
+ return out.stdout.strip() or None
132
+ except (OSError, subprocess.SubprocessError):
133
+ return None
134
+
135
+
136
+ def hardware_snapshot(profile: HardwareProfile) -> dict[str, Any]:
137
+ return {
138
+ "cpu_name": profile.cpu_name,
139
+ "arch": profile.arch,
140
+ "platform": platform.platform(),
141
+ "python_version": platform.python_version(),
142
+ "physical_cores": profile.physical_cores,
143
+ "logical_cores": profile.logical_cores,
144
+ "instruction_sets": [
145
+ f.name for f in type(profile.instruction_sets) if f in profile.instruction_sets
146
+ ],
147
+ "total_ram_gb": profile.total_ram_gb,
148
+ "available_ram_gb": profile.available_ram_gb,
149
+ "estimated_bandwidth_gbps": profile.estimated_bandwidth_gbps,
150
+ "notes": list(profile.notes),
151
+ }
152
+
153
+
154
+ def model_snapshot(spec: ModelSpec, model_path: Path | None) -> dict[str, Any]:
155
+ snap: dict[str, Any] = {
156
+ "name": spec.name,
157
+ "repo_id": spec.repo_id,
158
+ "filename": spec.filename,
159
+ "params_b": spec.params_b,
160
+ "quant": spec.quant,
161
+ "size_gb": spec.size_gb,
162
+ "task": spec.task,
163
+ "context_length": spec.context_length,
164
+ }
165
+ if model_path is not None and Path(model_path).exists():
166
+ snap["local_path"] = str(model_path)
167
+ snap["sha256"] = sha256_file(model_path)
168
+ snap["file_size_bytes"] = Path(model_path).stat().st_size
169
+ else:
170
+ snap["local_path"] = None
171
+ snap["sha256"] = None
172
+ snap["warning"] = "model file not found; sha256 not recorded"
173
+ return snap
174
+
175
+
176
+ def engine_snapshot(engine: InferenceEngine, config: GenerationConfig) -> dict[str, Any]:
177
+ p = engine.profile
178
+ return {
179
+ "n_ctx": min(p.recommended_context_length, engine.model_spec.context_length)
180
+ if engine.model_spec
181
+ else p.recommended_context_length,
182
+ "n_threads": p.recommended_threads,
183
+ "n_gpu_layers": 0,
184
+ "backend": "llama-cpp-python",
185
+ "generation": {
186
+ "max_tokens": config.max_tokens,
187
+ "temperature": config.temperature,
188
+ "top_p": config.top_p,
189
+ "top_k": config.top_k,
190
+ "repeat_penalty": config.repeat_penalty,
191
+ "enable_layer_skip": config.enable_layer_skip,
192
+ "enable_sparse_inference": config.enable_sparse_inference,
193
+ },
194
+ }
195
+
196
+
197
+ def check_quality(case: QualityCase, output: str, completion_tokens: int) -> tuple[bool, str]:
198
+ """Cheap deterministic quality gate; not a substitute for human eval."""
199
+ problems: list[str] = []
200
+ lowered = output.lower()
201
+ for needle in case.expected_contains:
202
+ if needle.lower() not in lowered:
203
+ problems.append(f"missing expected text: {needle!r}")
204
+ if completion_tokens < case.min_tokens:
205
+ problems.append(f"too few tokens: {completion_tokens} < {case.min_tokens}")
206
+ return (not problems, "; ".join(problems) if problems else "ok")
207
+
208
+
209
+ def measure_case(
210
+ engine: InferenceEngine,
211
+ case: QualityCase,
212
+ config: GenerationConfig,
213
+ run_index: int = 0,
214
+ ) -> CaseResult:
215
+ """Run one case via streaming so prefill (TTFT) and decode are separated.
216
+
217
+ Completion tokens are counted with the backend tokenizer (or response
218
+ usage), never by counting stream chunks. `run_index` 0 is tagged cold.
219
+ """
220
+ if config.stream:
221
+ raise ValueError("measure_case requires a non-streaming config; it streams internally")
222
+
223
+ stream_config = GenerationConfig(
224
+ max_tokens=case.max_tokens,
225
+ temperature=config.temperature,
226
+ top_p=config.top_p,
227
+ top_k=config.top_k,
228
+ repeat_penalty=config.repeat_penalty,
229
+ stream=True,
230
+ )
231
+
232
+ start = time.perf_counter()
233
+ first_token_at: float | None = None
234
+ chunks: list[str] = []
235
+ for chunk in engine.stream(case.prompt, stream_config):
236
+ if first_token_at is None:
237
+ first_token_at = time.perf_counter()
238
+ chunks.append(chunk)
239
+ total = time.perf_counter() - start
240
+
241
+ ttft = (first_token_at - start) if first_token_at is not None else total
242
+ output = "".join(chunks)
243
+ completion_tokens = _count_tokens(engine, output)
244
+ decode_time = max(total - ttft, 0.0)
245
+ decode_tps = completion_tokens / decode_time if decode_time > 0 else 0.0
246
+ quality_pass, quality_detail = check_quality(case, output, completion_tokens)
247
+
248
+ return CaseResult(
249
+ case_id=case.id,
250
+ prompt=case.prompt,
251
+ output=output,
252
+ prompt_tokens_estimated=max(len(case.prompt) // 4, 1),
253
+ completion_tokens=completion_tokens,
254
+ time_to_first_token_s=ttft,
255
+ decode_time_s=decode_time,
256
+ total_time_s=total,
257
+ decode_tokens_per_second=decode_tps,
258
+ quality_pass=quality_pass,
259
+ quality_detail=quality_detail,
260
+ run_index=run_index,
261
+ warm=run_index > 0,
262
+ )
263
+
264
+
265
+ def _count_tokens(engine: InferenceEngine, text: str) -> int:
266
+ """Token count from the backend tokenizer; falls back to a chars/4 estimate."""
267
+ llm = getattr(engine, "_llm", None)
268
+ if llm is not None and hasattr(llm, "tokenize"):
269
+ try:
270
+ return len(llm.tokenize(text.encode("utf-8")))
271
+ except Exception:
272
+ pass
273
+ return max(len(text) // 4, 1)
274
+
275
+
276
+ def summarize(results: list[CaseResult]) -> dict[str, Any]:
277
+ def _stats(rows: list[CaseResult]) -> dict[str, float]:
278
+ tps = [r.decode_tokens_per_second for r in rows]
279
+ ttft = [r.time_to_first_token_s for r in rows]
280
+ return {
281
+ "decode_tok_s_median": statistics.median(tps) if tps else 0.0,
282
+ "decode_tok_s_mean": statistics.mean(tps) if tps else 0.0,
283
+ "decode_tok_s_min": min(tps) if tps else 0.0,
284
+ "decode_tok_s_max": max(tps) if tps else 0.0,
285
+ "ttft_s_median": statistics.median(ttft) if ttft else 0.0,
286
+ }
287
+
288
+ cold = [r for r in results if not r.warm]
289
+ warm = [r for r in results if r.warm]
290
+ passed = sum(1 for r in results if r.quality_pass)
291
+ return {
292
+ "cases": len(results),
293
+ "quality_pass_rate": passed / len(results) if results else 0.0,
294
+ **{f"all_{k}": v for k, v in _stats(results).items()},
295
+ "cold": _stats(cold),
296
+ "warm": _stats(warm),
297
+ "note": "tokens counted via backend tokenizer; cold = first run per case, warm = later runs",
298
+ }
299
+
300
+
301
+ def build_report(
302
+ engine: InferenceEngine,
303
+ spec: ModelSpec,
304
+ model_path: Path | None,
305
+ config: GenerationConfig,
306
+ results: list[CaseResult],
307
+ ) -> BenchmarkReport:
308
+ from eigencore import __version__
309
+
310
+ return BenchmarkReport(
311
+ schema_version=SCHEMA_VERSION,
312
+ created_at=datetime.now(timezone.utc).isoformat(),
313
+ git_commit=_git_commit(),
314
+ eigencore_version=__version__,
315
+ hardware=hardware_snapshot(engine.profile),
316
+ model=model_snapshot(spec, model_path),
317
+ engine=engine_snapshot(engine, config),
318
+ results=[asdict(r) for r in results],
319
+ summary=summarize(results),
320
+ )
321
+
322
+
323
+ def run_cases(
324
+ engine: InferenceEngine,
325
+ cases: list[QualityCase],
326
+ config: GenerationConfig | None = None,
327
+ runs: int = 1,
328
+ ) -> list[CaseResult]:
329
+ """Run each case `runs` times (first run is cold/warm-up, all are recorded)."""
330
+ if config is None:
331
+ config = GenerationConfig(
332
+ max_tokens=256,
333
+ temperature=0.0,
334
+ stream=False,
335
+ enable_layer_skip=False,
336
+ enable_sparse_inference=False,
337
+ )
338
+ results: list[CaseResult] = []
339
+ for case in cases:
340
+ for run_idx in range(max(runs, 1)):
341
+ results.append(measure_case(engine, case, config, run_index=run_idx))
342
+ return results
File without changes