forked from Dan4ick/Lidar_Muxa
видеокарта прогревается в фоне, пока считает процессор
This commit is contained in:
parent
89ca5b4163
commit
e0999ad870
4 changed files with 215 additions and 12 deletions
|
|
@ -97,7 +97,7 @@ flyguard/ ядро: стадии обработки, память, сч
|
|||
export.py 3D-рамки, время до столкновения, маркеры RViz
|
||||
synth.py вставка предметов трассировкой лучей
|
||||
tools/ обучение, оценка, разбор, полигон с аугментациями
|
||||
tests/ 50 тестов, запускаются без данных и без ROS
|
||||
tests/ 52 теста, запускаются без данных и без ROS
|
||||
docs/ методика и результаты
|
||||
artifacts/ обученные модели
|
||||
```
|
||||
|
|
|
|||
|
|
@ -3318,3 +3318,66 @@ cuBLAS и прочие не нужны, но PyTorch без них не загр
|
|||
`TORCH_WHEELS` (`docker/fetch_wheels.py`, `docker/wheels/README.md`). Через
|
||||
контекст сборки колёса не передаются: первая попытка так делала, и контекст в
|
||||
4.1 ГБ плюс слой с колёсами плюс установка чуть не заняли весь диск C:.
|
||||
|
||||
### 21.7. Docker Desktop: тот же архив на чистом движке
|
||||
|
||||
Образ перенесён файлом, как на стенд: выгружен из Docker в WSL
|
||||
(`flyguard_image.tar`, 4.7 ГБ — слои внутри уже сжаты) и загружен
|
||||
`docker load -i` в Docker Desktop 29.8 на Windows 11 (движок в WSL 2) за 6 мин.
|
||||
Флаг `--gpus all` работает, как на Linux: PyTorch в контейнере видит RTX 5070 Ti,
|
||||
без флага CUDA не видна и узел считает на процессоре. Бэги монтируются из Ubuntu
|
||||
(`docker run` из WSL, `-v /root/bags:/data`), дымовой тест проходит.
|
||||
|
||||
| запись | счёт | кадр, медиана / p95 | принято | итог |
|
||||
|---|---|---|---|---|
|
||||
| `doubleT_obstacle` | видеокарта | 23.9 / 35.2 мс | 201 из 201 | 189 из 190, 55.8 м |
|
||||
| `doubleT_obstacle` | процессор | 34.8 / 39.4 мс | 201 из 201 | 189 из 190, 55.8 м |
|
||||
| синтетика | видеокарта | 26.4 / 42.6 мс | 1510 из 1510 | 398 кадров с тревогой |
|
||||
| синтетика | процессор | 42.7 / 48.0 мс | 1510 из 1510 | 398 кадров с тревогой |
|
||||
|
||||
Это итоговый архив, с прогревом видеокарты в фоне (п. 21.8); замер ждёт
|
||||
готовности видеокарты. Первый архив, без фонового прогрева, дал те же решения
|
||||
и то же время с точностью до шума: 24.2 / 33.7 и 25.9 / 42.5 мс на видеокарте,
|
||||
32.7 / 36.0 и 42.0 / 47.0 на процессоре. Синтетика с видеокартой здесь впервые
|
||||
после правки прогрева (п. 21.5): 1510 кадров из 1510 и те же 398 кадров
|
||||
тревоги, что на процессоре. Худший кадр синтетики на видеокарте — 0.37 с (и до
|
||||
фонового прогрева был 0.33 с), но кадры не теряются: отброшено 0.
|
||||
|
||||
Каждый контейнер с видеокартой под WSL пишет в свой слой кэш драйвера CUDA,
|
||||
около 330 МБ, и диск Docker Desktop рос: 12 → 18.7 ГБ за день вместе со второй
|
||||
загрузкой архива. С `CUDA_CACHE_DISABLE=1` четыре прогона добавили 32 МБ, а
|
||||
видеокарта готова через те же 16–17 с.
|
||||
|
||||
### 21.8. Долгий старт видеокарты и прогрев в фоне
|
||||
|
||||
С видеокартой узел поднимался 15–16 с, на процессоре — 0.8 с. Разбор по шагам
|
||||
в контейнере: импорт PyTorch 0.9 с, контекст CUDA 0.5 с, прогрев стадий 14.6 с,
|
||||
из них первая раскладка по порядку точек 8.3 с, по углам 4.5 с. На Windows без
|
||||
контейнера тот же прогрев — 0.9 с. Дело в первом запуске ядер CUDA под WSL:
|
||||
драйвер складывает загруженные модули в `~/.nv/ComputeCache` (321 МБ), и во
|
||||
втором процессе того же контейнера прогрев идёт 0.3 с. Компиляция PTX ни при
|
||||
чём: с `CUDA_DISABLE_PTX_JIT=1` все ядра работают и время то же. Новый контейнер
|
||||
начинает с пустым кэшем, поэтому 15 с платятся при каждом `docker run`.
|
||||
|
||||
Узел всё это время не был подписан на лидар. Запуск через launch с `bag:=`
|
||||
ждал подписки и кадров не терял, но запись, пущенная снаружи сразу после старта
|
||||
контейнера, теряла бы первые 15 с. Предмет в начале контрольной записи мог так
|
||||
и не попасть в обработку.
|
||||
|
||||
Поэтому видеокарта теперь поднимается в своём потоке (`FlyGuard(...,
|
||||
gpu_background=True)` в узле). Конвейер тем временем считает на процессоре и
|
||||
забирает готовые стадии только в начале кадра: ламина и кластеризация берут
|
||||
образ, который сетчатка оставила на видеокарте, и переход посреди кадра их
|
||||
сломал бы. Замер на `doubleT_obstacle`, Docker в WSL:
|
||||
|
||||
| запуск | готова через | кадр, медиана / p95 | принято | итог |
|
||||
|---|---|---|---|---|
|
||||
| запись после прогрева | 16.0 с | 23.4 / 30.2 мс | 201 из 201 | 189 из 190 |
|
||||
| запись сразу, как на стенде | 16.5 с | 31.0 / 36.6 мс (почти всё на процессоре) | 201 из 201 | 189 из 190 |
|
||||
| без видеокарты | — | 31.8 / 34.9 мс | 201 из 201 | 189 из 190 |
|
||||
|
||||
Заодно закрыт случай, который `try/except` не ловил: если драйвер зависнет, узел
|
||||
останется на процессоре, а не встанет. Тесты: прогрев, отпущенный на 20-м кадре,
|
||||
дальше ведёт те же кандидаты и решения, что процессор; упавший прогрев оставляет
|
||||
процессор и причину в `gpu_error`. `docker/demo_test.sh` для замера скорости
|
||||
ждёт видеокарту (`WAIT_GPU=0` — не ждать, как на стенде).
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import threading
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
|
|
@ -225,7 +226,8 @@ class Params:
|
|||
# 'auto' — видеокарта, если PyTorch её видит, иначе процессор; 'cuda';
|
||||
# 'cpu'. Результат одинаков: на видеокарте те же операции в тех же типах.
|
||||
# Любой сбой видеокарты переводит конвейер на процессор до конца работы,
|
||||
# кадр досчитывается там же. EXPERIMENTS п. 21.
|
||||
# кадр досчитывается там же. Узел поднимает видеокарту в фоне
|
||||
# (`gpu_background`), считая первые кадры на процессоре. EXPERIMENTS п. 21.
|
||||
device: str = "auto"
|
||||
enable_motion: bool = True
|
||||
enable_memory: bool = True
|
||||
|
|
@ -268,19 +270,26 @@ class FlyGuard:
|
|||
memory: MushroomBody | None = None,
|
||||
layout: ScanLayout | None = None,
|
||||
readout: "MbonReadout | None" = None,
|
||||
track_readout=None):
|
||||
track_readout=None,
|
||||
gpu_background: bool = False):
|
||||
self.p = params or Params()
|
||||
from .device import get_device
|
||||
self.device = get_device(self.p.device)
|
||||
self.device = "cpu"
|
||||
self.gpu = None # плотные стадии на видеокарте (gpu.GpuStages)
|
||||
self.gpu_error: str | None = None
|
||||
if self.device.startswith("cuda"):
|
||||
try:
|
||||
from .gpu import GpuStages
|
||||
self.gpu = GpuStages(self.device)
|
||||
self.gpu.warmup()
|
||||
except Exception as e: # нет драйвера, не та видеокарта, нет памяти
|
||||
self._gpu_off(e)
|
||||
self._gpu_ready = None # (стадии, устройство) — готовы, ждут начала кадра
|
||||
# gpu_background: видеокарта поднимается в своём потоке, а кадры тем
|
||||
# временем считает процессор. Первый запуск ядер CUDA бывает долгим —
|
||||
# в Docker под WSL 15 с на каждый новый контейнер, — и узел всё это время
|
||||
# не был подписан и терял кадры. Зависни драйвер совсем — узел так и
|
||||
# останется на процессоре, а не встанет.
|
||||
self.gpu_pending = str(self.p.device).strip().lower() != "cpu"
|
||||
if self.gpu_pending:
|
||||
if gpu_background:
|
||||
threading.Thread(target=self._bring_up_gpu, name="flyguard-gpu",
|
||||
daemon=True).start()
|
||||
else:
|
||||
self._bring_up_gpu()
|
||||
self._take_gpu()
|
||||
self.memory = memory if (memory is not None and self.p.enable_memory) else NoMemory()
|
||||
self.readout = readout if (readout is not None and self.p.enable_mbon) else None
|
||||
self.layout_full = layout
|
||||
|
|
@ -345,6 +354,36 @@ class FlyGuard:
|
|||
|
||||
# ------------------------------------------------------------------ видеокарта
|
||||
|
||||
def _bring_up_gpu(self) -> None:
|
||||
"""Проверить CUDA, прогреть стадии и отдать их конвейеру (`_take_gpu`)."""
|
||||
try:
|
||||
from .device import get_device
|
||||
dev = get_device(self.p.device)
|
||||
if dev.startswith("cuda"):
|
||||
from .gpu import GpuStages
|
||||
stages = GpuStages(dev)
|
||||
stages.warmup()
|
||||
self._gpu_ready = (stages, dev)
|
||||
except Exception as e: # нет драйвера, не та видеокарта, нет памяти
|
||||
self._gpu_off(e)
|
||||
finally:
|
||||
self.gpu_pending = False
|
||||
|
||||
def _take_gpu(self) -> None:
|
||||
"""Перейти на видеокарту, если она готова. Только между кадрами.
|
||||
|
||||
Посреди кадра нельзя: ламина и кластеризация берут образ, который
|
||||
сетчатка оставила на видеокарте.
|
||||
"""
|
||||
if self._gpu_ready is not None:
|
||||
self.gpu, self.device = self._gpu_ready
|
||||
self._gpu_ready = None
|
||||
|
||||
@property
|
||||
def gpu_active(self) -> bool:
|
||||
"""Плотные стадии на видеокарте — уже или с ближайшего кадра."""
|
||||
return self.gpu is not None or self._gpu_ready is not None
|
||||
|
||||
def _gpu_off(self, exc: Exception) -> None:
|
||||
"""Сбой видеокарты: до конца работы считать на процессоре."""
|
||||
from .device import notify_cuda_error
|
||||
|
|
@ -439,6 +478,7 @@ class FlyGuard:
|
|||
self.time_jumps += 1
|
||||
self._last_stamp = pc.stamp
|
||||
self.frames_seen += 1
|
||||
self._take_gpu()
|
||||
|
||||
with t("retina"):
|
||||
img: RangeImage | None = None
|
||||
|
|
|
|||
|
|
@ -979,3 +979,103 @@ def test_pipeline_survives_gpu_failure_mid_frame():
|
|||
assert "device lost" in fg.gpu_error
|
||||
finally:
|
||||
reset_device_cache()
|
||||
|
||||
|
||||
class _CpuStages:
|
||||
"""Подмена видеокарты: те же стадии на процессоре, прогрев ждёт сигнала."""
|
||||
|
||||
gate = None
|
||||
|
||||
def __init__(self, device):
|
||||
self.calls = 0
|
||||
self._img = None
|
||||
|
||||
def warmup(self):
|
||||
if self.gate is not None:
|
||||
self.gate.wait()
|
||||
|
||||
def project(self, L, pc, cols):
|
||||
self.calls += 1
|
||||
self._img = L.project(pc, cols)
|
||||
return self._img
|
||||
|
||||
def lamina(self):
|
||||
from flyguard import lamina
|
||||
return lamina.process(self._img.r_near, self._img.valid, device="cpu")
|
||||
|
||||
def cluster_by_depth(self, mask, r, **kw):
|
||||
return cluster_by_depth(mask, r, **kw)
|
||||
|
||||
|
||||
def _wait_gpu(fg, timeout=5.0):
|
||||
import time
|
||||
t_end = time.monotonic() + timeout
|
||||
while fg.gpu_pending and time.monotonic() < t_end:
|
||||
time.sleep(0.01)
|
||||
assert not fg.gpu_pending, "поток видеокарты не закончил"
|
||||
|
||||
|
||||
def test_gpu_comes_up_in_background_while_cpu_counts(monkeypatch):
|
||||
"""Прогрев видеокарты идёт в фоне: кадры тем временем считает процессор,
|
||||
а на видеокарту конвейер переходит только с начала кадра — с тем же итогом.
|
||||
Зависший прогрев (здесь — до 20-го кадра) узел не держит."""
|
||||
import threading
|
||||
import flyguard.device as device
|
||||
import flyguard.gpu as gpu
|
||||
from flyguard.bag import Bag
|
||||
from flyguard.pipeline import FlyGuard, Params
|
||||
|
||||
if not DATA.exists():
|
||||
pytest.skip("нет записей")
|
||||
made = []
|
||||
|
||||
class Stages(_CpuStages):
|
||||
gate = threading.Event()
|
||||
|
||||
def __init__(self, dev):
|
||||
super().__init__(dev)
|
||||
made.append(self)
|
||||
|
||||
monkeypatch.setattr(device, "get_device", lambda preferred="auto": "cuda")
|
||||
monkeypatch.setattr(gpu, "GpuStages", Stages)
|
||||
frames = [pc for _, pc in Bag(next(p for p in DATA.iterdir() if p.is_dir())).frames(stop=30)]
|
||||
ref = FlyGuard(Params(device="cpu"))
|
||||
fg = FlyGuard(Params(device="auto"), gpu_background=True)
|
||||
try:
|
||||
for i, pc in enumerate(frames):
|
||||
if i == 20:
|
||||
assert fg.gpu_pending and fg.gpu is None and fg.device == "cpu"
|
||||
Stages.gate.set()
|
||||
_wait_gpu(fg)
|
||||
assert fg.gpu_active and fg.gpu is None # готова, но ждёт начала кадра
|
||||
a, b = ref.process(pc), fg.process(pc)
|
||||
assert (a is None) == (b is None)
|
||||
if a is not None:
|
||||
assert a.decision.detected == b.decision.detected
|
||||
assert [c.d for c in a.candidates] == [c.d for c in b.candidates]
|
||||
assert fg.gpu is made[0] and fg.device == "cuda"
|
||||
assert made[0].calls == len(frames) - 20
|
||||
finally:
|
||||
Stages.gate.set()
|
||||
|
||||
|
||||
def test_gpu_failing_to_warm_up_leaves_cpu(monkeypatch):
|
||||
"""Прогрев в фоне упал: конвейер остаётся на процессоре, причина — в gpu_error."""
|
||||
import flyguard.device as device
|
||||
import flyguard.gpu as gpu
|
||||
from flyguard.device import reset_device_cache
|
||||
from flyguard.pipeline import FlyGuard, Params
|
||||
|
||||
class Bad(_CpuStages):
|
||||
def warmup(self):
|
||||
raise RuntimeError("CUDA error: no kernel image is available (имитация)")
|
||||
|
||||
monkeypatch.setattr(device, "get_device", lambda preferred="auto": "cuda")
|
||||
monkeypatch.setattr(gpu, "GpuStages", Bad)
|
||||
try:
|
||||
fg = FlyGuard(Params(device="auto"), gpu_background=True)
|
||||
_wait_gpu(fg)
|
||||
assert fg.gpu is None and not fg.gpu_active and fg.device == "cpu"
|
||||
assert "no kernel image" in fg.gpu_error
|
||||
finally:
|
||||
reset_device_cache()
|
||||
|
|
|
|||
Loading…
Reference in a new issue