mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
fix(ai): detect a paddle-only GPU so OCR uses PaddleOCR-GPU not Tesseract (#439)
gpu_available() probed torch, then ONNX Runtime, then nvidia-smi, but never paddle. The OCR bundle ships paddlepaddle-gpu with no torch or ONNX, so on an OCR-only GPU host every probe missed the GPU: nvidia-smi saw it but returned False by design, and OCR silently fell back to Tesseract (CPU, lower quality) with no signal why. Add a paddle probe as the last resort in gpu_available(). It runs only after nvidia-smi confirms a GPU is physically present, and in an isolated subprocess, because importing paddlepaddle-gpu on a GPU-less host segfaults and would wedge the shared AI dispatcher. It returns True only when paddle reports both a CUDA build and a visible device, signalling the result through the exit code so paddle's own import chatter on stdout cannot corrupt the reading. CPU-only and torch/ONNX GPU hosts are unaffected: the probe never runs on the former (nvidia-smi finds nothing) and is never reached on the latter (the torch step already returns True first). Claude-Session: https://claude.ai/code/session_01NfaRxjek8ex5nawvx3mVMf
This commit is contained in:
@@ -43,12 +43,16 @@ def gpu_available():
|
||||
if onnx_available:
|
||||
return True
|
||||
|
||||
# Last resort: check nvidia-smi alone. The GPU is present even if
|
||||
# neither torch nor ONNX Runtime can use it (e.g. CPU-only packages).
|
||||
# A GPU is physically present but neither torch nor ONNX Runtime can use it.
|
||||
# The OCR bundle ships paddlepaddle-gpu, which still can, so probe paddle in
|
||||
# an isolated subprocess. This runs only now that nvidia-smi confirms a GPU,
|
||||
# and never in-process, because a GPU-less paddle import segfaults.
|
||||
gpu_name = _nvidia_smi_gpu_name()
|
||||
if gpu_name:
|
||||
print(f"[gpu] nvidia-smi found GPU ({gpu_name}) but neither torch "
|
||||
"nor ONNX Runtime can use it -- reinstall AI features for GPU support",
|
||||
if _try_paddle_cuda_subprocess():
|
||||
return True
|
||||
print(f"[gpu] nvidia-smi found GPU ({gpu_name}) but neither torch, ONNX "
|
||||
"Runtime, nor paddle can use it; reinstall AI features for GPU support",
|
||||
file=sys.stderr, flush=True)
|
||||
return False
|
||||
|
||||
@@ -113,6 +117,36 @@ def _try_onnx_cuda():
|
||||
return False
|
||||
|
||||
|
||||
def _try_paddle_cuda_subprocess():
|
||||
"""Check GPU via paddle in an isolated subprocess.
|
||||
|
||||
The OCR bundle ships paddlepaddle-gpu with no torch or ONNX Runtime, so paddle
|
||||
is the only framework that can see the GPU on an OCR-only host. Importing
|
||||
paddlepaddle-gpu in-process segfaults on a GPU-less machine, so this runs in a
|
||||
throwaway subprocess and callers must confirm a GPU is present (via nvidia-smi)
|
||||
before invoking it. Returns True only when paddle has a CUDA build and a
|
||||
visible GPU. The result is signalled through the exit code so paddle's own
|
||||
import chatter on stdout cannot corrupt the reading.
|
||||
"""
|
||||
probe = (
|
||||
"import paddle, sys; "
|
||||
"sys.exit(0 if (paddle.is_compiled_with_cuda() "
|
||||
"and paddle.device.cuda.device_count() > 0) else 1)"
|
||||
)
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-c", probe],
|
||||
capture_output=True, text=True, timeout=30,
|
||||
)
|
||||
except (OSError, subprocess.SubprocessError):
|
||||
return False
|
||||
if result.returncode == 0:
|
||||
print("[gpu] CUDA available via paddle (paddlepaddle-gpu)",
|
||||
file=sys.stderr, flush=True)
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def onnx_providers():
|
||||
"""Return (providers, device) tuple.
|
||||
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
"""gpu_available() must recognize a GPU that only paddle can use.
|
||||
|
||||
The OCR feature bundle ships paddlepaddle-gpu but no torch or ONNX Runtime. On an
|
||||
OCR-only GPU host the torch and ONNX probes both come up empty, so before this
|
||||
fix gpu_available() fell through to a plain nvidia-smi check that returned False
|
||||
by design, and OCR silently downgraded to Tesseract. gpu_available() now probes
|
||||
paddle too, in an isolated subprocess, but only after nvidia-smi confirms a GPU
|
||||
is physically present (importing paddlepaddle-gpu in-process segfaults on a
|
||||
GPU-less host and would wedge the shared AI dispatcher).
|
||||
"""
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
||||
import gpu # noqa: E402
|
||||
|
||||
|
||||
# --- The isolated paddle probe --------------------------------------------
|
||||
|
||||
def test_paddle_probe_true_when_subprocess_reports_cuda(monkeypatch):
|
||||
def fake_run(cmd, **kwargs):
|
||||
return subprocess.CompletedProcess(cmd, returncode=0, stdout="", stderr="")
|
||||
|
||||
monkeypatch.setattr(gpu.subprocess, "run", fake_run)
|
||||
assert gpu._try_paddle_cuda_subprocess() is True
|
||||
|
||||
|
||||
def test_paddle_probe_false_when_subprocess_reports_no_cuda(monkeypatch):
|
||||
def fake_run(cmd, **kwargs):
|
||||
return subprocess.CompletedProcess(cmd, returncode=1, stdout="", stderr="")
|
||||
|
||||
monkeypatch.setattr(gpu.subprocess, "run", fake_run)
|
||||
assert gpu._try_paddle_cuda_subprocess() is False
|
||||
|
||||
|
||||
def test_paddle_probe_false_on_timeout(monkeypatch):
|
||||
def fake_run(cmd, **kwargs):
|
||||
raise subprocess.TimeoutExpired(cmd, 30)
|
||||
|
||||
monkeypatch.setattr(gpu.subprocess, "run", fake_run)
|
||||
assert gpu._try_paddle_cuda_subprocess() is False
|
||||
|
||||
|
||||
# --- gpu_available() orchestration -----------------------------------------
|
||||
|
||||
def _patch_probes(monkeypatch, torch, onnx, smi):
|
||||
"""Stub the three existing probes and reset the lru_cache for one call."""
|
||||
monkeypatch.delenv("SNAPOTTER_GPU", raising=False)
|
||||
monkeypatch.setattr(gpu, "_try_torch_cuda", lambda: torch)
|
||||
monkeypatch.setattr(gpu, "_try_onnx_cuda", lambda: onnx)
|
||||
monkeypatch.setattr(gpu, "_nvidia_smi_gpu_name", lambda: smi)
|
||||
gpu.gpu_available.cache_clear()
|
||||
|
||||
|
||||
def test_gpu_available_true_when_only_paddle_sees_gpu(monkeypatch):
|
||||
# OCR-only GPU box: torch and ONNX absent, GPU present, paddle can use it.
|
||||
_patch_probes(monkeypatch, torch=False, onnx=False, smi="NVIDIA GeForce RTX 4070")
|
||||
monkeypatch.setattr(gpu, "_try_paddle_cuda_subprocess", lambda: True)
|
||||
assert gpu.gpu_available() is True
|
||||
|
||||
|
||||
def test_gpu_available_false_when_paddle_cannot_use_gpu(monkeypatch):
|
||||
# GPU present but paddle is a CPU build or cannot init CUDA: stay conservative.
|
||||
_patch_probes(monkeypatch, torch=False, onnx=False, smi="NVIDIA GeForce RTX 4070")
|
||||
monkeypatch.setattr(gpu, "_try_paddle_cuda_subprocess", lambda: False)
|
||||
assert gpu.gpu_available() is False
|
||||
|
||||
|
||||
def test_gpu_available_never_probes_paddle_without_a_gpu(monkeypatch):
|
||||
# Safety invariant: on a GPU-less host a paddlepaddle-gpu import segfaults, so
|
||||
# the probe must never run when nvidia-smi finds no GPU.
|
||||
_patch_probes(monkeypatch, torch=False, onnx=False, smi=None)
|
||||
called = {"paddle": False}
|
||||
|
||||
def spy():
|
||||
called["paddle"] = True
|
||||
return True
|
||||
|
||||
monkeypatch.setattr(gpu, "_try_paddle_cuda_subprocess", spy)
|
||||
assert gpu.gpu_available() is False
|
||||
assert called["paddle"] is False
|
||||
Reference in New Issue
Block a user