Skip to content

Commit ebbebdb

Browse files
committed
[feat]: decouple Dreamverse fMP4 streaming from generation
Move Dreamverse fMP4 packaging out of the synchronous USER_STEP path. The worker still runs generate_step synchronously so continuation state is saved before the next segment can start, but after generation returns it starts a single background fMP4 stream thread and emits StepComplete immediately. Add MediaError for post-generation fMP4 failures. These errors cannot use the normal WorkerError command path because StepComplete may already have resolved, so they are routed through the media event queue with MediaInit, MediaChunk, and MediaComplete. Add a SessionController media relay task that forwards per-user fMP4 events from the GPU slot queue to the browser WebSocket while the main generation loop can request the next segment. The relay keeps ltx2_segment_complete tied to media completion and gates ltx2_stream_complete until media completion catches up with generated segments. Keep media chunk ordering by allowing only one active fMP4 stream thread per worker and joining it before leave, shutdown, or starting the next segment's media stream. Disable the shared stream buffer for this async path and send chunk bytes through IPC for this version. Update the session logging fake and expectations for the split generation/media lifecycle: step_complete carries generation latency, while segment_complete logs media bytes after fMP4 completion.
1 parent 89fcf08 commit ebbebdb

4 files changed

Lines changed: 268 additions & 142 deletions

File tree

apps/dreamverse/dreamverse/gpu_pool.py

