客户端心跳时打印显卡温度功耗和算力日志,并带回今日预估积分。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-10-11 00:55:08 +08:00
parent fe530a619d
commit 80d46dcd5d
4 changed files with 350 additions and 50 deletions
+284 -50
View File
@@ -78,6 +78,7 @@ import hashlib
import json
import os
import subprocess
import sys
import time
import urllib.error
import urllib.request
@@ -86,17 +87,29 @@ import uuid
CONFIG = json.loads(__CONFIG__)
def hardware():
gpu_model, vram_gb, gpu_uuid = _gpu()
return {
"sn": CONFIG["sn"],
"token": CONFIG["token"],
"gpu_model": gpu_model,
"vram_gb": vram_gb,
"cpu_count": os.cpu_count(),
"memory_gb": _memory_gb(),
"hardware_fingerprint": _fingerprint(gpu_uuid),
}
SHARES = 0
_REFERENCE_MH = (
("4090", 130.0),
("4080 SUPER", 95.0),
("4080", 90.0),
("4070 TI SUPER", 75.0),
("4070 TI", 70.0),
("4070 SUPER", 62.0),
("4070", 55.0),
("3090 TI", 125.0),
("3090", 115.0),
("3080 TI", 105.0),
("3080", 95.0),
("3070 TI", 65.0),
("3070", 60.0),
("3060 TI", 48.5),
("3060", 40.0),
)
def current_mode():
mode = os.environ.get("HYPERNODE_MODE") or CONFIG.get("current_mode") or "pow_mining"
return mode if mode in {"pow_mining", "gpu_rental"} else "pow_mining"
def _fingerprint(gpu_uuid):
@@ -134,72 +147,136 @@ def _board_serial():
def _gpu():
probed = _pynvml()
if probed[0]:
if probed and probed.get("name"):
return probed
return _nvidia_smi()
probed = _nvidia_smi()
return probed or {}
def _blank_gpu():
return {
"name": None,
"vram_gb": None,
"uuid": None,
"vram_used_mb": None,
"vram_total_mb": None,
"gpu_temp": None,
"fan_speed": None,
"power_usage": None,
"utilization": None,
}
def _pynvml():
try:
import pynvml
except ImportError:
return None, None, None
return None
try:
pynvml.nvmlInit()
best_name = None
best = None
best_bytes = -1
best_uuid = None
for index in range(pynvml.nvmlDeviceGetCount()):
handle = pynvml.nvmlDeviceGetHandleByIndex(index)
name = pynvml.nvmlDeviceGetName(handle)
if isinstance(name, bytes):
name = name.decode()
total = int(pynvml.nvmlDeviceGetMemoryInfo(handle).total)
if total > best_bytes:
uid = pynvml.nvmlDeviceGetUUID(handle)
if isinstance(uid, bytes):
uid = uid.decode()
best_name = str(name)
best_bytes = total
best_uuid = str(uid)
memory = pynvml.nvmlDeviceGetMemoryInfo(handle)
total = int(memory.total)
if total <= best_bytes:
continue
uid = pynvml.nvmlDeviceGetUUID(handle)
if isinstance(uid, bytes):
uid = uid.decode()
best_bytes = total
best = _blank_gpu()
best.update({
"name": str(name),
"vram_gb": round(total / (1024 ** 3), 2),
"uuid": str(uid),
"vram_used_mb": int(round(int(memory.used) / (1024 ** 2))),
"vram_total_mb": int(round(total / (1024 ** 2))),
"gpu_temp": _nvml_call(pynvml.nvmlDeviceGetTemperature, handle, pynvml.NVML_TEMPERATURE_GPU),
"fan_speed": _nvml_call(pynvml.nvmlDeviceGetFanSpeed, handle),
"power_usage": _nvml_watts(pynvml, handle),
"utilization": _nvml_util(pynvml, handle),
})
pynvml.nvmlShutdown()
except Exception:
return None, None, None
if not best_name or best_bytes < 0:
return None, None, None
return best_name, round(best_bytes / (1024 ** 3), 2), best_uuid
return None
return best
def _nvml_call(func, *args):
try:
value = func(*args)
return int(value)
except Exception:
return None
def _nvml_watts(pynvml, handle):
try:
return round(pynvml.nvmlDeviceGetPowerUsage(handle) / 1000, 1)
except Exception:
return None
def _nvml_util(pynvml, handle):
try:
return int(pynvml.nvmlDeviceGetUtilizationRates(handle).gpu)
except Exception:
return None
def _nvidia_smi():
query = "name,memory.total,memory.used,uuid,temperature.gpu,fan.speed,power.draw,utilization.gpu"
try:
output = subprocess.check_output(
["nvidia-smi", "--query-gpu=name,memory.total,uuid", "--format=csv,noheader,nounits"],
["nvidia-smi", "--query-gpu=" + query, "--format=csv,noheader,nounits"],
stderr=subprocess.DEVNULL,
timeout=8,
text=True,
)
except (OSError, subprocess.SubprocessError):
return None, None, None
best_name = None
return None
best = None
best_mib = -1.0
best_uuid = None
for line in output.splitlines():
parts = [part.strip() for part in line.split(",", 2)]
if len(parts) < 2:
parts = [part.strip() for part in line.split(",")]
if len(parts) < 8:
continue
name, memory = parts[0], parts[1]
gpu_uuid = parts[2] if len(parts) > 2 else None
try:
mib = float(memory)
except ValueError:
total = _number(parts[-7])
if total is None or total <= best_mib:
continue
if mib > best_mib:
best_name = name or None
best_mib = mib
best_uuid = gpu_uuid or None
if best_name is None:
return None, None, None
return best_name, round(best_mib / 1024, 2), best_uuid
best_mib = total
best = _blank_gpu()
best.update({
"name": ",".join(parts[:-7]).strip() or None,
"vram_gb": round(total / 1024, 2),
"vram_total_mb": int(round(total)),
"vram_used_mb": _int_number(parts[-6]),
"uuid": parts[-5] or None,
"gpu_temp": _int_number(parts[-4]),
"fan_speed": _int_number(parts[-3]),
"power_usage": _number(parts[-2]),
"utilization": _int_number(parts[-1]),
})
return best
def _number(value):
try:
return float(value)
except (TypeError, ValueError):
return None
def _int_number(value):
number = _number(value)
if number is None:
return None
return int(round(number))
def _memory_gb():
@@ -233,28 +310,185 @@ def _memory_gb():
def beat():
body = json.dumps(hardware()).encode()
global SHARES
gpu = _gpu()
payload = hardware_from(gpu)
body = json.dumps(payload).encode()
request = urllib.request.Request(
CONFIG["base"] + "/api/v1/node/heartbeat",
data=body,
headers={"Content-Type": "application/json"},
)
with urllib.request.urlopen(request, timeout=20) as response:
print(response.read().decode())
reply = json.loads(response.read().decode())
SHARES += 1
_save_shares(SHARES)
_report(gpu, reply, SHARES)
def hardware_from(gpu):
card = gpu or {}
return {
"sn": CONFIG["sn"],
"token": CONFIG["token"],
"gpu_model": card.get("name"),
"vram_gb": card.get("vram_gb"),
"cpu_count": os.cpu_count(),
"memory_gb": _memory_gb(),
"hardware_fingerprint": _fingerprint(card.get("uuid")),
}
def _report(gpu, reply, shares):
card = gpu or {}
name = card.get("name") or "GPU not detected"
vram = card.get("vram_gb")
vram_text = f"{vram:.2f} GB" if isinstance(vram, (int, float)) else "n/a"
_log("INFO", f"{name} · {vram_text}")
_log("HARDWARE", _hardware_line(card))
if current_mode() == "gpu_rental":
load = card.get("utilization")
load_text = f"{load}% Load" if load is not None else "n/a"
_log("TASK", f"[AI Task Executing] · {load_text}")
else:
rate = _hashrate(name)
rate_text = f"{rate:.1f} MH/s" if rate is not None else "n/a"
_log("HASHRATE", f"{rate_text} · Accepted {shares}")
weight = reply.get("hardware_weight")
weight_text = f"{float(weight):.1f}" if isinstance(weight, (int, float)) else "n/a"
node_id = reply.get("display_node_id") or "----"
status = reply.get("status") or "unknown"
_log("HEARTBEAT", f"{status} · Node #{node_id} · Weight {weight_text}")
_log("RECOVERY", _recovery_line(reply))
print(flush=True)
def _hardware_line(card):
used = card.get("vram_used_mb")
total = card.get("vram_total_mb")
vram = f"{used} / {total} MB" if used is not None and total is not None else "n/a"
return " · ".join([
_suffix(card.get("gpu_temp"), "°C"),
"Fan " + _suffix(card.get("fan_speed"), "%"),
_suffix(card.get("power_usage"), " W"),
"VRAM " + vram,
])
def _recovery_line(reply):
days = int(reply.get("recovery_days") or 0)
purchase = _decimal(reply.get("purchase_amount"))
remaining = _decimal(reply.get("recoverable_stake_amount"))
estimate = reply.get("estimated_daily_points") or "0"
if purchase is not None and remaining is not None and purchase > 0:
done = max(0.0, (purchase - remaining) / purchase * 100)
progress = f"{done:.1f}% returned"
else:
progress = "n/a"
title = "30-day fast recovery" if days == 30 else f"{days}-day recovery"
return f"{title} · {progress} · Est. Points {estimate}"
def _hashrate(name):
upper = (name or "").upper().replace("-", " ")
for token, rate in _REFERENCE_MH:
if token in upper:
return rate
return None
def _suffix(value, suffix):
if value is None:
return "n/a"
return f"{value}{suffix}"
def _decimal(value):
if value is None or value == "":
return None
try:
return float(value)
except (TypeError, ValueError):
return None
def _log(tag, message):
color = {
"INFO": "36",
"HARDWARE": "33",
"HASHRATE": "32",
"TASK": "32",
"HEARTBEAT": "96",
"RECOVERY": "33",
"WARN": "31",
}.get(tag, "37")
label = f"[{tag}]".ljust(12)
if sys.stdout.isatty():
label = f"\033[1;{color}m{label}\033[0m"
stamp = time.strftime("%Y-%m-%d %H:%M:%S")
print(f"{stamp} {label} {message}", flush=True)
def _share_path():
folder = os.path.dirname(os.path.abspath(__file__))
return os.path.join(folder, "." + CONFIG["sn"] + ".shares")
def _load_shares():
try:
with open(_share_path(), encoding="utf-8") as handle:
return max(0, int(handle.read().strip() or "0"))
except (OSError, ValueError):
return 0
def _save_shares(count):
try:
with open(_share_path(), "w", encoding="utf-8") as handle:
handle.write(str(count))
except OSError:
return None
def _enable_ansi():
if os.name != "nt":
return
try:
kernel = ctypes.windll.kernel32
handle = kernel.GetStdHandle(-11)
mode = ctypes.c_ulong()
if kernel.GetConsoleMode(handle, ctypes.byref(mode)):
kernel.SetConsoleMode(handle, mode.value | 0x0004)
except Exception:
return None
if __name__ == "__main__":
_enable_ansi()
SHARES = _load_shares()
_log("INFO", "HyperNode client · " + CONFIG["sn"] + " · " + current_mode())
while True:
try:
beat()
except urllib.error.HTTPError as exc:
detail = exc.read().decode(errors="replace").strip()
_log("WARN", f"heartbeat {exc.code} {detail}")
except (urllib.error.URLError, TimeoutError, OSError) as exc:
print(exc)
_log("WARN", str(exc))
time.sleep(60)
"""
def render_client(sn: str, token: str, base_url: str) -> str:
config = json.dumps({"sn": sn, "token": token, "base": base_url.rstrip("/")}, ensure_ascii=True)
config = json.dumps(
{
"sn": sn,
"token": token,
"base": base_url.rstrip("/"),
"current_mode": "pow_mining",
},
ensure_ascii=True,
)
return _CLIENT.replace("__CONFIG__", repr(config))