Реализует subscriber-side wrapper над cuframes_subscriber_* и
cuframes_frame_* C API.
Что добавлено:
- CuframesFrame — owning RAII wrapper над cuframes_frame_t*
- properties: cuda_ptr, format, width, height, pitch_y, pitch_uv,
seq, pts_ns, released
- release() idempotent
- context manager (__enter__/__exit__) — release при выходе
- после release() property access бросает CuframesError
- CuframesSubscriber — owning RAII wrapper над cuframes_subscriber_t*
- конструктор с key/consumer_name/mode/cuda_device/connect_timeout_ms
- next_frame(timeout_ms) → CuframesFrame
- close() idempotent
- context manager
- GIL released на блокирующих вызовах (create, next_frame)
- subscribe() — module-level factory shortcut
Архитектурные решения:
- GIL release в py::gil_scoped_release на subscriber_create и _next —
чтобы другие Python потоки могли работать пока ждём frame
- consumer_stream передаётся как nullptr в Phase 0 (default stream);
per-subscriber stream в task #201
- Frame держит raw pointer на subscriber, refcount Python-стороной;
если subscriber уничтожен раньше, frame.release() становится no-op
Smoke tests расширены до 8 — добавлены проверки exposed API и
error mapping на subscribe к несуществующему publisher'у.
Verify: pytest tests/test_smoke.py — 8/8 passed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
py::exception<T>(...) уже возвращает Python class object. Дополнительный
.attr("__class__") давал metaclass (type), из-за чего issubclass()
проверка для всех subexc возвращала False.
Verify: pytest tests/test_smoke.py — 5/5 passed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
При сборке cuframes как подпроекта родительского CMake-проекта
(add_subdirectory) CMAKE_SOURCE_DIR указывает на корень родителя,
а не cuframes. Из-за этого target_include_directories cuframes
получал неверный путь и компиляция падала с
fatal error: cuframes/cuframes.h: No such file or directory
PROJECT_SOURCE_DIR резолвится в каталог project(), то есть всегда
указывает на корень cuframes независимо от способа подключения.
Standalone-сборка ведёт себя как раньше — оба пути одинаковы.
Нужен для loop-publisher.sh wrapper в cctv stack — heartbeat и alert MQTT
publish. 4.5 MB добавил, runtime image теперь ~590 MB. Без него wrapper
silent fail на mqtt_alert/mqtt_state (но retry-loop работает).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Заменяет cudaMalloc + cudaIpcGetMemHandle на cuMemCreate (VMM) +
cuMemExportToShareableHandle(POSIX_FILE_DESCRIPTOR). FDs передаются consumer'у
через sendmsg(SCM_RIGHTS) в handshake. Frigate (s6-overlay не даёт share PID)
и любой другой consumer работают БЕЗ pid namespace share — только volume mount
unix socket'a /run/cuframes и IPC share для /dev/shm header.
Sync: cudaEventRecord+IPC events → cuStreamSynchronize в do_publish.
Producer ждёт ~1 ms что stream flush'нулся, потом atomic_store(seq).
Consumer читает seq через memory_order_acquire и копирует DtoD без
event wait — HW coherence гарантирована на одном GPU.
ABI break (согласован с user'ом):
- magic 0xCC7C1DCC → 0xCC7C1DCE (старые consumers fail cleanly)
- protocol V3 → V4
- libcuframes.so.0 SOVERSION остаётся, но .so.0.3.0 → .so.0.4.0
- EXTERNAL ownership убран (VMM требует cuMemCreate-allocated memory,
нельзя export'нуть произвольный cudaMalloc-pointer как POSIX FD)
- cuframes-rtsp-source переведён на LIBRARY mode + один D2D memcpy
в acquire'нутый slot (overhead малый — публишер всё равно делал такой
D2D из FFmpeg hwframe pool в EXTERNAL pool раньше)
Размер: granularity 2 MB на 5090 → NV12 1920×1080 (~3.1 MB) округляется до
4 MB, +1 MB на slot × 16 × 4 камеры = +64 MB VRAM. Терпимо.
Packet ring (cuframes_packets://) НЕ затронут — отдельный SHM с своим
magic, работает как раньше.
PoC + smoke в spike/:
- vmm_fd_pingpong/ — minimal cuMemCreate+FD round-trip
- smoke_v04/ — full publisher+subscriber, 100/100 frames без pid share
Base image: Dockerfile.runtime → CUDA 12.4 (был 13.0). Matching prod
pipeline + Frigate base, иначе libcudart conflict при load.
Compose stack (localhost-infra repo) — параллельный commit:
- убран pid: container:cuframes-pub-parking из subscribers
- image теги: gx/cuframes:0.4, gx/cuda-grid-pipeline:phase8,
gx/frigate:cuframes-v0.4
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Bug: cudaEventRecord(event[slot]) overwrites previous state каждый publish.
Когда producer wraps ring (~640ms при ring=16), event[slot] re-recorded для
new content. Consumer's pending cudaStreamWaitEvent satisfied новым signal —
consumer reads slot[slot_idx] thinking it's target_seq, реально получает
seq+ring_size content (stale-by-1-wrap drift).
После 50k+ wraps в long-running pipeline (9h uptime) drift накапливается:
output stream имеет 60-70% duplicate frames (vs 10% сразу после restart).
Симптом: TV picture freezes на 1-2 sec периодически. Encoder fps=25 stable
(content duplicates same PTS-advance), но motion choppy на 8-9 fps real.
Fix: unconditional post-sync verify (atomic re-read slot.seq после event wait).
Если producer wrap occurred — slot.seq != target_seq → continue к новому
target_seq. Cheap (one atomic load), correctness > perf.
Verified: после deploy с fresh pipeline, 18-sec sample = 4% duplicates
(vs 8.4% при том же setup но без fix).
Proper v0.4 fix: per-slot+per-publish event pool с unique handle per cycle.
Текущий v0.3.3 — sufficient mitigation для current production scale.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Opt-in для STRICT_WAIT policy (default остаётся DROP_OLDEST).
Use case STRICT_WAIT:
Frame integrity критичен (e.g. recording, frame-accurate analytics).
Producer ждёт ack от всех subscribers перед wrap ring → no torn frames.
Trade-off: slow consumer задерживает all (default 200ms timeout затем
subscriber dropped from bitmap).
Use case DROP_OLDEST (default):
Low-latency real-time display (TV grid). Producer wraps freely; v0.3
per-slot CUDA events закрывают race без waiting.
Validation: policy=wait + ack-timeout-ms<=0 = infinite hold dead consumer —
warning + force к 200ms safe default.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Bug: handshake_subscriber assigned bit + activated slot но НЕ tracked
client_fd. Когда subscriber container exited, socket closed on client side
но producer не detected → bit оставался set forever → после 32 connections
subscribe_create('cam-X'): too many subscribers (max 32).
Симптом в production: каждый pipeline recreate accumulated 1 stale subscriber.
После 4-5 recreate операций publishers перестали accept new pipeline →
"too many subscribers" crash loop.
Fix: после успешного handshake spawn detached pthread monitoring socket
via blocking recv(). recv() returns 0 (EOF) когда other side closes —
monitor clears bit (subscriber_bitmap &= ~(1<<bit)) + state[bit] = 0,
closes fd, exits.
Cost: 1 thread per active subscriber. Max 32 threads — небольшой
overhead. Threads detached, no join needed.
Stress test: 5x pipeline recreate без single "too many subscribers" error.
Раньше: 2-3 recreate → bitmap overflow.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Bug: producer signals **один global** cudaEvent для всего ring (один на
producer). Consumer waits этот event после slot_seq validation, но event
соответствует ПОСЛЕДНЕМУ published frame, не slot[target_seq]. Если
producer wrap'нет ring во время event wait (ring=6 = 240ms окно), slot
содержит уже next-gen data, consumer возвращает torn/stale frame.
Симптом в production: video stream показывает «back-jump на момент»
periodically — camera OSD timestamp дёргается, motion machines briefly
teleport назад. cluster md5 analysis НЕ ловит (содержимое frames всё ещё
unique, просто из неправильной epoch).
Fix: post-sync verify. После cudaStreamWaitEvent / cudaEventSynchronize
re-check slots[slot_idx].seq == target_seq. Если producer перезаписал —
continue outer loop с новым target_seq.
Закрывает race window между slot validation и event sync return. Остаются
открытыми:
- downstream GPU access после frame fill (consumer-side) — producer
может wrap во время этого. Mitigation: STRICT_WAIT policy в publisher
+ ack discipline в consumer (cuframes_release_frame ack уже works).
- bigger ring size снижает wrap frequency (240ms → 1.2s при ring=30).
Test: после deploy в cuda-grid-pipeline (Phase 7 single cam), camera OSD
clock больше не дёргается (раньше дёргалось каждые ~16 sec).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Real-world numbers с production deploy 2026-05-19:
- RTSP к камерам: 12 → 4 (−67%)
- NVDEC sessions: 8 → 4 (−50%)
- Camera bandwidth: 34 → 16 Mbps (−54%)
- PCIe D2H copies: 346 MB/s → ~0 (−100% через zero-copy CUDA IPC)
- Frigate прямые RTSP: 8 → 0 (−100%)
Plus live nvidia-smi metrics, что сохранилось vs не сэкономлено,
projection table для других setup'ов (8/16 cam × 2/3/4 consumer).
Для promotional material — public-facing claims на основе measured deploy.
Symptom (run #1826 fail на u4-runner):
Bootstrap step молча установил Node 12 (Ubuntu default) вместо Node 20
из NodeSource → actions/checkout@v4 не парсится (ES2022 static blocks).
Cause:
curl ... setup_20.x на slow network (u4 через VPN) timeout/fail silently,
apt install fallback на default ubuntu nodejs (Node 12). Без error.
Fix:
- curl --retry 3 --retry-delay 5 --connect-timeout 30
- retry-loop на NodeSource setup (3 попытки)
- явная verification major version >= 18 после install, fail с exit 1
если установился Node < 18
Применяется к обоим jobs (cmake-build и filter-build).
Связано: PR #4 (v0.2), run #1826 fail.
Тесты:
- libcuframes/tests/test_packet_ring.c — 2 scenarios:
1) normal flow: 1 pub × 1 sub × 2000 packets, varied sizes, GOP=30,
payload integrity check (seq в первых 8 байтах + pattern). PTS
monotonicity, first KEY seq, нет data errors.
2) slow consumer (10ms delay): publisher 200 fps, subscriber должен
detect OVERRUN, library resync на keyframe — verify received >10
даже на сильно медленном консьюмере.
- libcuframes/tests/CMakeLists.txt: add_test packet_ring_basic.
Docs:
- CHANGELOG.md: новая [Unreleased] секция с full v0.2 highlights и
явно declared limitations (sub-stream, audio, codec change → v0.3).
- docs/integrations/frigate.md: новая секция "v0.2: dual-input (detect +
record через один RTSP)" с config example, requirements, trade-offs.
Связано: #2, PR #4. Step 6 (final) перед снятием draft.
- cuframes::Publisher (C++ wrapper): добавлены enable_packets(),
set_codec_extradata(), publish_packet() методы.
- cuframes-rtsp-source: новый CLI flag --enable-packet-ring. При его
установке после opening stream — pub.enable_packets(codec_id) +
set_codec_extradata из vstream->codecpar->extradata.
- В main loop: после av_read_frame, до avcodec_send_packet, packet
публикуется в packet ring с конверсией pts/dts из stream_tb в ns,
AV_PKT_FLAG_KEY/CORRUPT/DISCONTINUITY → CUFRAMES_PKT_FLAG_*.
Тест:
cuframes-rtsp-source --rtsp rtsp://... --key cam1 --enable-packet-ring
# frames consumer'ы продолжают работать через cuframes:// (как v0.1)
# record consumer'ы могут brать packets через cuframes_packets:// (Step 5)
Связано: #2, PR #4.
Две идеи добавлены в новую секцию "Future ideas" (без ETA):
- gst-cuframes-src: GStreamer source-element для DeepStream / обычных
GStreamer pipeline'ов. Аналог FFmpeg-демуксера для другого стека.
- vf_cuda_grid: FFmpeg filter с runtime grid composition полностью
на GPU. Заменяет custom C++ GridComposer cctv-processor (см. gx/cctv#22).
Превращает cuframes в GPU-native video routing platform.
Обе идеи waiting на планирование, scope для v0.5+.
Жёсткий URL git.goldix.org не работает на u4-runner — там
gitea доступен только через VPN (10.8.0.6:3222). Используем
переменную runner'а — на R9 = 192.168.88.23:3222, на u4 = 10.8.0.6:3222.
git clone github.com/FFmpeg/FFmpeg на слабом интернете оборвался через 11 мин
(RPC HTTP/2 CANCEL). Local gx/ffmpeg-patched n7.1-cuframes branch имеет
patch уже applied — clone instant без internet round-trip.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- docs/troubleshooting.md — 13 секций с реальными grабельками которые мы
прошли: cudaIpcOpenEventHandle invalid device context (pid namespace),
s6-overlay vs pid share, scale_cuda missing (cuda-llvm + stdbit.h glibc 2.36),
libcuframes not found install paths, ffbuild/ missing source, GMP no working
compiler (long-long reliability), zlib.net deprecated URL, RTSP/RTP UDP
docker NAT, gitea actions Node version
- docs/architecture.md — Appendix A "Production deployment notes" с реальными
observations после 24h+ run: что подтвердилось, что доработали, что не учли
- docs/requirements.md — production deployment matrix + Docker namespace
requirements таблица (cross-container CUDA IPC требует 5 условий)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
cmake --install теперь правильно кладёт libcuframes.so/.a в lib/ и
headers в include/cuframes/. Нужно для downstream builders (FFmpeg
patched build, deb packaging).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
docs/integration.md — детальный guide для интеграции в существующий CCTV
docker-compose: критичные требования (ipc=shareable/container, общий
shared volume для socket), пример CuframesSource для cctv-processor,
verification checklist, troubleshooting (timeout, ipc namespace mismatch,
high latency). Зафиксировано: v0.1 frigate-decode не убирается без
patch'а FFmpeg — это v0.2 scope.
docker/Dockerfile.runtime — multi-stage build (devel → runtime), копирует
libcuframes.so + cuframes-rtsp-source + sub_count в /usr/local. Образ
~700 MB (vs ~7 GB у dev'а). Smoke-test: бинарки запускаются, ldd видит
все нужные libs.
docker-compose.example.yml — reference docker-compose с правильным ipc
mode и volume mounts для копирования в свои проекты.
.dockerignore — исключает build/ и build-*/ из COPY context.
README обновлён: статус v0.1 done, quickstart с реальным docker run,
ссылка на integration guide.
cuframes-rtsp-source — standalone bridge между RTSP/file и cuframes IPC.
Декодирует на CUDA (nvdec), копирует D2D в pre-allocated pool (EXTERNAL
ownership), публикует через cuframes. --realtime для pacing файлового
ввода, --loop для зацикливания. Альтернатива FFmpeg-фильтра до v0.2
(filter требует patch FFmpeg, конфликтует с Frigate's bundled build).
examples/sub_count — reference subscriber на raw C API: counts frames,
trackit gaps, выходит clean при disconnect/timeout/SIGINT.
test_stress (4 subscribers × 2000 frames @ 120fps) — PASS на RTX 5090.
0 torn frames у всех consumers (включая 2 slow с 5ms sleep).
Smoke-проверено: testsrc 25fps → cuframes-rtsp-source → cuframes IPC
→ sub_count (отдельный процесс) → 200/200 frames, 0 gaps, avg_fps=25.2.
R3 (publisher API не работает с FFmpeg's hwframe pool):
- Добавлен ownership_mode field: LIBRARY (default, текущий API) или EXTERNAL.
- Новая функция publisher_create_external(cuda_ptrs[], ptr_count, frame_size)
для случая когда CUDA память выделена upstream (FFmpeg AVHWFramesContext).
- Новая publish_external(cuda_ptr) — публикует один из pre-registered handles.
- Для FFmpeg filter теперь zero-copy: filter получает AVFrame, library уже
имеет IPC handle на этот pointer (registered в create), publish — atomic seq bump.
R1/R2 closure отражено в API:
- publish() теперь принимает cudaStream_t — library делает cudaEventRecord
вместо stream sync.
- next() теперь принимает consumer_stream — library делает cudaStreamWaitEvent
перед возвратом frame. Cross-process sync через cudaIpcEventHandle_t.
Y6 (opaque frame через handle, не struct с _internal_*):
- cuframes_frame_t стал opaque (typedef struct, не определена).
- Accessor functions: cuda_ptr, format, size, pitch_y, pitch_uv, seq, pts_ns.
- ABI-stable при добавлении полей в minor releases.
Y7 (redundant try_next):
- Удалён subscriber_try_next. next(.., timeout_ms=0) — non-blocking
с CUFRAMES_ERR_WOULD_BLOCK.
Y5 (consumer_name uniqueness):
- Документировано что duplicate name → ALREADY_EXISTS.
- Добавлен CUFRAMES_ERR_TOO_MANY для случая >32 subscribers.
Y9 (pts_ns clock):
- Документировано что MONOTONIC у publisher'а, consumer должен sanity-check
на epoch reset при publisher restart.
Также:
- meta-блок (cuframes_frame_meta_t) перестал быть public — meta доступна
через accessor'ы на opaque frame.
- _reserved[4] в configs для forward-compat без breaking ABI.
- Добавлен cuframes_protocol_version() — wire protocol majoring отдельно
от lib version.
Готов к Step 2 (docs/protocol.md + implementation).
См. 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.
Architectural review (2026-05-15) указал что cudaStreamSynchronize-only на
producer-side не достаточен для cross-process visibility — NVIDIA Programming
Guide §3.2.8 требует cudaIpcEventHandle_t. Phase 0 PoC v1 не проверял этот
случай из-за cudaMemcpy который имеет implicit barriers.
spike-v2 воспроизводит правильный сценарий: consumer запускает verify_kernel
на ОТДЕЛЬНОМ stream'е (real-world use case — PyTorch / OpenCV CUDA), pattern
включает row-based component для отлова partial-frame torn.
Запуск 4 scenarios × 1500/600 frames:
A-fhd60 (stream sync, FHD@60): 0 torn, p99=267µs, max=14.7ms
B-fhd60 (event sync, FHD@60): 0 torn, p99=344µs, max=5.2ms
A-4k30 (stream sync, 4K@30): 0 torn, p99=606µs, max=4.4ms
B-4k30 (event sync, 4K@30): 0 torn, p99=437µs, max=3.7ms
Все 4 показали 0 torn frames. R1 на single-host single-GPU фактически
не воспроизводится — но NVIDIA contractually не гарантирует это.
Decision: events as default (R1/R2 resolved). Architecture.md §6.6 закрыт.
Tradeoff: mean latency +20µs, max latency в 3× ниже (predictable tail) +
future-proof для multi-GPU.
Также Dockerfile.dev — апдейт CUDA до 13.0.3 (12.4 не существует с devel-ubuntu24.04).
Связано с PR review: R1, R2, R3 (R3, R4 — в следующих коммитах).