nexuscompute-sdk 2.0.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.
- nexus_sdk/__init__.py +8 -0
- nexus_sdk/cli.py +165 -0
- nexus_sdk/client.py +137 -0
- nexus_sdk/provider.py +214 -0
- nexuscompute_sdk-2.0.0.dist-info/METADATA +16 -0
- nexuscompute_sdk-2.0.0.dist-info/RECORD +9 -0
- nexuscompute_sdk-2.0.0.dist-info/WHEEL +5 -0
- nexuscompute_sdk-2.0.0.dist-info/entry_points.txt +3 -0
- nexuscompute_sdk-2.0.0.dist-info/top_level.txt +1 -0
nexus_sdk/__init__.py
ADDED
nexus_sdk/cli.py
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import argparse
|
|
3
|
+
import json
|
|
4
|
+
from .client import Renter
|
|
5
|
+
from .provider import Provider, detect_hardware
|
|
6
|
+
|
|
7
|
+
def renter_cli():
|
|
8
|
+
parser = argparse.ArgumentParser(description="NexusCompute Renter CLI Client")
|
|
9
|
+
parser.add_argument("--api-key", help="API key for authentication")
|
|
10
|
+
parser.add_argument("--token", help="Bearer token for authentication")
|
|
11
|
+
parser.add_argument("--api-url", default="http://localhost:3002/api/v1", help="Nexus API Endpoint URL")
|
|
12
|
+
|
|
13
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
14
|
+
|
|
15
|
+
# signup
|
|
16
|
+
signup_parser = subparsers.add_parser("signup", help="Create a marketplace account")
|
|
17
|
+
signup_parser.add_argument("-e", "--email", required=True, help="Account email")
|
|
18
|
+
signup_parser.add_argument("-p", "--password", required=True, help="Account password")
|
|
19
|
+
|
|
20
|
+
# login
|
|
21
|
+
login_parser = subparsers.add_parser("login", help="Authenticate and get token")
|
|
22
|
+
login_parser.add_argument("-e", "--email", required=True, help="Account email")
|
|
23
|
+
login_parser.add_argument("-p", "--password", required=True, help="Account password")
|
|
24
|
+
|
|
25
|
+
# pay
|
|
26
|
+
pay_parser = subparsers.add_parser("pay", help="Add USD balance via Square")
|
|
27
|
+
pay_parser.add_argument("-a", "--amount", default="5.00", help="Amount in USD")
|
|
28
|
+
pay_parser.add_argument("-n", "--nonce", default="cnon:card-nonce-ok", help="Square nonce")
|
|
29
|
+
|
|
30
|
+
# train
|
|
31
|
+
train_parser = subparsers.add_parser("train", help="Submit a training job")
|
|
32
|
+
train_parser.add_argument("-m", "--model", default="gemma-4-e2b-it", help="Model name")
|
|
33
|
+
train_parser.add_argument("-d", "--dataset", default="synthetic", help="Dataset name")
|
|
34
|
+
train_parser.add_argument("-e", "--epochs", default="3", help="Training epochs")
|
|
35
|
+
train_parser.add_argument("-c", "--compute", default="local", help="local or colab compute")
|
|
36
|
+
train_parser.add_argument("-p", "--prompt", help="Natural language training prompt")
|
|
37
|
+
|
|
38
|
+
# status
|
|
39
|
+
status_parser = subparsers.add_parser("status", help="Check wallet, jobs, or job status")
|
|
40
|
+
status_parser.add_argument("-j", "--job", help="Specific job ID")
|
|
41
|
+
|
|
42
|
+
# contribute
|
|
43
|
+
contribute_parser = subparsers.add_parser("contribute", help="Open an agent contribution task")
|
|
44
|
+
contribute_parser.add_argument("-a", "--agent", required=True, help="Partner agent name/id")
|
|
45
|
+
contribute_parser.add_argument("-t", "--task", required=True, help="Contribution task description")
|
|
46
|
+
contribute_parser.add_argument("-j", "--job", help="Linked training job ID")
|
|
47
|
+
|
|
48
|
+
# list-hardware
|
|
49
|
+
subparsers.add_parser("list-hardware", help="List all active providers / compute hardware")
|
|
50
|
+
|
|
51
|
+
# get-wallet
|
|
52
|
+
subparsers.add_parser("get-wallet", help="Check renter wallet token balance")
|
|
53
|
+
|
|
54
|
+
# benchmark
|
|
55
|
+
benchmark_parser = subparsers.add_parser("benchmark", help="Run benchmark on target hardware")
|
|
56
|
+
benchmark_parser.add_argument("-H", "--hardware", required=True, help="Target hardware node ID")
|
|
57
|
+
benchmark_parser.add_argument("-s", "--size", default="512", help="Matrix size")
|
|
58
|
+
benchmark_parser.add_argument("-i", "--iterations", default="3", help="Number of iterations")
|
|
59
|
+
benchmark_parser.add_argument("-b", "--backend", default="cpu", help="Execution backend")
|
|
60
|
+
|
|
61
|
+
# gemm
|
|
62
|
+
gemm_parser = subparsers.add_parser("gemm", help="Submit General Matrix Multiply operation")
|
|
63
|
+
gemm_parser.add_argument("-H", "--hardware", required=True, help="Target hardware node ID")
|
|
64
|
+
gemm_parser.add_argument("-a", "--matrix-a", required=True, help="Matrix A (JSON nested array, e.g. [[1,2]])")
|
|
65
|
+
gemm_parser.add_argument("-b", "--matrix-b", required=True, help="Matrix B (JSON nested array, e.g. [[3,4]])")
|
|
66
|
+
gemm_parser.add_argument("-t", "--transpose-a", action="store_true", help="Transpose matrix A")
|
|
67
|
+
gemm_parser.add_argument("-u", "--transpose-b", action="store_true", help="Transpose matrix B")
|
|
68
|
+
gemm_parser.add_argument("-k", "--backend", default="cpu", help="Execution backend")
|
|
69
|
+
|
|
70
|
+
args = parser.parse_args()
|
|
71
|
+
|
|
72
|
+
client = Renter(api_key=args.api_key, token=args.token, base_url=args.api_url)
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
if args.command == "signup":
|
|
76
|
+
res = client.register(args.email, args.password)
|
|
77
|
+
print(f"Signed up successfully as {res.get('user', {}).get('email')}")
|
|
78
|
+
print(json.dumps(res, indent=2))
|
|
79
|
+
elif args.command == "login":
|
|
80
|
+
res = client.login(args.email, args.password)
|
|
81
|
+
print(f"Logged in successfully. Session token saved.")
|
|
82
|
+
print(json.dumps(res, indent=2))
|
|
83
|
+
elif args.command == "pay":
|
|
84
|
+
res = client.pay(args.amount, args.nonce)
|
|
85
|
+
print(json.dumps(res, indent=2))
|
|
86
|
+
elif args.command == "train":
|
|
87
|
+
payload = {
|
|
88
|
+
"model": args.model,
|
|
89
|
+
"dataset": args.dataset,
|
|
90
|
+
"epochs": int(args.epochs),
|
|
91
|
+
"computeTarget": args.compute,
|
|
92
|
+
"prompt": args.prompt
|
|
93
|
+
}
|
|
94
|
+
res = client.train(payload)
|
|
95
|
+
print(json.dumps(res, indent=2))
|
|
96
|
+
elif args.command == "status":
|
|
97
|
+
if args.job:
|
|
98
|
+
res = client.get_job(args.job)
|
|
99
|
+
else:
|
|
100
|
+
res = client.list_jobs()
|
|
101
|
+
print(json.dumps(res, indent=2))
|
|
102
|
+
elif args.command == "contribute":
|
|
103
|
+
payload = {
|
|
104
|
+
"agentId": args.agent,
|
|
105
|
+
"taskDescription": args.task
|
|
106
|
+
}
|
|
107
|
+
if args.job:
|
|
108
|
+
payload["linkedJobId"] = args.job
|
|
109
|
+
res = client.contribute(payload)
|
|
110
|
+
print(json.dumps(res, indent=2))
|
|
111
|
+
elif args.command == "list-hardware":
|
|
112
|
+
res = client.list_hardware()
|
|
113
|
+
print(json.dumps(res, indent=2))
|
|
114
|
+
elif args.command == "get-wallet":
|
|
115
|
+
res = client.get_wallet()
|
|
116
|
+
print(json.dumps(res, indent=2))
|
|
117
|
+
elif args.command == "benchmark":
|
|
118
|
+
payload = {
|
|
119
|
+
"hardwareId": args.hardware,
|
|
120
|
+
"size": int(args.size),
|
|
121
|
+
"iterations": int(args.iterations),
|
|
122
|
+
"backend": args.backend
|
|
123
|
+
}
|
|
124
|
+
res = client.quote_train(payload) if False else client._request("POST", "/ops/benchmark", json=payload)
|
|
125
|
+
print(json.dumps(res, indent=2))
|
|
126
|
+
elif args.command == "gemm":
|
|
127
|
+
payload = {
|
|
128
|
+
"hardwareId": args.hardware,
|
|
129
|
+
"a": json.loads(args.matrix_a),
|
|
130
|
+
"b": json.loads(args.matrix_b),
|
|
131
|
+
"transposeA": args.transpose_a,
|
|
132
|
+
"transposeB": args.transpose_b,
|
|
133
|
+
"backend": args.backend
|
|
134
|
+
}
|
|
135
|
+
res = client._request("POST", "/ops/gemm", json=payload)
|
|
136
|
+
print(json.dumps(res, indent=2))
|
|
137
|
+
except Exception as e:
|
|
138
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
139
|
+
sys.exit(1)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def provider_cli():
|
|
143
|
+
parser = argparse.ArgumentParser(description="NexusCompute Provider Agent Daemon")
|
|
144
|
+
parser.add_argument("--token", help="Hardware node connection token")
|
|
145
|
+
parser.add_argument("--api-url", default="http://localhost:3002/api/v1", help="Nexus API Endpoint URL")
|
|
146
|
+
parser.add_argument("--detect", action="store_true", help="Detect system specifications and print them as JSON")
|
|
147
|
+
|
|
148
|
+
args = parser.parse_args()
|
|
149
|
+
|
|
150
|
+
if args.detect:
|
|
151
|
+
hw = detect_hardware()
|
|
152
|
+
print(json.dumps(hw, indent=2))
|
|
153
|
+
sys.exit(0)
|
|
154
|
+
|
|
155
|
+
if not args.token:
|
|
156
|
+
parser.print_help()
|
|
157
|
+
print("\nError: --token is required to run the provider agent.", file=sys.stderr)
|
|
158
|
+
sys.exit(1)
|
|
159
|
+
|
|
160
|
+
try:
|
|
161
|
+
agent = Provider(token=args.token, api_url=args.api_url)
|
|
162
|
+
agent.start()
|
|
163
|
+
except Exception as e:
|
|
164
|
+
print(f"Fatal: {e}", file=sys.stderr)
|
|
165
|
+
sys.exit(1)
|
nexus_sdk/client.py
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
import time
|
|
3
|
+
|
|
4
|
+
class Renter:
|
|
5
|
+
def __init__(self, api_key=None, token=None, base_url="http://localhost:3002/api/v1"):
|
|
6
|
+
self.api_key = api_key
|
|
7
|
+
self.token = token
|
|
8
|
+
self.base_url = base_url.rstrip("/")
|
|
9
|
+
|
|
10
|
+
def _headers(self):
|
|
11
|
+
headers = {"Content-Type": "application/json"}
|
|
12
|
+
if self.api_key:
|
|
13
|
+
headers["X-API-Key"] = self.api_key
|
|
14
|
+
elif self.token:
|
|
15
|
+
headers["Authorization"] = f"Bearer {self.token}"
|
|
16
|
+
return headers
|
|
17
|
+
|
|
18
|
+
def _request(self, method, path, json=None, params=None):
|
|
19
|
+
url = f"{self.base_url}{path}"
|
|
20
|
+
res = requests.request(method, url, json=json, params=params, headers=self._headers())
|
|
21
|
+
if not res.ok:
|
|
22
|
+
try:
|
|
23
|
+
err_msg = res.json().get("error", f"HTTP {res.status_code}")
|
|
24
|
+
except Exception:
|
|
25
|
+
err_msg = f"HTTP {res.status_code}"
|
|
26
|
+
raise RuntimeError(err_msg)
|
|
27
|
+
try:
|
|
28
|
+
return res.json()
|
|
29
|
+
except Exception:
|
|
30
|
+
return {}
|
|
31
|
+
|
|
32
|
+
def login(self, email, password):
|
|
33
|
+
data = self._request("POST", "/auth/login", json={"email": email, "password": password})
|
|
34
|
+
self.token = data.get("token")
|
|
35
|
+
return data
|
|
36
|
+
|
|
37
|
+
def register(self, email, password):
|
|
38
|
+
data = self._request("POST", "/auth/register", json={"email": email, "password": password})
|
|
39
|
+
self.token = data.get("token")
|
|
40
|
+
return data
|
|
41
|
+
|
|
42
|
+
def create_api_key(self, name):
|
|
43
|
+
return self._request("POST", "/auth/api-keys", json={"name": name})
|
|
44
|
+
|
|
45
|
+
def list_hardware(self, **params):
|
|
46
|
+
return self._request("GET", "/hardware", params=params)
|
|
47
|
+
|
|
48
|
+
def get_hardware(self, hardware_id):
|
|
49
|
+
return self._request("GET", f"/hardware/{hardware_id}")
|
|
50
|
+
|
|
51
|
+
def get_priority_tiers(self):
|
|
52
|
+
return self._request("GET", "/network/priority-tiers")
|
|
53
|
+
|
|
54
|
+
def get_exchange_rate(self):
|
|
55
|
+
return self._request("GET", "/exchange/rate")
|
|
56
|
+
|
|
57
|
+
def get_wallet(self):
|
|
58
|
+
return self._request("GET", "/exchange/wallet")
|
|
59
|
+
|
|
60
|
+
def exchange_usd_to_nexus(self, usd_amount):
|
|
61
|
+
return self._request("POST", "/exchange/usd-to-nexus", json={"usdAmount": usd_amount})
|
|
62
|
+
|
|
63
|
+
def get_exchange_history(self, limit=20):
|
|
64
|
+
return self._request("GET", "/exchange/history", params={"limit": limit})
|
|
65
|
+
|
|
66
|
+
def submit_job(self, target, operation, params=None, backend=None, priority=None, priority_tier=None, scheduled_at=None):
|
|
67
|
+
if params is None:
|
|
68
|
+
params = {}
|
|
69
|
+
if isinstance(target, dict):
|
|
70
|
+
hardware_id = target.get("hardwareId")
|
|
71
|
+
priority_tier = target.get("priorityTier", priority_tier) or "standard"
|
|
72
|
+
else:
|
|
73
|
+
hardware_id = target
|
|
74
|
+
priority_tier = priority_tier or "standard"
|
|
75
|
+
|
|
76
|
+
if not hardware_id:
|
|
77
|
+
raise ValueError("hardwareId is required")
|
|
78
|
+
|
|
79
|
+
body = {
|
|
80
|
+
"hardwareId": hardware_id,
|
|
81
|
+
"priorityTier": priority_tier,
|
|
82
|
+
"operation": operation,
|
|
83
|
+
"params": params
|
|
84
|
+
}
|
|
85
|
+
if backend:
|
|
86
|
+
body["backend"] = backend
|
|
87
|
+
if priority:
|
|
88
|
+
body["priority"] = priority
|
|
89
|
+
if scheduled_at:
|
|
90
|
+
body["scheduledAt"] = scheduled_at
|
|
91
|
+
|
|
92
|
+
return self._request("POST", "/jobs", json=body)
|
|
93
|
+
|
|
94
|
+
def get_job(self, job_id):
|
|
95
|
+
return self._request("GET", f"/jobs/{job_id}")
|
|
96
|
+
|
|
97
|
+
def wait_for_job(self, job_id, max_ms=60000, poll_ms=250):
|
|
98
|
+
start = time.time()
|
|
99
|
+
while (time.time() - start) * 1000 < max_ms:
|
|
100
|
+
job = self.get_job(job_id)
|
|
101
|
+
if job.get("status") == "completed":
|
|
102
|
+
return job
|
|
103
|
+
if job.get("status") == "failed":
|
|
104
|
+
raise RuntimeError(job.get("error", "Job failed"))
|
|
105
|
+
time.sleep(poll_ms / 1000.0)
|
|
106
|
+
raise TimeoutError("Timed out waiting for job completion")
|
|
107
|
+
|
|
108
|
+
def get_network_stats(self):
|
|
109
|
+
return self._request("GET", "/network/stats")
|
|
110
|
+
|
|
111
|
+
def get_pricing(self):
|
|
112
|
+
return self._request("GET", "/network/pricing")
|
|
113
|
+
|
|
114
|
+
def get_backends(self):
|
|
115
|
+
return self._request("GET", "/ops/backends")
|
|
116
|
+
|
|
117
|
+
def pay(self, amount_usd, source_id):
|
|
118
|
+
return self._request("POST", "/payments/charge", json={
|
|
119
|
+
"amountUsd": float(amount_usd),
|
|
120
|
+
"sourceId": source_id
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
def train(self, payload):
|
|
124
|
+
return self._request("POST", "/marketplace/train", json=payload)
|
|
125
|
+
|
|
126
|
+
def quote_train(self, payload):
|
|
127
|
+
return self._request("POST", "/marketplace/train/quote", json=payload)
|
|
128
|
+
|
|
129
|
+
def list_jobs(self):
|
|
130
|
+
return self._request("GET", "/marketplace/train")
|
|
131
|
+
|
|
132
|
+
def contribute(self, payload):
|
|
133
|
+
return self._request("POST", "/marketplace/contribute", json=payload)
|
|
134
|
+
|
|
135
|
+
def list_contributions(self):
|
|
136
|
+
return self._request("GET", "/marketplace/contribute")
|
|
137
|
+
|
nexus_sdk/provider.py
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import time
|
|
4
|
+
import shutil
|
|
5
|
+
import socket
|
|
6
|
+
import platform
|
|
7
|
+
import subprocess
|
|
8
|
+
import requests
|
|
9
|
+
import psutil
|
|
10
|
+
|
|
11
|
+
def detect_hardware():
|
|
12
|
+
# 1. Total CPU cores and system memory
|
|
13
|
+
total_mem_gb = round(psutil.virtual_memory().total / (1024 ** 3), 1)
|
|
14
|
+
free_mem_gb = round(psutil.virtual_memory().available / (1024 ** 3), 1)
|
|
15
|
+
cpu_count = psutil.cpu_count(logical=False) or 1
|
|
16
|
+
|
|
17
|
+
hardware = {
|
|
18
|
+
"type": "cpu",
|
|
19
|
+
"model": platform.processor() or "Generic CPU",
|
|
20
|
+
"cores": cpu_count,
|
|
21
|
+
"memory": {
|
|
22
|
+
"totalGb": total_mem_gb,
|
|
23
|
+
"freeGb": free_mem_gb
|
|
24
|
+
},
|
|
25
|
+
"gpus": []
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
# 2. Try to detect NVIDIA GPUs via nvidia-smi
|
|
29
|
+
nvidia_smi = shutil.which("nvidia-smi")
|
|
30
|
+
if nvidia_smi:
|
|
31
|
+
try:
|
|
32
|
+
output = subprocess.check_output([
|
|
33
|
+
nvidia_smi,
|
|
34
|
+
"--query-gpu=name,memory.total,memory.free,utilization.gpu,utilization.memory,temperature.gpu,power.draw",
|
|
35
|
+
"--format=csv,noheader,nounits"
|
|
36
|
+
], encoding="utf-8")
|
|
37
|
+
|
|
38
|
+
gpus = []
|
|
39
|
+
for line in output.strip().splitlines():
|
|
40
|
+
parts = [p.strip() for p in line.split(",")]
|
|
41
|
+
if len(parts) >= 7:
|
|
42
|
+
gpu_name = parts[0]
|
|
43
|
+
total_vram = float(parts[1])
|
|
44
|
+
free_vram = float(parts[2])
|
|
45
|
+
gpu_util = float(parts[3])
|
|
46
|
+
mem_util = float(parts[4])
|
|
47
|
+
temp = float(parts[5])
|
|
48
|
+
power = float(parts[6])
|
|
49
|
+
|
|
50
|
+
gpus.append({
|
|
51
|
+
"name": gpu_name,
|
|
52
|
+
"vramTotalMb": total_vram,
|
|
53
|
+
"vramFreeMb": free_vram,
|
|
54
|
+
"utilization": gpu_util,
|
|
55
|
+
"memoryUsedPercent": mem_util,
|
|
56
|
+
"temperature": temp,
|
|
57
|
+
"powerDraw": power
|
|
58
|
+
})
|
|
59
|
+
if gpus:
|
|
60
|
+
hardware["type"] = "gpu"
|
|
61
|
+
hardware["model"] = gpus[0]["name"]
|
|
62
|
+
hardware["vramGb"] = round(sum(g["vramTotalMb"] for g in gpus) / 1024.0, 1)
|
|
63
|
+
hardware["gpus"] = gpus
|
|
64
|
+
except Exception:
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
return hardware
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class Provider:
|
|
71
|
+
def __init__(self, token, api_url="http://localhost:3002/api/v1", detect_interval_secs=60, heartbeat_secs=15, poll_secs=5):
|
|
72
|
+
self.token = token
|
|
73
|
+
self.api_url = api_url.rstrip("/")
|
|
74
|
+
self.detect_interval_secs = detect_interval_secs
|
|
75
|
+
self.heartbeat_secs = heartbeat_secs
|
|
76
|
+
self.poll_secs = poll_secs
|
|
77
|
+
self.node_id = None
|
|
78
|
+
self.cached_hardware = None
|
|
79
|
+
self.cached_hardware_at = 0
|
|
80
|
+
|
|
81
|
+
def _headers(self):
|
|
82
|
+
return {
|
|
83
|
+
"Content-Type": "application/json",
|
|
84
|
+
"X-Node-Token": self.token
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
def _request(self, method, path, json=None):
|
|
88
|
+
url = f"{self.api_url}{path}"
|
|
89
|
+
res = requests.request(method, url, json=json, headers=self._headers())
|
|
90
|
+
if not res.ok:
|
|
91
|
+
try:
|
|
92
|
+
err_msg = res.json().get("error", f"HTTP {res.status_code}")
|
|
93
|
+
except Exception:
|
|
94
|
+
err_msg = f"HTTP {res.status_code}"
|
|
95
|
+
raise RuntimeError(err_msg)
|
|
96
|
+
try:
|
|
97
|
+
return res.json()
|
|
98
|
+
except Exception:
|
|
99
|
+
return {}
|
|
100
|
+
|
|
101
|
+
def get_hardware_detection(self):
|
|
102
|
+
now = time.time()
|
|
103
|
+
if not self.cached_hardware or now - self.cached_hardware_at > self.detect_interval_secs:
|
|
104
|
+
self.cached_hardware = detect_hardware()
|
|
105
|
+
self.cached_hardware_at = now
|
|
106
|
+
hw = self.cached_hardware
|
|
107
|
+
gpus_text = f", {len(hw['gpus'])} GPU(s)" if hw["gpus"] else ""
|
|
108
|
+
print(f"[detect] {hw['type']} {hw['model']} — {hw['memory']['totalGb']} GB RAM, {hw['cores']} cores{gpus_text}")
|
|
109
|
+
return self.cached_hardware
|
|
110
|
+
|
|
111
|
+
def build_telemetry(self, hardware):
|
|
112
|
+
gpus = hardware.get("gpus", [])
|
|
113
|
+
if gpus:
|
|
114
|
+
gpu_util = sum(g["utilization"] for g in gpus) / len(gpus)
|
|
115
|
+
gpu_mem = sum(g["memoryUsedPercent"] for g in gpus) / len(gpus)
|
|
116
|
+
temp = sum(g["temperature"] for g in gpus) / len(gpus)
|
|
117
|
+
power = sum(g["powerDraw"] for g in gpus)
|
|
118
|
+
return {
|
|
119
|
+
"gpuUtilization": round(gpu_util, 1),
|
|
120
|
+
"gpuMemory": round(gpu_mem, 1),
|
|
121
|
+
"temperature": round(temp, 1),
|
|
122
|
+
"powerDraw": round(power, 1),
|
|
123
|
+
"cpuCompute": 0,
|
|
124
|
+
"cpuMemory": round(((hardware["memory"]["totalGb"] - hardware["memory"]["freeGb"]) / hardware["memory"]["totalGb"]) * 100, 1)
|
|
125
|
+
}
|
|
126
|
+
else:
|
|
127
|
+
return {
|
|
128
|
+
"gpuUtilization": 0,
|
|
129
|
+
"gpuMemory": 0,
|
|
130
|
+
"temperature": 0,
|
|
131
|
+
"powerDraw": 0,
|
|
132
|
+
"cpuCompute": round(psutil.cpu_percent(), 1),
|
|
133
|
+
"cpuMemory": round(((hardware["memory"]["totalGb"] - hardware["memory"]["freeGb"]) / hardware["memory"]["totalGb"]) * 100, 1)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
def collect_metrics(self):
|
|
137
|
+
hardware = self.get_hardware_detection()
|
|
138
|
+
return {
|
|
139
|
+
"hostname": socket.gethostname(),
|
|
140
|
+
"platform": sys.platform,
|
|
141
|
+
"cpus": psutil.cpu_count(logical=True),
|
|
142
|
+
"memoryUsedMb": round(psutil.Process(os.getpid()).memory_info().rss / (1024 ** 2), 1),
|
|
143
|
+
"uptime": round(time.time() - psutil.boot_time()),
|
|
144
|
+
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
|
145
|
+
"hardware": hardware,
|
|
146
|
+
"telemetry": self.build_telemetry(hardware),
|
|
147
|
+
"relay": {
|
|
148
|
+
"host": "localhost",
|
|
149
|
+
"port": 0,
|
|
150
|
+
"url": None
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
def heartbeat(self):
|
|
155
|
+
metrics = self.collect_metrics()
|
|
156
|
+
data = self._request("POST", "/nodes/heartbeat", json={"metrics": metrics})
|
|
157
|
+
if data.get("nodeId"):
|
|
158
|
+
self.node_id = data["nodeId"]
|
|
159
|
+
return data
|
|
160
|
+
|
|
161
|
+
def poll_jobs(self):
|
|
162
|
+
try:
|
|
163
|
+
data = self._request("GET", "/nodes/jobs/next")
|
|
164
|
+
if data.get("nodeId"):
|
|
165
|
+
self.node_id = data["nodeId"]
|
|
166
|
+
job = data.get("job")
|
|
167
|
+
if not job or job.get("status") != "running":
|
|
168
|
+
return
|
|
169
|
+
|
|
170
|
+
print(f"[job] received job {job.get('operation')} ({job.get('id')})")
|
|
171
|
+
|
|
172
|
+
# Simple mock execution for Python SDK provider
|
|
173
|
+
try:
|
|
174
|
+
print(f"[job] executing {job.get('operation')}...")
|
|
175
|
+
time.sleep(1) # Simulation
|
|
176
|
+
result = {"status": "success", "message": "Python SDK mock execution successful"}
|
|
177
|
+
|
|
178
|
+
self._request("POST", f"/nodes/jobs/{job.get('id')}/complete", json={
|
|
179
|
+
"result": result,
|
|
180
|
+
"ttftMs": 100
|
|
181
|
+
})
|
|
182
|
+
print(f"[job] completed {job.get('id')}")
|
|
183
|
+
except Exception as ex:
|
|
184
|
+
self._request("POST", f"/nodes/jobs/{job.get('id')}/complete", json={
|
|
185
|
+
"error": str(ex)
|
|
186
|
+
})
|
|
187
|
+
print(f"[job] failed {job.get('id')}: {ex}")
|
|
188
|
+
except Exception as e:
|
|
189
|
+
print(f"[job] poll error: {e}")
|
|
190
|
+
|
|
191
|
+
def start(self):
|
|
192
|
+
print("NexusCompute Python Provider SDK starting")
|
|
193
|
+
print(f" Platform: {platform.platform()}")
|
|
194
|
+
print(f" API: {self.api_url}")
|
|
195
|
+
|
|
196
|
+
first = self.heartbeat()
|
|
197
|
+
print(f" Node ID: {self.node_id or first.get('nodeId')} (assigned by server)")
|
|
198
|
+
|
|
199
|
+
last_heartbeat = time.time()
|
|
200
|
+
last_poll = time.time()
|
|
201
|
+
|
|
202
|
+
print("Provider agent running. Press Ctrl+C to stop.")
|
|
203
|
+
try:
|
|
204
|
+
while True:
|
|
205
|
+
now = time.time()
|
|
206
|
+
if now - last_heartbeat >= self.heartbeat_secs:
|
|
207
|
+
self.heartbeat()
|
|
208
|
+
last_heartbeat = now
|
|
209
|
+
if now - last_poll >= self.poll_secs:
|
|
210
|
+
self.poll_jobs()
|
|
211
|
+
last_poll = now
|
|
212
|
+
time.sleep(1)
|
|
213
|
+
except KeyboardInterrupt:
|
|
214
|
+
print("\nStopping provider agent...")
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nexuscompute-sdk
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: NexusCompute Unified SDK — python client for renting compute and hosting provider nodes
|
|
5
|
+
Author: NexusCompute
|
|
6
|
+
Classifier: Programming Language :: Python :: 3
|
|
7
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
8
|
+
Classifier: Operating System :: OS Independent
|
|
9
|
+
Requires-Python: >=3.8
|
|
10
|
+
Requires-Dist: requests>=2.25.0
|
|
11
|
+
Requires-Dist: psutil>=5.8.0
|
|
12
|
+
Dynamic: author
|
|
13
|
+
Dynamic: classifier
|
|
14
|
+
Dynamic: requires-dist
|
|
15
|
+
Dynamic: requires-python
|
|
16
|
+
Dynamic: summary
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
nexus_sdk/__init__.py,sha256=XcRGnlv_kLkdUG0UUovqpStSIdid3A69eQ7Kz27lFws,143
|
|
2
|
+
nexus_sdk/cli.py,sha256=e3Zrp_ZIQ3-2kSXMEJMGWg8Cne1NrOcesBUR5FVVjWw,7744
|
|
3
|
+
nexus_sdk/client.py,sha256=xDdAllqVnhplW8f_9H-dPXuSeaNi0_FUVqWJ8hKvr0Y,4785
|
|
4
|
+
nexus_sdk/provider.py,sha256=W9QvTCIAiCmsji8v5GlST4aL_82SpdwLk4p0wq-bBmE,8145
|
|
5
|
+
nexuscompute_sdk-2.0.0.dist-info/METADATA,sha256=JTqW50MY4shCYZwSaVp9z6wwO5JuMXjdsnN8bam-UGc,527
|
|
6
|
+
nexuscompute_sdk-2.0.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
7
|
+
nexuscompute_sdk-2.0.0.dist-info/entry_points.txt,sha256=AwBChgPvlLcgujrgDUBy6KQ-ktrdubqO2mF5-CdGQeI,102
|
|
8
|
+
nexuscompute_sdk-2.0.0.dist-info/top_level.txt,sha256=pnVwzorEN7yUu42HPO_hoXtI3QxHS956R0LyyG7yueI,10
|
|
9
|
+
nexuscompute_sdk-2.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
nexus_sdk
|