Skip to content

Commit af4a775

Browse files
authored
[ci]: add SSIM reference bootstrap flow (#1522) (#1547)
1 parent c096fda commit af4a775

13 files changed

Lines changed: 687 additions & 16 deletions

.buildkite/scripts/pr_test.sh

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,23 @@ MODAL_ENV="BUILDKITE_REPO=$BUILDKITE_REPO BUILDKITE_COMMIT=$BUILDKITE_COMMIT BUI
8080

8181
POST_RUN_HOOK=""
8282

83+
is_truthy() {
84+
case "${1:-}" in
85+
1|true|TRUE|yes|YES|on|ON) return 0 ;;
86+
*) return 1 ;;
87+
esac
88+
}
89+
90+
ssim_bootstrap_args() {
91+
local title="${PR_TITLE:-}"
92+
local message="${BUILDKITE_MESSAGE:-}"
93+
if is_truthy "${FASTVIDEO_SSIM_BOOTSTRAP_MODE:-}" \
94+
|| [[ "$title" == *"[new-model]"* ]] \
95+
|| [[ "$message" == *"[new-model]"* ]]; then
96+
printf ' --bootstrap-mode'
97+
fi
98+
}
99+
83100
upload_performance_artifacts() {
84101
SHORT_SHA=${BUILDKITE_COMMIT:0:7}
85102
LOCAL_DIR="downloaded_reports"
@@ -172,7 +189,12 @@ case "$TEST_TYPE" in
172189
;;
173190
"ssim")
174191
log "Running SSIM tests..."
175-
MODAL_COMMAND="$MODAL_ENV HF_API_KEY=$HF_API_KEY python3 -m modal run $MODAL_SSIM_TEST_FILE::run_ssim_tests"
192+
SSIM_BOOTSTRAP_ARGS=$(ssim_bootstrap_args)
193+
if [ -n "$SSIM_BOOTSTRAP_ARGS" ]; then
194+
log "SSIM bootstrap mode enabled for new-model reference draft generation"
195+
fi
196+
MODAL_COMMAND="$MODAL_ENV HF_API_KEY=$HF_API_KEY python3 -m modal run "
197+
MODAL_COMMAND+="$MODAL_SSIM_TEST_FILE::run_ssim_tests$SSIM_BOOTSTRAP_ARGS"
176198
;;
177199
"training")
178200
log "Running training tests..."

.github/workflows/ci-slash-commands.yml

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ jobs:
5252
core.setOutput('pr_sha', pr.head.sha);
5353
core.setOutput('pr_branch', pr.head.ref);
5454
core.setOutput('pr_number', String(prNumber));
55+
core.setOutput('pr_title', pr.title);
5556
5657
- name: Trigger Full Suite
5758
if: steps.perm.outputs.has_write == 'true'
@@ -60,6 +61,7 @@ jobs:
6061
PR_SHA: ${{ steps.label.outputs.pr_sha }}
6162
PR_BRANCH: ${{ steps.label.outputs.pr_branch }}
6263
PR_NUMBER: ${{ steps.label.outputs.pr_number }}
64+
PR_TITLE: ${{ steps.label.outputs.pr_title }}
6365
BK_ORG: ${{ vars.BUILDKITE_ORG_SLUG }}
6466
BK_PIPELINE: ${{ vars.BUILDKITE_PIPELINE_SLUG }}
6567
run: |
@@ -71,6 +73,7 @@ jobs:
7173
--arg commit "$PR_SHA" \
7274
--arg branch "$PR_BRANCH" \
7375
--arg message "Full Suite for PR #${PR_NUMBER} (via /merge)" \
76+
--arg pr_title "$PR_TITLE" \
7477
--argjson pr_id "$PR_NUMBER" \
7578
'{
7679
commit: $commit,
@@ -80,11 +83,12 @@ jobs:
8083
pull_request_id: $pr_id,
8184
pull_request_base_branch: "main",
8285
env: {
83-
TEST_SCOPE: "full",
84-
FULL_SUITE: "true",
85-
PR_NUMBER: ($pr_id | tostring)
86-
}
87-
}')"
86+
TEST_SCOPE: "full",
87+
FULL_SUITE: "true",
88+
PR_NUMBER: ($pr_id | tostring),
89+
PR_TITLE: $pr_title
90+
}
91+
}')"
8892
8993
parse-command:
9094
if: >-
@@ -241,6 +245,7 @@ jobs:
241245
TEST_SCOPE: ${{ needs.parse-command.outputs.test_scope }}
242246
FULL_SUITE: ${{ needs.parse-command.outputs.full_suite }}
243247
TEST_TYPE: ${{ needs.parse-command.outputs.test_type }}
248+
PR_TITLE: ${{ github.event.issue.title }}
244249
BK_ORG: ${{ vars.BUILDKITE_ORG_SLUG }}
245250
BK_PIPELINE: ${{ vars.BUILDKITE_PIPELINE_SLUG }}
246251
run: |
@@ -257,6 +262,7 @@ jobs:
257262
--arg full_suite "$FULL_SUITE" \
258263
--arg test_type "$TEST_TYPE" \
259264
--arg pr_number "$PR_NUMBER" \
265+
--arg pr_title "$PR_TITLE" \
260266
'{
261267
commit: $commit,
262268
branch: $branch,
@@ -266,8 +272,9 @@ jobs:
266272
pull_request_base_branch: "main",
267273
env: {
268274
TEST_SCOPE: $test_scope,
269-
FULL_SUITE: $full_suite,
270-
TEST_TYPE: $test_type,
271-
PR_NUMBER: $pr_number
272-
}
273-
}')"
275+
FULL_SUITE: $full_suite,
276+
TEST_TYPE: $test_type,
277+
PR_NUMBER: $pr_number,
278+
PR_TITLE: $pr_title
279+
}
280+
}')"

