Split webapp for Raspberry Pi deployment, remote generation API and sync

La webapp (carte + vignettes) et la génération de tuiles se déploient sur
deux machines : image légère Dockerfile.webapp (FastAPI + Pillow AVIF
natif + pyproj) sur Raspberry Pi, pipeline complet sur la machine de
traitement. LIDAR_GENERATION_URL délègue /api/generate, /api/preview et
/api/status ; /api/sync ramène les tuiles par rsync puis régénère
vignettes et index localement. Token partagé optionnel
(LIDAR_API_TOKEN/LIDAR_REMOTE_TOKEN). Retire du dépôt les journaux
internes (.swival, audit-findings) et les données (data/, notebooks/).
Doc : docs/DEPLOY_WEBAPP.md.
This commit is contained in:
Antoine Jacquin
2026-09-02 19:44:39 +02:00
parent ed3e90ea89
commit 422f58d772
24 changed files with 2390 additions and 329 deletions

View File

@ -27,6 +27,12 @@ _gpu_reason = None
# GPU restriction from -g flag (host-level indices)
_restricted_gpu_ids: list[int] | None = None
# Vrai si CUDA_VISIBLE_DEVICES a été écrit par _init_gpu lui-même (choix du
# meilleur GPU). Cette valeur NE DOIT PAS être traitée comme une restriction
# externe : sinon available_gpu_ids() ne retourne plus que le GPU choisi et
# tous les workers reçoivent le même gpu_id (GPU 1 jamais utilisé).
_env_set_by_init = False
# Discovered GPU candidates (populated by _pick_gpu)
_gpu_candidates: list = []
@ -38,7 +44,7 @@ def _pick_gpu() -> list:
result = subprocess.run(
['nvidia-smi', '--query-gpu=index,name,compute_cap,memory.total',
'--format=csv,noheader,nounits'],
capture_output=True, text=True, timeout=5,
capture_output=True, text=True, timeout=15,
)
if result.returncode != 0:
return []
@ -86,10 +92,12 @@ def _filter_candidates(gpus: list) -> list:
nvidia-smi lists ALL GPUs even when CUDA_VISIBLE_DEVICES is set
(driver 580.x behavior), so we must filter manually.
La variable écrite par _init_gpu (choix auto du meilleur GPU) est ignorée :
seules les restrictions externes (run.sh -g, compose) comptent.
"""
# Filter by CUDA_VISIBLE_DEVICES if set
# Filter by CUDA_VISIBLE_DEVICES if set externally
cuda_visible = os.environ.get('CUDA_VISIBLE_DEVICES')
if cuda_visible is not None:
if cuda_visible is not None and not _env_set_by_init:
try:
visible = {int(i.strip()) for i in cuda_visible.split(',')}
gpus = [g for g in gpus if g[0] in visible]
@ -104,6 +112,40 @@ def _filter_candidates(gpus: list) -> list:
return gpus
def _runtime_candidates():
"""Détection GPU de repli via le runtime CuPy (sans nvidia-smi).
nvidia-smi peut dépasser son timeout quand le système est chargé :
_pick_gpu() retourne alors [] et les workers passent à tort en CPU.
Les indices CuPy sont renumérotés selon CUDA_VISIBLE_DEVICES — on les
remappe en indices hôtes pour rester compatible avec _filter_candidates.
"""
try:
import cupy as _cp_runtime
n = _cp_runtime.cuda.runtime.getDeviceCount()
cuda_visible = os.environ.get('CUDA_VISIBLE_DEVICES')
try:
host_ids = [int(v.strip()) for v in cuda_visible.split(',')]
except (ValueError, AttributeError):
host_ids = list(range(n))
gpus = []
for i in range(min(n, len(host_ids))):
props = _cp_runtime.cuda.runtime.getDeviceProperties(i)
name = props.get('name', b'?')
if isinstance(name, bytes):
name = name.decode()
major = int(props.get('major', 0))
minor = int(props.get('minor', 0))
mem_mi = int(props.get('totalGlobalMem', 0)) // (1024 * 1024)
cap = f"{major}.{minor}"
score = major * 1000 + minor * 100 + mem_mi
gpus.append((host_ids[i], name, cap, mem_mi, score, major))
gpus.sort(key=lambda g: g[4], reverse=True)
return gpus
except Exception:
return []
def _init_gpu():
"""Lazily initialize CuPy on first GPU use.
@ -118,6 +160,9 @@ def _init_gpu():
_gpu_initialized = True
candidates = _filter_candidates(_gpu_candidates)
if not candidates:
# Repli runtime : nvidia-smi a échoué (timeout système chargé)
candidates = _filter_candidates(_runtime_candidates())
if not candidates:
logger.info("Pas de GPU utilisable — mode CPU uniquement")
_xp = np
@ -192,6 +237,8 @@ def _init_gpu():
return
idx, name, mem_mi = _working_gpu
global _env_set_by_init
_env_set_by_init = True
os.environ['CUDA_VISIBLE_DEVICES'] = str(idx)
import cupy as _real_cupy
@ -238,9 +285,19 @@ def restrict_gpus(gpu_ids: list[int], set_env_var: bool = False):
def set_active_gpu(gpu_id):
"""Restrict to a single GPU by host-level index."""
"""Restrict to a single GPU by host-level index.
Appelé par les workers spawnés (_process_file_standalone) : CuPy n'y est
pas encore initialisé (le worker n'exécute pas log_gpu_status), il faut
donc déclencher _init_gpu() ici, sinon le worker retombe silencieusement
en CPU. On réduit aussi CUDA_VISIBLE_DEVICES à ce seul GPU avant l'init
pour que le device 0 du worker soit le bon : sans cela, tous les workers
avec plusieurs GPU visibles partagent le premier d'entre eux.
"""
global _restricted_gpu_ids
_restricted_gpu_ids = [gpu_id]
os.environ['CUDA_VISIBLE_DEVICES'] = str(gpu_id)
_init_gpu()
def _gpu_available():