видеокарта прогревается в фоне, пока считает процессор
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
|
export.py 3D-рамки, время до столкновения, маркеры RViz
|
||||||
synth.py вставка предметов трассировкой лучей
|
synth.py вставка предметов трассировкой лучей
|
||||||
tools/ обучение, оценка, разбор, полигон с аугментациями
|
tools/ обучение, оценка, разбор, полигон с аугментациями
|
||||||
tests/ 50 тестов, запускаются без данных и без ROS
|
tests/ 52 теста, запускаются без данных и без ROS
|
||||||
docs/ методика и результаты
|
docs/ методика и результаты
|
||||||
artifacts/ обученные модели
|
artifacts/ обученные модели
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -3318,3 +3318,66 @@ cuBLAS и прочие не нужны, но PyTorch без них не загр
|
||||||
`TORCH_WHEELS` (`docker/fetch_wheels.py`, `docker/wheels/README.md`). Через
|
`TORCH_WHEELS` (`docker/fetch_wheels.py`, `docker/wheels/README.md`). Через
|
||||||
контекст сборки колёса не передаются: первая попытка так делала, и контекст в
|
контекст сборки колёса не передаются: первая попытка так делала, и контекст в
|
||||||
4.1 ГБ плюс слой с колёсами плюс установка чуть не заняли весь диск C:.
|
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
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import threading
|
||||||
import time
|
import time
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
|
|
@ -225,7 +226,8 @@ class Params:
|
||||||
# 'auto' — видеокарта, если PyTorch её видит, иначе процессор; 'cuda';
|
# 'auto' — видеокарта, если PyTorch её видит, иначе процессор; 'cuda';
|
||||||
# 'cpu'. Результат одинаков: на видеокарте те же операции в тех же типах.
|
# 'cpu'. Результат одинаков: на видеокарте те же операции в тех же типах.
|
||||||
# Любой сбой видеокарты переводит конвейер на процессор до конца работы,
|
# Любой сбой видеокарты переводит конвейер на процессор до конца работы,
|
||||||
# кадр досчитывается там же. EXPERIMENTS п. 21.
|
# кадр досчитывается там же. Узел поднимает видеокарту в фоне
|
||||||
|
# (`gpu_background`), считая первые кадры на процессоре. EXPERIMENTS п. 21.
|
||||||
device: str = "auto"
|
device: str = "auto"
|
||||||
enable_motion: bool = True
|
enable_motion: bool = True
|
||||||
enable_memory: bool = True
|
enable_memory: bool = True
|
||||||
|
|
@ -268,19 +270,26 @@ class FlyGuard:
|
||||||
memory: MushroomBody | None = None,
|
memory: MushroomBody | None = None,
|
||||||
layout: ScanLayout | None = None,
|
layout: ScanLayout | None = None,
|
||||||
readout: "MbonReadout | None" = None,
|
readout: "MbonReadout | None" = None,
|
||||||
track_readout=None):
|
track_readout=None,
|
||||||
|
gpu_background: bool = False):
|
||||||
self.p = params or Params()
|
self.p = params or Params()
|
||||||
from .device import get_device
|
self.device = "cpu"
|
||||||
self.device = get_device(self.p.device)
|
|
||||||
self.gpu = None # плотные стадии на видеокарте (gpu.GpuStages)
|
self.gpu = None # плотные стадии на видеокарте (gpu.GpuStages)
|
||||||
self.gpu_error: str | None = None
|
self.gpu_error: str | None = None
|
||||||
if self.device.startswith("cuda"):
|
self._gpu_ready = None # (стадии, устройство) — готовы, ждут начала кадра
|
||||||
try:
|
# gpu_background: видеокарта поднимается в своём потоке, а кадры тем
|
||||||
from .gpu import GpuStages
|
# временем считает процессор. Первый запуск ядер CUDA бывает долгим —
|
||||||
self.gpu = GpuStages(self.device)
|
# в Docker под WSL 15 с на каждый новый контейнер, — и узел всё это время
|
||||||
self.gpu.warmup()
|
# не был подписан и терял кадры. Зависни драйвер совсем — узел так и
|
||||||
except Exception as e: # нет драйвера, не та видеокарта, нет памяти
|
# останется на процессоре, а не встанет.
|
||||||
self._gpu_off(e)
|
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.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.readout = readout if (readout is not None and self.p.enable_mbon) else None
|
||||||
self.layout_full = layout
|
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:
|
def _gpu_off(self, exc: Exception) -> None:
|
||||||
"""Сбой видеокарты: до конца работы считать на процессоре."""
|
"""Сбой видеокарты: до конца работы считать на процессоре."""
|
||||||
from .device import notify_cuda_error
|
from .device import notify_cuda_error
|
||||||
|
|
@ -439,6 +478,7 @@ class FlyGuard:
|
||||||
self.time_jumps += 1
|
self.time_jumps += 1
|
||||||
self._last_stamp = pc.stamp
|
self._last_stamp = pc.stamp
|
||||||
self.frames_seen += 1
|
self.frames_seen += 1
|
||||||
|
self._take_gpu()
|
||||||
|
|
||||||
with t("retina"):
|
with t("retina"):
|
||||||
img: RangeImage | None = None
|
img: RangeImage | None = None
|
||||||
|
|
|
||||||
|
|
@ -979,3 +979,103 @@ def test_pipeline_survives_gpu_failure_mid_frame():
|
||||||
assert "device lost" in fg.gpu_error
|
assert "device lost" in fg.gpu_error
|
||||||
finally:
|
finally:
|
||||||
reset_device_cache()
|
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