From faf5be476037d3baf4da3515789906a176f9b040 Mon Sep 17 00:00:00 2001
From: shuishen <1109946754@qq.com>
Date: Tue, 25 Aug 2026 12:23:59 +0800
Subject: [PATCH] feat(pointcloud): auto-select verified GPU runtime

---
 scripts/serve_workbench_console.py |   65 +++++++++++++++++++++++---------
 1 files changed, 47 insertions(+), 18 deletions(-)

diff --git a/scripts/serve_workbench_console.py b/scripts/serve_workbench_console.py
index 65cd83f..b1d12ca 100644
--- a/scripts/serve_workbench_console.py
+++ b/scripts/serve_workbench_console.py
@@ -76,6 +76,8 @@
 POINTCLOUD_INFERENCE_JOBS_LOCK = threading.Lock()
 MAX_ANNOTATION_LABELS = 400_000
 POINTCLOUD_CLASS_CODES = {1, 2, 5, 6, 15, 16}
+POINTCLOUD_CPU_ENVIRONMENT = "05-3d-pointcloud"
+POINTCLOUD_GPU_ENVIRONMENT = "05-3d-pointcloud-gpu"
 
 
 class ApiError(ValueError):
@@ -128,6 +130,39 @@
         for chunk in iter(lambda: stream.read(8 * 1024 * 1024), b""):
             digest.update(chunk)
     return digest.hexdigest()
+
+
+def pointcloud_execution_environment(root: Path, requested_device: str = "auto") -> dict[str, str]:
+    """Select only a fixed point-cloud interpreter after a short CUDA probe.
+
+    The console never accepts a browser-supplied Python path.  A failed or
+    unavailable GPU environment is an expected condition for ``auto`` and
+    falls back to the retained CPU environment.
+    """
+    if requested_device not in {"auto", "cpu", "cuda"}:
+        raise ApiError("Point-cloud device must be auto, cpu, or cuda.")
+    cpu_python = root / ".venvs" / POINTCLOUD_CPU_ENVIRONMENT / "Scripts" / "python.exe"
+    gpu_python = root / ".venvs" / POINTCLOUD_GPU_ENVIRONMENT / "Scripts" / "python.exe"
+    if requested_device != "cpu" and gpu_python.is_file():
+        try:
+            probe = subprocess.run(
+                [str(gpu_python), "-c", "import json, torch; print(json.dumps({'cuda': bool(torch.cuda.is_available()), 'torch': torch.__version__}))"],
+                cwd=root,
+                capture_output=True,
+                text=True,
+                timeout=20,
+                check=False,
+            )
+            payload = json.loads(probe.stdout.strip().splitlines()[-1]) if probe.returncode == 0 and probe.stdout.strip() else {}
+            if payload.get("cuda") is True and isinstance(payload.get("torch"), str):
+                return {"python": str(gpu_python), "device": "cuda", "environment": POINTCLOUD_GPU_ENVIRONMENT, "torchVersion": payload["torch"]}
+        except (OSError, subprocess.SubprocessError, json.JSONDecodeError, IndexError):
+            pass
+    if requested_device == "cuda":
+        raise ApiError("CUDA was requested, but the fixed point-cloud GPU environment is unavailable.")
+    if not cpu_python.is_file():
+        raise ApiError("3D point-cloud CPU virtual environment is unavailable. Run the capability setup first.")
+    return {"python": str(cpu_python), "device": "cpu", "environment": POINTCLOUD_CPU_ENVIRONMENT, "torchVersion": "unknown"}
 
 
 def trajectory_runs(root: Path) -> list[dict[str, Any]]:
@@ -441,11 +476,10 @@
         return dict(value) if value else None
 
 
-def execute_pointcloud_training_job(root: Path, job_id: str, annotation: Path, output: Path, device: str) -> None:
+def execute_pointcloud_training_job(root: Path, job_id: str, annotation: Path, output: Path, execution: dict[str, str]) -> None:
     with POINTCLOUD_TRAINING_JOBS_LOCK:
         POINTCLOUD_TRAINING_JOBS[job_id].update({"status": "running", "stage": "training", "startedAt": datetime.now(UTC).isoformat()})
-    python = root / ".venvs" / "05-3d-pointcloud" / "Scripts" / "python.exe"
-    command = [str(python), str(root / "capabilities" / "05-3d-pointcloud" / "train_pointcloud_semantic_model.py"), "--annotation", str(annotation), "--output", str(output), "--device", device]
+    command = [execution["python"], str(root / "capabilities" / "05-3d-pointcloud" / "train_pointcloud_semantic_model.py"), "--annotation", str(annotation), "--output", str(output), "--device", execution["device"]]
     try:
         with RUN_LOCK:
             completed = subprocess.run(command, cwd=root, capture_output=True, text=True, timeout=14_400, check=False)
@@ -521,11 +555,10 @@
         raise ApiError("LAS/LAZ 文件没有点记录。请选择包含实际 RGB 点位的完整点云文件,而不是空分块。")
 
 
-def execute_pointcloud_inference_job(root: Path, job_id: str, model: Path, source: Path, output: Path) -> None:
+def execute_pointcloud_inference_job(root: Path, job_id: str, model: Path, source: Path, output: Path, execution: dict[str, str]) -> None:
     with POINTCLOUD_INFERENCE_JOBS_LOCK:
         POINTCLOUD_INFERENCE_JOBS[job_id].update({"status": "running", "stage": "inference", "startedAt": datetime.now(UTC).isoformat()})