.github/workflows/ci-trigger-full-suite.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ jobs:
5656
PR_SHA: ${{ github.event.pull_request.head.sha }}
5757
PR_BRANCH: ${{ github.event.pull_request.head.ref }}
5858
PR_NUMBER: ${{ github.event.pull_request.number }}
59+
PR_TITLE: ${{ github.event.pull_request.title }}
5960
BK_ORG: ${{ vars.BUILDKITE_ORG_SLUG }}
6061
BK_PIPELINE: ${{ vars.BUILDKITE_PIPELINE_SLUG }}
6162
run: |
@@ -67,6 +68,7 @@ jobs:
6768
--arg commit "$PR_SHA" \
6869
--arg branch "$PR_BRANCH" \
6970
--arg message "Full Suite for PR #${PR_NUMBER}" \
71+
--arg pr_title "$PR_TITLE" \
7072
--argjson pr_id "$PR_NUMBER" \
7173
'{
7274
commit: $commit,
@@ -78,6 +80,7 @@ jobs:
7880
env: {
7981
TEST_SCOPE: "full",
8082
FULL_SUITE: "true",
81-
PR_NUMBER: ($pr_id | tostring)
83+
PR_NUMBER: ($pr_id | tostring),
84+
PR_TITLE: $pr_title
8285
}
8386
}')"

