--- title: "Синхронизация: stream sync, не CUDA events" sidebar_position: 3 --- # Синхронизация: stream sync, не CUDA events Между producer'ом и consumer'ом в разных процессах нужен механизм, который гарантирует: к моменту когда consumer начинает читать slot, **все GPU writes producer'а в этот slot уже зафиксированы в HBM**. До v0.4 этим занимались CUDA IPC events. С v0.4 — `cuStreamSynchronize` + atomic ordering. Смена не косметическая, и здесь объяснено почему. ## Что было в v0.3 — CUDA IPC events Producer на каждый publish делал `cudaEventRecord` на свой stream. Handle event'а (`cudaIpcEventHandle_t`) экспортировался один раз при старте и шарился со всеми subscriber'ами. Subscriber на каждый frame делал `cudaStreamWaitEvent` на свой stream — GPU scheduler сам ждал completion record'а producer'а перед тем как пустить DtoD copy в очередь. Преимущество: CPU не блокируется. Producer кидает работу в очередь и едет дальше; ожидание происходит в GPU command queue. Недостаток: **CUDA IPC events требуют shared PID namespace между процессами** — точно так же как требовал `cudaIpcOpenMemHandle`. NVIDIA Driver API экспортирует event handle только через тот же legacy IPC механизм, для которого нет POSIX FD аналога. `CU_MEM_HANDLE_TYPE_POSIX_FILE_DESCRIPTOR` существует для memory, для events — нет. Этот недостаток нас и убил на Frigate. Frigate под s6-overlay не отдаёт shared PID — а попытка `cudaIpcOpenEventHandle` без shared PID падает молча и subscribe зависает на первом frame timeout'ом. См. [memory feedback про pid share](/docs/intro). ## Что делает v0.4 вместо Producer перед публикацией seq делает **`cuStreamSynchronize`** на тот stream куда писались GPU данные. Это блокирующая CPU-операция — функция возвращается только когда все pending writes этого stream'а зафиксированы. После этого atomic store в shared header. Из `producer.c::do_publish` (v0.4): ```c static int do_publish(cuframes_publisher_t *pub, int32_t slot, void *stream, int64_t pts_ns) { /* 1. ждём GPU writes этого stream'а */ cuStreamSynchronize((CUstream)stream); /* 2. обнуляем ack-bitmap для нового seq */ atomic_store_explicit(&pub->hdr->slots[slot].ack_bitmap, 0, memory_order_release); atomic_store_explicit(&pub->hdr->slots[slot].pts_ns, pts_ns, memory_order_release); /* 3. publish slot.seq — после этого consumer его увидит */ atomic_store_explicit(&pub->hdr->slots[slot].seq, pub->next_seq, memory_order_release); /* 4. publish global_seq — wake-up для poll'ящих consumer'ов */ atomic_store_explicit(&pub->hdr->global_seq, pub->next_seq, memory_order_release); return 0; } ``` Consumer на той стороне (`consumer.c`) делает acquire-load, читает slot, и **повторно проверяет seq** уже после того как зафиксировал указатель — это защита от того что producer успел перезаписать slot между чтением `global_seq` и реальным копированием: ```c uint64_t gs = atomic_load_explicit(&sub->hdr->global_seq, memory_order_acquire); // ... найти slot_idx по gs ... uint64_t slot_seq = atomic_load_explicit(&sub->hdr->slots[slot_idx].seq, memory_order_acquire); int64_t pts = atomic_load_explicit(&sub->hdr->slots[slot_idx].pts_ns, memory_order_acquire); /* v0.4: producer уже cuStreamSynchronize'нул перед atomic_store seq. * post-check verify_seq защищает от перезаписи slot'а producer'ом. */ uint64_t verify_seq = atomic_load_explicit(&sub->hdr->slots[slot_idx].seq, memory_order_acquire); ``` После post-check consumer делает DtoD memcpy в свой stream. На одном GPU **hardware coherence гарантирована** — HBM один, cache L2 общий, после `cuStreamSynchronize` у producer'а все его writes уже в L2/HBM, и любой subsequent kernel/copy с того же GPU их увидит. ## Sequence diagram ```mermaid sequenceDiagram participant PApp as Publisher app participant PStream as Publisher CUDA stream participant HBM as GPU HBM (shared) participant Header as SHM header (atomic) participant CApp as Consumer app participant CStream as Consumer CUDA stream PApp->>PStream: kernel / NVDEC writes Note over PStream,HBM: async — pending in stream PApp->>PStream: cuStreamSynchronize PStream-->>HBM: all writes flushed PStream-->>PApp: return (CPU unblocked) PApp->>Header: atomic_store(seq, release) loop poll CApp->>Header: atomic_load(global_seq, acquire) end CApp->>Header: read pts, slot_seq CApp->>Header: atomic_load(slot_seq, acquire) [verify] CApp->>CStream: cuMemcpyDtoDAsync(slot → dst) CStream->>HBM: read — sees flushed data CStream-->>CApp: copy enqueued ``` ## Trade-offs | | v0.3 (CUDA IPC events) | v0.4 (stream sync) | |---|---|---| | Cross-namespace (без shared PID) | **нет** | да | | CPU блокировка на publish | нет | да, ~1 ms | | GPU command queue ordering | автоматически | вручную (acquire/release) | | Лишний race без post-check | нет | да, защищён verify_seq | | Зависимость от CUDA Driver feature | `cudaIpcGetEventHandle` | `cuStreamSynchronize` (всегда есть) | | Совместимость с s6-overlay / Frigate | сломано | работает | `cuStreamSynchronize` стоит порядка миллисекунды (зависит от того сколько pending work на stream'е). На 25 fps publisher это ≈ 2.5% CPU времени publisher thread'а — заметно, но не критично для real-time CCTV. Если для твоего сценария это дорого — возможна оптимизация через `cuEventQuery` polling, но v0.4 этого пока не делает (sync проще, корректнее, и достаточно дёшев). Отказ от events — это не «лучше или хуже», это смена области применения. v0.3 не работал в s6/Frigate. v0.4 работает, ценой ~1 ms CPU per publish. ## Что это означает для разработчика - Передавай в `cuframes_publisher_publish` **тот же stream**, на котором писались данные. Иначе `cuStreamSynchronize` будет ждать чужие writes (в худшем случае — никаких) и data race вернётся. - `stream = NULL` (default stream) допустим, но default stream сериализуется со всем GPU-контекстом — это обычно медленнее чем dedicated stream. - Consumer не должен полагаться на CUDA event sync — его больше нет. Stream sync на producer'е + atomic ordering на consumer'е заменяют всю старую IPC event machinery. ## Следующее - [Frame ring vs Packet ring](/docs/concepts/frame-vs-packet-ring) — packet ring sync проще, там нет CUDA вообще. - [Ownership modes](/docs/concepts/ownership-modes) — почему VMM ограничение убрало и EXTERNAL и события одновременно. - [Protocol reference](/docs/reference/protocol) — точный layout shared header и atomic-полей.