AI-Studio / worker /worker.py
NathMen12's picture
Upload 16 files
4d9a630 verified
Raw
History Blame Contribute Delete
14.6 kB
#!/usr/bin/env python3
"""
AI Studio GPU Worker.
Ce worker expose un serveur HTTP local. Le serveur Hugging Face Space l'appelle
via l'URL ngrok enregistrée dans l'interface. Les requêtes du Space vers ngrok
incluent le header `ngrok-skip-browser-warning: true`.
Installation minimale:
pip install psutil pynvml
Pour l'entraînement Hugging Face:
pip install torch transformers datasets peft accelerate evaluate scikit-learn
Lancement exemple:
python worker.py --server-url https://ton-space.hf.space --user-token TON_TOKEN --worker-name "Colab T4" --port 8765
"""
import argparse
import json
import signal
import sys
import threading
import time
import traceback
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen
try:
import psutil
except Exception:
psutil = None
try:
import pynvml
except Exception:
pynvml = None
STATE = {
"server_url": "",
"user_token": "",
"worker_name": "",
"ngrok_url": "",
"current_job": None,
"stop_event": None,
"lock": threading.Lock(),
"started_at": time.time()
}
def log(message):
print(f"[worker] {message}", flush=True)
def post_json(path, payload, token=None, timeout=20):
url = f"{STATE['server_url'].rstrip('/')}{path}"
body = json.dumps(payload).encode("utf-8")
headers = {
"Content-Type": "application/json",
"ngrok-skip-browser-warning": "true"
}
if token:
headers["Authorization"] = f"Bearer {token}"
request = Request(url, data=body, headers=headers, method="POST")
try:
with urlopen(request, timeout=timeout) as response:
raw = response.read().decode("utf-8")
return json.loads(raw) if raw else {}
except HTTPError as error:
raw = error.read().decode("utf-8", errors="replace")
raise RuntimeError(f"POST {path} -> HTTP {error.code}: {raw}") from error
except URLError as error:
raise RuntimeError(f"POST {path} -> {error}") from error
def send_job_log(job_id, level, message, progress=None):
try:
post_json(
f"/api/jobs/{job_id}/logs",
{
"level": level,
"message": message,
"progress": progress
},
token=STATE["user_token"],
timeout=15
)
except Exception as exc:
log(f"Impossible d'envoyer le log: {exc}")
def send_job_status(job_id, status, progress=None, error=None):
try:
post_json(
f"/api/jobs/{job_id}/status",
{
"status": status,
"progress": progress,
"error": error
},
token=STATE["user_token"],
timeout=20
)
except Exception as exc:
log(f"Impossible d'envoyer le statut: {exc}")
def decode_nvml_value(value):
if isinstance(value, bytes):
return value.decode("utf-8", errors="ignore")
return value
def get_gpu_metrics():
metrics = {
"gpu_count": 0,
"gpu_name": None,
"gpu_index": 0,
"vram_total": 0,
"vram_used": 0,
"vram_free": 0,
"gpu_util": 0,
"gpu_temperature": None
}
if pynvml is not None:
try:
pynvml.nvmlInit()
count = pynvml.nvmlDeviceGetCount()
metrics["gpu_count"] = count
if count > 0:
handle = pynvml.nvmlDeviceGetHandleByIndex(0)
name = decode_nvml_value(pynvml.nvmlDeviceGetName(handle))
memory = pynvml.nvmlDeviceGetMemoryInfo(handle)
utilization = pynvml.nvmlDeviceGetUtilizationRates(handle)
metrics.update({
"gpu_name": name,
"gpu_index": 0,
"vram_total": int(memory.total),
"vram_used": int(memory.used),
"vram_free": int(memory.free),
"gpu_util": int(utilization.gpu),
"gpu_temperature": int(pynvml.nvmlDeviceGetTemperature(
handle,
pynvml.NVML_TEMPERATURE_GPU
))
})
pynvml.nvmlShutdown()
return metrics
except Exception as exc:
metrics["gpu_error"] = f"pynvml: {exc}"
try:
import subprocess
command = [
"nvidia-smi",
"--query-gpu=name,memory.total,memory.used,memory.free,utilization.gpu,temperature.gpu",
"--format=csv,noheader,nounits"
]
result = subprocess.run(command, check=True, capture_output=True, text=True, timeout=10)
lines = [line for line in result.stdout.splitlines() if line.strip()]
if lines:
parts = [part.strip() for part in lines[0].split(",")]
if len(parts) >= 6:
metrics.update({
"gpu_count": len(lines),
"gpu_name": parts[0],
"gpu_index": 0,
"vram_total": int(float(parts[1]) * 1024 * 1024),
"vram_used": int(float(parts[2]) * 1024 * 1024),
"vram_free": int(float(parts[3]) * 1024 * 1024),
"gpu_util": int(float(parts[4])),
"gpu_temperature": int(float(parts[5]))
})
except Exception as exc:
if "gpu_error" not in metrics:
metrics["gpu_error"] = f"nvidia-smi: {exc}"
return metrics
def get_metrics():
metrics = {
"worker_url": STATE["ngrok_url"],
"worker_name": STATE["worker_name"],
"uptime_seconds": int(time.time() - STATE["started_at"]),
"current_job": STATE["current_job"],
"python_version": sys.version.split()[0]
}
if psutil is not None:
try:
cpu = psutil.cpu_percent(interval=0.2)
memory = psutil.virtual_memory()
disk = psutil.disk_usage("/")
metrics.update({
"cpu_percent": cpu,
"cpu_count": psutil.cpu_count(logical=True),
"ram_total": int(memory.total),
"ram_used": int(memory.used),
"ram_free": int(memory.free),
"ram_percent": memory.percent,
"disk_total": int(disk.total),
"disk_used": int(disk.used),
"disk_free": int(disk.free),
"disk_percent": disk.percent
})
except Exception as exc:
metrics["system_error"] = f"psutil: {exc}"
else:
metrics["system_error"] = "psutil non installé"
metrics.update(get_gpu_metrics())
return metrics
def register_worker():
try:
with STATE["lock"]:
current_job = STATE["current_job"]
post_json(
"/api/workers/register",
{
"userToken": STATE["user_token"],
"workerName": STATE["worker_name"],
"status": "busy" if current_job else "online",
"metrics": get_metrics()
},
timeout=15
)
log("Worker enregistré auprès du serveur.")
except Exception as exc:
log(f"Échec de l'enregistrement: {exc}")
def heartbeat_loop(interval):
while True:
time.sleep(interval)
register_worker()
def run_job_thread(job, stop_event):
job_id = job.get("jobId")
log(f"Démarrage du job {job_id}")
send_job_status(job_id, "running", progress=0)
send_job_log(job_id, "info", "Démarrage de l'entraînement sur le worker GPU.", progress=0)
try:
from train_hf import run_training
result = run_training(
job=job,
log_callback=lambda level, message, progress=None: send_job_log(job_id, level, message, progress),
stop_event=stop_event
)
if stop_event.is_set():
send_job_status(job_id, "cancelled", progress=100)
send_job_log(job_id, "warning", "Job arrêté par l'utilisateur.", progress=100)
else:
send_job_status(job_id, "completed", progress=100)
send_job_log(
job_id,
"success",
f"Entraînement terminé. Sortie: {result.get('output_dir', 'inconnue')}",
progress=100
)
except KeyboardInterrupt:
send_job_status(job_id, "cancelled", progress=100)
send_job_log(job_id, "warning", "Job annulé.", progress=100)
except Exception as exc:
log(traceback.format_exc())
send_job_status(job_id, "failed", error=str(exc))
send_job_log(job_id, "error", f"Erreur pendant l'entraînement: {exc}")
finally:
with STATE["lock"]:
STATE["current_job"] = None
STATE["stop_event"] = None
log(f"Job {job_id} terminé.")
class WorkerHandler(BaseHTTPRequestHandler):
server_version = "AIStudioWorker/1.0"
def log_message(self, format, *args):
log(f"HTTP {args[0] if args else ''}")
def send_json(self, status, payload):
body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
self.send_response(status)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Content-Length", str(len(body)))
self.send_header("Access-Control-Allow-Origin", "*")
self.send_header("Access-Control-Allow-Headers", "Content-Type, Authorization, ngrok-skip-browser-warning")
self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
self.end_headers()
self.wfile.write(body)
def read_json(self):
length = int(self.headers.get("Content-Length", "0") or 0)
if not length:
return {}
raw = self.rfile.read(length).decode("utf-8")
return json.loads(raw) if raw else {}
def do_OPTIONS(self):
self.send_response(204)
self.send_header("Access-Control-Allow-Origin", "*")
self.send_header("Access-Control-Allow-Headers", "Content-Type, Authorization, ngrok-skip-browser-warning")
self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
self.end_headers()
def do_GET(self):
if self.path == "/health":
with STATE["lock"]:
current_job = STATE["current_job"]
stop_requested = bool(STATE["stop_event"].is_set() if STATE["stop_event"] else False)
self.send_json(200, {
"ok": True,
"status": "busy" if current_job else "idle",
"current_job": current_job,
"stop_requested": stop_requested,
"ngrok_url": STATE["ngrok_url"],
"worker_name": STATE["worker_name"],
"metrics": get_metrics()
})
return
if self.path == "/metrics":
self.send_json(200, get_metrics())
return
self.send_json(404, {"error": "Endpoint inconnu."})
def do_POST(self):
try:
payload = self.read_json()
if self.path == "/start-job":
with STATE["lock"]:
if STATE["current_job"]:
self.send_json(409, {
"ok": False,
"error": "Un job est déjà en cours sur ce worker."
})
return
STATE["current_job"] = payload.get("jobId")
STATE["stop_event"] = threading.Event()
stop_event = STATE["stop_event"]
thread = threading.Thread(
target=run_job_thread,
args=(payload, stop_event),
daemon=True
)
thread.start()
self.send_json(200, {"ok": True, "jobId": payload.get("jobId")})
return
if self.path == "/stop-job":
job_id = payload.get("jobId")
with STATE["lock"]:
current_job = STATE["current_job"]
if current_job and current_job == job_id and STATE["stop_event"]:
STATE["stop_event"].set()
self.send_json(200, {"ok": True, "jobId": job_id, "stop_requested": True})
else:
self.send_json(404, {"ok": False, "error": "Aucun job correspondant n'est en cours."})
return
self.send_json(404, {"error": "Endpoint inconnu."})
except Exception as exc:
self.send_json(500, {"error": str(exc)})
def parse_args():
parser = argparse.ArgumentParser(description="AI Studio GPU Worker")
parser.add_argument("--server-url", required=True, help="URL du serveur AI Studio, par exemple https://ton-space.hf.space")
parser.add_argument("--user-token", required=True, help="Token utilisateur pour s'authentifier auprès du serveur")
parser.add_argument("--worker-name", default="GPU Worker", help="Nom de la machine worker")
parser.add_argument("--ngrok-url", default="", help="URL publique ngrok, utile pour l'affichage et le diagnostic")
parser.add_argument("--host", default="0.0.0.0")
parser.add_argument("--port", type=int, default=8765)
parser.add_argument("--heartbeat-interval", type=int, default=15)
return parser.parse_args()
def main():
args = parse_args()
STATE["server_url"] = args.server_url.rstrip("/")
STATE["user_token"] = args.user_token
STATE["worker_name"] = args.worker_name
STATE["ngrok_url"] = args.ngrok_url
def shutdown(signum, frame):
log("Signal reçu, arrêt du worker...")
with STATE["lock"]:
if STATE["stop_event"]:
STATE["stop_event"].set()
server.shutdown()
signal.signal(signal.SIGTERM, shutdown)
signal.signal(signal.SIGINT, shutdown)
register_worker()
heartbeat = threading.Thread(
target=heartbeat_loop,
args=(args.heartbeat_interval,),
daemon=True
)
heartbeat.start()
global server
server = ThreadingHTTPServer((args.host, args.port), WorkerHandler)
log(f"Worker HTTP démarré sur {args.host}:{args.port}")
log(f"Worker name: {STATE['worker_name']}")
log("Le serveur Space doit utiliser l'URL ngrok enregistrée dans l'interface.")
server.serve_forever()
if __name__ == "__main__":
main()