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.
- eigencore/__init__.py +82 -0
- eigencore/agent/__init__.py +45 -0
- eigencore/agent/executor.py +297 -0
- eigencore/agent/grammar.py +121 -0
- eigencore/agent/policy.py +262 -0
- eigencore/agent/react.py +261 -0
- eigencore/agent/tool.py +149 -0
- eigencore/analysis/__init__.py +3 -0
- eigencore/analysis/sparsity.py +184 -0
- eigencore/benchmark/__init__.py +33 -0
- eigencore/benchmark/harness.py +342 -0
- eigencore/cli/__init__.py +0 -0
- eigencore/cli/main.py +856 -0
- eigencore/context/__init__.py +3 -0
- eigencore/context/manager.py +318 -0
- eigencore/engine/__init__.py +3 -0
- eigencore/engine/inference.py +378 -0
- eigencore/engine/layer_skip.py +268 -0
- eigencore/engine/prompt_lookup.py +39 -0
- eigencore/engine/pruning.py +116 -0
- eigencore/engine/sparse_inference.py +218 -0
- eigencore/engine/sparsity_experiment.py +109 -0
- eigencore/engine/speculative.py +238 -0
- eigencore/fit/__init__.py +34 -0
- eigencore/fit/estimator.py +310 -0
- eigencore/fit/gguf_meta.py +130 -0
- eigencore/forge.py +216 -0
- eigencore/hal/__init__.py +3 -0
- eigencore/hal/_cpuid_win.py +119 -0
- eigencore/hal/profiler.py +349 -0
- eigencore/kernels/__init__.py +28 -0
- eigencore/kernels/q4k.py +76 -0
- eigencore/kernels/reference.py +168 -0
- eigencore/models/__init__.py +3 -0
- eigencore/models/registry.py +193 -0
- eigencore/router/__init__.py +3 -0
- eigencore/router/classifier.py +176 -0
- eigencore/training/__init__.py +3 -0
- eigencore/training/convergence.py +112 -0
- eigencore/training/trainer.py +401 -0
- eigencore/tui/__init__.py +5 -0
- eigencore/tui/chat.py +265 -0
- eigencore/tui/onboarding.py +134 -0
- eigencore/tui/theme.py +61 -0
- eigencore-0.4.0.dist-info/METADATA +144 -0
- eigencore-0.4.0.dist-info/RECORD +49 -0
- eigencore-0.4.0.dist-info/WHEEL +4 -0
- eigencore-0.4.0.dist-info/entry_points.txt +2 -0
- 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
|
+
]
|
eigencore/kernels/q4k.py
ADDED
|
@@ -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)
|