f10413580d
Реальный тест на 192.168.88.98 (1920x1080 HEVC, 25fps) показал: для отдельных consumer-container'ов недостаточно ipc=container:X — нужен также pid=container:X, иначе cudaIpcOpenEventHandle падает с invalid device context. CUDA driver валидирует IPC peer через /proc/<pid>/... E2E на реальной камере проверен: publisher (отдельный контейнер) -> consumer (docker exec): 250 frames, 0 gaps publisher (отдельный контейнер) -> consumer (отдельный с pid+ipc): 200, 0 gaps Обновлено: - docs/integration.md compose snippet, verification, troubleshooting section - docker-compose.example.yml — добавлен pid: container:cuframes-cam-test - README.md quickstart — добавлен --pid в docker run subscriber Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
269 lines
12 KiB
Markdown
269 lines
12 KiB
Markdown
# Integration guide
|
||
|
||
Этот guide описывает, как использовать cuframes для устранения дублирующего
|
||
GPU-декодирования между несколькими consumer'ами одного RTSP-потока.
|
||
|
||
## Целевой сценарий (motivation)
|
||
|
||
В типичной CCTV-системе один и тот же RTSP-stream декодируется несколько раз:
|
||
|
||
```
|
||
Камера ──► RTSP ──► Frigate (decode #1: detection + recording)
|
||
─► mosaic-сервер (decode #2: компоновка сетки)
|
||
─► AI-скрипт (decode #3: классификация / OCR)
|
||
```
|
||
|
||
На 16 камер × 25 fps × 3 consumer'а = 1200 NVDEC-операций/сек. RTX 5090 имеет
|
||
~3 NVDEC-движка, но шина PCIe и memory bandwidth становятся узким местом.
|
||
|
||
С cuframes:
|
||
|
||
```
|
||
Камера ──► cuframes-rtsp-source ──► CUDA frame в /dev/shm + cudaIpcEvent
|
||
│
|
||
├──► Frigate (zero-copy)
|
||
├──► mosaic-сервер (zero-copy)
|
||
└──► AI-скрипт (zero-copy)
|
||
```
|
||
|
||
Decode выполняется **один раз** на источник, потребители получают тот же CUDA
|
||
device pointer без копий.
|
||
|
||
## Текущие limitations v0.1
|
||
|
||
- **Frigate** (по состоянию на 0.17) **не имеет** plugin-точки для приёма
|
||
готовых CUDA-frames. Чтобы убрать Frigate decode полностью, нужен:
|
||
- либо FFmpeg-filter `vf_cuda_ipc_input` (planned для cuframes v0.2 — требует
|
||
patch FFmpeg upstream и пересборку Frigate's bundled ffmpeg),
|
||
- либо Frigate-plugin (требует upstream работы с командой Frigate).
|
||
- В v0.1 практическое улучшение: **исключить decode для всех custom consumer'ов
|
||
кроме Frigate** (то есть cctv-processor, AI-скрипты — на cuframes; Frigate
|
||
остаётся как есть, со своим decode).
|
||
|
||
Это уже даёт значительную экономию: было 1×Frigate + N×consumer decode'ов,
|
||
стало 1×Frigate + 1×cuframes-rtsp-source (один на все consumer'ы).
|
||
|
||
## Сценарий 1: cuframes-rtsp-source + cctv-processor (FRIGATE остаётся)
|
||
|
||
### docker-compose.yml
|
||
|
||
```yaml
|
||
services:
|
||
# Один источник на камеру — публикует декодированный поток через cuframes IPC
|
||
cuframes-cam-parking:
|
||
image: gx/cuframes-rtsp-source:0.1
|
||
restart: unless-stopped
|
||
runtime: nvidia
|
||
environment:
|
||
NVIDIA_VISIBLE_DEVICES: all
|
||
NVIDIA_DRIVER_CAPABILITIES: compute,video,utility
|
||
# CRITICAL: --ipc=shareable для cross-container CUDA IPC
|
||
ipc: shareable
|
||
shm_size: 1g
|
||
volumes:
|
||
- cuframes_sock:/run/cuframes
|
||
command:
|
||
- --rtsp=rtsp://admin:${CAM_PASS}@192.168.88.98:554/cam/realmonitor?channel=1&subtype=0
|
||
- --key=cam-parking
|
||
- --ring=6
|
||
- --realtime # не нужен для RTSP (real-time источник), оставлен для file://
|
||
|
||
# Frigate (как и был — со своим decode на main+sub streams)
|
||
frigate:
|
||
image: ghcr.io/blakeblackshear/frigate:stable-tensorrt
|
||
# ... как обычно
|
||
|
||
# cctv-processor — подписывается на cuframes (без отдельного RTSP decode)
|
||
cctv-backend:
|
||
image: gx/cctv-processor:cuda
|
||
restart: unless-stopped
|
||
runtime: nvidia
|
||
# CRITICAL: shared IPC + PID namespace с publisher'ом (см. ниже)
|
||
ipc: container:cuframes-cam-parking
|
||
pid: container:cuframes-cam-parking
|
||
volumes:
|
||
- cuframes_sock:/run/cuframes:ro
|
||
environment:
|
||
# cuframes-keys для backend'а:
|
||
CCTV_SOURCES: cuframes:cam-parking,cuframes:cam-front-gate,...
|
||
|
||
volumes:
|
||
cuframes_sock:
|
||
```
|
||
|
||
**Важно — оба флага обязательны** для cross-container CUDA IPC:
|
||
|
||
| Флаг | Зачем |
|
||
|---|---|
|
||
| `ipc: container:<publisher>` | shared `/dev/shm` (нужен для `shm_open` под header/sockets) |
|
||
| `pid: container:<publisher>` | CUDA driver валидирует IPC peer через `/proc/<pid>/...`; без этого `cudaIpcOpenEventHandle` падает с `invalid device context` |
|
||
|
||
Альтернативы:
|
||
- Запускать consumer внутри того же container'а через `docker exec` (наследует все namespaces) — удобно для отладки.
|
||
- `--ipc=host --pid=host` — убирает namespacing вообще, но ослабляет изоляцию (не рекомендуется в production).
|
||
|
||
### Изменения в cctv-processor
|
||
|
||
Нужно добавить новый Source-тип (рядом с RtspSource) — `CuframesSource`:
|
||
|
||
```cpp
|
||
// cpp/apps/cctv-processor/src/sources/cuframes_source.hpp
|
||
#include <cuframes/cuframes.hpp>
|
||
|
||
class CuframesSource : public IVideoSource {
|
||
public:
|
||
CuframesSource(const std::string &key) : key_(key) {
|
||
cuframes::SubscriberOptions opt;
|
||
opt.key = key;
|
||
opt.consumer_name = "cctv-processor";
|
||
opt.mode = CUFRAMES_MODE_NEWEST_ONLY;
|
||
sub_ = std::make_unique<cuframes::Subscriber>(opt);
|
||
cudaStreamCreate(&stream_);
|
||
}
|
||
|
||
// Вызывается processing-loop'ом
|
||
std::optional<GpuFrame> nextFrame() override {
|
||
auto f = sub_->next(stream_, 100); // 100ms timeout
|
||
if (!f) return std::nullopt;
|
||
// cudaStreamWaitEvent уже сделан внутри next() — frame готов на stream_
|
||
return GpuFrame{
|
||
.cuda_ptr = f->cuda_ptr(),
|
||
.width = f->width(),
|
||
.height = f->height(),
|
||
.pitch_y = f->pitch_y(),
|
||
.pitch_uv = f->pitch_uv(),
|
||
.seq = f->seq(),
|
||
.pts_ns = f->pts_ns(),
|
||
.stream = stream_,
|
||
._release = std::move(f), // RAII release при destroy
|
||
};
|
||
}
|
||
|
||
private:
|
||
std::string key_;
|
||
std::unique_ptr<cuframes::Subscriber> sub_;
|
||
cudaStream_t stream_;
|
||
};
|
||
```
|
||
|
||
Конфиг `cameras.json` — добавить альтернативный source-тип:
|
||
|
||
```jsonc
|
||
{
|
||
"cameras": [
|
||
{
|
||
"id": "parking",
|
||
"source_type": "cuframes", // вместо "rtsp"
|
||
"cuframes_key": "cam-parking",
|
||
// rtsp_url больше не нужен — он используется cuframes-rtsp-source'ом
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
## Сценарий 2: AI-скрипт на Python (subscriber)
|
||
|
||
Python-bindings — в Phase 3 cuframes. Сейчас простой workaround через
|
||
ctypes:
|
||
|
||
```python
|
||
import ctypes
|
||
lib = ctypes.CDLL("libcuframes.so")
|
||
# ... wrap нужные функции — см. include/cuframes/cuframes.h
|
||
```
|
||
|
||
Или: writer simple C-обёртку, которая принимает callback и публикует
|
||
данные через ZMQ / shared memory в python-process.
|
||
|
||
## Сценарий 3: Замена Frigate decode (v0.2+)
|
||
|
||
Целевой сценарий — Frigate тоже подписан на cuframes. Реализуется через
|
||
один из двух путей:
|
||
|
||
### Путь A: FFmpeg filter
|
||
|
||
Добавить out-of-tree filter `vf_cuda_ipc_input` который читает кадр из
|
||
cuframes ring и эмитит AVFrame в pipeline. Frigate использует ffmpeg для
|
||
RTSP/decode — заменяем "RTSP→decode→detect" на
|
||
"cuframes_ipc_input→detect" (без decode'а вообще).
|
||
|
||
Требования:
|
||
- Patch ffmpeg sources (libavfilter/vf_cuda_ipc_input.c + Makefile)
|
||
- Сборка кастомного Frigate-образа с patched ffmpeg
|
||
- Тестирование на совместимость с Frigate's pipeline assumptions
|
||
|
||
### Путь B: Frigate plugin
|
||
|
||
Engage с upstream Frigate чтобы добавить custom Source-type ("cuframes://").
|
||
Это требует Python-API изменений в Frigate's source layer.
|
||
|
||
## Verification checklist
|
||
|
||
После настройки убедитесь:
|
||
|
||
```bash
|
||
# 1. Publisher запущен и socket существует
|
||
ls -la /run/cuframes/cam-parking.sock
|
||
ls -la /dev/shm/cuframes-cam-parking
|
||
|
||
# 2. Контейнеры в одном IPC и PID namespace
|
||
docker inspect cuframes-cam-parking cctv-backend \
|
||
-f '{{.Name}} ipc={{.HostConfig.IpcMode}} pid={{.HostConfig.PidMode}}'
|
||
# Publisher: ipc=shareable pid=(default)
|
||
# Consumer: ipc=container:cuframes-cam-parking pid=container:cuframes-cam-parking
|
||
|
||
# 3. Subscriber connect успешен
|
||
docker exec cctv-backend /usr/local/bin/sub_count --key cam-parking --max-frames 10
|
||
# Ожидаем:
|
||
# [sub_count] connected to 'cuframes-cam-parking'
|
||
# [sub_count] received=10 gaps=0 elapsed=0.4s avg_fps=25
|
||
|
||
# 4. NVDEC utilization — должно быть N decodes, а не N*M
|
||
nvidia-smi dmon -s u
|
||
# Колонка %dec должна показать decode-нагрузку одного instance на камеру
|
||
```
|
||
|
||
## Troubleshooting
|
||
|
||
### `Subscriber::create: timeout`
|
||
Subscriber не нашёл publisher. Причины:
|
||
- Publisher не запущен или crashed — проверь `docker logs cuframes-cam-parking`
|
||
- Socket-файл не volumes'нут в consumer-контейнер — добавь `volumes:
|
||
- cuframes_sock:/run/cuframes:ro` в consumer'е
|
||
- IPC namespace не совпадает — см. checklist пункт 2
|
||
|
||
### `cudaIpcOpenEventHandle: invalid device context`
|
||
Проявляется в **отдельном** consumer-container'е после успешного handshake (socket
|
||
открыт, header валиден, но open event handle не проходит).
|
||
|
||
Причина: CUDA driver валидирует sender'а IPC peer'а через `/proc`. Если PID
|
||
namespace не совпадает, sender невидим — context считается невалидным.
|
||
|
||
Fix: добавить `pid: container:<publisher>` в consumer's compose service (рядом
|
||
с `ipc: container:<publisher>`). Проверено на CUDA 13.0 + driver 555+.
|
||
|
||
### `cudaIpcOpenMemHandle returned 'invalid device pointer'`
|
||
- Контейнеры в РАЗНЫХ ipc namespace — должны быть в одном (через
|
||
`ipc: container:<publisher>` или общий `ipc: shareable`)
|
||
- Subscriber работает на другом CUDA device — `--cuda-device` должен совпадать
|
||
у publisher и subscriber (одно и то же физическое GPU)
|
||
|
||
### Высокая latency (>50ms tail)
|
||
- Subscriber slow — frames копятся в ring, по политике DROP_OLDEST они
|
||
пропускаются. Используй `CUFRAMES_MODE_NEWEST_ONLY` (default) — это нормально
|
||
для real-time системы.
|
||
- При STRICT_ORDER + STRICT_WAIT — slow consumer блокирует publisher. Не
|
||
рекомендуется для CCTV.
|
||
|
||
### Frigate показывает чёрный экран после интеграции
|
||
- Frigate не подключён к cuframes (v0.1 — это not yet supported). В v0.1
|
||
Frigate должен оставаться на своём RTSP decode (см. Сценарий 1).
|
||
|
||
## Roadmap
|
||
|
||
- **v0.1** (текущая): standalone publisher/subscriber, C/C++ API, examples.
|
||
- **v0.2**: FFmpeg filter `vf_cuda_ipc_input` (out-of-tree), Python bindings.
|
||
- **v0.3**: NVENC-bridge для re-encode подписчиков, Frigate plugin
|
||
proof-of-concept.
|
||
- **v1.0**: stable ABI, multi-GPU, documented Frigate integration.
|