Skip to content

Commit 7d08010

Browse files
[ci][diagnostic]: capture GameCraft FA4 candidate on GB200
1 parent c9c5585 commit 7d08010

2 files changed

Lines changed: 312 additions & 0 deletions

File tree

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
#!/usr/bin/env python3
2+
"""Emit and recover a small GameCraft MP4 through a Buildkite job log."""
3+
4+
from __future__ import annotations
5+
6+
import argparse
7+
import base64
8+
import binascii
9+
import hashlib
10+
import json
11+
import re
12+
import sys
13+
from pathlib import Path
14+
15+
MARKER = "FV_GAMECRAFT_FA4_MP4"
16+
CHUNK_BYTES = 3 * 1024
17+
MAX_MEDIA_BYTES = 1024 * 1024
18+
19+
20+
def _sha256(data: bytes) -> str:
21+
return hashlib.sha256(data).hexdigest()
22+
23+
24+
def emit(media_path: Path) -> None:
25+
if media_path.suffix.lower() != ".mp4":
26+
raise ValueError(f"Candidate must be an MP4: {media_path}")
27+
28+
data = media_path.read_bytes()
29+
size = len(data)
30+
if not 0 < size <= MAX_MEDIA_BYTES:
31+
raise ValueError(f"Candidate size must be between 1 and {MAX_MEDIA_BYTES} bytes; got {size}")
32+
33+
digest = _sha256(data)
34+
chunks = [data[offset:offset + CHUNK_BYTES] for offset in range(0, size, CHUNK_BYTES)]
35+
print(
36+
f"{MARKER}_BEGIN version=1 sha256={digest} size={size} "
37+
f"chunks={len(chunks)} chunk_bytes={CHUNK_BYTES}",
38+
flush=True,
39+
)
40+
for index, chunk in enumerate(chunks):
41+
encoded = base64.b64encode(chunk).decode("ascii")
42+
print(f"{MARKER}_CHUNK index={index:06d} data={encoded}", flush=True)
43+
print(
44+
f"{MARKER}_END version=1 sha256={digest} size={size} chunks={len(chunks)}",
45+
flush=True,
46+
)
47+
48+
49+
def _buildkite_output(text: str) -> str:
50+
"""Unwrap the public Buildkite job-log JSON response when supplied."""
51+
try:
52+
payload = json.loads(text)
53+
except json.JSONDecodeError:
54+
return text
55+
if isinstance(payload, dict) and isinstance(payload.get("output"), str):
56+
return payload["output"]
57+
return text
58+
59+
60+
def decode(log_text: str) -> tuple[bytes, str]:
61+
log_text = _buildkite_output(log_text)
62+
begin_pattern = re.compile(
63+
rf"{MARKER}_BEGIN version=1 sha256=([0-9a-f]{{64}}) size=([0-9]+) "
64+
rf"chunks=([0-9]+) chunk_bytes=([0-9]+)"
65+
)
66+
end_pattern = re.compile(
67+
rf"{MARKER}_END version=1 sha256=([0-9a-f]{{64}}) size=([0-9]+) chunks=([0-9]+)"
68+
)
69+
chunk_pattern = re.compile(rf"{MARKER}_CHUNK index=([0-9]{{6}}) data=([A-Za-z0-9+/]+={{0,2}})")
70+
71+
begin_matches = begin_pattern.findall(log_text)
72+
end_matches = end_pattern.findall(log_text)
73+
if len(begin_matches) != 1 or len(end_matches) != 1:
74+
raise ValueError(
75+
"Expected exactly one candidate envelope; "
76+
f"found {len(begin_matches)} begin and {len(end_matches)} end markers"
77+
)
78+
79+
digest, size_text, count_text, chunk_bytes_text = begin_matches[0]
80+
end_digest, end_size_text, end_count_text = end_matches[0]
81+
if (digest, size_text, count_text) != (end_digest, end_size_text, end_count_text):
82+
raise ValueError("Candidate begin/end metadata does not match")
83+
84+
size = int(size_text)
85+
count = int(count_text)
86+
chunk_bytes = int(chunk_bytes_text)
87+
if not 0 < size <= MAX_MEDIA_BYTES:
88+
raise ValueError(f"Candidate size is outside the accepted range: {size}")
89+
if chunk_bytes != CHUNK_BYTES:
90+
raise ValueError(f"Unexpected chunk size: {chunk_bytes}")
91+
92+
matches = chunk_pattern.findall(log_text)
93+
if len(matches) != count:
94+
raise ValueError(f"Expected {count} candidate chunks; found {len(matches)}")
95+
96+
encoded_chunks: dict[int, str] = {}
97+
for index_text, encoded in matches:
98+
index = int(index_text)
99+
if index in encoded_chunks:
100+
raise ValueError(f"Duplicate candidate chunk: {index}")
101+
encoded_chunks[index] = encoded
102+
if sorted(encoded_chunks) != list(range(count)):
103+
raise ValueError("Candidate chunk sequence is incomplete or out of range")
104+
105+
try:
106+
data = b"".join(base64.b64decode(encoded_chunks[index], validate=True) for index in range(count))
107+
except binascii.Error as error:
108+
raise ValueError(f"Candidate chunk is not valid base64: {error}") from error
109+
if len(data) != size:
110+
raise ValueError(f"Decoded candidate size mismatch: expected {size}, got {len(data)}")
111+
actual_digest = _sha256(data)
112+
if actual_digest != digest:
113+
raise ValueError(f"Decoded candidate sha256 mismatch: expected {digest}, got {actual_digest}")
114+
return data, digest
115+
116+
117+
def _parse_args() -> argparse.Namespace:
118+
parser = argparse.ArgumentParser(description=__doc__)
119+
subparsers = parser.add_subparsers(dest="command", required=True)
120+
121+
emit_parser = subparsers.add_parser("emit", help="Encode one MP4 to stdout between strict log markers")
122+
emit_parser.add_argument("media_path", type=Path)
123+
124+
decode_parser = subparsers.add_parser("decode", help="Recover and verify one MP4 from a raw or public JSON log")
125+
decode_parser.add_argument("--input", type=Path, help="Log file (default: stdin)")
126+
decode_parser.add_argument("--output", type=Path, required=True)
127+
return parser.parse_args()
128+
129+
130+
def main() -> int:
131+
args = _parse_args()
132+
if args.command == "emit":
133+
emit(args.media_path)
134+
return 0
135+
136+
log_text = args.input.read_text() if args.input else sys.stdin.read()
137+
data, digest = decode(log_text)
138+
if args.output.exists():
139+
raise FileExistsError(f"Refusing to overwrite existing output: {args.output}")
140+
args.output.parent.mkdir(parents=True, exist_ok=True)
141+
args.output.write_bytes(data)
142+
print(f"Recovered {len(data)} bytes with sha256={digest} to {args.output}")
143+
return 0
144+
145+
146+
if __name__ == "__main__":
147+
raise SystemExit(main())

