multimodalart HF Staff commited on
Commit
9075d2d
Β·
verified Β·
1 Parent(s): 9563308

Add server-side frame pacing scheduler to smooth real-time stream

Browse files
Files changed (1) hide show
  1. app.py +63 -6
app.py CHANGED
@@ -81,6 +81,20 @@ MAX_BLOCKS_PER_SESSION = 512 # hard cap so a session can't run forever
81
  SESSION_IDLE_TIMEOUT = 600 # seconds; janitor reaps abandoned sessions
82
  GPU_DURATION = 120 # seconds per @spaces.GPU allocation
83
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
  # Actions map to the 8-key one-hot the model was trained on (W A S D I J K L).
85
  # The browser sends the currently-held key set; we translate to this dict.
86
  KEY_ORDER = ["W", "A", "S", "D", "I", "J", "K", "L"]
@@ -292,9 +306,17 @@ def gpu_worker_thread(session: "GameSession"):
292
  session.command_queue, session.seed_path, session.prompt, session.seed,
293
  )
294
  first = True
 
 
 
 
 
 
 
 
295
  while not session.stop_event.is_set():
296
  try:
297
- frame, frame_count = next(gen)
298
  except StopIteration:
299
  print("Rollout generator exhausted", flush=True)
300
  break
@@ -310,20 +332,47 @@ def gpu_worker_thread(session: "GameSession"):
310
  broadcast_status("Rolling out β€” use WASD / IJKL to steer.")
311
  first = False
312
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
313
  now = time.time()
314
  session.frame_times.append(now)
315
  fps = 0.0
316
  if len(session.frame_times) >= 2:
317
  elapsed = session.frame_times[-1] - session.frame_times[0]
318
  fps = (len(session.frame_times) - 1) / elapsed if elapsed > 0 else 0.0
319
- # Keep only the freshest frame if the consumer fell behind.
320
- while session.frame_queue.qsize() > 2:
 
321
  try:
322
  session.frame_queue.get_nowait()
323
  except queue.Empty:
324
  break
325
  try:
326
- session.frame_queue.put_nowait((frame, frame_count, round(fps, 1)))
327
  except queue.Full:
328
  pass
329
  finally:
@@ -377,13 +426,21 @@ def create_gpu_rollout_loop(command_queue, seed_path, prompt_text, seed):
377
  if stop_requested:
378
  break
379
 
 
 
 
380
  pipeline.set_act(current_keys, height=STREAM_HEIGHT, width=STREAM_WIDTH,
381
  num_frames=NUM_FPB, device=device)
382
  lat_block = pipeline.generate_next_block(noise)
383
  noise = torch.randn_like(noise)
384
  frames = _decode_block_to_frames(lat_block)
385
- for f in frames:
386
- yield (f, b)
 
 
 
 
 
387
  finally:
388
  try:
389
  pipeline.reset_stream(batch_size=1, dtype=torch.bfloat16,
 
81
  SESSION_IDLE_TIMEOUT = 600 # seconds; janitor reaps abandoned sessions
82
  GPU_DURATION = 120 # seconds per @spaces.GPU allocation
83
 
84
+ # ── Real-time pacing configuration ───────────────────────────────────────────
85
+ # The GPU decodes a whole block (NUM_FPB frames) at once, so all of a block's
86
+ # frames become available in a burst. If we forward them to the browser the
87
+ # instant they finish, the client sees N frames clustered together followed by a
88
+ # gap while the next block generates β€” the fps counter averages out fine, but
89
+ # the *felt* cadence is bursty. To deliver a steady real-time stream we pace the
90
+ # frames of each block evenly across the time we expect one block to take
91
+ # (mirroring the official ABot-World web_client's block-frame spreader), and we
92
+ # smooth the per-block generation time with an EMA so a single slow/fast block
93
+ # doesn't cause a visible speed-up/slow-down. See gpu_worker_thread().
94
+ PACING_EMA_ALPHA = 0.25 # smoothing factor for per-block generation time
95
+ MIN_PACING_SLEEP = 0.004 # don't bother sleeping for sub-4ms slices
96
+ DEFAULT_BLOCK_SECONDS = 0.5 # initial per-block estimate before first measure
97
+
98
  # Actions map to the 8-key one-hot the model was trained on (W A S D I J K L).
99
  # The browser sends the currently-held key set; we translate to this dict.
100
  KEY_ORDER = ["W", "A", "S", "D", "I", "J", "K", "L"]
 
306
  session.command_queue, session.seed_path, session.prompt, session.seed,
307
  )
