Files
rpa_vision_v3/core/gpu/device_policy.py
Dom 0ee54157e5
Some checks failed
tests / Lint (ruff + black) (push) Failing after 1m44s
tests / Tests unitaires (sans GPU) (push) Failing after 1m49s
tests / Tests sécurité (critique) (push) Has been skipped
fix(p1g): garde-fou VRAM adapté à la mémoire unifiée (DGX GB10)
resolve_device('auto') renvoyait 'cpu' sur le GB10 : le plafond max_total_gb=6
(pensé pour la RTX 12 Go dédiés) voyait used≈99 Go car la mémoire UNIFIÉE compte
la RAM système. Au-dessus de DEFAULT_LARGE_VRAM_GB=24 (grosse carte / mémoire
unifiée), le plafond n'est plus appliqué ; seul free >= min_free_gb décide.
RTX (<=24 Go) inchangée.

Détecté au bench GB10 2026-06-08 (auto->cpu, OCR 10x plus lent). +2 tests (17/17).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 17:43:12 +02:00

165 lines
5.8 KiB
Python

"""Résolution de device paramétrable (auto/cuda/cpu) avec garde-fou VRAM.
Permet de basculer les étages CPU-par-défaut de la cascade vision (OCR docTR,
EasyOCR, YOLO/SoM) vers le GPU local **quand la VRAM est libre**, SANS jamais
hardcoder cuda. La politique anti-concurrence VRAM (tout sur CPU) datait d'une
époque où les VLM tournaient sur la RTX 5070 locale ; ils tournent désormais
sur un DGX distant (tunnel SSH `:11434`), libérant ~9 Go localement.
Logique de garde-fou inspirée de `core/embedding/clip_embedder.py` (lignes
~65-82) : `torch.cuda.is_available()` + `torch.cuda.mem_get_info()`.
Contraintes :
- JAMAIS de hardcode cuda ;
- aucun appel réseau ;
- import-safe : aucun chargement de modèle, aucune allocation GPU à l'import ;
- fallback CPU propre partout (jamais de crash si pas de GPU).
Override global : variable d'environnement `RPA_VISION_DEVICE` ∈ {cpu, cuda, auto}.
"""
from __future__ import annotations
import logging
import os
from typing import Optional
import torch
logger = logging.getLogger(__name__)
_GB = 1024 ** 3
# Valeurs reconnues pour l'argument `requested` et l'override env.
_VALID = {"cpu", "cuda", "auto"}
# Garde-fous par défaut (Go).
DEFAULT_MIN_FREE_GB = 2.0 # VRAM libre minimale pour autoriser cuda
DEFAULT_MAX_TOTAL_GB = 6.0 # plafond d'usage VRAM total après bascule
# Au-delà de ce total VRAM, on considère une grosse carte (data-center) ou une
# mémoire UNIFIÉE (DGX GB10 : ~121 Go partagés CPU+GPU). Dans ce cas `used`
# (= total - free) inclut la RAM système → le plafond fixe `max_total_gb` (pensé
# pour la RTX 12 Go dédiés) devient un faux positif qui force CPU à tort. On ne
# l'applique donc QUE sous ce seuil ; au-dessus, seul `free ≥ min_free_gb` décide.
DEFAULT_LARGE_VRAM_GB = 24.0
def _env_override() -> Optional[str]:
"""Lit l'override `RPA_VISION_DEVICE` s'il est présent et valide.
Retourne None si absent ou invalide (on retombe alors sur `requested`).
"""
raw = os.getenv("RPA_VISION_DEVICE", "").strip().lower()
if not raw:
return None
if raw in _VALID:
return raw
logger.warning(
"RPA_VISION_DEVICE='%s' invalide (attendu cpu/cuda/auto) — ignoré",
raw,
)
return None
def _cuda_available() -> bool:
"""`torch.cuda.is_available()` protégé contre toute exception driver."""
try:
return bool(torch.cuda.is_available())
except Exception as e: # pragma: no cover - dépend du driver
logger.debug("torch.cuda.is_available a levé : %s — CPU", e)
return False
def _free_total_gb() -> Optional[tuple[float, float]]:
"""VRAM (libre, totale) en Go via mem_get_info, ou None si indisponible."""
try:
free_bytes, total_bytes = torch.cuda.mem_get_info()
return free_bytes / _GB, total_bytes / _GB
except Exception as e: # pragma: no cover - dépend du driver
logger.debug("torch.cuda.mem_get_info a levé : %s", e)
return None
def resolve_device(
requested: str = "auto",
min_free_gb: float = DEFAULT_MIN_FREE_GB,
max_total_gb: float = DEFAULT_MAX_TOTAL_GB,
) -> str:
"""Résout le device effectif ("cuda" ou "cpu") selon la politique VRAM.
Args:
requested: "cpu", "cuda" ou "auto" (défaut). L'override env
`RPA_VISION_DEVICE` prime sur cet argument s'il est présent/valide.
min_free_gb: VRAM libre minimale (Go) pour autoriser cuda en mode auto.
max_total_gb: plafond d'usage VRAM total (Go). Si basculer cuda ferait
dépasser ce plafond (used = total - free), on reste CPU. Garde-fou
contre la saturation quand d'autres process occupent déjà le GPU.
Returns:
"cuda" ou "cpu". Toujours "cpu" en cas de doute (fallback propre).
Politique :
- "cpu""cpu" ;
- "cuda""cuda" si cuda dispo, sinon "cpu" (fallback loggé) ;
- "auto""cuda" si cuda dispo ET free ≥ min_free_gb ET
used ≤ max_total_gb, sinon "cpu".
"""
effective = _env_override() or (requested or "auto").strip().lower()
if effective not in _VALID:
logger.warning(
"device demandé '%s' invalide (attendu cpu/cuda/auto) — auto",
effective,
)
effective = "auto"
if effective == "cpu":
return "cpu"
if not _cuda_available():
if effective == "cuda":
logger.info("device=cuda demandé mais CUDA indisponible — fallback CPU")
return "cpu"
if effective == "cuda":
# Demande explicite : on respecte sans appliquer le garde-fou VRAM
# (l'appelant assume). CUDA est dispo → cuda.
return "cuda"
# effective == "auto" : garde-fou VRAM.
mem = _free_total_gb()
if mem is None:
logger.info("auto: mem_get_info indisponible — CPU par prudence")
return "cpu"
free_gb, total_gb = mem
used_gb = total_gb - free_gb
if free_gb < min_free_gb:
logger.info(
"auto: VRAM libre %.1f Go < seuil %.1f Go — CPU",
free_gb, min_free_gb,
)
return "cpu"
# Plafond d'usage : seulement sur carte dédiée "petite" (type RTX). Sur grosse
# mémoire / mémoire unifiée (GB10), `used` inclut la RAM système → non pertinent.
if total_gb <= DEFAULT_LARGE_VRAM_GB and used_gb > max_total_gb:
logger.info(
"auto: usage VRAM %.1f Go > plafond %.1f Go (carte %.1f Go) — CPU",
used_gb, max_total_gb, total_gb,
)
return "cpu"
if total_gb > DEFAULT_LARGE_VRAM_GB:
logger.info(
"auto: grosse mémoire/unifiée %.1f Go, libre %.1f Go — CUDA (plafond ignoré)",
total_gb, free_gb,
)
return "cuda"
logger.info(
"auto: VRAM libre %.1f Go (usage %.1f/%.1f Go) — CUDA",
free_gb, used_gb, total_gb,
)
return "cuda"