fe330ca279
См. spike-v2 (commit ad54305) + arch review 2026-05-15.
cudaStreamSynchronize-only фактически работает на single-host single-GPU
(0 torn в 4 scenarios PoC), но NVIDIA Programming Guide §3.2.8 не даёт
contractual гарантии. Переключаемся на cudaIpcEventHandle_t как default,
stream-sync остаётся опциональным fallback.
Net: +20µs mean latency, -3× max latency (predictable tail), future-proof
для multi-GPU.
426 lines
20 KiB
Markdown
426 lines
20 KiB
Markdown
# CUDA IPC Frame Bus — open-source FFmpeg-плагин
|
||
|
||
**Working name:** `cuframes` (рабочий, finalize при release)
|
||
**License:** LGPL-2.1+ (compatibility с FFmpeg)
|
||
**Статус:** дизайн-спецификация v0.1
|
||
**Дата:** 2026-05-14
|
||
|
||
## TL;DR
|
||
|
||
FFmpeg-плагин и C/C++ библиотека для zero-copy sharing декодированных
|
||
видеокадров между процессами через CUDA IPC. Один процесс делает decode на
|
||
GPU, кадр остаётся в VRAM, любое количество других процессов читает его без
|
||
копирования. Distributed как open-source.
|
||
|
||
Use case 0 (наш): Frigate decode'ит видео, `cctv-companion` consume'ит те же
|
||
кадры для mosaic-композиции на TV. Без двойного decode.
|
||
|
||
Use case N (общий): любая комбинация AI-pipeline + NVR + аналитика на одной
|
||
машине с NVIDIA GPU.
|
||
|
||
## 1. Контекст и мотивация
|
||
|
||
Сейчас типичный setup:
|
||
|
||
```
|
||
Camera → ffmpeg #1 (NVR, decode + record)
|
||
→ ffmpeg #2 (AI-detector, decode + inference)
|
||
→ ffmpeg #3 (custom analytics, decode + ...)
|
||
|
||
Каждый decode = NVDEC operation + VRAM. Inter-process передача
|
||
требует cudaMemcpy в host RAM и обратно. На 16 cameras × 25 fps ×
|
||
3 consumers = 75% VRAM-bandwidth впустую.
|
||
```
|
||
|
||
Существующие решения:
|
||
|
||
| | Что | Почему не подходит |
|
||
|---|---|---|
|
||
| NVIDIA DeepStream | Production-grade SDK, в т.ч. shared GPU buffers | Закрытое, vendor lock-in, требует переписи pipeline |
|
||
| GStreamer NVMM | NVIDIA Multimedia memory plugin | Inter-process только через DMA-buf, сложная интеграция |
|
||
| Custom CUDA IPC | Прямой `cudaIpcGetMemHandle` | Требует написать с нуля у каждого; нет интеграции с FFmpeg |
|
||
| Frigate +1 ffmpeg | Каждый сервис свой decode | Дублирование, что и побудило проект |
|
||
|
||
Пустая ниша: **простой FFmpeg-filter** который добавляется в любой
|
||
ffmpeg-pipeline и публикует декодированные кадры через CUDA IPC. Без
|
||
рерайта пайплайна, без vendor SDK, без специальной библиотеки на стороне
|
||
producer'а — просто extra-flag к ffmpeg.
|
||
|
||
## 1.bis Prior art / competitive analysis
|
||
|
||
Research проведён 2026-05-14, документирует что **ниша свободна**.
|
||
|
||
### Существующие технологии (building blocks, не plug-and-play)
|
||
|
||
| Проект | License | Что | Почему не аналог |
|
||
|---|---|---|---|
|
||
| **NVIDIA CUDA IPC API** | Proprietary headers, free use | Core `cudaIpcGetMemHandle`/`cudaIpcOpenMemHandle` | Сырой API, не плагин для FFmpeg — наш фундамент |
|
||
| **NVIDIA DeepStream** | Closed-source SDK | `NvDsBufSurface` shared-buffers + ML pipeline | Vendor lock-in, требует переписать pipeline в DeepStream API. Не FFmpeg-filter. |
|
||
| **NVIDIA VPF** ([github](https://github.com/NVIDIA/VideoProcessingFramework)) | Apache 2.0 | Python bindings вокруг NvCodec | **Single-process только**, inter-process не делает |
|
||
| **PyNvVideoCodec** | NVIDIA-proprietary, free | Python decoder с PyTorch zero-copy interop | Single-process |
|
||
| **dora-rs CUDA IPC** ([dora-rs.ai](https://dora-rs.ai/docs/guides/Development/Cuda/)) | Apache 2.0 | `torch_to_ipc_buffer()` / `ipc_buffer_to_ipc_handle()` для dataflow framework | Robotics-focused, работает на уровне torch tensors. Не FFmpeg-плагин. **Можем переиспользовать low-level подходы**. |
|
||
| **GStreamer NVMM** | LGPL | NVIDIA Multimedia Memory plugins | Inter-thread (intra-process); inter-process через DMA-buf только на Jetson |
|
||
| **BabitMF** (Bytedance) | Apache 2.0 | GPU filtering framework | Single-process intra-pipeline, не FFmpeg-filter |
|
||
| **Frigate go2rtc relay** | MIT | Re-stream compressed bytes между consumers | Не zero-copy — каждый consumer декодит снова |
|
||
|
||
### Подтверждение спроса в community
|
||
|
||
Threads в `blakeblackshear/frigate`, ровно описывающие нашу проблему:
|
||
|
||
- **#17033** — «Recommended GPU for Video Decoding with ~80 Cameras» — обсуждение saturation NVDEC engines при наивном double-decode
|
||
- **#20191** — «As soon as I add a second camera, nothing works» — пользователи натыкаются на decode-related issues
|
||
- **#21559** — «System with 25 cameras — which GPU» — все ответы про "купите больше железа", решения через shared decode нет
|
||
- **#21865** — «Combining Nvidia GPU and integrated GPU» — workaround через split decoders, что лишний раз доказывает что shared decode был бы полезен
|
||
|
||
### Проверка FFmpeg upstream
|
||
|
||
- FFmpeg-devel mailing list 2024-2026: **ноль** patches содержащих CUDA IPC inter-process filter
|
||
- FFmpeg master tree (8.0.1): CUDA-фильтры (`scale_cuda`, `bilateral_cuda`, `bwdif_cuda`, `libvmaf_cuda`, `overlay_cuda`) — все intra-process
|
||
- Search GitHub `vf_cuda_ipc | cudaipc | cuda_ipc_export`: zero forks/patches с этой функциональностью
|
||
|
||
### Вывод
|
||
|
||
**Ниша свободна.** Подтверждённый спрос (Frigate community), стабильные building-blocks (NVIDIA CUDA IPC API + dora-rs reference). Нет risk'а дублирования усилий. Sources проверены 2026-05-14.
|
||
|
||
## 2. Архитектура
|
||
|
||
### 2.1 Компоненты репозитория
|
||
|
||
```
|
||
cuframes/ ← OSS repo
|
||
├── filter/
|
||
│ └── vf_cuda_ipc_export.c FFmpeg filter (C, ~300 строк)
|
||
├── libcuframes/ producer/consumer ядро
|
||
│ ├── include/cuframes.h публичный C API
|
||
│ ├── src/
|
||
│ │ ├── producer.c ring buffer + Unix socket server
|
||
│ │ ├── consumer.c subscribe + cudaIpcOpenMemHandle
|
||
│ │ └── protocol.c wire format, версионирование
|
||
│ └── tests/
|
||
│ ├── pingpong.c smoke test: 1 producer ↔ 1 consumer
|
||
│ ├── multi_consumer.c 1 → N
|
||
│ ├── stress.c 24h, leak detection
|
||
│ └── bench.c latency, throughput
|
||
├── bindings/
|
||
│ ├── cpp/ C++ RAII wrapper
|
||
│ └── python/ pybind11 binding (numpy/torch interop)
|
||
├── examples/
|
||
│ ├── ffmpeg-basic/ минимальный FFmpeg → consumer
|
||
│ ├── opencv-consumer/ cv::cuda::GpuMat из shared frame
|
||
│ ├── pytorch-consumer/ torch.Tensor zero-copy
|
||
│ ├── frigate-integration/ Frigate config + Docker compose
|
||
│ └── mosaic-compositor/ наш cctv-companion (как demo)
|
||
├── docker/
|
||
│ ├── ffmpeg-cuframes/ FFmpeg с встроенным filter
|
||
│ └── ffmpeg-cuframes-frigate/ Drop-in replacement для Frigate's ffmpeg
|
||
├── docs/
|
||
│ ├── architecture.md эта спека после редактирования для OSS
|
||
│ ├── protocol.md wire format
|
||
│ ├── quickstart.md 5-минутный пример
|
||
│ ├── building-ffmpeg.md как пересобрать FFmpeg с filter
|
||
│ ├── frigate-howto.md конкретно под Frigate
|
||
│ ├── benchmarks.md numbers
|
||
│ └── faq.md
|
||
├── scripts/
|
||
│ ├── build-ffmpeg-patched.sh автоматизация upstream build
|
||
│ ├── package-deb.sh .deb для Ubuntu
|
||
│ └── ci/
|
||
└── .github/
|
||
└── workflows/ CI: build matrix Ubuntu 22.04/24.04, CUDA 12.x
|
||
```
|
||
|
||
### 2.2 Wire protocol (краткое)
|
||
|
||
Producer:
|
||
- Аллоцирует ring buffer (default 4 slots) в VRAM через `cudaMalloc`
|
||
- На каждый decoded frame — пишет в next slot, делает `cudaStreamSynchronize`
|
||
- Публикует `seq` атомарно
|
||
- Сигналит consumers через eventfd
|
||
|
||
Consumer:
|
||
- Connect к `/run/cuframes/<key>.sock`, handshake (negotiate format, ring size)
|
||
- Mmap shared memory header `/dev/shm/cuframes.<key>`
|
||
- Wait eventfd → read latest slot → `cudaIpcOpenMemHandle` → CUDA pointer
|
||
- Использует frame zero-copy → ACK через atomic bit в bitmap
|
||
|
||
Backpressure: drop oldest (newest-wins). Multi-consumer независимы — медленный
|
||
не блокирует быстрых.
|
||
|
||
Полный формат — в `docs/protocol.md`. Здесь — концепт.
|
||
|
||
### 2.3 FFmpeg filter API
|
||
|
||
```
|
||
-vf "[any pipeline before],cuda_ipc_export=key=NAME[:ring=N][:sync_event=1],[pipeline after]"
|
||
```
|
||
|
||
Опции:
|
||
- `key` *(required)* — имя shared resource. Должно совпадать у producer и consumer.
|
||
- `ring` — глубина ring buffer (default 4)
|
||
- `sync_event` — использовать `cudaIpcEventHandle_t` вместо `cudaStreamSynchronize`
|
||
(zero-copy sync, но experimental)
|
||
|
||
Filter:
|
||
- Принимает frame в CUDA hwframe-context
|
||
- Side-effect: публикует через `libcuframes`
|
||
- Passthrough — frame идёт дальше в pipeline без модификации (refcount++)
|
||
|
||
Пример:
|
||
|
||
```bash
|
||
ffmpeg -hwaccel cuda -hwaccel_output_format cuda \
|
||
-i rtsp://camera/stream \
|
||
-vf "scale_cuda=1920:1080,cuda_ipc_export=key=cam1" \
|
||
-c:v copy -f segment recording.mp4
|
||
```
|
||
|
||
В этой команде frame и публикуется через CUDA IPC, и пишется в архив. Один
|
||
decode для всего.
|
||
|
||
### 2.4 Consumer API
|
||
|
||
#### C
|
||
```c
|
||
#include <cuframes.h>
|
||
|
||
cuframes_subscriber_t* sub = cuframes_subscribe("cam1", "my-app");
|
||
|
||
cuframes_frame_t frame;
|
||
while (cuframes_next(sub, &frame, /*timeout_ms=*/100) == 0) {
|
||
// frame.cuda_ptr — device pointer, zero-copy
|
||
// frame.width, height, pitch_y, pitch_uv, format
|
||
// frame.pts_ns
|
||
|
||
do_inference_on_cuda(frame.cuda_ptr, frame.width, frame.height);
|
||
|
||
cuframes_release(&frame); // ACK обратно producer'у
|
||
}
|
||
cuframes_unsubscribe(sub);
|
||
```
|
||
|
||
#### C++ (RAII)
|
||
```cpp
|
||
#include <cuframes.hpp>
|
||
|
||
cuframes::Subscriber sub("cam1", "my-app");
|
||
|
||
while (auto frame = sub.next(std::chrono::milliseconds(100))) {
|
||
// frame->cuda_ptr — zero-copy
|
||
cv::cuda::GpuMat mat(frame->height, frame->width, CV_8UC2, // NV12
|
||
frame->cuda_ptr, frame->pitch_y);
|
||
// ... use mat
|
||
// frame destructor → ACK
|
||
}
|
||
```
|
||
|
||
#### Python (numpy/torch interop)
|
||
```python
|
||
import cuframes
|
||
|
||
with cuframes.Subscriber("cam1", "my-app") as sub:
|
||
for frame in sub:
|
||
# frame.tensor: torch.Tensor on CUDA, zero-copy
|
||
# frame.dlpack: __dlpack__ для numpy/torch/jax interop
|
||
output = model(frame.tensor)
|
||
```
|
||
|
||
### 2.5 Distribution и установка
|
||
|
||
#### Уровень 1 — FFmpeg patch
|
||
|
||
Минимальный патч к FFmpeg `libavfilter/`:
|
||
- `vf_cuda_ipc_export.c` (новый файл)
|
||
- `Makefile`: добавить filter
|
||
- `allfilters.c`: регистрация
|
||
|
||
Это **один commit** к FFmpeg-репо. Возможен upstream PR (надеемся примут;
|
||
прецедент есть — Nvidia donated `scale_cuda` и др.).
|
||
|
||
#### Уровень 2 — `libcuframes` runtime
|
||
|
||
Stand-alone shared library. Зависимости:
|
||
- `libcuda.so.1` (NVIDIA driver, и так у всех)
|
||
- `cudart` (CUDA runtime)
|
||
- Linux только (POSIX SHM, eventfd, AF_UNIX)
|
||
|
||
#### Уровень 3 — упаковка
|
||
|
||
- **Debian/Ubuntu**: `.deb` через `dpkg-buildpackage`
|
||
- **Arch**: AUR package
|
||
- **Docker**: pre-built image `ghcr.io/<org>/cuframes-ffmpeg:N` — drop-in
|
||
замена `/usr/lib/ffmpeg/N/bin/ffmpeg` в Frigate image
|
||
- **Frigate-specific image**: `ghcr.io/<org>/cuframes-frigate:0.17` —
|
||
Frigate stable-tensorrt с pre-installed cuframes ffmpeg
|
||
|
||
## 3. Use cases для документации
|
||
|
||
### 3.1 Наш кейс — `cctv-companion` (mosaic)
|
||
|
||
```
|
||
Frigate (ffmpeg с cuda_ipc_export filter):
|
||
Camera 1..16 → decode CUDA → publish key=cam_N
|
||
↘ свой Frigate pipeline (detect, record)
|
||
|
||
cctv-companion:
|
||
- подписан на cam_1..cam_16 через cuframes
|
||
- подписан на frigate/events через MQTT
|
||
- decision logic: какую сетку показать (full/2x2/4x4)
|
||
- composite через CUDA-ядра (CUB / OpenCV CUDA / custom kernels)
|
||
- encode → RTSP → TV
|
||
```
|
||
|
||
### 3.2 Multi-AI на одном decode
|
||
|
||
```
|
||
ffmpeg decode → publish key=cam_main
|
||
|
||
Subscriber A: YOLOv8 object detection
|
||
Subscriber B: face recognition (ArcFace)
|
||
Subscriber C: license plate OCR
|
||
Subscriber D: motion-triggered cloud upload
|
||
```
|
||
|
||
Все читают **тот же** decoded frame. Один NVDEC operation вместо четырёх.
|
||
|
||
### 3.3 Замена RAM-based mmstreams
|
||
|
||
Существующие custom-решения часто строят бypass через FIFO/pipe в RAM с
|
||
encoding между процессами. cuframes делает это zero-copy на CUDA.
|
||
|
||
## 4. План работ
|
||
|
||
### Phase 0 — Spike (3 дня)
|
||
- [ ] Прототип producer + consumer без FFmpeg: голый CUDA IPC + Unix socket
|
||
- [ ] Замерить bandwidth, latency на RTX 5090
|
||
- [ ] Verify cross-container (docker → docker) и host → docker
|
||
- [ ] Решить: sync через `cudaStreamSynchronize` или CUDA IPC events
|
||
- **Deliverable:** `tools/spike/pingpong.cpp` + измерения
|
||
|
||
### Phase 1 — libcuframes (1 неделя)
|
||
- [ ] Producer C API
|
||
- [ ] Consumer C API
|
||
- [ ] Wire-protocol implementation
|
||
- [ ] Unit tests (pingpong, multi-consumer, crash-recovery)
|
||
- [ ] Stress test rig (24h-runner)
|
||
- **Deliverable:** Self-contained library + tests passing
|
||
|
||
### Phase 2 — FFmpeg filter (1-2 недели)
|
||
- [ ] `vf_cuda_ipc_export.c` в стиле FFmpeg
|
||
- [ ] Patch script для применения к FFmpeg N.x (4.4, 5.x, 6.x, 7.x)
|
||
- [ ] Build на CUDA 12.0+ matrix
|
||
- [ ] Integration test: `ffmpeg → cuframes filter → consumer` end-to-end
|
||
- **Deliverable:** Patched FFmpeg builds (Ubuntu 22.04, 24.04, in Docker)
|
||
|
||
### Phase 3 — Bindings (1 неделя)
|
||
- [ ] C++ RAII wrapper
|
||
- [ ] Python binding через pybind11
|
||
- [ ] PyTorch tensor interop (DLPack)
|
||
- **Deliverable:** `pip install cuframes` (через `pyproject.toml`)
|
||
|
||
### Phase 4 — Docker / packaging (3-5 дней)
|
||
- [ ] `ghcr.io/<org>/cuframes-ffmpeg:N.x-cuda12.X`
|
||
- [ ] `ghcr.io/<org>/cuframes-frigate:0.17` (Frigate с patched ffmpeg)
|
||
- [ ] `.deb` packages для Ubuntu 22.04/24.04
|
||
- [ ] CI matrix: build всех вариантов на каждый push
|
||
- **Deliverable:** `docker pull` ready-to-use
|
||
|
||
### Phase 5 — Naш use case: cctv-companion (1 неделя)
|
||
- [ ] Минимальный C++ daemon
|
||
- [ ] Subscribe на N камер
|
||
- [ ] MQTT subscriber на Frigate events
|
||
- [ ] Smart-grid logic (existing в проекте, переиспользовать)
|
||
- [ ] Mosaic compositor на CUDA (cv::cuda::warpAffine для placement)
|
||
- [ ] Output через `nvenc` → RTSP сервер для TV
|
||
- **Deliverable:** Working setup на R9 со всеми 16 камерами
|
||
|
||
### Phase 6 — OSS launch (3-5 дней)
|
||
- [ ] README с GIF (visual что это даёт)
|
||
- [ ] Quickstart (5 минут от `docker pull` до working demo)
|
||
- [ ] Benchmarks page с numbers
|
||
- [ ] HackerNews / r/selfhosted / r/homeassistant анонс
|
||
- [ ] Submit upstream PR в FFmpeg
|
||
|
||
**Итого: ~6-8 недель** для одного разработчика, **3-4 недели** для двоих.
|
||
|
||
## 5. Acceptance criteria для v1.0
|
||
|
||
- [ ] FFmpeg-filter работает на 4.4, 5.x, 6.x, 7.x (Frigate diversity)
|
||
- [ ] Latency producer→consumer ≤ 1 ms (p99)
|
||
- [ ] 16 cameras × 4 consumers × 25 fps без drops 24h
|
||
- [ ] Frigate (stable-tensorrt) drop-in replacement работает
|
||
- [ ] PyTorch consumer: `frame.tensor` без копий на CUDA
|
||
- [ ] Docker images < 4 GB сжатые
|
||
- [ ] CI green на Ubuntu 22.04, 24.04
|
||
- [ ] `pip install cuframes` works на Python 3.10/3.11/3.12
|
||
- [ ] README с benchmark vs naive double-decode
|
||
|
||
## 6. Open questions для discussion
|
||
|
||
1. **Name.** `cuframes` рабочий, варианты: `vramshare`, `cudapipe`,
|
||
`nvframe-bus`, `cuda-frame-bridge`, `framesink`. Финализируем перед public.
|
||
|
||
2. **License.** LGPL-2.1+ для совместимости с FFmpeg LGPL и spirit (linkable
|
||
с MIT/Apache). Sourcing — на LGPL.
|
||
|
||
3. **Upstream FFmpeg PR vs custom build.** Best-effort вклад в FFmpeg, но не
|
||
блокер — distrubution через own Docker images работает независимо.
|
||
|
||
4. **AMD/ROCm support.** Out of scope для v1. После v1 — если будет
|
||
community-interest, добавить через HIP IPC.
|
||
|
||
5. **Multi-GPU.** Producer и consumer на разных CUDA devices — CUDA IPC
|
||
ограничивает same device. В v1 — ограничение, документировать.
|
||
|
||
6. **Sync semantics.** ✅ **РЕШЕНО** (architectural review 2026-05-15 + spike-v2):
|
||
Default — **CUDA IPC events** (`cudaIpcEventHandle_t`). Rationale:
|
||
- NVIDIA Programming Guide §3.2.8 явно требует cross-process sync на стороне
|
||
application; `cudaStreamSynchronize` producer'а гарантий не даёт contractually.
|
||
- Spike-v2 measurements (`docs/measurements/spike-v2/`) показали: на нашем
|
||
single-host single-GPU setup stream-only sync **работает** (0 torn frames
|
||
на 1500 frames × 4 scenarios), но это **happens-to-work**, не contract.
|
||
- Events добавляют mean +20µs overhead но дают **predictable tail latency**
|
||
(max 5.2ms vs 14.7ms для FHD@60fps) и future-proof для multi-GPU.
|
||
- API option `--sync=stream` оставлен как fallback для legacy / debugging.
|
||
|
||
7. **Где хостить.** GitHub `<personal-org>/cuframes`. Возможно перенос в
|
||
`frigate-community/cuframes` после adoption.
|
||
|
||
## 7. Риски
|
||
|
||
| Риск | Митигация |
|
||
|---|---|
|
||
| Upstream FFmpeg PR не примут | Distributable через custom builds, не блокирует release |
|
||
| CUDA IPC subtle bugs | Phase 0 spike до commit'а; Stress test 24h |
|
||
| Frigate не пропустит custom filter | `frigate-cuframes` docker image — pre-patched, готов к использованию |
|
||
| Conflict с NVIDIA DeepStream APIs | None expected (разные scope) |
|
||
| Community не подхватит | Это secondary — primary value для нас (cctv-companion работает) |
|
||
|
||
## 8. Зависимости (build-time)
|
||
|
||
- CUDA Toolkit ≥ 12.0
|
||
- FFmpeg sources (target versions 4.4, 5.x, 6.x, 7.x)
|
||
- pkg-config, autoconf, make
|
||
- Optional: OpenCV CUDA (для C++ examples)
|
||
- Optional: pybind11 (для Python binding)
|
||
|
||
## 9. Связь с этим репозиторием (cctv)
|
||
|
||
Текущий `cpp/apps/cctv-processor` — full-featured C++ NVR с своим decode +
|
||
mosaic + RTSP-server. После v1 cuframes:
|
||
|
||
- `cuframes` выпускается **как отдельный** opensource-репо
|
||
(`/home/claude/projects/cuframes/`)
|
||
- В `cctv` создаётся новый `cpp/apps/cctv-companion/` — тонкий клиент к
|
||
Frigate + cuframes (не Replaces existing processor, а соседствует)
|
||
- Существующий cctv-processor — оставить как baseline / fallback для
|
||
setups без Frigate
|
||
|
||
## 10. Готовность к next step
|
||
|
||
После approval спеки:
|
||
1. `git init /home/claude/projects/cuframes/` — пустой OSS-репо
|
||
2. Phase 0 spike — 3 дня PoC
|
||
3. После Phase 0 — review результатов, корректировка дизайна (если CUDA IPC
|
||
повёл себя не как ожидали)
|
||
4. Phase 1+ по плану
|