Spaces:
Running on Zero
Running on Zero
keep video decode + trend rendering off the GPU; tighten ZeroGPU duration
Browse files
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
|
| 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(
|
|
|
|
|
|
|
| 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 =
|
| 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
|
| 356 |
-
n = nf * (side / 384.0) ** 2
|
| 357 |
-
return int(min(
|
| 358 |
|
| 359 |
|
| 360 |
@spaces.GPU(duration=_gpu_duration)
|
| 361 |
-
def
|
| 362 |
-
video,
|
| 363 |
instruction,
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
|
|
|
| 367 |
max_image_side=DEFAULT_MAX_IMAGE_SIDE,
|
| 368 |
-
max_new_tokens=DEFAULT_MAX_NEW_TOKENS,
|
| 369 |
-
progress=gr.Progress(),
|
| 370 |
):
|
| 371 |
-
|
| 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 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 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=
|
| 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,
|
| 416 |
pred_value = outputs.value.pred_value.float()
|
| 417 |
-
|
| 418 |
-
|
| 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.
|
| 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** —
|
| 461 |
-
f"{pred_value
|
|
|
|
|
|
|
|
|
|
| 462 |
"",
|
| 463 |
-
f"**Relative head** — the
|
| 464 |
-
f"frames
|
|
|
|
| 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'])} · "
|
| 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>",
|