.buildkite/scripts/lanes/ssim.sh

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,19 @@ if [ "$selected" != all ]; then
2525
done
2626
fi
2727

28+
# This disposable branch defaults to a focused GameCraft FA4 candidate run.
29+
# The canonical lane remains available for local inspection by setting this to
30+
# zero. Always exit 2 in candidate mode so the run cannot be mistaken for a
31+
# passing quality gate and cannot trigger the production exit-1 retry policy.
32+
candidate_enabled=${FASTVIDEO_GAMECRAFT_FA4_CANDIDATE_LOG:-1}
33+
force_candidate_exit() {
34+
trap - EXIT
35+
exit 2
36+
}
37+
if [ "$candidate_enabled" = 1 ]; then
38+
trap force_candidate_exit EXIT
39+
fi
40+
2841
# MoGe's utils3d dependency builds glcontext from source on ARM64. The current
2942
# runner image predates the baked-in X11 headers below, so keep this guarded
3043
# bootstrap until every deployed image digest contains libx11-dev.
@@ -37,4 +50,156 @@ fi
3750
uv pip install git+https://github.com/microsoft/MoGe.git
3851
uv pip install k_diffusion einops_exts alias_free_torch torchsde
3952

53+
if [ "$candidate_enabled" = 1 ]; then
54+
checkout_sha=$(git rev-parse HEAD)
55+
echo "+++ GameCraft T2V current-FA4 candidate on GB200"
56+
echo "checkout_sha=$checkout_sha"
57+
echo "buildkite_commit=${BUILDKITE_COMMIT:-<unset>}"
58+
if [ -n "${BUILDKITE_COMMIT:-}" ] && [ "$checkout_sha" != "$BUILDKITE_COMMIT" ]; then
59+
echo "Checkout does not match BUILDKITE_COMMIT" >&2
60+
exit 2
61+
fi
62+
63+
visible_gpus=${CUDA_VISIBLE_DEVICES:-0}
64+
candidate_gpu=${visible_gpus%%,*}
65+
candidate_log=/tmp/fastvideo-gamecraft-fa4-candidate-pytest.log
66+
candidate_sentinel=$(mktemp /tmp/fastvideo-gamecraft-fa4-candidate.XXXXXX)
67+
candidate_generated_root=fastvideo/tests/ssim/generated_videos/default
68+
candidate_env=(
69+
"CUDA_VISIBLE_DEVICES=$candidate_gpu"
70+
"PYTORCH_CUDA_ALLOC_CONF=expandable_segments:False"
71+
"FASTVIDEO_ATTENTION_BACKEND=FLASH_ATTN"
72+
"FASTVIDEO_FA4=1"
73+
"FASTVIDEO_SSIM_BOOTSTRAP_MODE=0"
74+
"FASTVIDEO_SSIM_FULL_QUALITY=0"
75+
)
76+
77+
if env "${candidate_env[@]}" python - <<'PY'
78+
import os
79+
80+
import torch
81+
82+
print(f"requested_backend={os.environ['FASTVIDEO_ATTENTION_BACKEND']}")
83+
print(f"requested_FASTVIDEO_FA4={os.environ['FASTVIDEO_FA4']}")
84+
print(f"torch_version={torch.__version__}")
85+
print(f"torch_cuda_version={torch.version.cuda}")
86+
print(f"cuda_available={torch.cuda.is_available()}")
87+
print(f"cuda_visible_devices={os.environ.get('CUDA_VISIBLE_DEVICES', '<unset>')}")
88+
print(f"gpu_count={torch.cuda.device_count()}")
89+
90+
if torch.cuda.device_count() != 1:
91+
raise RuntimeError(f"Expected exactly one visible candidate GPU, got {torch.cuda.device_count()}")
92+
gpu_name = torch.cuda.get_device_name(0)
93+
print(f"gpu[0]={gpu_name} capability={torch.cuda.get_device_capability(0)}")
94+
if "GB200" not in gpu_name:
95+
raise RuntimeError(f"Candidate must run on GB200, got {gpu_name}")
96+
97+
from fastvideo.attention.selector import get_attn_backend
98+
from fastvideo.attention.utils.flash_attn_default import fa_version
99+
from fastvideo.platforms import AttentionBackendEnum
100+
101+
requested_backend = AttentionBackendEnum[os.environ["FASTVIDEO_ATTENTION_BACKEND"]]
102+
resolved_backend = get_attn_backend(
103+
128,
104+
torch.bfloat16,
105+
supported_attention_backends=(AttentionBackendEnum.FLASH_ATTN,),
106+
requested=requested_backend,
107+
)
108+
print(f"resolved_backend={resolved_backend.get_name()}")
109+
print(f"resolved_flash_attention=FA{fa_version}")
110+
if fa_version != "4":
111+
raise RuntimeError(f"Candidate must use FA4, resolved FA{fa_version}")
112+
PY
113+
then
114+
probe_rc=0
115+
else
116+
probe_rc=$?
117+
fi
118+
if [ "$probe_rc" -ne 0 ]; then
119+
echo "GameCraft candidate environment probe failed with rc=$probe_rc" >&2
120+
exit 2
121+
fi
122+
123+
if env "${candidate_env[@]}" python -m pytest \
124+
fastvideo/tests/ssim/test_gamecraft_similarity.py::test_gamecraft_t2v_similarity \
125+
-vs >"$candidate_log" 2>&1
126+
then
127+
test_rc=0
128+
else
129+
test_rc=$?
130+
fi
131+
132+
echo "+++ GameCraft candidate pytest tail"
133+
python - "$candidate_log" <<'PY'
134+
import sys
135+
from pathlib import Path
136+
137+
log_path = Path(sys.argv[1])
138+
data = log_path.read_bytes()
139+
print(f"pytest_log_bytes={len(data)}")
140+
tail = data[-131072:].decode("utf-8", errors="replace").replace("\r", "\n")
141+
lines = tail.splitlines()[-160:]
142+
for line in lines:
143+
print(line[:2000])
144+
PY
145+
echo "pytest_rc=$test_rc"
146+
147+
candidate_videos=()
148+
if [ -d "$candidate_generated_root" ]; then
149+
mapfile -d '' -t candidate_videos < <(
150+
find "$candidate_generated_root" -type f -name '*.mp4' -newer "$candidate_sentinel" -print0
151+
)
152+
fi
153+
if [ "${#candidate_videos[@]}" -ne 1 ]; then
154+
echo "Expected exactly one newly generated candidate MP4; found ${#candidate_videos[@]}" >&2
155+
printf 'candidate_path=%s\n' "${candidate_videos[@]}" >&2
156+
exit 2
157+
fi
158+
candidate_video=${candidate_videos[0]}
159+
case "$candidate_video" in
160+
*/default/GB200_reference_videos/HunyuanGameCraft-T2V/FLASH_ATTN/*.mp4) ;;
161+
*)
162+
echo "Candidate path is outside the expected GB200 T2V subtree: $candidate_video" >&2
163+
exit 2
164+
;;
165+
esac
166+
167+
candidate_results=()
168+
mapfile -d '' -t candidate_results < <(
169+
find "$(dirname "$candidate_video")" -type f -name '*_ssim.json' -newer "$candidate_sentinel" -print0
170+
)
171+
if [ "${#candidate_results[@]}" -ne 1 ]; then
172+
echo "Expected exactly one newly generated SSIM JSON; found ${#candidate_results[@]}" >&2
173+
exit 2
174+
fi
175+
candidate_result=${candidate_results[0]}
176+
177+
python - "$candidate_video" "$candidate_result" <<'PY'
178+
import json
179+
import sys
180+
from pathlib import Path
181+
182+
video_path = Path(sys.argv[1]).resolve()
183+
result_path = Path(sys.argv[2])
184+
result = json.loads(result_path.read_text())
185+
if Path(result["generated_video"]).resolve() != video_path:
186+
raise RuntimeError("SSIM JSON does not describe the candidate MP4")
187+
if result["parameters"]["num_inference_steps"] != 20:
188+
raise RuntimeError("Candidate did not use the expected 20 inference steps")
189+
print(f"candidate_path={video_path}")
190+
print(f"candidate_ssim_mean={result['mean_ssim']}")
191+
print(f"candidate_ssim_min={result['min_ssim']}")
192+
print(f"candidate_ssim_max={result['max_ssim']}")
193+
print(f"candidate_prompt={result['parameters']['prompt']}")
194+
PY
195+
196+
echo "FV_GAMECRAFT_FA4_SSIM_JSON_BEGIN"
197+
cat "$candidate_result"
198+
echo "FV_GAMECRAFT_FA4_SSIM_JSON_END"
199+
echo "+++ GameCraft candidate MP4 log envelope"
200+
python .buildkite/scripts/gamecraft_candidate_log.py emit "$candidate_video"
201+
echo "--- GameCraft candidate complete: probe_rc=$probe_rc pytest_rc=$test_rc (forced lane rc=2)"
202+
exit 2
203+
fi
204+
40205
exec python fastvideo/tests/ssim/ci_runner.py "${args[@]}"

0 commit comments

Comments
 (0)