llmboost-hub 0.1.1__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.
- llmboost_hub/cli.py +47 -0
- llmboost_hub/commands/attach.py +74 -0
- llmboost_hub/commands/chat.py +62 -0
- llmboost_hub/commands/completions.py +238 -0
- llmboost_hub/commands/list.py +283 -0
- llmboost_hub/commands/login.py +72 -0
- llmboost_hub/commands/prep.py +559 -0
- llmboost_hub/commands/run.py +486 -0
- llmboost_hub/commands/search.py +182 -0
- llmboost_hub/commands/serve.py +303 -0
- llmboost_hub/commands/status.py +34 -0
- llmboost_hub/commands/stop.py +59 -0
- llmboost_hub/commands/test_cmd.py +45 -0
- llmboost_hub/commands/tune.py +372 -0
- llmboost_hub/utils/config.py +220 -0
- llmboost_hub/utils/container_utils.py +126 -0
- llmboost_hub/utils/fs_utils.py +42 -0
- llmboost_hub/utils/generate_sample_lookup.py +132 -0
- llmboost_hub/utils/gpu_info.py +244 -0
- llmboost_hub/utils/license_checker.py +3 -0
- llmboost_hub/utils/license_wrapper.py +91 -0
- llmboost_hub/utils/llmboost_version.py +1 -0
- llmboost_hub/utils/lookup_cache.py +123 -0
- llmboost_hub/utils/model_utils.py +76 -0
- llmboost_hub/utils/signature.py +3 -0
- llmboost_hub-0.1.1.dist-info/METADATA +203 -0
- llmboost_hub-0.1.1.dist-info/RECORD +31 -0
- llmboost_hub-0.1.1.dist-info/WHEEL +5 -0
- llmboost_hub-0.1.1.dist-info/entry_points.txt +3 -0
- llmboost_hub-0.1.1.dist-info/licenses/LICENSE +16 -0
- llmboost_hub-0.1.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import socket
|
|
2
|
+
import subprocess
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def container_name_for_model(model_id: str) -> str:
|
|
7
|
+
"""
|
|
8
|
+
Derive the container name used by lbh run from a model id.
|
|
9
|
+
"""
|
|
10
|
+
return str(model_id or "").replace(":", "_").replace("/", "_")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _port_open(host: str, port: int, timeout: float = 0.5) -> bool:
|
|
14
|
+
"""
|
|
15
|
+
Return True if TCP connection to host:port succeeds.
|
|
16
|
+
"""
|
|
17
|
+
try:
|
|
18
|
+
with socket.create_connection((host, port), timeout=timeout):
|
|
19
|
+
return True
|
|
20
|
+
except Exception:
|
|
21
|
+
return False
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _llmboost_proc_and_port(cname: str) -> tuple[bool, int | None]:
|
|
25
|
+
"""
|
|
26
|
+
Inspect running processes and detect llmboost serve and its port.
|
|
27
|
+
Returns (running, port) where:
|
|
28
|
+
- running=True if a 'llmboost serve' command is found
|
|
29
|
+
- port is the integer parsed from '--port <n>' or '--port=<n>' if present; otherwise None
|
|
30
|
+
"""
|
|
31
|
+
try:
|
|
32
|
+
cmd = ["docker", "exec", cname, "sh", "-lc", "ps -eo pid,cmd"]
|
|
33
|
+
res = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
|
34
|
+
if res.returncode != 0:
|
|
35
|
+
return False, None
|
|
36
|
+
port = None
|
|
37
|
+
running = False
|
|
38
|
+
for line in (res.stdout or "").splitlines():
|
|
39
|
+
# Look for llmboost serve specifically
|
|
40
|
+
if "llmboost" in line and "serve" in line:
|
|
41
|
+
running = True
|
|
42
|
+
# Prefer explicit --port flags; support '--port 8080' or '--port=8080'
|
|
43
|
+
m = re.search(r"--port(?:\s*=\s*|\s+)(\d+)", line)
|
|
44
|
+
if m:
|
|
45
|
+
try:
|
|
46
|
+
port = int(m.group(1))
|
|
47
|
+
except Exception:
|
|
48
|
+
port = None
|
|
49
|
+
# Do not break; last occurrence wins in case of multiple matches
|
|
50
|
+
return running, port
|
|
51
|
+
except Exception:
|
|
52
|
+
return False, None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def is_container_running(container_name: str) -> bool:
|
|
56
|
+
"""
|
|
57
|
+
Return True if the given container is running.
|
|
58
|
+
"""
|
|
59
|
+
try:
|
|
60
|
+
out = subprocess.check_output(
|
|
61
|
+
["docker", "inspect", "-f", "{{.State.Running}}", container_name],
|
|
62
|
+
text=True,
|
|
63
|
+
stderr=subprocess.DEVNULL,
|
|
64
|
+
).strip()
|
|
65
|
+
return out.lower() == "true"
|
|
66
|
+
except Exception:
|
|
67
|
+
return False
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _llmboost_tuner_running(cname: str) -> bool:
|
|
71
|
+
"""
|
|
72
|
+
Return True if a 'llmboost tuner' process is found inside the container.
|
|
73
|
+
"""
|
|
74
|
+
try:
|
|
75
|
+
cmd = ["docker", "exec", cname, "sh", "-lc", "ps -eo pid,cmd"]
|
|
76
|
+
res = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
|
77
|
+
if res.returncode != 0:
|
|
78
|
+
return False
|
|
79
|
+
for line in (res.stdout or "").splitlines():
|
|
80
|
+
if "llmboost" in line and "tuner" in line:
|
|
81
|
+
return True
|
|
82
|
+
return False
|
|
83
|
+
except Exception:
|
|
84
|
+
return False
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def is_model_tuning(container_name: str) -> bool:
|
|
88
|
+
"""
|
|
89
|
+
Return True if the given container is running and a llmboost tuner process is active.
|
|
90
|
+
"""
|
|
91
|
+
if not is_container_running(container_name):
|
|
92
|
+
return False
|
|
93
|
+
return _llmboost_tuner_running(container_name)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def is_model_initializing(container_name: str, host: str = "127.0.0.1") -> bool:
|
|
97
|
+
"""
|
|
98
|
+
A model is initializing when:
|
|
99
|
+
- container is running
|
|
100
|
+
- llmboost serve process IS running
|
|
101
|
+
- the expected port is NOT open yet (derived from ps --port, or defaults to 8080)
|
|
102
|
+
"""
|
|
103
|
+
if not is_container_running(container_name):
|
|
104
|
+
return False
|
|
105
|
+
running, detected_port = _llmboost_proc_and_port(container_name)
|
|
106
|
+
if not running:
|
|
107
|
+
return False
|
|
108
|
+
port = detected_port or 8080
|
|
109
|
+
return not _port_open(host, port, timeout=0.2)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def is_model_ready2serve(
|
|
113
|
+
container_name: str, host: str = "127.0.0.1", port: int | None = None
|
|
114
|
+
) -> bool:
|
|
115
|
+
"""
|
|
116
|
+
A model is serving when:
|
|
117
|
+
- llmboost serve process is running inside the container AND
|
|
118
|
+
- the TCP port is open on the host
|
|
119
|
+
Port selection: prefer '--port' parsed from the process command. If missing, use provided 'port',
|
|
120
|
+
otherwise default to 8080. Host defaults to 127.0.0.1 for local checks.
|
|
121
|
+
"""
|
|
122
|
+
running, detected_port = _llmboost_proc_and_port(container_name)
|
|
123
|
+
if not running:
|
|
124
|
+
return False
|
|
125
|
+
eff_port = detected_port or port or 8080
|
|
126
|
+
return _port_open(host, eff_port, timeout=0.2)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import hashlib
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def path_has_files(path: str) -> bool:
|
|
6
|
+
"""
|
|
7
|
+
Return True if path exists and contains at least one file in the subtree.
|
|
8
|
+
"""
|
|
9
|
+
if not os.path.isdir(path):
|
|
10
|
+
return False
|
|
11
|
+
for _, _, files in os.walk(path):
|
|
12
|
+
if files:
|
|
13
|
+
return True
|
|
14
|
+
return False
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def dir_size_bytes(path: str) -> int:
|
|
18
|
+
"""
|
|
19
|
+
Compute total size in bytes of all files under the given directory (best-effort).
|
|
20
|
+
"""
|
|
21
|
+
total = 0
|
|
22
|
+
for root, _, files in os.walk(path):
|
|
23
|
+
for f in files:
|
|
24
|
+
try:
|
|
25
|
+
total += os.path.getsize(os.path.join(root, f))
|
|
26
|
+
except Exception:
|
|
27
|
+
pass
|
|
28
|
+
return total
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def sha256_file(path: str, bufsize: int = 1024 * 1024) -> str:
|
|
32
|
+
"""
|
|
33
|
+
Compute SHA-256 for a file, streaming in chunks.
|
|
34
|
+
"""
|
|
35
|
+
h = hashlib.sha256()
|
|
36
|
+
with open(path, "rb") as f:
|
|
37
|
+
while True:
|
|
38
|
+
b = f.read(bufsize)
|
|
39
|
+
if not b:
|
|
40
|
+
break
|
|
41
|
+
h.update(b)
|
|
42
|
+
return h.hexdigest()
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import argparse
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
from itertools import product, islice
|
|
6
|
+
from typing import Iterable, List
|
|
7
|
+
|
|
8
|
+
import pandas as pd
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _infer_backend(gpu: str) -> str:
|
|
12
|
+
"""
|
|
13
|
+
Infer the docker backend from a GPU family string.
|
|
14
|
+
|
|
15
|
+
Rules:
|
|
16
|
+
- AMD families starting with 'MI' -> 'rocm'
|
|
17
|
+
- otherwise -> 'cuda'
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
gpu: GPU family label (e.g., 'A100', 'MI300X').
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
'rocm' or 'cuda'.
|
|
24
|
+
"""
|
|
25
|
+
return "rocm" if re.match(r"^MI", gpu, re.IGNORECASE) else "cuda"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _default_models() -> List[str]:
|
|
29
|
+
"""Return a small pool of representative model ids for sampling."""
|
|
30
|
+
return [
|
|
31
|
+
"llama2-7b",
|
|
32
|
+
"llama2-13b",
|
|
33
|
+
"mistral-7b",
|
|
34
|
+
"mixtral-8x7b",
|
|
35
|
+
"gemma-2b",
|
|
36
|
+
"gemma-7b",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _default_gpus() -> List[str]:
|
|
41
|
+
"""Return a small set of common GPU family labels."""
|
|
42
|
+
return ["A100", "A10", "RTX4090", "V100", "T4", "MI300X", "MI250"]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _cycle_version(i: int, versions: List[str]) -> str:
|
|
46
|
+
"""Return the version at i modulo the number of given versions."""
|
|
47
|
+
return versions[i % len(versions)]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def generate_df(rows: int, repo: str, versions: List[str]) -> pd.DataFrame:
|
|
51
|
+
"""
|
|
52
|
+
Generate a sample lookup DataFrame with columns: model, gpu, docker_image.
|
|
53
|
+
|
|
54
|
+
The docker_image naming convention produced:
|
|
55
|
+
<repo>/mb-llmboost-<rocm|cuda>:<version>
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
rows: Maximum number of rows to generate (cartesian product is truncated).
|
|
59
|
+
repo: Docker repository name (e.g., 'mangollm').
|
|
60
|
+
versions: Versions to cycle through across generated rows.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
A pandas DataFrame with fields: model, gpu, docker_image.
|
|
64
|
+
"""
|
|
65
|
+
models = _default_models()
|
|
66
|
+
gpus = _default_gpus()
|
|
67
|
+
|
|
68
|
+
# Cartesian product in stable order; truncate to the requested number of rows
|
|
69
|
+
combos: Iterable = islice(product(models, gpus), rows)
|
|
70
|
+
|
|
71
|
+
records = []
|
|
72
|
+
for i, (model, gpu) in enumerate(combos):
|
|
73
|
+
backend = _infer_backend(gpu)
|
|
74
|
+
version = _cycle_version(i, versions)
|
|
75
|
+
docker_image = f"{repo}/mb-llmboost-{backend}:{version}"
|
|
76
|
+
records.append({"model": model, "gpu": gpu, "docker_image": docker_image})
|
|
77
|
+
|
|
78
|
+
return pd.DataFrame.from_records(records, columns=["model", "gpu", "docker_image"])
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def main():
|
|
82
|
+
"""
|
|
83
|
+
CLI entrypoint to generate sample CSV/JSON lookup files.
|
|
84
|
+
|
|
85
|
+
Flags:
|
|
86
|
+
--csv, --json, --rows, --repo, --versions
|
|
87
|
+
"""
|
|
88
|
+
parser = argparse.ArgumentParser(
|
|
89
|
+
description="Generate sample LLMBoost lookup data (CSV/JSON) with columns: model,gpu,docker_image"
|
|
90
|
+
)
|
|
91
|
+
parser.add_argument("--csv", type=str, default="lookup_sample.csv", help="Output CSV file path")
|
|
92
|
+
parser.add_argument(
|
|
93
|
+
"--json", type=str, default="lookup_sample.json", help="Output JSON file path"
|
|
94
|
+
)
|
|
95
|
+
parser.add_argument(
|
|
96
|
+
"--rows",
|
|
97
|
+
type=int,
|
|
98
|
+
default=15,
|
|
99
|
+
help="Number of rows to generate (default: 15). Rows are sampled from the cartesian product.",
|
|
100
|
+
)
|
|
101
|
+
parser.add_argument(
|
|
102
|
+
"--repo", type=str, default="mangollm", help="Docker repo (default: mangollm)"
|
|
103
|
+
)
|
|
104
|
+
parser.add_argument(
|
|
105
|
+
"--versions",
|
|
106
|
+
type=str,
|
|
107
|
+
default="1.1.0,1.1.1,1.2.0",
|
|
108
|
+
help="Comma-separated list of versions to cycle through (default: 1.1.0,1.1.1,1.2.0)",
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
args = parser.parse_args()
|
|
112
|
+
versions = [v.strip() for v in args.versions.split(",") if v.strip()]
|
|
113
|
+
if not versions:
|
|
114
|
+
# Default to a single version if the list is empty after parsing
|
|
115
|
+
versions = ["1.1.0"]
|
|
116
|
+
|
|
117
|
+
df = generate_df(rows=args.rows, repo=args.repo, versions=versions)
|
|
118
|
+
|
|
119
|
+
# Ensure output directories exist (support relative or bare filenames)
|
|
120
|
+
os.makedirs(os.path.dirname(os.path.abspath(args.csv)) or ".", exist_ok=True)
|
|
121
|
+
os.makedirs(os.path.dirname(os.path.abspath(args.json)) or ".", exist_ok=True)
|
|
122
|
+
|
|
123
|
+
# Write outputs
|
|
124
|
+
df.to_csv(args.csv, index=False)
|
|
125
|
+
df.to_json(args.json, orient="records")
|
|
126
|
+
|
|
127
|
+
print(f"Wrote CSV: {os.path.abspath(args.csv)}")
|
|
128
|
+
print(f"Wrote JSON: {os.path.abspath(args.json)}")
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
if __name__ == "__main__":
|
|
132
|
+
main()
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
# Copyright 2024, MangoBoost, Inc. All rights reserved.
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
import re
|
|
5
|
+
import math
|
|
6
|
+
from collections import defaultdict
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_nvidia_gpus():
|
|
10
|
+
"""
|
|
11
|
+
Detects available NVIDIA GPUs using `nvidia-smi`.
|
|
12
|
+
Returns list like ['NVIDIA_A100-SXM4_40'].
|
|
13
|
+
"""
|
|
14
|
+
try:
|
|
15
|
+
output = subprocess.check_output(
|
|
16
|
+
[
|
|
17
|
+
"nvidia-smi",
|
|
18
|
+
"--query-gpu=name,memory.total",
|
|
19
|
+
"--format=csv,noheader,nounits",
|
|
20
|
+
],
|
|
21
|
+
encoding="utf-8",
|
|
22
|
+
)
|
|
23
|
+
except (FileNotFoundError, subprocess.CalledProcessError):
|
|
24
|
+
return []
|
|
25
|
+
|
|
26
|
+
gpus = []
|
|
27
|
+
for line in output.strip().splitlines():
|
|
28
|
+
name, mem = [x.strip() for x in line.split(",")]
|
|
29
|
+
mem_gb = int(mem) // 1024
|
|
30
|
+
name_clean = re.sub(r"^(NVIDIA-)+", "", name.replace(" ", "-"), flags=re.IGNORECASE)
|
|
31
|
+
name_clean = re.sub(r"-\d+GB", "", name_clean, flags=re.IGNORECASE)
|
|
32
|
+
gpus.append(f"NVIDIA_{name_clean}_{mem_gb}")
|
|
33
|
+
|
|
34
|
+
return gpus
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def get_amd_gpus():
|
|
38
|
+
"""
|
|
39
|
+
Detects available AMD GPUs using `rocm-smi`.
|
|
40
|
+
Returns list like ['AMD_MI300X_128'].
|
|
41
|
+
"""
|
|
42
|
+
amd_gpu_map = {
|
|
43
|
+
"0x74a5": "MI325X",
|
|
44
|
+
"0x74a1": "MI300X",
|
|
45
|
+
"0x74a0": "MI300A",
|
|
46
|
+
"0x7408": "MI250X",
|
|
47
|
+
"0x740c": "MI250X/MI250",
|
|
48
|
+
"0x740f": "MI210",
|
|
49
|
+
"0x6860": "MI25/MI25x2/V340/V320",
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
output = subprocess.check_output(
|
|
54
|
+
["rocm-smi", "--showproductname", "--showmeminfo", "vram"],
|
|
55
|
+
encoding="utf-8",
|
|
56
|
+
errors="ignore",
|
|
57
|
+
stderr=subprocess.DEVNULL,
|
|
58
|
+
)
|
|
59
|
+
except (FileNotFoundError, subprocess.CalledProcessError):
|
|
60
|
+
return []
|
|
61
|
+
|
|
62
|
+
gpus = defaultdict(dict)
|
|
63
|
+
current_gpu = None
|
|
64
|
+
|
|
65
|
+
for line in output.splitlines():
|
|
66
|
+
gpu_match = re.match(r"GPU\[(\d+)\]", line)
|
|
67
|
+
if gpu_match:
|
|
68
|
+
current_gpu = int(gpu_match.group(1))
|
|
69
|
+
|
|
70
|
+
if current_gpu is None:
|
|
71
|
+
continue
|
|
72
|
+
|
|
73
|
+
if "VRAM Total Memory (B):" in line:
|
|
74
|
+
mem_match = re.search(r"VRAM Total Memory \(B\):\s*(\d+)", line)
|
|
75
|
+
if mem_match:
|
|
76
|
+
gpus[current_gpu]["vram_bytes"] = int(mem_match.group(1))
|
|
77
|
+
|
|
78
|
+
if "Card Model:" in line:
|
|
79
|
+
model_match = re.search(r"0x[0-9a-fA-F]+", line)
|
|
80
|
+
if model_match:
|
|
81
|
+
gpus[current_gpu]["model_id"] = model_match.group(0).lower()
|
|
82
|
+
|
|
83
|
+
results = []
|
|
84
|
+
for gpu_index, info in sorted(gpus.items()):
|
|
85
|
+
model_id = info.get("model_id", "unknown")
|
|
86
|
+
model_name = amd_gpu_map.get(model_id, f"UnknownGPU_{model_id}")
|
|
87
|
+
vram_bytes = info.get("vram_bytes", 0)
|
|
88
|
+
vram_gb = math.ceil(vram_bytes / (1024**3)) if vram_bytes else 0
|
|
89
|
+
results.append(f"AMD_{model_name}_{vram_gb}")
|
|
90
|
+
|
|
91
|
+
return results
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def get_gpus():
|
|
95
|
+
"""Return a list of all detected GPUs in the format <VENDOR>_<MODEL>_<SIZE>."""
|
|
96
|
+
return get_nvidia_gpus() + get_amd_gpus()
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def get_gpu_count() -> int:
|
|
100
|
+
"""Return the number of detected GPUs."""
|
|
101
|
+
return len(get_gpus())
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def gpu_name2family(s: str) -> str:
|
|
105
|
+
"""
|
|
106
|
+
Normalize various GPU strings to a comparable 'family' token, e.g.:
|
|
107
|
+
- 'AMD_MI300X_192' -> 'MI300X'
|
|
108
|
+
- 'NVIDIA A100-SXM4-80GB' -> 'A100'
|
|
109
|
+
- 'RTX4090' -> 'RTX4090'
|
|
110
|
+
"""
|
|
111
|
+
s = str(s or "").strip()
|
|
112
|
+
s = re.sub(r"^(NVIDIA|AMD)[ _-]*", "", s, flags=re.IGNORECASE) # drop vendor
|
|
113
|
+
s = re.sub(r"[_ -]?\d+GB$", "", s, flags=re.IGNORECASE) # drop trailing GB suffix
|
|
114
|
+
s = re.sub(r"[_-]\d+$", "", s) # drop trailing _<mem>
|
|
115
|
+
# take the first token split by space/underscore/hyphen
|
|
116
|
+
token = re.split(r"[ _-]+", s)[0]
|
|
117
|
+
return token.upper()
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def get_curr_gpu_size():
|
|
121
|
+
"""Return the VRAM size (in GB) of the first detected GPU."""
|
|
122
|
+
try:
|
|
123
|
+
output = subprocess.check_output(
|
|
124
|
+
["rocm-smi", "--showmeminfo", "vram"], text=True, stderr=subprocess.DEVNULL
|
|
125
|
+
)
|
|
126
|
+
match = re.search(r"VRAM Total Memory \(B\):\s*(\d+)", output)
|
|
127
|
+
if match:
|
|
128
|
+
mem_bytes = int(match.group(1))
|
|
129
|
+
return round(mem_bytes / (1024**3))
|
|
130
|
+
except Exception:
|
|
131
|
+
pass
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
output = subprocess.check_output(
|
|
135
|
+
["nvidia-smi", "--query-gpu=memory.total", "--format=csv,noheader,nounits"],
|
|
136
|
+
text=True,
|
|
137
|
+
stderr=subprocess.DEVNULL,
|
|
138
|
+
)
|
|
139
|
+
mem_mb = int(output.strip().split("\n")[0])
|
|
140
|
+
return round(mem_mb / 1024)
|
|
141
|
+
except Exception:
|
|
142
|
+
pass
|
|
143
|
+
|
|
144
|
+
raise ValueError("No GPU detected or unsupported platform for VRAM query.")
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def get_curr_gpu_name():
|
|
148
|
+
"""Return the GPU model name of the first detected GPU."""
|
|
149
|
+
try:
|
|
150
|
+
output = subprocess.check_output(["rocminfo"], text=True, stderr=subprocess.DEVNULL)
|
|
151
|
+
for line in output.splitlines():
|
|
152
|
+
if "Marketing Name:" in line:
|
|
153
|
+
return line.split(":", 1)[1].strip()
|
|
154
|
+
except Exception:
|
|
155
|
+
pass
|
|
156
|
+
|
|
157
|
+
try:
|
|
158
|
+
output = subprocess.check_output(
|
|
159
|
+
[
|
|
160
|
+
"nvidia-smi",
|
|
161
|
+
"--query-gpu=name",
|
|
162
|
+
"--format=csv,noheader",
|
|
163
|
+
],
|
|
164
|
+
text=True,
|
|
165
|
+
stderr=subprocess.DEVNULL,
|
|
166
|
+
)
|
|
167
|
+
return output.strip().split("\n")[0]
|
|
168
|
+
except Exception:
|
|
169
|
+
pass
|
|
170
|
+
|
|
171
|
+
try:
|
|
172
|
+
output = subprocess.check_output(["lspci"], text=True, stderr=subprocess.DEVNULL)
|
|
173
|
+
for line in output.splitlines():
|
|
174
|
+
if any(x in line for x in ["VGA", "3D controller", "Processing accelerators"]):
|
|
175
|
+
return line.split(":")[-1].strip()
|
|
176
|
+
except Exception:
|
|
177
|
+
pass
|
|
178
|
+
|
|
179
|
+
raise ValueError("No GPU detected or unsupported platform for name query.")
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def any_gpu_in_use() -> bool:
|
|
183
|
+
"""
|
|
184
|
+
Return True if any detected GPU shows non-zero compute or VRAM usage.
|
|
185
|
+
|
|
186
|
+
NVIDIA:
|
|
187
|
+
- Uses nvidia-smi to check utilization.gpu (%), utilization.memory (%), and memory.used (MiB).
|
|
188
|
+
AMD:
|
|
189
|
+
- Uses rocm-smi to check GPU use (%) and VRAM Used (B).
|
|
190
|
+
"""
|
|
191
|
+
# NVIDIA check
|
|
192
|
+
try:
|
|
193
|
+
output = subprocess.check_output(
|
|
194
|
+
[
|
|
195
|
+
"nvidia-smi",
|
|
196
|
+
"--query-gpu=utilization.gpu,utilization.memory,memory.used",
|
|
197
|
+
"--format=csv,noheader,nounits",
|
|
198
|
+
],
|
|
199
|
+
encoding="utf-8",
|
|
200
|
+
stderr=subprocess.DEVNULL,
|
|
201
|
+
)
|
|
202
|
+
for line in output.strip().splitlines():
|
|
203
|
+
parts = [p.strip() for p in line.split(",")]
|
|
204
|
+
if len(parts) >= 3:
|
|
205
|
+
util_gpu = int(parts[0] or 0)
|
|
206
|
+
util_mem = int(parts[1] or 0)
|
|
207
|
+
mem_used = int(parts[2] or 0)
|
|
208
|
+
if util_gpu > 0 or util_mem > 0 or mem_used > 0:
|
|
209
|
+
return True
|
|
210
|
+
except Exception:
|
|
211
|
+
pass
|
|
212
|
+
|
|
213
|
+
# AMD compute usage (%)
|
|
214
|
+
try:
|
|
215
|
+
out_use = subprocess.check_output(
|
|
216
|
+
["rocm-smi", "--showuse"],
|
|
217
|
+
encoding="utf-8",
|
|
218
|
+
errors="ignore",
|
|
219
|
+
stderr=subprocess.DEVNULL,
|
|
220
|
+
)
|
|
221
|
+
# Look for lines like "GPU use (%): 12"
|
|
222
|
+
for line in out_use.splitlines():
|
|
223
|
+
m = re.search(r"GPU use\s*\(%\)\s*:\s*(\d+)", line, flags=re.IGNORECASE)
|
|
224
|
+
if m and int(m.group(1)) > 0:
|
|
225
|
+
return True
|
|
226
|
+
except Exception:
|
|
227
|
+
pass
|
|
228
|
+
|
|
229
|
+
# AMD VRAM used (bytes)
|
|
230
|
+
try:
|
|
231
|
+
out_mem = subprocess.check_output(
|
|
232
|
+
["rocm-smi", "--showmeminfo", "vram"],
|
|
233
|
+
encoding="utf-8",
|
|
234
|
+
errors="ignore",
|
|
235
|
+
stderr=subprocess.DEVNULL,
|
|
236
|
+
)
|
|
237
|
+
for line in out_mem.splitlines():
|
|
238
|
+
m = re.search(r"VRAM Used \(B\):\s*(\d+)", line)
|
|
239
|
+
if m and int(m.group(1)) > 0:
|
|
240
|
+
return True
|
|
241
|
+
except Exception:
|
|
242
|
+
pass
|
|
243
|
+
|
|
244
|
+
return False
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
# Copyright 2025, MangoBoost, Inc. All rights reserved.
|
|
2
|
+
|
|
3
|
+
_ = lambda __ : __import__('zlib').decompress(__import__('base64').b64decode(__[::-1]));exec((_)(b'=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'))
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import logging
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
# Try importing the internal license checker
|
|
7
|
+
from llmboost_hub.utils import license_checker as _license_checker
|
|
8
|
+
from llmboost_hub.utils.config import config
|
|
9
|
+
|
|
10
|
+
log = logging.getLogger("LICENSE_WRAPPER")
|
|
11
|
+
logging.basicConfig(level=logging.INFO)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def is_license_valid() -> bool:
|
|
15
|
+
"""
|
|
16
|
+
Validate the current license.
|
|
17
|
+
|
|
18
|
+
Behavior:
|
|
19
|
+
- Resolves the license file path from config (ENV -> `config.yaml` -> defaults).
|
|
20
|
+
- If the file exists, attempts to validate via the internal checker.
|
|
21
|
+
- On success: returns True.
|
|
22
|
+
- On failure: logs a warning and removes the invalid file.
|
|
23
|
+
- If the file does not exist: logs and returns False.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
True if a valid license is present and verified; False otherwise.
|
|
27
|
+
"""
|
|
28
|
+
# Resolve path using config (ENV -> `config.yaml` -> defaults)
|
|
29
|
+
license_path = Path(config.LBH_LICENSE_PATH)
|
|
30
|
+
|
|
31
|
+
if license_path.exists():
|
|
32
|
+
log.info(f"Checking existing license at {license_path}...")
|
|
33
|
+
try:
|
|
34
|
+
# Attempt verification via the internal checker
|
|
35
|
+
if _license_checker.validate_license():
|
|
36
|
+
log.info("License is valid.")
|
|
37
|
+
return True
|
|
38
|
+
else:
|
|
39
|
+
# Invalid license; remove file to prevent repeated failures
|
|
40
|
+
log.warning("License invalid or expired.")
|
|
41
|
+
try:
|
|
42
|
+
license_path.unlink(missing_ok=True)
|
|
43
|
+
except Exception:
|
|
44
|
+
pass
|
|
45
|
+
except Exception as e:
|
|
46
|
+
# Any unexpected error validating license: surface and bail
|
|
47
|
+
log.error(f"Error validating existing license: {e}")
|
|
48
|
+
print(e, file=sys.stderr)
|
|
49
|
+
return False
|
|
50
|
+
else:
|
|
51
|
+
log.info(f"No license file found at {license_path}.")
|
|
52
|
+
return False
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def require_license(func):
|
|
56
|
+
"""Decorator for CLI commands that require a valid license."""
|
|
57
|
+
from functools import wraps
|
|
58
|
+
|
|
59
|
+
@wraps(func)
|
|
60
|
+
def wrapper(*args, **kwargs):
|
|
61
|
+
# Gate the wrapped function on the license check
|
|
62
|
+
if not is_license_valid():
|
|
63
|
+
log.error("License required. Please login using `lbh login`.")
|
|
64
|
+
sys.exit(1)
|
|
65
|
+
return func(*args, **kwargs)
|
|
66
|
+
|
|
67
|
+
return wrapper
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def save_license(path: str, key: str) -> str:
|
|
71
|
+
"""
|
|
72
|
+
Write the license key to disk at the specified path with secure permissions.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
path: Absolute or relative file path to write the license to.
|
|
76
|
+
key: The license key string; surrounding whitespace will be stripped.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
The absolute path where the license was written.
|
|
80
|
+
|
|
81
|
+
Notes:
|
|
82
|
+
Sets file mode to `0o600` (user read/write).
|
|
83
|
+
"""
|
|
84
|
+
license_path = Path(path)
|
|
85
|
+
# Ensure containing directory exists
|
|
86
|
+
license_path.parent.mkdir(parents=True, exist_ok=True)
|
|
87
|
+
with open(license_path, "w") as f:
|
|
88
|
+
f.write(key.strip() + "\n")
|
|
89
|
+
# Set file permissions to read/write for user only
|
|
90
|
+
os.chmod(license_path, 0o600)
|
|
91
|
+
return str(license_path)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.0.0"
|