OmniDreams WebRTC: Add support for GPU-accelerated H.264 (NVENC) encoding - #359
OmniDreams WebRTC: Add support for GPU-accelerated H.264 (NVENC) encoding#359ksheth-dev wants to merge 7 commits into
Conversation
Greptile SummaryThis PR moves WebRTC video encoding off the CPU and onto NVIDIA NVENC (H.264, PyNvVideoCodec 2.1) behind a
Confidence Score: 5/5Safe to merge; the encoder abstraction, two-stage probe, SDP fallback cleanup, and master-rank guard are all correctly implemented. The one latent defect (PTS counter skipping for empty NVENC output) cannot trigger in the configured zero-latency mode. The encoder abstraction, two-stage probe, SDP fallback cleanup, and master-rank guard are all correctly implemented. The previously flagged import-isolation hole is now covered by try/except (ImportError, RuntimeError) around the deferred import. The PyNvVideoCodec version constraint is pinned to >=2.1,<3. The _enforce_h264_or_fallback path explicitly closes the orphaned NVENCVideoTrack before overwriting the reference. The single latent defect cannot fire under the configured bf=0/lookahead=0 zero-latency settings. Files Needing Attention: flashdreams/flashdreams/serving/webrtc/nvenc.py — the encode_chunk_sync PTS counter logic. Important Files Changed
Reviews (7): Last reviewed commit: "webrtc: isolate PyNvVideoCodec in a dedi..." | Re-trigger Greptile |
|
Thanks for putting this together. I think this is the right direction, but I would not merge it yet. A few things need to be addressed first:
After those are addressed and the branch has current CI results, I think this is worth a full review. |
|
Agreeing with @jarcherNV request for rebase. I asked an agent for a rebase and I'm seeing issues (the stream freezes after a couple of frames, reporting increasing chunk id but remaining visually frozen). I would delay in-depth review until we have a concrete rebased version. |
|
There were some changes merged that unifies Lingbot and omnidreams WebRTC serving, which caused the conflict. |
|
It seems there are some ordering issues in the chunk generation and encoder
The generation order is sequential, but the packet enqueue order is whichever encode worker finishes/schedules first. |
fa14978 to
f4edab9
Compare
|
Thanks for updating this. It looks like it is making good progress. I still see a couple of issues that I think should be addressed before merge:
|
|
The feature changes have been refactored in the latest version of dev/ksheth/pynvvideocodec_v2 branch which is rebased on latest from main branch. With the latest refactored changes, the following above comments are addressed:
|
|
Tested the nvenc code path on RTX6000 pro, and it did improve the omnidreams interactive demo demo in webRTC - from 12 FPS to 14 FPS testing it with and without I think we can get this merged once @jarcherNV 's comments get addressed. The PR is general enough and I think other demos that use webRTC can easily use this nvenc implementation. |
|
Another issue when testing in multi-gpu env - The hardware encoder is initialized on every distributed rank even though only rank 0 serves WebRTC media. In |
- VideoEncoder Protocol + ChunkDeliveryResult in
flashdreams/serving/webrtc/encoders.py, with two implementations:
DefaultRTCEncoder (adapter over aiortc's software encoder) and
PyNvHardwareEncoder (NVENC H.264 via PyNvVideoCodec, output as
av.Packet).
- NVENCVideoTrack (media.py) delivers pre-encoded packets on aiortc's
public av.Packet -> H264Encoder.pack() path -- no reach into
aiortc private attributes. Drop-oldest overflow with a bounded
Queue[av.Packet]; recv() paces at 1/fps like BufferedVideoTrack.
- select_encoder() factory with a two-stage capability probe:
Stage 1 GetEncoderCaps (silent fallback to DefaultRTCEncoder on
environmental "not supported"; loud EncoderInitError under
backend='nvenc'); Stage 2 CreateEncoder (hard error, never a
silent fallback -- masking a driver / session-pool / hardware
failure would hide real problems).
- NVENC configured for interactive low-latency H.264: fmt=ABGR
(NV_ENC_BUFFER_FORMAT_ABGR is word-ordered -> little-endian byte
layout [R,G,B,A]; see _chunk_to_abgr_cuda_frames docstring),
preset=P4, ULL tuning, CBR rate control, bf=0, lookahead=0,
repeatspspps=1 (aiortc's H264Encoder.pack does not synthesize SPS
and PPS). Packets carry pts on the RTP 90 kHz clock so
H264Encoder.pack rescales cleanly via convert_timebase.
- PyNvVideoCodec>=2.1,<3 added as a hard dep on
integrations/omnidreams -- omnidreams already requires CUDA at
runtime, so the install matrix is unchanged.
- Startup emits a single INFO log line per session,
'Video encoder ready: backend={pynvvideocodec|aiortc} ...',
giving one grep anchor regardless of which backend was selected.
- ci_cpu tests cover: Protocol conformance, factory branch coverage
(Stage-1 library / caps / bounds failures under both auto and nvenc,
Stage-2 hard-error regression guard under both), pre-encoded packet
plumbing on NVENCVideoTrack (pts / time_base / drop-oldest overflow),
cross-chunk packet-ordering invariant that guards the manager's
sequential-await pattern from a future create_task rewrite, and
compat guards for aiortc + PyNvVideoCodec public surfaces.
- ci_gpu smoke test encodes a 4-frame chunk on real hardware and
asserts Annex-B start code + IDR + SPS + PPS in the first packet,
plus pts=0 and time_base=1/90000.
No runtime path currently consumes the new abstraction; behavior
change lands in the follow-up commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
… fallback
- OmnidreamsRuntimeConfig gains encoder_backend
(auto | nvenc | default), encoder_bitrate_bps, encoder_gop.
select_encoder() runs on the runtime executor thread inside
_initialize_video_encoder_sync; the encoder is closed cleanly in
_close_sync so its NVENC session slot is released promptly.
- _generate_one_chunk_sync drops the .cpu() D2H copy on the tensor
and replaces it with torch.cuda.current_stream().synchronize().
Same wall-clock cost as before (both wait for the compute stream to
drain), but the hardware encoder can now read the CUDA tensor
directly via DLPack; the software path picks up the D2H inside
BufferedVideoTrack's worker.
- omnidreams.webrtc.server exposes one new CLI flag,
--prefer_sw_encoder. Defaults false -> encoder_backend='auto'
(probe NVENC and fall back silently to aiortc's software encoder
on Stage-1 unsupported). Setting it maps to encoder_backend='default'
and skips the NVENC probe entirely -- useful for A/B profiling and
known-flaky NVENC hosts. The tri-state config field stays on
OmnidreamsRuntimeConfig for programmatic use and test coverage of
the 'nvenc' loud-on-failure branch.
- Manager wiring: addTransceiver + setCodecPreferences constrain the
SDP to H.264 when the encoder emits pre-encoded packets
(prefers_codec == 'h264'). Post-answer, transceiver._codecs is
inspected; if H.264 did not land (e.g. a browser that will not
offer it), _enforce_h264_or_fallback closes the NVENC session and
installs DefaultRTCEncoder + BufferedVideoTrack via
sender.replaceTrack -- a pre-stream swap, no renegotiation.
- Chunk delivery in the generation worker goes through
encoder.deliver_chunk(...). The await is load-bearing for
cross-chunk packet ordering (guarded by the regression test added
in the previous commit): a create_task rewrite here would allow
chunks to complete out of order and packets to land on the track
with non-monotonic pts.
- Test suite extends _FakeVideoEncoder to satisfy the VideoEncoder
Protocol, threads video_encoder through every ManagedWebRTCSession
construction, and adds ci_cpu tests for _enforce_h264_or_fallback
(swap on VP8-negotiated, keep on H.264-negotiated, swap on empty
codec list) plus a parametrized test that --prefer_sw_encoder
correctly maps to encoder_backend ('default' when set, 'auto' when
unset).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- encoders.py: guard the ``PyNvVideoCodec`` import under ``TYPE_CHECKING`` so ty sees a single ``Any``-typed ``nvc`` name. Without the guard, ty preserved a ``<module PyNvVideoCodec> | None | Any`` union and rejected the ``None`` fallback and every attribute access on ``GetEncoderCaps`` / ``FORCEIDR`` / ``CreateEncoder``. Runtime path (the ``else:`` branch) keeps the same guarded try / except behaviour. - encoders.py: narrow the ``VideoEncoder.create_track`` return type from ``MediaStreamTrack`` to ``BufferedVideoTrack | NVENCVideoTrack`` so the manager can assign the result to the ``ManagedWebRTCSession .video_track`` field without a widening error. - manager.py: change ``ManagedWebRTCSession.video_track`` annotation from ``MediaStreamTrack`` to ``BufferedVideoTrack | NVENCVideoTrack``. The aiortc base type does not advertise ``.close`` / ``.fps`` / ``.qsize``, which the generation worker and liveness watchdog need. Drop the now-unused ``MediaStreamTrack`` import. - test_encoders.py: swap mypy-style ``# type: ignore[misc|arg-type]`` to ty-style ``# ty:ignore[invalid-assignment|invalid-argument-type]`` on the frozen-dataclass and SimpleNamespace test sites. Add ``assert packet.pts is not None`` before ``int(packet.pts)`` in the ordering tests -- ``av.Packet.pts`` is nullable at the type level even though the fake encoder always sets it. - test_webrtc_manager.py: add a local ``_FakeVideoEncoder`` stub and thread ``video_encoder=`` through the ``ManagedWebRTCSession`` construction the shared base test uses. - integrations/lingbot/tests/test_webrtc_runtime.py: same treatment for the three ``_ManagedLingbotSession`` constructions -- the lingbot tests broke when we promoted ``video_encoder`` to a required field on the shared ``ManagedWebRTCSession`` base. - integrations/omnidreams/tests/test_webrtc_runtime.py: drop 8 unused ``# ty:ignore[invalid-argument-type]`` comments ty flagged (5 on ``video_encoder=…``, 3 on ``transceiver=transceiver``). - integrations/omnidreams/ludus-renderer/examples/render_mirror_augmented.py: drop the ``# ty:ignore[unresolved-import]`` on the ``PyNvVideoCodec`` import. The comment was pre-existing but became unused when this feature promoted PyNvVideoCodec from an optional extra to a hard dependency of ``integrations/omnidreams``. - Ruff format touched a few files in passing (test_nvenc_track.py, test_nvenc_smoke.py, test_encoders.py); those changes are formatting-only and included here. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
``_enforce_h264_or_fallback`` had two ownership violations: 1. It overwrote ``managed_session.video_track`` without closing the pre-encoded NVENC track. ``ManagedWebRTCSession.close()`` then only saw the fallback track, leaving the original's ``readyState`` "live" and its packet queue undrained. 2. It called ``close()`` on ``managed_session.video_encoder``, which is the same object the runtime owns via ``runtime._video_encoder`` (created once in ``_initialize_video_encoder_sync`` and reused across sessions). Subsequent sessions would read a closed ``PyNvHardwareEncoder`` from ``runtime.video_encoder``. - ``_enforce_h264_or_fallback``: sync → async; await ``old_track.close()`` before installing the fallback. Do not close the encoder — the runtime keeps ownership and releases it at runtime shutdown. - Update the one caller in ``_create_answer_with_runtime_ready_locked`` to await the coroutine. - Tests: run the three cases under ``pytest-asyncio``; assert the orphaned track IS closed (session-scoped) and the runtime-owned encoder is NOT closed (survives session-scope fallback). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
``BaseWebRTCSessionManager._create_answer_with_runtime_ready_locked`` unconditionally read ``runtime.video_encoder``. Only ``OmnidreamsInferenceRuntime`` defines that property; ``LingbotInferenceRuntime`` does not, so a real Lingbot WebRTC session would raise ``AttributeError`` before returning the SDP answer. Lingbot's session-construction tests sidestep this because they build ``_ManagedLingbotSession`` directly with a fake encoder, bypassing the answer-creation path that triggers the read. - ``BaseWebRTCSessionManager``: add ``_resolve_video_encoder()`` that reads ``runtime.video_encoder`` if present, else constructs a session-scope ``DefaultRTCEncoder(fps=self.fps)``. - ``_create_answer_with_runtime_ready_locked`` routes through the new method instead of the direct attribute read. - ``test_webrtc_manager.py``: add ci_cpu tests for both branches (runtime provides an encoder, runtime does not). Extend ``_FakeVideoEncoder`` with an async ``deliver_chunk`` so the existing generation-worker test that drives one chunk end-to-end keeps passing after the manager started calling into the encoder. - ``integrations/lingbot/tests/test_webrtc_runtime.py``: add ``video_encoder=_FakeVideoEncoder()`` on three ``_ManagedLingbotSession`` construction sites that were missed when the field became required. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
… rank ``_initialize_video_encoder_sync`` ran on every rank via ``_initialize_sync_all_ranks`` (``distributed_op``), allocating an NVENC session on every worker even though only the master rank serves WebRTC media. Worker ranks then held a session slot they would never use, and if the local NVENC pool could not accommodate one allocation per rank, startup would fail on the ranks that were unable to allocate. - ``_initialize_video_encoder_sync``: early-return on non-master ranks. ``runtime.video_encoder`` is only ever read on the master, so leaving ``_video_encoder`` as ``None`` on workers is safe. - ci_cpu tests: worker rank never reaches ``select_encoder``; master rank still initializes normally. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
f4edab9 to
8e63d69
Compare
|
/ok to test 8e63d69 |
8e63d69 to
0ba2d2b
Compare
|
/ok to test 0ba2d2b |
0ba2d2b to
f921e2c
Compare
|
/ok to test f921e2c |
f921e2c to
bec5ea2
Compare
|
/ok to test bec5ea2 |
Split PyNvHardwareEncoder and its ABGR-frame / NAL-scan helpers out of encoders.py into a sibling nvenc.py that owns the top-level PyNvVideoCodec import. encoders.py no longer touches PyNvVideoCodec at module load; select_encoder probes availability via importlib.util.find_spec (no side effects) and imports nvenc only when a hardware backend is actually about to be constructed. The Lingbot WebRTC path resolves to DefaultRTCEncoder in the base manager and never enters the hardware branch, so its process no longer loads PyNvVideoCodec at all — sidestepping a silent early-exit observed during the Lingbot server launch when PyNvVideoCodec was importable. Tests updated to patch _pynvvideocodec_installed and inject a fake PyNvVideoCodec into sys.modules so nvenc's top-level import binds to the mock. The GPU smoke test's imports follow the split. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
bec5ea2 to
6434238
Compare
|
Updated this PR to address all the comments and issues raised by the reviewers.
|
|
/ok to test 6434238 |
Summary
Move the WebRTC video encode off the CPU and onto the GPU via NVIDIA NVENC
(H.264, PyNvVideoCodec 2.1) for integrations that opt in, behind a small
VideoEncoderabstraction the session picks at startup:PyNvHardwareEncoder(flashdreams/serving/webrtc/nvenc.py) —hardware H.264 via PyNvVideoCodec. Emits pre-encoded
av.Packets thataiortc's
H264Encoder.pack()forwards straight to the RTP packetizerthrough a new
NVENCVideoTrack.DefaultRTCEncoder(flashdreams/serving/webrtc/encoders.py) —thin wrapper over aiortc's built-in software encoder: raw frames on a
BufferedVideoTrack, aiortc negotiates the codec via SDP.select_encoder(backend=...)picks between them:backend="auto"(default on omnidreams) — two-stage probe:Stage 1 (
GetEncoderCaps+ resolution bounds) is a silent fallbackto
DefaultRTCEncoder(with an INFO log); Stage 2 (CreateEncoder)raising after Stage 1 passed is a hard error — never a silent degrade,
because it means the environment reports support but session
allocation still fails.
backend="nvenc"— any probe/init failure raisesEncoderInitErrorat startup so misconfigured deployments surface loudly.
backend="default"— bypasses the probe entirely; forced software path.Availability is checked with
importlib.util.find_spec— noPyNvVideoCodecimport, no CUDA-driver side effects on processes thatdon't opt in.
PyNvHardwareEncoderand its top-levelimport PyNvVideoCodeclive in a dedicatednvenc.pymodule thatselect_encoderlazy-imports only when constructing the hardwarebackend; integrations on the software path (Lingbot today) never load
the library.
New CLI flag
--prefer_sw_encoderonomnidreams.webrtc.servermapsto
backend="default"for A/B profiling or forced-software runs.Also on this branch:
BaseWebRTCSessionManager._resolve_video_encoderfalls back to asession-scope
DefaultRTCEncoderwhen the runtime does not exposevideo_encoder, fixing anAttributeErrora real Lingbot WebRTCsession would otherwise raise.
_initialize_video_encoder_syncearly-returns on non-master ranks,so worker ranks (rank > 0) do not burn NVENC session slots they
never use.
_enforce_h264_or_fallbackawaitsclose on the orphaned session-scope track before installing the
software fallback, and does not close the runtime-owned encoder
(which is reused across sessions).
Measured impact with NVENC
At 1280x704, 30 fps, 10 Mb/s target on a single-GPU host:
enqueue_ms: ~4x reduction (28 ms -> 7 ms)Test plan
CI-safe unit tests (CPU)
uv run pre-commit run -a— ruff format/fix, ty type check, uv-lockfile validation.uv run pytest -m ci_cpu— Full CPU-safe suite.uv run pytest flashdreams/tests/test_encoders.py -v—select_encoderbranch coverage:backend="default"short-circuit, Stage-1 (find_spec/GetEncoderCaps) silent fallback vs.backend="nvenc"hard-raise, Stage-2CreateEncoderhard-error guard, cross-chunk packet-ordering invariant (240-packet regression), aiortc/PyNvVideoCodec compatibility guards.uv run pytest flashdreams/tests/test_webrtc_manager.py -v— Base manager:_resolve_video_encoderruntime-encoder pass-through and software fallback (fix for the missing-video_encoderAttributeError), H.264 codec preference, SDP-fallback session-scope cleanup.uv run pytest integrations/omnidreams/tests/test_webrtc_runtime.py -v— Omnidreams session init:select_encoderwiring,--prefer_sw_encoder→encoder_backendmapping, master-rank-only NVENC allocation guard, session-scope orphan-track close on H.264 SDP fallback (runtime-owned encoder is not closed).uv run pytest integrations/lingbot/tests/test_webrtc_runtime.py -v— Lingbot session construction still passes with the base-manager software fallback (novideo_encoderon the runtime).Structural / no-side-effect invariants
encoders.pydoes not import PyNvVideoCodec ornvencat module load:GPU tests (needs CUDA + NVENC-capable GPU)
End-to-end (needs GPU + browser)