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,349 @@
1
+ """
2
+ Hardware Abstraction Layer — auto-detects CPU capabilities, RAM, and memory bandwidth
3
+ to determine optimal model configuration without manual user input.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import platform
9
+ import time
10
+ from dataclasses import dataclass, field
11
+ from enum import Flag, auto
12
+
13
+ import psutil
14
+
15
+
16
+ class InstructionSet(Flag):
17
+ NONE = 0
18
+ SSE2 = auto()
19
+ SSE4_1 = auto()
20
+ SSE4_2 = auto()
21
+ AVX = auto()
22
+ AVX2 = auto()
23
+ AVX512F = auto()
24
+ AVX512BW = auto()
25
+ AVX512VNNI = auto()
26
+ FMA = auto()
27
+ F16C = auto()
28
+ AMX_TILE = auto()
29
+ AMX_INT8 = auto()
30
+ AMX_BF16 = auto()
31
+ NEON = auto()
32
+ SVE = auto()
33
+
34
+
35
+ @dataclass(frozen=True)
36
+ class HardwareProfile:
37
+ cpu_name: str
38
+ arch: str
39
+ physical_cores: int
40
+ logical_cores: int
41
+ instruction_sets: InstructionSet
42
+ total_ram_gb: float
43
+ available_ram_gb: float
44
+ estimated_bandwidth_gbps: float
45
+ max_model_params_b: float
46
+ optimal_quantization: str
47
+ estimated_tokens_per_sec: float
48
+ recommended_threads: int
49
+ recommended_context_length: int
50
+ gpu_name: str | None = None
51
+ total_vram_gb: float = 0.0 # 0 = no GPU / unknown
52
+ available_vram_gb: float = 0.0
53
+ notes: list[str] = field(default_factory=list)
54
+
55
+ def summary(self) -> str:
56
+ isa_names = [
57
+ f.name for f in InstructionSet if f in self.instruction_sets and f.name != "NONE"
58
+ ]
59
+ lines = [
60
+ f"CPU: {self.cpu_name} ({self.arch})",
61
+ f"Cores: {self.physical_cores}P / {self.logical_cores}L",
62
+ f"ISA: {', '.join(isa_names) if isa_names else 'baseline'}",
63
+ f"RAM: {self.available_ram_gb:.1f} / {self.total_ram_gb:.1f} GB available",
64
+ f"Bandwidth: ~{self.estimated_bandwidth_gbps:.1f} GB/s",
65
+ (
66
+ f"GPU: {self.gpu_name} | VRAM: {self.available_vram_gb:.1f} / {self.total_vram_gb:.1f} GB available"
67
+ if self.gpu_name
68
+ else "GPU: none detected (CPU-only mode)"
69
+ ),
70
+ "",
71
+ f"Max model: ~{self.max_model_params_b:.1f}B params at {self.optimal_quantization}",
72
+ f"Est. speed: ~{self.estimated_tokens_per_sec:.0f} tok/s",
73
+ f"Threads: {self.recommended_threads}",
74
+ f"Context: {self.recommended_context_length} tokens",
75
+ ]
76
+ if self.notes:
77
+ lines.append("")
78
+ for note in self.notes:
79
+ lines.append(f" * {note}")
80
+ return "\n".join(lines)
81
+
82
+
83
+ def _detect_instruction_sets_x86() -> InstructionSet:
84
+ """Detect x86 SIMD capabilities via CPUID."""
85
+ isa = InstructionSet.NONE
86
+
87
+ if platform.system() == "Windows":
88
+ try:
89
+ from eigencore.hal._cpuid_win import detect_isa_windows
90
+
91
+ return detect_isa_windows()
92
+ except ImportError:
93
+ pass
94
+
95
+ try:
96
+ with open("/proc/cpuinfo", "r") as f:
97
+ cpuinfo = f.read().lower()
98
+
99
+ flag_map = {
100
+ "sse2": InstructionSet.SSE2,
101
+ "sse4_1": InstructionSet.SSE4_1,
102
+ "sse4_2": InstructionSet.SSE4_2,
103
+ "avx": InstructionSet.AVX,
104
+ "avx2": InstructionSet.AVX2,
105
+ "avx512f": InstructionSet.AVX512F,
106
+ "avx512bw": InstructionSet.AVX512BW,
107
+ "avx512_vnni": InstructionSet.AVX512VNNI,
108
+ "fma": InstructionSet.FMA,
109
+ "f16c": InstructionSet.F16C,
110
+ "amx_tile": InstructionSet.AMX_TILE,
111
+ "amx_int8": InstructionSet.AMX_INT8,
112
+ "amx_bf16": InstructionSet.AMX_BF16,
113
+ }
114
+ for flag, isa_val in flag_map.items():
115
+ if flag in cpuinfo:
116
+ isa |= isa_val
117
+ except FileNotFoundError:
118
+ pass
119
+
120
+ return isa
121
+
122
+
123
+ def _detect_instruction_sets_arm() -> InstructionSet:
124
+ """Detect ARM SIMD capabilities."""
125
+ isa = InstructionSet.NONE
126
+
127
+ if platform.machine().lower() in ("aarch64", "arm64"):
128
+ isa |= InstructionSet.NEON
129
+
130
+ try:
131
+ with open("/proc/cpuinfo", "r") as f:
132
+ cpuinfo = f.read().lower()
133
+ if "sve" in cpuinfo:
134
+ isa |= InstructionSet.SVE
135
+ except FileNotFoundError:
136
+ pass
137
+
138
+ return isa
139
+
140
+
141
+ def _measure_bandwidth() -> float:
142
+ """Estimate memory bandwidth via timed memcpy benchmark (GB/s)."""
143
+ try:
144
+ block_size = 64 * 1024 * 1024 # 64 MB
145
+ src = bytearray(block_size)
146
+ iterations = 5
147
+
148
+ # warmup
149
+ _ = bytes(src)
150
+
151
+ times = []
152
+ for _ in range(iterations):
153
+ start = time.perf_counter()
154
+ _ = bytearray(src)
155
+ elapsed = time.perf_counter() - start
156
+ times.append(elapsed)
157
+
158
+ median_time = sorted(times)[len(times) // 2]
159
+ bandwidth_gbps = (block_size / (1024**3)) / median_time
160
+ # memcpy measures roughly half of peak bandwidth (read + write)
161
+ return bandwidth_gbps * 2
162
+ except Exception:
163
+ return 20.0 # conservative fallback
164
+
165
+
166
+ def _get_cpu_name() -> str:
167
+ """Get human-readable CPU name."""
168
+ if platform.system() == "Windows":
169
+ try:
170
+ import winreg
171
+
172
+ key = winreg.OpenKey(
173
+ winreg.HKEY_LOCAL_MACHINE,
174
+ r"HARDWARE\DESCRIPTION\System\CentralProcessor\0",
175
+ )
176
+ name, _ = winreg.QueryValueEx(key, "ProcessorNameString")
177
+ winreg.CloseKey(key)
178
+ return name.strip()
179
+ except Exception:
180
+ pass
181
+
182
+ try:
183
+ with open("/proc/cpuinfo", "r") as f:
184
+ for line in f:
185
+ if "model name" in line.lower():
186
+ return line.split(":")[1].strip()
187
+ except FileNotFoundError:
188
+ pass
189
+
190
+ return platform.processor() or "Unknown CPU"
191
+
192
+
193
+ def _compute_model_limits(
194
+ available_ram_gb: float,
195
+ bandwidth_gbps: float,
196
+ ) -> tuple[float, str, float, int, int]:
197
+ """
198
+ Given available RAM and bandwidth, compute:
199
+ - max model size in billions of parameters
200
+ - optimal quantization level
201
+ - estimated tokens per second
202
+ - recommended thread count
203
+ - recommended context length
204
+
205
+ All sizes in GiB (1 GiB = 1024**3 bytes) end to end.
206
+ We reserve 2 GiB for OS/runtime overhead plus a KV-cache share, then fit
207
+ the largest model possible.
208
+ """
209
+ usable_ram_gib = max(available_ram_gb - 2.0, 1.0)
210
+
211
+ quant_options = [
212
+ ("Q4_K_M", 0.55), # ~0.55 bytes per param (4-bit with some FP16 layers)
213
+ ("Q5_K_M", 0.68), # ~0.68 bytes per param
214
+ ("Q8_0", 1.05), # ~1.05 bytes per param
215
+ ("F16", 2.0), # 2 bytes per param
216
+ ]
217
+
218
+ best_params_b = 0.0
219
+ best_quant = "Q4_K_M"
220
+
221
+ for quant_name, bytes_per_param in quant_options:
222
+ max_params = (usable_ram_gib * 1024**3) / (bytes_per_param * 1e9)
223
+ if max_params > best_params_b:
224
+ best_params_b = max_params
225
+ best_quant = quant_name
226
+
227
+ # cap at realistic model sizes available in GGUF
228
+ best_params_b = min(best_params_b, 70.0)
229
+
230
+ # estimate tokens/sec: bandwidth / model_size_in_memory (ceiling estimate)
231
+ model_size_gib = best_params_b * 0.55 # assuming best quant (Q4)
232
+ if model_size_gib > 0:
233
+ est_tps = bandwidth_gbps / model_size_gib
234
+ est_tps = min(est_tps, 100.0) # cap at reasonable maximum
235
+ else:
236
+ est_tps = 0.0
237
+
238
+ # recommended threads: physical cores (hyperthreads don't help much for inference)
239
+ recommended_threads = psutil.cpu_count(logical=False) or 4
240
+
241
+ # context length: RAM left after model, reserving 25% of usable RAM for
242
+ # the KV cache so the recommendation does not collapse to the 2048 floor
243
+ # on machines under ~38 GiB.
244
+ kv_budget_gib = 0.25 * usable_ram_gib
245
+ remaining_gib = usable_ram_gib - model_size_gib - kv_budget_gib
246
+ if best_params_b > 0 and remaining_gib > 0:
247
+ # pessimistic geometry-free KV rate: ~2 MB per 1K tokens per 1B params (fp16)
248
+ kv_per_1k = 0.002 * best_params_b # GiB per 1K tokens
249
+ max_ctx = int((remaining_gib / kv_per_1k) * 1000) if kv_per_1k > 0 else 2048
250
+ recommended_ctx = min(max_ctx, 8192) # cap at 8K for CPU
251
+ recommended_ctx = max(recommended_ctx, 512) # minimum usable
252
+ else:
253
+ recommended_ctx = 2048
254
+
255
+ return best_params_b, best_quant, est_tps, recommended_threads, recommended_ctx
256
+
257
+
258
+ def profile_hardware() -> HardwareProfile:
259
+ """Run full hardware profiling and return a HardwareProfile with recommendations."""
260
+ arch = platform.machine().lower()
261
+ is_arm = arch in ("aarch64", "arm64")
262
+ is_x86 = arch in ("x86_64", "amd64", "x86")
263
+
264
+ cpu_name = _get_cpu_name()
265
+
266
+ if is_x86:
267
+ isa = _detect_instruction_sets_x86()
268
+ elif is_arm:
269
+ isa = _detect_instruction_sets_arm()
270
+ else:
271
+ isa = InstructionSet.NONE
272
+
273
+ mem = psutil.virtual_memory()
274
+ gpu_name, total_vram_gb, available_vram_gb = _detect_gpu()
275
+ total_ram_gb = mem.total / (1024**3)
276
+ available_ram_gb = mem.available / (1024**3)
277
+
278
+ bandwidth = _measure_bandwidth()
279
+
280
+ max_params, quant, est_tps, threads, ctx = _compute_model_limits(available_ram_gb, bandwidth)
281
+
282
+ notes = []
283
+ if not is_x86 and not is_arm:
284
+ notes.append(f"Unrecognized architecture: {arch}. Using conservative defaults.")
285
+ if InstructionSet.AVX2 not in isa and is_x86:
286
+ notes.append("No AVX2 detected — inference will be significantly slower.")
287
+ if available_ram_gb < 4.0:
288
+ notes.append("Very low available RAM. Close other applications for better performance.")
289
+ if InstructionSet.AVX512F in isa:
290
+ notes.append("AVX-512 detected — llama.cpp will use optimized 512-bit kernels.")
291
+ if InstructionSet.AMX_INT8 in isa:
292
+ notes.append("AMX detected — hardware matrix acceleration available.")
293
+
294
+ return HardwareProfile(
295
+ cpu_name=cpu_name,
296
+ arch=arch,
297
+ physical_cores=psutil.cpu_count(logical=False) or 1,
298
+ logical_cores=psutil.cpu_count(logical=True) or 1,
299
+ instruction_sets=isa,
300
+ total_ram_gb=total_ram_gb,
301
+ available_ram_gb=available_ram_gb,
302
+ estimated_bandwidth_gbps=bandwidth,
303
+ max_model_params_b=max_params,
304
+ optimal_quantization=quant,
305
+ estimated_tokens_per_sec=est_tps,
306
+ recommended_threads=threads,
307
+ recommended_context_length=ctx,
308
+ gpu_name=gpu_name,
309
+ total_vram_gb=total_vram_gb,
310
+ available_vram_gb=available_vram_gb,
311
+ notes=notes,
312
+ )
313
+
314
+
315
+ def _detect_gpu() -> tuple[str | None, float, float]:
316
+ """Best-effort GPU/VRAM detection.
317
+
318
+ Tries NVIDIA via `nvidia-smi` (name + memory), else returns no GPU.
319
+ Never raises; unknown hardware just yields zeros and a note.
320
+ """
321
+ import shutil
322
+ import subprocess
323
+
324
+ if not shutil.which("nvidia-smi"):
325
+ return None, 0.0, 0.0
326
+ try:
327
+ out = (
328
+ subprocess.run(
329
+ [
330
+ "nvidia-smi",
331
+ "--query-gpu=name,memory.total,memory.used",
332
+ "--format=csv,noheader,nounits",
333
+ ],
334
+ capture_output=True,
335
+ text=True,
336
+ timeout=5,
337
+ check=True,
338
+ )
339
+ .stdout.strip()
340
+ .splitlines()
341
+ )
342
+ if not out:
343
+ return None, 0.0, 0.0
344
+ name, total_mb, used_mb = out[0].split(",")
345
+ total_gb = float(total_mb) / 1024.0
346
+ used_gb = float(used_mb) / 1024.0
347
+ return name.strip(), total_gb, max(total_gb - used_gb, 0.0)
348
+ except Exception:
349
+ return None, 0.0, 0.0
@@ -0,0 +1,28 @@
1
+ """Phase 6: own low-level kernels (reference implementations + backend boundary)."""
2
+
3
+ from eigencore.kernels.q4k import Q4_K_BLOCK_BYTES, QK_K, dequantize_q4_k, quantized_dot_q4_k
4
+ from eigencore.kernels.reference import (
5
+ Q4_0_BLOCK_SIZE,
6
+ Q8_0_BLOCK_SIZE,
7
+ BackendProtocol,
8
+ ScalarBackend,
9
+ dequantize_q4_0,
10
+ dequantize_q8_0,
11
+ quantized_dot_q8_0,
12
+ rms_norm,
13
+ )
14
+
15
+ __all__ = [
16
+ "Q4_K_BLOCK_BYTES",
17
+ "QK_K",
18
+ "Q4_0_BLOCK_SIZE",
19
+ "Q8_0_BLOCK_SIZE",
20
+ "BackendProtocol",
21
+ "ScalarBackend",
22
+ "dequantize_q4_k",
23
+ "dequantize_q4_0",
24
+ "dequantize_q8_0",
25
+ "quantized_dot_q4_k",
26
+ "quantized_dot_q8_0",
27
+ "rms_norm",
28
+ ]
@@ -0,0 +1,76 @@
1
+ """
2
+ Q4_K reference kernel — Stage E slice 1.
3
+
4
+ GGUF Q4_K super-block layout (256 values = 8 sub-blocks of 32):
5
+ bytes [0:2) fp16 d (super-block scale)
6
+ bytes [2:4) fp16 dmin (super-block min)
7
+ bytes [4:16) 12 bytes of 8x (6-bit sc + 6-bit min), packed per ggml
8
+ bytes [16:144) 128 bytes of 4-bit quants, low nibble = even index
9
+
10
+ Dequant: y[j] = d*sc[b]*q[j] - dmin*m[b] (ggml uses d*sc*q - dm, note the MINUS)
11
+
12
+ This is the scalar reference: bit-exact-tested against gguf-py. A future
13
+ SIMD/C-intrinsics kernel must match it within documented tolerance.
14
+ NO performance is claimed here.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import numpy as np
20
+
21
+ QK_K = 256 # values per Q4_K super-block
22
+ Q4_K_BLOCK_BYTES = 2 + 2 + 12 + QK_K // 2 # 144
23
+ Q4_K_SCALE_SIZE = 12
24
+
25
+
26
+ def _get_scale_min(scales: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
27
+ """Unpack 12 scale bytes into 8 (sc, min) 6-bit pairs (ggml layout)."""
28
+ scales = np.asarray(scales, dtype=np.uint8).reshape(-1, 3, 4)
29
+ d, m, m_d = np.split(scales, 3, axis=-2)
30
+ sc = np.concatenate([d & 0x3F, (m_d & 0x0F) | ((d >> 2) & 0x30)], axis=-1)
31
+ mn = np.concatenate([m & 0x3F, (m_d >> 4) | ((m >> 2) & 0x30)], axis=-1)
32
+ return sc.reshape(-1, 8).astype(np.float32), mn.reshape(-1, 8).astype(np.float32)
33
+
34
+
35
+ def dequantize_q4_k(blocks: np.ndarray | bytes) -> np.ndarray:
36
+ """Dequantize Q4_K super-blocks to float32 (n_blocks * 256,).
37
+
38
+ Accepts a raw bytes buffer of n*144 bytes, or a uint8 array (n, 144).
39
+ """
40
+ if isinstance(blocks, (bytes, bytearray)):
41
+ raw = np.frombuffer(bytes(blocks), dtype=np.uint8)
42
+ if raw.size % Q4_K_BLOCK_BYTES != 0:
43
+ raise ValueError(f"raw buffer not a multiple of {Q4_K_BLOCK_BYTES} bytes")
44
+ blocks = raw.reshape(-1, Q4_K_BLOCK_BYTES)
45
+ blocks = np.asarray(blocks, dtype=np.uint8)
46
+ if blocks.ndim != 2 or blocks.shape[1] != Q4_K_BLOCK_BYTES:
47
+ raise ValueError(f"expected (n, {Q4_K_BLOCK_BYTES}) blocks, got {blocks.shape}")
48
+ n_blocks = blocks.shape[0]
49
+
50
+ d = blocks[:, 0:2].copy().view(np.float16).astype(np.float32).reshape(n_blocks)
51
+ dmin = blocks[:, 2:4].copy().view(np.float16).astype(np.float32).reshape(n_blocks)
52
+ sc, m = _get_scale_min(blocks[:, 4 : 4 + Q4_K_SCALE_SIZE])
53
+ qs = blocks[:, 4 + Q4_K_SCALE_SIZE :]
54
+
55
+ # 4-bit quants: byte j//2 within each 32-value sub-block, low nibble even
56
+ qs = qs.reshape(n_blocks, -1, 1, 32) >> np.array([0, 4], dtype=np.uint8).reshape(1, 1, 2, 1)
57
+ qs = (qs & np.uint8(0x0F)).reshape(n_blocks, -1, 32).astype(np.float32)
58
+
59
+ d_sc = (d[:, None] * sc).reshape(n_blocks, -1, 1)
60
+ dm = (dmin[:, None] * m).reshape(n_blocks, -1, 1)
61
+ return (d_sc * qs - dm).reshape(n_blocks, QK_K).reshape(-1)
62
+
63
+
64
+ def quantized_dot_q4_k(packed: np.ndarray | bytes, vec: np.ndarray) -> float:
65
+ """Dot product of a Q4_K-quantized weight row with a float vector.
66
+
67
+ Dequantizes super-block by super-block and accumulates in float32.
68
+ """
69
+ w = dequantize_q4_k(packed)
70
+ v = np.asarray(vec, dtype=np.float32)
71
+ if w.shape[0] != v.shape[0]:
72
+ raise ValueError(f"shape mismatch: weights {w.shape} vs vec {v.shape}")
73
+ acc = 0.0
74
+ for i in range(0, w.shape[0], QK_K):
75
+ acc += float(np.dot(w[i : i + QK_K], v[i : i + QK_K]))
76
+ return acc
@@ -0,0 +1,168 @@
1
+ """
2
+ Scalar reference kernels — Phase 6 slice 1.
3
+
4
+ These are deliberately simple, correct-by-inspection implementations of the
5
+ core math a custom inference engine needs. They serve two purposes:
6
+
7
+ 1. A trusted reference for differential-testing any future optimized
8
+ (SIMD/assembly) kernel — an optimized kernel must match these within a
9
+ documented tolerance.
10
+ 2. A backend boundary (`BackendProtocol`) so a future native engine can be
11
+ swapped in behind the same interface without touching callers.
12
+
13
+ NO performance is claimed for these. They exist to make correctness
14
+ measurable before optimization.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import math
20
+ from typing import Protocol
21
+
22
+ import numpy as np
23
+
24
+ # GGUF Q8_0 block: 32 values per block, one fp16 scale.
25
+ Q8_0_BLOCK_SIZE = 32
26
+ # GGUF Q4_0 block: 32 values per block (4-bit packed), one fp16 scale.
27
+ Q4_0_BLOCK_SIZE = 32
28
+ # Raw on-disk block sizes: 2-byte fp16 scale + payload.
29
+ Q8_0_BLOCK_BYTES = 2 + Q8_0_BLOCK_SIZE # 34
30
+ Q4_0_BLOCK_BYTES = 2 + Q4_0_BLOCK_SIZE // 2 # 18
31
+
32
+
33
+ class BackendProtocol(Protocol):
34
+ """Interface a custom/native inference backend must satisfy.
35
+
36
+ Implementations (e.g. a future compiled sidecar) plug in here; the
37
+ llama.cpp path remains the default until a kernel proves itself.
38
+ """
39
+
40
+ def rms_norm(self, x: np.ndarray, weight: np.ndarray, eps: float = 1e-5) -> np.ndarray: ...
41
+
42
+ def dequantize_q8_0(self, blocks: np.ndarray) -> np.ndarray: ...
43
+
44
+ def dequantize_q4_0(
45
+ self, packed: np.ndarray, scales: np.ndarray | None = None
46
+ ) -> np.ndarray: ...
47
+
48
+ def dequantize_q4_k(self, blocks: np.ndarray | bytes) -> np.ndarray: ...
49
+
50
+ def quantized_dot_q4_k(self, packed: np.ndarray | bytes, vec: np.ndarray) -> float: ...
51
+
52
+ def quantized_dot_q8_0(
53
+ self, packed: np.ndarray, vec: np.ndarray, group_size: int = Q8_0_BLOCK_SIZE
54
+ ) -> float: ...
55
+
56
+
57
+ def rms_norm(x: np.ndarray, weight: np.ndarray, eps: float = 1e-5) -> np.ndarray:
58
+ """Root-mean-square layer norm: x / rms(x) * weight."""
59
+ x = np.asarray(x, dtype=np.float32)
60
+ weight = np.asarray(weight, dtype=np.float32)
61
+ ms = np.mean(x * x)
62
+ return x * (1.0 / math.sqrt(float(ms) + eps)) * weight
63
+
64
+
65
+ def dequantize_q8_0(blocks: np.ndarray) -> np.ndarray:
66
+ """Dequantize GGUF Q8_0 blocks.
67
+
68
+ `blocks` has shape (n_blocks, 1 + Q8_0_BLOCK_SIZE): column 0 is the fp16
69
+ scale, the rest are int8 quantized values. Returns float32 vector of
70
+ length n_blocks * Q8_0_BLOCK_SIZE.
71
+ """
72
+ blocks = np.asarray(blocks)
73
+ if blocks.ndim != 2 or blocks.shape[1] != 1 + Q8_0_BLOCK_SIZE:
74
+ raise ValueError(f"expected (n, {1 + Q8_0_BLOCK_SIZE}) blocks, got {blocks.shape}")
75
+ scales = blocks[:, 0].astype(np.float32)
76
+ quants = blocks[:, 1:].astype(np.float32)
77
+ return (scales[:, None] * quants).reshape(-1)
78
+
79
+
80
+ def dequantize_q4_0(packed: np.ndarray | bytes, scales: np.ndarray | None = None) -> np.ndarray:
81
+ """Dequantize GGUF Q4_0 blocks (ggml layout).
82
+
83
+ ggml packs 32 values into 16 bytes per block: byte j holds element j in
84
+ its LOW nibble and element j+16 in its HIGH nibble (strided, not
85
+ interleaved), each nibble biased by -8 and scaled by the block's fp16
86
+ scale. Accepts either (a) a raw bytes buffer of n_blocks × 18-byte
87
+ blocks, or (b) `packed` uint8 (n, 16) plus `scales` fp16 (n,).
88
+ Returns float32 of length n_blocks * Q4_0_BLOCK_SIZE.
89
+ """
90
+ if isinstance(packed, (bytes, bytearray)):
91
+ raw = np.frombuffer(bytes(packed), dtype=np.uint8)
92
+ if raw.size % Q4_0_BLOCK_BYTES != 0:
93
+ raise ValueError(f"raw buffer not a multiple of {Q4_0_BLOCK_BYTES} bytes")
94
+ blocks = raw.reshape(-1, Q4_0_BLOCK_BYTES)
95
+ scales = blocks[:, :2].copy().view(np.float16).astype(np.float32).reshape(-1)
96
+ packed = blocks[:, 2:]
97
+ else:
98
+ packed = np.asarray(packed)
99
+ if scales is None:
100
+ raise ValueError("scales required when packed is an array")
101
+ scales = np.asarray(scales, dtype=np.float32)
102
+ if packed.ndim != 2 or packed.shape[1] != Q4_0_BLOCK_SIZE // 2:
103
+ raise ValueError(f"expected (n, {Q4_0_BLOCK_SIZE // 2}) packed, got {packed.shape}")
104
+ if scales.shape[0] != packed.shape[0]:
105
+ raise ValueError("scales/packed block count mismatch")
106
+ low = (packed & 0x0F).astype(np.float32) - 8.0
107
+ high = (packed >> 4).astype(np.float32) - 8.0
108
+ # ggml strided order: low nibbles fill elements 0..15, high nibbles 16..31
109
+ out = np.empty((packed.shape[0], Q4_0_BLOCK_SIZE), dtype=np.float32)
110
+ out[:, : Q4_0_BLOCK_SIZE // 2] = low
111
+ out[:, Q4_0_BLOCK_SIZE // 2 :] = high
112
+ return (scales[:, None] * out).reshape(-1)
113
+
114
+
115
+ def quantized_dot_q8_0(
116
+ packed: np.ndarray, vec: np.ndarray, group_size: int = Q8_0_BLOCK_SIZE
117
+ ) -> float:
118
+ """Dot product of a Q8_0-quantized weight with a float vector.
119
+
120
+ Dequantizes block-by-block and accumulates in float32. This is the
121
+ hot inner loop of CPU LLM decode, so it's the first kernel a native
122
+ implementation would replace.
123
+ """
124
+ w = dequantize_q8_0(packed)
125
+ v = np.asarray(vec, dtype=np.float32)
126
+ if w.shape[0] != v.shape[0]:
127
+ raise ValueError(f"shape mismatch: weights {w.shape} vs vec {v.shape}")
128
+ n_groups = w.shape[0] // group_size
129
+ acc = 0.0
130
+ for g in range(n_groups):
131
+ acc += float(
132
+ np.dot(
133
+ w[g * group_size : (g + 1) * group_size], v[g * group_size : (g + 1) * group_size]
134
+ )
135
+ )
136
+ return acc
137
+
138
+
139
+ class ScalarBackend:
140
+ """Reference implementation of BackendProtocol (NumPy, scalar loops)."""
141
+
142
+ name = "scalar-numpy"
143
+
144
+ def rms_norm(self, x: np.ndarray, weight: np.ndarray, eps: float = 1e-5) -> np.ndarray:
145
+ return rms_norm(x, weight, eps)
146
+
147
+ def dequantize_q8_0(self, blocks: np.ndarray) -> np.ndarray:
148
+ return dequantize_q8_0(blocks)
149
+
150
+ def dequantize_q4_0(
151
+ self, packed: np.ndarray | bytes, scales: np.ndarray | None = None
152
+ ) -> np.ndarray:
153
+ return dequantize_q4_0(packed, scales)
154
+
155
+ def quantized_dot_q8_0(
156
+ self, packed: np.ndarray, vec: np.ndarray, group_size: int = Q8_0_BLOCK_SIZE
157
+ ) -> float:
158
+ return quantized_dot_q8_0(packed, vec, group_size)
159
+
160
+ def dequantize_q4_k(self, blocks: np.ndarray | bytes) -> np.ndarray:
161
+ from eigencore.kernels.q4k import dequantize_q4_k
162
+
163
+ return dequantize_q4_k(blocks)
164
+
165
+ def quantized_dot_q4_k(self, packed: np.ndarray | bytes, vec: np.ndarray) -> float:
166
+ from eigencore.kernels.q4k import quantized_dot_q4_k
167
+
168
+ return quantized_dot_q4_k(packed, vec)
@@ -0,0 +1,3 @@
1
+ from eigencore.models.registry import ModelRegistry, ModelSpec
2
+
3
+ __all__ = ["ModelRegistry", "ModelSpec"]