ggufit 0.1.0__tar.gz
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.
- ggufit-0.1.0/PKG-INFO +102 -0
- ggufit-0.1.0/README.md +84 -0
- ggufit-0.1.0/ggufit/__init__.py +1 -0
- ggufit-0.1.0/ggufit/calculator.py +105 -0
- ggufit-0.1.0/ggufit/cli.py +104 -0
- ggufit-0.1.0/ggufit/formatting.py +88 -0
- ggufit-0.1.0/ggufit/hardware.py +74 -0
- ggufit-0.1.0/ggufit/models_db.py +726 -0
- ggufit-0.1.0/ggufit.egg-info/PKG-INFO +102 -0
- ggufit-0.1.0/ggufit.egg-info/SOURCES.txt +14 -0
- ggufit-0.1.0/ggufit.egg-info/dependency_links.txt +1 -0
- ggufit-0.1.0/ggufit.egg-info/entry_points.txt +2 -0
- ggufit-0.1.0/ggufit.egg-info/requires.txt +2 -0
- ggufit-0.1.0/ggufit.egg-info/top_level.txt +1 -0
- ggufit-0.1.0/pyproject.toml +31 -0
- ggufit-0.1.0/setup.cfg +4 -0
ggufit-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ggufit
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Check whether a local LLM can run on your machine (CPU-only inference).
|
|
5
|
+
Author: Mouad
|
|
6
|
+
License: MIT
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Environment :: Console
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Topic :: Utilities
|
|
14
|
+
Requires-Python: >=3.9
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
Requires-Dist: psutil>=5.9
|
|
17
|
+
Requires-Dist: numpy>=1.24
|
|
18
|
+
|
|
19
|
+
# ggufit
|
|
20
|
+
|
|
21
|
+
Check whether a local LLM can run on your machine, CPU-only — from PowerShell or any terminal.
|
|
22
|
+
|
|
23
|
+
Uses real formulas (model memory footprint, KV cache size, and a measured memory-bandwidth
|
|
24
|
+
micro-benchmark) rather than guesses, to estimate whether a model fits in RAM and roughly
|
|
25
|
+
how fast it'll generate tokens. Correctly handles MoE models (speed driven by active
|
|
26
|
+
experts, not total params) and SSM/MLA architectures (Mamba, DeepSeek's MLA) that don't
|
|
27
|
+
use standard multi-head attention.
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
# Linux/macOS - recommended (avoids PEP 668 "externally managed environment" errors)
|
|
33
|
+
sudo apt install pipx # if not already installed
|
|
34
|
+
pipx ensurepath # reopen terminal after this
|
|
35
|
+
cd ggufit/ # the folder containing pyproject.toml
|
|
36
|
+
pipx install .
|
|
37
|
+
|
|
38
|
+
# Windows PowerShell
|
|
39
|
+
pip install .
|
|
40
|
+
|
|
41
|
+
# Active development (editable install - code changes apply without reinstalling)
|
|
42
|
+
pipx install -e .
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`ggufit` is now available as a global command.
|
|
46
|
+
|
|
47
|
+
## Usage
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
# Full hardware scan: shows your CPU/RAM/bandwidth and which models fit
|
|
51
|
+
ggufit scan
|
|
52
|
+
|
|
53
|
+
# Check one specific model
|
|
54
|
+
ggufit llama3.1
|
|
55
|
+
ggufit mistral
|
|
56
|
+
ggufit qwen2.5-14b
|
|
57
|
+
|
|
58
|
+
# Force a specific quantization level
|
|
59
|
+
ggufit llama3.1 --quant q4
|
|
60
|
+
|
|
61
|
+
# Evaluate at a longer context length
|
|
62
|
+
ggufit qwen2.5-32b --context 16384
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## How it works
|
|
66
|
+
|
|
67
|
+
For each model:
|
|
68
|
+
1. **Model size (RAM)** = total params × bytes-per-parameter (varies by quant: FP16, Q8, Q6, Q5, Q4, Q3, Q2)
|
|
69
|
+
2. **KV cache** = `2 × layers × hidden_size × context_len × bytes_per_param × kv_cache_multiplier`
|
|
70
|
+
- `kv_cache_multiplier` defaults to 1.0 (standard MHA), and is set lower for architectures
|
|
71
|
+
that don't use full attention at every layer: `0.0` for pure SSM/Mamba (no attention at all),
|
|
72
|
+
`~0.15` for MLA (DeepSeek-V2/V3/R1, MiniCPM3), `~0.125` for hybrid Mamba+attention (Jamba).
|
|
73
|
+
3. **Total RAM needed** = `(model size + KV cache) × 1.2` overhead factor
|
|
74
|
+
4. **Speed estimate** = `measured_memory_bandwidth / active_size`, where `active_size` is the
|
|
75
|
+
total model size for dense models, or just the active experts' size for MoE models
|
|
76
|
+
(`active_params_billion`) — since only those weights are streamed from RAM per token.
|
|
77
|
+
|
|
78
|
+
`ggufit` runs a quick real memory-bandwidth benchmark (a large array copy) instead of guessing
|
|
79
|
+
from RAM specs, since achievable bandwidth depends heavily on channel configuration.
|
|
80
|
+
|
|
81
|
+
## Adding models
|
|
82
|
+
|
|
83
|
+
Edit `ggufit/models_db.py` and add an entry to the `MODELS` dict with `params_billion`,
|
|
84
|
+
`layers`, and `hidden_size` (found in the model's Hugging Face `config.json`). Optional:
|
|
85
|
+
- `moe: True` + `active_params_billion: X` for Mixture-of-Experts models
|
|
86
|
+
- `kv_cache_multiplier: X` for non-standard attention architectures (see above)
|
|
87
|
+
|
|
88
|
+
## Known limitations
|
|
89
|
+
|
|
90
|
+
- Speed estimates assume batch size 1, single-user chat.
|
|
91
|
+
- The bandwidth benchmark is single-threaded; real inference engines use multiple threads.
|
|
92
|
+
- Quant byte-per-param values are approximations of real GGUF file sizes.
|
|
93
|
+
- KV cache still assumes full hidden_size for standard (non-flagged) models even though most
|
|
94
|
+
modern ones use GQA with fewer KV heads than query heads — this is intentionally
|
|
95
|
+
conservative (over-estimates KV, doesn't affect the dominant "does it load" answer).
|
|
96
|
+
- `kv_cache_multiplier` values for MLA/hybrid architectures are approximate, not
|
|
97
|
+
per-model-measured.
|
|
98
|
+
|
|
99
|
+
## Notes
|
|
100
|
+
|
|
101
|
+
This is a side-project CLI. The full hardware-scan desktop app (Rust/Tauri) is a separate,
|
|
102
|
+
more thorough tool still in development.
|
ggufit-0.1.0/README.md
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# ggufit
|
|
2
|
+
|
|
3
|
+
Check whether a local LLM can run on your machine, CPU-only — from PowerShell or any terminal.
|
|
4
|
+
|
|
5
|
+
Uses real formulas (model memory footprint, KV cache size, and a measured memory-bandwidth
|
|
6
|
+
micro-benchmark) rather than guesses, to estimate whether a model fits in RAM and roughly
|
|
7
|
+
how fast it'll generate tokens. Correctly handles MoE models (speed driven by active
|
|
8
|
+
experts, not total params) and SSM/MLA architectures (Mamba, DeepSeek's MLA) that don't
|
|
9
|
+
use standard multi-head attention.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# Linux/macOS - recommended (avoids PEP 668 "externally managed environment" errors)
|
|
15
|
+
sudo apt install pipx # if not already installed
|
|
16
|
+
pipx ensurepath # reopen terminal after this
|
|
17
|
+
cd ggufit/ # the folder containing pyproject.toml
|
|
18
|
+
pipx install .
|
|
19
|
+
|
|
20
|
+
# Windows PowerShell
|
|
21
|
+
pip install .
|
|
22
|
+
|
|
23
|
+
# Active development (editable install - code changes apply without reinstalling)
|
|
24
|
+
pipx install -e .
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
`ggufit` is now available as a global command.
|
|
28
|
+
|
|
29
|
+
## Usage
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
# Full hardware scan: shows your CPU/RAM/bandwidth and which models fit
|
|
33
|
+
ggufit scan
|
|
34
|
+
|
|
35
|
+
# Check one specific model
|
|
36
|
+
ggufit llama3.1
|
|
37
|
+
ggufit mistral
|
|
38
|
+
ggufit qwen2.5-14b
|
|
39
|
+
|
|
40
|
+
# Force a specific quantization level
|
|
41
|
+
ggufit llama3.1 --quant q4
|
|
42
|
+
|
|
43
|
+
# Evaluate at a longer context length
|
|
44
|
+
ggufit qwen2.5-32b --context 16384
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## How it works
|
|
48
|
+
|
|
49
|
+
For each model:
|
|
50
|
+
1. **Model size (RAM)** = total params × bytes-per-parameter (varies by quant: FP16, Q8, Q6, Q5, Q4, Q3, Q2)
|
|
51
|
+
2. **KV cache** = `2 × layers × hidden_size × context_len × bytes_per_param × kv_cache_multiplier`
|
|
52
|
+
- `kv_cache_multiplier` defaults to 1.0 (standard MHA), and is set lower for architectures
|
|
53
|
+
that don't use full attention at every layer: `0.0` for pure SSM/Mamba (no attention at all),
|
|
54
|
+
`~0.15` for MLA (DeepSeek-V2/V3/R1, MiniCPM3), `~0.125` for hybrid Mamba+attention (Jamba).
|
|
55
|
+
3. **Total RAM needed** = `(model size + KV cache) × 1.2` overhead factor
|
|
56
|
+
4. **Speed estimate** = `measured_memory_bandwidth / active_size`, where `active_size` is the
|
|
57
|
+
total model size for dense models, or just the active experts' size for MoE models
|
|
58
|
+
(`active_params_billion`) — since only those weights are streamed from RAM per token.
|
|
59
|
+
|
|
60
|
+
`ggufit` runs a quick real memory-bandwidth benchmark (a large array copy) instead of guessing
|
|
61
|
+
from RAM specs, since achievable bandwidth depends heavily on channel configuration.
|
|
62
|
+
|
|
63
|
+
## Adding models
|
|
64
|
+
|
|
65
|
+
Edit `ggufit/models_db.py` and add an entry to the `MODELS` dict with `params_billion`,
|
|
66
|
+
`layers`, and `hidden_size` (found in the model's Hugging Face `config.json`). Optional:
|
|
67
|
+
- `moe: True` + `active_params_billion: X` for Mixture-of-Experts models
|
|
68
|
+
- `kv_cache_multiplier: X` for non-standard attention architectures (see above)
|
|
69
|
+
|
|
70
|
+
## Known limitations
|
|
71
|
+
|
|
72
|
+
- Speed estimates assume batch size 1, single-user chat.
|
|
73
|
+
- The bandwidth benchmark is single-threaded; real inference engines use multiple threads.
|
|
74
|
+
- Quant byte-per-param values are approximations of real GGUF file sizes.
|
|
75
|
+
- KV cache still assumes full hidden_size for standard (non-flagged) models even though most
|
|
76
|
+
modern ones use GQA with fewer KV heads than query heads — this is intentionally
|
|
77
|
+
conservative (over-estimates KV, doesn't affect the dominant "does it load" answer).
|
|
78
|
+
- `kv_cache_multiplier` values for MLA/hybrid architectures are approximate, not
|
|
79
|
+
per-model-measured.
|
|
80
|
+
|
|
81
|
+
## Notes
|
|
82
|
+
|
|
83
|
+
This is a side-project CLI. The full hardware-scan desktop app (Rust/Tauri) is a separate,
|
|
84
|
+
more thorough tool still in development.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Core math: given a model's architecture specs, a quant level, a context length,
|
|
3
|
+
and this machine's hardware profile, estimate whether it fits and how fast it'll run.
|
|
4
|
+
|
|
5
|
+
Fixed in this version:
|
|
6
|
+
- MoE models now use active_params_billion (not total params_billion) for the
|
|
7
|
+
tokens/sec estimate. Total params still governs the RAM/fit check, since all
|
|
8
|
+
expert weights must be resident in memory even if only some are touched per
|
|
9
|
+
token. Previously this made every MoE entry (Mixtral, DeepSeek-V3, Qwen3-MoE,
|
|
10
|
+
etc.) look far slower than it actually is - up to ~40x for the biggest ones.
|
|
11
|
+
- KV cache now respects an optional per-model kv_cache_multiplier, so SSM/Mamba
|
|
12
|
+
(0.0, no attention at all) and MLA architectures (~0.15, heavily compressed KV)
|
|
13
|
+
aren't computed as if they used standard multi-head attention.
|
|
14
|
+
"""
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
|
|
17
|
+
from .models_db import QUANT_BYTES_PER_PARAM, QUANT_FALLBACK_ORDER
|
|
18
|
+
|
|
19
|
+
OVERHEAD_FACTOR = 1.2 # framework / buffer overhead on top of raw weights + KV cache
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class FitResult:
|
|
24
|
+
quant: str
|
|
25
|
+
model_size_gb: float # total weight size (what must fit in RAM)
|
|
26
|
+
active_size_gb: float # size of weights touched per token (drives speed)
|
|
27
|
+
kv_cache_gb: float
|
|
28
|
+
total_ram_needed_gb: float
|
|
29
|
+
fits: bool
|
|
30
|
+
est_tokens_per_sec: float
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def model_size_gb(params_billion: float, quant: str) -> float:
|
|
34
|
+
bytes_per_param = QUANT_BYTES_PER_PARAM[quant]
|
|
35
|
+
total_bytes = params_billion * 1e9 * bytes_per_param
|
|
36
|
+
return total_bytes / (1024**3)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def kv_cache_gb(layers: int, hidden_size: int, seq_len: int, batch: int = 1,
|
|
40
|
+
bytes_per_param: float = 2.0, kv_cache_multiplier: float = 1.0) -> float:
|
|
41
|
+
# 2x for K and V, one value per layer/hidden-dim/token/batch.
|
|
42
|
+
# kv_cache_multiplier corrects for architectures that don't use full MHA
|
|
43
|
+
# at every layer (SSM/Mamba = 0.0, MLA-compressed = ~0.15, hybrid = ~0.125).
|
|
44
|
+
total_bytes = 2 * layers * hidden_size * seq_len * batch * bytes_per_param * kv_cache_multiplier
|
|
45
|
+
return total_bytes / (1024**3)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def estimate_tokens_per_sec(active_size_gb: float, bandwidth_gbps: float) -> float:
|
|
49
|
+
if active_size_gb <= 0:
|
|
50
|
+
return 0.0
|
|
51
|
+
return round(bandwidth_gbps / active_size_gb, 2)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def evaluate_fit(params_billion: float, layers: int, hidden_size: int,
|
|
55
|
+
quant: str, context_len: int, available_ram_gb: float,
|
|
56
|
+
bandwidth_gbps: float, moe: bool = False,
|
|
57
|
+
active_params_billion: float = None,
|
|
58
|
+
kv_cache_multiplier: float = 1.0) -> FitResult:
|
|
59
|
+
m_gb = model_size_gb(params_billion, quant)
|
|
60
|
+
|
|
61
|
+
# Speed is driven by whatever weights are actually touched per token.
|
|
62
|
+
# For MoE models that's active_params_billion; for dense models it's the same
|
|
63
|
+
# as the total (m_gb).
|
|
64
|
+
if moe and active_params_billion is not None:
|
|
65
|
+
active_gb = model_size_gb(active_params_billion, quant)
|
|
66
|
+
else:
|
|
67
|
+
active_gb = m_gb
|
|
68
|
+
|
|
69
|
+
kv_gb = kv_cache_gb(layers, hidden_size, context_len, kv_cache_multiplier=kv_cache_multiplier)
|
|
70
|
+
|
|
71
|
+
# RAM/fit check always uses the TOTAL model size - every expert must be
|
|
72
|
+
# resident in memory even for MoE, regardless of how many are active per token.
|
|
73
|
+
total_needed = (m_gb + kv_gb) * OVERHEAD_FACTOR
|
|
74
|
+
fits = total_needed <= available_ram_gb
|
|
75
|
+
|
|
76
|
+
tok_s = estimate_tokens_per_sec(active_gb, bandwidth_gbps)
|
|
77
|
+
|
|
78
|
+
return FitResult(
|
|
79
|
+
quant=quant,
|
|
80
|
+
model_size_gb=round(m_gb, 2),
|
|
81
|
+
active_size_gb=round(active_gb, 2),
|
|
82
|
+
kv_cache_gb=round(kv_gb, 2),
|
|
83
|
+
total_ram_needed_gb=round(total_needed, 2),
|
|
84
|
+
fits=fits,
|
|
85
|
+
est_tokens_per_sec=tok_s,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def best_fitting_quant(params_billion: float, layers: int, hidden_size: int,
|
|
90
|
+
context_len: int, available_ram_gb: float,
|
|
91
|
+
bandwidth_gbps: float, moe: bool = False,
|
|
92
|
+
active_params_billion: float = None,
|
|
93
|
+
kv_cache_multiplier: float = 1.0):
|
|
94
|
+
"""Try quant levels from highest quality to lowest, return the best one that fits, or None."""
|
|
95
|
+
results = {}
|
|
96
|
+
best = None
|
|
97
|
+
for quant in QUANT_FALLBACK_ORDER:
|
|
98
|
+
result = evaluate_fit(params_billion, layers, hidden_size, quant,
|
|
99
|
+
context_len, available_ram_gb, bandwidth_gbps,
|
|
100
|
+
moe=moe, active_params_billion=active_params_billion,
|
|
101
|
+
kv_cache_multiplier=kv_cache_multiplier)
|
|
102
|
+
results[quant] = result
|
|
103
|
+
if result.fits and best is None:
|
|
104
|
+
best = result
|
|
105
|
+
return best, results
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
from . import hardware, calculator, formatting
|
|
5
|
+
from .models_db import MODELS, find_model, suggest_models
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _model_kwargs(info):
|
|
9
|
+
"""Extract the optional architecture-aware fields, with safe defaults."""
|
|
10
|
+
return {
|
|
11
|
+
"moe": info.get("moe", False),
|
|
12
|
+
"active_params_billion": info.get("active_params_billion"),
|
|
13
|
+
"kv_cache_multiplier": info.get("kv_cache_multiplier", 1.0),
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def cmd_scan(args):
|
|
18
|
+
print("Scanning hardware (running a quick memory-bandwidth benchmark)...")
|
|
19
|
+
profile = hardware.get_full_profile()
|
|
20
|
+
formatting.print_hardware_profile(profile)
|
|
21
|
+
|
|
22
|
+
available_ram = profile["ram"]["available_gb"]
|
|
23
|
+
bandwidth = profile["measured_bandwidth_gbps"]
|
|
24
|
+
context_len = args.context
|
|
25
|
+
|
|
26
|
+
recommended, marginal, not_recommended = [], [], []
|
|
27
|
+
|
|
28
|
+
for key, info in MODELS.items():
|
|
29
|
+
best, _all = calculator.best_fitting_quant(
|
|
30
|
+
info["params_billion"], info["layers"], info["hidden_size"],
|
|
31
|
+
context_len, available_ram, bandwidth, **_model_kwargs(info)
|
|
32
|
+
)
|
|
33
|
+
if best is None:
|
|
34
|
+
not_recommended.append(info["display_name"])
|
|
35
|
+
elif best.quant in ("q4", "q3", "q2"):
|
|
36
|
+
marginal.append((info["display_name"], best.quant, best.est_tokens_per_sec))
|
|
37
|
+
else:
|
|
38
|
+
recommended.append((info["display_name"], best.quant, best.est_tokens_per_sec))
|
|
39
|
+
|
|
40
|
+
formatting.print_scan_summary(recommended, marginal, not_recommended)
|
|
41
|
+
print()
|
|
42
|
+
print(formatting.info("Run 'ggufit <model-name>' for a detailed breakdown of a specific model."))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def cmd_model(args):
|
|
46
|
+
key, info = find_model(args.model)
|
|
47
|
+
if info is None:
|
|
48
|
+
print(formatting.bad(f"Model '{args.model}' not found in the database."))
|
|
49
|
+
suggestions = suggest_models(args.model)
|
|
50
|
+
if suggestions:
|
|
51
|
+
print("Did you mean:")
|
|
52
|
+
for s in suggestions:
|
|
53
|
+
print(f" - {s} ({MODELS[s]['display_name']})")
|
|
54
|
+
else:
|
|
55
|
+
print("Try 'ggufit scan' to see all known models.")
|
|
56
|
+
sys.exit(1)
|
|
57
|
+
|
|
58
|
+
print("Checking hardware (quick benchmark)...")
|
|
59
|
+
profile = hardware.get_full_profile()
|
|
60
|
+
available_ram = profile["ram"]["available_gb"]
|
|
61
|
+
bandwidth = profile["measured_bandwidth_gbps"]
|
|
62
|
+
|
|
63
|
+
formatting.print_hardware_profile(profile)
|
|
64
|
+
|
|
65
|
+
kwargs = _model_kwargs(info)
|
|
66
|
+
|
|
67
|
+
if args.quant:
|
|
68
|
+
result = calculator.evaluate_fit(
|
|
69
|
+
info["params_billion"], info["layers"], info["hidden_size"],
|
|
70
|
+
args.quant, args.context, available_ram, bandwidth, **kwargs
|
|
71
|
+
)
|
|
72
|
+
best = result if result.fits else None
|
|
73
|
+
all_results = {args.quant: result}
|
|
74
|
+
else:
|
|
75
|
+
best, all_results = calculator.best_fitting_quant(
|
|
76
|
+
info["params_billion"], info["layers"], info["hidden_size"],
|
|
77
|
+
args.context, available_ram, bandwidth, **kwargs
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
formatting.print_model_result(info["display_name"], args.context, best, all_results,
|
|
81
|
+
available_ram, is_moe=info.get("moe", False))
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def main():
|
|
85
|
+
parser = argparse.ArgumentParser(prog="ggufit", description="Check whether local LLMs can run on this machine (CPU-only inference).")
|
|
86
|
+
parser.add_argument("target", nargs="?", help="'scan' for a full hardware scan, or a model name e.g. llama3.1, mistral, qwen2.5-14b")
|
|
87
|
+
parser.add_argument("--quant", choices=["fp32", "fp16", "q8", "q6", "q5", "q4", "q3", "q2"], help="Force a specific quantization level (model mode only)")
|
|
88
|
+
parser.add_argument("--context", type=int, default=4096, help="Context length to evaluate (default 4096)")
|
|
89
|
+
|
|
90
|
+
args = parser.parse_args()
|
|
91
|
+
|
|
92
|
+
if args.target is None:
|
|
93
|
+
parser.print_help()
|
|
94
|
+
return
|
|
95
|
+
|
|
96
|
+
if args.target.lower() == "scan":
|
|
97
|
+
cmd_scan(args)
|
|
98
|
+
else:
|
|
99
|
+
args.model = args.target
|
|
100
|
+
cmd_model(args)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
if __name__ == "__main__":
|
|
104
|
+
main()
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Simple ANSI-colored terminal output. Works in Windows Terminal / PowerShell 7+.
|
|
3
|
+
On very old cmd.exe without ANSI support, colors just render as raw escape
|
|
4
|
+
codes doing nothing harmful - text still reads fine.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
GREEN = "\033[92m"
|
|
8
|
+
RED = "\033[91m"
|
|
9
|
+
YELLOW = "\033[93m"
|
|
10
|
+
CYAN = "\033[96m"
|
|
11
|
+
BOLD = "\033[1m"
|
|
12
|
+
RESET = "\033[0m"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def ok(text): return f"{GREEN}{text}{RESET}"
|
|
16
|
+
def bad(text): return f"{RED}{text}{RESET}"
|
|
17
|
+
def warn(text): return f"{YELLOW}{text}{RESET}"
|
|
18
|
+
def info(text): return f"{CYAN}{text}{RESET}"
|
|
19
|
+
def bold(text): return f"{BOLD}{text}{RESET}"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def print_header(title):
|
|
23
|
+
print()
|
|
24
|
+
print(bold(f"=== {title} ==="))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def print_hardware_profile(profile):
|
|
28
|
+
cpu = profile["cpu"]
|
|
29
|
+
ram = profile["ram"]
|
|
30
|
+
print_header("Hardware Profile")
|
|
31
|
+
print(f" OS: {cpu['os']}")
|
|
32
|
+
print(f" CPU: {cpu['processor']}")
|
|
33
|
+
print(f" Cores: {cpu['physical_cores']} physical / {cpu['logical_cores']} logical")
|
|
34
|
+
if cpu["max_freq_ghz"]:
|
|
35
|
+
print(f" Max clock: {cpu['max_freq_ghz']} GHz")
|
|
36
|
+
print(f" RAM total: {ram['total_gb']} GB")
|
|
37
|
+
print(f" RAM available: {ram['available_gb']} GB ({ram['used_percent']}% in use)")
|
|
38
|
+
print(f" Measured bandwidth: {profile['measured_bandwidth_gbps']} GB/s (single-thread, approximate)")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def print_model_result(display_name, context_len, best_result, all_results, available_ram_gb, is_moe=False):
|
|
42
|
+
print_header(f"{display_name} @ {context_len} ctx")
|
|
43
|
+
if best_result is None:
|
|
44
|
+
if len(all_results) == 1:
|
|
45
|
+
# a specific quant was forced via --quant and it doesn't fit
|
|
46
|
+
forced = list(all_results.values())[0]
|
|
47
|
+
print(bad(f" ✗ Does not fit at {forced.quant.upper()} on this machine."))
|
|
48
|
+
print(f" Needs ~{forced.total_ram_needed_gb} GB, you have {available_ram_gb} GB available.")
|
|
49
|
+
print(warn(" Suggestion: try a lower quant level (e.g. --quant q4) or drop --quant to auto-pick the best fit."))
|
|
50
|
+
else:
|
|
51
|
+
print(bad(" ✗ Does not fit at any quant level on this machine."))
|
|
52
|
+
smallest = all_results[list(all_results.keys())[-1]]
|
|
53
|
+
print(f" Even the smallest quant ({smallest.quant.upper()}) needs ~{smallest.total_ram_needed_gb} GB, "
|
|
54
|
+
f"you have {available_ram_gb} GB available.")
|
|
55
|
+
print(warn(" Suggestion: this model likely needs more RAM, or run a smaller model."))
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
print(ok(f" ✓ Runnable at {best_result.quant.upper()} quantization"))
|
|
59
|
+
print(f" Model size (RAM): {best_result.model_size_gb} GB")
|
|
60
|
+
if is_moe:
|
|
61
|
+
print(f" Active per token: {best_result.active_size_gb} GB (MoE - only active experts drive speed)")
|
|
62
|
+
print(f" KV cache: {best_result.kv_cache_gb} GB")
|
|
63
|
+
print(f" Total RAM needed: {best_result.total_ram_needed_gb} GB (you have {available_ram_gb} GB)")
|
|
64
|
+
print(f" Estimated speed: ~{best_result.est_tokens_per_sec} tokens/sec (memory-bandwidth bound)")
|
|
65
|
+
|
|
66
|
+
# Show whether a higher-quality quant is close to fitting too
|
|
67
|
+
quant_order = list(all_results.keys())
|
|
68
|
+
idx = quant_order.index(best_result.quant)
|
|
69
|
+
if idx > 0:
|
|
70
|
+
next_up = all_results[quant_order[idx - 1]]
|
|
71
|
+
print(info(f" Note: {next_up.quant.upper()} needs {next_up.total_ram_needed_gb} GB "
|
|
72
|
+
f"(short by {round(next_up.total_ram_needed_gb - available_ram_gb, 2)} GB) - close but no fit."))
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def print_scan_summary(recommended, marginal, not_recommended):
|
|
76
|
+
print_header("Model Recommendations For This Machine")
|
|
77
|
+
if recommended:
|
|
78
|
+
print(ok(" Comfortably runnable (Q5/Q6/Q8+):"))
|
|
79
|
+
for name, quant, speed in recommended:
|
|
80
|
+
print(f" - {name} [{quant.upper()}] ~{speed} tok/s")
|
|
81
|
+
if marginal:
|
|
82
|
+
print(warn("\n Runnable but tight (Q4 or lower):"))
|
|
83
|
+
for name, quant, speed in marginal:
|
|
84
|
+
print(f" - {name} [{quant.upper()}] ~{speed} tok/s")
|
|
85
|
+
if not_recommended:
|
|
86
|
+
print(bad("\n Not recommended (won't fit even at Q2):"))
|
|
87
|
+
for name in not_recommended:
|
|
88
|
+
print(f" - {name}")
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Hardware detection: CPU info, RAM, and a quick real memory-bandwidth benchmark.
|
|
3
|
+
We benchmark bandwidth instead of guessing from RAM generation, since actual
|
|
4
|
+
achievable bandwidth depends heavily on channel config, which is hard to detect
|
|
5
|
+
reliably cross-platform.
|
|
6
|
+
"""
|
|
7
|
+
import platform
|
|
8
|
+
import time
|
|
9
|
+
|
|
10
|
+
import psutil
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
import numpy as np
|
|
14
|
+
_HAS_NUMPY = True
|
|
15
|
+
except ImportError:
|
|
16
|
+
_HAS_NUMPY = False
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_cpu_info():
|
|
20
|
+
freq = psutil.cpu_freq()
|
|
21
|
+
return {
|
|
22
|
+
"physical_cores": psutil.cpu_count(logical=False) or psutil.cpu_count(logical=True),
|
|
23
|
+
"logical_cores": psutil.cpu_count(logical=True),
|
|
24
|
+
"max_freq_ghz": round(freq.max / 1000, 2) if freq and freq.max else None,
|
|
25
|
+
"current_freq_ghz": round(freq.current / 1000, 2) if freq and freq.current else None,
|
|
26
|
+
"architecture": platform.machine(),
|
|
27
|
+
"processor": platform.processor() or platform.uname().processor or "Unknown",
|
|
28
|
+
"os": f"{platform.system()} {platform.release()}",
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def get_ram_info():
|
|
33
|
+
vm = psutil.virtual_memory()
|
|
34
|
+
return {
|
|
35
|
+
"total_gb": round(vm.total / (1024**3), 2),
|
|
36
|
+
"available_gb": round(vm.available / (1024**3), 2),
|
|
37
|
+
"used_percent": vm.percent,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def benchmark_memory_bandwidth(size_mb: int = 256, repeats: int = 5) -> float:
|
|
42
|
+
"""
|
|
43
|
+
Rough single-threaded memory bandwidth estimate in GB/s, via a large array copy.
|
|
44
|
+
Not a substitute for a proper tool like Intel MLC, but good enough to
|
|
45
|
+
ballpark CPU-inference speed since that's also largely single/few-thread
|
|
46
|
+
memory-bound work in most llama.cpp style runtimes.
|
|
47
|
+
Returns GB/s. Falls back to a conservative default if numpy isn't available.
|
|
48
|
+
"""
|
|
49
|
+
if not _HAS_NUMPY:
|
|
50
|
+
return 15.0 # conservative fallback estimate (typical dual-channel DDR4)
|
|
51
|
+
|
|
52
|
+
n_elements = (size_mb * 1024 * 1024) // 8 # float64 = 8 bytes
|
|
53
|
+
src = np.random.rand(n_elements)
|
|
54
|
+
best = 0.0
|
|
55
|
+
for _ in range(repeats):
|
|
56
|
+
start = time.perf_counter()
|
|
57
|
+
dst = src.copy()
|
|
58
|
+
elapsed = time.perf_counter() - start
|
|
59
|
+
bytes_moved = n_elements * 8 * 2 # read + write
|
|
60
|
+
gbps = (bytes_moved / elapsed) / (1024**3)
|
|
61
|
+
best = max(best, gbps)
|
|
62
|
+
del dst
|
|
63
|
+
return round(best, 2)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def get_full_profile():
|
|
67
|
+
cpu = get_cpu_info()
|
|
68
|
+
ram = get_ram_info()
|
|
69
|
+
bandwidth = benchmark_memory_bandwidth()
|
|
70
|
+
return {
|
|
71
|
+
"cpu": cpu,
|
|
72
|
+
"ram": ram,
|
|
73
|
+
"measured_bandwidth_gbps": bandwidth,
|
|
74
|
+
}
|