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 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
+ }