Files
SnapOtter/packages/ai/python/tests/test_gpu_detection.py
T
SnapOtterandGitHub 7cd514dd8c 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
2026-07-05 22:57:36 +08:00

83 lines
3.3 KiB
Python

"""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