multimodalart HF Staff commited on
Commit
2c647a5
·
verified ·
1 Parent(s): 47d431f

keep video decode + trend rendering off the GPU; tighten ZeroGPU duration

Browse files
Files changed (1) hide show
  1. app.py +79 -55
app.py CHANGED
@@ -202,7 +202,7 @@ def _render_trend_frames(values, sampled_indices, fps, size, task_title):
202
  actual = (x[-1] - x) / float(fps)
203
 
204
  ax1.plot(x, y, color="tab:blue", linewidth=1.0, alpha=0.25)
205
- (line,) = ax1.plot([], [], color="tab:blue", linewidth=2.0, label="predicted remaining")
206
  pt1 = ax1.scatter([x[0]], [y[0]], color="red", s=32, zorder=3, label="current prediction")
207
  ax1.set_xlabel("Frame", fontsize=9)
208
  ax1.set_ylabel("Predicted value (s)", color="tab:blue", fontsize=9)
@@ -219,7 +219,9 @@ def _render_trend_frames(values, sampled_indices, fps, size, task_title):
219
  ax1.set_ylim(y_min - margin, y_max + margin)
220
 
221
  ax2 = ax1.twinx()
222
- ax2.plot(x, actual, color="green", linestyle="--", linewidth=2.0, label="actual remaining")
 
 
223
  pt2 = ax2.scatter([x[0]], [actual[0]], color="green", s=26, zorder=3)
224
  ax2.set_ylabel("Actual remaining (s)", color="green", fontsize=9)
225
  ax2.tick_params(axis="y", labelcolor="green", labelsize=8)