308
  first = True
309
+ # Steady send clock: `next_send` is the monotonic time at which the next
310
+ # frame *should* be delivered. Each frame's slot is one smoothed
311
+ # inter-frame interval after the previous, so frames leave at a constant
312
+ # cadence regardless of the bursty block boundaries. `block_seconds` is
313
+ # an EMA of measured per-block generation time (frames/block Γ· that gives
314
+ # the target inter-frame interval).
315
+ block_seconds = DEFAULT_BLOCK_SECONDS
316
+ next_send = None
317
  while not session.stop_event.is_set():
318
  try:
319
+ frame, block_idx, frame_idx, frames_in_block, block_elapsed = next(gen)
320
  except StopIteration:
321
  print("Rollout generator exhausted", flush=True)
322
  break
 
332
  broadcast_status("Rolling out β€” use WASD / IJKL to steer.")
333
  first = False
334
 
335
+ # Update the smoothed per-block time on the first frame of each block
336
+ # (block_elapsed is constant across a block's frames).
337
+ if frame_idx == 0 and block_elapsed > 0:
338
+ block_seconds = (
339
+ PACING_EMA_ALPHA * block_elapsed
340
+ + (1.0 - PACING_EMA_ALPHA) * block_seconds
341
+ )
342
+ fpb = max(1, frames_in_block)
343
+ interval = block_seconds / fpb # target seconds between frames
344
+
345
+ # ── Steady-cadence gate ──────────────────────────────────────────
346
+ # Hold each frame until its scheduled slot so the parent emits at a
347
+ # constant interval instead of dumping a whole block at once.
348
+ now = time.time()
349
+ if next_send is None:
350
+ next_send = now
351
+ sleep_for = next_send - now
352
+ if sleep_for > MIN_PACING_SLEEP:
353
+ # Wake early if a stop is requested so we stay responsive.
354
+ if session.stop_event.wait(timeout=sleep_for):
355
+ break
356
+ now = time.time()
357
+ # Advance the schedule; if we've fallen far behind (e.g. a long GPU
358
+ # stall), resync to now so we don't try to "catch up" in a burst.
359
+ next_send = max(now, next_send + interval)
360
+
361
  now = time.time()
362
  session.frame_times.append(now)
363
  fps = 0.0
364
  if len(session.frame_times) >= 2:
365
  elapsed = session.frame_times[-1] - session.frame_times[0]
366
  fps = (len(session.frame_times) - 1) / elapsed if elapsed > 0 else 0.0
367
+ # Keep only the freshest frame if the consumer fell behind: coalesce
368
+ # stale frames rather than letting them queue up and flush in a burst.
369
+ while session.frame_queue.qsize() > 1:
370
  try:
371
  session.frame_queue.get_nowait()
372
  except queue.Empty:
373
  break
374
  try:
375
+ session.frame_queue.put_nowait((frame, block_idx, round(fps, 1)))
376
  except queue.Full:
377
  pass
378
  finally:
 
426
  if stop_requested:
427
  break
428
 
429
+ # Time the full generate+decode of one block so the parent
430
+ # thread can pace this block's frames over that duration.
431
+ block_start = time.time()
432
  pipeline.set_act(current_keys, height=STREAM_HEIGHT, width=STREAM_WIDTH,
433
  num_frames=NUM_FPB, device=device)
434
  lat_block = pipeline.generate_next_block(noise)
435
  noise = torch.randn_like(noise)
436
  frames = _decode_block_to_frames(lat_block)
437
+ block_elapsed = time.time() - block_start
438
+ n = len(frames)
439
+ for i, f in enumerate(frames):
440
+ # (frame, block_idx, frame_idx_in_block, frames_in_block,
441
+ # block_elapsed) β€” the pacing metadata lets the parent
442
+ # spread this block's frames evenly rather than bursting.
443
+ yield (f, b, i, n, block_elapsed)
444
  finally:
445
  try:
446
  pipeline.reset_stream(batch_size=1, dtype=torch.bfloat16,