122 lines
8.4 KiB
Markdown
122 lines
8.4 KiB
Markdown
---
|
||
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-полей.
|