Skip to content

Commit f610f08

Browse files
committed
[misc] Add Helios-Distilled inference example
1 parent 70007fc commit f610f08

4 files changed

Lines changed: 173 additions & 2 deletions

File tree

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
"""Generate one Helios-Distilled T2V chunk through FastVideo's typed API.
3+
4+
Set ``HELIOS_MODEL_PATH`` to a local snapshot to avoid downloading the public
5+
checkpoint again. The 33-frame example is a short integration run; increase
6+
``num_frames`` to 240 for the official eight-chunk default.
7+
"""
8+
9+
from __future__ import annotations
10+
11+
import os
12+
13+
from fastvideo import VideoGenerator
14+
from fastvideo.api import (
15+
EngineConfig,
16+
GenerationRequest,
17+
GeneratorConfig,
18+
OffloadConfig,
19+
OutputConfig,
20+
SamplingConfig,
21+
)
22+
23+
MODEL_PATH = os.getenv("HELIOS_MODEL_PATH", "BestWishYsh/Helios-Distilled")
24+
OUTPUT_PATH = os.getenv(
25+
"HELIOS_OUTPUT_PATH",
26+
"outputs_video/helios/helios_distilled_t2v.mp4",
27+
)
28+
PROMPT = ("A vibrant tropical fish swims gracefully through a colorful coral reef "
29+
"in clear turquoise water, cinematic close-up, fluid motion, vivid detail.")
30+
NEGATIVE_PROMPT = ("Bright tones, overexposed, static, blurred details, subtitles, paintings, "
31+
"images, overall gray, worst quality, low quality, JPEG artifacts, ugly, "
32+
"deformed, disfigured, still picture, messy background.")
33+
34+
35+
def main() -> None:
36+
generator = VideoGenerator.from_config(
37+
GeneratorConfig(
38+
model_path=MODEL_PATH,
39+
engine=EngineConfig(
40+
num_gpus=1,
41+
use_fsdp_inference=False,
42+
offload=OffloadConfig(
43+
dit=False,
44+
dit_layerwise=True,
45+
text_encoder=True,
46+
vae=True,
47+
pin_cpu_memory=False,
48+
),
49+
),
50+
))
51+
request = GenerationRequest(
52+
prompt=PROMPT,
53+
negative_prompt=NEGATIVE_PROMPT,
54+
sampling=SamplingConfig(
55+
seed=42,
56+
height=384,
57+
width=640,
58+
num_frames=33,
59+
fps=24,
60+
num_inference_steps=2,
61+
guidance_scale=1.0,
62+
pyramid_num_inference_steps_list=[2, 2, 2],
63+
history_sizes=[16, 2, 1],
64+
num_latent_frames_per_chunk=9,
65+
keep_first_frame=True,
66+
is_skip_first_chunk=False,
67+
use_zero_init=True,
68+
zero_steps=1,
69+
is_amplify_first_chunk=True,
70+
),
71+
output=OutputConfig(
72+
output_path=OUTPUT_PATH,
73+
save_video=True,
74+
return_frames=False,
75+
),
76+
)
77+
78+
try:
79+
generator.generate(request=request)
80+
finally:
81+
generator.shutdown()
82+
83+
84+
if __name__ == "__main__":
85+
main()

fastvideo/pipelines/basic/helios/stages.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -363,7 +363,9 @@ class HeliosChunkDecodingStage(DecodingStage):
363363
def forward(self, batch: ForwardBatch, fastvideo_args: FastVideoArgs) -> ForwardBatch:
364364
if fastvideo_args.output_type == "latent":
365365
assert batch.latents is not None
366-
batch.output = batch.latents.to(torch.float32)
366+
batch.output = batch.latents.detach().to(dtype=torch.float32, device="cpu")
367+
batch.latents = None
368+
batch.helios_latent_chunks = None
367369
return batch
368370

369371
pipeline = self.pipeline() if self.pipeline else None
@@ -381,7 +383,9 @@ def forward(self, batch: ForwardBatch, fastvideo_args: FastVideoArgs) -> Forward
381383

382384
temporal_scale = fastvideo_args.pipeline_config.vae_config.arch_config.scale_factor_temporal
383385
generated_frames = get_generated_pixel_frames(frames.shape[2], temporal_scale)
384-
batch.output = frames[:, :, :generated_frames].to(torch.float32)
386+
batch.output = frames[:, :, :generated_frames].detach().to(dtype=torch.float32, device="cpu")
387+
batch.latents = None
388+
batch.helios_latent_chunks = None
385389