@@ -335,67 +337,45 @@ def save_video_with_trend(frames, values, sampled_indices, fps, output_path, tas
335
 
336
 
337
  def _gpu_duration(
338
- video=None,
339
  instruction="",
 
340
  robot_description=DEFAULT_ROBOT,
341
  camera_description=DEFAULT_CAMERA,
342
- num_frames=DEFAULT_NUM_FRAMES,
343
- max_image_side=DEFAULT_MAX_IMAGE_SIDE,
344
  max_new_tokens=DEFAULT_MAX_NEW_TOKENS,
 
345
  *args,
346
  **kwargs,
347
  ):
348
  try:
349
- nf = int(num_frames)
350
  side = int(max_image_side)
351
  except (TypeError, ValueError):
352
  nf, side = DEFAULT_NUM_FRAMES, DEFAULT_MAX_IMAGE_SIDE
353
  # Sequence length scales with frames x tokens-per-frame, and the eager
354
  # value-isolation attention makes the forward grow ~quadratically in it.
355
- # Calibrated against a measured 32-frame @384px run.
356
- n = nf * (side / 384.0) ** 2
357
- return int(min(240, 20 + 0.6 * n + 0.012 * n**2))
358
 
359
 
360
  @spaces.GPU(duration=_gpu_duration)
361
- def analyze(
362
- video,
363
  instruction,
364
- robot_description=DEFAULT_ROBOT,
365
- camera_description=DEFAULT_CAMERA,
366
- num_frames=DEFAULT_NUM_FRAMES,
 
367
  max_image_side=DEFAULT_MAX_IMAGE_SIDE,
368
- max_new_tokens=DEFAULT_MAX_NEW_TOKENS,
369
- progress=gr.Progress(),
370
  ):
371
- started = time.time()
372
- instruction = (instruction or "").strip()
373
- if not instruction:
374
- raise gr.Error("Please describe the task the agent is supposed to perform.")
375
-
376
- robot_description = (robot_description or "").strip() or None
377
- camera_description = (camera_description or "").strip() or None
378
- if robot_description is None and camera_description is None:
379
- # The checkpoint was trained with use_meta=True; the conversation
380
- # builder refuses to build a prompt without at least one meta field.
381
- robot_description = DEFAULT_ROBOT
382
-
383
- num_frames = int(num_frames)
384
- max_image_side = int(max_image_side)
385
- max_new_tokens = int(max_new_tokens)
386
 
387
- progress(0.03, desc="Decoding video...")
388
- frames, fps = load_video_frames(video)
389
- total = len(frames)
390
- model_images = resize_frames(frames, max_image_side)
391
-
392
- num_frames = max(2, min(num_frames, total))
393
- sampled_indices = np.linspace(0, total - 1, num_frames, dtype=int).tolist()
394
-
395
- progress(0.18, desc="Building episode prompt...")
396
  sample = processor.process_episode(
397
  instruction=instruction,
398
- images=[model_images[j] for j in sampled_indices],
399
  robot_description=robot_description,
400
  camera_description=camera_description,
401
  )
@@ -404,7 +384,6 @@ def analyze(
404
  pixel_values = sample["pixel_values"].flatten(0, 1).to("cuda")
405
  image_grid_thw = sample["image_grid_thw"].flatten(0, 1).to("cuda").long()
406
 
407
- progress(0.28, desc="Predicting temporal distance...")
408
  with torch.inference_mode():
409
  outputs = model(
410
  input_ids=input_ids,
@@ -412,22 +391,19 @@ def analyze(
412
  pixel_values=pixel_values,
413
  image_grid_thw=image_grid_thw,
414
  )
415
- # (num_value_heads, num_frames) -> one remaining-time prediction per frame
416
  pred_value = outputs.value.pred_value.float()
417
- if pred_value.dim() > 1:
418
- pred_value = pred_value.reshape(-1, len(sampled_indices)).mean(dim=0)
419
- pred_value = pred_value.reshape(-1).tolist()
420
- # relative head: signed time delta between consecutive sampled frames
421
  rel_value = outputs.relative.pred_value.float().reshape(-1).tolist()
422
 
423
- progress(0.6, desc="Generating analysis...")
424
  with torch.inference_mode():
425
  gen_out = model.generate(
426
  input_ids=input_ids,
427
  attention_mask=attention_mask,
428
  pixel_values=pixel_values,
429
  image_grid_thw=image_grid_thw,
430
- max_new_tokens=max_new_tokens,
431
  do_sample=False,
432
  num_beams=1,
433
  eos_token_id=EOS_TOKEN_ID,
@@ -435,10 +411,54 @@ def analyze(
435
  use_cache=True,
436
  )
437
  analysis_text = tokenizer.decode(gen_out[0, input_ids.shape[1] :], skip_special_tokens=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
438
  analysis = parse_analysis(analysis_text)
439
- gpu_seconds = time.time() - started
440
 
441
- progress(0.85, desc="Rendering trend video...")
442
  out_path = os.path.join(tempfile.mkdtemp(), "rynnvalue_trend.mp4")
443
  save_video_with_trend(frames, pred_value, sampled_indices, fps, out_path, instruction)
444
 
@@ -457,11 +477,15 @@ def analyze(
457
  f"**Matches the instruction?** {_verdict(analysis['match'])}   ·   "
458
  f"**Task completed?** {_verdict(analysis['success'])}",
459
  "",
460
- f"**Temporal distance** — {pred_value[0]:.2f} s remaining at the first sampled frame → "
461
- f"{pred_value[-1]:.2f} s at the last. The clip itself is {clip_seconds:.1f} s long.",
 
 
 
462
  "",
463
- f"**Relative head** — the predicted time deltas between the {num_frames} sampled "
464
- f"frames sum to {predicted_span:.1f} s (actual clip length {clip_seconds:.1f} s).",
 
465
  "",
466
  f"<sub>{num_frames} frames · longest side {max_image_side}px · "
467
  f"{gpu_seconds:.1f} s on GPU</sub>",
 
202
  actual = (x[-1] - x) / float(fps)
203
 
204
  ax1.plot(x, y, color="tab:blue", linewidth=1.0, alpha=0.25)
205
+ (line,) = ax1.plot([], [], color="tab:blue", linewidth=2.0, label="predicted (left axis)")
206
  pt1 = ax1.scatter([x[0]], [y[0]], color="red", s=32, zorder=3, label="current prediction")
207
  ax1.set_xlabel("Frame", fontsize=9)
208
  ax1.set_ylabel("Predicted value (s)", color="tab:blue", fontsize=9)
 
219
  ax1.set_ylim(y_min - margin, y_max + margin)
220
 
221
  ax2 = ax1.twinx()
222
+ ax2.plot(
223
+ x, actual, color="green", linestyle="--", linewidth=2.0, label="actual (right axis)"
224
+ )
225
  pt2 = ax2.scatter([x[0]], [actual[0]], color="green", s=26, zorder=3)
226
  ax2.set_ylabel("Actual remaining (s)", color="green", fontsize=9)
227
  ax2.tick_params(axis="y", labelcolor="green", labelsize=8)
 
337
 
338
 
339
  def _gpu_duration(
 
340
  instruction="",
341
+ model_images=(),
342
  robot_description=DEFAULT_ROBOT,
343
  camera_description=DEFAULT_CAMERA,
 
 
344
  max_new_tokens=DEFAULT_MAX_NEW_TOKENS,
345
+ max_image_side=DEFAULT_MAX_IMAGE_SIDE,
346
  *args,
347
  **kwargs,
348
  ):
349
  try:
350
+ nf = len(model_images)
351
  side = int(max_image_side)
352
  except (TypeError, ValueError):
353
  nf, side = DEFAULT_NUM_FRAMES, DEFAULT_MAX_IMAGE_SIDE
354
  # Sequence length scales with frames x tokens-per-frame, and the eager
355
  # value-isolation attention makes the forward grow ~quadratically in it.
356
+ # Calibrated against measured 48-frame @384px runs (~8 s on GPU).
357
+ n = max(nf, 1) * (side / 384.0) ** 2
358
+ return int(min(120, 6 + 0.10 * n + 0.0016 * n**2))
359
 
360
 
361
  @spaces.GPU(duration=_gpu_duration)
362
+ def _run_model(
 
363
  instruction,
364
+ model_images,
365
+ robot_description,
366
+ camera_description,
367
+ max_new_tokens,
368
  max_image_side=DEFAULT_MAX_IMAGE_SIDE,
 
 
369
  ):
370
+ """GPU half: build the episode prompt, read the value heads, write the Analysis.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
371
 
372
+ Video decoding and the trend rendering deliberately stay on the CPU side so
373
+ they don't consume ZeroGPU time.
374
+ """
375
+ started = time.time()
 
 
 
 
 
376
  sample = processor.process_episode(
377
  instruction=instruction,
378
+ images=list(model_images),
379
  robot_description=robot_description,
380
  camera_description=camera_description,
381
  )
 
384
  pixel_values = sample["pixel_values"].flatten(0, 1).to("cuda")
385
  image_grid_thw = sample["image_grid_thw"].flatten(0, 1).to("cuda").long()
386
 
 
387
  with torch.inference_mode():
388
  outputs = model(
389
  input_ids=input_ids,
 
391
  pixel_values=pixel_values,
392
  image_grid_thw=image_grid_thw,
393
  )
394
+ # decode_from_bins yields (num_value_heads, num_slots); one slot per frame.
395
  pred_value = outputs.value.pred_value.float()
396
+ pred_value = pred_value.reshape(-1, len(model_images)).mean(dim=0).tolist()
397
+ # relative head: signed time delta between consecutive presented frames
 
 
398
  rel_value = outputs.relative.pred_value.float().reshape(-1).tolist()
399
 
 
400
  with torch.inference_mode():
401
  gen_out = model.generate(
402
  input_ids=input_ids,
403
  attention_mask=attention_mask,
404
  pixel_values=pixel_values,
405
  image_grid_thw=image_grid_thw,
406
+ max_new_tokens=int(max_new_tokens),
407
  do_sample=False,
408
  num_beams=1,
409
  eos_token_id=EOS_TOKEN_ID,
 
411
  use_cache=True,
412
  )
413
  analysis_text = tokenizer.decode(gen_out[0, input_ids.shape[1] :], skip_special_tokens=True)
414
+ return pred_value, rel_value, analysis_text, time.time() - started
415
+
416
+
417
+ def analyze(
418
+ video,
419
+ instruction,
420
+ robot_description=DEFAULT_ROBOT,
421
+ camera_description=DEFAULT_CAMERA,
422
+ num_frames=DEFAULT_NUM_FRAMES,
423
+ max_image_side=DEFAULT_MAX_IMAGE_SIDE,
424
+ max_new_tokens=DEFAULT_MAX_NEW_TOKENS,
425
+ progress=gr.Progress(),
426
+ ):
427
+ instruction = (instruction or "").strip()
428
+ if not instruction:
429
+ raise gr.Error("Please describe the task the agent is supposed to perform.")
430
+
431
+ robot_description = (robot_description or "").strip() or None
432
+ camera_description = (camera_description or "").strip() or None
433
+ if robot_description is None and camera_description is None:
434
+ # The checkpoint was trained with use_meta=True; the conversation
435
+ # builder refuses to build a prompt without at least one meta field.
436
+ robot_description = DEFAULT_ROBOT
437
+
438
+ num_frames = int(num_frames)
439
+ max_image_side = int(max_image_side)
440
+ max_new_tokens = int(max_new_tokens)
441
+
442
+ progress(0.03, desc="Decoding video...")
443
+ frames, fps = load_video_frames(video)
444
+ total = len(frames)
445
+ model_images = resize_frames(frames, max_image_side)
446
+
447
+ num_frames = max(2, min(num_frames, total))
448
+ sampled_indices = np.linspace(0, total - 1, num_frames, dtype=int).tolist()
449
+
450
+ progress(0.2, desc="Predicting temporal distance...")
451
+ pred_value, rel_value, analysis_text, gpu_seconds = _run_model(
452
+ instruction,
453
+ [model_images[j] for j in sampled_indices],
454
+ robot_description,
455
+ camera_description,
456
+ max_new_tokens,
457
+ max_image_side,
458
+ )
459
  analysis = parse_analysis(analysis_text)
 
460
 
461
+ progress(0.75, desc="Rendering trend video...")
462
  out_path = os.path.join(tempfile.mkdtemp(), "rynnvalue_trend.mp4")
463
  save_video_with_trend(frames, pred_value, sampled_indices, fps, out_path, instruction)
464
 
 
477
  f"**Matches the instruction?** {_verdict(analysis['match'])} &nbsp;&nbsp;·&nbsp;&nbsp; "
478
  f"**Task completed?** {_verdict(analysis['success'])}",
479
  "",
480
+ f"**Temporal distance (absolute head)** — cost-to-go falls from "
481
+ f"**{max(pred_value):.2f} s** to **{min(pred_value):.2f} s**, ending at "
482
+ f"**{pred_value[-1]:.2f} s** on the final frame. Values are the model's learned "
483
+ f"estimate of time-to-completion, not a rescaling of the clip's own "
484
+ f"{clip_seconds:.1f} s length.",
485
  "",
486
+ f"**Relative head** — the signed Δt it predicts between the {num_frames} presented "
487
+ f"frames sums to {predicted_span:.1f} s, against {clip_seconds:.1f} s of real elapsed "
488
+ "time.",
489
  "",
490
  f"<sub>{num_frames} frames · longest side {max_image_side}px · "
491
  f"{gpu_seconds:.1f} s on GPU</sub>",