-    python = root / ".venvs" / "05-3d-pointcloud" / "Scripts" / "python.exe"
-    command = [str(python), str(root / "capabilities" / "05-3d-pointcloud" / "apply_pointcloud_semantic_model.py"), "--model", str(model), "--input", str(source), "--output", str(output), "--device", "cpu"]
+    command = [execution["python"], str(root / "capabilities" / "05-3d-pointcloud" / "apply_pointcloud_semantic_model.py"), "--model", str(model), "--input", str(source), "--output", str(output), "--device", execution["device"]]
     try:
         with RUN_LOCK:
             completed = subprocess.run(command, cwd=root, capture_output=True, text=True, timeout=14_400, check=False)
@@ -1643,9 +1676,9 @@
             source_bytes[name] = raw_path.stat().st_size
             shutil.copyfile(raw_path, processed_root / name)
         output = self.root / "shared" / "outputs" / "05-3d-pointcloud" / "runs" / run_id
-        python = self.root / ".venvs" / "05-3d-pointcloud" / "Scripts" / "python.exe"
+        python = self.root / ".venvs" / POINTCLOUD_CPU_ENVIRONMENT / "Scripts" / "python.exe"
         if not python.is_file():
-            raise ApiError("3D point-cloud virtual environment is unavailable. Run the capability setup first.")
+            raise ApiError("3D point-cloud CPU virtual environment is unavailable. Run the capability setup first.")
         command = [str(python), str(self.root / "capabilities" / "05-3d-pointcloud" / "run_pointcloud_understanding.py"), "--input", str(processed_root), "--output", str(output), "--ground-up-axis", "z"]
         with RUN_LOCK:
             self.run_command(command, 900)
@@ -1710,15 +1743,13 @@
         record = load_json(annotation)
         if record.get("schema_version") != 1:
             raise ApiError("The selected annotation revision is unavailable.")
-        python = self.root / ".venvs" / "05-3d-pointcloud" / "Scripts" / "python.exe"
-        if not python.is_file():
-            raise ApiError("3D point-cloud virtual environment is unavailable. Run the capability setup first.")
+        execution = pointcloud_execution_environment(self.root, device)
         job_id = uuid4().hex
         output = self.root / "shared" / "outputs" / "05-3d-pointcloud" / "training-runs" / make_run_id("semantic-model")
-        job = {"id": job_id, "annotationId": annotation_id, "status": "queued", "stage": "queued", "device": device, "createdAt": datetime.now(UTC).isoformat()}
+        job = {"id": job_id, "annotationId": annotation_id, "status": "queued", "stage": "queued", "requestedDevice": device, "device": execution["device"], "environment": execution["environment"], "torchVersion": execution["torchVersion"], "createdAt": datetime.now(UTC).isoformat()}
         with POINTCLOUD_TRAINING_JOBS_LOCK:
             POINTCLOUD_TRAINING_JOBS[job_id] = job
-        thread = threading.Thread(target=execute_pointcloud_training_job, args=(self.root, job_id, annotation, output, device), daemon=True, name=f"pointcloud-training-{job_id[:8]}")
+        thread = threading.Thread(target=execute_pointcloud_training_job, args=(self.root, job_id, annotation, output, execution), daemon=True, name=f"pointcloud-training-{job_id[:8]}")
         thread.start()
         return dict(job)
 
@@ -1735,9 +1766,7 @@
         if Path(name).suffix.lower() not in suffixes:
             raise ApiError("Model inference requires a PLY, PCD, XYZ, LAS, or LAZ point cloud.")
         validate_pointcloud_model_input(staged_path)
-        python = self.root / ".venvs" / "05-3d-pointcloud" / "Scripts" / "python.exe"
-        if not python.is_file():
-            raise ApiError("3D point-cloud virtual environment is unavailable. Run the capability setup first.")
+        execution = pointcloud_execution_environment(self.root)
         run_id = make_run_id("semantic-inference")
         raw_root = self.root / "shared" / "data" / "raw" / "05-3d-pointcloud" / "model-inference-runs" / run_id
         processed_root = self.root / "shared" / "data" / "processed" / "05-3d-pointcloud" / "model-inference-runs" / run_id
@@ -1760,10 +1789,10 @@
         except ValueError as exc:
             raise ApiError("Selected model is outside the allowed training output directory.") from exc
         job_id = uuid4().hex
-        job = {"id": job_id, "runId": run_id, "modelId": model_id, "inputName": name, "status": "queued", "stage": "queued", "device": "cpu", "createdAt": datetime.now(UTC).isoformat(), "sourceSha256": actual_sha256, "rawInput": relative_path(self.root, raw_path), "processedInput": relative_path(self.root, processed_path)}
+        job = {"id": job_id, "runId": run_id, "modelId": model_id, "inputName": name, "status": "queued", "stage": "queued", "requestedDevice": "auto", "device": execution["device"], "environment": execution["environment"], "torchVersion": execution["torchVersion"], "createdAt": datetime.now(UTC).isoformat(), "sourceSha256": actual_sha256, "rawInput": relative_path(self.root, raw_path), "processedInput": relative_path(self.root, processed_path)}
         with POINTCLOUD_INFERENCE_JOBS_LOCK:
             POINTCLOUD_INFERENCE_JOBS[job_id] = job
-        thread = threading.Thread(target=execute_pointcloud_inference_job, args=(self.root, job_id, model_path, processed_path, output), daemon=True, name=f"pointcloud-inference-{job_id[:8]}")
+        thread = threading.Thread(target=execute_pointcloud_inference_job, args=(self.root, job_id, model_path, processed_path, output, execution), daemon=True, name=f"pointcloud-inference-{job_id[:8]}")
         thread.start()
         return dict(job)
 

--
Gitblit v1.9.3