386390
if fastvideo_args.vae_cpu_offload:
387391
self.vae.to("cpu")
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
"""Asset-optional video-level quality and container regression checks."""
3+
4+
from __future__ import annotations
5+
6+
import json
7+
import os
8+
from pathlib import Path
9+
import subprocess
10+
11+
import numpy as np
12+
import pytest
13+
14+
15+
def _video_summary(path: Path) -> dict:
16+
probe = subprocess.run(
17+
[
18+
"ffprobe", "-v", "error", "-select_streams", "v:0", "-count_frames",
19+
"-show_entries", "stream=codec_name,width,height,avg_frame_rate,nb_read_frames",
20+
"-of", "json", str(path),
21+
],
22+
check=True,
23+
capture_output=True,
24+
text=True,
25+
)
26+
stream = json.loads(probe.stdout)["streams"][0]
27+
width, height = int(stream["width"]), int(stream["height"])
28+
raw = subprocess.run(
29+
["ffmpeg", "-v", "error", "-i", str(path), "-f", "rawvideo", "-pix_fmt", "gray", "-"],
30+
check=True,
31+
capture_output=True,
32+
).stdout
33+
frames = np.frombuffer(raw, dtype=np.uint8).reshape(-1, height, width)
34+
means = frames.mean(axis=(1, 2))
35+
return {
36+
"codec": stream["codec_name"],
37+
"width": width,
38+
"height": height,
39+
"fps": stream["avg_frame_rate"],
40+
"frames": int(stream["nb_read_frames"]),
41+
"decoded_frames": len(frames),
42+
"mean": float(frames.mean()),
43+
"std": float(frames.std()),
44+
"black_frame_count": int((means < 1.0).sum()),
45+
"frame_mean_std": float(means.std()),
46+
}
47+
48+
49+
def test_helios_quality_candidate_has_valid_384x640_video():
50+
candidate = os.environ.get("HELIOS_QUALITY_CANDIDATE")
51+
if not candidate:
52+
pytest.skip("Set HELIOS_QUALITY_CANDIDATE to run the real-video quality gate")
53+
path = Path(candidate)
54+
if not path.is_file():
55+
pytest.skip(f"Quality candidate does not exist: {path}")
56+
summary = _video_summary(path)
57+
assert summary["codec"] in {"h264", "hevc", "av1"}
58+
assert (summary["width"], summary["height"]) == (640, 384)
59+
assert summary["fps"] == "24/1"
60+
assert summary["frames"] == summary["decoded_frames"] == 33
61+
assert summary["std"] > 5.0
62+
assert summary["black_frame_count"] == 0
63+
64+
65+
def test_helios_quality_candidate_is_stable_against_reference():
66+
candidate = os.environ.get("HELIOS_QUALITY_CANDIDATE")
67+
reference = os.environ.get("HELIOS_QUALITY_REFERENCE")
68+
if not candidate or not reference:
69+
pytest.skip("Set HELIOS_QUALITY_CANDIDATE and HELIOS_QUALITY_REFERENCE for comparison")
70+
candidate_summary = _video_summary(Path(candidate))
71+
reference_summary = _video_summary(Path(reference))
72+
assert abs(candidate_summary["mean"] - reference_summary["mean"]) < 35.0
73+
assert abs(candidate_summary["std"] - reference_summary["std"]) < 35.0
74+
assert abs(candidate_summary["frame_mean_std"] - reference_summary["frame_mean_std"]) < 25.0

tests/local_tests/pipelines/test_helios_pipeline_stages.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,10 @@ def decode(self, latent):
296296
)
297297
decoded = HeliosChunkDecodingStage(vae).forward(decode_batch, decode_args).output
298298
299+
latent_batch = ForwardBatch(data_type="video", latents=expected)
300+
latent_args = SimpleNamespace(output_type="latent")
301+
latent_output = HeliosChunkDecodingStage(vae).forward(latent_batch, latent_args).output
302+
299303
print(json.dumps({
300304
"cuda_available": True,
301305
"latent_max_diff": (actual_batch.latents - expected).abs().max().item(),
@@ -308,8 +312,10 @@ def decode(self, latent):
308312
"autoregressive_second_short_prefix_mean": autoregressive_model.calls[6]["short_prefix_mean"],
309313
"vae_calls": vae.calls,
310314
"decoded_shape": list(decoded.shape),
315+
"decoded_device": decoded.device.type,
311316
"decoded_first_mean": decoded[:, :, :33].mean().item(),
312317
"decoded_second_mean": decoded[:, :, 33:].mean().item(),
318+
"latent_output_device": latent_output.device.type,
313319
}))
314320
"""
315321
result = subprocess.run(
@@ -368,5 +374,7 @@ def test_chunk_decoder_calls_vae_per_chunk_and_matches_frame_rounding():
368374
result = _results()
369375
assert result["vae_calls"] == [[1, 2, 9, 8, 8], [1, 2, 9, 8, 8]]
370376
assert result["decoded_shape"] == [1, 3, 65, 64, 64]
377+
assert result["decoded_device"] == "cpu"
371378
assert result["decoded_first_mean"] == 0
372379
assert result["decoded_second_mean"] == 1
380+
assert result["latent_output_device"] == "cpu"

0 commit comments

Comments
 (0)