docs/contributing/testing.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,30 @@ python fastvideo/tests/ssim/reference_videos_cli.py copy-local \
180180
--device-folder L40S_reference_videos
181181
```
182182

183+
### SSIM Bootstrap Mode
184+
185+
Normal SSIM runs are strict: if a reference video or latent is missing, the
186+
test fails. For new-model PRs, CI can run SSIM in bootstrap mode so missing
187+
references are uploaded as draft artifacts for review instead of immediately
188+
blocking on a missing canonical reference.
189+
190+
Buildkite enables SSIM bootstrap mode when either condition is true:
191+
192+
- the PR title or Buildkite message contains `[new-model]`;
193+
- `FASTVIDEO_SSIM_BOOTSTRAP_MODE=1` is set for the Buildkite job.
194+
195+
Bootstrap mode passes `--ssim-bootstrap-mode` to pytest. When a generated
196+
artifact is available, the test uploads it under the `drafts/...` namespace in
197+
the SSIM reference repo and marks that case as expected-failed. After reviewing
198+
the draft, promote it into the canonical reference layout:
199+
200+
```bash
201+
python fastvideo/tests/ssim/reference_videos_cli.py promote-draft \
202+
--quality-tier default \
203+
--device-folder L40S_reference_videos \
204+
--model-id <model_id>
205+
```
206+
183207
## CI Integration
184208

185209
FastVideo CI tests are orchestrated by Buildkite and run on Modal GPU

fastvideo/tests/modal/ssim_test.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -550,6 +550,7 @@ def _spawn_ssim_task(
550550
log_dir: str,
551551
task_index: int,
552552
pytest_extra_args: list[str],
553+
hf_api_key: str,
553554
) -> _RunningTask:
554555
import shlex
555556

@@ -559,6 +560,7 @@ def _spawn_ssim_task(
559560
command = f"set -euo pipefail && source $HOME/.local/bin/env && source /opt/venv/bin/activate && {pytest_command}"
560561
env = os.environ.copy()
561562
env["HF_HOME"] = "/root/data/.cache"
563+
env["HF_API_KEY"] = hf_api_key
562564
# MultiprocExecutor returns CUDA tensors through mp pipes (CUDA IPC).
563565
# On kernels without pidfd_open support, PyTorch fails when
564566
# expandable_segments=True. Force False for CI compatibility.
@@ -675,6 +677,7 @@ def _build_pytest_extra_args(
675677
ssim_full_quality: bool,
676678
ssim_reference_repo: str,
677679
skip_ssim_reference_download: bool,
680+
ssim_bootstrap_mode: bool,
678681
pytest_k: str,
679682
) -> list[str]:
680683
args = []
@@ -684,6 +687,8 @@ def _build_pytest_extra_args(
684687
args.extend(["--ssim-reference-repo", ssim_reference_repo.strip()])
685688
if skip_ssim_reference_download:
686689
args.append("--skip-ssim-reference-download")
690+
if ssim_bootstrap_mode:
691+
args.append("--ssim-bootstrap-mode")
687692
if pytest_k.strip():
688693
args.extend(["-k", pytest_k.strip()])
689694
return args
@@ -693,6 +698,7 @@ def _schedule_ssim_tasks(
693698
repo_root: str,
694699
tasks: list[SSIMTask],
695700
pytest_extra_args: list[str],
701+
hf_api_key: str,
696702
fail_fast: bool = True,
697703
) -> dict[int, _TaskResult]:
698704
import tempfile
@@ -735,6 +741,7 @@ def _schedule_ssim_tasks(
735741
log_dir=log_dir,
736742
task_index=task.task_id,
737743
pytest_extra_args=pytest_extra_args,
744+
hf_api_key=hf_api_key,
738745
)
739746
print(f"Started {task.test_name} on GPUs {','.join(assigned_gpu_ids)}")
740747
running_tasks.append(running_task)
@@ -885,6 +892,7 @@ def run_ssim_partition(
885892
ssim_full_quality: bool = False,
886893
ssim_reference_repo: str = "",
887894
skip_ssim_reference_download: bool = False,
895+
ssim_bootstrap_mode: bool = False,
888896
pytest_k: str = "",
889897
sync_generated_to_volume: bool = False,
890898
generated_volume_subdir: str = "",
@@ -913,12 +921,14 @@ def run_ssim_partition(
913921
ssim_full_quality=ssim_full_quality,
914922
ssim_reference_repo=ssim_reference_repo,
915923
skip_ssim_reference_download=skip_ssim_reference_download,
924+
ssim_bootstrap_mode=ssim_bootstrap_mode,
916925
pytest_k=pytest_k,
917926
)
918927
results = _schedule_ssim_tasks(
919928
repo_root,
920929
partition,
921930
pytest_extra_args=pytest_extra_args,
931+
hf_api_key=hf_api_key,
922932
fail_fast=fail_fast,
923933
)
924934
summaries = _collect_task_summaries(partition, results)
@@ -955,6 +965,7 @@ def run_ssim_tests(
955965
full_quality: bool = False,
956966
reference_repo: str = "",
957967
skip_reference_download: bool = False,
968+
bootstrap_mode: bool = False,
958969
pytest_k: str = "",
959970
sync_generated_to_volume: bool = False,
960971
generated_volume_subdir: str = "",
@@ -975,15 +986,24 @@ def run_ssim_tests(
975986
print(f"Selected model ids: {model_ids}")
976987
if pytest_k.strip():
977988
print(f"Using pytest -k filter: {pytest_k}")
989+
if bootstrap_mode:
990+
print(
991+
"SSIM bootstrap mode enabled: missing references will upload "
992+
"draft artifacts and xfail."
993+
)
978994
quality_tier = _resolve_output_quality_tier(full_quality)
979995
if sync_generated_to_volume:
980996
resolved_subdir = _resolve_generated_volume_subdir(
981997
generated_volume_subdir,
982998
resolved_git_commit,
983999
)
1000+
generated_volume_path = _build_generated_volume_relative_path(
1001+
generated_volume_subdir=resolved_subdir,
1002+
quality_tier=quality_tier,
1003+
)
9841004
print(
9851005
"Raw generated videos will be saved to Modal volume path: "
986-
f"{_build_generated_volume_relative_path(generated_volume_subdir=resolved_subdir, quality_tier=quality_tier)}"
1006+
f"{generated_volume_path}"
9871007
)
9881008
else:
9891009
resolved_subdir = ""
@@ -998,6 +1018,7 @@ def run_ssim_tests(
9981018
ssim_full_quality=full_quality,
9991019
ssim_reference_repo=reference_repo,
10001020
skip_ssim_reference_download=skip_reference_download,
1021+
ssim_bootstrap_mode=bootstrap_mode,
10011022
pytest_k=pytest_k,
10021023
sync_generated_to_volume=sync_generated_to_volume,
10031024
generated_volume_subdir=resolved_subdir,
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
3+
from __future__ import annotations
4+
5+
import importlib.util
6+
import sys
7+
import types
8+
from pathlib import Path
9+
10+
11+
class _FakeImage:
12+
13+
@classmethod
14+
def from_registry(cls, *_args, **_kwargs):
15+
return cls()
16+
17+
def apt_install(self, *_args, **_kwargs):
18+
return self
19+
20+
def run_commands(self, *_args, **_kwargs):
21+
return self
22+
23+
def env(self, *_args, **_kwargs):
24+
return self
25+
26+
27+
class _FakeVolume:
28+
29+
@classmethod
30+
def from_name(cls, *_args, **_kwargs):
31+
return cls()
32+
33+
34+
class _FakeApp:
35+
36+
def function(self, *_args, **_kwargs):
37+
def decorator(func):
38+
return func
39+
40+
return decorator
41+
42+
def local_entrypoint(self, *_args, **_kwargs):
43+
def decorator(func):
44+
return func
45+
46+
return decorator
47+
48+
49+
def _load_ssim_test_module(monkeypatch):
50+
fake_modal = types.SimpleNamespace(
51+
App=lambda: _FakeApp(),
52+
Image=_FakeImage,
53+
Volume=_FakeVolume,
54+
)
55+
monkeypatch.setitem(sys.modules, "modal", fake_modal)
56+
module_path = Path(__file__).with_name("ssim_test.py")
57+
spec = importlib.util.spec_from_file_location("modal_ssim_test_under_test", module_path)
58+
module = importlib.util.module_from_spec(spec)
59+
assert spec.loader is not None
60+
spec.loader.exec_module(module)
61+
return module
62+
63+
64+
def test_spawn_ssim_task_passes_hf_api_key_to_pytest_env(monkeypatch, tmp_path):
65+
module = _load_ssim_test_module(monkeypatch)
66+
captured = {}
67+
68+
class FakeProcess:
69+
pass
70+
71+
def fake_popen(*args, **kwargs):
72+
captured["args"] = args
73+
captured["kwargs"] = kwargs
74+
return FakeProcess()
75+
76+
monkeypatch.setattr(module.subprocess, "Popen", fake_popen)
77+
78+
task = module.SSIMTask(
79+
task_id=0,
80+
test_file="fastvideo/tests/ssim/test_example.py",
81+
required_gpus=1,
82+
model_id="model-a",
83+
)
84+
85+
running_task = module._spawn_ssim_task(
86+
task=task,
87+
repo_root=str(tmp_path),
88+
assigned_gpu_ids=["2"],
89+
log_dir=str(tmp_path),
90+
task_index=0,
91+
pytest_extra_args=["--ssim-bootstrap-mode"],
92+
hf_api_key="hf_test_token",
93+
)
94+
95+
env = captured["kwargs"]["env"]
96+
assert env["HF_API_KEY"] == "hf_test_token"
97+
assert env["HF_HOME"] == "/root/data/.cache"
98+
assert env["CUDA_VISIBLE_DEVICES"] == "2"
99+
assert env["FASTVIDEO_SSIM_MODEL_ID"] == "model-a"
100+
assert running_task.process.__class__ is FakeProcess
101+
running_task.log_handle.close()

0 commit comments

Comments
 (0)