Lines changed: 97 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import multiprocessing as mp
66
import os
77
import subprocess
8+
import threading
89
import time
910
import traceback
1011
from dataclasses import dataclass
@@ -35,6 +36,7 @@
3536
LeaveAck,
3637
MediaChunk,
3738
MediaComplete,
39+
MediaError,
3840
MediaInit,
3941
ReloadAck,
4042
ReloadModelPayload,
@@ -163,11 +165,95 @@ def gpu_worker_process(
163165
from dreamverse.video_generation import VideoGenerationWorker
164166

165167
worker = VideoGenerationWorker(gpu_id)
168+
# One active fMP4 stream per worker keeps binary media chunks ordered by segment.
169+
active_stream_thread: threading.Thread | None = None
170+
active_stream_segment_idx: int | None = None
166171

167172
def event_loop(first_cmd: Command = None):
168173
"""Blocking event loop for LTX2; dispatches user commands."""
169174
print(f"[GPU {gpu_id}] Entering event loop")
170175

176+
def wait_for_active_stream() -> None:
177+
"""Join the active fMP4 segment stream before leave, shutdown, or the next stream."""
178+
nonlocal active_stream_thread
179+
nonlocal active_stream_segment_idx
180+
if active_stream_thread is None:
181+
return
182+
if active_stream_thread.is_alive():
183+
print(f"[GPU {gpu_id}] Waiting for AV stream "
184+
f"segment {active_stream_segment_idx} before continuing")
185+
active_stream_thread.join()
186+
active_stream_thread = None
187+
active_stream_segment_idx = None
188+
189+
def start_stream_thread(
190+
*,
191+
step_result,
192+
user_id: str,
193+
segment_idx: int,
194+
stream_id: str,
195+
head_trim_frames: int,
196+
head_trim_audio_frames: int,
197+
) -> None:
198+
"""Start fMP4 encoding for one segment and publish media events to the controller."""
199+
nonlocal active_stream_thread
200+
nonlocal active_stream_segment_idx
201+
wait_for_active_stream()
202+
stream_timings = dict(step_result.timings)
203+
204+
def _publish(event: StreamEvent) -> None:
205+
response_queue.put(_stream_event_to_worker_event(event, user_id, segment_idx))
206+
207+
def _run_stream() -> None:
208+
try:
209+
av_ok, av_error = stream_fmp4(
210+
frames=step_result.frames,
211+
audio=step_result.audio,
212+
audio_sample_rate=step_result.audio_sample_rate,
213+
stream_id=stream_id,
214+
timings=stream_timings,
215+
head_trim_frames=head_trim_frames,
216+
head_trim_audio_frames=head_trim_audio_frames,
217+
shared_buffer=None,
218+
shared_buffer_bytes=0,
219+
publish=_publish,
220+
log_prefix=f"[GPU {gpu_id}]",
221+
)
222+
if not av_ok:
223+
response_queue.put(
224+
MediaError(
225+
user_id=user_id,
226+
segment_idx=segment_idx,
227+
stream_id=stream_id,
228+
message=av_error or "worker av_fmp4 stream failed",
229+
))
230+
return
231+
print(f"[GPU {gpu_id}] AV streamed segment {segment_idx}: "
232+
f"encode_total={stream_timings.get('av_encode_stream_ms', 0):.0f}ms "
233+
f"wav_write={stream_timings.get('av_wav_write_ms', 0):.1f}ms "
234+
f"spawn={stream_timings.get('av_ffmpeg_spawn_ms', 0):.1f}ms "
235+
f"first_chunk={stream_timings.get('av_first_chunk_ms', 0):.0f}ms "
236+
f"chunk_interval_med={stream_timings.get('av_chunk_interval_ms_median', 0):.1f}ms "
237+
f"chunk_interval_p95={stream_timings.get('av_chunk_interval_ms_p95', 0):.1f}ms "
238+
f"publish_med={stream_timings.get('av_chunk_publish_ms_median', 0):.2f}ms "
239+
f"read_med={stream_timings.get('av_chunk_read_ms_median', 0):.1f}ms")
240+
except Exception as exc:
241+
response_queue.put(
242+
MediaError(
243+
user_id=user_id,
244+
segment_idx=segment_idx,
245+
stream_id=stream_id,
246+
message=str(exc),
247+
))
248+
249+
active_stream_thread = threading.Thread(
250+
target=_run_stream,
251+
name=f"dreamverse-av-stream-gpu{gpu_id}-seg{segment_idx}",
252+
daemon=False,
253+
)
254+
active_stream_segment_idx = segment_idx
255+
active_stream_thread.start()
256+
171257
def handle_command(cmd: Command):
172258
if cmd.type == CommandType.USER_JOIN:
173259
print(f"[GPU {gpu_id}] User {cmd.user_id[:8]} joined")
@@ -200,40 +286,21 @@ def handle_command(cmd: Command):
200286
f"audio_shape={audio_shape}, "
201287
f"audio_sample_rate={step_result.audio_sample_rate}")
202288
stream_id = generate_stream_id(segment_idx)
203-
204-
def _publish(event: StreamEvent) -> None:
205-
response_queue.put(_stream_event_to_worker_event(event, cmd.user_id, segment_idx))
206-
207-
av_ok, av_error = stream_fmp4(
208-
frames=step_result.frames,
209-
audio=step_result.audio,
210-
audio_sample_rate=step_result.audio_sample_rate,
289+
start_stream_thread(
290+
step_result=step_result,
291+
user_id=cmd.user_id,
292+
segment_idx=segment_idx,
211293
stream_id=stream_id,
212-
timings=step_result.timings,
213294
head_trim_frames=head_trim_frames,
214295
head_trim_audio_frames=head_trim_audio_frames,
215-
shared_buffer=shared_stream_buffer,
216-
shared_buffer_bytes=shared_stream_buffer_bytes,
217-
publish=_publish,
218-
log_prefix=f"[GPU {gpu_id}]",
219296
)
220-
if not av_ok:
221-
raise RuntimeError(av_error or "worker av_fmp4 stream failed")
222-
print(f"[GPU {gpu_id}] AV streamed segment {segment_idx}: "
223-
f"encode_total={step_result.timings.get('av_encode_stream_ms', 0):.0f}ms "
224-
f"wav_write={step_result.timings.get('av_wav_write_ms', 0):.1f}ms "
225-
f"spawn={step_result.timings.get('av_ffmpeg_spawn_ms', 0):.1f}ms "
226-
f"first_chunk={step_result.timings.get('av_first_chunk_ms', 0):.0f}ms "
227-
f"chunk_interval_med={step_result.timings.get('av_chunk_interval_ms_median', 0):.1f}ms "
228-
f"chunk_interval_p95={step_result.timings.get('av_chunk_interval_ms_p95', 0):.1f}ms "
229-
f"publish_med={step_result.timings.get('av_chunk_publish_ms_median', 0):.2f}ms "
230-
f"read_med={step_result.timings.get('av_chunk_read_ms_median', 0):.1f}ms")
231-
step_result.timings["ipc_put_start_ns"] = time.time_ns()
297+
step_timings = dict(step_result.timings)
298+
step_timings["ipc_put_start_ns"] = time.time_ns()
232299
response_queue.put(
233300
StepComplete(
234301
user_id=cmd.user_id,
235302
segment_idx=segment_idx,
236-
timings=step_result.timings,
303+
timings=step_timings,
237304
))
238305
except Exception as e:
239306
print(f"[GPU {gpu_id}] Step error: {e}")
@@ -245,6 +312,7 @@ def _publish(event: StreamEvent) -> None:
245312

246313
elif cmd.type == CommandType.USER_LEAVE:
247314
print(f"[GPU {gpu_id}] User {cmd.user_id[:8]} left")
315+
wait_for_active_stream()
248316
worker.clear_conditioning()
249317
response_queue.put(LeaveAck(user_id=cmd.user_id))
250318

@@ -286,6 +354,7 @@ def _publish(event: StreamEvent) -> None:
286354

287355
if first_cmd is not None:
288356
if first_cmd.type == CommandType.SHUTDOWN:
357+
wait_for_active_stream()
289358
worker.shutdown()
290359
response_queue.put(ShutdownAck())
291360
return
@@ -300,6 +369,7 @@ def _publish(event: StreamEvent) -> None:
300369

301370
if cmd.type == CommandType.SHUTDOWN:
302371
print(f"[GPU {gpu_id}] Event loop shutting down")
372+
wait_for_active_stream()
303373
worker.shutdown()
304374
response_queue.put(ShutdownAck())
305375
return
@@ -600,7 +670,7 @@ def get_response_nonblocking():
600670
continue
601671

602672
# AV streaming events → route to the user's stream queue.
603-
if isinstance(event, (MediaInit, MediaChunk, MediaComplete)):
673+
if isinstance(event, (MediaInit, MediaChunk, MediaComplete, MediaError)):
604674
stream_queue = self._stream_queues.get(event.user_id)
605675
if stream_queue is not None:
606676
await stream_queue.put(event)

0 commit comments

Comments
 (0)