Compare commits
890
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc1b93f15c | ||
|
|
0ee43c01f9 | ||
|
|
06c8c5f4b8 | ||
|
|
c535797e3d | ||
|
|
59fddb00f2 | ||
|
|
9f59519959 | ||
|
|
d5253fd710 | ||
|
|
2b164405c3 | ||
|
|
1033fa8e1c | ||
|
|
e1e2559359 | ||
|
|
8d2e1cf328 | ||
|
|
4ebf11f7d9 | ||
|
|
ef63de1f60 | ||
|
|
e5f31c1b4c | ||
|
|
b7bd7775ed | ||
|
|
dc72a35f4e | ||
|
|
7d61590213 | ||
|
|
3c713d808a | ||
|
|
6ef5b78020 | ||
|
|
6275ac2c8e | ||
|
|
73e878a01c | ||
|
|
3907a69727 | ||
|
|
d1416b5930 | ||
|
|
c520878fd2 | ||
|
|
99f8f3e7a7 | ||
|
|
14e4b7189b | ||
|
|
8530fdc550 | ||
|
|
315b25d26d | ||
|
|
b8920ba50a | ||
|
|
3467f8befb | ||
|
|
7320d9d350 | ||
|
|
af78c759f1 | ||
|
|
f82c190cbb | ||
|
|
e722023bd6 | ||
|
|
bc0191d775 | ||
|
|
2d6e7d9b6b | ||
|
|
5e61accc37 | ||
|
|
f60514183c | ||
|
|
156611bab3 | ||
|
|
6d6195a9f1 | ||
|
|
8348e83e80 | ||
|
|
2452022f15 | ||
|
|
4b3f99b224 | ||
|
|
d967b99928 | ||
|
|
8f1eb2992c | ||
|
|
683689de4f | ||
|
|
d5287f8352 | ||
|
|
cdfbd64b04 | ||
|
|
299d0f6838 | ||
|
|
c18436125a | ||
|
|
5d515ee617 | ||
|
|
37b9dc1836 | ||
|
|
a0291a57c1 | ||
|
|
6102fac00d | ||
|
|
1ad33c1832 | ||
|
|
da87973412 | ||
|
|
a3fd1d0bab | ||
|
|
3e87d6f4ae | ||
|
|
4bb5627fa8 | ||
|
|
08ab8b5c00 | ||
|
|
fb5efebdab | ||
|
|
6e4813f8b4 | ||
|
|
13ef81e6da | ||
|
|
13b3b36b8b | ||
|
|
72795d0af8 | ||
|
|
af63a92f6a | ||
|
|
a4e7322ba6 | ||
|
|
d6e13f9cc8 | ||
|
|
6c4c18690e | ||
|
|
a8688dddd9 | ||
|
|
cacc8cfcb3 | ||
|
|
ef62ac48d1 | ||
|
|
00cb7d0ba2 | ||
|
|
69a6215e7f | ||
|
|
abe98e87f5 | ||
|
|
274b922d39 | ||
|
|
38966056d0 | ||
|
|
d8ca702dfd | ||
|
|
33d81730e9 | ||
|
|
02a027c49f | ||
|
|
e235b6a332 | ||
|
|
8b51380466 | ||
|
|
527a3165b9 | ||
|
|
f06d824054 | ||
|
|
6ded84ee0f | ||
|
|
a4d6416c52 | ||
|
|
0cc41a22dc | ||
|
|
8444c0897a | ||
|
|
9491c8638a | ||
|
|
6809a751fb | ||
|
|
8322b01815 | ||
|
|
9edc8adf5f | ||
|
|
e1f3904799 | ||
|
|
02f1ce11ae | ||
|
|
7f03e03dc6 | ||
|
|
e3b88bb12a | ||
|
|
cb66acd400 | ||
|
|
442e2d2e18 | ||
|
|
dd35763ad6 | ||
|
|
e90be598e5 | ||
|
|
ba5e81083c | ||
|
|
76ce9c7fd6 | ||
|
|
08d99c089e | ||
|
|
20751a21aa | ||
|
|
9dd2a837f4 | ||
|
|
93aab45ac2 | ||
|
|
017ce6602d | ||
|
|
a575055eec | ||
|
|
81f3fec7fd | ||
|
|
d265a454bf | ||
|
|
c100c66578 | ||
|
|
8760eb7a06 | ||
|
|
361f919c88 | ||
|
|
8b5377aab2 | ||
|
|
d995516da0 | ||
|
|
f47ad3f5b7 | ||
|
|
10bdf5e076 | ||
|
|
3e26db40b0 | ||
|
|
c73dd0ab55 | ||
|
|
430e52154e | ||
|
|
384eee8aef | ||
|
|
c4824c7764 | ||
|
|
9b0e57fe4b | ||
|
|
0100218594 | ||
|
|
39718cd54d | ||
|
|
37d06a832f | ||
|
|
8839ba8d4d | ||
|
|
61b91220c0 | ||
|
|
316f3876c2 | ||
|
|
614b59543c | ||
|
|
1c14afd559 | ||
|
|
9a3c45779c | ||
|
|
bfc9c01797 | ||
|
|
3a3ad3d209 | ||
|
|
aef4e9b3b1 | ||
|
|
a943220c11 | ||
|
|
556ac7088e | ||
|
|
e7456f1b75 | ||
|
|
c993d7393e | ||
|
|
7f83164233 | ||
|
|
4e52f47d1e | ||
|
|
e19913f6e9 | ||
|
|
2413a57651 | ||
|
|
7bb76b5ec9 | ||
|
|
0bd19a976b | ||
|
|
40b93784d2 | ||
|
|
33d3478bad | ||
|
|
3d8ac9d14b | ||
|
|
aaef49bfc6 | ||
|
|
cf6a00b9be | ||
|
|
1ae39562dd | ||
|
|
26064193e2 | ||
|
|
8446fc003e | ||
|
|
a28f2bab4b | ||
|
|
f82d8be4bf | ||
|
|
8e1775183e | ||
|
|
620bc36dc4 | ||
|
|
29ff16ec96 | ||
|
|
8f9d76a80d | ||
|
|
a4d9a75e2c | ||
|
|
b2db0c0a13 | ||
|
|
6aa7d8a278 | ||
|
|
ccc9014430 | ||
|
|
a159b63c67 | ||
|
|
ac48bb3cd1 | ||
|
|
3987b9ddcd | ||
|
|
c7da2f5d60 | ||
|
|
39ae1decc0 | ||
|
|
1aed667377 | ||
|
|
c1612ff397 | ||
|
|
a534ba20a0 | ||
|
|
e9bbaca07d | ||
|
|
9bfa585448 | ||
|
|
b2062556a9 | ||
|
|
c9c5585758 | ||
|
|
9212f4f218 | ||
|
|
6388db815b | ||
|
|
7a4285189f | ||
|
|
a837fe841a | ||
|
|
f9e3680f11 | ||
|
|
98f761ec45 | ||
|
|
c041318f2c | ||
|
|
604e0205a4 | ||
|
|
13213395b4 | ||
|
|
46afee5998 | ||
|
|
c488fa1211 | ||
|
|
d3cff517cd | ||
|
|
2f3d407406 | ||
|
|
bcffa4026e | ||
|
|
6d6a10be7a | ||
|
|
73dd105f3d | ||
|
|
56d4a6074f | ||
|
|
c4ad4227c0 | ||
|
|
a63ccce73d | ||
|
|
0462e1b0e7 | ||
|
|
907f2100ec | ||
|
|
e0a3db5651 | ||
|
|
fca45bc8e1 | ||
|
|
86d639c848 | ||
|
|
00338aa9ca | ||
|
|
8537dcd6de | ||
|
|
8208536cd1 | ||
|
|
0653f8f3af | ||
|
|
e0d702decb | ||
|
|
541ef014ee | ||
|
|
ffc1a7a58b | ||
|
|
9028953625 | ||
|
|
6eb95693a1 | ||
|
|
c3567eb468 | ||
|
|
15568f27db | ||
|
|
126a75ad63 | ||
|
|
a2bfc7cdb2 | ||
|
|
b963a24612 | ||
|
|
fb7be2fe2c | ||
|
|
ab00392664 | ||
|
|
9f1e7c19d2 | ||
|
|
e8b0e4c61e | ||
|
|
9145ffdc46 | ||
|
|
c3d07c870b | ||
|
|
e8812bef0b | ||
|
|
b9be2449dc | ||
|
|
bc7a804618 | ||
|
|
7b094c945b | ||
|
|
eeb3e8a597 | ||
|
|
05406c5d1b | ||
|
|
99d04a7f98 | ||
|
|
1b2b2a0161 | ||
|
|
98d65835b5 | ||
|
|
422585d08f | ||
|
|
e59a1ce16a | ||
|
|
d71acc0eb5 | ||
|
|
af2934dd6b | ||
|
|
1801512818 | ||
|
|
5ae05b032e | ||
|
|
7a592ff09a | ||
|
|
8b23984c79 | ||
|
|
bf18371afe | ||
|
|
69349dd2aa | ||
|
|
8d89f30d3f | ||
|
|
10546353da | ||
|
|
9fb74b9732 | ||
|
|
521dee0e82 | ||
|
|
229419208e | ||
|
|
65f3b946b9 | ||
|
|
191fcbf46c | ||
|
|
755a4e4470 | ||
|
|
9709b7513b | ||
|
|
32cd603515 | ||
|
|
d4bdd3621a | ||
|
|
e2f8322842 | ||
|
|
6966f9e0bc | ||
|
|
743f4ed5f9 | ||
|
|
1c04ace573 | ||
|
|
6cbff73687 | ||
|
|
dec8b10939 | ||
|
|
133a5278af | ||
|
|
da856274cc | ||
|
|
a253856147 | ||
|
|
6e25d94ebc | ||
|
|
cae8fa18dc | ||
|
|
821e5a0832 | ||
|
|
c1abc42782 | ||
|
|
ef15ea2391 | ||
|
|
1ea2517e22 | ||
|
|
0c63528c59 | ||
|
|
b063f8ca41 | ||
|
|
e7fff0173a | ||
|
|
d82abc271e | ||
|
|
970409962f | ||
|
|
055586703d | ||
|
|
5d89f86675 | ||
|
|
19a51a1fe6 | ||
|
|
d3232cea5a | ||
|
|
0c90c8c24d | ||
|
|
4c08ffce49 | ||
|
|
af4a77553c | ||
|
|
c096fda1eb | ||
|
|
8f47e85be0 | ||
|
|
90d3bd19eb | ||
|
|
afb4f7d3c5 | ||
|
|
02e1143f22 | ||
|
|
e2f4d1a7b5 | ||
|
|
f037351146 | ||
|
|
1ee11e08dc | ||
|
|
595f0ea60e | ||
|
|
d921832cd2 | ||
|
|
629697629a | ||
|
|
a25313beec | ||
|
|
dbde64385b | ||
|
|
9d909f5f04 | ||
|
|
76b0550c15 | ||
|
|
384c1e9493 | ||
|
|
b1dbcc93f6 | ||
|
|
b93833772e | ||
|
|
30b523edd6 | ||
|
|
6aab7f3832 | ||
|
|
9cd53fe5f8 | ||
|
|
6a32cf3a5e | ||
|
|
98be9b3da2 | ||
|
|
c53e85b767 | ||
|
|
40a8bd2d3b | ||
|
|
31aa115611 | ||
|
|
98ac10a528 | ||
|
|
a5a6d171e5 | ||
|
|
51ed1ea423 | ||
|
|
00ec3e7388 | ||
|
|
0d626ef2d1 | ||
|
|
b36d0ef085 | ||
|
|
10c8c5df4d | ||
|
|
31e26abec4 | ||
|
|
9e83ba630c | ||
|
|
fc02a9ce8e | ||
|
|
3d36160fc4 | ||
|
|
a11ec43de2 | ||
|
|
2656d6530c | ||
|
|
e3f54e7169 | ||
|
|
4ba5681307 | ||
|
|
8c23c86994 | ||
|
|
78d606a3a7 | ||
|
|
c4e108de78 | ||
|
|
16bf2eaf77 | ||
|
|
1d15d974fa | ||
|
|
5e57868b76 | ||
|
|
6be280c914 | ||
|
|
3ccdec9798 | ||
|
|
8658f774f3 | ||
|
|
4f3ad3f6df | ||
|
|
b454aa56c3 | ||
|
|
f9b8e30ff3 | ||
|
|
0356205b84 | ||
|
|
719a1879bd | ||
|
|
b57180bf97 | ||
|
|
7cebf5f82c | ||
|
|
dd0f4b6753 | ||
|
|
d303b4e03a | ||
|
|
31b719ae49 | ||
|
|
887aaf3d3e | ||
|
|
b1d89eba1f | ||
|
|
995a5fdf97 | ||
|
|
70a70b689e | ||
|
|
4d6ac89b43 | ||
|
|
4171cacd93 | ||
|
|
b2ade71467 | ||
|
|
82ed9fe58d | ||
|
|
3d8cc4f0a0 | ||
|
|
0557f7a7d9 | ||
|
|
dc66cd97ef | ||
|
|
6da206e196 | ||
|
|
87f98c9b8b | ||
|
|
e60601df7f | ||
|
|
eed9c4bfbf | ||
|
|
1dee77f4a4 | ||
|
|
b80148819c | ||
|
|
c3b971488e | ||
|
|
88e753f281 | ||
|
|
77832059cc | ||
|
|
633d393568 | ||
|
|
5854aec2ce | ||
|
|
30e45c2411 | ||
|
|
2a4fe697a6 | ||
|
|
921db7479d | ||
|
|
7f539424cb | ||
|
|
19a838f54f | ||
|
|
d922ab2cbc | ||
|
|
9ea77d37f3 | ||
|
|
2e35b0c6bd | ||
|
|
1c627a3f98 | ||
|
|
a931efe33a | ||
|
|
041e5e9029 | ||
|
|
efcc245c2e | ||
|
|
922e7e0813 | ||
|
|
c62a8514b0 | ||
|
|
3eb8081801 | ||
|
|
3505d09564 | ||
|
|
570607945c | ||
|
|
d3a821cdcf | ||
|
|
3f24578139 | ||
|
|
89fcf08378 | ||
|
|
c2930b2aa1 | ||
|
|
019239690b | ||
|
|
d6119c1f82 | ||
|
|
84214c80bb | ||
|
|
c2d7143c72 | ||
|
|
afdb6fbfa5 | ||
|
|
ba4c02d883 | ||
|
|
2c137931f3 | ||
|
|
36682797a0 | ||
|
|
0ef1357a77 | ||
|
|
a75d19786a | ||
|
|
be548a78ea | ||
|
|
6a610e2bc9 | ||
|
|
321d5112b4 | ||
|
|
ba75ad82db | ||
|
|
58caa5109f | ||
|
|
2f3ca8aaad | ||
|
|
3a67319cb6 | ||
|
|
266fa044b3 | ||
|
|
f3398db868 | ||
|
|
fda02036bc | ||
|
|
68179cd752 | ||
|
|
2dd5760291 | ||
|
|
af2ee9c78a | ||
|
|
1c80371b27 | ||
|
|
eef473225d | ||
|
|
44fb84ef6a | ||
|
|
e8597b7448 | ||
|
|
e2252c0a5e | ||
|
|
72cb427cd9 | ||
|
|
63030cf6ec | ||
|
|
773d44b875 | ||
|
|
1df513922f | ||
|
|
30c45620a2 | ||
|
|
6b2c731596 | ||
|
|
e6022c20b2 | ||
|
|
460f6e398e | ||
|
|
d2ffec5cce | ||
|
|
cb12e88713 | ||
|
|
0958c344b8 | ||
|
|
71dc27ea7b | ||
|
|
1263449d2a | ||
|
|
b4de5a9f1b | ||
|
|
8acd8e21f9 | ||
|
|
d45d82334f | ||
|
|
6392bd40a9 | ||
|
|
17f07bc313 | ||
|
|
325861fb99 | ||
|
|
c5088670c8 | ||
|
|
0403c6f47e | ||
|
|
a55b1cdde4 | ||
|
|
aec9a20a16 | ||
|
|
b4458e5bae | ||
|
|
a790153705 | ||
|
|
0df1445d0d | ||
|
|
038da6e02b | ||
|
|
3fb2fbe1a2 | ||
|
|
acea9d23e1 | ||
|
|
b7a448cf5b | ||
|
|
04e32991c6 | ||
|
|
424c643b6e | ||
|
|
de803cb250 | ||
|
|
effc1d3492 | ||
|
|
b1ddb1ba33 | ||
|
|
9ff65c83b6 | ||
|
|
15473f3c77 | ||
|
|
490641cf47 | ||
|
|
4ad6880ca6 | ||
|
|
9d0af28307 | ||
|
|
d6dfe95466 | ||
|
|
636d3b743e | ||
|
|
e3a5c6954f | ||
|
|
f633e30ebb | ||
|
|
5ce4947ac2 | ||
|
|
323d74c0d2 | ||
|
|
d98aeafc86 | ||
|
|
f6396fb8c6 | ||
|
|
6300329cd5 | ||
|
|
c17d33bf33 | ||
|
|
2aaeee2ab8 | ||
|
|
eb3a394224 | ||
|
|
f673423b51 | ||
|
|
eb0a41528a | ||
|
|
140bd1a6cf | ||
|
|
11f5a8e582 | ||
|
|
71b3cb8c34 | ||
|
|
c85f6a477f | ||
|
|
40d4930d73 | ||
|
|
f9be085243 | ||
|
|
36b53ff350 | ||
|
|
9801037c3d | ||
|
|
74d09b0efd | ||
|
|
38dc8820ac | ||
|
|
c77a76c6af | ||
|
|
d14d5aadea | ||
|
|
4c915b7742 | ||
|
|
9a8bbe18fa | ||
|
|
ea25441ef0 | ||
|
|
48957fcde1 | ||
|
|
7b872cc41e | ||
|
|
37418946c8 | ||
|
|
95fd29e0cb | ||
|
|
e17cd2633c | ||
|
|
e0dc5f2b0c | ||
|
|
70ee5d230c | ||
|
|
24ced500f5 | ||
|
|
4ddcdf541f | ||
|
|
0e3529869c | ||
|
|
e1e0d91c00 | ||
|
|
145a3f166b | ||
|
|
88a5a933ab | ||
|
|
c591d6d2a6 | ||
|
|
65dff806a8 | ||
|
|
b85f0f4c2a | ||
|
|
76c62d7a00 | ||
|
|
f6e65ff668 | ||
|
|
c220aa8000 | ||
|
|
4713fc17ed | ||
|
|
5789955bbe | ||
|
|
2ad84a3b78 | ||
|
|
12d699cd78 | ||
|
|
34f14ded21 | ||
|
|
71d1ab411f | ||
|
|
805e487773 | ||
|
|
8803b4547e | ||
|
|
3b3806b3f6 | ||
|
|
38d962e89d | ||
|
|
3966a365d0 | ||
|
|
d73fd14af0 | ||
|
|
a87cc89916 | ||
|
|
81fd80c8ee | ||
|
|
ff22439f28 | ||
|
|
de0de04212 | ||
|
|
7f2c3e1f64 | ||
|
|
ab55e57c22 | ||
|
|
46f6b43a53 | ||
|
|
833a33b663 | ||
|
|
9ea1307cd4 | ||
|
|
be35003cb1 | ||
|
|
26bd4db253 | ||
|
|
e294ca011c | ||
|
|
2085a4fc4a | ||
|
|
c0c8e39c04 | ||
|
|
f72618dafb | ||
|
|
b3edfacdd8 | ||
|
|
30129a3350 | ||
|
|
4d49f7b0aa | ||
|
|
71bfc13d75 | ||
|
|
74db6e18d1 | ||
|
|
7d263c6a36 | ||
|
|
454c32d1d1 | ||
|
|
d1240b9238 | ||
|
|
4105094fa5 | ||
|
|
f036469d3d | ||
|
|
14261bc98c | ||
|
|
d92858659d | ||
|
|
1a383f3f66 | ||
|
|
bc27a032c5 | ||
|
|
2b13e117f0 | ||
|
|
99c166c381 | ||
|
|
95066245db | ||
|
|
6dcaac768b | ||
|
|
02c1c49b75 | ||
|
|
cd1b7cf139 | ||
|
|
e63b7d8ac4 | ||
|
|
5190c1bb1e | ||
|
|
2cb3bba658 | ||
|
|
f9e1c46c3c | ||
|
|
e1eda47589 | ||
|
|
d902967208 | ||
|
|
fea556269b | ||
|
|
69dd3c68f6 | ||
|
|
5433f6e80b | ||
|
|
e315657066 | ||
|
|
f8d9a0c57f | ||
|
|
fa6d276925 | ||
|
|
fc80d95d7e | ||
|
|
37cab18780 | ||
|
|
128d0b7fc5 | ||
|
|
8092f02e6d | ||
|
|
03d9ce2edb | ||
|
|
10fc92dba5 | ||
|
|
6736dc06a5 | ||
|
|
8c002c62af | ||
|
|
7061313d04 | ||
|
|
76d3ba69e0 | ||
|
|
8e39ce38c9 | ||
|
|
d4bd8bf2c0 | ||
|
|
e83d7bc50c | ||
|
|
ff3d5aff75 | ||
|
|
959dbcc8a2 | ||
|
|
36bf37e9ba | ||
|
|
7a83e0e6fc | ||
|
|
8be1313b86 | ||
|
|
d925ad05f3 | ||
|
|
7f795600c8 | ||
|
|
ec16b6b01d | ||
|
|
31c0f1b341 | ||
|
|
4bee0fa199 | ||
|
|
530e6b8363 | ||
|
|
9ab2725db1 | ||
|
|
0aff68f51d | ||
|
|
ad58f802f3 | ||
|
|
04fa356ee3 | ||
|
|
f9c076fe2b | ||
|
|
f76efe798e | ||
|
|
09f455233e | ||
|
|
aea300f690 | ||
|
|
b92219f6a6 | ||
|
|
a321b95a8a | ||
|
|
98308db7e0 | ||
|
|
c1e18f6722 | ||
|
|
75e193a2c9 | ||
|
|
d6e0a7d0dd | ||
|
|
7fc5f241da | ||
|
|
aae48a7e90 | ||
|
|
88f38eb0f4 | ||
|
|
d750b463dc | ||
|
|
e10b26a3d8 | ||
|
|
74636ba246 | ||
|
|
7e2f3f14e7 | ||
|
|
caa1c402ba | ||
|
|
38a6bd93d3 | ||
|
|
b867ef7e7c | ||
|
|
3ae58c277a | ||
|
|
0c6862ca55 | ||
|
|
e8c854bcf1 | ||
|
|
06860e96fe | ||
|
|
1b503554d1 | ||
|
|
351ceb7c59 | ||
|
|
10875e0d7b | ||
|
|
1eaae8a10b | ||
|
|
59e00f6164 | ||
|
|
745cc05b10 | ||
|
|
c5dc244871 | ||
|
|
dbf3917bf4 | ||
|
|
050f189c95 | ||
|
|
029216029f | ||
|
|
31f44110b5 | ||
|
|
21f3ce6577 | ||
|
|
785d123e36 | ||
|
|
d58c551c11 | ||
|
|
560628709c | ||
|
|
0f53b51e6c | ||
|
|
06093a9c4e | ||
|
|
dbddfab6d2 | ||
|
|
7188170277 | ||
|
|
b7f69c2c1d | ||
|
|
23a4531491 | ||
|
|
7d52ad0118 | ||
|
|
4d7bf35fa3 | ||
|
|
a6a9c9ca07 | ||
|
|
f4704847c2 | ||
|
|
d9c996310b | ||
|
|
d6651afd2e | ||
|
|
cf67618cad | ||
|
|
2f0a2b3c57 | ||
|
|
e7748d9952 | ||
|
|
8eb3140b2f | ||
|
|
d6ddcea682 | ||
|
|
3559ba2377 | ||
|
|
61e63ea0d7 | ||
|
|
4ce4ac4734 | ||
|
|
e7f6db9bd1 | ||
|
|
d83f45a6a0 | ||
|
|
dd91542cd1 | ||
|
|
581e8115fe | ||
|
|
dea69cf651 | ||
|
|
60ac6537df | ||
|
|
5285116e73 | ||
|
|
40ce2d72f5 | ||
|
|
704bc9aaf9 | ||
|
|
de264fcc99 | ||
|
|
7b952e4673 | ||
|
|
551b2d2048 | ||
|
|
7bfaf82fd7 | ||
|
|
9cd6a86b95 | ||
|
|
16e9552778 | ||
|
|
87f8a2782d | ||
|
|
cbbb09d7b8 | ||
|
|
2f6230abcf | ||
|
|
f8bfc76015 | ||
|
|
8f1e6c3336 | ||
|
|
8e7d2e7879 | ||
|
|
6ab2870942 | ||
|
|
e0ad145152 | ||
|
|
da04d08426 | ||
|
|
1f70032af5 | ||
|
|
7f71994653 | ||
|
|
2bb3349da1 | ||
|
|
8fe1689968 | ||
|
|
e53730f324 | ||
|
|
7a4fe9086a | ||
|
|
d277361aae | ||
|
|
734a54e7a9 | ||
|
|
91364982df | ||
|
|
50145e4fcb | ||
|
|
4112507e99 | ||
|
|
424fc2b4ae | ||
|
|
e6066223e6 | ||
|
|
b6fa3d24d8 | ||
|
|
55c2e7cd76 | ||
|
|
5a549af823 | ||
|
|
92fb660c2e | ||
|
|
3ff640b2e6 | ||
|
|
c722429ab5 | ||
|
|
e04a192de6 | ||
|
|
c9ca6d1298 | ||
|
|
754292c419 | ||
|
|
0082bc66fc | ||
|
|
8b1937422e | ||
|
|
fb6cbf23e6 | ||
|
|
c8fdd5ed7b | ||
|
|
1c19a6a00c | ||
|
|
d44409c704 | ||
|
|
5d1c7852b7 | ||
|
|
77a211d006 | ||
|
|
bef8169bb1 | ||
|
|
681f1583f9 | ||
|
|
e3b4564d5a | ||
|
|
c0d03fc43d | ||
|
|
404ee8538e | ||
|
|
e57ac59462 | ||
|
|
8c55fdaf7e | ||
|
|
c30779184f | ||
|
|
9d188c0b6c | ||
|
|
9dd7c54221 | ||
|
|
62b95d8287 | ||
|
|
fdf21702f5 | ||
|
|
2972fc9449 | ||
|
|
8f5712629f | ||
|
|
436c701b9f | ||
|
|
543fea88e3 | ||
|
|
bdec816b31 | ||
|
|
2cd2e57d2e | ||
|
|
9370234294 | ||
|
|
50da62e722 | ||
|
|
4f3e8751db | ||
|
|
f4c58894d9 | ||
|
|
01c94ef385 | ||
|
|
2415226d25 | ||
|
|
404314d00f | ||
|
|
87489f0872 | ||
|
|
9ce7c8039e | ||
|
|
e1e25e95f9 | ||
|
|
490bde90e1 | ||
|
|
dc7596b973 | ||
|
|
335afa4457 | ||
|
|
3f77a6805a | ||
|
|
13d0aae706 | ||
|
|
404cbf4f3c | ||
|
|
958ffec844 | ||
|
|
31f000d1cc | ||
|
|
cd32b3e02f | ||
|
|
bf27908095 | ||
|
|
c5f9ea53b2 | ||
|
|
d32a7184da | ||
|
|
2930abe456 | ||
|
|
b93ef4289d | ||
|
|
401bdbd316 | ||
|
|
1048d79cf8 | ||
|
|
1e8406162d | ||
|
|
03edd35c83 | ||
|
|
ac11127397 | ||
|
|
e028dcc7c0 | ||
|
|
076f45c1ee | ||
|
|
85eb7265db | ||
|
|
d3ceb67e66 | ||
|
|
7ac153a5ca | ||
|
|
d1e7aa0abd | ||
|
|
2d846c55a1 | ||
|
|
b318063c0a | ||
|
|
4aa307be55 | ||
|
|
055e52e5ea | ||
|
|
7d2069596b | ||
|
|
c45009c9a4 | ||
|
|
b91020b407 | ||
|
|
2dcc5ea4f6 | ||
|
|
359151d9a0 | ||
|
|
ce67cd3729 | ||
|
|
7c554e5da8 | ||
|
|
663ea33ff1 | ||
|
|
3ef04f1654 | ||
|
|
0eced76a41 | ||
|
|
3ab6470d1a | ||
|
|
989a03532c | ||
|
|
fa15369a02 | ||
|
|
78a9cb88d8 | ||
|
|
a0bff12746 | ||
|
|
98f2af94e5 | ||
|
|
46f7b6d574 | ||
|
|
911a6a6a35 | ||
|
|
38c7949d5c | ||
|
|
7e7a0dba9d | ||
|
|
f62e210ae6 | ||
|
|
6ceb4942a0 | ||
|
|
8cae5e4708 | ||
|
|
2a773fa34e | ||
|
|
5357f63327 | ||
|
|
60f61c8101 | ||
|
|
3d75ba8251 | ||
|
|
6c6bcd914d | ||
|
|
f79b08de81 | ||
|
|
f2bc037fff | ||
|
|
86604a684b | ||
|
|
47bd1e0178 | ||
|
|
c41305ad18 | ||
|
|
98ce9034f0 | ||
|
|
0ceff110da | ||
|
|
1d018acb3e | ||
|
|
7d8cf38dbe | ||
|
|
8d483fe4aa | ||
|
|
c1191250bf | ||
|
|
4b7266349a | ||
|
|
22f9b7681f | ||
|
|
589d32cc39 | ||
|
|
89199837db | ||
|
|
d6ebaf1b49 | ||
|
|
fac927777c | ||
|
|
ecbd697dae | ||
|
|
7d4acef64d | ||
|
|
9f0ce517cf | ||
|
|
c718e56b0d | ||
|
|
b65f0316d1 | ||
|
|
8d8bcb76b0 | ||
|
|
5f42748ed1 | ||
|
|
c9005045dc | ||
|
|
6c81befc87 | ||
|
|
dfe0b288e1 | ||
|
|
31200fbb83 | ||
|
|
9185978c55 | ||
|
|
fcba463553 | ||
|
|
2c53d3eecf | ||
|
|
516ecd374a | ||
|
|
3b1b54a74d | ||
|
|
6914e7c904 | ||
|
|
5452369749 | ||
|
|
a113311e77 | ||
|
|
44da97da92 | ||
|
|
f759980a58 | ||
|
|
37e0f8c236 | ||
|
|
51711d5906 | ||
|
|
6375223b16 | ||
|
|
4cb046768d | ||
|
|
3322542444 | ||
|
|
65f707354b | ||
|
|
109e2e7e9d | ||
|
|
cbc3a6bb9d | ||
|
|
2fa8d4ae6d | ||
|
|
7b6c8aee99 | ||
|
|
6284eaa363 | ||
|
|
636524e87f | ||
|
|
202b2f3972 | ||
|
|
247fe273d8 | ||
|
|
cb320dfa3a | ||
|
|
d8bb5abc46 | ||
|
|
cc703eca51 | ||
|
|
81c9df629c | ||
|
|
d3c0c52208 | ||
|
|
744e0555c0 | ||
|
|
3a38f7dfdc | ||
|
|
f572319bd9 | ||
|
|
48528f468c | ||
|
|
4264a80ca9 | ||
|
|
8573d4f05e | ||
|
|
210a733515 | ||
|
|
0aef0e6f63 | ||
|
|
dd022ad9be | ||
|
|
832ad61e5b | ||
|
|
9419c04ee3 | ||
|
|
a37b39d83c | ||
|
|
bb8c769c8e | ||
|
|
576c214f28 | ||
|
|
b79d1fc15b | ||
|
|
eb66e1c18d | ||
|
|
616d43c1cf | ||
|
|
7244a4b27f | ||
|
|
7e5ebb4582 | ||
|
|
65ed588570 | ||
|
|
14adfe2edc | ||
|
|
6198c6a640 | ||
|
|
e6b71b531b | ||
|
|
ae1d112c6a | ||
|
|
bf4de1f38f | ||
|
|
66fdcc8e76 | ||
|
|
ed1e8d6bad | ||
|
|
b9423ca3f8 | ||
|
|
ad16289871 | ||
|
|
2a41da1e6b | ||
|
|
32133171da | ||
|
|
19674c6f29 | ||
|
|
508afb7002 | ||
|
|
288ea88105 | ||
|
|
eb0f1318f3 | ||
|
|
ce9b5910cc | ||
|
|
d0e5a6214a | ||
|
|
834562b2db | ||
|
|
060cc7b9ba | ||
|
|
6c58a5ba62 | ||
|
|
48d9f61f86 | ||
|
|
5f938b5844 | ||
|
|
74da2a7370 | ||
|
|
580d6dfe1f | ||
|
|
344e43006a | ||
|
|
c5155b256e | ||
|
|
e005c7f3ac | ||
|
|
ff5a79ef60 | ||
|
|
ab01dc4ba5 | ||
|
|
285a950c1b | ||
|
|
46a0a85d85 | ||
|
|
4aeabbc629 | ||
|
|
949bb5c835 |
@@ -0,0 +1,73 @@
|
||||
---
|
||||
date: 2026-05-07
|
||||
experiment: PR #1280 (daVinci-MagiHuman port), distill DiT parity bring-up
|
||||
category: porting
|
||||
severity: important
|
||||
---
|
||||
|
||||
# Conversion `--cast-bf16` Needs an FP32-Keep Suffix Allowlist
|
||||
|
||||
## What Happened
|
||||
|
||||
`scripts/checkpoint_conversion/convert_magi_human_to_diffusers.py --cast-bf16`
|
||||
produced a converted distill DiT checkpoint that loaded cleanly, ran end-to-
|
||||
end, and emitted reasonable output — but `test_magi_human_distill_parity`
|
||||
showed `diff_mean=0.114` against the upstream reference. The base DiT was
|
||||
bit-exact with the same conversion script. Only the distill variant
|
||||
regressed.
|
||||
|
||||
The error was small enough that visual quality looked normal, but large
|
||||
enough to fail bit-exact parity. The MagiHuman base + distill DiTs share
|
||||
most of their architecture, so a difference that affected only distill was
|
||||
counterintuitive.
|
||||
|
||||
## Root Cause
|
||||
|
||||
`--cast-bf16` was downcasting **all** fp32 tensors to bf16 indiscriminately.
|
||||
The base checkpoint and the FastVideo `final_linear` / adapter modules
|
||||
require eight specific tensors to remain in fp32:
|
||||
|
||||
- LayerNorm `gamma` / `beta` weights for the final residual exit
|
||||
- Adapter projection biases
|
||||
- A handful of scale parameters in the output projection chain
|
||||
|
||||
These tensors participate in chains where bf16 precision causes accumulation
|
||||
error large enough to drift the parity check. The base DiT happened to not
|
||||
hit those specific chains in the path the test exercised (different
|
||||
attention mask shape, different audio interleave); the distill variant did.
|
||||
|
||||
## Fix / Workaround
|
||||
|
||||
Added `_FP32_KEEP_SUFFIXES` allowlist to
|
||||
`convert_magi_human_to_diffusers.py` (commit `829f70d3`) and gated `--cast-
|
||||
bf16` on it. Tensors whose state-dict key ends with any allowlisted suffix
|
||||
keep their original fp32 dtype regardless of the flag.
|
||||
|
||||
Distill DiT parity went from `diff_mean=0.114` (silently wrong) to bit-exact
|
||||
in one commit.
|
||||
|
||||
## Prevention
|
||||
|
||||
1. **Treat `--cast-bf16` as opinionated, not blanket.** Any conversion
|
||||
script that supports a global dtype downcast flag MUST own an explicit
|
||||
allowlist of fp32-keep tensors, documented at the top of the file.
|
||||
|
||||
2. **Check two things** for any converter that ships a `--cast-bf16`-style
|
||||
flag:
|
||||
- Run the parity test for **every** variant of the model (base, distill,
|
||||
SR, etc.), not just the headline variant. Different variants exercise
|
||||
different code paths.
|
||||
- Diff the converted checkpoint's dtype map against the upstream
|
||||
reference and assert the allowlist covers every fp32 tensor in the
|
||||
reference.
|
||||
|
||||
3. **For MagiHuman specifically**: if you add or rename DiT modules that
|
||||
touch `final_linear`, the adapter, or any LayerNorm in the residual exit
|
||||
path, **check that any fp32-required tensors are covered by
|
||||
`_FP32_KEEP_SUFFIXES`** in the conversion script and re-run
|
||||
`test_magi_human_distill_parity` (it's the canary).
|
||||
|
||||
4. The lesson generalizes beyond MagiHuman: any DiT that uses bf16 mixed
|
||||
precision but keeps specific tensors in fp32 (a common pattern with
|
||||
flash-attn-style backends) needs this allowlist for any conversion that
|
||||
downcasts.
|
||||
@@ -0,0 +1,77 @@
|
||||
---
|
||||
date: 2026-05-07
|
||||
experiment: PR #1280 (daVinci-MagiHuman port), DiT parity bring-up
|
||||
category: porting
|
||||
severity: important
|
||||
---
|
||||
|
||||
# DiT Dtype Boundary Alignment with Flash-Attn-Style Backends
|
||||
|
||||
## What Happened
|
||||
|
||||
DiT bit-exact parity for daVinci-MagiHuman against the upstream reference
|
||||
sat at `diff_max=0.5` after the architecture port was complete and weight
|
||||
loading was correct. The error grew with depth (later layers diverged more
|
||||
than earlier ones), suggesting an accumulating numerical drift rather than
|
||||
a structural mismatch. None of the obvious culprits (RoPE, GQA expansion,
|
||||
attention mask handling) accounted for the pattern.
|
||||
|
||||
## Root Cause
|
||||
|
||||
Four cumulative dtype-boundary mismatches, each individually small but
|
||||
together pushing parity from `diff_max=0.5` to bit-exact (`diff_max=0.0`):
|
||||
|
||||
1. **SDPA inputs were not cast to bf16.** Upstream's `flash_attn_with_cp`
|
||||
internally casts Q/K/V to bf16 at `dit_module.py:508` before the kernel.
|
||||
FastVideo was passing fp32 tensors through, getting numerically different
|
||||
intermediates even though the kernel accepts both.
|
||||
|
||||
2. **Post-attention output was kept in bf16 across the per-head gating
|
||||
multiply.** Upstream upcasts to fp32 before the gating, FastVideo did the
|
||||
gate in bf16 then upcast.
|
||||
|
||||
3. **A residual-stream cast at the block boundary.** FastVideo had a
|
||||
`.to(bf16)` then `.to(fp32)` at the start of each block. Upstream keeps
|
||||
the residual stream **continuously in fp32** across all 40 layers; only
|
||||
the inputs to specific kernels are temporarily downcast.
|
||||
|
||||
4. **Parity test scheduler used a double-shift.** A separate per-block fix
|
||||
(Wave 11 production migration) — single-shift schedule is what upstream
|
||||
uses; the parity test was double-shifting.
|
||||
|
||||
## Fix / Workaround
|
||||
|
||||
Four cumulative changes in `fastvideo/models/dits/magi_human.py` (commit
|
||||
`3a4816cb`), each with a comment at the call site explaining the upstream
|
||||
parity rationale:
|
||||
|
||||
- Cast SDPA inputs to bf16 right before the attention call.
|
||||
- Upcast attention output to fp32 before the per-head gate multiply.
|
||||
- Drop the residual-stream `.to(bf16)`/`.to(fp32)` wrapper at the block
|
||||
boundary; let the residual stay fp32 throughout.
|
||||
- Single-shift schedule in the parity test fixture (matches upstream Wave 11).
|
||||
|
||||
## Prevention
|
||||
|
||||
1. **For any DiT port with a flash-attn-style backend**, treat the dtype of
|
||||
the residual stream as a load-bearing invariant, not a performance knob.
|
||||
Document it in the model's per-pipeline AGENTS.md. MagiHuman's invariant:
|
||||
*residual stream stays fp32 across all blocks; only kernel inputs are
|
||||
temporarily bf16*.
|
||||
|
||||
2. **Use layer-by-layer activation hooks** when DiT parity is close-but-not-
|
||||
bit-exact and the gap grows with depth. The
|
||||
`fastvideo/hooks/activation_trace.py` infra exists exactly for this case
|
||||
(see `docs/contributing/activation_trace.md`). In MagiHuman's case it would have localized the
|
||||
first divergence point in one pass.
|
||||
|
||||
3. **DiT ports must account for:**
|
||||
- SDPA input dtype must match the upstream kernel's internal cast.
|
||||
- Post-attention upcast happens **before** any per-head gate, not after.
|
||||
- Residual stream dtype across block boundaries is a parity invariant.
|
||||
These rules apply to any DiT port whose upstream uses a flash-attn-style
|
||||
backend (`flash_attn_with_cp`, `flex_flash_attn_func`, etc.).
|
||||
|
||||
4. **Add an "intermediate-layer parity" test** for new DiT ports — comparing
|
||||
activations at layer 5, 10, 20, 30 — not just the final output. A growing-
|
||||
with-depth pattern is otherwise indistinguishable from "almost right".
|
||||
@@ -0,0 +1,68 @@
|
||||
---
|
||||
date: 2026-05-07
|
||||
experiment: PR #1280 (daVinci-MagiHuman port), Wave 14
|
||||
category: porting
|
||||
severity: critical
|
||||
---
|
||||
|
||||
# Silent Channel-Major Token-Packing Bugs
|
||||
|
||||
## What Happened
|
||||
|
||||
While porting daVinci-MagiHuman (`fastvideo/pipelines/basic/magi_human/`),
|
||||
the pipeline-parity test passed bit-exactly but the E2E user-visible output
|
||||
was **pure static noise**. Latent tensors compared identically against the
|
||||
upstream reference at every checkpointed boundary, yet decoded videos showed
|
||||
no recognizable content. The discrepancy reproduced on every variant
|
||||
(base / distill / SR-540p / SR-1080p) with the same noise profile.
|
||||
|
||||
## Root Cause
|
||||
|
||||
Video tokens were being packed **spatial-major** instead of **channel-major**:
|
||||
|
||||
```python
|
||||
# What we had (spatial-major, WRONG)
|
||||
einops.rearrange(x, "b c (T pT) (H pH) (W pW) -> b (T H W) (pT pH pW C)", ...)
|
||||
|
||||
# What upstream's UnfoldNd produces (channel-major, CORRECT)
|
||||
einops.rearrange(x, "b c (T pT) (H pH) (W pW) -> b (T H W) (C pT pH pW)", ...)
|
||||
```
|
||||
|
||||
A single-character einops reorder. The pipeline-parity test used FastVideo's
|
||||
own packer on **both** sides of the comparison, so the bug was invisible there
|
||||
— both sides agreed on the wrong layout. The DiT consumed those tokens
|
||||
without complaint because the channel dimension only matters at decode time,
|
||||
when the VAE's first conv expects channel-major input. By that point the test
|
||||
boundary was already passed.
|
||||
|
||||
The bug was load-bearing for any token-packed format that downstream feeds
|
||||
into a `UnfoldNd`-shaped consumer. Wave 14 of the port took multiple bug-hunt
|
||||
iterations and an Oracle consultation to localize.
|
||||
|
||||
## Fix / Workaround
|
||||
|
||||
Single-character einops change in `stages/latent_preparation.py:_img2tokens`
|
||||
(commit `6d190693` of the original PR). After the fix, all four variants
|
||||
produced expected E2E output and the pipeline-parity tests still passed
|
||||
because both sides of the parity check are now correct.
|
||||
|
||||
## Prevention
|
||||
|
||||
1. **Never use the FastVideo-side packer on both sides of a parity test.**
|
||||
At least one parity boundary must compare against an upstream tensor
|
||||
produced by the upstream packer. For MagiHuman this means a separate
|
||||
`_img2tokens` parity test that feeds upstream `UnfoldNd` output as the
|
||||
reference, not FastVideo's reformatted equivalent.
|
||||
|
||||
2. **Add an E2E hash check** alongside latent-parity. The mp4 SHA was the
|
||||
first signal that something was wrong; if it had been part of the standard
|
||||
parity battery, the bug would have surfaced in Wave 1, not Wave 14. See
|
||||
`fastvideo/tests/ssim/test_magi_human_similarity.py` for the CI version.
|
||||
|
||||
3. **For any new model port that involves explicit tensor reshaping into
|
||||
tokens**, document the expected packing order (`(C pT pH pW)` vs
|
||||
`(pT pH pW C)`) at the call site and assert the layout matches the
|
||||
downstream consumer's expectation.
|
||||
|
||||
4. DiT parity for any model that does video token packing should include an
|
||||
E2E hash check, not just latent bit-exactness.
|
||||
@@ -0,0 +1,41 @@
|
||||
---
|
||||
date: 2026-05-22
|
||||
experiment: PR #1386 DreamVerse app CI backend tests
|
||||
category: infrastructure
|
||||
severity: important
|
||||
---
|
||||
|
||||
# DreamVerse App CI Streaming Imports Need GPU
|
||||
|
||||
## What Happened
|
||||
|
||||
DreamVerse app CI backend pytest collection imports FastVideo streaming surfaces.
|
||||
When those tests run in a CPU-only Modal environment, collection can fail before
|
||||
any app assertions run with Triton reporting:
|
||||
|
||||
```text
|
||||
RuntimeError: 0 active drivers
|
||||
```
|
||||
|
||||
## Root Cause
|
||||
|
||||
Some streaming import paths can import `fastvideo_kernel` at module import time.
|
||||
Triton then probes for an active GPU driver during pytest collection. A CPU-only
|
||||
Modal container has no active driver, so the failure appears as an import-time
|
||||
collection error rather than a DreamVerse app behavior failure.
|
||||
|
||||
## Fix / Workaround
|
||||
|
||||
For PR #1386, use a surgical CI fix: allocate a GPU to
|
||||
`run_dreamverse_app_tests`. Do not refactor core streaming/kernel imports just to
|
||||
unstick this app CI path.
|
||||
|
||||
Keep `build_kernel=False` for this job. The DreamVerse app backend test imports
|
||||
streaming surfaces but does not need to rebuild or exercise custom kernels.
|
||||
|
||||
## Prevention
|
||||
|
||||
When adding or modifying DreamVerse app CI jobs that import FastVideo streaming
|
||||
modules, make the GPU requirement explicit if the import graph may touch
|
||||
`fastvideo_kernel`. Prefer small CI resource fixes for app test collection issues
|
||||
unless the product code genuinely requires lazy import cleanup.
|
||||
@@ -0,0 +1,66 @@
|
||||
---
|
||||
date: 2026-09-29
|
||||
experiment: Wan-VACE multi-GPU inference on shared Slurm nodes
|
||||
category: infrastructure
|
||||
severity: important
|
||||
---
|
||||
|
||||
# Spawn workers fail with ENOENT in `SemLock._rebuild`
|
||||
|
||||
## What Happened
|
||||
|
||||
On Slurm nodes, standard `VideoGenerator` runs with the `mp` executor sometimes
|
||||
failed during worker startup. A spawn child raised `FileNotFoundError` from
|
||||
`multiprocessing.synchronize.SemLock._rebuild` while unpickling its arguments,
|
||||
even though the parent still held its Queue objects. Failures showed up on
|
||||
several nodes and looked intermittent.
|
||||
|
||||
## Root Cause
|
||||
|
||||
The cluster's Slurm epilog `80-epilog-cleanup-shm-tmp.sh` runs
|
||||
`find /dev/shm -maxdepth 2 -user "$SLURM_JOB_USER" -delete` at the end of
|
||||
**every** job. When one of a user's jobs ends, the epilog deletes that user's
|
||||
`/dev/shm` files on the node, including those that the user's *other* running
|
||||
jobs are still using. Python's POSIX named semaphores (`/dev/shm/sem.mp-*`) are
|
||||
among them. If the deletion lands between queue creation and a spawn child
|
||||
unpickling it, the child fails with ENOENT.
|
||||
|
||||
Evidence:
|
||||
|
||||
- **Timing.** Every recorded deletion burst during a probe job matched, to the
|
||||
second, the end of another job by the same user on the same node
|
||||
(`sacct -u $USER -N <node>` end times).
|
||||
- **Controlled reproduction.**
|
||||
- Job A held a spawn `Lock` semaphore and a marker file in `/dev/shm`.
|
||||
- Job B, on the same node, started and then ended.
|
||||
- Job B's start deleted nothing.
|
||||
- Within 1 s of job B's end, both of job A's objects were gone, and job A's
|
||||
finalizer then hit the same ENOENT.
|
||||
- **Still unexplained.** One historical failure has no same-user job
|
||||
end in its window, so another deleter cannot be ruled out for it. logind
|
||||
`RemoveIPC` was the earlier hypothesis; it is not needed to explain the other
|
||||
cases.
|
||||
|
||||
## Fix / Workaround
|
||||
|
||||
- **FastVideo hardening.** Standard inference no longer creates the two
|
||||
streaming queues it never uses. `FastVideoArgs.enable_streaming_ipc_queues`
|
||||
defaults to `False`, and `StreamingVideoGenerator` sets it to `True`. This
|
||||
removes the exposure for standard inference only.
|
||||
- **Still exposed.** Streaming queues, NCCL shared-memory segments, and
|
||||
DataLoader shared memory on the same node can still be deleted by the epilog.
|
||||
- **Cluster fix (administrators).** Clean `/dev/shm` only when the user has no
|
||||
other job on the node, or give each job a private `/dev/shm` with
|
||||
`JobContainerType=job_container/tmpfs`.
|
||||
- **User workaround.** Don't co-locate your own jobs on one node
|
||||
(`--exclusive=user`), or avoid ending short jobs next to long-running ones.
|
||||
|
||||
## Prevention
|
||||
|
||||
- Before blaming an application for `/dev/shm` ENOENT on Slurm, read the
|
||||
node-local prolog/epilog hooks (`/cm/local/apps/slurm/var/{prologs,epilogs}`).
|
||||
Then correlate the failure window with `sacct -u $USER -N <node>` job end
|
||||
times.
|
||||
- Do not pass IPC primitives to spawn workers unless the worker needs them.
|
||||
- `fastvideo/tests/worker/test_multiproc_executor.py` checks that standard
|
||||
workers survive semaphore removal during spawn.
|
||||
@@ -0,0 +1,48 @@
|
||||
# Lessons Learned Database
|
||||
|
||||
This directory stores documented mistakes, unexpected behaviors, and their fixes.
|
||||
Each lesson is a permanent record that helps agents and humans avoid repeating
|
||||
past errors.
|
||||
|
||||
## When to Create a Lesson
|
||||
|
||||
- An experiment failed for a non-obvious reason.
|
||||
- A configuration or hyperparameter choice led to wasted compute.
|
||||
- A porting, data, or infrastructure issue was discovered and resolved.
|
||||
- A workaround was needed for a known framework/library bug.
|
||||
|
||||
## File Naming
|
||||
|
||||
`<YYYY-MM-DD>_<short-slug>.md` — e.g., `2026-03-02_lr-too-high-for-lora.md`
|
||||
|
||||
## Template
|
||||
|
||||
```markdown
|
||||
---
|
||||
date: <ISO-8601>
|
||||
experiment: <reference to experiment_journal.md entry, if applicable>
|
||||
category: hyperparameter | data | infrastructure | evaluation | porting | other
|
||||
severity: critical | important | minor
|
||||
---
|
||||
|
||||
# <Short Descriptive Title>
|
||||
|
||||
## What Happened
|
||||
<Description of the problem and its symptoms.>
|
||||
|
||||
## Root Cause
|
||||
<Analysis of why it happened.>
|
||||
|
||||
## Fix / Workaround
|
||||
<What resolved the issue.>
|
||||
|
||||
## Prevention
|
||||
<How to avoid this in the future — updated skills, SOPs, or checks.>
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
- Before starting a task, **search this directory** for relevant lessons.
|
||||
- After completing or failing a task, **check if a new lesson should be created**.
|
||||
- Periodically review lessons for **patterns** — recurring themes may warrant
|
||||
a new skill, SOP, or codebase fix.
|
||||
Executable
+96
@@ -0,0 +1,96 @@
|
||||
#!/usr/bin/env bash
|
||||
# Sync .agents/skills/ into .claude/skills/ via per-skill symlinks.
|
||||
#
|
||||
# Why: Claude Code only scans .claude/skills/ and ~/.claude/skills/ for
|
||||
# user-invocable skills (no skillsPath config exists — see
|
||||
# https://code.claude.com/docs/en/skills.md). This repo's skills live
|
||||
# in .agents/skills/ so they travel with the repo and stay under git.
|
||||
# Run this once after cloning (or after adding/removing a skill) to
|
||||
# expose them to Claude Code without maintaining a parallel tree.
|
||||
#
|
||||
# Usage:
|
||||
# .agents/scripts/sync-skills.sh
|
||||
#
|
||||
# Idempotent and safe to re-run. Prunes stale symlinks whose source
|
||||
# has been removed from .agents/skills/. Leaves hand-written
|
||||
# .claude/skills/<name>/ directories untouched (only symlinks are
|
||||
# managed).
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(git -C "$(dirname "$0")" rev-parse --show-toplevel)"
|
||||
SRC_DIR="$REPO_ROOT/.agents/skills"
|
||||
DST_DIR="$REPO_ROOT/.claude/skills"
|
||||
|
||||
if [[ ! -d "$SRC_DIR" ]]; then
|
||||
echo "Error: $SRC_DIR does not exist." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$DST_DIR"
|
||||
|
||||
linked=0
|
||||
unchanged=0
|
||||
skipped=0
|
||||
pruned=0
|
||||
|
||||
link_skill() {
|
||||
local name="$1"
|
||||
local src="$SRC_DIR/$name"
|
||||
local dst="$DST_DIR/$name"
|
||||
# Relative target keeps symlinks portable across clones.
|
||||
local rel="../../.agents/skills/$name"
|
||||
|
||||
if [[ -L "$dst" ]]; then
|
||||
if [[ "$(readlink "$dst")" == "$rel" ]]; then
|
||||
unchanged=$((unchanged + 1))
|
||||
return
|
||||
fi
|
||||
rm "$dst"
|
||||
elif [[ -e "$dst" ]]; then
|
||||
echo "Skipped (not a symlink): .claude/skills/$name" >&2
|
||||
skipped=$((skipped + 1))
|
||||
return
|
||||
fi
|
||||
|
||||
ln -s "$rel" "$dst"
|
||||
echo "Linked: .claude/skills/$name -> $rel"
|
||||
linked=$((linked + 1))
|
||||
}
|
||||
|
||||
prune_stale() {
|
||||
local link="$1"
|
||||
local target
|
||||
target="$(readlink "$link")"
|
||||
case "$target" in
|
||||
../../.agents/skills/*) ;;
|
||||
*) return ;;
|
||||
esac
|
||||
local name="${target##*/}"
|
||||
if [[ ! -d "$SRC_DIR/$name" ]]; then
|
||||
rm "$link"
|
||||
echo "Pruned stale: .claude/skills/$(basename "$link")"
|
||||
pruned=$((pruned + 1))
|
||||
fi
|
||||
}
|
||||
|
||||
for src in "$SRC_DIR"/*/; do
|
||||
[[ -d "$src" ]] || continue
|
||||
name="$(basename "$src")"
|
||||
# Only treat directories that actually contain a SKILL.md as skills.
|
||||
[[ -f "$src/SKILL.md" ]] || continue
|
||||
link_skill "$name"
|
||||
done
|
||||
|
||||
shopt -s nullglob
|
||||
for link in "$DST_DIR"/*; do
|
||||
[[ -L "$link" ]] || continue
|
||||
prune_stale "$link"
|
||||
done
|
||||
shopt -u nullglob
|
||||
|
||||
printf "\nSummary: %d linked, %d unchanged, %d pruned" "$linked" "$unchanged" "$pruned"
|
||||
if [[ "$skipped" -gt 0 ]]; then
|
||||
printf ", %d skipped (non-symlink collision)" "$skipped"
|
||||
fi
|
||||
printf "\n"
|
||||
@@ -0,0 +1,55 @@
|
||||
---
|
||||
name: <skill-name>
|
||||
description: <one-line description — Codex uses this for implicit invocation matching>
|
||||
---
|
||||
|
||||
# <Skill Name>
|
||||
|
||||
## Purpose
|
||||
<Why this skill exists and when to use it.>
|
||||
|
||||
## Prerequisites
|
||||
- <What must be true before using this skill>
|
||||
|
||||
## Inputs
|
||||
| Parameter | Required | Description |
|
||||
|-----------|----------|-------------|
|
||||
| `param1` | Yes | ... |
|
||||
|
||||
## Steps
|
||||
|
||||
1. **Step 1 title**
|
||||
- Detail...
|
||||
|
||||
2. **Step 2 title**
|
||||
- Detail...
|
||||
|
||||
## Outputs
|
||||
- <What this skill produces>
|
||||
|
||||
## Example Usage
|
||||
|
||||
```
|
||||
<Example invocation or prompt snippet>
|
||||
```
|
||||
|
||||
## References
|
||||
- <Links to relevant files in the codebase>
|
||||
|
||||
---
|
||||
|
||||
## Folder Structure
|
||||
|
||||
Each skill lives in its own directory under `.agents/skills/`:
|
||||
|
||||
```
|
||||
.agents/skills/<skill-name>/
|
||||
├── SKILL.md # Required: instructions + metadata (this file)
|
||||
├── scripts/ # Optional: executable helper scripts
|
||||
├── references/ # Optional: documentation, papers
|
||||
└── assets/ # Optional: templates, resources
|
||||
```
|
||||
|
||||
Skill discovery is directory-based; no hand-maintained registry entry is
|
||||
required. Run `.agents/scripts/sync-skills.sh` if a local Claude Code checkout
|
||||
needs refreshed `.claude/skills/` symlinks.
|
||||
@@ -0,0 +1,99 @@
|
||||
---
|
||||
name: ci-runner
|
||||
description: Work on FastVideo's Slurm-only, change-aware GPU CI lanes, static Buildkite graph, trusted ci-runner policy, lane scripts, and GB200 validation.
|
||||
---
|
||||
|
||||
# Slinky Slurm CI lanes
|
||||
|
||||
FastVideo's `ci-runner` Buildkite queue is the control plane for all active
|
||||
GPU CI. A private host-owned dispatcher leases GPUs from the Slinky Slurm tray
|
||||
and runs the immutable PR SHA inside an isolated Enroot container. Buildkite
|
||||
pipeline upload and Slurm submission occur on the login plane; every test
|
||||
payload executes on Slurm compute.
|
||||
|
||||
The files under `fastvideo/tests/modal/` and `.buildkite/scripts/pr_test.sh`
|
||||
are dormant rollback code. Never add an active Buildkite or slash-command
|
||||
route to them. `pr_test.sh` must continue to reject Buildkite invocations.
|
||||
|
||||
The private operator bundle is deliberately outside this repository because
|
||||
it contains site paths and credentials. See
|
||||
`docs/contributing/ci_architecture.md`; this skill covers the repository half
|
||||
and the coordination contract with that bundle.
|
||||
|
||||
## Invariants
|
||||
|
||||
- `.buildkite/pipeline.yml` contains exactly one static step for every active
|
||||
GPU lane. Each step pins a unique key and label, a 90-minute timeout, the
|
||||
trusted `/opt/fastvideo-ci-runner/run-ci` command (`run-unit` is the one
|
||||
compatibility wrapper), step-level internal `TEST_TYPE`, and
|
||||
`queue: "ci-runner"`.
|
||||
- Active CI contains no `pr_test.sh` command, Modal invocation, default queue,
|
||||
Buildkite plugin, `soft_fail`, or job-controlled artifact glob.
|
||||
- The six Fastcheck lanes use `:microscope:` labels. Full-Suite-only lanes use
|
||||
`:test_tube:` or `:bar_chart:` so direct reruns update the right aggregate.
|
||||
- SSIM and vanilla training request all four GPUs. Keep both in the
|
||||
`fastvideo/slinky/whole-tray` Buildkite concurrency group with a limit of one
|
||||
so the second job does not consume an agent or command timeout while waiting
|
||||
for the same tray.
|
||||
- `/test full` schedules all twenty lanes. `/merge`, `ready`, and new pushes to
|
||||
ready PRs use the trusted base-branch planner in
|
||||
`.github/scripts/plan_merge_ci.py`: automatic Fastcheck remains the universal
|
||||
six-lane baseline, and the merge build adds only path-relevant integration
|
||||
lanes. Unknown source/build paths fail closed to all fourteen additive lanes.
|
||||
The trusted uploader still normalizes and validates the complete static graph
|
||||
before Buildkite evaluates its plan conditions.
|
||||
- Focused merge builds may pass allowlisted golden-gate and SSIM test basenames.
|
||||
The private host validates the lane plan and basenames before staging them,
|
||||
and the in-container scripts validate them again. Direct `/test ssim`,
|
||||
explicit `/test full`, and the weekly main-branch schedule run the complete
|
||||
SSIM matrix.
|
||||
- The trusted uploader serves exactly three entry pipelines:
|
||||
`pr-fastcheck` for automatic PR builds, `ci` for slash-command/ready-label
|
||||
API builds, and `fastvideo-performance-lane` for the weekly schedule. Keep
|
||||
incoming GitHub webhook processing disabled on `ci` so it cannot duplicate
|
||||
`pr-fastcheck` on every PR update.
|
||||
- Test payloads live in `.buildkite/scripts/unit_test.sh` or executable
|
||||
`.buildkite/scripts/lanes/<lane>.sh`. Backend policy (GPU count, extras,
|
||||
secrets, kernel build, artifacts) stays in the agent-owned lane table.
|
||||
- Tests must preserve an inherited `MASTER_PORT`. Packed containers share the
|
||||
tray network namespace, so the private runner assigns a distinct port range
|
||||
per GPU lease and the SSIM scheduler assigns task offsets within its range.
|
||||
- The ARM64 runner image includes the pinned FA4 CuTe overlay validated on
|
||||
GB200. Keep SSIM at `FASTVIDEO_FA4=1` because its references were seeded with
|
||||
FA4; keep lanes with FA2 baselines at `FASTVIDEO_FA4=0`. A runner image change
|
||||
must revalidate both the FA4 import and an actual GB200 forward kernel.
|
||||
- `fastvideo/tests/ssim/ci_runner.py` is the active four-GPU SSIM scheduler.
|
||||
New SSIM files are discovered through `REQUIRED_GPUS` and
|
||||
`*_MODEL_TO_PARAMS`; do not wire them through the dormant Modal scheduler.
|
||||
- The host policy fail-closes unknown tuples. A repository-side lane change is
|
||||
inert until the operator updates the private lane table and uploader policy
|
||||
in the same rollout.
|
||||
|
||||
## Adding or changing a lane
|
||||
|
||||
1. Read the closest `AGENTS.md` and the domain-specific testing guide.
|
||||
2. Add or update the executable lane payload under `.buildkite/scripts/`.
|
||||
Keep it deterministic and free of host-specific paths or credential fetches.
|
||||
3. Add the static pipeline step and canonical `/test <name>` mapping. Keep the
|
||||
`<name>-ci` alias only when compatibility requires it.
|
||||
4. Add its source/test path ownership to `.github/scripts/plan_merge_ci.py`.
|
||||
Prefer the narrowest correctness-preserving lane set; leave unknown paths
|
||||
fail-closed. Extend `fastvideo/tests/contract/test_ci_test_collection.py`,
|
||||
`test_merge_ci_plan.py`, and focused CPU-only scheduler/policy tests.
|
||||
5. Coordinate the private lane row: GPU count (1-4), wall time, script, scope
|
||||
pairs, step key, command, HF cache/token, tracking mode, extras, attention
|
||||
backend policy, kernel policy, and artifact relay. Active training lanes
|
||||
keep W&B offline and do not stage a W&B credential.
|
||||
6. Update the trusted pipeline-uploader schema. A mismatch must reject the
|
||||
pipeline rather than silently skip a lane.
|
||||
7. Run `pre-commit run --files <changed paths>`, the planner's representative
|
||||
diff matrix, contract tests, private driver tests, and a real GB200 canary.
|
||||
Multi-GPU, hardware-reference, training, performance, and SSIM changes need
|
||||
their own target-hardware evidence.
|
||||
|
||||
## Rollback
|
||||
|
||||
Rollback the Slurm routing/configuration change or pause the `ci-runner` queue.
|
||||
Do not silently reactivate Modal. A manual Modal experiment requires the
|
||||
explicit local opt-in documented in `ci_architecture.md`; returning it to
|
||||
production CI needs a separate reviewed decision.
|
||||
@@ -0,0 +1,196 @@
|
||||
---
|
||||
name: dreamverse-deploy
|
||||
description: Use when redeploying the migrated Dreamverse app backend and frontend on a chosen local GPU; tears down existing ports, launches services, and waits for readiness checks.
|
||||
---
|
||||
|
||||
# dreamverse-deploy — redeploy migrated Dreamverse on a chosen GPU
|
||||
|
||||
**Scope:** project (lives in this repo at `.agents/skills/dreamverse-deploy/`)
|
||||
|
||||
**When to use:** you want to (re)launch the migrated `apps/dreamverse/` backend
|
||||
and frontend on this dev node, pinned to a specific physical GPU. Tears down
|
||||
any existing deploy on the same ports first, then boots fresh and waits for
|
||||
both `/readyz` and the FE root to return 200.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Working tree containing `apps/dreamverse/`
|
||||
- `dreamverse-server` installed from this checkout; if missing, run
|
||||
`uv pip install -e ".[dreamverse]"`
|
||||
- Local conda env at `~/miniconda3/envs/fv-main/` with `flashinfer-python`,
|
||||
`cerebras-cloud-sdk`, `openai` installed (override the default path with
|
||||
`DREAMVERSE_PYTHON=/path/to/python`)
|
||||
- `~/.env` exporting `CEREBRAS_API_KEY`, `GROQ_API_KEY`, etc.
|
||||
- npm available in `$PATH` (or set `NPM=/path/to/npm`)
|
||||
- `gcc-13` + `g++-13` at `/usr/bin/` (workaround for nvcc gcc-15 rejection)
|
||||
- **Recommended:** native ffmpeg at `$HOME/opt/ffmpeg-native/bin/ffmpeg`, built
|
||||
via `bash apps/dreamverse/scripts/install_native_ffmpeg.sh`. The deploy
|
||||
detects that binary directly and exports it for the backend. The installer's
|
||||
generated `apps/dreamverse/scripts/ffmpeg-env.sh` is for manual launches.
|
||||
When the binary is missing, the deploy falls back to system ffmpeg with a
|
||||
warning. Set
|
||||
`DREAMVERSE_REQUIRE_NATIVE_FFMPEG=true` to make the missing binary a hard
|
||||
failure.
|
||||
|
||||
If any required prereq is missing, the script fails fast with a clear message.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
# Deploy on GPU 4 with the current web port. The legacy helper default remains
|
||||
# 5274, so pass 5299 explicitly. Torch compile and warmup are both off.
|
||||
./.agents/skills/dreamverse-deploy/scripts/dreamverse-deploy.sh 4 8009 5299
|
||||
|
||||
# Deploy on GPU 6 with custom ports
|
||||
./.agents/skills/dreamverse-deploy/scripts/dreamverse-deploy.sh 6 8089 5275
|
||||
|
||||
# Deploy on GPU 0 with warmup enabled
|
||||
./.agents/skills/dreamverse-deploy/scripts/dreamverse-deploy.sh --warmup 0 8009 5299
|
||||
|
||||
# Deploy with torch.compile enabled (max-autotune; first segment ~3-4min,
|
||||
# subsequent segments save ~3s — only worth it for benchmarking)
|
||||
./.agents/skills/dreamverse-deploy/scripts/dreamverse-deploy.sh --torch-compile 4 8009 5299
|
||||
|
||||
# Deploy with both warmup AND torch.compile enabled
|
||||
./.agents/skills/dreamverse-deploy/scripts/dreamverse-deploy.sh --warmup --torch-compile 4 8009 5299
|
||||
|
||||
# Flags can appear before, between, or after positional args
|
||||
./.agents/skills/dreamverse-deploy/scripts/dreamverse-deploy.sh 4 8089 5275 --warmup
|
||||
```
|
||||
|
||||
### Arguments
|
||||
|
||||
| Position | Name | Default | Notes |
|
||||
|---|---|---|---|
|
||||
| 1 | `GPU` | (required) | Physical GPU index, e.g. `4` |
|
||||
| 2 | `BACKEND_PORT` | `8009` | TCP port for the FastAPI server |
|
||||
| 3 | `FRONTEND_PORT` | `5274` | TCP port for the Next.js dev server |
|
||||
|
||||
### Flags
|
||||
|
||||
| Flag | Default | Notes |
|
||||
|---|---|---|
|
||||
| `--warmup` / `--no-warmup` | off | Run GPU warmup at boot (~minutes). Overrides `DREAMVERSE_WARMUP` |
|
||||
| `--torch-compile` / `--no-torch-compile` | off | Enable max-autotune `torch.compile`. First segment ~3-4min when on, ~45s when off. Overrides `DREAMVERSE_TORCH_COMPILE` |
|
||||
| `--nvenc` / `--no-nvenc` | off | Use `h264_nvenc` hardware encoder instead of `libx264` software. Eliminates ~1100ms/segment of CPU encoding cost (raises realtime ratio from ~0.78x → ≥1.0x, eliminating inter-segment buffer-drain stutter). Requires native ffmpeg built with `--enable-nvenc` (the install script's default since the NVENC update). Hard-fails up-front if the binary is missing or lacks NVENC. Overrides `DREAMVERSE_NVENC` |
|
||||
| `-h` / `--help` | — | Show usage |
|
||||
|
||||
Flags can appear in any position relative to the positional args. Explicit flag values always win over env-var defaults.
|
||||
|
||||
### Environment variables (used when no flag is given)
|
||||
|
||||
| Var | Default | Purpose |
|
||||
|---|---|---|
|
||||
| `DREAMVERSE_WARMUP` | `false` | Same as `--warmup`/`--no-warmup`. Flag takes precedence |
|
||||
| `DREAMVERSE_TORCH_COMPILE` | `false` | Same as `--torch-compile`/`--no-torch-compile`. Flag takes precedence |
|
||||
| `DREAMVERSE_NVENC` | `false` | Same as `--nvenc`/`--no-nvenc`. Flag takes precedence |
|
||||
| `DREAMVERSE_PYTHON` | `~/miniconda3/envs/fv-main/bin/python` | Conda environment used for the flashinfer prerequisite probe; `dreamverse-server` itself is resolved from `PATH` |
|
||||
| `DREAMVERSE_REPO_ROOT` | git rev-parse | Repo root override |
|
||||
| `DREAMVERSE_LOG_DIR` | `/tmp/opencode/dreamverse-deploy` | Directory for the per-GPU backend and per-port frontend logs |
|
||||
| `DREAMVERSE_REQUIRE_NATIVE_FFMPEG` | `false` | If `true`, fail when `$HOME/opt/ffmpeg-native/bin/ffmpeg` is absent |
|
||||
|
||||
## What it does
|
||||
|
||||
1. Validates prereqs.
|
||||
2. Kills any process on the target backend/frontend ports + waits for the
|
||||
target GPU to release memory (allows up to 30s for cleanup).
|
||||
3. Sources `~/.env`.
|
||||
4. Exports the env recipe required for boot:
|
||||
- `CUDA_VISIBLE_DEVICES=<gpu>`
|
||||
- `FASTVIDEO_ENABLE_DEVTOOLS=1`
|
||||
- `FASTVIDEO_ENABLE_STARTUP_WARMUP=<DREAMVERSE_WARMUP>`
|
||||
- `FASTVIDEO_GPU_COUNT=1`
|
||||
- `ENABLE_TORCH_COMPILE=<0|1 derived from DREAMVERSE_TORCH_COMPILE>`
|
||||
- `CC=/usr/bin/gcc-13 CXX=/usr/bin/g++-13 CUDAHOSTCXX=/usr/bin/g++-13`
|
||||
- `NVCC_PREPEND_FLAGS="-ccbin /usr/bin/gcc-13 -allow-unsupported-compiler"`
|
||||
- `FASTVIDEO_FFMPEG_BIN=$HOME/opt/ffmpeg-native/bin/ffmpeg` +
|
||||
`FASTVIDEO_VIDEO_CODEC=<libx264|h264_nvenc>` (when the native binary exists)
|
||||
5. Launches the installed `dreamverse-server` console command in a detached
|
||||
`setsid` session and captures its PID.
|
||||
6. Polls `/readyz` until 200. The budget is 5 minutes by default, 8 minutes
|
||||
with one startup optimization enabled, and 15 minutes with both warmup and
|
||||
`torch.compile` enabled.
|
||||
7. Launches the devtools frontend through npm in a detached session and
|
||||
captures its PID.
|
||||
8. Polls FE `/` until 200 (max 60s).
|
||||
9. Prints URLs, PIDs, and log paths.
|
||||
|
||||
## What it does NOT do
|
||||
|
||||
- Does not modify `~/.env` or the FastVideo `.venv`.
|
||||
- Does not push code or commit anything.
|
||||
- Does not run Playwright. Use the e2e wrapper separately:
|
||||
```bash
|
||||
cd apps/dreamverse/web
|
||||
PLAYWRIGHT_SKIP_WEBSERVER=1 BACKEND_HOST=127.0.0.1 BACKEND_PORT=8009 \
|
||||
PLAYWRIGHT_BASE_URL=http://127.0.0.1:5299 \
|
||||
NEXT_PUBLIC_INCLUDE_DEVTOOLS=1 \
|
||||
npm exec -- playwright test
|
||||
```
|
||||
The standard suite runs by default; the long-running two-segment
|
||||
audio-continuation spec is gated behind
|
||||
`PLAYWRIGHT_LONG_RUNNING=1` (see below).
|
||||
|
||||
## Long-running e2e (paired with `--warmup --torch-compile`)
|
||||
|
||||
[`apps/dreamverse/web/e2e/long-running-segments.spec.ts`](../../../apps/dreamverse/web/e2e/long-running-segments.spec.ts)
|
||||
drives a real two-segment session through the FE, captures every WS
|
||||
frame, and asserts segments 1 AND 2 both reach `media_segment_complete`
|
||||
with at least one binary fMP4 chunk per segment. It guards against the
|
||||
BrokenPipe regression previously caused by dropped LTX-2 audio continuation
|
||||
kwargs.
|
||||
Skipped by default. Enable with:
|
||||
|
||||
```bash
|
||||
./.agents/skills/dreamverse-deploy/scripts/dreamverse-deploy.sh \
|
||||
--warmup --torch-compile 4 8009 5299
|
||||
|
||||
cd apps/dreamverse/web
|
||||
PLAYWRIGHT_SKIP_WEBSERVER=1 \
|
||||
BACKEND_HOST=127.0.0.1 \
|
||||
BACKEND_PORT=8009 \
|
||||
PLAYWRIGHT_BASE_URL=http://127.0.0.1:5299 \
|
||||
NEXT_PUBLIC_INCLUDE_DEVTOOLS=1 \
|
||||
PLAYWRIGHT_LONG_RUNNING=1 \
|
||||
npm exec -- playwright test e2e/long-running-segments.spec.ts
|
||||
```
|
||||
|
||||
Expected runtime: ~7-9 minutes on a B200 (torch.compile max-autotune
|
||||
warm-up dominates the cold start; per-test timeout is 900s). The spec
|
||||
hard-fails on any WS `error`/`step_error` frame so the BrokenPipe
|
||||
regression surfaces with the actual ffmpeg/audio diagnostics rather
|
||||
than an opaque "test timed out".
|
||||
|
||||
## Teardown
|
||||
|
||||
Stop both services without redeploying:
|
||||
|
||||
```bash
|
||||
# Stop services on default ports (port-pattern based)
|
||||
./.agents/skills/dreamverse-deploy/scripts/dreamverse-deploy.sh --stop
|
||||
|
||||
# Stop AND nuke any process holding GPU N
|
||||
./.agents/skills/dreamverse-deploy/scripts/dreamverse-deploy.sh --stop 4
|
||||
```
|
||||
|
||||
The redeploy path (`<GPU>` mode) automatically nukes any process holding the
|
||||
target GPU before launching — including orphan `multiproc_executor` worker
|
||||
subprocesses left over from a parent backend that was killed without grace.
|
||||
This was the failure mode of an earlier naive port-only kill: parent dies,
|
||||
children survive, GPU stays full, next deploy OOMs.
|
||||
|
||||
## Notes
|
||||
|
||||
- The installed `dreamverse-server` console command enters
|
||||
`apps/dreamverse/dreamverse/server_entry.py`, which loads the current
|
||||
Dreamverse runtime from `apps/dreamverse/dreamverse/`.
|
||||
- The B200 / sm_100a NVCC flags are mandatory on this dev node because the
|
||||
conda toolchain ships gcc-15, which nvcc rejects. The script requires the
|
||||
configured gcc-13 and g++-13 binaries during preflight.
|
||||
|
||||
## Deployment boundary
|
||||
|
||||
This skill is for a local checkout on a directly attached GPU. For a container
|
||||
image, use `apps/dreamverse/docker/README.md`. For Modal, follow
|
||||
`apps/dreamverse/scripts/modal/README.md`; do not adapt this process-killing
|
||||
workflow to a remote deployment.
|
||||
@@ -0,0 +1,460 @@
|
||||
#!/usr/bin/env bash
|
||||
# See ../SKILL.md for full usage.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
is_pid_alive() {
|
||||
kill -0 "$1" 2>/dev/null
|
||||
}
|
||||
|
||||
terminate_pid() {
|
||||
local pid="$1"
|
||||
local label="${2:-pid=${pid}}"
|
||||
|
||||
[[ -n "${pid}" ]] && [[ "${pid}" != "$$" ]] || return 0
|
||||
is_pid_alive "${pid}" || return 0
|
||||
|
||||
kill "${pid}" 2>/dev/null || true
|
||||
for _ in $(seq 1 10); do
|
||||
is_pid_alive "${pid}" || return 0
|
||||
sleep 0.5
|
||||
done
|
||||
|
||||
if is_pid_alive "${pid}"; then
|
||||
kill -9 "${pid}" 2>/dev/null && echo " force-killed ${label}" || true
|
||||
fi
|
||||
}
|
||||
|
||||
terminate_pattern() {
|
||||
local pattern="$1"
|
||||
local pid
|
||||
|
||||
if ! command -v pgrep >/dev/null 2>&1; then
|
||||
pkill -TERM -f "${pattern}" 2>/dev/null || true
|
||||
sleep 2
|
||||
pkill -KILL -f "${pattern}" 2>/dev/null || true
|
||||
return 0
|
||||
fi
|
||||
|
||||
for pid in $(pgrep -f -- "${pattern}" 2>/dev/null || true); do
|
||||
terminate_pid "${pid}" "pattern='${pattern}' pid=${pid}"
|
||||
done
|
||||
}
|
||||
|
||||
list_port_pids() {
|
||||
local port="$1"
|
||||
|
||||
if command -v lsof >/dev/null 2>&1; then
|
||||
lsof -t -iTCP:"${port}" -sTCP:LISTEN 2>/dev/null || true
|
||||
return 0
|
||||
fi
|
||||
|
||||
ss -tlnp 2>/dev/null | awk -v port=":${port}" '
|
||||
$0 ~ port {
|
||||
while (match($0, /pid=[0-9]+/)) {
|
||||
print substr($0, RSTART + 4, RLENGTH - 4)
|
||||
$0 = substr($0, RSTART + RLENGTH)
|
||||
}
|
||||
}
|
||||
' || true
|
||||
}
|
||||
|
||||
if [[ "${1:-}" == "--stop" ]]; then
|
||||
for pat in 'apps/dreamverse/dreamverse/main.py' 'dreamverse-server --host 0.0.0.0 --port' 'next dev --port' 'next-server (v'; do
|
||||
terminate_pattern "${pat}"
|
||||
done
|
||||
if [[ -n "${2:-}" ]] && [[ "${2}" =~ ^[0-9]+$ ]]; then
|
||||
gpu_uuid="$(nvidia-smi --query-gpu=index,uuid --format=csv,noheader 2>/dev/null | awk -F', ' -v g="${2}" '$1==g {print $2}')"
|
||||
if [[ -n "${gpu_uuid}" ]]; then
|
||||
for pid in $(nvidia-smi --query-compute-apps=pid,gpu_uuid --format=csv,noheader 2>/dev/null \
|
||||
| awk -F', ' -v u="${gpu_uuid}" '$2==u {print $1}'); do
|
||||
terminate_pid "${pid}" "GPU${2} pid=${pid}"
|
||||
done
|
||||
fi
|
||||
fi
|
||||
sleep 2
|
||||
echo "stopped: ports may take a few seconds to free"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Args
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
usage() {
|
||||
cat <<USAGE
|
||||
Usage: $(basename "$0") [FLAGS] <GPU> [BACKEND_PORT] [FRONTEND_PORT]
|
||||
$(basename "$0") --stop [GPU]
|
||||
|
||||
Positional:
|
||||
GPU Physical GPU index (required), e.g. 4
|
||||
BACKEND_PORT default 8009
|
||||
FRONTEND_PORT default 5274
|
||||
|
||||
Flags (override env vars when both set):
|
||||
--warmup / --no-warmup run GPU warmup at boot (default off)
|
||||
--torch-compile / --no-torch-compile
|
||||
enable max-autotune torch.compile
|
||||
(default off — first segment ~3-4min
|
||||
when on, ~45s when off)
|
||||
--nvenc / --no-nvenc use h264_nvenc hardware encoder (default
|
||||
off — uses libx264 software encoder).
|
||||
Requires native ffmpeg built with NVENC.
|
||||
-h, --help show this help
|
||||
|
||||
Env overrides:
|
||||
DREAMVERSE_WARMUP 'true'|'false' (default false)
|
||||
DREAMVERSE_TORCH_COMPILE 'true'|'false' (default false)
|
||||
DREAMVERSE_NVENC 'true'|'false' (default false)
|
||||
DREAMVERSE_REPO_ROOT default: \$(git rev-parse --show-toplevel)
|
||||
DREAMVERSE_LOG_DIR default: /tmp/opencode/dreamverse-deploy
|
||||
DREAMVERSE_REQUIRE_NATIVE_FFMPEG 'true'|'false' (default false)
|
||||
USAGE
|
||||
}
|
||||
|
||||
WARMUP_OVERRIDE=""
|
||||
TORCH_COMPILE_OVERRIDE=""
|
||||
NVENC_OVERRIDE=""
|
||||
POSITIONAL=()
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-h|--help) usage; exit 0 ;;
|
||||
--warmup) WARMUP_OVERRIDE=true; shift ;;
|
||||
--no-warmup) WARMUP_OVERRIDE=false; shift ;;
|
||||
--torch-compile) TORCH_COMPILE_OVERRIDE=true; shift ;;
|
||||
--no-torch-compile) TORCH_COMPILE_OVERRIDE=false; shift ;;
|
||||
--nvenc) NVENC_OVERRIDE=true; shift ;;
|
||||
--no-nvenc) NVENC_OVERRIDE=false; shift ;;
|
||||
--) shift; while [[ $# -gt 0 ]]; do POSITIONAL+=("$1"); shift; done ;;
|
||||
-*) echo "error: unknown flag '$1'" >&2; usage >&2; exit 2 ;;
|
||||
*) POSITIONAL+=("$1"); shift ;;
|
||||
esac
|
||||
done
|
||||
set -- "${POSITIONAL[@]+"${POSITIONAL[@]}"}"
|
||||
|
||||
if [[ $# -lt 1 ]]; then
|
||||
usage >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
GPU="${1}"
|
||||
BACKEND_PORT="${2:-8009}"
|
||||
FRONTEND_PORT="${3:-5274}"
|
||||
|
||||
if ! [[ "${GPU}" =~ ^[0-9]+$ ]]; then
|
||||
echo "error: GPU must be a non-negative integer (got '${GPU}')" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
WARMUP="${WARMUP_OVERRIDE:-${DREAMVERSE_WARMUP:-false}}"
|
||||
case "${WARMUP}" in
|
||||
true|false) ;;
|
||||
*) echo "error: warmup must be 'true' or 'false' (got '${WARMUP}')" >&2; exit 2 ;;
|
||||
esac
|
||||
|
||||
TORCH_COMPILE="${TORCH_COMPILE_OVERRIDE:-${DREAMVERSE_TORCH_COMPILE:-false}}"
|
||||
case "${TORCH_COMPILE}" in
|
||||
true|false) ;;
|
||||
*) echo "error: torch-compile must be 'true' or 'false' (got '${TORCH_COMPILE}')" >&2; exit 2 ;;
|
||||
esac
|
||||
TORCH_COMPILE_FLAG=$([[ "${TORCH_COMPILE}" == "true" ]] && echo 1 || echo 0)
|
||||
|
||||
NVENC="${NVENC_OVERRIDE:-${DREAMVERSE_NVENC:-false}}"
|
||||
case "${NVENC}" in
|
||||
true|false) ;;
|
||||
*) echo "error: nvenc must be 'true' or 'false' (got '${NVENC}')" >&2; exit 2 ;;
|
||||
esac
|
||||
|
||||
REPO_ROOT="${DREAMVERSE_REPO_ROOT:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
|
||||
LOG_DIR="${DREAMVERSE_LOG_DIR:-/tmp/opencode/dreamverse-deploy}"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Prereq checks
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
bail() { echo "error: $*" >&2; exit 3; }
|
||||
|
||||
[[ -d "${REPO_ROOT}/apps/dreamverse" ]] \
|
||||
|| bail "REPO_ROOT '${REPO_ROOT}' does not contain apps/dreamverse/. Are you on a migration branch?"
|
||||
DREAMVERSE_SERVER="$(command -v dreamverse-server 2>/dev/null || true)"
|
||||
[[ -n "${DREAMVERSE_SERVER}" ]] && [[ -x "${DREAMVERSE_SERVER}" ]] \
|
||||
|| bail "dreamverse-server not executable or not in PATH (run: uv pip install -e \".[dreamverse]\")"
|
||||
|
||||
CONDA_ENV_PYTHON="${DREAMVERSE_PYTHON:-${HOME}/miniconda3/envs/fv-main/bin/python}"
|
||||
[[ -x "${CONDA_ENV_PYTHON}" ]] \
|
||||
|| bail "conda env python missing at ${CONDA_ENV_PYTHON} (set DREAMVERSE_PYTHON to override)"
|
||||
"${CONDA_ENV_PYTHON}" -c 'import flashinfer' 2>/dev/null \
|
||||
|| bail "flashinfer-python not installed in ${CONDA_ENV_PYTHON} (run: ${CONDA_ENV_PYTHON} -m pip install flashinfer-python --no-build-isolation)"
|
||||
|
||||
NPM="${NPM:-npm}"
|
||||
NPM_REQUESTED="${NPM}"
|
||||
NPM="$(command -v "${NPM}" 2>/dev/null || true)"
|
||||
[[ -n "${NPM}" ]] && [[ -x "${NPM}" ]] || bail "npm not executable or not in PATH: ${NPM_REQUESTED} (set NPM to override)"
|
||||
|
||||
GCC13="$(command -v "${GCC13:-gcc-13}" 2>/dev/null || true)"
|
||||
GPP13="$(command -v "${GPP13:-g++-13}" 2>/dev/null || true)"
|
||||
[[ -n "${GCC13}" ]] && command -v "${GCC13}" >/dev/null 2>&1 \
|
||||
|| bail "gcc-13 not found or not executable (needed for nvcc workaround). Set GCC13 or install gcc-13 in PATH"
|
||||
[[ -n "${GPP13}" ]] && command -v "${GPP13}" >/dev/null 2>&1 \
|
||||
|| bail "g++-13 not found or not executable (needed for nvcc workaround). Set GPP13 or install g++-13 in PATH"
|
||||
|
||||
[[ -f "${HOME}/.env" ]] || echo "warn: ${HOME}/.env missing — provider API keys may be unset" >&2
|
||||
|
||||
NATIVE_FFMPEG_BIN="${HOME}/opt/ffmpeg-native/bin/ffmpeg"
|
||||
if [[ "${NVENC}" == "true" ]]; then
|
||||
NATIVE_VIDEO_CODEC=h264_nvenc
|
||||
else
|
||||
NATIVE_VIDEO_CODEC=libx264
|
||||
fi
|
||||
REQUIRE_NATIVE_FFMPEG="${DREAMVERSE_REQUIRE_NATIVE_FFMPEG:-false}"
|
||||
case "${REQUIRE_NATIVE_FFMPEG}" in
|
||||
true|false) ;;
|
||||
*) bail "DREAMVERSE_REQUIRE_NATIVE_FFMPEG must be 'true' or 'false' (got '${REQUIRE_NATIVE_FFMPEG}')" ;;
|
||||
esac
|
||||
if [[ -x "${NATIVE_FFMPEG_BIN}" ]]; then
|
||||
if [[ "${NVENC}" == "true" ]]; then
|
||||
encoder_list="$("${NATIVE_FFMPEG_BIN}" -hide_banner -encoders 2>/dev/null || true)"
|
||||
if [[ "${encoder_list}" != *h264_nvenc* ]]; then
|
||||
bail "--nvenc requested but ${NATIVE_FFMPEG_BIN} was not built with NVENC. Rebuild: bash apps/dreamverse/scripts/install_native_ffmpeg.sh (with ENABLE_NVENC=1, the default)"
|
||||
fi
|
||||
if ! "${NATIVE_FFMPEG_BIN}" -hide_banner -loglevel error -y \
|
||||
-f lavfi -i 'color=red:size=64x64:rate=24:duration=0.2' \
|
||||
-c:v h264_nvenc -f null - >/dev/null 2>&1; then
|
||||
bail "--nvenc requested but the GPU on this host has no NVENC silicon (probe failed: 'OpenEncodeSessionEx unsupported device'). Datacenter Blackwell (B200) and some H100 SKUs ship without NVENC; --nvenc only works on hosts with NVENC-capable GPUs (RTX 50-series, T4, A10, etc.)."
|
||||
fi
|
||||
fi
|
||||
echo " native ffmpeg: ${NATIVE_FFMPEG_BIN} (codec=${NATIVE_VIDEO_CODEC})"
|
||||
elif [[ "${REQUIRE_NATIVE_FFMPEG}" == "true" ]] || [[ "${NVENC}" == "true" ]]; then
|
||||
bail "${NATIVE_FFMPEG_BIN} missing (required by --nvenc or DREAMVERSE_REQUIRE_NATIVE_FFMPEG=true). Run: bash apps/dreamverse/scripts/install_native_ffmpeg.sh"
|
||||
else
|
||||
echo "warn: ${NATIVE_FFMPEG_BIN} missing — backend will fall back to system ffmpeg (\$(command -v ffmpeg))." >&2
|
||||
echo " Build native ffmpeg with: bash apps/dreamverse/scripts/install_native_ffmpeg.sh" >&2
|
||||
fi
|
||||
echo " python: ${CONDA_ENV_PYTHON}"
|
||||
|
||||
mkdir -p "${LOG_DIR}"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Teardown anything on target ports
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
echo "[1/8] killing any existing deploy on ports ${BACKEND_PORT}/${FRONTEND_PORT} and GPU ${GPU}..."
|
||||
|
||||
kill_port_pid() {
|
||||
local port="$1"
|
||||
local pid
|
||||
|
||||
for pid in $(list_port_pids "${port}"); do
|
||||
terminate_pid "${pid}" "port=${port} pid=${pid}"
|
||||
done
|
||||
}
|
||||
|
||||
for pat in "dreamverse-server --host 0.0.0.0 --port ${BACKEND_PORT}" "next dev --port ${FRONTEND_PORT}" "NEXT_PUBLIC_INCLUDE_DEVTOOLS=1 next dev --port ${FRONTEND_PORT}"; do
|
||||
terminate_pattern "${pat}"
|
||||
done
|
||||
kill_port_pid "${BACKEND_PORT}"
|
||||
kill_port_pid "${FRONTEND_PORT}"
|
||||
|
||||
gpu_uuid="$(nvidia-smi --query-gpu=index,uuid --format=csv,noheader 2>/dev/null | awk -F', ' -v g="${GPU}" '$1==g {print $2}')"
|
||||
if [[ -n "${gpu_uuid}" ]]; then
|
||||
for pid in $(nvidia-smi --query-compute-apps=pid,gpu_uuid --format=csv,noheader 2>/dev/null \
|
||||
| awk -F', ' -v u="${gpu_uuid}" '$2==u {print $1}'); do
|
||||
if [[ -n "${pid}" ]] && [[ "${pid}" != "$$" ]]; then
|
||||
cmd="$(ps -p "${pid}" -o comm= 2>/dev/null || true)"
|
||||
terminate_pid "${pid}" "GPU${GPU} pid=${pid} (${cmd:-?})"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
for i in $(seq 1 30); do
|
||||
free_be=true
|
||||
free_fe=true
|
||||
ss -tln 2>/dev/null | grep -qE ":${BACKEND_PORT}\b" && free_be=false
|
||||
ss -tln 2>/dev/null | grep -qE ":${FRONTEND_PORT}\b" && free_fe=false
|
||||
gpu_mem="$(nvidia-smi --query-gpu=memory.used --format=csv,noheader,nounits 2>/dev/null | sed -n "$((GPU + 1))p" || echo 99999)"
|
||||
if "${free_be}" && "${free_fe}" && [[ "${gpu_mem}" -lt 1000 ]]; then
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
gpu_mem="$(nvidia-smi --query-gpu=memory.used --format=csv,noheader,nounits 2>/dev/null | sed -n "$((GPU + 1))p" || echo 0)"
|
||||
echo " ports cleared; GPU${GPU} at ${gpu_mem} MiB"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Launch backend
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
echo "[2/8] launching backend on GPU ${GPU} port ${BACKEND_PORT} (warmup=${WARMUP} torch_compile=${TORCH_COMPILE} nvenc=${NVENC})..."
|
||||
|
||||
backend_log="${LOG_DIR}/backend-gpu${GPU}.log"
|
||||
: > "${backend_log}"
|
||||
|
||||
setsid bash -c "
|
||||
set -a
|
||||
if [[ -f \"${HOME}/.env\" ]]; then
|
||||
source \"${HOME}/.env\"
|
||||
fi
|
||||
set +a
|
||||
if [[ -x \"${NATIVE_FFMPEG_BIN}\" ]]; then
|
||||
export FASTVIDEO_FFMPEG_BIN=\"${NATIVE_FFMPEG_BIN}\"
|
||||
export FASTVIDEO_VIDEO_CODEC=\"${NATIVE_VIDEO_CODEC}\"
|
||||
fi
|
||||
export DREAMVERSE_PYTHON=\"${CONDA_ENV_PYTHON}\"
|
||||
export CUDA_VISIBLE_DEVICES=${GPU}
|
||||
export FASTVIDEO_ENABLE_DEVTOOLS=1
|
||||
export FASTVIDEO_ENABLE_STARTUP_WARMUP=${WARMUP}
|
||||
export FASTVIDEO_GPU_COUNT=1
|
||||
export ENABLE_TORCH_COMPILE=${TORCH_COMPILE_FLAG}
|
||||
export CC=${GCC13}
|
||||
export CXX=${GPP13}
|
||||
export CUDAHOSTCXX=${GPP13}
|
||||
export NVCC_PREPEND_FLAGS=\"-ccbin ${GCC13} -allow-unsupported-compiler\"
|
||||
cd \"${REPO_ROOT}\"
|
||||
exec \"${DREAMVERSE_SERVER}\" --host 0.0.0.0 --port ${BACKEND_PORT}
|
||||
" > "${backend_log}" 2>&1 < /dev/null &
|
||||
disown
|
||||
|
||||
# Wait briefly, then resolve actual python PID (the inner process, not the
|
||||
# wrapper bash).
|
||||
sleep 4
|
||||
backend_pid="$(pgrep -f "dreamverse-server --host 0.0.0.0 --port ${BACKEND_PORT}" | head -1 || true)"
|
||||
|
||||
if [[ -z "${backend_pid}" ]]; then
|
||||
echo "error: backend failed to spawn. Last 30 lines of log:" >&2
|
||||
tail -30 "${backend_log}" >&2
|
||||
exit 4
|
||||
fi
|
||||
|
||||
echo " backend pid=${backend_pid} log=${backend_log}"
|
||||
|
||||
# Poll /readyz. Deadline scales with warmup + torch.compile flags
|
||||
# because warmup runs two synthetic segments before /readyz=200, and
|
||||
# torch.compile max-autotune adds ~3-4min cold start to the first
|
||||
# segment. Empirical worst case (warmup=true, torch_compile=true):
|
||||
# ~7 min on B200; we budget 15 min for safety.
|
||||
if [[ "${WARMUP}" == "true" ]] && [[ "${TORCH_COMPILE}" == "true" ]]; then
|
||||
READYZ_BUDGET_SECONDS=900
|
||||
elif [[ "${WARMUP}" == "true" ]] || [[ "${TORCH_COMPILE}" == "true" ]]; then
|
||||
READYZ_BUDGET_SECONDS=480
|
||||
else
|
||||
READYZ_BUDGET_SECONDS=300
|
||||
fi
|
||||
READYZ_POLL_INTERVAL=6
|
||||
READYZ_MAX_ITERS=$(( READYZ_BUDGET_SECONDS / READYZ_POLL_INTERVAL ))
|
||||
|
||||
echo "[3/8] polling http://127.0.0.1:${BACKEND_PORT}/readyz (budget=${READYZ_BUDGET_SECONDS}s) ..."
|
||||
ready=0
|
||||
for i in $(seq 1 ${READYZ_MAX_ITERS}); do
|
||||
code="$(curl -s -o /dev/null -w '%{http_code}' --max-time 2 "http://127.0.0.1:${BACKEND_PORT}/readyz" 2>/dev/null || echo 000)"
|
||||
if [[ "${code}" == "200" ]]; then
|
||||
ready=1
|
||||
break
|
||||
fi
|
||||
if ! kill -0 "${backend_pid}" 2>/dev/null; then
|
||||
echo "error: backend pid ${backend_pid} died. Last 50 lines:" >&2
|
||||
tail -50 "${backend_log}" >&2
|
||||
exit 5
|
||||
fi
|
||||
sleep ${READYZ_POLL_INTERVAL}
|
||||
done
|
||||
|
||||
if [[ "${ready}" != "1" ]]; then
|
||||
echo "error: backend did not become /readyz=200 within ${READYZ_BUDGET_SECONDS}s. Last 50 lines:" >&2
|
||||
tail -50 "${backend_log}" >&2
|
||||
exit 5
|
||||
fi
|
||||
|
||||
echo "[4/8] backend /readyz OK"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Launch frontend
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
echo "[5/8] launching frontend on port ${FRONTEND_PORT}..."
|
||||
|
||||
frontend_log="${LOG_DIR}/frontend-port${FRONTEND_PORT}.log"
|
||||
: > "${frontend_log}"
|
||||
|
||||
# Resolve dev script: dev:devtools forces port 5274 + devtools env. If the
|
||||
# requested port differs, run `next dev --port` directly with devtools env.
|
||||
fe_cmd="run dev:devtools"
|
||||
if [[ "${FRONTEND_PORT}" != "5274" ]]; then
|
||||
fe_cmd="exec -- next dev --port ${FRONTEND_PORT}"
|
||||
fi
|
||||
|
||||
setsid bash -c "
|
||||
cd \"${REPO_ROOT}/apps/dreamverse/web\"
|
||||
export NEXT_PUBLIC_INCLUDE_DEVTOOLS=1
|
||||
export BACKEND_URL=http://127.0.0.1:${BACKEND_PORT}
|
||||
export BACKEND_HOST=127.0.0.1
|
||||
export BACKEND_PORT=${BACKEND_PORT}
|
||||
exec '${NPM}' ${fe_cmd}
|
||||
" > "${frontend_log}" 2>&1 < /dev/null &
|
||||
disown
|
||||
|
||||
sleep 4
|
||||
frontend_pid="$(pgrep -f "next dev --port ${FRONTEND_PORT}" | head -1 || true)"
|
||||
if [[ -z "${frontend_pid}" ]]; then
|
||||
echo "error: frontend failed to spawn. Last 30 lines:" >&2
|
||||
tail -30 "${frontend_log}" >&2
|
||||
exit 6
|
||||
fi
|
||||
|
||||
echo " frontend pid=${frontend_pid} log=${frontend_log}"
|
||||
|
||||
# Poll FE root
|
||||
echo "[6/8] polling http://127.0.0.1:${FRONTEND_PORT}/ ..."
|
||||
fe_ready=0
|
||||
for i in $(seq 1 30); do
|
||||
code="$(curl -s -o /dev/null -w '%{http_code}' --max-time 2 "http://127.0.0.1:${FRONTEND_PORT}/" 2>/dev/null || echo 000)"
|
||||
if [[ "${code}" == "200" ]]; then
|
||||
fe_ready=1
|
||||
break
|
||||
fi
|
||||
if ! kill -0 "${frontend_pid}" 2>/dev/null; then
|
||||
echo "error: frontend pid ${frontend_pid} died. Last 30 lines:" >&2
|
||||
tail -30 "${frontend_log}" >&2
|
||||
exit 7
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
|
||||
if [[ "${fe_ready}" != "1" ]]; then
|
||||
echo "error: frontend did not respond 200 within 60s. Last 30 lines:" >&2
|
||||
tail -30 "${frontend_log}" >&2
|
||||
exit 7
|
||||
fi
|
||||
|
||||
echo "[7/8] frontend / OK"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Print summary
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
cwd="$(readlink "/proc/${backend_pid}/cwd" 2>/dev/null || echo unknown)"
|
||||
gpu_mem_now="$(nvidia-smi --query-gpu=memory.used --format=csv,noheader,nounits 2>/dev/null | sed -n "$((GPU + 1))p" || echo 0)"
|
||||
ffmpeg_in_use="$(tr '\0' '\n' < "/proc/${backend_pid}/environ" 2>/dev/null | sed -n 's/^FASTVIDEO_FFMPEG_BIN=//p' | head -1)"
|
||||
[[ -z "${ffmpeg_in_use}" ]] && ffmpeg_in_use="$(command -v ffmpeg 2>/dev/null || echo '<not found>') (system fallback)"
|
||||
|
||||
cat <<SUMMARY
|
||||
[8/8] redeploy OK
|
||||
|
||||
Frontend : http://localhost:${FRONTEND_PORT} (PID ${frontend_pid})
|
||||
Backend : http://localhost:${BACKEND_PORT} (PID ${backend_pid})
|
||||
cwd=${cwd}
|
||||
gpu=${GPU} mem=${gpu_mem_now} MiB
|
||||
ffmpeg=${ffmpeg_in_use}
|
||||
|
||||
Logs : ${backend_log}
|
||||
${frontend_log}
|
||||
|
||||
Stop : ./.agents/skills/dreamverse-deploy/scripts/dreamverse-deploy.sh --stop
|
||||
|
||||
E2E : cd apps/dreamverse/web && \\
|
||||
PLAYWRIGHT_SKIP_WEBSERVER=1 \\
|
||||
BACKEND_URL=http://127.0.0.1:${BACKEND_PORT} \\
|
||||
PLAYWRIGHT_BASE_URL=http://127.0.0.1:${FRONTEND_PORT} \\
|
||||
NEXT_PUBLIC_INCLUDE_DEVTOOLS=1 \\
|
||||
npm exec -- playwright test
|
||||
SUMMARY
|
||||
@@ -0,0 +1,79 @@
|
||||
---
|
||||
name: env-var-conventions
|
||||
description: Add, read, rename, or remove an environment variable in FastVideo, or change the environment-variable policy. Use before touching fastvideo/envs.py, os.environ, os.getenv, or monkeypatch.setenv in fastvideo/, and when fastvideo/tests/contract/test_env_policy.py fails.
|
||||
---
|
||||
|
||||
# Environment Variable Conventions
|
||||
|
||||
## Purpose
|
||||
|
||||
FastVideo registers its environment variables as typed fields in
|
||||
`fastvideo/envs.py`. The policy that governs them is
|
||||
`docs/contributing/env_vars.md`, and the contract test
|
||||
`fastvideo/tests/contract/test_env_policy.py` enforces the policy in the unit
|
||||
CI lane. This skill routes an environment-variable change through that policy.
|
||||
The policy doc is the single source of the rules; read it instead of relying
|
||||
on a summary here.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Read `docs/contributing/env_vars.md` in full.
|
||||
- Decide whether the setting belongs in an environment variable or an argument
|
||||
(rule 5 in the policy doc). Settings that users change per deployment are
|
||||
arguments; add them through `fastvideo/fastvideo_args.py` instead.
|
||||
|
||||
## Inputs
|
||||
|
||||
| Parameter | Required | Description |
|
||||
| ---------- | -------- | -------------------------------------------------------------- |
|
||||
| `change` | Yes | Add, read, rename, or remove a variable, or change the policy. |
|
||||
| `variable` | Yes | The variable name, with the `FASTVIDEO_` prefix. |
|
||||
|
||||
## Steps
|
||||
|
||||
1. **Declare or edit the variable in `fastvideo/envs.py`.**
|
||||
- Pick the field type and category that the policy doc lists.
|
||||
- Write a description that states what the variable does and its units.
|
||||
- To rename, keep the old name in `deprecated_names`. To remove, add the
|
||||
name to `DEPRECATED_VARIABLES`. Update the uses in `examples/`,
|
||||
`scripts/`, `docs/`, `apps/`, and the tests.
|
||||
2. **Read the variable with `envs.NAME.get()` inside a function.**
|
||||
- In tests, change the value with `envs.NAME.override(value)`, and a variable
|
||||
outside the registry with `envs.override_external(name, value)`; the
|
||||
`env_overrides` fixture keeps either until the end of the test.
|
||||
- Name a variable that only tests read `FASTVIDEO_TEST_*`.
|
||||
- Do not call `os.environ`, `os.getenv`, or `monkeypatch.setenv` for a
|
||||
FastVideo variable.
|
||||
- To set a variable that another tool reads, call `envs.set_external`,
|
||||
`envs.setdefault_external`, or `envs.unset_external`.
|
||||
3. **Regenerate the table in the policy doc.**
|
||||
- Run `python fastvideo/tests/contract/test_env_policy.py`.
|
||||
4. **Run the contract test.**
|
||||
- Run `pytest fastvideo/tests/contract/test_env_policy.py`.
|
||||
- When the test reports a fixed known violation, delete or lower its entry
|
||||
in `KNOWN_VIOLATIONS`. Never add an entry to `KNOWN_VIOLATIONS`.
|
||||
5. **When the policy itself changes, update the policy doc and the contract
|
||||
test in the same pull request.**
|
||||
- The rules in `docs/contributing/env_vars.md`, the checks and allowlist in
|
||||
`fastvideo/tests/contract/test_env_policy.py`, and this skill must agree.
|
||||
|
||||
## Outputs
|
||||
|
||||
- A registry entry in `fastvideo/envs.py` and call sites that use
|
||||
`envs.NAME.get()`.
|
||||
- A regenerated table in `docs/contributing/env_vars.md`.
|
||||
- A passing `fastvideo/tests/contract/test_env_policy.py`.
|
||||
|
||||
## Example Usage
|
||||
|
||||
```
|
||||
Add a FASTVIDEO_DEBUG_MY_STAGE switch that logs MyStage inputs.
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
- `docs/contributing/env_vars.md`: the policy, the field types, and the
|
||||
violation kinds that the contract test reports.
|
||||
- `fastvideo/envs.py`: the registry.
|
||||
- `fastvideo/tests/contract/test_env_policy.py`: the contract test,
|
||||
`EXTERNAL_ALLOWLIST`, and `KNOWN_VIOLATIONS`.
|
||||
@@ -0,0 +1,642 @@
|
||||
---
|
||||
name: reseed-performance-baseline
|
||||
description: Re-seed the HF performance-tracking baseline for an intentional runtime, dependency, environment-caused benchmark shift, or reviewed v2 calibration using one or more reviewed normalized performance JSONs. Use when performance CI fails because metrics such as latency, throughput, component time, or peak memory changed for an accepted reason and the rolling median baseline in FastVideo/performance-tracking must be advanced, or when a new v2 exact comparable identity needs its first approved baseline. The workflow backs up existing history under /tmp, validates all source JSONs for the same legacy (model_id, gpu_type) target or the same v2 exact identity, rejects internally inconsistent source batches, uploads one success=true baseline record per accepted source JSON, and offers to clean local temp state after a successful upload.
|
||||
---
|
||||
|
||||
# Re-seed Performance Baseline
|
||||
|
||||
## Purpose
|
||||
|
||||
Replace or advance the rolling performance baseline in the HF dataset
|
||||
`FastVideo/performance-tracking`. Legacy targets are scoped by
|
||||
`(model_id, gpu_type)`. V2 targets are scoped by exact comparable identity:
|
||||
`workload_id`, `variant_id`, `benchmark_version`, `hardware_profile_id`,
|
||||
`software_profile_id`, and `recipe_fingerprint`.
|
||||
|
||||
Performance comparison uses the median of up to the last 5 successful,
|
||||
baseline-eligible records for the same target. Failed or calibration-only
|
||||
records are useful audit history, but they do not move the future baseline
|
||||
because `compare_baseline.py` loads records with `successful_only=True` and
|
||||
`baseline_eligible_only=True`.
|
||||
|
||||
This skill now reseeds from a reviewed batch of one or more source performance
|
||||
JSONs. It uploads one new `success=true` record per accepted source JSON; it
|
||||
does not blindly replicate one measurement into 3 or 5 records. The effective
|
||||
reseed size is therefore dynamic and equals the number of provided, validated,
|
||||
internally consistent source JSONs.
|
||||
|
||||
For baseline shifts with existing history, if the operator provides fewer than
|
||||
3 records, call out that the last-5 rolling median may not move immediately. If
|
||||
the operator provides 3 consistent shifted records, the rolling median usually
|
||||
moves immediately. If the operator provides 5 consistent shifted records, the
|
||||
last-5 window is effectively reset to the new runtime profile. For the first
|
||||
approved v2 baseline of a new exact identity, one reviewed calibration seed is
|
||||
enough for the next comparable run to leave `CALIBRATION_NEEDED`.
|
||||
|
||||
These records are intentional operator-approved baseline resets, not ordinary
|
||||
independent main-branch persistence. Mark them clearly with provenance fields
|
||||
so the HF history remains auditable.
|
||||
|
||||
Use this skill when a performance test fails for an intentional and reviewed
|
||||
reason, such as a torch/runtime/container upgrade that legitimately increases
|
||||
peak memory or changes timings. This is the performance equivalent of
|
||||
`reseed-ssim-references`: backup first, scope tightly, require explicit human
|
||||
approval, then upload reviewed accepted baseline records.
|
||||
|
||||
## When to use
|
||||
|
||||
- A PR or main run failed the rolling performance comparison by more than the
|
||||
allowed regression threshold, and maintainers agree the shift is caused by
|
||||
an intentional runtime, dependency, hardware image, or benchmark environment
|
||||
change rather than a FastVideo logic regression.
|
||||
- One or more shifted source result JSONs have been reviewed and accepted, and
|
||||
the operator wants to use those exact reviewed results to advance the rolling
|
||||
baseline.
|
||||
- The source batch is internally consistent: no provided source JSON regresses
|
||||
against the batch median by more than the configured tolerance.
|
||||
|
||||
## When not to use
|
||||
|
||||
- The benchmark failure might be a real code regression. Fix or investigate
|
||||
the code path first.
|
||||
- The fixed benchmark thresholds in
|
||||
`.buildkite/performance-benchmarks/tests/*.json` are too low. Those are a
|
||||
separate gate from the rolling HF baseline and may need a code review change.
|
||||
- There is no clear source run, commit, and rationale. Baseline history is a
|
||||
production signal; do not edit it without provenance.
|
||||
- The provided source JSONs disagree materially with each other. Rerun or
|
||||
investigate instead of uploading a noisy reseed batch.
|
||||
|
||||
## Inputs
|
||||
|
||||
| Parameter | Required | Description |
|
||||
|-----------|----------|-------------|
|
||||
| `model_id` | Legacy required; v2 inferred | Benchmark id, e.g. `wan-t2v-1.3b-2gpu`. This maps to the HF subdirectory after `sanitize(model_id)`. For v2 records, use the `model_id` from each source artifact only as the upload directory; comparison is by exact identity. |
|
||||
| `gpu_type` | Legacy required; v2 inferred | Exact GPU device string from the performance record, e.g. the L40S device name emitted by CI. V2 hardware matching uses `hardware_profile_id`; preserve `gpu_type` as display metadata. |
|
||||
| `source_results` | Yes | One or more local paths or Buildkite artifact URLs for accepted shifted performance JSONs. Prefer normalized `normalized_perf_*.json` artifacts emitted by `compare_baseline.py`. Accept `source_result` as an alias only for a single JSON. |
|
||||
| `max_intra_batch_regression` | No | Maximum allowed regression of any source JSON against the source batch median. Default: `0.05` (5%). |
|
||||
| `intent_rationale` | Yes | One-line explanation for why the baseline shift is legitimate. This is written into provenance and should be reused in the PR. |
|
||||
|
||||
Hardcoded defaults:
|
||||
|
||||
- HF repo: `FastVideo/performance-tracking` (`HF_REPO_ID` override is
|
||||
supported by the code, but use the default unless the user explicitly asks).
|
||||
- Local sync root: `/tmp/perf-tracking` (`PERFORMANCE_TRACKING_ROOT` override
|
||||
is supported).
|
||||
- Prepared-record staging root: `/tmp/performance_reseed_prepared`
|
||||
(`PERFORMANCE_RESEED_STAGING_ROOT` override is supported). Keep it separate
|
||||
and non-nested from the sync root.
|
||||
- Backup root: `/tmp/performance_reseed_backup`.
|
||||
- Download scratch root for source artifact URLs: `/tmp/performance_reseed_source`.
|
||||
- Baseline window: last 5 `success=true`, `baseline_eligible=true` records
|
||||
for the same legacy `(model_id, gpu_type)` target or the same v2 exact
|
||||
comparable identity.
|
||||
- Reseed count: dynamic. Upload exactly one accepted seed record per validated
|
||||
source JSON.
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Validate the target and source results
|
||||
|
||||
Normalize `source_results` to a list. If the user passes a single
|
||||
`source_result`, treat it as a one-element `source_results` list and report
|
||||
that a single record may not move the last-5 median immediately.
|
||||
|
||||
If any source result is a Buildkite artifact URL, download it first into a
|
||||
local scratch directory under `/tmp/performance_reseed_source/` and use the
|
||||
downloaded JSON path for the rest of the workflow. If the agent cannot access
|
||||
the artifact because Buildkite authentication is missing, ask the user to
|
||||
download the artifact manually and provide the local path.
|
||||
|
||||
Prefer the normalized Buildkite artifact emitted by `compare_baseline.py`:
|
||||
|
||||
```text
|
||||
perf_reports/results/normalized_perf_*.json
|
||||
```
|
||||
|
||||
That file is already in the HF tracking schema. Load each normalized JSON
|
||||
directly:
|
||||
|
||||
```python
|
||||
import json
|
||||
|
||||
with open(source_result, encoding="utf-8") as f:
|
||||
record = json.load(f)
|
||||
```
|
||||
|
||||
Classify the source batch before continuing:
|
||||
|
||||
- **Legacy source records** have no v2 exact identity fields. Stop if any
|
||||
normalized record's `model_id` or `gpu_type` does not match the requested
|
||||
`model_id` and `gpu_type`.
|
||||
- **V2 source records** have exact identity fields. Stop unless every source
|
||||
record has all six comparable identity fields and they are identical across
|
||||
the batch: `workload_id`, `variant_id`, `benchmark_version`,
|
||||
`hardware_profile_id`, `software_profile_id`, and `recipe_fingerprint`.
|
||||
Do not fall back to legacy `(model_id, gpu_type)` matching for v2 records.
|
||||
|
||||
The source records may have `success: false` when they came from failed
|
||||
rolling baseline comparisons. That is expected; only the reviewed reseed
|
||||
records become new `success: true` baseline records after explicit approval.
|
||||
For a first v2 baseline seed, the source records must instead be successful
|
||||
scheduled-main full-suite `CALIBRATION_NEEDED` normalized artifacts. Reject PR,
|
||||
local, direct-run, non-main-branch, or non-full-suite calibration artifacts as
|
||||
seed sources.
|
||||
|
||||
Sort validated source records by their original `timestamp` ascending before
|
||||
preparing the seed records. If a source timestamp is missing or unparsable,
|
||||
preserve input order for those records and print a warning. This makes the
|
||||
fresh reseed timestamps deterministic and makes it clear which records enter
|
||||
the last-5 window when more than 5 source JSONs are provided.
|
||||
|
||||
Check that `HF_API_KEY` is exported. The sync path may be public, but the
|
||||
upload path requires write access.
|
||||
|
||||
### 1a. Check source batch consistency
|
||||
|
||||
Before syncing or preparing uploads, reject source batches that are internally
|
||||
inconsistent. Use the same metric direction as `compare_baseline.py`:
|
||||
|
||||
- Lower is better: `latency`, `memory`, `text_encoder_time_s`, `dit_time_s`,
|
||||
`vae_decode_time_s`.
|
||||
- Higher is better: `throughput`.
|
||||
|
||||
For each metric with at least two non-null source values:
|
||||
|
||||
1. Compute the source batch median.
|
||||
2. For lower-is-better metrics, compute `(source_value - batch_median) / batch_median`.
|
||||
3. For `throughput`, compute `(batch_median - source_value) / batch_median`.
|
||||
4. Stop if any source record regresses against the batch median by more than
|
||||
`max_intra_batch_regression`.
|
||||
|
||||
Default `max_intra_batch_regression` to `0.05`. Print a table with per-source values, batch median, and
|
||||
worst intra-batch regression.
|
||||
|
||||
This check prevents uploading a mixed batch where one JSON is materially
|
||||
slower or faster than the others. If the batch fails this check, ask the user
|
||||
to provide a cleaner batch or explicitly investigate the variance. Do not
|
||||
silently drop outliers unless the user gives a concrete reviewed reason and a
|
||||
new source list.
|
||||
|
||||
### 1b. How to obtain source results from CI
|
||||
|
||||
The performance CI exports normalized source results for failed rolling
|
||||
baseline comparisons when `compare_baseline.py` ran. The preferred artifacts
|
||||
come from:
|
||||
|
||||
```text
|
||||
perf_reports/results/normalized_perf_*.json
|
||||
```
|
||||
|
||||
The normal operator flow is:
|
||||
|
||||
1. Open the failed Buildkite performance job or several reruns of the same
|
||||
benchmark after the accepted environment shift.
|
||||
2. Download the `normalized_perf_*.json` artifacts for the target benchmark.
|
||||
3. Pass all reviewed local paths or artifact URLs as `source_results`.
|
||||
|
||||
Do not scrape the Markdown performance summary to reconstruct JSON. The
|
||||
normalized JSON artifacts are the only supported source of truth for reseed
|
||||
metrics and provenance. Raw `fastvideo/tests/performance/results/perf_*.json`
|
||||
artifacts are not accepted by this skill. If no normalized JSON artifact is
|
||||
present, that run is not a valid source for baseline reseeding.
|
||||
|
||||
### 2. Sync and back up existing HF records under /tmp
|
||||
|
||||
Use `fastvideo/performance/hf_store.py` helpers directly. Do **not** use
|
||||
`compare_baseline.py` as a sync shortcut; on full main runs it can persist
|
||||
records, while this step must only fetch and back up existing history.
|
||||
|
||||
The sync command pattern is:
|
||||
|
||||
```bash
|
||||
export PERFORMANCE_TRACKING_ROOT="${PERFORMANCE_TRACKING_ROOT:-/tmp/perf-tracking}"
|
||||
export HF_REPO_ID="${HF_REPO_ID:-FastVideo/performance-tracking}"
|
||||
python -c 'from fastvideo.performance.hf_store import sync_from_hf; import os; sync_from_hf(os.environ["PERFORMANCE_TRACKING_ROOT"], strict=True)'
|
||||
```
|
||||
|
||||
For legacy records, back up the sanitized model directory under `/tmp`:
|
||||
|
||||
```bash
|
||||
SHORT_COMMIT=$(git rev-parse --short=12 HEAD)
|
||||
TIMESTAMP=$(date -u +%Y%m%d_%H%M%S)
|
||||
MODEL_SAFE=$(python - <<'PY'
|
||||
from fastvideo.performance.hf_store import sanitize
|
||||
print(sanitize("<model_id>"))
|
||||
PY
|
||||
)
|
||||
BACKUP_DIR="/tmp/performance_reseed_backup/${TIMESTAMP}_${SHORT_COMMIT}_${MODEL_SAFE}"
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
cp -R "${PERFORMANCE_TRACKING_ROOT}/${MODEL_SAFE}" "$BACKUP_DIR/" 2>/dev/null || true
|
||||
```
|
||||
|
||||
For v2 records, back up the full local tracking root after sync. Exact identity
|
||||
lookup scans across model directories, so a benchmark rename may have relevant
|
||||
history outside the current source artifact's `model_id` directory:
|
||||
|
||||
```bash
|
||||
BACKUP_DIR="/tmp/performance_reseed_backup/${TIMESTAMP}_${SHORT_COMMIT}_v2_exact_identity"
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
cp -R "${PERFORMANCE_TRACKING_ROOT}" "$BACKUP_DIR/tracking-root"
|
||||
```
|
||||
|
||||
Write provenance next to the backup:
|
||||
|
||||
```bash
|
||||
cat > "$BACKUP_DIR/PROVENANCE.txt" <<EOF
|
||||
model_id: <model_id>
|
||||
gpu_type: <gpu_type>
|
||||
source_results:
|
||||
- <source_result_1>
|
||||
- <source_result_2>
|
||||
reseed_record_count: <len(source_results)>
|
||||
max_intra_batch_regression: <threshold>
|
||||
head_commit: $(git rev-parse HEAD)
|
||||
timestamp_utc: $(date -u +%FT%TZ)
|
||||
reason: <intent_rationale>
|
||||
EOF
|
||||
```
|
||||
|
||||
If the backup has no prior records, this is not a destructive reseed; it is a
|
||||
first baseline seed. Continue, but report that baseline history was empty.
|
||||
|
||||
### 3. Compute old baseline and candidate shift
|
||||
|
||||
Load the last 5 successful baseline records for the target.
|
||||
|
||||
For legacy targets:
|
||||
|
||||
```python
|
||||
from fastvideo.performance.hf_store import load_records_for_model
|
||||
|
||||
records = load_records_for_model(
|
||||
"/tmp/perf-tracking",
|
||||
"<model_id>",
|
||||
"<gpu_type>",
|
||||
last_n=5,
|
||||
successful_only=True,
|
||||
baseline_eligible_only=True,
|
||||
)
|
||||
```
|
||||
|
||||
For v2 exact-identity targets:
|
||||
|
||||
```python
|
||||
from fastvideo.performance.hf_store import load_records_for_identity
|
||||
|
||||
records = load_records_for_identity(
|
||||
"/tmp/perf-tracking",
|
||||
{
|
||||
"workload_id": "<workload_id>",
|
||||
"variant_id": "<variant_id>",
|
||||
"benchmark_version": "<benchmark_version>",
|
||||
"hardware_profile_id": "<hardware_profile_id>",
|
||||
"software_profile_id": "<software_profile_id>",
|
||||
"recipe_fingerprint": "<recipe_fingerprint>",
|
||||
},
|
||||
last_n=5,
|
||||
successful_only=True,
|
||||
baseline_eligible_only=True,
|
||||
)
|
||||
```
|
||||
|
||||
Print a small table showing old medians, source batch medians, candidate
|
||||
medians after appending the proposed seed records, and source batch spread for:
|
||||
|
||||
- `latency`
|
||||
- `throughput`
|
||||
- `memory`
|
||||
- `text_encoder_time_s`
|
||||
- `dit_time_s`
|
||||
- `vae_decode_time_s`
|
||||
|
||||
Also print how many successful old records exist. Make clear:
|
||||
|
||||
- 1 seed record usually does not move an existing last-5 median by itself, but
|
||||
it is enough to establish the first v2 baseline for a new exact identity.
|
||||
- 3 consistent seed records usually move the last-5 median immediately.
|
||||
- 5 consistent seed records effectively reset the last-5 window.
|
||||
- The records are intentional approved baseline resets and must be labeled
|
||||
that way.
|
||||
|
||||
### 4. Confirm intent
|
||||
|
||||
Require an explicit confirmation phrase before preparing the upload:
|
||||
|
||||
> About to RE-SEED performance baseline for `<target description>`.
|
||||
> This will upload `<N>` new `success=true` records to
|
||||
> `FastVideo/performance-tracking/<sanitize(model_id)>/` or the source
|
||||
> artifact's v2 model directory, one per accepted source JSON.
|
||||
>
|
||||
> Reason: `<intent_rationale>`
|
||||
> Source results: `<source_results>`
|
||||
> Reseed record count: `<N>`
|
||||
> Max intra-batch regression: `<threshold>`
|
||||
> Note: these records come from a reviewed source batch and are intended to
|
||||
> move the rolling median to the accepted runtime profile. They are not
|
||||
> ordinary main-branch persistence.
|
||||
> HEAD: `<git rev-parse --short=12 HEAD>`
|
||||
> Backup: `<BACKUP_DIR>`
|
||||
>
|
||||
> Reply `confirm performance reseed` to proceed, anything else to abort.
|
||||
|
||||
Do not continue unless the user types exactly `confirm performance reseed`.
|
||||
|
||||
### 5. Create the accepted seed records
|
||||
|
||||
Create one seed record from each normalized source result.
|
||||
|
||||
For first v2 baseline seeds, use the scoped utility. It validates exact
|
||||
identity, requires successful scheduled-main full-suite `CALIBRATION_NEEDED`
|
||||
source artifacts, preserves the normalized v2 identity and metadata fields,
|
||||
and writes seed records with `success=true`, `baseline_eligible=true`, and
|
||||
`comparison_status=PASS`:
|
||||
|
||||
```bash
|
||||
python fastvideo/tests/performance/seed_baseline.py \
|
||||
--source-result <normalized_perf_1.json> \
|
||||
--source-result <normalized_perf_2.json> \
|
||||
--intent-rationale "<intent_rationale>" \
|
||||
--max-intra-batch-regression 0.05 \
|
||||
--tracking-root "${PERFORMANCE_TRACKING_ROOT}" \
|
||||
--staging-root "${PERFORMANCE_RESEED_STAGING_ROOT:-/tmp/performance_reseed_prepared}"
|
||||
```
|
||||
|
||||
The utility is prepare-only and intentionally has no upload option. Upload the
|
||||
scoped records only after the separate confirmation in step 6.
|
||||
|
||||
The utility validates against an isolated fresh HF snapshot and leaves
|
||||
`PERFORMANCE_TRACKING_ROOT` untouched; that argument only proves the staging
|
||||
root is separate from the operator's tracking mirror. Before writing, it stops
|
||||
if the exact identity already has a successful baseline-eligible record or if
|
||||
the workload/variant/version already trusts another recipe. It atomically
|
||||
reserves the exact identity and writes a digest-protected upload manifest bound
|
||||
to the current HF endpoint, repository id, and repository type. Keep the
|
||||
prepared records, manifest, source files, and reservation unchanged until the
|
||||
operation is uploaded or explicitly cleaned up.
|
||||
|
||||
If the prepared seed records look correct, upload only those scoped records in
|
||||
step 7. Do not rerun the utility with a different source list after approval.
|
||||
|
||||
For legacy reseeds or accepted v2 baseline shifts from regression artifacts,
|
||||
create one seed record from each normalized source result. Do not copy the
|
||||
source JSON wholesale.
|
||||
|
||||
Infer the baseline field allowlist from all existing HF records for the target
|
||||
after syncing, including both `success=true` and `success=false` records. For
|
||||
legacy targets the target is `(model_id, gpu_type)`. For v2 baseline-shift
|
||||
reseeds the target is the exact comparable identity. Use the union of
|
||||
non-provenance keys present in those target records, preserving only fields
|
||||
that also exist in the normalized source record or are explicitly set by the
|
||||
reseed workflow. Always include `model_id`, `timestamp`, `success`,
|
||||
`baseline_eligible`, and `comparison_status` because the upload path and
|
||||
baseline loader depend on them. Always set `timestamp` to a fresh reseed
|
||||
timestamp, `success` to `true`, `baseline_eligible` to `true`, and
|
||||
`comparison_status` to `PASS`. Do not include unrelated source-only fields
|
||||
that are absent from existing HF records.
|
||||
|
||||
Exclude existing provenance or operator metadata from the inferred baseline
|
||||
field allowlist. At minimum, exclude keys prefixed with `baseline_reseed` and
|
||||
any fields known to be local-only audit metadata.
|
||||
|
||||
If there are no previous HF records for the target, fall back to this default
|
||||
baseline field list:
|
||||
|
||||
- `model_id`
|
||||
- `timestamp`
|
||||
- `commit_sha`
|
||||
- `gpu_type`
|
||||
- `latency`
|
||||
- `throughput`
|
||||
- `memory`
|
||||
- `text_encoder_time_s`
|
||||
- `dit_time_s`
|
||||
- `vae_decode_time_s`
|
||||
- `success`
|
||||
- `baseline_eligible`
|
||||
- `comparison_status`
|
||||
|
||||
For v2 baseline-shift reseeds with no previous HF records for the exact
|
||||
identity, also preserve:
|
||||
|
||||
- `workload_id`
|
||||
- `variant_id`
|
||||
- `benchmark_version`
|
||||
- `recipe_fingerprint`
|
||||
- `hardware_profile_id`
|
||||
- `software_profile_id`
|
||||
- `recipe`
|
||||
- `hardware_profile`
|
||||
- `software_profile`
|
||||
- `software_comparison_profile`
|
||||
|
||||
Do not upload extra fields from the source artifact.
|
||||
|
||||
Optional provenance fields are allowed and useful:
|
||||
|
||||
- `baseline_reseed: true`
|
||||
- `baseline_reseed_reason`
|
||||
- `baseline_reseed_source_result`
|
||||
- `baseline_reseed_source_timestamp`
|
||||
- `baseline_reseed_source_success`
|
||||
- `baseline_reseed_batch_size`
|
||||
- `baseline_reseed_batch_index`
|
||||
- `baseline_reseed_operator`
|
||||
- `baseline_reseed_max_intra_batch_regression`
|
||||
|
||||
The v2 calibration seed utility writes analogous first-seed provenance:
|
||||
|
||||
- `baseline_seed: true`
|
||||
- `baseline_seed_reason`
|
||||
- `baseline_seed_source_result`
|
||||
- `baseline_seed_source_status`
|
||||
- `baseline_seed_source_timestamp`
|
||||
- `baseline_seed_source_success`
|
||||
- `baseline_seed_source_run_source`
|
||||
- `baseline_seed_source_branch`
|
||||
- `baseline_seed_source_test_scope`
|
||||
- `baseline_seed_source_pr_number`
|
||||
- `baseline_seed_batch_size`
|
||||
- `baseline_seed_batch_index`
|
||||
- `baseline_seed_operator`
|
||||
|
||||
Use a fresh reseed timestamp for each seed record, not the original source
|
||||
result timestamp. This is required because
|
||||
`load_records_for_model(..., last_n=5)` keeps the last records after loading
|
||||
the model directory; stale filenames/timestamps may not enter the last-5
|
||||
window and therefore may not move the median. Preserve the original source
|
||||
timestamp in `baseline_reseed_source_timestamp`.
|
||||
|
||||
Use the existing filename convention from `_write_tracking_record()`:
|
||||
`<sanitize(timestamp)>_<sanitize(commit_sha)>.json` under the sanitized model
|
||||
directory, but include a deterministic suffix such as `_reseed_01`,
|
||||
`_reseed_02`, and so on before `.json` so multiple records from the same
|
||||
batch do not overwrite each other.
|
||||
|
||||
If a source record already exists on HF with `success=false`, do not edit it
|
||||
in place unless the user explicitly asked for an audit-preserving correction.
|
||||
Prefer uploading new accepted seed records so failed history remains visible.
|
||||
|
||||
### 6. Pause before upload
|
||||
|
||||
Print:
|
||||
|
||||
- Backup directory path under `/tmp`.
|
||||
- Prepared local record paths under `PERFORMANCE_RESEED_STAGING_ROOT`.
|
||||
- Prepared upload-manifest path under the identity reservation.
|
||||
- HF paths that will receive the new records.
|
||||
- Old rolling medians.
|
||||
- Source batch medians, source batch spread, reseed count, and candidate
|
||||
medians.
|
||||
- Rationale.
|
||||
|
||||
Ask the user to reply exactly `upload`. Anything else aborts and leaves the
|
||||
prepared records plus backup on disk.
|
||||
|
||||
### 7. Upload only the scoped records
|
||||
|
||||
For a first v2 calibration seed, use the manifest uploader after the user
|
||||
replies exactly `upload`:
|
||||
|
||||
```bash
|
||||
python -c 'from fastvideo.tests.performance.seed_baseline import upload_prepared_seed_manifest; print(upload_prepared_seed_manifest("<prepared_manifest>"))'
|
||||
```
|
||||
|
||||
The uploader verifies the source and prepared-record digests, pins and scans
|
||||
the current HF revision, rechecks exact-identity and recipe-cohort conflicts,
|
||||
and writes the entire batch in one commit whose `parent_commit` must still be
|
||||
current. A concurrent Hub update makes the commit fail. Do not retry
|
||||
automatically: preserve staging, refresh/review remote state, and request a new
|
||||
explicit `upload` after the conflict is understood. Each record goes to:
|
||||
|
||||
```text
|
||||
FastVideo/performance-tracking/<sanitize(model_id)>/<record_filename>.json
|
||||
```
|
||||
|
||||
Never call `upload_record()` once per first-seed record: that can partially
|
||||
land the batch and has no compare-and-swap guard.
|
||||
|
||||
For a legacy reseed or an accepted v2 baseline shift, the first-seed manifest
|
||||
validator does not apply because an eligible baseline already exists. Upload
|
||||
only the individually reviewed records prepared in step 5 with the shared
|
||||
`upload_record(local_path, record, strict=True)` helper. Stop on the first
|
||||
failure and report exactly which records reached HF; do not silently rerun or
|
||||
replicate the remainder.
|
||||
|
||||
Never bulk upload the tracking or staging root, and never modify another
|
||||
model's directory in the same operation.
|
||||
|
||||
### 8. Report outcome and offer cleanup
|
||||
|
||||
Report:
|
||||
|
||||
- Uploaded HF paths.
|
||||
- Backup directory under `/tmp`.
|
||||
- Local tracking root, usually `/tmp/perf-tracking`.
|
||||
- Old baseline window count and medians.
|
||||
- Source batch medians, source batch spread, reseed count, and candidate
|
||||
medians.
|
||||
- Expected effect based on reseed count.
|
||||
- Any separate threshold changes still needed in
|
||||
`.buildkite/performance-benchmarks/tests/*.json`.
|
||||
|
||||
Include the `intent_rationale` in the PR or follow-up comment so reviewers can
|
||||
distinguish an accepted baseline shift from a hidden regression.
|
||||
|
||||
After the upload is verified, ask whether the user wants to clear temporary
|
||||
local state. Explain what each directory is for:
|
||||
|
||||
- `PERFORMANCE_TRACKING_ROOT`, usually `/tmp/perf-tracking`: read-only local
|
||||
synced mirror used for operator review and reporting. First-v2 preparation
|
||||
independently proves remote state from a fresh temporary HF snapshot.
|
||||
- `PERFORMANCE_RESEED_STAGING_ROOT`, usually
|
||||
`/tmp/performance_reseed_prepared`: prepared local seed records used for the
|
||||
scoped upload, plus the identity reservation and digest manifest. Keeping
|
||||
this separate prevents aborted preparations from appearing in later
|
||||
baseline reads.
|
||||
- `/tmp/performance_reseed_backup/<...>`: local backup of the target model's
|
||||
pre-reseed HF history plus `PROVENANCE.txt`, kept so a bad reseed can be
|
||||
audited or corrected.
|
||||
- `/tmp/performance_reseed_source/<...>` when used: downloaded source JSON
|
||||
artifacts from Buildkite URLs.
|
||||
|
||||
Ask:
|
||||
|
||||
> Reseed succeeded. Do you want me to delete the local temp tracking mirror,
|
||||
> this reseed's prepared staging records, source downloads, and reseed backup
|
||||
> under `/tmp`? These files are local safety/audit artifacts only; HF already
|
||||
> has the uploaded records.
|
||||
>
|
||||
> Reply `cleanup reseed temp` to delete them, anything else to keep them.
|
||||
|
||||
Do not delete anything unless the user replies exactly
|
||||
`cleanup reseed temp`. If cleanup is requested, remove only the specific
|
||||
directories and prepared record paths created for this reseed. Do not remove
|
||||
the shared staging root when it contains other records. Remove this operation's
|
||||
identity reservation only with its prepared records and manifest, and never
|
||||
remove unrelated `/tmp` contents.
|
||||
|
||||
## Failure modes and handling
|
||||
|
||||
- **`HF_API_KEY` unset.** Stop before upload. Do not create an untracked
|
||||
process that appears to have reseeded but never reached HF.
|
||||
- **Source result does not match target.** Stop. The wrong benchmark or GPU
|
||||
would poison a separate baseline.
|
||||
- **Source batch is internally inconsistent.** Stop if any source regresses
|
||||
against the source batch median by more than `max_intra_batch_regression`.
|
||||
Ask for cleaner sources or a reviewed explanation before continuing.
|
||||
- **Too few source records to move the median.** Continue only after making
|
||||
clear that one or two records may not immediately move an existing last-5
|
||||
median. This warning does not block a first v2 calibration seed for an exact
|
||||
identity with no eligible baseline yet.
|
||||
- **The source results are noisy or suspicious.** Stop. Reseeding amplifies
|
||||
those measurements into the baseline, so they must be reviewed first.
|
||||
- **HF sync fails.** Stop for destructive reseeds. A stale or empty sync can
|
||||
make the old baseline look missing.
|
||||
- **The exact v2 identity already has an eligible baseline.** Stop. The
|
||||
`CALIBRATION_NEEDED` artifact is stale; use the reviewed baseline-shift path
|
||||
instead of the first-seed utility.
|
||||
- **The workload/variant/version trusts another recipe.** Stop. The source is
|
||||
stale relative to the current recipe cohort and must not bypass
|
||||
`RECIPE_MISMATCH` by creating a second trusted recipe.
|
||||
- **The staging root already has a prepared seed for the exact identity.**
|
||||
Stop and reuse, upload, or explicitly clean that preparation. Do not prepare
|
||||
another copy of the same measurement.
|
||||
- **The conditional Hub commit loses its parent race.** Stop without retrying.
|
||||
Keep the preparation, refresh and review the new remote state, then request
|
||||
a new explicit `upload` only if the seed is still valid.
|
||||
- **Candidate still violates fixed thresholds.** Report that this skill only
|
||||
handles the rolling HF baseline; update benchmark JSON thresholds in code
|
||||
review if maintainers accept the new absolute limit.
|
||||
- **The user aborts at either confirmation.** Leave the backup and prepared
|
||||
records on disk. Nothing should be uploaded.
|
||||
- **The user declines cleanup.** Keep `/tmp/perf-tracking`, the prepared seed
|
||||
records under `/tmp/performance_reseed_prepared`, the source download
|
||||
directory if any, and `/tmp/performance_reseed_backup/<...>` in place for
|
||||
audit/debugging.
|
||||
- **A bad seed was uploaded.** Use the backup and HF history to identify the
|
||||
uploaded file, then remove or supersede it with an explicitly reviewed
|
||||
corrective record. Do not silently rewrite unrelated history.
|
||||
|
||||
## References
|
||||
|
||||
- `.agents/skills/reseed-ssim-references/SKILL.md` — safety pattern for
|
||||
intentional baseline replacement.
|
||||
- `fastvideo/tests/performance/compare_baseline.py` — normalization, rolling
|
||||
median comparison, and persistence rules.
|
||||
- `fastvideo/performance/hf_store.py` — HF sync and record loading helpers.
|
||||
- `fastvideo/tests/performance/seed_baseline.py` — first-seed preparation,
|
||||
staging reservation, manifest validation, and conditional batch upload.
|
||||
- `fastvideo/tests/performance/test_inference_performance.py` — source result
|
||||
JSON schema.
|
||||
- `.buildkite/performance-benchmarks/tests/*.json` — fixed absolute benchmark
|
||||
thresholds, separate from rolling baseline comparisons.
|
||||
|
||||
## Changelog
|
||||
|
||||
| Date | Change |
|
||||
|------|--------|
|
||||
| 2026-05-03 | Initial version. Sister workflow to `reseed-ssim-references`, scoped to one performance `(model_id, gpu_type)` baseline seed with backup, confirmation, provenance, and `success=true` upload. |
|
||||
| 2026-05-03 | Previous policy: replicate one approved shifted source result into 3 success records by default, or 5 only when explicitly requested. Add provenance marker for replicated-source reseeds. Superseded by the 2026-05-08 dynamic multi-source policy. |
|
||||
| 2026-05-08 | Replace fixed 3/5 replication with dynamic multi-source reseeding: upload one seed record per reviewed source JSON, validate intra-batch consistency, move backup/source scratch under `/tmp`, and ask whether to clean temp state after successful upload. |
|
||||
| 2026-07-13 | Keep first-v2-seed preparation outside the canonical mirror, reserve staging identities atomically, reject stale or replayed calibration seeds, and upload reviewed manifests with a single parent-guarded Hub commit. |
|
||||
@@ -0,0 +1,344 @@
|
||||
---
|
||||
name: reseed-ssim-references
|
||||
description: Re-seed HF reference videos for a single existing SSIM test on Modal L40S. Always backs up current refs locally first, regenerates on Modal, pauses for the user to eyeball before-vs-after quality, then overwrites the targeted model subtree on `FastVideo/ssim-reference-videos` with `--force`. Use when an intentional code change (model port fix, attention backend swap, kernel upgrade, hyperparameter change) has invalidated existing refs and they need to be regenerated. Pairs with `seed-ssim-references`, which is for first-time seeding only.
|
||||
---
|
||||
|
||||
# Re-seed SSIM Reference Videos
|
||||
|
||||
## Purpose
|
||||
|
||||
Replace the existing SSIM reference videos for a single `(test_file, model_id)`
|
||||
pair on the HF dataset (`FastVideo/ssim-reference-videos`). This is **destructive**
|
||||
on HF — the old refs are overwritten — so the skill always:
|
||||
|
||||
1. Confirms intent with a one-liner the user has to type.
|
||||
2. Downloads the existing refs as a local, timestamped backup.
|
||||
3. Regenerates through the manual legacy Modal L40S maintenance path.
|
||||
4. Pauses for a side-by-side eyeball of backup vs new mp4s.
|
||||
5. Uploads with `--force`, scoped to the single `--model-id`.
|
||||
6. Reminds the user to keep the backup until the PR lands.
|
||||
|
||||
Pairs with `seed-ssim-references`, which is the inverse (first-time seeding
|
||||
only, refuses to overwrite). Re-seeding is intentionally a separate, more
|
||||
ceremonial operation because mistakenly clobbering production refs is much
|
||||
harder to recover from than failing closed.
|
||||
|
||||
## When to use
|
||||
|
||||
- An intentional code change (model port fix, kernel upgrade, attention
|
||||
backend swap, hyperparameter change in the test itself) has shifted the
|
||||
expected SSIM output and the existing refs no longer represent the new
|
||||
ground truth.
|
||||
- A test is failing in CI **for the right reason** (the new code is correct,
|
||||
the old refs are stale).
|
||||
|
||||
## When not to use
|
||||
|
||||
- A test is failing for the **wrong** reason (the port is buggy, not the
|
||||
refs). Fix the port; re-seeding hides the bug.
|
||||
- A brand-new test that has no refs on HF yet. Use `seed-ssim-references`.
|
||||
- "Just to clean up drift" without a concrete code change to point at. The
|
||||
PR description has to justify *why* refs changed; without a concrete
|
||||
change, there's nothing to write.
|
||||
|
||||
## Inputs
|
||||
|
||||
| Parameter | Required | Description |
|
||||
|-----------|----------|-------------|
|
||||
| `test_file` | Yes | Path to the SSIM test, e.g. `fastvideo/tests/ssim/test_matrixgame_similarity.py`. Validated against `fastvideo/tests/ssim/test_*_similarity.py`. |
|
||||
| `model_id` | Yes | Single model id from the test's `*_MODEL_TO_PARAMS`, e.g. `Matrix-Game-2.0-Diffusers-Base`. Re-seed runs are **per model**. For multi-model tests, invoke the skill once per model. |
|
||||
| `intent_rationale` | Yes | One-line explanation of *why* refs are being regenerated (e.g. "Relax FA-2 head_size whitelist to include 80 — matrix_game now uses FLASH_ATTN instead of TORCH_SDPA"). Recorded in the backup directory and reused in the PR description. |
|
||||
|
||||
Hardcoded:
|
||||
|
||||
- Modal GPU: **L40S**. This is a manual reference-maintenance target, not the
|
||||
active Slurm CI compute path; changing the SKU also changes the historical
|
||||
`L40S_reference_videos` contract.
|
||||
- Quality tier: **`default`**. `full_quality` is a separate, deliberate
|
||||
operation.
|
||||
- HF repo: `FastVideo/ssim-reference-videos` (override via
|
||||
`FASTVIDEO_TEST_SSIM_REFERENCE_HF_REPO`).
|
||||
- Device folder: `L40S_reference_videos`.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
The user has confirmed:
|
||||
|
||||
- `modal` CLI authenticated.
|
||||
- `hf` CLI authenticated, **and** `HF_API_KEY` (or `HUGGINGFACE_HUB_TOKEN` /
|
||||
`HF_TOKEN`) exported with **write** access to
|
||||
`FastVideo/ssim-reference-videos`.
|
||||
- The current branch's code is the change that motivated the re-seed (i.e.
|
||||
`git rev-parse HEAD` is the commit that intentionally invalidated refs).
|
||||
|
||||
Fail fast if any of these are missing.
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Validate inputs and confirm intent
|
||||
|
||||
- Verify `test_file` exists and matches `fastvideo/tests/ssim/test_*_similarity.py`.
|
||||
- Grep the file for `*_MODEL_TO_PARAMS` and assert `model_id` is one of its
|
||||
keys. If the file has only a single hardcoded model, accept that model id
|
||||
as the only valid value.
|
||||
- Print the rationale and ask the user to type **`confirm reseed`** (not just
|
||||
`y` — make it deliberate):
|
||||
|
||||
> About to RE-SEED references for model `<model_id>` from test `<test_file>`.
|
||||
> This will OVERWRITE existing refs on
|
||||
> `FastVideo/ssim-reference-videos/reference_videos/default/L40S_reference_videos/<model_id>/`
|
||||
> after backup + Modal regen + eyeball.
|
||||
>
|
||||
> Reason: `<intent_rationale>`
|
||||
> HEAD: `<git rev-parse --short=12 HEAD>`
|
||||
>
|
||||
> Reply `confirm reseed` to proceed, anything else to abort.
|
||||
|
||||
Stop until the user types exactly `confirm reseed`. Anything else aborts
|
||||
with no side effects.
|
||||
|
||||
### 2. Back up existing refs
|
||||
|
||||
Always required. The backup is the only graceful path back if anything goes
|
||||
wrong later.
|
||||
|
||||
```bash
|
||||
SHORT_COMMIT=$(git rev-parse --short=12 HEAD)
|
||||
TIMESTAMP=$(date -u +%Y%m%d_%H%M%S)
|
||||
MODEL_SAFE=$(echo "<model_id>" | tr '/' '_')
|
||||
BACKUP_DIR="ssim_reseed_backup/${TIMESTAMP}_${SHORT_COMMIT}_${MODEL_SAFE}"
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
|
||||
hf download \
|
||||
--repo-type dataset FastVideo/ssim-reference-videos \
|
||||
--include "reference_videos/default/L40S_reference_videos/<model_id>/**" \
|
||||
--local-dir "$BACKUP_DIR"
|
||||
|
||||
mp4_count=$(find "$BACKUP_DIR" -name "*.mp4" | wc -l)
|
||||
echo "Backup mp4 count: $mp4_count"
|
||||
[ "$mp4_count" -gt 0 ] || {
|
||||
echo "ERROR: backup is empty for <model_id>. Either the model id is wrong"
|
||||
echo "or there are no existing refs (use seed-ssim-references instead)."
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Provenance — used in the PR description
|
||||
cat > "$BACKUP_DIR/PROVENANCE.txt" <<EOF
|
||||
test_file: <test_file>
|
||||
model_id: <model_id>
|
||||
head_commit: $(git rev-parse HEAD)
|
||||
timestamp_utc: $(date -u +%FT%TZ)
|
||||
reason: <intent_rationale>
|
||||
EOF
|
||||
```
|
||||
|
||||
If the `hf download` produces zero mp4s, abort — the user has either picked a
|
||||
non-existent `model_id` or there are no refs yet (in which case
|
||||
`seed-ssim-references` is the right tool).
|
||||
|
||||
### 3. Regenerate on Modal L40S
|
||||
|
||||
Mirror CI's exact env recipe so the regenerated refs are byte-comparable to
|
||||
what CI will produce on the same commit. Two differences from CI:
|
||||
|
||||
1. **Pass the same env prefix CI uses** (`IMAGE_VERSION`, `BUILDKITE_*`) — see
|
||||
`.buildkite/pipeline.yml:1-3` and `.buildkite/scripts/pr_test.sh:62-83`.
|
||||
Without this, `ssim_test.py:17-18` resolves a different GHCR image tag
|
||||
(default is `latest`, CI is `py3.12-latest`), and `ssim_test.py:38-46`
|
||||
bakes different values into the image's frozen env block. **Mismatched
|
||||
image or env is the most common source of SSIM drift between reseed and
|
||||
CI runs.**
|
||||
2. **Do not pass `--skip-reference-download`**. Letting the test fetch the
|
||||
existing refs and run the full SSIM compare gives "before" SSIM numbers
|
||||
for the PR description, and the test still produces the new mp4s
|
||||
regardless of whether the comparison passes or fails.
|
||||
|
||||
```bash
|
||||
SUBDIR="${TIMESTAMP}_${SHORT_COMMIT}"
|
||||
|
||||
IMAGE_VERSION="py3.12-latest" \
|
||||
BUILDKITE_REPO="$(git config --get remote.origin.url)" \
|
||||
BUILDKITE_COMMIT="$(git rev-parse HEAD)" \
|
||||
BUILDKITE_PULL_REQUEST="${BUILDKITE_PULL_REQUEST:-false}" \
|
||||
modal run fastvideo/tests/modal/ssim_test.py \
|
||||
--git-repo="$(git config --get remote.origin.url)" \
|
||||
--git-commit="$(git rev-parse HEAD)" \
|
||||
--hf-api-key="$HF_API_KEY" \
|
||||
--test-files="<test_file>" \
|
||||
--sync-generated-to-volume \
|
||||
--generated-volume-subdir="$SUBDIR" \
|
||||
--no-fail-fast
|
||||
```
|
||||
|
||||
Capture the printed `modal volume get ...` hint — its `<SUBDIR>` matches
|
||||
`$SUBDIR` and is needed for step 4. Capture the SSIM numbers from the test
|
||||
output (or from the JSON next to the generated mp4) for the PR description.
|
||||
|
||||
### 4. Download generated videos
|
||||
|
||||
```bash
|
||||
modal volume get --force hf-model-weights \
|
||||
ssim_generated_videos/default/"$SUBDIR"/generated_videos \
|
||||
./generated_videos_modal/default
|
||||
```
|
||||
|
||||
After this, the new mp4s live at:
|
||||
|
||||
```
|
||||
./generated_videos_modal/default/generated_videos/L40S_reference_videos/<model_id>/<backend>/<prompt>.mp4
|
||||
```
|
||||
|
||||
`--force` is required when `./generated_videos_modal/default` already exists
|
||||
from a prior run; safe on the first run too.
|
||||
|
||||
### 5. PAUSE — user reviews quality side-by-side
|
||||
|
||||
Print the diff and the comparison:
|
||||
|
||||
```bash
|
||||
echo "=== File list diff (backup vs new) ==="
|
||||
diff -u \
|
||||
<(find "$BACKUP_DIR/reference_videos/default/L40S_reference_videos/<model_id>" -name "*.mp4" \
|
||||
| sed "s|$BACKUP_DIR/reference_videos/default/L40S_reference_videos/||" | sort) \
|
||||
<(find ./generated_videos_modal/default/generated_videos/L40S_reference_videos/<model_id> -name "*.mp4" \
|
||||
| sed "s|./generated_videos_modal/default/generated_videos/L40S_reference_videos/||" | sort) \
|
||||
|| true
|
||||
|
||||
echo
|
||||
echo "=== SSIM numbers from this run (paste into PR) ==="
|
||||
find ./generated_videos_modal/default/generated_videos/L40S_reference_videos/<model_id> -name "*_ssim.json" -exec cat {} \;
|
||||
```
|
||||
|
||||
Then stop and tell the user:
|
||||
|
||||
> Old refs backed up to `$BACKUP_DIR`.
|
||||
> New videos in `./generated_videos_modal/default/generated_videos/L40S_reference_videos/<model_id>/`.
|
||||
>
|
||||
> Open both in a video player. Confirm the new videos:
|
||||
> 1. Look correct (no obvious artifacts, no black/static frames).
|
||||
> 2. Are *intentionally* different from the backup in the way described
|
||||
> in `<intent_rationale>` (e.g. slight numerical drift only, not a
|
||||
> different scene / different motion / corrupted output).
|
||||
>
|
||||
> Reply **`upload`** to overwrite HF, anything else to abort.
|
||||
> Aborting leaves the backup and new videos on disk for inspection — nothing
|
||||
> on HF changes.
|
||||
|
||||
Do not proceed until the user types exactly `upload`. If they abort, leave
|
||||
everything on disk and stop here.
|
||||
|
||||
### 6. Copy into the local reference layout
|
||||
|
||||
Same as `seed-ssim-references` step 5:
|
||||
|
||||
```bash
|
||||
python fastvideo/tests/ssim/reference_videos_cli.py copy-local \
|
||||
--quality-tier default \
|
||||
--device-folder L40S_reference_videos \
|
||||
--generated-dir ./generated_videos_modal/default/generated_videos/L40S_reference_videos
|
||||
```
|
||||
|
||||
Result: `fastvideo/tests/ssim/reference_videos/default/L40S_reference_videos/<model_id>/<backend>/<prompt>.mp4`.
|
||||
|
||||
### 7. Upload with `--force`, scoped to `--model-id`
|
||||
|
||||
The `--force` flag is what makes this skill different from `seed-ssim-references`.
|
||||
Always pair it with `--model-id` so a typo cannot accidentally overwrite a
|
||||
neighboring model's refs.
|
||||
|
||||
```bash
|
||||
python fastvideo/tests/ssim/reference_videos_cli.py upload \
|
||||
--quality-tier default \
|
||||
--device-folder L40S_reference_videos \
|
||||
--model-id "<model_id>" \
|
||||
--force
|
||||
```
|
||||
|
||||
The CLI's overwrite guard refuses without `--force`; with `--force` it
|
||||
overwrites only files under
|
||||
`reference_videos/default/L40S_reference_videos/<model_id>/`.
|
||||
|
||||
### 8. Report success and retention guidance
|
||||
|
||||
Print:
|
||||
|
||||
- The HF path that was overwritten (`<repo>/reference_videos/default/L40S_reference_videos/<model_id>/`).
|
||||
- The local backup directory path.
|
||||
- The new SSIM numbers from step 5.
|
||||
- This restore command, in case the PR review surfaces a problem after
|
||||
upload:
|
||||
|
||||
```bash
|
||||
python fastvideo/tests/ssim/reference_videos_cli.py upload \
|
||||
--quality-tier default \
|
||||
--device-folder L40S_reference_videos \
|
||||
--model-id "<model_id>" \
|
||||
--reference-dir "$BACKUP_DIR/reference_videos/default/L40S_reference_videos" \
|
||||
--force
|
||||
```
|
||||
|
||||
- This PR-description checklist (see `fastvideo/tests/ssim/AGENTS.md` →
|
||||
*Updating Reference Videos*):
|
||||
1. Source commit that produced the new refs (HEAD at re-seed time).
|
||||
2. Test command and GPU SKU (`L40S`).
|
||||
3. Before/after SSIM numbers.
|
||||
4. The `<intent_rationale>` from step 1.
|
||||
5. A note that the backup lives at `$BACKUP_DIR` and should be retained
|
||||
until CI on the PR is green.
|
||||
|
||||
Do **not** auto-rerun the SSIM test — the user does that as part of the PR.
|
||||
|
||||
## Failure modes and how to handle them
|
||||
|
||||
- **`HF_API_KEY` unset.** Stop before step 2.
|
||||
- **Backup is empty (zero mp4s).** Stop before step 3 — the model id is
|
||||
wrong or the refs don't exist yet (use `seed-ssim-references`).
|
||||
- **Modal run fails before generation.** No mp4s on the volume. Don't
|
||||
upload. Investigate the failure (test crash, OOM, partition exhaustion),
|
||||
fix, then retry from step 3. Backup is still intact.
|
||||
- **Quality regressed (visual or metric).** User aborts at step 5. Backup
|
||||
retained. New videos retained on disk for inspection. Nothing on HF
|
||||
changed. Either fix the underlying code change or abandon the re-seed.
|
||||
- **User confirmed `upload` but later realized the new refs are wrong.**
|
||||
Run the restore command from step 8 with the backup `--reference-dir`.
|
||||
This is exactly why the backup exists.
|
||||
- **Multi-model test, only one model is being re-seeded.** Run the skill
|
||||
once per model id. The `--model-id` scope on upload guarantees the others
|
||||
are untouched.
|
||||
|
||||
## Design notes (for future skill maintainers)
|
||||
|
||||
- Per-`model_id` scope is mandatory. The dataset houses many model subtrees;
|
||||
re-seeding the wrong one is hard to undo without backup.
|
||||
- `default` tier only; `full_quality` is a separate, deliberate operation
|
||||
with different params and ~doubled runtime, and isn't what CI gates on.
|
||||
- The skill deliberately does **not** pass `--skip-reference-download` to
|
||||
Modal so we get pre-reseed SSIM numbers for the PR. The `seed`-skill
|
||||
passes it because no refs exist yet; for re-seed, refs do exist and
|
||||
exposing the comparison is informative.
|
||||
- The two-token confirm (`confirm reseed`, then `upload`) is intentional.
|
||||
Re-seeding is high-blast-radius and should not be one-keystroke.
|
||||
- The backup directory is plain mp4s + `PROVENANCE.txt`. No HF metadata is
|
||||
preserved; the restore path uses `reference_videos_cli.py upload
|
||||
--reference-dir` which doesn't need it.
|
||||
|
||||
## References
|
||||
|
||||
- `.agents/skills/seed-ssim-references/SKILL.md` — the first-time seed
|
||||
skill this one parallels. Read it for the Modal flag rationale shared
|
||||
between the two flows.
|
||||
- `fastvideo/tests/ssim/AGENTS.md` — directory rules, including the PR
|
||||
expectations for any reference-video change (rationale, before/after
|
||||
SSIM, source commit/model/backend).
|
||||
- `fastvideo/tests/ssim/reference_videos_cli.py` — `copy-local`, `upload`
|
||||
(with `--model-id`, `--force`), `download`. The overwrite guard at
|
||||
`upload_reference_videos` is the safety net this skill leans on.
|
||||
- `fastvideo/tests/modal/ssim_test.py` — Modal orchestrator;
|
||||
`--sync-generated-to-volume`, `--generated-volume-subdir`,
|
||||
`--skip-reference-download`, `--no-fail-fast`.
|
||||
|
||||
## Changelog
|
||||
|
||||
| Date | Change |
|
||||
|------|--------|
|
||||
| 2026-05-02 | Initial version. Sister skill to `seed-ssim-references`, scoped to single `(test_file, model_id)` re-seeds, with mandatory backup and two-token confirm. |
|
||||
@@ -0,0 +1,380 @@
|
||||
---
|
||||
name: seed-ssim-references
|
||||
description: Seed HF reference artefacts for a single newly-added SSIM test (pixel `.mp4` for `run_text_to_video_similarity_test`-style tests, or latent `.pt` for `run_text_to_latent_similarity_test`-style tests). Runs the test on Modal L40S, downloads the generated artefacts via `modal volume get`, pauses for the user to verify (visual eyeball for mp4, numerics dump for pt), then uploads only that test's files to `FastVideo/ssim-reference-videos`. Use when a new `fastvideo/tests/ssim/test_*_similarity.py` has just been added and has no references on HF yet.
|
||||
---
|
||||
|
||||
# Seed SSIM Reference Artefacts (mp4 or pt)
|
||||
|
||||
## Purpose
|
||||
|
||||
A brand-new SSIM test in `fastvideo/tests/ssim/` fails forever until its
|
||||
reference artefacts exist on the HF dataset
|
||||
(`FastVideo/ssim-reference-videos`). The dataset hosts two kinds of artefacts
|
||||
side-by-side per `(model_id, backend, prompt)`:
|
||||
|
||||
- **`.mp4`** — pixel ground-truth for tests that call
|
||||
`run_text_to_video_similarity_test` / `run_image_to_video_similarity_test`
|
||||
in `inference_similarity_utils.py`. Compared via SSIM.
|
||||
- **`.pt`** — pre-VAE latent bundle (fp16 full latent + fp32 slice +
|
||||
metadata + `slice_spec` + `format_version`) for tests that call
|
||||
`run_text_to_latent_similarity_test` in `latent_similarity_utils.py`.
|
||||
Compared via cosine distance on the slice and the full tensor.
|
||||
|
||||
This skill:
|
||||
|
||||
1. Detects which artefact type the test produces (pixel vs latent).
|
||||
2. Runs the test on Modal's L40S pool to generate the artefacts.
|
||||
3. Downloads them to the local repo via `modal volume get`.
|
||||
4. Pauses so the user can verify quality:
|
||||
- **mp4**: visual eyeball in a video player.
|
||||
- **pt**: numerics dump (shape, slice stats, NaN/Inf check, metadata).
|
||||
5. Uploads only the new test's files to HF, with a guard that refuses to
|
||||
overwrite anything already present.
|
||||
|
||||
The skill is run **manually**, once per new test. Before invoking it, the user
|
||||
has already sanity-tested the new test locally — it launches `VideoGenerator`
|
||||
and writes an artefact without crashing (the missing-reference assertion at
|
||||
the end is expected). The skill does not re-test locally; it goes straight
|
||||
to the manual legacy Modal L40S reference-maintenance target. Active CI runs
|
||||
on the Slinky Slurm cluster and only consumes the resulting references.
|
||||
|
||||
## When to use
|
||||
|
||||
- A new `test_*_similarity.py` file has been added in `fastvideo/tests/ssim/`
|
||||
and the HF dataset has no `reference_videos/default/L40S_reference_videos/<model_id>/`
|
||||
subtree for it yet.
|
||||
|
||||
## When not to use
|
||||
|
||||
- Regular CI runs — once refs exist, `pytest fastvideo/tests/ssim/` downloads
|
||||
them automatically.
|
||||
- Re-seeding an existing test. That requires `--force` on the upload step, and
|
||||
is out of scope here; treat as a separate, deliberate operation.
|
||||
|
||||
## Inputs
|
||||
|
||||
The skill has **one required input**: the path to the new SSIM test file.
|
||||
Prompt the user for it if they didn't supply it.
|
||||
|
||||
| Parameter | Required | Description |
|
||||
|-----------|----------|-------------|
|
||||
| `test_file` | Yes | e.g. `fastvideo/tests/ssim/test_ltx2_similarity.py`. The skill's first action is to ask for this if missing. |
|
||||
|
||||
Everything else is fixed:
|
||||
|
||||
- Modal maintenance GPU: **L40S** (hardcoded in
|
||||
`fastvideo/tests/modal/ssim_test.py`; this is not the active CI compute path).
|
||||
- Device folder: `L40S_reference_videos`.
|
||||
- Quality tier: `default` (the tier CI runs). The `full_quality` tier is not
|
||||
seeded by this skill.
|
||||
- HF repo: `FastVideo/ssim-reference-videos` (dataset).
|
||||
- Multi-model test files: all model ids in `*_MODEL_TO_PARAMS` are seeded
|
||||
together; the Modal run produces one mp4 per (model, prompt, backend) and
|
||||
the upload scopes by `--model-id`, looping if there is more than one.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
The user has confirmed:
|
||||
|
||||
- `modal` CLI authenticated.
|
||||
- `HF_API_KEY` (or `HUGGINGFACE_HUB_TOKEN` / `HF_TOKEN`) exported with write
|
||||
access to `FastVideo/ssim-reference-videos`.
|
||||
- The test file runs locally end-to-end (generates an mp4; SSIM assertion
|
||||
failure due to missing reference is expected and fine).
|
||||
|
||||
Fail fast if the token env var is missing.
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Ask for the test file, then detect artefact type
|
||||
|
||||
If the user didn't name one, ask: *"Which SSIM test file do you want to seed
|
||||
references for? (e.g. `fastvideo/tests/ssim/test_ltx2_similarity.py`)"*.
|
||||
|
||||
Validate:
|
||||
|
||||
- Path exists and matches `fastvideo/tests/ssim/test_*_similarity.py`.
|
||||
- File defines a `*_MODEL_TO_PARAMS` dict — grep it to extract the set of
|
||||
model ids. Those ids drive step 5.
|
||||
|
||||
Detect artefact type by inspecting the file's imports / helper call:
|
||||
|
||||
- **latent** (`.pt`) — file imports `run_text_to_latent_similarity_test`
|
||||
from `fastvideo.tests.ssim.latent_similarity_utils` (or any other helper
|
||||
that ends with `_latent_similarity_test`).
|
||||
- **pixel** (`.mp4`) — file imports
|
||||
`run_text_to_video_similarity_test` / `run_image_to_video_similarity_test`
|
||||
from `fastvideo.tests.ssim.inference_similarity_utils`, OR uses the
|
||||
legacy custom-inline helper pattern (see `test_gen3c`). Default to pixel
|
||||
when both heuristics fail.
|
||||
|
||||
Record `ARTEFACT_TYPE ∈ {pixel, latent}` for use in step 4. Steps 2, 3, 5,
|
||||
and 6 are artefact-type-agnostic — `_iter_reference_files`,
|
||||
`copy_generated_to_reference`, and `upload_reference_videos` already walk
|
||||
both `.mp4` and `.pt` (see `reference_videos_cli.py`).
|
||||
|
||||
If either check fails, stop and tell the user what's wrong.
|
||||
|
||||
### 2. Run the test on Modal L40S
|
||||
|
||||
Pick a subdir name so repeated runs don't collide:
|
||||
|
||||
```bash
|
||||
SHORT_COMMIT=$(git rev-parse --short=12 HEAD)
|
||||
TIMESTAMP=$(date -u +%Y%m%d_%H%M%S)
|
||||
SUBDIR="${TIMESTAMP}_${SHORT_COMMIT}"
|
||||
```
|
||||
|
||||
Then launch the Modal run. The `IMAGE_VERSION` and `BUILDKITE_*` env-prefix
|
||||
**must** match what CI exports in `.buildkite/scripts/pr_test.sh`, otherwise
|
||||
`fastvideo/tests/modal/ssim_test.py` resolves a different GHCR image tag
|
||||
(default is `latest`, CI is `py3.12-latest`) and bakes different values into
|
||||
the image's frozen env block (`ssim_test.py:17-18, 38-46`). Mismatched image
|
||||
or env produces SSIM drift that doesn't show up until the same commit runs
|
||||
in CI.
|
||||
|
||||
```bash
|
||||
IMAGE_VERSION="py3.12-latest" \
|
||||
BUILDKITE_REPO="$(git config --get remote.origin.url)" \
|
||||
BUILDKITE_COMMIT="$(git rev-parse HEAD)" \
|
||||
BUILDKITE_PULL_REQUEST="${BUILDKITE_PULL_REQUEST:-false}" \
|
||||
modal run fastvideo/tests/modal/ssim_test.py \
|
||||
--git-repo="$(git config --get remote.origin.url)" \
|
||||
--git-commit="$(git rev-parse HEAD)" \
|
||||
--hf-api-key="$HF_API_KEY" \
|
||||
--test-files="<test_file>" \
|
||||
--sync-generated-to-volume \
|
||||
--generated-volume-subdir="$SUBDIR" \
|
||||
--skip-reference-download \
|
||||
--no-fail-fast
|
||||
```
|
||||
|
||||
Env prefix rationale (parity with CI; see `.buildkite/pipeline.yml:1-3` and
|
||||
`.buildkite/scripts/pr_test.sh:62-83`):
|
||||
- `IMAGE_VERSION=py3.12-latest`: pins the Modal image tag to the same one CI
|
||||
uses. The published `py3.12-latest` and `latest` tags point at Python 3.12 /
|
||||
CUDA 12.6.3 / cu126; `py3.12-cuda12.6.3-latest` is the explicit alias for the
|
||||
same image. CUDA 13 / cu130 is available under the explicit
|
||||
`py3.12-cuda13.0.0-latest` tag. This tag policy comes from
|
||||
`infra-build-image.yml`; the unparameterized `docker/Dockerfile` build itself
|
||||
still defaults to CUDA 13 / cu130.
|
||||
- `BUILDKITE_REPO`/`BUILDKITE_COMMIT`/`BUILDKITE_PULL_REQUEST`: mirror what
|
||||
Buildkite exports. `ssim_test.py:38-46` bakes these into the image's
|
||||
`.env(...)` block; mismatched values can perturb in-container code paths
|
||||
that branch on PR-vs-non-PR. `false` for `BUILDKITE_PULL_REQUEST` matches
|
||||
Buildkite's "non-PR build" sentinel.
|
||||
|
||||
Flag rationale:
|
||||
- `--skip-reference-download`: no refs exist yet, so conftest must not try to
|
||||
pull them.
|
||||
- `--no-fail-fast`: lets the test finish generation before `_assert_similarity`
|
||||
raises `FileNotFoundError: Reference video folder does not exist`. The
|
||||
expected failure is what we want — the mp4 has already been written.
|
||||
- `--sync-generated-to-volume` + `--generated-volume-subdir`: copies the
|
||||
generated mp4s to the `hf-model-weights` Modal volume under
|
||||
`ssim_generated_videos/default/<SUBDIR>/generated_videos/` so we can pull
|
||||
them locally.
|
||||
|
||||
The Modal run will end with a nonzero exit (expected) and print a
|
||||
`modal volume get hf-model-weights ssim_generated_videos/default/<SUBDIR>/generated_videos ./generated_videos_modal/default`
|
||||
command. Capture that `<SUBDIR>` — you need it for step 3.
|
||||
|
||||
### 3. Download generated videos locally
|
||||
|
||||
```bash
|
||||
modal volume get --force hf-model-weights \
|
||||
ssim_generated_videos/default/"$SUBDIR"/generated_videos \
|
||||
./generated_videos_modal/default
|
||||
```
|
||||
|
||||
`--force` is required when the parent `./generated_videos_modal/default`
|
||||
already exists; without it, `modal volume get` errors with `[Errno 21] Is a
|
||||
directory`. Safe to pass on the first run too.
|
||||
|
||||
After this, the mp4s live at
|
||||
`./generated_videos_modal/default/generated_videos/L40S_reference_videos/<model_id>/<backend>/<prompt>.mp4`.
|
||||
The extra `generated_videos/` level comes from the volume layout in
|
||||
`_sync_generated_videos_to_volume` (`ssim_test.py`) — the command copies
|
||||
`<repo>/fastvideo/tests/ssim/generated_videos/<tier>` to
|
||||
`ssim_generated_videos/<tier>/<SUBDIR>/generated_videos/`, and `modal volume
|
||||
get` preserves that trailing `generated_videos/` segment.
|
||||
|
||||
### 4. PAUSE — user reviews quality
|
||||
|
||||
Type-aware verification.
|
||||
|
||||
**For `ARTEFACT_TYPE = pixel`** — list the downloaded mp4s and ask the user to
|
||||
open them in a video player:
|
||||
|
||||
> "Generated videos downloaded to `./generated_videos_modal/default/generated_videos/L40S_reference_videos/`. Please open them and confirm the quality looks correct. Reply **`upload`** to continue, or anything else to abort."
|
||||
|
||||
**For `ARTEFACT_TYPE = latent`** — `.pt` files are not human-watchable. Print
|
||||
a numerics dump for each `.pt` so the user can sanity-check shape, distribution,
|
||||
and metadata:
|
||||
|
||||
```python
|
||||
import torch
|
||||
from pathlib import Path
|
||||
ROOT = Path("./generated_videos_modal/default/generated_videos/L40S_reference_videos")
|
||||
for p in sorted(ROOT.rglob("*.pt")):
|
||||
d = torch.load(p, map_location="cpu", weights_only=False)
|
||||
s = d["expected_slice"]
|
||||
L = d["latent"].float()
|
||||
print(f"=== {p.relative_to(ROOT)} ===")
|
||||
print(f" format_version: {d['format_version']}")
|
||||
print(f" shape: {d['shape']}")
|
||||
print(f" dtype_original: {d['dtype_original']}")
|
||||
print(f" slice_spec: {d['slice_spec']}")
|
||||
print(f" slice shape={tuple(s.shape)} mean={s.mean():+.4f} std={s.std():.4f} min={s.min():+.4f} max={s.max():+.4f}")
|
||||
print(f" latent shape={tuple(L.shape)} mean={L.mean():+.4f} std={L.std():.4f} min={L.min():+.4f} max={L.max():+.4f}")
|
||||
print(f" finite: latent NaN={torch.isnan(L).any().item()} Inf={torch.isinf(L).any().item()}; "
|
||||
f"slice NaN={torch.isnan(s).any().item()} Inf={torch.isinf(s).any().item()}")
|
||||
print(f" metadata: {d['metadata']}\n")
|
||||
```
|
||||
|
||||
Sanity criteria:
|
||||
- `format_version == 1` (matches `LATENT_REFERENCE_FORMAT_VERSION`).
|
||||
- `shape` matches what the model produces (e.g. LTX-2 distilled =
|
||||
`[1, 128, T_lat, H_lat, W_lat]`; Stable Audio Open 1.0 = `[1, 64, 1024]`).
|
||||
- `slice_spec.kind` matches a registered kind (`corner_3x3_first_frame`
|
||||
for video, `audio_first_8_timesteps` for audio).
|
||||
- No `NaN`/`Inf`. `mean ≈ 0`, `std ≈ 1` (denoised latents stay close to
|
||||
the initial Gaussian distribution; very wide deviations suggest
|
||||
numerical drift).
|
||||
- `metadata.prompt` matches the test's prompt.
|
||||
|
||||
Then ask:
|
||||
|
||||
> "Numerics look right? Reply **`upload`** to continue, or anything else to abort."
|
||||
|
||||
Do not proceed until the user explicitly says `upload`. If they abort, leave
|
||||
everything on disk so they can inspect further — no cleanup.
|
||||
|
||||
### 5. Copy into the local reference layout
|
||||
|
||||
Scoped copy — only the new test's artefacts. Single command works for both
|
||||
artefact types because `_iter_reference_files` walks `.mp4` and `.pt`:
|
||||
|
||||
```bash
|
||||
python fastvideo/tests/ssim/reference_videos_cli.py copy-local \
|
||||
--quality-tier default \
|
||||
--device-folder L40S_reference_videos \
|
||||
--generated-dir ./generated_videos_modal/default/generated_videos/L40S_reference_videos
|
||||
```
|
||||
|
||||
(The `--generated-dir` points at the device-folder root inside the
|
||||
downloaded tree; `copy-local` walks all `<model>/<backend>/*.{mp4,pt}`
|
||||
underneath it. Since the Modal run was scoped to a single test file via
|
||||
`--test-files`, only that test's model(s) are present — so the copy is
|
||||
implicitly per-test.)
|
||||
|
||||
Result for pixel: `fastvideo/tests/ssim/reference_videos/default/L40S_reference_videos/<model_id>/<backend>/<prompt>.mp4`.
|
||||
Result for latent: same path with `.pt` extension.
|
||||
|
||||
### 6. Upload to HF — scoped per model_id, with overwrite guard
|
||||
|
||||
For each `<model_id>`:
|
||||
|
||||
```bash
|
||||
python fastvideo/tests/ssim/reference_videos_cli.py upload \
|
||||
--quality-tier default \
|
||||
--device-folder L40S_reference_videos \
|
||||
--model-id "<model_id>"
|
||||
```
|
||||
|
||||
The upload command:
|
||||
|
||||
- Uploads **only** `reference_videos/default/L40S_reference_videos/<model_id>/`.
|
||||
- **Refuses** if any file already exists at that path on HF (this is the
|
||||
guard — seeding a new test should never clobber existing refs). To override,
|
||||
the user must re-run with `--force`. If the guard fires, stop and report
|
||||
exactly which files exist; do not silently `--force`.
|
||||
|
||||
Reads the HF token from `HF_API_KEY` / `HUGGINGFACE_HUB_TOKEN` / `HF_TOKEN`.
|
||||
|
||||
### 7. Report success
|
||||
|
||||
List what was uploaded (paths in repo) and remind the user to push any
|
||||
related code changes. Do **not** auto-verify by re-running Modal — the user
|
||||
can run `pytest fastvideo/tests/ssim/<test_file>` later to confirm end-to-end;
|
||||
it will auto-download the refs they just uploaded.
|
||||
|
||||
## Failure modes and how to handle them
|
||||
|
||||
- **`HF_API_KEY` unset.** Stop before step 2. The Modal run needs it (passed
|
||||
via `--hf-api-key`), and step 6 needs it for upload. If the user
|
||||
ran `hf auth login` instead of exporting an env var, read the cached
|
||||
token via `huggingface_hub.get_token()` and forward it to Modal as
|
||||
`--hf-api-key="$CACHED_TOKEN"`.
|
||||
- **Modal run fails before generation.** No artefacts on the volume — nothing
|
||||
to download. Fix the test locally (`pytest fastvideo/tests/ssim/<test_file>`)
|
||||
and retry from step 2.
|
||||
- **`./generated_videos_modal/default/L40S_reference_videos/` missing after
|
||||
`modal volume get`.** The run didn't produce artefacts (most likely the
|
||||
test crashed before writing, or `REQUIRED_GPUS` exceeded the partition
|
||||
capacity — see Modal logs).
|
||||
- **Latent test crashed with FSDP / inference_mode error
|
||||
(`RuntimeError: Inference tensors do not track version counter`).** The
|
||||
test must pass `init_kwargs_override={"use_fsdp_inference": False}` when
|
||||
`sp_size == 1` — see `test_stable_audio_similarity.py` for the pattern.
|
||||
Fix in the test, push, retry.
|
||||
- **Upload guard fires (files already exist).** The test name / model id
|
||||
collides with something already on HF. Verify the user actually wants to
|
||||
replace existing refs; if so, re-run the upload with `--force`. If not,
|
||||
rename the model id in `*_MODEL_TO_PARAMS` and re-seed.
|
||||
- **Quality looks wrong in step 4.** Abort. The artefacts stay on disk for
|
||||
inspection. The fix is usually in the test's params (resolution, steps,
|
||||
seed) — edit the test, then re-run the skill.
|
||||
- For latent: also check `slice_spec.kind` matches the latent rank
|
||||
(`corner_3x3_first_frame` requires 5-D, `audio_first_8_timesteps`
|
||||
requires 3-D); a rank/kind mismatch raises in `_extract_expected_slice`.
|
||||
|
||||
## Design notes (for future skill maintainers)
|
||||
|
||||
- The skill deliberately runs on Modal, **not** locally, because the CI
|
||||
runner is L40S. Seeding from a different GPU SKU produces refs that CI's
|
||||
L40S runs can't match (pixel SSIM drifts across SKUs; latent cosine has
|
||||
tighter cross-SKU bf16 drift but the configured tolerances assume
|
||||
same-SKU seed → same-SKU verify).
|
||||
- The skill is default-tier only. `full_quality` refs are seeded by a
|
||||
separate, deliberate operation — they double runtime and aren't what CI
|
||||
gates on.
|
||||
- The overwrite guard in `reference_videos_cli.py upload` is default-on
|
||||
specifically because this skill exists. Re-seeding is a distinct operation
|
||||
that requires explicit `--force`.
|
||||
- Both artefact types share the same Modal flow: the orchestrator sets
|
||||
`--skip-reference-download` + `--no-fail-fast`, runs pytest, the test's
|
||||
helper writes the artefact (`.mp4` via `imageio` for pixel,
|
||||
`save_latent_reference` → `torch.save` for latent) BEFORE the
|
||||
missing-reference assertion raises. `_sync_generated_videos_to_volume` in
|
||||
`ssim_test.py` does a `shutil.copytree` of the whole `generated_videos/`
|
||||
tree, picking up `.mp4`, `.pt`, and the `*_ssim.json` / `*_latent.json`
|
||||
metric files alongside.
|
||||
|
||||
## References
|
||||
|
||||
- `fastvideo/tests/modal/ssim_test.py` — Modal orchestrator; see
|
||||
`--sync-generated-to-volume`, `--generated-volume-subdir`,
|
||||
`--skip-reference-download`, `--no-fail-fast`.
|
||||
- `fastvideo/tests/ssim/reference_videos_cli.py` — `copy-local`, `upload`
|
||||
(with `--model-id`, `--force`), `download`, `ensure` subcommands.
|
||||
Extension allowlist is `REFERENCE_EXTENSIONS = VIDEO_EXTENSIONS +
|
||||
LATENT_EXTENSIONS` (`.pt`).
|
||||
- `fastvideo/tests/ssim/README.md` — reference layout, HF repo conventions.
|
||||
- `fastvideo/tests/ssim/inference_similarity_utils.py` — pixel helpers
|
||||
(`run_text_to_video_similarity_test`,
|
||||
`run_image_to_video_similarity_test`, `build_init_kwargs`).
|
||||
- `fastvideo/tests/ssim/latent_similarity_utils.py` — latent helper
|
||||
(`run_text_to_latent_similarity_test`), slice spec dispatch
|
||||
(`_extract_expected_slice`), reference schema
|
||||
(`save_latent_reference` / `load_latent_reference`),
|
||||
`LATENT_REFERENCE_FORMAT_VERSION`.
|
||||
|
||||
## Changelog
|
||||
|
||||
| Date | Change |
|
||||
|------|--------|
|
||||
| 2026-04-17 | Initial version (Modal sync-to-volume flow). |
|
||||
| 2026-04-21 | Rewrite: single-test scope, explicit user-review pause, per-`model_id` upload, HF overwrite guard. Dropped `scripts/seed_ssim.sh`. |
|
||||
| 2026-04-21 | Post-first-run fixes: `modal volume get` needs `--force` when parent exists; download tree has an extra `generated_videos/` level so `--generated-dir` must reflect it. |
|
||||
| 2026-05-01 | Latent (`*.pt`) artefact support: artefact-type detection in step 1, type-aware verification (visual eyeball for mp4, numerics dump for pt) in step 4, FSDP+inference_mode failure-mode added, design notes for the unified Modal flow. Triggered by PR #1253 (LTX-2 latent migration + Stable Audio latent test). |
|
||||
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"benchmark_id": "wan-t2v-1.3b-1gpu-gb10",
|
||||
"config_schema_version": 2,
|
||||
"workload_id": "wan-t2v",
|
||||
"variant_id": "1.3b-sp1",
|
||||
"benchmark_version": 3,
|
||||
"description": "Wan2.1 T2V 1.3B single-GPU inference performance on NVIDIA DGX Spark (GB10). Single-GPU variant of wan-t2v-1.3b (same workload_id for dashboard comparability). Gated to the GB10 via run_config.gpu_types so it does not run on the shared H100/L40S lanes.",
|
||||
"model": {
|
||||
"model_path": "Wan-AI/Wan2.1-T2V-1.3B-Diffusers",
|
||||
"model_short_name": "Wan2.1-T2V-1.3B"
|
||||
},
|
||||
"init_kwargs": {
|
||||
"num_gpus": 1,
|
||||
"flow_shift": 7.0,
|
||||
"sp_size": 1,
|
||||
"tp_size": 1,
|
||||
"vae_sp": false,
|
||||
"vae_tiling": true,
|
||||
"text_encoder_precisions": ["fp32"]
|
||||
},
|
||||
"generation_kwargs": {
|
||||
"height": 480,
|
||||
"width": 832,
|
||||
"num_frames": 45,
|
||||
"num_inference_steps": 4,
|
||||
"guidance_scale": 3,
|
||||
"embedded_cfg_scale": 6,
|
||||
"seed": 1024,
|
||||
"fps": 24,
|
||||
"neg_prompt": "Bright tones, overexposed, static, blurred details, subtitles, style, works, paintings, images, static, overall gray, worst quality, low quality, JPEG compression residue, ugly, incomplete, extra fingers, poorly drawn hands, poorly drawn faces, deformed, disfigured, misshapen limbs, fused fingers, still picture, messy background, three legs, many people in the background, walking backwards"
|
||||
},
|
||||
"test_prompts": [
|
||||
"Will Smith casually eats noodles, his relaxed demeanor contrasting with the energetic background of a bustling street food market. The scene captures a mix of humor and authenticity. Mid-shot framing, vibrant lighting."
|
||||
],
|
||||
"run_config": {
|
||||
"num_warmup_runs": 2,
|
||||
"num_measurement_runs": 5,
|
||||
"required_gpus": 1,
|
||||
"gpu_types": ["GB10"]
|
||||
},
|
||||
"thresholds": {
|
||||
"GB10": {
|
||||
"max_generation_time_s": 55.0,
|
||||
"max_peak_memory_mb": 12000.0
|
||||
},
|
||||
"default": {
|
||||
"max_generation_time_s": 120.0,
|
||||
"max_peak_memory_mb": 40000.0
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"benchmark_id": "wan-t2v-1.3b-2gpu",
|
||||
"config_schema_version": 2,
|
||||
"workload_id": "wan-t2v",
|
||||
"variant_id": "1.3b-sp2",
|
||||
"benchmark_version": 3,
|
||||
"description": "Wan2.1 T2V 1.3B inference performance",
|
||||
"model": {
|
||||
"model_path": "Wan-AI/Wan2.1-T2V-1.3B-Diffusers",
|
||||
"model_short_name": "Wan2.1-T2V-1.3B"
|
||||
},
|
||||
"init_kwargs": {
|
||||
"num_gpus": 2,
|
||||
"flow_shift": 7.0,
|
||||
"sp_size": 2,
|
||||
"tp_size": 1,
|
||||
"vae_sp": true,
|
||||
"vae_tiling": true,
|
||||
"text_encoder_precisions": ["fp32"]
|
||||
},
|
||||
"generation_kwargs": {
|
||||
"height": 480,
|
||||
"width": 832,
|
||||
"num_frames": 45,
|
||||
"num_inference_steps": 4,
|
||||
"guidance_scale": 3,
|
||||
"embedded_cfg_scale": 6,
|
||||
"seed": 1024,
|
||||
"fps": 24,
|
||||
"neg_prompt": "Bright tones, overexposed, static, blurred details, subtitles, style, works, paintings, images, static, overall gray, worst quality, low quality, JPEG compression residue, ugly, incomplete, extra fingers, poorly drawn hands, poorly drawn faces, deformed, disfigured, misshapen limbs, fused fingers, still picture, messy background, three legs, many people in the background, walking backwards"
|
||||
},
|
||||
"test_prompts": [
|
||||
"Will Smith casually eats noodles, his relaxed demeanor contrasting with the energetic background of a bustling street food market. The scene captures a mix of humor and authenticity. Mid-shot framing, vibrant lighting."
|
||||
],
|
||||
"run_config": {
|
||||
"num_warmup_runs": 2,
|
||||
"num_measurement_runs": 5,
|
||||
"required_gpus": 2
|
||||
},
|
||||
"thresholds": {
|
||||
"L40S": {
|
||||
"max_generation_time_s": 34.0,
|
||||
"max_peak_memory_mb": 11000.0,
|
||||
"max_text_encoder_time_s": 5.0,
|
||||
"max_dit_time_s": 10.0,
|
||||
"max_vae_decode_time_s": 10.0
|
||||
},
|
||||
"default": {
|
||||
"max_generation_time_s": 120.0,
|
||||
"max_peak_memory_mb": 30000.0
|
||||
}
|
||||
}
|
||||
}
|
||||
+479
-61
@@ -1,66 +1,484 @@
|
||||
env:
|
||||
IMAGE_VERSION: "py3.12-latest"
|
||||
BUILDKITE_CLEAN_CHECKOUT: true
|
||||
# Slurm workers clone the immutable commit and initialize submodules inside
|
||||
# their isolated container. The Buildkite login-plane checkout is a no-op.
|
||||
BUILDKITE_GIT_SUBMODULES: false
|
||||
|
||||
notify:
|
||||
- github_commit_status:
|
||||
context: "fastcheck-passed"
|
||||
if: build.env("TEST_SCOPE") == "fastcheck" || build.env("TEST_SCOPE") == null
|
||||
- github_commit_status:
|
||||
context: "full-suite-passed"
|
||||
if: build.env("TEST_SCOPE") == "full" || build.env("TEST_SCOPE") == "merge"
|
||||
- github_commit_status:
|
||||
context: "direct-test-completed"
|
||||
if: build.env("TEST_SCOPE") == "direct"
|
||||
- github_commit_status:
|
||||
context: "scheduled-ssim-passed"
|
||||
if: build.env("TEST_SCOPE") == "scheduled"
|
||||
|
||||
# This is the complete active GPU CI surface. Every command is a trusted host
|
||||
# dispatcher, and every test payload executes inside the Slinky Slurm tray.
|
||||
# fastvideo/tests/modal remains available only for an explicit manual rollback;
|
||||
# no active pipeline or slash-command route invokes it.
|
||||
# Buildkite hands jobs to free agents in the order they appear here. Golden-gate comes first
|
||||
# because every later merge lane waits for it; the fastcheck lanes follow from longest to
|
||||
# shortest measured runtime, so the longest lane never starts last and stretches the build.
|
||||
steps:
|
||||
- block: "Start Build"
|
||||
blocked_state: "running"
|
||||
prompt: "Approve build?"
|
||||
- label: ":test_tube: Golden-Gate Tests"
|
||||
key: "golden-gate"
|
||||
if: |
|
||||
build.env("TEST_SCOPE") == "full" ||
|
||||
(build.env("TEST_SCOPE") == "merge" &&
|
||||
build.env("MERGE_TEST_PLAN") =~ /,golden-gate,/) ||
|
||||
(build.env("TEST_SCOPE") == "direct" &&
|
||||
(build.env("TEST_TYPE") == "golden_gate" || build.env("TEST_TYPE") == "golden_gate_ci"))
|
||||
command: "/opt/fastvideo-ci-runner/run-ci"
|
||||
timeout_in_minutes: 90
|
||||
env:
|
||||
TEST_TYPE: "golden_gate_ci"
|
||||
retry:
|
||||
automatic:
|
||||
- exit_status: 128
|
||||
limit: 3
|
||||
- exit_status: -1
|
||||
limit: 2
|
||||
agents:
|
||||
queue: "ci-runner"
|
||||
|
||||
- label: "Trigger Tests"
|
||||
command: |
|
||||
echo "Current working directory: $(pwd)"
|
||||
echo "Current branch:"
|
||||
git branch --show-current
|
||||
echo "Full diff:"
|
||||
git diff --name-only $BUILDKITE_PULL_REQUEST_BASE_BRANCH...HEAD
|
||||
plugins:
|
||||
- monorepo-diff#v1.4.0:
|
||||
diff: "git diff --name-only $BUILDKITE_PULL_REQUEST_BASE_BRANCH...HEAD"
|
||||
watch:
|
||||
- path:
|
||||
- "fastvideo/v1/models/encoders/**"
|
||||
- "fastvideo/v1/models/loaders/**"
|
||||
- "fastvideo/v1/tests/encoders/**"
|
||||
config:
|
||||
command: "timeout 30m .buildkite/scripts/pr_test.sh"
|
||||
label: "Encoder Tests"
|
||||
env:
|
||||
- BUILDKITE_CLEAN_CHECKOUT=true
|
||||
- TEST_TYPE=encoder
|
||||
agents:
|
||||
queue: "default"
|
||||
- path:
|
||||
- "fastvideo/v1/models/vaes/**"
|
||||
- "fastvideo/v1/models/loaders/**"
|
||||
- "fastvideo/v1/tests/vaes/**"
|
||||
config:
|
||||
command: "timeout 30m .buildkite/scripts/pr_test.sh"
|
||||
label: "VAE Tests"
|
||||
env:
|
||||
- BUILDKITE_CLEAN_CHECKOUT=true
|
||||
- TEST_TYPE=vae
|
||||
agents:
|
||||
queue: "default"
|
||||
- path:
|
||||
- "fastvideo/v1/models/dits/**"
|
||||
- "fastvideo/v1/models/loaders/**"
|
||||
- "fastvideo/v1/tests/transformers/**"
|
||||
- "fastvideo/v1/layers/**"
|
||||
- "fastvideo/v1/attention/**"
|
||||
config:
|
||||
command: "timeout 30m .buildkite/scripts/pr_test.sh"
|
||||
label: "Transformer Tests"
|
||||
env:
|
||||
- BUILDKITE_CLEAN_CHECKOUT=true
|
||||
- TEST_TYPE=transformer
|
||||
agents:
|
||||
queue: "default"
|
||||
- path: "fastvideo/v1/**/*.py"
|
||||
config:
|
||||
command: "timeout 60m .buildkite/scripts/pr_test.sh"
|
||||
label: "SSIM Tests"
|
||||
env:
|
||||
- BUILDKITE_CLEAN_CHECKOUT=true
|
||||
- TEST_TYPE=ssim
|
||||
agents:
|
||||
queue: "default"
|
||||
- label: ":microscope: Unit Tests"
|
||||
key: "unit"
|
||||
if: |
|
||||
build.env("TEST_SCOPE") == "full" ||
|
||||
(build.env("TEST_SCOPE") == "merge" &&
|
||||
build.env("MERGE_TEST_PLAN") =~ /,unit,/) ||
|
||||
build.env("TEST_SCOPE") == "fastcheck" ||
|
||||
build.env("TEST_SCOPE") == null ||
|
||||
(build.env("TEST_SCOPE") == "direct" &&
|
||||
(build.env("TEST_TYPE") == "unit_test" || build.env("TEST_TYPE") == "unit_test_ci"))
|
||||
command: "/opt/fastvideo-ci-runner/run-unit"
|
||||
timeout_in_minutes: 90
|
||||
env:
|
||||
TEST_TYPE: "unit_test_ci"
|
||||
retry:
|
||||
automatic:
|
||||
- exit_status: 128
|
||||
limit: 3
|
||||
- exit_status: -1
|
||||
limit: 2
|
||||
agents:
|
||||
queue: "ci-runner"
|
||||
|
||||
- label: ":microscope: Kernel Tests"
|
||||
key: "kernel-tests"
|
||||
if: |
|
||||
build.env("TEST_SCOPE") == "full" ||
|
||||
(build.env("TEST_SCOPE") == "merge" &&
|
||||
build.env("MERGE_TEST_PLAN") =~ /,kernel-tests,/) ||
|
||||
build.env("TEST_SCOPE") == "fastcheck" ||
|
||||
build.env("TEST_SCOPE") == null ||
|
||||
(build.env("TEST_SCOPE") == "direct" &&
|
||||
(build.env("TEST_TYPE") == "kernel_tests" || build.env("TEST_TYPE") == "kernel_tests_ci"))
|
||||
command: "/opt/fastvideo-ci-runner/run-ci"
|
||||
timeout_in_minutes: 90
|
||||
env:
|
||||
TEST_TYPE: "kernel_tests_ci"
|
||||
retry:
|
||||
automatic:
|
||||
- exit_status: 128
|
||||
limit: 3
|
||||
- exit_status: -1
|
||||
limit: 2
|
||||
agents:
|
||||
queue: "ci-runner"
|
||||
|
||||
- label: ":microscope: DreamVerse App Tests"
|
||||
key: "dreamverse"
|
||||
if: |
|
||||
build.env("TEST_SCOPE") == "full" ||
|
||||
(build.env("TEST_SCOPE") == "merge" &&
|
||||
build.env("MERGE_TEST_PLAN") =~ /,dreamverse,/) ||
|
||||
build.env("TEST_SCOPE") == "fastcheck" ||
|
||||
build.env("TEST_SCOPE") == null ||
|
||||
(build.env("TEST_SCOPE") == "direct" &&
|
||||
(build.env("TEST_TYPE") == "dreamverse_app" || build.env("TEST_TYPE") == "dreamverse_app_ci"))
|
||||
command: "/opt/fastvideo-ci-runner/run-ci"
|
||||
timeout_in_minutes: 90
|
||||
env:
|
||||
TEST_TYPE: "dreamverse_app_ci"
|
||||
retry:
|
||||
automatic:
|
||||
- exit_status: 128
|
||||
limit: 3
|
||||
- exit_status: -1
|
||||
limit: 2
|
||||
agents:
|
||||
queue: "ci-runner"
|
||||
|
||||
- label: ":microscope: Encoder Tests"
|
||||
key: "encoder"
|
||||
if: |
|
||||
build.env("TEST_SCOPE") == "full" ||
|
||||
(build.env("TEST_SCOPE") == "merge" &&
|
||||
build.env("MERGE_TEST_PLAN") =~ /,encoder,/) ||
|
||||
build.env("TEST_SCOPE") == "fastcheck" ||
|
||||
build.env("TEST_SCOPE") == null ||
|
||||
(build.env("TEST_SCOPE") == "direct" &&
|
||||
(build.env("TEST_TYPE") == "encoder" || build.env("TEST_TYPE") == "encoder_ci"))
|
||||
command: "/opt/fastvideo-ci-runner/run-ci"
|
||||
timeout_in_minutes: 90
|
||||
env:
|
||||
TEST_TYPE: "encoder_ci"
|
||||
retry:
|
||||
automatic:
|
||||
- exit_status: 128
|
||||
limit: 3
|
||||
- exit_status: -1
|
||||
limit: 2
|
||||
agents:
|
||||
queue: "ci-runner"
|
||||
|
||||
- label: ":microscope: VAE Tests"
|
||||
key: "vae"
|
||||
if: |
|
||||
build.env("TEST_SCOPE") == "full" ||
|
||||
(build.env("TEST_SCOPE") == "merge" &&
|
||||
build.env("MERGE_TEST_PLAN") =~ /,vae,/) ||
|
||||
build.env("TEST_SCOPE") == "fastcheck" ||
|
||||
build.env("TEST_SCOPE") == null ||
|
||||
(build.env("TEST_SCOPE") == "direct" &&
|
||||
(build.env("TEST_TYPE") == "vae" || build.env("TEST_TYPE") == "vae_ci"))
|
||||
command: "/opt/fastvideo-ci-runner/run-ci"
|
||||
timeout_in_minutes: 90
|
||||
env:
|
||||
TEST_TYPE: "vae_ci"
|
||||
retry:
|
||||
automatic:
|
||||
- exit_status: 128
|
||||
limit: 3
|
||||
- exit_status: -1
|
||||
limit: 2
|
||||
agents:
|
||||
queue: "ci-runner"
|
||||
|
||||
- label: ":microscope: Transformer Tests"
|
||||
key: "transformer"
|
||||
if: |
|
||||
build.env("TEST_SCOPE") == "full" ||
|
||||
(build.env("TEST_SCOPE") == "merge" &&
|
||||
build.env("MERGE_TEST_PLAN") =~ /,transformer,/) ||
|
||||
build.env("TEST_SCOPE") == "fastcheck" ||
|
||||
build.env("TEST_SCOPE") == null ||
|
||||
(build.env("TEST_SCOPE") == "direct" &&
|
||||
(build.env("TEST_TYPE") == "transformer" || build.env("TEST_TYPE") == "transformer_ci"))
|
||||
command: "/opt/fastvideo-ci-runner/run-ci"
|
||||
timeout_in_minutes: 90
|
||||
env:
|
||||
TEST_TYPE: "transformer_ci"
|
||||
retry:
|
||||
automatic:
|
||||
- exit_status: 128
|
||||
limit: 3
|
||||
- exit_status: -1
|
||||
limit: 2
|
||||
agents:
|
||||
queue: "ci-runner"
|
||||
|
||||
- label: ":bar_chart: SSIM Tests"
|
||||
key: "ssim"
|
||||
depends_on: "golden-gate"
|
||||
if: |
|
||||
build.env("TEST_SCOPE") == "full" ||
|
||||
build.env("TEST_SCOPE") == "scheduled" ||
|
||||
(build.env("TEST_SCOPE") == "merge" &&
|
||||
build.env("MERGE_TEST_PLAN") =~ /,ssim,/) ||
|
||||
(build.env("TEST_SCOPE") == "direct" &&
|
||||
(build.env("TEST_TYPE") == "ssim" || build.env("TEST_TYPE") == "ssim_ci"))
|
||||
command: "/opt/fastvideo-ci-runner/run-ci"
|
||||
timeout_in_minutes: 90
|
||||
concurrency: 1
|
||||
concurrency_group: "fastvideo/slinky/whole-tray"
|
||||
env:
|
||||
TEST_TYPE: "ssim_ci"
|
||||
retry:
|
||||
automatic:
|
||||
- exit_status: 128
|
||||
limit: 3
|
||||
- exit_status: -1
|
||||
limit: 2
|
||||
- exit_status: 1
|
||||
limit: 2
|
||||
agents:
|
||||
queue: "ci-runner"
|
||||
|
||||
- label: ":test_tube: LoRA Inference Tests"
|
||||
key: "lora-inference"
|
||||
depends_on: "golden-gate"
|
||||
if: |
|
||||
build.env("TEST_SCOPE") == "full" ||
|
||||
(build.env("TEST_SCOPE") == "merge" &&
|
||||
build.env("MERGE_TEST_PLAN") =~ /,lora-inference,/) ||
|
||||
(build.env("TEST_SCOPE") == "direct" &&
|
||||
(build.env("TEST_TYPE") == "inference_lora" || build.env("TEST_TYPE") == "inference_lora_ci"))
|
||||
command: "/opt/fastvideo-ci-runner/run-ci"
|
||||
timeout_in_minutes: 90
|
||||
env:
|
||||
TEST_TYPE: "inference_lora_ci"
|
||||
retry:
|
||||
automatic:
|
||||
- exit_status: 128
|
||||
limit: 3
|
||||
- exit_status: -1
|
||||
limit: 2
|
||||
agents:
|
||||
queue: "ci-runner"
|
||||
|
||||
- label: ":test_tube: LoRA Extraction Tests"
|
||||
key: "lora-extraction"
|
||||
depends_on: "golden-gate"
|
||||
if: |
|
||||
build.env("TEST_SCOPE") == "full" ||
|
||||
(build.env("TEST_SCOPE") == "merge" &&
|
||||
build.env("MERGE_TEST_PLAN") =~ /,lora-extraction,/) ||
|
||||
(build.env("TEST_SCOPE") == "direct" &&
|
||||
(build.env("TEST_TYPE") == "lora_extraction" || build.env("TEST_TYPE") == "lora_extraction_ci"))
|
||||
command: "/opt/fastvideo-ci-runner/run-ci"
|
||||
timeout_in_minutes: 90
|
||||
env:
|
||||
TEST_TYPE: "lora_extraction_ci"
|
||||
retry:
|
||||
automatic:
|
||||
- exit_status: 128
|
||||
limit: 3
|
||||
- exit_status: -1
|
||||
limit: 2
|
||||
agents:
|
||||
queue: "ci-runner"
|
||||
|
||||
- label: ":test_tube: Training Tests"
|
||||
key: "training"
|
||||
depends_on: "golden-gate"
|
||||
if: |
|
||||
build.env("TEST_SCOPE") == "full" ||
|
||||
(build.env("TEST_SCOPE") == "merge" &&
|
||||
build.env("MERGE_TEST_PLAN") =~ /,training,/) ||
|
||||
(build.env("TEST_SCOPE") == "direct" &&
|
||||
(build.env("TEST_TYPE") == "training" || build.env("TEST_TYPE") == "training_ci"))
|
||||
command: "/opt/fastvideo-ci-runner/run-ci"
|
||||
timeout_in_minutes: 90
|
||||
concurrency: 1
|
||||
concurrency_group: "fastvideo/slinky/whole-tray"
|
||||
env:
|
||||
TEST_TYPE: "training_ci"
|
||||
retry:
|
||||
automatic:
|
||||
- exit_status: 128
|
||||
limit: 3
|
||||
- exit_status: -1
|
||||
limit: 2
|
||||
agents:
|
||||
queue: "ci-runner"
|
||||
|
||||
- label: ":test_tube: Distillation DMD Tests"
|
||||
key: "distillation"
|
||||
depends_on: "golden-gate"
|
||||
if: |
|
||||
build.env("TEST_SCOPE") == "full" ||
|
||||
(build.env("TEST_SCOPE") == "merge" &&
|
||||
build.env("MERGE_TEST_PLAN") =~ /,distillation,/) ||
|
||||
(build.env("TEST_SCOPE") == "direct" &&
|
||||
(build.env("TEST_TYPE") == "distillation_dmd" || build.env("TEST_TYPE") == "distillation_dmd_ci"))
|
||||
command: "/opt/fastvideo-ci-runner/run-ci"
|
||||
timeout_in_minutes: 90
|
||||
env:
|
||||
TEST_TYPE: "distillation_dmd_ci"
|
||||
retry:
|
||||
automatic:
|
||||
- exit_status: 128
|
||||
limit: 3
|
||||
- exit_status: -1
|
||||
limit: 2
|
||||
agents:
|
||||
queue: "ci-runner"
|
||||
|
||||
- label: ":test_tube: Self-Forcing Tests"
|
||||
key: "self-forcing"
|
||||
depends_on: "golden-gate"
|
||||
if: |
|
||||
build.env("TEST_SCOPE") == "full" ||
|
||||
(build.env("TEST_SCOPE") == "merge" &&
|
||||
build.env("MERGE_TEST_PLAN") =~ /,self-forcing,/) ||
|
||||
(build.env("TEST_SCOPE") == "direct" &&
|
||||
(build.env("TEST_TYPE") == "self_forcing" || build.env("TEST_TYPE") == "self_forcing_ci"))
|
||||
command: "/opt/fastvideo-ci-runner/run-ci"
|
||||
timeout_in_minutes: 90
|
||||
env:
|
||||
TEST_TYPE: "self_forcing_ci"
|
||||
retry:
|
||||
automatic:
|
||||
- exit_status: 128
|
||||
limit: 3
|
||||
- exit_status: -1
|
||||
limit: 2
|
||||
agents:
|
||||
queue: "ci-runner"
|
||||
|
||||
- label: ":test_tube: LoRA Training Tests"
|
||||
key: "lora-training"
|
||||
depends_on: "golden-gate"
|
||||
if: |
|
||||
build.env("TEST_SCOPE") == "full" ||
|
||||
(build.env("TEST_SCOPE") == "merge" &&
|
||||
build.env("MERGE_TEST_PLAN") =~ /,lora-training,/) ||
|
||||
(build.env("TEST_SCOPE") == "direct" &&
|
||||
(build.env("TEST_TYPE") == "training_lora" || build.env("TEST_TYPE") == "training_lora_ci"))
|
||||
command: "/opt/fastvideo-ci-runner/run-ci"
|
||||
timeout_in_minutes: 90
|
||||
env:
|
||||
TEST_TYPE: "training_lora_ci"
|
||||
retry:
|
||||
automatic:
|
||||
- exit_status: 128
|
||||
limit: 3
|
||||
- exit_status: -1
|
||||
limit: 2
|
||||
- exit_status: 1
|
||||
limit: 2
|
||||
agents:
|
||||
queue: "ci-runner"
|
||||
|
||||
- label: ":test_tube: Training Tests VSA"
|
||||
key: "training-vsa"
|
||||
depends_on: "golden-gate"
|
||||
if: |
|
||||
build.env("TEST_SCOPE") == "full" ||
|
||||
(build.env("TEST_SCOPE") == "merge" &&
|
||||
build.env("MERGE_TEST_PLAN") =~ /,training-vsa,/) ||
|
||||
(build.env("TEST_SCOPE") == "direct" &&
|
||||
(build.env("TEST_TYPE") == "training_vsa" || build.env("TEST_TYPE") == "training_vsa_ci"))
|
||||
command: "/opt/fastvideo-ci-runner/run-ci"
|
||||
timeout_in_minutes: 90
|
||||
env:
|
||||
TEST_TYPE: "training_vsa_ci"
|
||||
retry:
|
||||
automatic:
|
||||
- exit_status: 128
|
||||
limit: 3
|
||||
- exit_status: -1
|
||||
limit: 2
|
||||
- exit_status: 1
|
||||
limit: 2
|
||||
agents:
|
||||
queue: "ci-runner"
|
||||
|
||||
- label: ":test_tube: Inference Tests VMoBA"
|
||||
key: "inference-vmoba"
|
||||
depends_on: "golden-gate"
|
||||
if: |
|
||||
build.env("TEST_SCOPE") == "full" ||
|
||||
(build.env("TEST_SCOPE") == "merge" &&
|
||||
build.env("MERGE_TEST_PLAN") =~ /,inference-vmoba,/) ||
|
||||
(build.env("TEST_SCOPE") == "direct" &&
|
||||
(build.env("TEST_TYPE") == "inference_vmoba" || build.env("TEST_TYPE") == "inference_vmoba_ci"))
|
||||
command: "/opt/fastvideo-ci-runner/run-ci"
|
||||
timeout_in_minutes: 90
|
||||
env:
|
||||
TEST_TYPE: "inference_vmoba_ci"
|
||||
retry:
|
||||
automatic:
|
||||
- exit_status: 128
|
||||
limit: 3
|
||||
- exit_status: -1
|
||||
limit: 2
|
||||
agents:
|
||||
queue: "ci-runner"
|
||||
|
||||
- label: ":test_tube: Performance Tests"
|
||||
key: "performance"
|
||||
depends_on: "golden-gate"
|
||||
if: |
|
||||
build.env("TEST_SCOPE") == "full" ||
|
||||
(build.env("TEST_SCOPE") == "merge" &&
|
||||
build.env("MERGE_TEST_PLAN") =~ /,performance,/) ||
|
||||
(build.env("TEST_SCOPE") == "direct" &&
|
||||
(build.env("TEST_TYPE") == "performance" || build.env("TEST_TYPE") == "performance_ci"))
|
||||
command: "/opt/fastvideo-ci-runner/run-ci"
|
||||
timeout_in_minutes: 90
|
||||
env:
|
||||
TEST_TYPE: "performance_ci"
|
||||
retry:
|
||||
automatic:
|
||||
- exit_status: 128
|
||||
limit: 3
|
||||
- exit_status: -1
|
||||
limit: 2
|
||||
agents:
|
||||
queue: "ci-runner"
|
||||
|
||||
- label: ":test_tube: API Server Tests"
|
||||
key: "api-server"
|
||||
depends_on: "golden-gate"
|
||||
if: |
|
||||
build.env("TEST_SCOPE") == "full" ||
|
||||
(build.env("TEST_SCOPE") == "merge" &&
|
||||
build.env("MERGE_TEST_PLAN") =~ /,api-server,/) ||
|
||||
(build.env("TEST_SCOPE") == "direct" &&
|
||||
(build.env("TEST_TYPE") == "api_server" || build.env("TEST_TYPE") == "api_server_ci"))
|
||||
command: "/opt/fastvideo-ci-runner/run-ci"
|
||||
timeout_in_minutes: 90
|
||||
env:
|
||||
TEST_TYPE: "api_server_ci"
|
||||
retry:
|
||||
automatic:
|
||||
- exit_status: 128
|
||||
limit: 3
|
||||
- exit_status: -1
|
||||
limit: 2
|
||||
agents:
|
||||
queue: "ci-runner"
|
||||
|
||||
- label: ":test_tube: Train Framework Tests"
|
||||
key: "train-framework"
|
||||
depends_on: "golden-gate"
|
||||
if: |
|
||||
build.env("TEST_SCOPE") == "full" ||
|
||||
(build.env("TEST_SCOPE") == "merge" &&
|
||||
build.env("MERGE_TEST_PLAN") =~ /,train-framework,/) ||
|
||||
(build.env("TEST_SCOPE") == "direct" &&
|
||||
(build.env("TEST_TYPE") == "train_framework" || build.env("TEST_TYPE") == "train_framework_ci"))
|
||||
command: "/opt/fastvideo-ci-runner/run-ci"
|
||||
timeout_in_minutes: 90
|
||||
env:
|
||||
TEST_TYPE: "train_framework_ci"
|
||||
retry:
|
||||
automatic:
|
||||
- exit_status: 128
|
||||
limit: 3
|
||||
- exit_status: -1
|
||||
limit: 2
|
||||
agents:
|
||||
queue: "ci-runner"
|
||||
|
||||
- label: ":test_tube: Eval Metrics Tests"
|
||||
key: "eval"
|
||||
depends_on: "golden-gate"
|
||||
if: |
|
||||
build.env("TEST_SCOPE") == "full" ||
|
||||
(build.env("TEST_SCOPE") == "merge" &&
|
||||
build.env("MERGE_TEST_PLAN") =~ /,eval,/) ||
|
||||
(build.env("TEST_SCOPE") == "direct" &&
|
||||
(build.env("TEST_TYPE") == "eval" || build.env("TEST_TYPE") == "eval_ci"))
|
||||
command: "/opt/fastvideo-ci-runner/run-ci"
|
||||
timeout_in_minutes: 90
|
||||
env:
|
||||
TEST_TYPE: "eval_ci"
|
||||
retry:
|
||||
automatic:
|
||||
- exit_status: 128
|
||||
limit: 3
|
||||
- exit_status: -1
|
||||
limit: 2
|
||||
agents:
|
||||
queue: "ci-runner"
|
||||
|
||||
Executable
+5
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
# Canonical Slurm CI selection for the OpenAI-compatible API lane.
|
||||
set -euo pipefail
|
||||
|
||||
exec pytest ./fastvideo/tests/entrypoints/test_openai_api_integration.py -vs
|
||||
Executable
+5
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
# Canonical Slurm CI selection for the distillation-DMD lane.
|
||||
set -euo pipefail
|
||||
|
||||
exec pytest ./fastvideo/tests/training/distill/test_distill_dmd.py -vs
|
||||
Executable
+87
@@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env bash
|
||||
# DreamVerse needs a GPU for import-time device resolution, but it does not
|
||||
# build or exercise fastvideo-kernel. A checksummed Node archive is installed
|
||||
# in the disposable Slurm container because the shared CI image is
|
||||
# Python/CUDA focused.
|
||||
set -euo pipefail
|
||||
|
||||
node_version=v22.23.2
|
||||
case $(uname -m) in
|
||||
aarch64 | arm64)
|
||||
node_arch=arm64
|
||||
node_archive_sha256=013b59cfd2819703a6f4a14ab891fc46fc2a4e3f5bcd92de3fb4929b43e35b30
|
||||
;;
|
||||
x86_64 | amd64)
|
||||
node_arch=x64
|
||||
node_archive_sha256=b294a556e639d64338823920e5866c21c02741742d2e1529ee1a225c1ec9252a
|
||||
;;
|
||||
*)
|
||||
echo "Unsupported architecture for DreamVerse Node runtime: $(uname -m)" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
node_archive="node-${node_version}-linux-${node_arch}.tar.gz"
|
||||
node_runtime_root=$(mktemp -d -t fastvideo-node.XXXXXX)
|
||||
node_archive_path="${node_runtime_root}/${node_archive}"
|
||||
node_install_dir="${node_runtime_root}/${node_archive%.tar.gz}"
|
||||
curl --proto '=https' --tlsv1.2 --retry 5 --retry-all-errors \
|
||||
--location --fail --silent --show-error \
|
||||
"https://nodejs.org/dist/${node_version}/${node_archive}" \
|
||||
--output "$node_archive_path"
|
||||
printf '%s %s\n' "$node_archive_sha256" "$node_archive_path" | sha256sum --check --status
|
||||
tar -xzf "$node_archive_path" -C "$node_runtime_root"
|
||||
export PATH="${node_install_dir}/bin:${PATH}"
|
||||
node --version
|
||||
npm --version
|
||||
|
||||
export PYTHONPATH="$(pwd)/apps/dreamverse${PYTHONPATH:+:$PYTHONPATH}"
|
||||
pytest apps/dreamverse/dreamverse/tests -q
|
||||
|
||||
cd apps/dreamverse/web
|
||||
npm ci
|
||||
npm run typecheck
|
||||
npm test
|
||||
machine_arch=$(uname -m)
|
||||
if [[ $machine_arch =~ ^(aarch64|arm64)$ ]]; then
|
||||
npx playwright install --with-deps chromium firefox
|
||||
else
|
||||
npx playwright install --with-deps chromium webkit firefox
|
||||
fi
|
||||
|
||||
master_port=${MASTER_PORT:-7959}
|
||||
BACKEND_PORT=${BACKEND_PORT:-$((master_port + 50))}
|
||||
python -m uvicorn dreamverse.mock_server:app --host 127.0.0.1 --port "$BACKEND_PORT" &
|
||||
mock_server_pid=$!
|
||||
cleanup() {
|
||||
kill "$mock_server_pid" 2>/dev/null || true
|
||||
wait "$mock_server_pid" 2>/dev/null || true
|
||||
}
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
for _ in {1..30}; do
|
||||
curl -fsS "http://127.0.0.1:$BACKEND_PORT/healthz" && break
|
||||
sleep 1
|
||||
done
|
||||
curl -fsS "http://127.0.0.1:$BACKEND_PORT/healthz"
|
||||
|
||||
if [[ $machine_arch =~ ^(aarch64|arm64)$ ]]; then
|
||||
# Playwright WebKit traps before opening a page on Linux ARM64, and its
|
||||
# bundled Chromium lacks the H.264/AAC codecs used by the fMP4 assertions.
|
||||
# Firefox covers every flow, including streaming. Chromium and its mobile
|
||||
# profile still cover all codec-independent UI behavior on GB200.
|
||||
BACKEND_HOST=127.0.0.1 BACKEND_PORT="$BACKEND_PORT" CI=1 \
|
||||
npm run e2e -- --project=firefox
|
||||
BACKEND_HOST=127.0.0.1 BACKEND_PORT="$BACKEND_PORT" CI=1 \
|
||||
npm run e2e -- \
|
||||
--project=chromium \
|
||||
--project=mobile-chromium \
|
||||
--grep-invert='streams, plays, and surfaces a downloadable clip|starts a new project and switches back to the prior session|saved projects persist across a page reload'
|
||||
else
|
||||
BACKEND_HOST=127.0.0.1 BACKEND_PORT="$BACKEND_PORT" CI=1 \
|
||||
npm run e2e -- \
|
||||
--project=chromium \
|
||||
--project=webkit \
|
||||
--project=firefox \
|
||||
--project=mobile-safari \
|
||||
--project=mobile-chromium
|
||||
fi
|
||||
Executable
+5
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
# Canonical Slurm CI selection for the encoder lane.
|
||||
set -euo pipefail
|
||||
|
||||
exec pytest ./fastvideo/tests/encoders -vs
|
||||
Executable
+5
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
# Canonical Slurm CI selection for the evaluation lane.
|
||||
set -euo pipefail
|
||||
|
||||
exec pytest ./fastvideo/tests/eval -vs
|
||||
Executable
+35
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env bash
|
||||
# Canonical Slurm CI selection for the golden-gate lane. Environment (HF_HOME
|
||||
# and authentication) is the runner's responsibility.
|
||||
set -euo pipefail
|
||||
|
||||
golden_root=./fastvideo/tests/golden_gate
|
||||
selected=${FASTVIDEO_GOLDEN_TEST_FILES-}
|
||||
if [ -z "$selected" ]; then
|
||||
if [ "${TEST_SCOPE:-}" = merge ]; then
|
||||
echo "Missing FASTVIDEO_GOLDEN_TEST_FILES for merge scope" >&2
|
||||
exit 2
|
||||
fi
|
||||
selected=all
|
||||
fi
|
||||
if [ "$selected" = all ]; then
|
||||
exec pytest "$golden_root" -xvs
|
||||
fi
|
||||
|
||||
[[ $selected =~ ^test_[a-z0-9_]+\.py(,test_[a-z0-9_]+\.py)*$ ]] || {
|
||||
echo "Invalid FASTVIDEO_GOLDEN_TEST_FILES selection" >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
IFS=, read -r -a golden_files <<< "$selected"
|
||||
golden_paths=()
|
||||
for golden_file in "${golden_files[@]}"; do
|
||||
golden_path="$golden_root/$golden_file"
|
||||
[ -f "$golden_path" ] || {
|
||||
echo "Selected golden test does not exist: $golden_file" >&2
|
||||
exit 2
|
||||
}
|
||||
golden_paths+=("$golden_path")
|
||||
done
|
||||
|
||||
exec pytest "${golden_paths[@]}" -xvs
|
||||
Executable
+5
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
# Canonical Slurm CI selection for the LoRA-inference lane.
|
||||
set -euo pipefail
|
||||
|
||||
exec pytest ./fastvideo/tests/inference/lora/test_lora_inference_similarity.py -vs
|
||||
Executable
+5
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
# Canonical Slurm CI selection for the VMoBA-inference lane.
|
||||
set -euo pipefail
|
||||
|
||||
exec python fastvideo/tests/inference/vmoba/test_vmoba_inference.py
|
||||
Executable
+5
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
# Canonical Slurm CI selection for the custom-kernel lane.
|
||||
set -euo pipefail
|
||||
|
||||
exec pytest fastvideo-kernel/tests/ -vs
|
||||
Executable
+5
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
# Canonical Slurm CI selection for the LoRA-extraction lane.
|
||||
set -euo pipefail
|
||||
|
||||
exec pytest ./fastvideo/tests/lora_extraction/ -vs
|
||||
Executable
+64
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env bash
|
||||
# Canonical Slurm performance lane. Reports are written outside the checkout
|
||||
# so the trusted host driver can upload them after untrusted code exits.
|
||||
set -uo pipefail
|
||||
|
||||
export PERFORMANCE_TRACKING_ROOT=/tmp/perf-tracking
|
||||
export PERF_REPORTS_DIR=/workspace/artifacts/performance
|
||||
mkdir -p "$PERF_REPORTS_DIR"
|
||||
|
||||
if [[ ${BUILDKITE_PULL_REQUEST:-false} =~ ^[1-9][0-9]*$ ]]; then
|
||||
export PERF_RUN_SOURCE=pr
|
||||
export PERF_UPLOAD_POLICY=pass
|
||||
elif [ "${BUILDKITE_BRANCH:-}" = main ] \
|
||||
&& { [ "${BUILDKITE_SOURCE:-}" = schedule ] || [ "${TEST_SCOPE:-}" = full ]; }; then
|
||||
export PERF_RUN_SOURCE=scheduled_main
|
||||
export PERF_UPLOAD_POLICY=always
|
||||
elif [ "${TEST_SCOPE:-}" = direct ]; then
|
||||
export PERF_RUN_SOURCE=unknown
|
||||
export PERF_UPLOAD_POLICY=pass
|
||||
else
|
||||
export PERF_RUN_SOURCE=unknown
|
||||
export PERF_UPLOAD_POLICY=never
|
||||
fi
|
||||
|
||||
nvidia-smi \
|
||||
--query-gpu=index,timestamp,clocks.sm,clocks.max.sm,power.draw,power.limit,temperature.gpu \
|
||||
--format=csv -l 10 > "$PERF_REPORTS_DIR/gpu_telemetry.csv" 2>/dev/null &
|
||||
telemetry_pid=$!
|
||||
cleanup() {
|
||||
kill "$telemetry_pid" 2>/dev/null || true
|
||||
wait "$telemetry_pid" 2>/dev/null || true
|
||||
}
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
pytest ./fastvideo/tests/performance/test_inference_performance.py -vs
|
||||
pytest_rc=$?
|
||||
compare_rc=0
|
||||
if [ "$pytest_rc" -eq 0 ] || [ "$PERF_UPLOAD_POLICY" = always ]; then
|
||||
PERF_PYTEST_RC=$pytest_rc python ./fastvideo/tests/performance/compare_baseline.py
|
||||
compare_rc=$?
|
||||
fi
|
||||
python ./fastvideo/tests/performance/dashboard.py || true
|
||||
cp -f fastvideo/tests/performance/results/*.json "$PERF_REPORTS_DIR/" 2>/dev/null || true
|
||||
# The trusted host relays only .md/.html/.json/.csv from PERF_REPORTS_DIR, so
|
||||
# mirror each captured worker log with an allowlisted extension.
|
||||
for worker_log in fastvideo/tests/performance/results/worker_logs/*.log; do
|
||||
[ -f "$worker_log" ] || continue
|
||||
base=$(basename "${worker_log%.log}")
|
||||
# WorkerLogCapture keeps a .log.1 backup after rollover, and read_log_tail
|
||||
# includes it; mirror that retained history too so the artifact is complete.
|
||||
if [ -f "$worker_log.1" ]; then
|
||||
cp -f "$worker_log.1" "$PERF_REPORTS_DIR/${base}.1.md" 2>/dev/null || true
|
||||
fi
|
||||
cp -f "$worker_log" "$PERF_REPORTS_DIR/${base}.md" 2>/dev/null || true
|
||||
done
|
||||
|
||||
echo "--- GPU telemetry (clocks.sm vs clocks.max.sm reveals capped hosts) ---"
|
||||
cat "$PERF_REPORTS_DIR/gpu_telemetry.csv" || true
|
||||
|
||||
final_rc=$pytest_rc
|
||||
if [ "$final_rc" -eq 0 ]; then
|
||||
final_rc=$compare_rc
|
||||
fi
|
||||
exit "$final_rc"
|
||||
Executable
+6
@@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
# Canonical Slurm CI selection for the self-forcing lane.
|
||||
set -euo pipefail
|
||||
|
||||
export WANDB_MODE=offline
|
||||
exec pytest ./fastvideo/tests/training/self-forcing/test_self_forcing.py -vs
|
||||
Executable
+40
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env bash
|
||||
# Canonical four-GPU SSIM lane for the Slinky Slurm worker.
|
||||
set -euo pipefail
|
||||
|
||||
args=()
|
||||
if [ "${FASTVIDEO_SSIM_BOOTSTRAP_MODE:-0}" = 1 ]; then
|
||||
args+=(--bootstrap-mode)
|
||||
fi
|
||||
selected=${FASTVIDEO_SSIM_TEST_FILES-}
|
||||
if [ -z "$selected" ]; then
|
||||
if [ "${TEST_SCOPE:-}" = merge ]; then
|
||||
echo "Missing FASTVIDEO_SSIM_TEST_FILES for merge scope" >&2
|
||||
exit 2
|
||||
fi
|
||||
selected=all
|
||||
fi
|
||||
if [ "$selected" != all ]; then
|
||||
[[ $selected =~ ^test_[a-z0-9_]+\.py(,test_[a-z0-9_]+\.py)*$ ]] || {
|
||||
echo "Invalid FASTVIDEO_SSIM_TEST_FILES selection" >&2
|
||||
exit 2
|
||||
}
|
||||
IFS=, read -r -a ssim_files <<< "$selected"
|
||||
for ssim_file in "${ssim_files[@]}"; do
|
||||
args+=(--test-file "$ssim_file")
|
||||
done
|
||||
fi
|
||||
|
||||
# MoGe's utils3d dependency builds glcontext from source on ARM64. The current
|
||||
# runner image predates the baked-in X11 headers below, so keep this guarded
|
||||
# bootstrap until every deployed image digest contains libx11-dev.
|
||||
if [ ! -f /usr/include/X11/Xlib.h ]; then
|
||||
apt-get -o Acquire::Retries=5 update
|
||||
apt-get -o Acquire::Retries=5 install -y --no-install-recommends libx11-dev
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
fi
|
||||
|
||||
uv pip install git+https://github.com/microsoft/MoGe.git
|
||||
uv pip install k_diffusion einops_exts alias_free_torch torchsde
|
||||
|
||||
exec python fastvideo/tests/ssim/ci_runner.py "${args[@]}"
|
||||
Executable
+5
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
# Canonical Slurm CI selection for the modular training-framework lane.
|
||||
set -euo pipefail
|
||||
|
||||
exec pytest ./fastvideo/tests/train/models ./fastvideo/tests/train/methods -vs
|
||||
Executable
+6
@@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
# Canonical Slurm CI selection for the legacy vanilla-training lane.
|
||||
set -euo pipefail
|
||||
|
||||
export WANDB_MODE=offline
|
||||
exec pytest ./fastvideo/tests/training/Vanilla -srP
|
||||
Executable
+6
@@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
# Canonical Slurm CI selection for the legacy LoRA-training lane.
|
||||
set -euo pipefail
|
||||
|
||||
export WANDB_MODE=offline
|
||||
exec pytest ./fastvideo/tests/training/lora/test_lora_training.py -srP
|
||||
Executable
+6
@@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
# Canonical Slurm CI selection for the legacy VSA-training lane.
|
||||
set -euo pipefail
|
||||
|
||||
export WANDB_MODE=offline
|
||||
exec pytest ./fastvideo/tests/training/VSA -srP
|
||||
Executable
+9
@@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
# Canonical Slurm CI selection for the transformer lane.
|
||||
set -euo pipefail
|
||||
|
||||
# The existing block reference records an absent FASTVIDEO_FA4 (FA2). Keep
|
||||
# that reference identity; the component lane also selects FA2 explicitly.
|
||||
env -u FASTVIDEO_FA4 pytest ./fastvideo/tests/golden_gate/test_wan_t2v.py -xvs
|
||||
pytest ./fastvideo/tests/golden_gate/test_wan_causal.py -xvs
|
||||
exec pytest ./fastvideo/tests/transformers -vs
|
||||
Executable
+6
@@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
# Canonical Slurm CI selection for the VAE lane.
|
||||
set -euo pipefail
|
||||
|
||||
pytest ./fastvideo/tests/golden_gate/test_wan_vae.py -xvs
|
||||
exec pytest ./fastvideo/tests/vaes -vs
|
||||
@@ -1,6 +1,19 @@
|
||||
#!/bin/bash
|
||||
set -uo pipefail
|
||||
|
||||
# DORMANT ROLLBACK ONLY. Active CI is Slurm-only and pipeline.yml never calls
|
||||
# this launcher. Refuse every Buildkite invocation even if a stale step or
|
||||
# operator typo reaches this file; local rollback experiments require an
|
||||
# explicit opt-in.
|
||||
if [ -n "${BUILDKITE:-}" ]; then
|
||||
echo "Legacy Modal CI is disabled; use the Slinky Slurm runner." >&2
|
||||
exit 2
|
||||
fi
|
||||
if [ "${FASTVIDEO_ENABLE_LEGACY_MODAL_CI:-0}" != 1 ]; then
|
||||
echo "Legacy Modal CI is dormant. Set FASTVIDEO_ENABLE_LEGACY_MODAL_CI=1 only for a manual rollback test." >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
log() {
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
|
||||
}
|
||||
@@ -15,8 +28,21 @@ log "Project root: $PROJECT_ROOT"
|
||||
# Install Modal if not available
|
||||
if ! python3 -m modal --version &> /dev/null; then
|
||||
log "Modal not found, installing..."
|
||||
python3 -m pip install modal
|
||||
|
||||
if ! command -v uv &> /dev/null; then
|
||||
log "uv not found, bootstrapping..."
|
||||
if ! curl -LsSf https://astral.sh/uv/install.sh | sh; then
|
||||
log "Error: Failed to bootstrap uv via astral.sh installer."
|
||||
exit 1
|
||||
fi
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
if ! command -v uv &> /dev/null; then
|
||||
log "Error: uv still not on PATH after bootstrap."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
# --break-system-packages preserves prior `pip install --user` semantics on PEP 668 agents.
|
||||
uv pip install --system --break-system-packages modal
|
||||
|
||||
# Verify installation
|
||||
if ! python3 -m modal --version &> /dev/null; then
|
||||
log "Error: Failed to install modal. Please install it manually."
|
||||
@@ -31,6 +57,10 @@ log "Setting up Modal authentication from Buildkite secrets..."
|
||||
MODAL_TOKEN_ID=$(buildkite-agent secret get modal_token_id)
|
||||
MODAL_TOKEN_SECRET=$(buildkite-agent secret get modal_token_secret)
|
||||
|
||||
# Retrieve other secrets
|
||||
WANDB_API_KEY=$(buildkite-agent secret get wandb_api_key)
|
||||
HF_API_KEY=$(buildkite-agent secret get hf_api_key)
|
||||
|
||||
if [ -n "$MODAL_TOKEN_ID" ] && [ -n "$MODAL_TOKEN_SECRET" ]; then
|
||||
log "Retrieved Modal credentials from Buildkite secrets"
|
||||
python3 -m modal token set --token-id "$MODAL_TOKEN_ID" --token-secret "$MODAL_TOKEN_SECRET" --profile buildkite-ci --activate --verify
|
||||
@@ -46,7 +76,8 @@ else
|
||||
exit 1
|
||||
fi
|
||||
|
||||
MODAL_TEST_FILE="fastvideo/v1/tests/modal/pr_test.py"
|
||||
MODAL_TEST_FILE="fastvideo/tests/modal/pr_test.py"
|
||||
MODAL_SSIM_TEST_FILE="fastvideo/tests/modal/ssim_test.py"
|
||||
|
||||
if [ -z "${TEST_TYPE:-}" ]; then
|
||||
log "Error: TEST_TYPE environment variable is not set"
|
||||
@@ -54,22 +85,195 @@ if [ -z "${TEST_TYPE:-}" ]; then
|
||||
fi
|
||||
log "Test type: $TEST_TYPE"
|
||||
|
||||
EFFECTIVE_PR=${BUILDKITE_PULL_REQUEST:-false}
|
||||
if [ "$EFFECTIVE_PR" = "false" ] && [ -n "${PR_NUMBER:-}" ]; then
|
||||
EFFECTIVE_PR=$PR_NUMBER
|
||||
fi
|
||||
MODAL_ENV="BUILDKITE_REPO=$BUILDKITE_REPO BUILDKITE_COMMIT=$BUILDKITE_COMMIT BUILDKITE_PULL_REQUEST=$EFFECTIVE_PR BUILDKITE_BRANCH=${BUILDKITE_BRANCH:-} BUILDKITE_SOURCE=${BUILDKITE_SOURCE:-} TEST_SCOPE=${TEST_SCOPE:-} BUILDKITE_BUILD_URL=${BUILDKITE_BUILD_URL:-} BUILDKITE_BUILD_ID=${BUILDKITE_BUILD_ID:-} BUILDKITE_JOB_ID=${BUILDKITE_JOB_ID:-} IMAGE_VERSION=$IMAGE_VERSION"
|
||||
|
||||
POST_RUN_HOOK=""
|
||||
|
||||
is_truthy() {
|
||||
case "${1:-}" in
|
||||
1|true|TRUE|yes|YES|on|ON) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
ssim_bootstrap_args() {
|
||||
local title="${PR_TITLE:-}"
|
||||
local message="${BUILDKITE_MESSAGE:-}"
|
||||
if is_truthy "${FASTVIDEO_SSIM_BOOTSTRAP_MODE:-}" \
|
||||
|| [[ "$title" == *"[new-model]"* ]] \
|
||||
|| [[ "$message" == *"[new-model]"* ]]; then
|
||||
printf ' --bootstrap-mode'
|
||||
fi
|
||||
}
|
||||
|
||||
upload_performance_artifacts() {
|
||||
SHORT_SHA=${BUILDKITE_COMMIT:0:7}
|
||||
LOCAL_DIR="downloaded_reports"
|
||||
|
||||
_download_reports() {
|
||||
log "Downloading perf_reports/ from Modal Volume..."
|
||||
mkdir -p "$LOCAL_DIR"
|
||||
if ! modal volume get hf-model-weights "perf_reports/" "$LOCAL_DIR"; then
|
||||
log "Error: Failed to download perf_reports/ from Modal Volume."
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
_upload_dashboard() {
|
||||
local target
|
||||
target=$(find "$LOCAL_DIR" -name "dashboard_${SHORT_SHA}_*" | head -n 1)
|
||||
log "TARGET dashboard: '$target'"
|
||||
|
||||
if [ -n "$target" ]; then
|
||||
log "Found dashboard: $target. Uploading to Buildkite..."
|
||||
buildkite-agent artifact upload "$target"
|
||||
buildkite-agent annotate --style info --context "perf-dashboard" < "$target"
|
||||
else
|
||||
log "Warning: Could not find a dashboard file matching $SHORT_SHA"
|
||||
fi
|
||||
}
|
||||
|
||||
_upload_perf_summary() {
|
||||
local target
|
||||
target=$(find "$LOCAL_DIR" -name "perf_${SHORT_SHA}_*" | head -n 1)
|
||||
log "TARGET perf summary: '$target'"
|
||||
|
||||
if [ -n "$target" ]; then
|
||||
log "Found perf summary: $target. Uploading to Buildkite..."
|
||||
buildkite-agent artifact upload "$target"
|
||||
buildkite-agent annotate --style info --context "perf-summary" < "$target"
|
||||
else
|
||||
log "Warning: Could not find a perf summary file matching $SHORT_SHA"
|
||||
fi
|
||||
}
|
||||
|
||||
_upload_normalized_perf_results() {
|
||||
local found=0
|
||||
while IFS= read -r -d '' target; do
|
||||
found=1
|
||||
log "Found normalized performance result: $target. Uploading to Buildkite..."
|
||||
buildkite-agent artifact upload "$target"
|
||||
done < <(find "$LOCAL_DIR" -path "*/results/normalized_perf_*.json" -print0)
|
||||
|
||||
if [ "$found" -eq 0 ]; then
|
||||
log "No normalized performance result artifacts found. This is expected when the rolling performance comparison did not run."
|
||||
fi
|
||||
}
|
||||
|
||||
_cleanup_modal_volume() {
|
||||
log "Cleaning up perf_reports/ from Modal Volume..."
|
||||
if modal volume rm hf-model-weights "perf_reports/" --recursive; then
|
||||
log "Successfully deleted perf_reports/ from Modal Volume."
|
||||
else
|
||||
log "Warning: Failed to delete perf_reports/ from Modal Volume. Manual cleanup may be required."
|
||||
fi
|
||||
}
|
||||
|
||||
_cleanup_local() {
|
||||
log "Cleaning up local download directory..."
|
||||
rm -rf "$LOCAL_DIR"
|
||||
}
|
||||
|
||||
# --- Main flow ---
|
||||
_download_reports || { _cleanup_local; return 1; }
|
||||
_upload_dashboard
|
||||
_upload_perf_summary
|
||||
_upload_normalized_perf_results
|
||||
_cleanup_modal_volume
|
||||
_cleanup_local
|
||||
}
|
||||
|
||||
case "$TEST_TYPE" in
|
||||
"encoder")
|
||||
log "Running encoder tests..."
|
||||
MODAL_COMMAND="python3 -m modal run $MODAL_TEST_FILE::run_encoder_tests"
|
||||
MODAL_COMMAND="$MODAL_ENV HF_API_KEY=$HF_API_KEY python3 -m modal run $MODAL_TEST_FILE::run_encoder_tests"
|
||||
;;
|
||||
"vae")
|
||||
log "Running VAE tests..."
|
||||
MODAL_COMMAND="python3 -m modal run $MODAL_TEST_FILE::run_vae_tests"
|
||||
MODAL_COMMAND="$MODAL_ENV HF_API_KEY=$HF_API_KEY python3 -m modal run $MODAL_TEST_FILE::run_vae_tests"
|
||||
;;
|
||||
"transformer")
|
||||
log "Running transformer tests..."
|
||||
MODAL_COMMAND="python3 -m modal run $MODAL_TEST_FILE::run_transformer_tests"
|
||||
MODAL_COMMAND="$MODAL_ENV HF_API_KEY=$HF_API_KEY python3 -m modal run $MODAL_TEST_FILE::run_transformer_tests"
|
||||
;;
|
||||
"golden_gate")
|
||||
log "Running golden-gate tests..."
|
||||
MODAL_COMMAND="$MODAL_ENV HF_API_KEY=$HF_API_KEY python3 -m modal run $MODAL_TEST_FILE::run_golden_gate_tests"
|
||||
;;
|
||||
"ssim")
|
||||
log "Running SSIM tests..."
|
||||
MODAL_COMMAND="python3 -m modal run $MODAL_TEST_FILE::run_ssim_tests"
|
||||
SSIM_BOOTSTRAP_ARGS=$(ssim_bootstrap_args)
|
||||
if [ -n "$SSIM_BOOTSTRAP_ARGS" ]; then
|
||||
log "SSIM bootstrap mode enabled for new-model reference draft generation"
|
||||
fi
|
||||
MODAL_COMMAND="$MODAL_ENV HF_API_KEY=$HF_API_KEY python3 -m modal run "
|
||||
MODAL_COMMAND+="$MODAL_SSIM_TEST_FILE::run_ssim_tests$SSIM_BOOTSTRAP_ARGS"
|
||||
;;
|
||||
"training")
|
||||
log "Running training tests..."
|
||||
MODAL_COMMAND="$MODAL_ENV WANDB_API_KEY=$WANDB_API_KEY python3 -m modal run $MODAL_TEST_FILE::run_training_tests"
|
||||
;;
|
||||
"training_lora")
|
||||
log "Running LoRA training tests..."
|
||||
MODAL_COMMAND="$MODAL_ENV WANDB_API_KEY=$WANDB_API_KEY python3 -m modal run $MODAL_TEST_FILE::run_training_lora_tests"
|
||||
;;
|
||||
"training_vsa")
|
||||
log "Running training VSA tests..."
|
||||
MODAL_COMMAND="$MODAL_ENV WANDB_API_KEY=$WANDB_API_KEY python3 -m modal run $MODAL_TEST_FILE::run_training_tests_VSA"
|
||||
;;
|
||||
"kernel_tests")
|
||||
log "Running kernel tests..."
|
||||
MODAL_COMMAND="$MODAL_ENV python3 -m modal run $MODAL_TEST_FILE::run_kernel_tests"
|
||||
;;
|
||||
"inference_lora")
|
||||
log "Running LoRA tests..."
|
||||
MODAL_COMMAND="$MODAL_ENV python3 -m modal run $MODAL_TEST_FILE::run_inference_lora_tests"
|
||||
;;
|
||||
"distillation_dmd")
|
||||
log "Running distillation DMD tests..."
|
||||
MODAL_COMMAND="$MODAL_ENV WANDB_API_KEY=$WANDB_API_KEY python3 -m modal run $MODAL_TEST_FILE::run_distill_dmd_tests"
|
||||
;;
|
||||
# run_inference_tests_vmoba
|
||||
"self_forcing")
|
||||
log "Running self-forcing tests..."
|
||||
MODAL_COMMAND="$MODAL_ENV WANDB_API_KEY=$WANDB_API_KEY python3 -m modal run $MODAL_TEST_FILE::run_self_forcing_tests"
|
||||
;;
|
||||
"inference_vmoba")
|
||||
log "Running V-MoBA inference tests..."
|
||||
MODAL_COMMAND="$MODAL_ENV python3 -m modal run $MODAL_TEST_FILE::run_inference_tests_vmoba"
|
||||
;;
|
||||
"unit_test")
|
||||
log "Running unit tests..."
|
||||
MODAL_COMMAND="$MODAL_ENV python3 -m modal run $MODAL_TEST_FILE::run_unit_test"
|
||||
;;
|
||||
"dreamverse_app")
|
||||
log "Running DreamVerse app tests..."
|
||||
MODAL_COMMAND="$MODAL_ENV python3 -m modal run $MODAL_TEST_FILE::run_dreamverse_app_tests"
|
||||
;;
|
||||
"train_framework")
|
||||
log "Running fastvideo.train framework tests..."
|
||||
MODAL_COMMAND="$MODAL_ENV HF_API_KEY=$HF_API_KEY python3 -m modal run $MODAL_TEST_FILE::run_train_framework_tests"
|
||||
;;
|
||||
"eval")
|
||||
log "Running eval metric tests..."
|
||||
MODAL_COMMAND="$MODAL_ENV HF_API_KEY=$HF_API_KEY python3 -m modal run $MODAL_TEST_FILE::run_eval_tests"
|
||||
;;
|
||||
"lora_extraction")
|
||||
log "Running LoRA extraction tests..."
|
||||
MODAL_COMMAND="$MODAL_ENV HF_API_KEY=$HF_API_KEY python3 -m modal run $MODAL_TEST_FILE::run_lora_extraction_tests"
|
||||
;;
|
||||
"performance")
|
||||
log "Running performance tests on Modal..."
|
||||
MODAL_COMMAND="$MODAL_ENV HF_API_KEY=$HF_API_KEY python3 -m modal run $MODAL_TEST_FILE::run_performance_tests"
|
||||
POST_RUN_HOOK="upload_performance_artifacts"
|
||||
;;
|
||||
"api_server")
|
||||
log "Running API server integration tests..."
|
||||
MODAL_COMMAND="$MODAL_ENV HF_API_KEY=$HF_API_KEY python3 -m modal run $MODAL_TEST_FILE::run_api_server_tests"
|
||||
;;
|
||||
*)
|
||||
log "Error: Unknown test type: $TEST_TYPE"
|
||||
@@ -87,5 +291,10 @@ else
|
||||
log "Error: Modal test failed with exit code: $TEST_EXIT_CODE"
|
||||
fi
|
||||
|
||||
if [ -n "$POST_RUN_HOOK" ]; then
|
||||
log "Executing post-run hook: $POST_RUN_HOOK"
|
||||
"$POST_RUN_HOOK"
|
||||
fi
|
||||
|
||||
log "=== Test execution completed with exit code: $TEST_EXIT_CODE ==="
|
||||
exit $TEST_EXIT_CODE
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
#!/bin/bash
|
||||
set -uo pipefail
|
||||
|
||||
log() {
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
|
||||
}
|
||||
|
||||
log "=== Starting pre-commit checks ==="
|
||||
|
||||
cd "$(dirname "$0")/../.."
|
||||
PROJECT_ROOT=$(pwd)
|
||||
log "Project root: $PROJECT_ROOT"
|
||||
|
||||
if ! python3 -m pre_commit --version &> /dev/null; then
|
||||
log "pre-commit not found, installing..."
|
||||
if ! command -v uv &> /dev/null; then
|
||||
log "uv not found, bootstrapping..."
|
||||
if ! curl -LsSf https://astral.sh/uv/install.sh | sh; then
|
||||
log "Error: Failed to bootstrap uv via astral.sh installer."
|
||||
exit 1
|
||||
fi
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
if ! command -v uv &> /dev/null; then
|
||||
log "Error: uv still not on PATH after bootstrap."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
# --break-system-packages preserves prior `pip install --user` semantics on PEP 668 agents.
|
||||
uv pip install --system --break-system-packages pre-commit==4.0.1
|
||||
|
||||
if ! python3 -m pre_commit --version &> /dev/null; then
|
||||
log "Error: Failed to install pre-commit."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
log "Pre-commit version: $(python3 -m pre_commit --version)"
|
||||
|
||||
log "Installing/updating pre-commit hooks..."
|
||||
python3 -m pre_commit install --install-hooks
|
||||
|
||||
log "Running pre-commit checks on all files..."
|
||||
python3 -m pre_commit run --all-files
|
||||
PRE_COMMIT_EXIT_CODE=$?
|
||||
|
||||
if [ $PRE_COMMIT_EXIT_CODE -eq 0 ]; then
|
||||
log "Pre-commit checks completed successfully"
|
||||
else
|
||||
log "Error: Pre-commit checks failed with exit code: $PRE_COMMIT_EXIT_CODE"
|
||||
fi
|
||||
|
||||
log "=== Pre-commit checks completed with exit code: $PRE_COMMIT_EXIT_CODE ==="
|
||||
exit $PRE_COMMIT_EXIT_CODE
|
||||
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Collect the whole attention directory so new files cannot land uncovered.
|
||||
# Its FA2/FA3 regression files skip when FA4 is selected (the Modal image
|
||||
# enables FA4 by default), so pin FA4 off for the directory to be real
|
||||
# coverage on every runner rather than a nominal collection.
|
||||
export FASTVIDEO_FA4=0
|
||||
|
||||
# The livestream app's tests are CPU-only; its single gpu-marked module is
|
||||
# deselected, and DreamVerse's GPU tests have their own lane.
|
||||
exec pytest \
|
||||
./apps/infinite_livestream/infinite_livestream/tests \
|
||||
./fastvideo/tests/api/ \
|
||||
./fastvideo/tests/contract/ \
|
||||
./fastvideo/tests/dataset/ \
|
||||
./fastvideo/tests/workflow/ \
|
||||
./fastvideo/tests/entrypoints/ \
|
||||
./fastvideo/tests/loader/ \
|
||||
./fastvideo/tests/pipelines/ \
|
||||
./fastvideo/tests/platforms/ \
|
||||
./fastvideo/tests/schedulers/ \
|
||||
./fastvideo/tests/train/ \
|
||||
./fastvideo/tests/stages/ \
|
||||
./fastvideo/tests/ops/ \
|
||||
./fastvideo/tests/worker/ \
|
||||
./fastvideo/tests/training/test_runner.py \
|
||||
./fastvideo/tests/training/test_trackers.py \
|
||||
./fastvideo/tests/training/test_ltx2_rope_fps.py \
|
||||
./fastvideo/tests/inference/test_basic_fasth3_omniref_pdd.py \
|
||||
./fastvideo/tests/inference/test_inference_regional_compile.py \
|
||||
./fastvideo/tests/attention/ \
|
||||
./fastvideo/tests/layers/test_pdd_linear.py \
|
||||
./fastvideo/tests/layers/test_triton_fused_norm.py \
|
||||
./fastvideo/tests/modal/test_kernel_build_cache.py \
|
||||
./fastvideo/tests/modal/test_pr_test.py \
|
||||
./fastvideo/tests/modal/test_ssim_test.py \
|
||||
--ignore=./fastvideo/tests/entrypoints/test_openai_api_integration.py \
|
||||
--ignore=./fastvideo/tests/train/models \
|
||||
--ignore=./fastvideo/tests/train/methods \
|
||||
-m "not gpu" \
|
||||
-vs
|
||||
@@ -0,0 +1,31 @@
|
||||
# Build-context excludes: keep the context small and the `COPY . .` layer cache
|
||||
# stable. Docker uploads everything here to the daemon and bakes it into a layer;
|
||||
# without this, the 6.5 GB host .venv alone is shipped + cached on every build.
|
||||
#
|
||||
# IMPORTANT: do NOT ignore .git — fastvideo-kernel's build runs
|
||||
# `git submodule update --init --recursive`, which needs the repo metadata.
|
||||
|
||||
# Virtualenvs — the image builds its own /opt/venv
|
||||
.venv/
|
||||
venv/
|
||||
env/
|
||||
|
||||
# Python caches & build/test/lint artifacts
|
||||
**/__pycache__/
|
||||
*.py[cod]
|
||||
*.egg-info/
|
||||
.eggs/
|
||||
.pytest_cache/
|
||||
.mypy_cache/
|
||||
.ruff_cache/
|
||||
.cache/
|
||||
|
||||
# Local run outputs / logs (not needed in the image)
|
||||
outputs/
|
||||
wandb/
|
||||
*.log
|
||||
|
||||
# Editor / OS cruft
|
||||
.DS_Store
|
||||
.idea/
|
||||
.vscode/
|
||||
@@ -23,7 +23,7 @@ body:
|
||||
attributes:
|
||||
label: Environment
|
||||
description: |
|
||||
Please share your environment with us. You can run the command **python fastvideo/utils/collect_env.py** and copy-paste its output below.
|
||||
Please share your environment with us. You can run the command **python collect_env.py** and copy-paste its output below.
|
||||
placeholder: FastVideo version, platform, python version, cuda version...
|
||||
validations:
|
||||
required: true
|
||||
@@ -0,0 +1,56 @@
|
||||
name: 💬 Request for comments (RFC).
|
||||
description: Ask for feedback on major architectural changes or design choices.
|
||||
title: "[RFC]: "
|
||||
labels: ["RFC"]
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: >
|
||||
#### Please take a look at previous [RFCs](https://github.com/hao-ai-lab/FastVideo/issues?q=label%3ARFC+sort%3Aupdated-desc) for reference.
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Motivation.
|
||||
description: >
|
||||
The motivation of the RFC.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Proposed Change.
|
||||
description: >
|
||||
The proposed change of the RFC.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Feedback Period.
|
||||
description: >
|
||||
The feedback period of the RFC. Usually at least one week.
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: CC List.
|
||||
description: >
|
||||
The list of people you want to CC.
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Any Other Things.
|
||||
description: >
|
||||
Any other things you would like to mention.
|
||||
validations:
|
||||
required: false
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: >
|
||||
Thanks for contributing 🎉!
|
||||
- type: checkboxes
|
||||
id: askllm
|
||||
attributes:
|
||||
label: Before submitting a new issue...
|
||||
options:
|
||||
- label: Make sure you already searched for relevant issues.
|
||||
required: true
|
||||
@@ -0,0 +1,70 @@
|
||||
<!--
|
||||
PR TITLE: Must start with a type tag, e.g.:
|
||||
[feat] Add new model [bugfix] Fix VAE tiling [refactor] Restructure pipeline
|
||||
[perf] Optimize kernel [ci] Update tests [docs] Add guide
|
||||
[misc] Cleanup configs [new-model] Port Flux2 [infra] Add trace hooks
|
||||
[skill] Add agent skill
|
||||
|
||||
MERGE WORKFLOW:
|
||||
1. Ensure pre-commit passes and you have at least 1 approval
|
||||
2. Comment /merge (or add the "ready" label) to enter the Merge Queue
|
||||
3. A path-aware merge gate runs only relevant integration tests → auto-merge on success
|
||||
|
||||
ON-DEMAND TESTING (write access required):
|
||||
/test full — Explicit all-lane run /test ssim — Full SSIM regression
|
||||
/test training — Training pipeline /test encoder — Encoder tests
|
||||
/test transformer — Transformer tests /test vae — VAE tests
|
||||
/test kernel — CUDA kernel tests /test unit — Unit tests
|
||||
See docs/contributing/pull_requests.md for all 17 test commands
|
||||
-->
|
||||
|
||||
## Purpose
|
||||
|
||||
<!-- What does this PR do? Link the related issue if applicable. -->
|
||||
|
||||
Fixes #
|
||||
|
||||
## Changes
|
||||
|
||||
<!-- Describe your changes concisely. What approach did you take? -->
|
||||
|
||||
-
|
||||
|
||||
## Test Plan
|
||||
|
||||
<!-- How did you verify your changes? Paste exact commands and output. -->
|
||||
|
||||
```bash
|
||||
# Commands you ran
|
||||
```
|
||||
|
||||
## Test Results
|
||||
|
||||
<!-- Paste test output, before/after comparisons, or SSIM scores for model changes. -->
|
||||
|
||||
<details>
|
||||
<summary>Test output</summary>
|
||||
|
||||
```
|
||||
# Paste output here
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] I ran `pre-commit run --all-files` and fixed all issues
|
||||
- [ ] I added or updated tests for my changes
|
||||
- [ ] I updated documentation if needed
|
||||
- [ ] I considered GPU memory impact of my changes
|
||||
|
||||
**For model/pipeline changes, also check:**
|
||||
- [ ] I verified SSIM regression tests pass
|
||||
- [ ] I updated the support matrix if adding a new model
|
||||
|
||||
**For new models or serving runtimes:**
|
||||
|
||||
- [ ] I added or updated the native serving YAML and [cookbook deployment](https://github.com/hao-ai-lab/FastVideo/blob/main/docs/contributing/cookbook_configuration.md),
|
||||
or explained why this is not applicable.
|
||||
- [ ] I documented the GPU type/count and minimum VRAM for the serving setup with supporting measurements,
|
||||
or marked the minimum as unknown and any VRAM estimate as an estimate.
|
||||
@@ -0,0 +1,250 @@
|
||||
merge_protections:
|
||||
- name: PR merge requirements
|
||||
if:
|
||||
- base = main
|
||||
success_conditions:
|
||||
- "title~=(?i)^\\[(feat|feature|bugfix|fix|refactor|perf|ci|doc|docs|misc|chore|kernel|new.?model|skill|skills|infra)\\]"
|
||||
- "#approved-reviews-by>=1"
|
||||
- check-success~=pre-commit
|
||||
- check-success=fastcheck-passed
|
||||
- check-success=full-suite-passed
|
||||
|
||||
pull_request_rules:
|
||||
|
||||
# ============================================================
|
||||
# Type labels (from PR title prefix)
|
||||
# ============================================================
|
||||
|
||||
- name: "label type: feat"
|
||||
conditions:
|
||||
- "title~=(?i)^\\[(feat|feature)\\]"
|
||||
- -closed
|
||||
actions:
|
||||
label:
|
||||
add: ["type: feat"]
|
||||
|
||||
- name: "label type: bugfix"
|
||||
conditions:
|
||||
- "title~=(?i)^\\[(bug)?fix\\]"
|
||||
- -closed
|
||||
actions:
|
||||
label:
|
||||
add: ["type: bugfix"]
|
||||
|
||||
- name: "label type: refactor"
|
||||
conditions:
|
||||
- "title~=(?i)^\\[refactor\\]"
|
||||
- -closed
|
||||
actions:
|
||||
label:
|
||||
add: ["type: refactor"]
|
||||
|
||||
- name: "label type: perf"
|
||||
conditions:
|
||||
- "title~=(?i)^\\[perf\\]"
|
||||
- -closed
|
||||
actions:
|
||||
label:
|
||||
add: ["type: perf"]
|
||||
|
||||
- name: "label type: ci"
|
||||
conditions:
|
||||
- "title~=(?i)^\\[ci\\]"
|
||||
- -closed
|
||||
actions:
|
||||
label:
|
||||
add: ["type: ci"]
|
||||
|
||||
- name: "label type: docs"
|
||||
conditions:
|
||||
- "title~=(?i)^\\[(doc|docs)\\]"
|
||||
- -closed
|
||||
actions:
|
||||
label:
|
||||
add: ["type: docs"]
|
||||
|
||||
- name: "label type: misc"
|
||||
conditions:
|
||||
- "title~=(?i)^\\[(misc|chore)\\]"
|
||||
- -closed
|
||||
actions:
|
||||
label:
|
||||
add: ["type: misc"]
|
||||
|
||||
- name: "label type: new-model"
|
||||
conditions:
|
||||
- "title~=(?i)^\\[new.?model\\]"
|
||||
- -closed
|
||||
actions:
|
||||
label:
|
||||
add: ["type: new-model"]
|
||||
|
||||
- name: "label type: infra"
|
||||
conditions:
|
||||
- "title~=(?i)^\\[infra\\]"
|
||||
- -closed
|
||||
actions:
|
||||
label:
|
||||
add: ["type: infra"]
|
||||
|
||||
- name: "label type: skill"
|
||||
conditions:
|
||||
- "title~=(?i)^\\[skills?\\]"
|
||||
- -closed
|
||||
actions:
|
||||
label:
|
||||
add: ["type: skill"]
|
||||
|
||||
# ============================================================
|
||||
# Scope labels (from changed files)
|
||||
# ============================================================
|
||||
|
||||
- name: "label scope: training"
|
||||
conditions:
|
||||
- or:
|
||||
- files~=^fastvideo/train/
|
||||
- files~=^fastvideo/training/
|
||||
- files~=^fastvideo/distillation/
|
||||
- files~=^examples/train/
|
||||
- files~=^examples/training/
|
||||
- files~=^examples/distill/
|
||||
- -closed
|
||||
actions:
|
||||
label:
|
||||
add: ["scope: training"]
|
||||
|
||||
- name: "label scope: inference"
|
||||
conditions:
|
||||
- or:
|
||||
- files~=^fastvideo/pipelines/basic/
|
||||
- files~=^fastvideo/pipelines/stages/
|
||||
- files~=^fastvideo/pipelines/samplers/
|
||||
- files~=^fastvideo/entrypoints/
|
||||
- files~=^fastvideo/worker/
|
||||
- files~=^fastvideo/api/sampling_param
|
||||
- files~=^fastvideo/configs/pipelines/
|
||||
- files~=^examples/inference/
|
||||
- -closed
|
||||
actions:
|
||||
label:
|
||||
add: ["scope: inference"]
|
||||
|
||||
- name: "label scope: attention"
|
||||
conditions:
|
||||
- files~=^fastvideo/attention/
|
||||
- -closed
|
||||
actions:
|
||||
label:
|
||||
add: ["scope: attention"]
|
||||
|
||||
- name: "label scope: kernel"
|
||||
conditions:
|
||||
- or:
|
||||
- files~=^fastvideo-kernel/
|
||||
- files~=^csrc/
|
||||
- -closed
|
||||
actions:
|
||||
label:
|
||||
add: ["scope: kernel"]
|
||||
|
||||
- name: "label scope: data"
|
||||
conditions:
|
||||
- or:
|
||||
- files~=^fastvideo/dataset/
|
||||
- files~=^fastvideo/pipelines/preprocess/
|
||||
- files~=^examples/preprocessing/
|
||||
- -closed
|
||||
actions:
|
||||
label:
|
||||
add: ["scope: data"]
|
||||
|
||||
- name: "label scope: infra"
|
||||
conditions:
|
||||
- or:
|
||||
- files~=^\.github/
|
||||
- files~=^\.buildkite/
|
||||
- files~=^fastvideo/tests/
|
||||
- files~=^docker/
|
||||
- -closed
|
||||
actions:
|
||||
label:
|
||||
add: ["scope: infra"]
|
||||
|
||||
- name: "label scope: distributed"
|
||||
conditions:
|
||||
- files~=^fastvideo/distributed/
|
||||
- -closed
|
||||
actions:
|
||||
label:
|
||||
add: ["scope: distributed"]
|
||||
|
||||
- name: "label scope: docs"
|
||||
conditions:
|
||||
- files~=^docs/
|
||||
- -closed
|
||||
actions:
|
||||
label:
|
||||
add: ["scope: docs"]
|
||||
|
||||
- name: "label scope: studio"
|
||||
conditions:
|
||||
- files~=^apps/fastvideo_studio/
|
||||
- -closed
|
||||
actions:
|
||||
label:
|
||||
add: ["scope: studio"]
|
||||
|
||||
- name: "label scope: model"
|
||||
conditions:
|
||||
- or:
|
||||
- files~=^fastvideo/models/
|
||||
- files~=^fastvideo/layers/
|
||||
- files~=^fastvideo/configs/models/
|
||||
- -closed
|
||||
actions:
|
||||
label:
|
||||
add: ["scope: model"]
|
||||
|
||||
# ============================================================
|
||||
# Merge conflict detection
|
||||
# ============================================================
|
||||
|
||||
- name: label conflicting PRs
|
||||
conditions:
|
||||
- conflict
|
||||
- -closed
|
||||
- label!=stale
|
||||
actions:
|
||||
label:
|
||||
add: [needs-rebase]
|
||||
|
||||
- name: remove conflict label when resolved
|
||||
conditions:
|
||||
- -conflict
|
||||
- -closed
|
||||
- label=needs-rebase
|
||||
actions:
|
||||
label:
|
||||
remove: [needs-rebase]
|
||||
|
||||
# ============================================================
|
||||
# Auto-merge
|
||||
# ============================================================
|
||||
|
||||
- name: auto-merge when ready and all checks pass
|
||||
conditions:
|
||||
- label=ready
|
||||
- "title~=(?i)^\\[(feat|feature|bugfix|fix|refactor|perf|ci|doc|docs|misc|chore|kernel|new.?model|skill|skills|infra)\\]"
|
||||
- "#approved-reviews-by>=1"
|
||||
- check-success~=pre-commit
|
||||
- check-success=fastcheck-passed
|
||||
- check-success=full-suite-passed
|
||||
- -conflict
|
||||
- -closed
|
||||
- -draft
|
||||
actions:
|
||||
merge:
|
||||
method: squash
|
||||
|
||||
merge_protections_settings:
|
||||
reporting_method: check-runs
|
||||
Executable
+133
@@ -0,0 +1,133 @@
|
||||
#!/usr/bin/env bash
|
||||
# Gate the path-aware Buildkite merge plan on the cheap GitHub checks.
|
||||
#
|
||||
# Polls the workflow runs for the PR head commit and only exits 0 once the
|
||||
# watched cheap workflows (pre-commit, docs build) have succeeded, so the
|
||||
# 'ready' label cannot burn path-selected GPU lanes on a head that a cheap
|
||||
# check has already doomed.
|
||||
#
|
||||
# Semantics:
|
||||
# - watched run completed with a bad conclusion -> exit 1 (fail CLOSED:
|
||||
# no merge gate; the next push re-arms via the 'synchronize' trigger)
|
||||
# - watched run cancelled -> still pending: the docs
|
||||
# workflow's repo-global 'pages' concurrency group cancels runs superseded
|
||||
# by unrelated pushes, so 'cancelled' is not a verdict on this PR
|
||||
# - watched runs pending -> poll until done
|
||||
# - docs run absent -> not applicable after a
|
||||
# short grace period ('Deploy Documentation' is path-filtered on PRs)
|
||||
# - pre-commit run absent -> keep polling: pre-commit
|
||||
# is never path-filtered, so its absence is always anomalous
|
||||
# - 'ready' label removed while waiting -> exit 1 (fail CLOSED:
|
||||
# un-labeling is a deliberate maintainer action)
|
||||
# - GitHub API unreachable or timeout -> exit 0 (fail OPEN,
|
||||
# loud warning: never brick CI on a GitHub outage)
|
||||
#
|
||||
# Required env: PR_SHA (PR head commit), PR_NUMBER, GITHUB_REPOSITORY, GH_TOKEN.
|
||||
set -euo pipefail
|
||||
|
||||
: "${PR_SHA:?PR_SHA (PR head commit) is required}"
|
||||
: "${PR_NUMBER:?PR_NUMBER (pull request number) is required}"
|
||||
: "${GITHUB_REPOSITORY:?GITHUB_REPOSITORY is required}"
|
||||
|
||||
# Workflow-level `name:` values that must be green before the merge gate
|
||||
# may start. "Deploy Documentation" is path-filtered on PRs, so its run may
|
||||
# legitimately never exist; pre-commit always runs, so it must appear.
|
||||
WATCHED_NAMES='["pre-commit", "Deploy Documentation"]'
|
||||
WATCHED_REGEX='^(pre-commit|Deploy Documentation)$'
|
||||
POLL_SECS="${POLL_SECS:-20}"
|
||||
GRACE_SECS="${GRACE_SECS:-60}"
|
||||
MAX_WAIT_SECS="${MAX_WAIT_SECS:-1500}"
|
||||
|
||||
# Bound each API call so a hung connection hits the 3-strike fail-open path
|
||||
# instead of pinning the loop until the job timeout (which would fail closed
|
||||
# on exactly the GitHub-outage case this script is meant to survive).
|
||||
if command -v timeout >/dev/null 2>&1; then
|
||||
gh_api() { timeout 30 gh api "$@"; }
|
||||
else
|
||||
gh_api() { gh api "$@"; } # macOS dev boxes; CI always has coreutils timeout
|
||||
fi
|
||||
|
||||
# The workflow checked the label before starting the gate, but the wait can
|
||||
# last ~25 min: re-check once before any exit 0 and fail closed if 'ready'
|
||||
# was removed in the meantime. An API error here proceeds (the label was
|
||||
# present when the gate started; never brick CI on an outage).
|
||||
recheck_ready_label() {
|
||||
local pr_json
|
||||
if pr_json=$(gh_api "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}" 2>/dev/null); then
|
||||
if ! jq -e '[.labels[]?.name] | index("ready")' <<<"$pr_json" >/dev/null 2>&1; then
|
||||
echo "::error::PR #${PR_NUMBER} no longer has the 'ready' label —" \
|
||||
"NOT triggering the Buildkite merge gate. Re-add the label to re-arm."
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "::warning::Could not re-check the 'ready' label on PR #${PR_NUMBER}; proceeding (it was present when the gate started)."
|
||||
fi
|
||||
}
|
||||
|
||||
start=$(date +%s)
|
||||
api_fails=0
|
||||
missing=""
|
||||
|
||||
while true; do
|
||||
elapsed=$(( $(date +%s) - start ))
|
||||
|
||||
if runs_json=$(gh_api "repos/${GITHUB_REPOSITORY}/actions/runs?head_sha=${PR_SHA}&per_page=100" 2>/dev/null) \
|
||||
&& state=$(jq --arg re "$WATCHED_REGEX" '
|
||||
[.workflow_runs[]? | select(.name // "" | test($re))]
|
||||
| group_by(.name) | map(max_by(.id))
|
||||
| map({name, status, conclusion})' <<<"$runs_json" 2>/dev/null); then
|
||||
api_fails=0
|
||||
echo "t+${elapsed}s watched checks: $(jq -c . <<<"$state")"
|
||||
|
||||
failed=$(jq -r '[.[] | select(.status == "completed"
|
||||
and (.conclusion | IN("success", "skipped", "neutral", "cancelled") | not))]
|
||||
| map(.name) | join(", ")' <<<"$state")
|
||||
if [ -n "$failed" ]; then
|
||||
echo "::error::Cheap check(s) failed on ${PR_SHA}: ${failed}." \
|
||||
"NOT triggering the Buildkite merge gate. Push a fix (the 'ready'" \
|
||||
"label re-arms on every push), or re-run the failed check and then" \
|
||||
"re-run this workflow."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 'cancelled' counts as pending: wait for a re-run to reach a real verdict
|
||||
# (bounded by MAX_WAIT, then the fail-open below).
|
||||
pending=$(jq '[.[] | select(.status != "completed" or .conclusion == "cancelled")] | length' <<<"$state")
|
||||
missing=$(jq -r --argjson watched "$WATCHED_NAMES" '($watched - map(.name)) | join(", ")' <<<"$state")
|
||||
if [ "$pending" -eq 0 ]; then
|
||||
if [ -z "$missing" ]; then
|
||||
recheck_ready_label
|
||||
echo "All watched cheap checks are green — merge gate may proceed."
|
||||
exit 0
|
||||
fi
|
||||
case "$missing" in
|
||||
*pre-commit*)
|
||||
echo "pre-commit run not found for ${PR_SHA} yet; waiting (pre-commit is never path-filtered, so its absence is anomalous)."
|
||||
;;
|
||||
*)
|
||||
if [ "$elapsed" -ge "$GRACE_SECS" ]; then
|
||||
recheck_ready_label
|
||||
echo "::warning::Watched run(s) never appeared for ${PR_SHA}: ${missing} (path-filtered, likely not applicable). Proceeding on the checks that did run."
|
||||
exit 0
|
||||
fi
|
||||
echo "Waiting up to ${GRACE_SECS}s grace for path-filtered run(s) to appear: ${missing}."
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
else
|
||||
api_fails=$(( api_fails + 1 ))
|
||||
echo "::warning::GitHub API error querying workflow runs for ${PR_SHA} (attempt ${api_fails}/3)."
|
||||
if [ "$api_fails" -ge 3 ]; then
|
||||
recheck_ready_label
|
||||
echo "::warning::FAILING OPEN: cannot query GitHub check status — triggering the merge gate WITHOUT the cheap-check gate."
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$elapsed" -ge "$MAX_WAIT_SECS" ]; then
|
||||
recheck_ready_label
|
||||
echo "::warning::FAILING OPEN: watched checks still pending after $(( MAX_WAIT_SECS / 60 )) min${missing:+ (never appeared: ${missing})} — triggering the merge gate anyway."
|
||||
exit 0
|
||||
fi
|
||||
sleep "$POLL_SECS"
|
||||
done
|
||||
@@ -0,0 +1,578 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Select the additive GPU integration lanes needed by a PR diff.
|
||||
|
||||
Fastcheck is the universal six-lane baseline and is intentionally not repeated
|
||||
here. This planner selects only the more expensive merge-gate lanes. Unknown
|
||||
source/build paths fail closed to the complete integration set, while explicit
|
||||
documentation and repository-metadata paths require no additional GPU work.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import fnmatch
|
||||
import re
|
||||
from collections.abc import Iterable
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import TextIO
|
||||
|
||||
MERGE_LANES = (
|
||||
"golden-gate",
|
||||
"ssim",
|
||||
"lora-inference",
|
||||
"lora-extraction",
|
||||
"training",
|
||||
"distillation",
|
||||
"self-forcing",
|
||||
"lora-training",
|
||||
"training-vsa",
|
||||
"inference-vmoba",
|
||||
"performance",
|
||||
"api-server",
|
||||
"train-framework",
|
||||
"eval",
|
||||
)
|
||||
|
||||
LANE_SCRIPT_TO_KEY = {
|
||||
"api_server.sh": "api-server",
|
||||
"distillation_dmd.sh": "distillation",
|
||||
"eval.sh": "eval",
|
||||
"golden_gate.sh": "golden-gate",
|
||||
"inference_lora.sh": "lora-inference",
|
||||
"inference_vmoba.sh": "inference-vmoba",
|
||||
"lora_extraction.sh": "lora-extraction",
|
||||
"performance.sh": "performance",
|
||||
"self_forcing.sh": "self-forcing",
|
||||
"ssim.sh": "ssim",
|
||||
"train_framework.sh": "train-framework",
|
||||
"training.sh": "training",
|
||||
"training_lora.sh": "lora-training",
|
||||
"training_vsa.sh": "training-vsa",
|
||||
}
|
||||
|
||||
FASTCHECK_LANE_SCRIPTS = {
|
||||
"dreamverse.sh",
|
||||
"encoder.sh",
|
||||
"kernel_tests.sh",
|
||||
"transformer.sh",
|
||||
"vae.sh",
|
||||
}
|
||||
|
||||
LEGACY_TRAINING_LANES = (
|
||||
"training",
|
||||
"distillation",
|
||||
"self-forcing",
|
||||
"lora-training",
|
||||
"training-vsa",
|
||||
)
|
||||
|
||||
ALL_TRAINING_LANES = (*LEGACY_TRAINING_LANES, "train-framework")
|
||||
|
||||
SSIM_SMOKE_TESTS = (
|
||||
"test_flux_t2i_similarity.py",
|
||||
"test_wan_t2v_similarity.py",
|
||||
)
|
||||
|
||||
SAFE_PATTERNS = (
|
||||
"*.md",
|
||||
"*.rst",
|
||||
".agents/**",
|
||||
".claude/**",
|
||||
".codex/**",
|
||||
".github/ISSUE_TEMPLATE/**",
|
||||
".github/PULL_REQUEST_TEMPLATE.md",
|
||||
".github/dependabot.yml",
|
||||
".github/mergify.yml",
|
||||
".github/scripts/**",
|
||||
".github/workflows/**",
|
||||
".buildkite/scripts/pre_commit.sh",
|
||||
".git-blame-ignore-revs",
|
||||
".gitattributes",
|
||||
".gitignore",
|
||||
".pre-commit-config.yaml",
|
||||
"AGENTS.md",
|
||||
"CITATION.cff",
|
||||
"CODE_OF_CONDUCT.md",
|
||||
"CONTRIBUTING.md",
|
||||
"LICENSE",
|
||||
"NOTICE",
|
||||
"__init__.py",
|
||||
"collect_env.py",
|
||||
"SECURITY.md",
|
||||
"assets/**",
|
||||
"comfyui/**",
|
||||
"docs/**",
|
||||
"examples/**",
|
||||
"mkdocs.yml",
|
||||
"requirements-mkdocs.in",
|
||||
"requirements-mkdocs.txt",
|
||||
"scripts/**",
|
||||
"tests/__init__.py",
|
||||
"tests/local_tests/**",
|
||||
)
|
||||
|
||||
ALL_IMPACT_PATTERNS = (
|
||||
".buildkite/pipeline.yml",
|
||||
"docker/**",
|
||||
"pyproject.toml",
|
||||
"requirements*.txt",
|
||||
"setup.cfg",
|
||||
"setup.py",
|
||||
"uv.lock",
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FamilyCoverage:
|
||||
pattern: re.Pattern[str]
|
||||
golden_tests: tuple[str, ...]
|
||||
ssim_tests: tuple[str, ...]
|
||||
|
||||
|
||||
FAMILY_COVERAGE = (
|
||||
FamilyCoverage(
|
||||
re.compile(r"(^|[/_.-])flux[_-]?2([/_.-]|$)"),
|
||||
("test_flux2_klein.py", ),
|
||||
("test_flux2_similarity.py", ),
|
||||
),
|
||||
FamilyCoverage(
|
||||
re.compile(r"(^|[/_.-])flux(?![_-]?2)([/_.-]|$)"),
|
||||
("test_flux.py", ),
|
||||
("test_flux_t2i_similarity.py", ),
|
||||
),
|
||||
FamilyCoverage(
|
||||
re.compile(r"(^|[/_.-])gen3c([/_.-]|$)"),
|
||||
("test_gen3c.py", ),
|
||||
("test_gen3c_similarity.py", ),
|
||||
),
|
||||
FamilyCoverage(
|
||||
re.compile(r"(^|[/_.-])hunyuan(video)?15([a-z0-9_-]*)([/_.-]|$)"),
|
||||
(),
|
||||
("test_hunyuan15_i2v_similarity.py", ),
|
||||
),
|
||||
FamilyCoverage(
|
||||
re.compile(r"(^|[/_.-])kandinsky[_-]?5([/_.-]|$)"),
|
||||
("test_kandinsky5.py", ),
|
||||
("test_kandinsky5_similarity.py", ),
|
||||
),
|
||||
FamilyCoverage(
|
||||
re.compile(r"(^|[/_.-])lingbot([a-z0-9_-]*)([/_.-]|$)"),
|
||||
("test_lingbot.py", ),
|
||||
("test_lingbot_similarity.py", ),
|
||||
),
|
||||
FamilyCoverage(
|
||||
re.compile(r"(^|[/_.-])ltx[_-]?2([/_.-]|$)"),
|
||||
("test_ltx2.py", ),
|
||||
("test_ltx2_similarity.py", ),
|
||||
),
|
||||
FamilyCoverage(
|
||||
re.compile(r"(^|[/_.-])matrixgame[_-]?2([/_.-]|$)"),
|
||||
("test_matrixgame.py", ),
|
||||
("test_matrixgame2_similarity.py", ),
|
||||
),
|
||||
FamilyCoverage(
|
||||
re.compile(r"(^|[/_.-])matrixgame[_-]?3([/_.-]|$)"),
|
||||
("test_matrixgame.py", ),
|
||||
("test_matrixgame3_similarity.py", ),
|
||||
),
|
||||
FamilyCoverage(
|
||||
re.compile(r"(^|[/_.-])minimax[_-]?h3([/_.-]|$)"),
|
||||
("test_minimax_h3_t2v.py", ),
|
||||
("test_minimax_h3_similarity.py", ),
|
||||
),
|
||||
FamilyCoverage(
|
||||
re.compile(r"(^|[/_.-])sd[_-]?3([._-]?5)?([/_.-]|$)"),
|
||||
("test_sd35.py", ),
|
||||
("test_sd35_similarity.py", ),
|
||||
),
|
||||
FamilyCoverage(
|
||||
re.compile(r"(^|[/_.-])stable[_-]?audio([/_.-]|$)"),
|
||||
("test_stable_audio.py", ),
|
||||
("test_stable_audio_similarity.py", ),
|
||||
),
|
||||
FamilyCoverage(
|
||||
re.compile(r"(^|[/_.-])wan(video|vae)?([/_.-]|$)"),
|
||||
("test_wan_t2v.py", "test_wan_vae.py", "test_wan_causal.py", "test_wan_denoising.py"),
|
||||
(
|
||||
"test_causal_similarity.py",
|
||||
"test_wan_i2v_similarity.py",
|
||||
"test_wan_t2v_similarity.py",
|
||||
"test_wan_ti2v_similarity.py",
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class MergePlan:
|
||||
lanes: set[str] = field(default_factory=set)
|
||||
golden_tests: set[str] = field(default_factory=set)
|
||||
ssim_tests: set[str] = field(default_factory=set)
|
||||
golden_all: bool = False
|
||||
ssim_all: bool = False
|
||||
reasons: list[str] = field(default_factory=list)
|
||||
|
||||
def add_lanes(self, *lanes: str, reason: str) -> None:
|
||||
unknown = set(lanes) - set(MERGE_LANES)
|
||||
if unknown:
|
||||
raise ValueError(f"Unknown merge lanes: {sorted(unknown)}")
|
||||
self.lanes.update(lanes)
|
||||
self.reasons.append(reason)
|
||||
|
||||
def add_golden(self, tests: tuple[str, ...], reason: str) -> None:
|
||||
self.add_lanes("golden-gate", reason=reason)
|
||||
self.golden_tests.update(tests)
|
||||
|
||||
def add_ssim(self, tests: tuple[str, ...], reason: str) -> None:
|
||||
self.add_lanes("ssim", reason=reason)
|
||||
self.ssim_tests.update(tests)
|
||||
|
||||
def require_all(self, reason: str) -> None:
|
||||
self.lanes.update(MERGE_LANES)
|
||||
self.golden_all = True
|
||||
self.ssim_all = True
|
||||
self.reasons.append(reason)
|
||||
|
||||
def ordered_lanes(self) -> tuple[str, ...]:
|
||||
return tuple(lane for lane in MERGE_LANES if lane in self.lanes)
|
||||
|
||||
def encoded_lanes(self) -> str:
|
||||
lanes = self.ordered_lanes()
|
||||
return "," + ",".join(lanes or ("none", )) + ","
|
||||
|
||||
def encoded_golden_tests(self) -> str:
|
||||
if "golden-gate" not in self.lanes:
|
||||
return "none"
|
||||
if self.golden_all or not self.golden_tests:
|
||||
return "all"
|
||||
return ",".join(sorted(self.golden_tests))
|
||||
|
||||
def encoded_ssim_tests(self) -> str:
|
||||
if "ssim" not in self.lanes:
|
||||
return "none"
|
||||
if self.ssim_all or not self.ssim_tests:
|
||||
return "all"
|
||||
return ",".join(sorted(self.ssim_tests))
|
||||
|
||||
|
||||
def _matches_any(path: str, patterns: tuple[str, ...]) -> bool:
|
||||
return any(fnmatch.fnmatchcase(path, pattern) for pattern in patterns)
|
||||
|
||||
|
||||
def _family_coverage(path: str) -> tuple[set[str], set[str]]:
|
||||
normalized = path.lower()
|
||||
golden: set[str] = set()
|
||||
ssim: set[str] = set()
|
||||
for family in FAMILY_COVERAGE:
|
||||
if family.pattern.search(normalized):
|
||||
golden.update(family.golden_tests)
|
||||
ssim.update(family.ssim_tests)
|
||||
# Select the component actually touched, including compatibility paths.
|
||||
# Family configs/pipeline wiring can affect all four Wan gates.
|
||||
if re.search(r"(^|[/_.-])wan(video|vae)?([/_.-]|$)", normalized):
|
||||
if (normalized.endswith(("/wan/vae.py", "/wan/vae_config.py", "/vaes/wanvae.py"))
|
||||
or normalized.endswith("/wan/stages/conditioning.py")):
|
||||
golden = {"test_wan_vae.py"}
|
||||
elif normalized.endswith(("/wan/causal_transformer.py", "/dits/causal_wanvideo.py",
|
||||
"/wan/stages/causal_denoising.py")):
|
||||
golden = {"test_wan_causal.py"}
|
||||
elif (normalized == "fastvideo/models/dits/wanvideo.py"
|
||||
or normalized.endswith(("/wan/transformer.py", "/wan/stages/denoising.py", "/wan/stages/dmd.py"))):
|
||||
golden = {"test_wan_t2v.py", "test_wan_denoising.py"}
|
||||
return golden, ssim
|
||||
|
||||
|
||||
def _select_output_coverage(plan: MergePlan, path: str) -> None:
|
||||
golden, ssim = _family_coverage(path)
|
||||
if golden:
|
||||
plan.add_golden(tuple(sorted(golden)), reason=f"model-family golden coverage: {path}")
|
||||
else:
|
||||
plan.golden_all = True
|
||||
plan.add_lanes("golden-gate", reason=f"shared output golden coverage: {path}")
|
||||
if ssim:
|
||||
plan.add_ssim(tuple(sorted(ssim)), reason=f"model-family SSIM coverage: {path}")
|
||||
else:
|
||||
plan.add_ssim(SSIM_SMOKE_TESTS, reason=f"shared output SSIM smoke coverage: {path}")
|
||||
|
||||
|
||||
def _normalize_path(raw_path: str) -> str:
|
||||
path = raw_path.strip()
|
||||
while path.startswith("./"):
|
||||
path = path[2:]
|
||||
return path
|
||||
|
||||
|
||||
def classify_paths(paths: list[str], removed_paths: Iterable[str] = ()) -> MergePlan:
|
||||
"""Plan merge lanes for ``paths``.
|
||||
|
||||
``removed_paths`` lists changed paths that no longer exist at the PR head
|
||||
(deleted files and rename sources); removed golden/SSIM tests are not run.
|
||||
"""
|
||||
plan = MergePlan()
|
||||
normalized_paths = sorted({path for path in map(_normalize_path, paths) if path})
|
||||
removed = {path for path in map(_normalize_path, removed_paths) if path}
|
||||
if not normalized_paths:
|
||||
plan.require_all("changed-file list was empty; failing closed")
|
||||
return plan
|
||||
|
||||
for path in normalized_paths:
|
||||
if path == "__FASTVIDEO_CI_PLAN_ALL__":
|
||||
plan.require_all("changed-file API failed; failing closed")
|
||||
continue
|
||||
|
||||
if path in {"requirements-mkdocs.in", "requirements-mkdocs.txt"}:
|
||||
plan.reasons.append(f"documentation dependencies need no GPU integration: {path}")
|
||||
continue
|
||||
|
||||
if _matches_any(path, ALL_IMPACT_PATTERNS):
|
||||
plan.require_all(f"cross-cutting build/runtime surface: {path}")
|
||||
continue
|
||||
|
||||
lane_script_prefix = ".buildkite/scripts/lanes/"
|
||||
if path.startswith(lane_script_prefix):
|
||||
script_name = Path(path).name
|
||||
lane = LANE_SCRIPT_TO_KEY.get(script_name)
|
||||
if lane is None:
|
||||
if script_name in FASTCHECK_LANE_SCRIPTS:
|
||||
plan.reasons.append(f"covered by automatic Fastcheck lane: {path}")
|
||||
else:
|
||||
plan.require_all(f"unknown lane script: {path}")
|
||||
elif lane == "golden-gate":
|
||||
plan.golden_all = True
|
||||
plan.add_lanes(lane, reason=f"golden lane implementation: {path}")
|
||||
elif lane == "ssim":
|
||||
plan.ssim_all = True
|
||||
plan.add_lanes(lane, reason=f"SSIM lane implementation: {path}")
|
||||
else:
|
||||
plan.add_lanes(lane, reason=f"lane implementation: {path}")
|
||||
continue
|
||||
|
||||
if path.startswith("fastvideo/tests/golden_gate/"):
|
||||
name = Path(path).name
|
||||
if name.startswith("test_") and name.endswith(".py"):
|
||||
if path in removed:
|
||||
plan.reasons.append(f"removed golden test has nothing to run: {path}")
|
||||
else:
|
||||
plan.add_golden((name, ), reason=f"changed golden test: {path}")
|
||||
elif name in {"AGENTS.md", "README.md"}:
|
||||
plan.reasons.append(f"golden documentation only: {path}")
|
||||
else:
|
||||
plan.golden_all = True
|
||||
plan.add_lanes("golden-gate", reason=f"shared golden harness/reference: {path}")
|
||||
continue
|
||||
|
||||
if path.startswith("fastvideo/tests/ssim/"):
|
||||
name = Path(path).name
|
||||
if name.startswith("test_") and name.endswith(".py"):
|
||||
if path in removed:
|
||||
plan.reasons.append(f"removed SSIM test has nothing to run: {path}")
|
||||
else:
|
||||
plan.add_ssim((name, ), reason=f"changed SSIM test: {path}")
|
||||
elif path.endswith((".py", ".json", ".pt", ".png", ".mp4")):
|
||||
plan.ssim_all = True
|
||||
plan.add_lanes("ssim", reason=f"shared SSIM harness/reference: {path}")
|
||||
continue
|
||||
|
||||
if path.startswith("fastvideo/tests/performance/") or path.startswith(".buildkite/performance-benchmarks/"):
|
||||
plan.add_lanes("performance", reason=f"performance coverage: {path}")
|
||||
continue
|
||||
if path.startswith(("fastvideo/performance/", "fastvideo/performance_dashboard/",
|
||||
"apps/performance_dashboard/")):
|
||||
plan.add_lanes("performance", reason=f"performance implementation: {path}")
|
||||
continue
|
||||
if path.startswith("fastvideo/benchmarks/"):
|
||||
if "/mlx_" in path or Path(path).name.startswith("mlx_"):
|
||||
plan.reasons.append(f"covered by the path-filtered macOS MLX workflow: {path}")
|
||||
else:
|
||||
plan.add_lanes("performance", reason=f"benchmark implementation: {path}")
|
||||
continue
|
||||
if path.startswith("fastvideo/tests/eval/") or path.startswith("fastvideo/eval/"):
|
||||
plan.add_lanes("eval", reason=f"evaluation coverage: {path}")
|
||||
continue
|
||||
if path.startswith("fastvideo/third_party/eval/"):
|
||||
plan.add_lanes("eval", reason=f"vendored evaluation implementation: {path}")
|
||||
continue
|
||||
if path.startswith("fastvideo/tests/lora_extraction/") or path.startswith("scripts/lora_extraction/"):
|
||||
plan.add_lanes("lora-extraction", reason=f"LoRA extraction coverage: {path}")
|
||||
continue
|
||||
if path.startswith("fastvideo/tests/inference/lora/"):
|
||||
plan.add_lanes("lora-inference", reason=f"LoRA inference coverage: {path}")
|
||||
continue
|
||||
if path.startswith("fastvideo/tests/inference/vmoba/"):
|
||||
plan.add_lanes("inference-vmoba", reason=f"VMoBA inference coverage: {path}")
|
||||
continue
|
||||
if path.startswith(("fastvideo/dataset/", "fastvideo/workflow/", "fastvideo/pipelines/preprocess/",
|
||||
"fastvideo/pipelines/training/")):
|
||||
plan.add_lanes(*ALL_TRAINING_LANES, reason=f"shared data/training input surface: {path}")
|
||||
continue
|
||||
if path.startswith("fastvideo/tests/train/") or path.startswith("fastvideo/train/"):
|
||||
plan.add_lanes("train-framework", reason=f"modular training coverage: {path}")
|
||||
continue
|
||||
|
||||
if path.startswith("fastvideo/tests/training/"):
|
||||
lowered = path.lower()
|
||||
if "/vanilla/" in lowered:
|
||||
plan.add_lanes("training", reason=f"vanilla training coverage: {path}")
|
||||
elif "/distill/" in lowered:
|
||||
plan.add_lanes("distillation", reason=f"distillation coverage: {path}")
|
||||
elif "/self-forcing/" in lowered:
|
||||
plan.add_lanes("self-forcing", reason=f"self-forcing coverage: {path}")
|
||||
elif "/lora/" in lowered:
|
||||
plan.add_lanes("lora-training", reason=f"LoRA training coverage: {path}")
|
||||
elif "/vsa/" in lowered:
|
||||
plan.add_lanes("training-vsa", reason=f"VSA training coverage: {path}")
|
||||
else:
|
||||
plan.add_lanes(*LEGACY_TRAINING_LANES, reason=f"shared legacy training coverage: {path}")
|
||||
continue
|
||||
|
||||
if path.startswith("fastvideo/training/"):
|
||||
lowered = path.lower()
|
||||
if "self_forcing" in lowered:
|
||||
plan.add_lanes("self-forcing", reason=f"self-forcing implementation: {path}")
|
||||
elif "distill" in lowered:
|
||||
plan.add_lanes("distillation", reason=f"distillation implementation: {path}")
|
||||
elif "lora" in lowered:
|
||||
plan.add_lanes("lora-training", reason=f"LoRA training implementation: {path}")
|
||||
else:
|
||||
plan.add_lanes(*LEGACY_TRAINING_LANES, reason=f"shared legacy training implementation: {path}")
|
||||
continue
|
||||
|
||||
lowered = path.lower()
|
||||
if "vmoba" in lowered and path.startswith(("fastvideo/", ".buildkite/")):
|
||||
plan.add_lanes("inference-vmoba", reason=f"VMoBA implementation: {path}")
|
||||
plan.add_golden(("test_wan_t2v.py", ), reason=f"VMoBA end-to-end coverage: {path}")
|
||||
continue
|
||||
if "lora" in lowered and path.startswith("fastvideo/"):
|
||||
plan.add_lanes(
|
||||
"lora-inference",
|
||||
"lora-extraction",
|
||||
"lora-training",
|
||||
reason=f"shared LoRA implementation: {path}",
|
||||
)
|
||||
_select_output_coverage(plan, path)
|
||||
continue
|
||||
|
||||
if path.startswith("fastvideo/entrypoints/") or path.startswith("fastvideo/api/"):
|
||||
plan.add_lanes("api-server", reason=f"API/entrypoint integration: {path}")
|
||||
if "openai" not in lowered and "/cli/" not in lowered:
|
||||
_select_output_coverage(plan, path)
|
||||
continue
|
||||
if path.startswith("fastvideo/worker/"):
|
||||
plan.add_lanes("api-server", reason=f"worker/API integration: {path}")
|
||||
_select_output_coverage(plan, path)
|
||||
continue
|
||||
if path.startswith("fastvideo/distributed/"):
|
||||
plan.add_lanes(
|
||||
"training",
|
||||
"train-framework",
|
||||
reason=f"distributed runtime integration: {path}",
|
||||
)
|
||||
_select_output_coverage(plan, path)
|
||||
continue
|
||||
if path.startswith(("fastvideo/hooks/", "fastvideo/platforms/", "fastvideo/third_party/")):
|
||||
_select_output_coverage(plan, path)
|
||||
continue
|
||||
if path.startswith(("fastvideo/models/", "fastvideo/pipelines/", "fastvideo/configs/",
|
||||
"fastvideo/layers/", "fastvideo/attention/")):
|
||||
_select_output_coverage(plan, path)
|
||||
continue
|
||||
if path in {
|
||||
"fastvideo/fastvideo_args.py",
|
||||
"fastvideo/forward_context.py",
|
||||
"fastvideo/image_processor.py",
|
||||
"fastvideo/registry.py",
|
||||
"fastvideo/utils.py",
|
||||
}:
|
||||
_select_output_coverage(plan, path)
|
||||
continue
|
||||
if path.startswith("fastvideo/mlx_runtime/"):
|
||||
plan.reasons.append(f"covered by the path-filtered macOS MLX workflow: {path}")
|
||||
continue
|
||||
if path.startswith("fastvideo/logging_utils/") or path in {
|
||||
"fastvideo/__init__.py",
|
||||
"fastvideo/envs.py",
|
||||
"fastvideo/logger.py",
|
||||
"fastvideo/profiler.py",
|
||||
"fastvideo/version.py",
|
||||
}:
|
||||
plan.reasons.append(f"covered by automatic Fastcheck: {path}")
|
||||
continue
|
||||
if path.startswith(("fastvideo-kernel/", "csrc/")):
|
||||
plan.add_golden(("test_wan_t2v.py", ), reason=f"kernel integration smoke: {path}")
|
||||
plan.add_ssim(("test_wan_t2v_similarity.py", ), reason=f"kernel numerical smoke: {path}")
|
||||
continue
|
||||
|
||||
if path.startswith("apps/dreamverse/"):
|
||||
# DreamVerse is already one of the six automatic Fastcheck lanes.
|
||||
plan.reasons.append(f"covered by automatic DreamVerse Fastcheck: {path}")
|
||||
continue
|
||||
if path.startswith("apps/infinite_livestream/"):
|
||||
# The app's CPU-only tests run in the automatic unit Fastcheck lane.
|
||||
plan.reasons.append(f"covered by automatic unit Fastcheck: {path}")
|
||||
continue
|
||||
if path.startswith("fastvideo/tests/"):
|
||||
# The automatic unit/component Fastcheck lanes own the remaining
|
||||
# package tests. Domain-specific expensive test roots were handled
|
||||
# above.
|
||||
plan.reasons.append(f"covered by automatic Fastcheck: {path}")
|
||||
continue
|
||||
if path in {".buildkite/scripts/unit_test.sh", ".buildkite/scripts/pr_test.sh"}:
|
||||
plan.reasons.append(f"covered by automatic unit Fastcheck: {path}")
|
||||
continue
|
||||
if _matches_any(path, SAFE_PATTERNS):
|
||||
plan.reasons.append(f"no additional GPU integration needed: {path}")
|
||||
continue
|
||||
|
||||
plan.require_all(f"unclassified path; failing closed: {path}")
|
||||
|
||||
return plan
|
||||
|
||||
|
||||
def _write_github_output(output: TextIO, plan: MergePlan) -> None:
|
||||
output.write(f"merge_test_plan={plan.encoded_lanes()}\n")
|
||||
output.write(f"merge_golden_tests={plan.encoded_golden_tests()}\n")
|
||||
output.write(f"merge_ssim_tests={plan.encoded_ssim_tests()}\n")
|
||||
output.write(f"merge_plan_label={','.join(plan.ordered_lanes()) or 'none'}\n")
|
||||
|
||||
|
||||
def _write_summary(output: TextIO, plan: MergePlan) -> None:
|
||||
output.write("## Change-aware merge test plan\n\n")
|
||||
output.write("| Selection | Value |\n|---|---|\n")
|
||||
output.write(f"| Additional Slurm lanes | `{','.join(plan.ordered_lanes()) or 'none'}` |\n")
|
||||
output.write(f"| Golden tests | `{plan.encoded_golden_tests()}` |\n")
|
||||
output.write(f"| SSIM tests | `{plan.encoded_ssim_tests()}` |\n\n")
|
||||
output.write("Fastcheck remains the universal six-lane baseline.\n")
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--paths-file", type=Path, required=True)
|
||||
parser.add_argument("--removed-paths-file", type=Path)
|
||||
parser.add_argument("--github-output", type=Path)
|
||||
parser.add_argument("--summary-file", type=Path)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
paths = args.paths_file.read_text(encoding="utf-8").splitlines()
|
||||
removed_paths = (args.removed_paths_file.read_text(encoding="utf-8").splitlines()
|
||||
if args.removed_paths_file else [])
|
||||
plan = classify_paths(paths, removed_paths)
|
||||
print(f"MERGE_TEST_PLAN={plan.encoded_lanes()}")
|
||||
print(f"MERGE_GOLDEN_TESTS={plan.encoded_golden_tests()}")
|
||||
print(f"MERGE_SSIM_TESTS={plan.encoded_ssim_tests()}")
|
||||
for reason in plan.reasons:
|
||||
print(f"- {reason}")
|
||||
if args.github_output:
|
||||
with args.github_output.open("a", encoding="utf-8") as output:
|
||||
_write_github_output(output, plan)
|
||||
if args.summary_file:
|
||||
with args.summary_file.open("a", encoding="utf-8") as output:
|
||||
_write_summary(output, plan)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -1,249 +0,0 @@
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
import requests
|
||||
|
||||
|
||||
def parse_arguments():
|
||||
"""Parse command line arguments"""
|
||||
parser = argparse.ArgumentParser(description='Run tests on RunPod GPU')
|
||||
parser.add_argument('--gpu-type', type=str, help='GPU type to use')
|
||||
parser.add_argument('--gpu-count',
|
||||
type=int,
|
||||
help='Number of GPUs to use',
|
||||
default=1)
|
||||
parser.add_argument('--test-command', type=str, help='Test command to run')
|
||||
parser.add_argument('--disk-size',
|
||||
type=int,
|
||||
default=20,
|
||||
help='Container disk size in GB (default: 20)')
|
||||
parser.add_argument('--volume-size',
|
||||
type=int,
|
||||
default=20,
|
||||
help='Persistent volume size in GB (default: 20)')
|
||||
parser.add_argument(
|
||||
'--image',
|
||||
type=str,
|
||||
required=True,
|
||||
help='Docker image to use')
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
args = parse_arguments()
|
||||
API_KEY = os.environ['RUNPOD_API_KEY']
|
||||
RUN_ID = os.environ['GITHUB_RUN_ID']
|
||||
JOB_ID = os.environ['JOB_ID']
|
||||
PODS_API = "https://rest.runpod.io/v1/pods"
|
||||
HEADERS = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {API_KEY}"
|
||||
}
|
||||
|
||||
|
||||
def create_pod():
|
||||
"""Create a RunPod instance"""
|
||||
# Ensure image name is lowercase (Docker requirement)
|
||||
image_name = args.image.lower()
|
||||
print(f"Using specified image: {image_name}")
|
||||
|
||||
docker_start_cmd = [
|
||||
"bash",
|
||||
"-c",
|
||||
"apt update;DEBIAN_FRONTEND=noninteractive apt-get install openssh-server -y;mkdir -p ~/.ssh;cd $_;chmod 700 ~/.ssh;echo \"$PUBLIC_KEY\" >> authorized_keys;chmod 700 authorized_keys;service ssh start;sleep infinity"
|
||||
]
|
||||
|
||||
print(f"Creating RunPod instance with GPU: {args.gpu_type}...")
|
||||
payload = {
|
||||
"name": f"fastvideo-{JOB_ID}-{RUN_ID}",
|
||||
"containerDiskInGb": args.disk_size,
|
||||
"volumeInGb": args.volume_size,
|
||||
"gpuTypeIds": [args.gpu_type],
|
||||
"gpuCount": args.gpu_count,
|
||||
"imageName": image_name,
|
||||
"allowedCudaVersions": ["12.4"],
|
||||
"dockerStartCmd": docker_start_cmd
|
||||
}
|
||||
|
||||
response = requests.post(PODS_API, headers=HEADERS, json=payload)
|
||||
response_data = response.json()
|
||||
print(f"Response: {json.dumps(response_data, indent=2)}")
|
||||
|
||||
return response_data["id"]
|
||||
|
||||
|
||||
def wait_for_pod(pod_id):
|
||||
"""Wait for pod to be in RUNNING state and fully ready with SSH access"""
|
||||
print("Waiting for RunPod to be ready...")
|
||||
|
||||
# First wait for RUNNING status
|
||||
max_attempts = 10
|
||||
attempts = 0
|
||||
while attempts < max_attempts:
|
||||
response = requests.get(f"{PODS_API}/{pod_id}", headers=HEADERS)
|
||||
pod_data = response.json()
|
||||
status = pod_data["desiredStatus"]
|
||||
|
||||
if status == "RUNNING":
|
||||
print("RunPod is running! Now waiting for ports to be assigned...")
|
||||
break
|
||||
|
||||
print(
|
||||
f"Current status: {status}, waiting... (attempt {attempts+1}/{max_attempts})"
|
||||
)
|
||||
time.sleep(2)
|
||||
attempts += 1
|
||||
|
||||
if attempts >= max_attempts:
|
||||
raise TimeoutError(
|
||||
"Timed out waiting for RunPod to reach RUNNING state")
|
||||
|
||||
# Wait for ports to be assigned
|
||||
max_attempts = 50
|
||||
attempts = 0
|
||||
while attempts < max_attempts:
|
||||
response = requests.get(f"{PODS_API}/{pod_id}", headers=HEADERS)
|
||||
pod_data = response.json()
|
||||
port_mappings = pod_data.get("portMappings")
|
||||
|
||||
if (port_mappings is not None and "22" in port_mappings
|
||||
and pod_data.get("publicIp", "") != ""):
|
||||
print("RunPod is ready with SSH access!")
|
||||
print(f"SSH IP: {pod_data['publicIp']}")
|
||||
print(f"SSH Port: {port_mappings['22']}")
|
||||
break
|
||||
|
||||
print(
|
||||
f"Waiting for SSH port and public IP to be available... (attempt {attempts+1}/{max_attempts})"
|
||||
)
|
||||
time.sleep(20)
|
||||
attempts += 1
|
||||
|
||||
if attempts >= max_attempts:
|
||||
raise TimeoutError("Timed out waiting for RunPod SSH access")
|
||||
|
||||
|
||||
def execute_command(pod_id):
|
||||
"""Execute command on the pod via SSH using system SSH client"""
|
||||
print(f"Running command: {args.test_command}")
|
||||
|
||||
response = requests.get(f"{PODS_API}/{pod_id}", headers=HEADERS)
|
||||
pod_data = response.json()
|
||||
ssh_ip = pod_data["publicIp"]
|
||||
ssh_port = pod_data["portMappings"]["22"]
|
||||
|
||||
# Copy the repository to the pod using scp
|
||||
repo_dir = os.path.abspath(os.getcwd())
|
||||
repo_name = os.path.basename(repo_dir)
|
||||
|
||||
print(f"Copying repository from {repo_dir} to RunPod...")
|
||||
|
||||
tar_command = [
|
||||
"tar", "-czf", "/tmp/repo.tar.gz", "-C",
|
||||
os.path.dirname(repo_dir), repo_name
|
||||
]
|
||||
subprocess.run(tar_command, check=True)
|
||||
|
||||
# Copy the tarball to the pod
|
||||
scp_command = [
|
||||
"scp", "-o", "StrictHostKeyChecking=no", "-o",
|
||||
"UserKnownHostsFile=/dev/null", "-o", "ServerAliveInterval=60", "-o",
|
||||
"ServerAliveCountMax=10", "-P",
|
||||
str(ssh_port), "/tmp/repo.tar.gz", f"root@{ssh_ip}:/tmp/"
|
||||
]
|
||||
subprocess.run(scp_command, check=True)
|
||||
|
||||
# For custom image, we can use the pre-configured environment
|
||||
setup_steps = [
|
||||
"tar -xzf /tmp/repo.tar.gz --no-same-owner -C /workspace/",
|
||||
f"cd /workspace/{repo_name}",
|
||||
"source $HOME/.local/bin/env && source /opt/venv/bin/activate",
|
||||
args.test_command
|
||||
]
|
||||
|
||||
remote_command = " && ".join(setup_steps)
|
||||
|
||||
ssh_command = [
|
||||
"ssh", "-o", "StrictHostKeyChecking=no", "-o",
|
||||
"UserKnownHostsFile=/dev/null", "-o", "ServerAliveInterval=60", "-o",
|
||||
"ServerAliveCountMax=10", "-p",
|
||||
str(ssh_port), f"root@{ssh_ip}", remote_command
|
||||
]
|
||||
|
||||
print(f"Connecting to {ssh_ip}:{ssh_port}...")
|
||||
|
||||
try:
|
||||
process = subprocess.Popen(ssh_command,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
universal_newlines=True,
|
||||
bufsize=0)
|
||||
|
||||
stdout_lines = []
|
||||
|
||||
print("Command output:")
|
||||
|
||||
for line in iter(process.stdout.readline, ''):
|
||||
print(line.strip())
|
||||
stdout_lines.append(line)
|
||||
|
||||
process.wait()
|
||||
|
||||
return_code = process.returncode
|
||||
success = return_code == 0
|
||||
|
||||
stdout_str = "".join(stdout_lines)
|
||||
|
||||
if success:
|
||||
print("Command executed successfully")
|
||||
else:
|
||||
print(f"Command failed with exit code {return_code}")
|
||||
|
||||
result = {
|
||||
"success": success,
|
||||
"return_code": return_code,
|
||||
"stdout": stdout_str,
|
||||
"stderr": ""
|
||||
}
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error executing SSH command: {str(e)}")
|
||||
result = {"success": False, "error": str(e), "stdout": "", "stderr": ""}
|
||||
return result
|
||||
|
||||
|
||||
def terminate_pod(pod_id):
|
||||
"""Terminate the pod"""
|
||||
print("Terminating RunPod...")
|
||||
requests.delete(f"{PODS_API}/{pod_id}", headers=HEADERS)
|
||||
print(f"Terminated pod {pod_id}")
|
||||
|
||||
|
||||
def main():
|
||||
pod_id = None
|
||||
try:
|
||||
pod_id = create_pod()
|
||||
wait_for_pod(pod_id)
|
||||
result = execute_command(pod_id)
|
||||
|
||||
if result.get("error") is not None:
|
||||
print(f"Error executing command: {result['error']}")
|
||||
sys.exit(1)
|
||||
|
||||
if not result.get("success", False):
|
||||
print(
|
||||
"Tests failed - check the output above for details on which tests failed"
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
finally:
|
||||
if pod_id:
|
||||
terminate_pod(pod_id)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,90 +0,0 @@
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import uuid
|
||||
|
||||
import requests
|
||||
|
||||
API_KEY = os.environ['RUNPOD_API_KEY']
|
||||
RUN_ID = os.environ.get('GITHUB_RUN_ID', str(uuid.uuid4()))
|
||||
PODS_API = "https://rest.runpod.io/v1/pods"
|
||||
HEADERS = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {API_KEY}"
|
||||
}
|
||||
|
||||
|
||||
def get_job_ids():
|
||||
"""Parse job IDs from environment variable"""
|
||||
job_ids_str = os.environ.get('JOB_IDS')
|
||||
try:
|
||||
job_ids = json.loads(job_ids_str)
|
||||
if not isinstance(job_ids, list):
|
||||
print("Error: JOB_IDS is not a list.")
|
||||
sys.exit(1)
|
||||
return job_ids
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"Error parsing JOB_IDS: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def cleanup_pods():
|
||||
"""Find and terminate RunPod instances"""
|
||||
print(f"Run ID: {RUN_ID}")
|
||||
|
||||
single_job_id = os.environ.get('JOB_ID')
|
||||
|
||||
if single_job_id:
|
||||
job_ids = [single_job_id]
|
||||
print(f"Job ID: {single_job_id}")
|
||||
else:
|
||||
job_ids = get_job_ids()
|
||||
print(f"Job IDs: {job_ids}")
|
||||
|
||||
# Get all pods associated with RunPod API_KEY
|
||||
try:
|
||||
response = requests.get(PODS_API, headers=HEADERS)
|
||||
response.raise_for_status()
|
||||
pods = response.json()
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"Error getting pods: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
# Find and terminate pods created by this workflow run
|
||||
terminated_pods = []
|
||||
for pod in pods:
|
||||
pod_name = pod.get("name", "")
|
||||
pod_id = pod.get("id")
|
||||
|
||||
# Check if this pod was created by one of our jobs
|
||||
if any(f"{job_id}-{RUN_ID}" in pod_name for job_id in job_ids):
|
||||
print(f"Found pod: {pod_id} ({pod_name})")
|
||||
try:
|
||||
print(f"Terminating pod {pod_id}...")
|
||||
term_response = requests.delete(f"{PODS_API}/{pod_id}",
|
||||
headers=HEADERS)
|
||||
term_response.raise_for_status()
|
||||
terminated_pods.append(pod_id)
|
||||
print(f"Successfully terminated pod {pod_id}")
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"Error terminating pod {pod_id}: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
if terminated_pods:
|
||||
if single_job_id:
|
||||
print(f"Terminated pod: {terminated_pods[0]}")
|
||||
else:
|
||||
print(f"Terminated {len(terminated_pods)} pods: {terminated_pods}")
|
||||
else:
|
||||
if single_job_id:
|
||||
print(f"No pod found matching pattern: {single_job_id}-{RUN_ID}")
|
||||
else:
|
||||
print("No pods found to terminate.")
|
||||
|
||||
|
||||
def main():
|
||||
cleanup_pods()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+122
@@ -0,0 +1,122 @@
|
||||
#!/usr/bin/env bash
|
||||
# Self-test for gate_full_suite.sh using a mocked `gh`. No network, runs on
|
||||
# any dev box: bash .github/scripts/test_gate_full_suite.sh
|
||||
set -u
|
||||
here=$(cd "$(dirname "$0")" && pwd)
|
||||
tmp=$(mktemp -d)
|
||||
trap 'rm -rf "$tmp"' EXIT
|
||||
|
||||
# Mock gh. Asserts the exact endpoint (including head_sha) it is called
|
||||
# with — an endpoint typo in the gate script fails the test rather than
|
||||
# silently serving canned data. On the runs endpoint it serves
|
||||
# $MOCK_DIR/response_<call#>.json, sticking on the highest existing file,
|
||||
# and exits 1 if none exist (simulates a GitHub API outage). On the pulls
|
||||
# endpoint it serves $MOCK_DIR/pr.json, defaulting to a 'ready'-labeled PR.
|
||||
cat > "$tmp/gh" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
if [ "${1:-}" != "api" ]; then
|
||||
echo "unexpected gh invocation: $*" >> "$MOCK_DIR/endpoint_error"
|
||||
exit 2
|
||||
fi
|
||||
case "${2:-}" in
|
||||
"repos/o/r/actions/runs?head_sha=deadbeef&per_page=100")
|
||||
n=$(( $(cat "$MOCK_DIR/count" 2>/dev/null || echo 0) + 1 ))
|
||||
echo "$n" > "$MOCK_DIR/count"
|
||||
while [ "$n" -gt 0 ]; do
|
||||
if [ -f "$MOCK_DIR/response_$n.json" ]; then
|
||||
cat "$MOCK_DIR/response_$n.json"
|
||||
exit 0
|
||||
fi
|
||||
n=$(( n - 1 ))
|
||||
done
|
||||
echo "api outage" >&2
|
||||
exit 1
|
||||
;;
|
||||
"repos/o/r/pulls/42")
|
||||
if [ -f "$MOCK_DIR/pr.json" ]; then
|
||||
cat "$MOCK_DIR/pr.json"
|
||||
else
|
||||
echo '{"labels": [{"name": "ready"}]}'
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
echo "unexpected gh endpoint: $2" >> "$MOCK_DIR/endpoint_error"
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
EOF
|
||||
chmod +x "$tmp/gh"
|
||||
|
||||
PC_OK='{"name": "pre-commit", "id": 1, "status": "completed", "conclusion": "success"}'
|
||||
PC_BAD='{"name": "pre-commit", "id": 1, "status": "completed", "conclusion": "failure"}'
|
||||
PC_PENDING='{"name": "pre-commit", "id": 1, "status": "in_progress", "conclusion": null}'
|
||||
DOCS_OK='{"name": "Deploy Documentation", "id": 2, "status": "completed", "conclusion": "success"}'
|
||||
DOCS_BAD='{"name": "Deploy Documentation", "id": 2, "status": "completed", "conclusion": "failure"}'
|
||||
DOCS_CANCELLED='{"name": "Deploy Documentation", "id": 2, "status": "completed", "conclusion": "cancelled"}'
|
||||
OTHER='{"name": "Trigger Merge Gate", "id": 3, "status": "in_progress", "conclusion": null}'
|
||||
NULL_NAME='{"name": null, "id": 4, "status": "completed", "conclusion": "failure"}'
|
||||
PC_OK_RERUN='{"name": "pre-commit", "id": 5, "status": "completed", "conclusion": "success"}'
|
||||
|
||||
fails=0
|
||||
want_log="" # optional: expect() also greps out.log for this regex, then resets
|
||||
pr_json="" # optional: served for the pulls (label re-check) endpoint, then resets
|
||||
raw_body="" # optional: serve responses verbatim instead of wrapping in workflow_runs
|
||||
expect() { # <name> <expected-exit> <response json>...
|
||||
local name=$1 want=$2 dir i=1
|
||||
shift 2
|
||||
dir=$(mktemp -d "$tmp/test_XXXXXX")
|
||||
for body in "$@"; do
|
||||
if [ -n "$raw_body" ]; then
|
||||
printf '%s' "$body" > "$dir/response_$i.json"
|
||||
else
|
||||
printf '{"workflow_runs": [%s]}' "$body" > "$dir/response_$i.json"
|
||||
fi
|
||||
i=$(( i + 1 ))
|
||||
done
|
||||
[ -n "$pr_json" ] && printf '%s' "$pr_json" > "$dir/pr.json"
|
||||
( export PATH="$tmp:$PATH" MOCK_DIR="$dir" PR_SHA=deadbeef PR_NUMBER=42 \
|
||||
GITHUB_REPOSITORY=o/r POLL_SECS=0 GRACE_SECS=1 MAX_WAIT_SECS=3
|
||||
bash "$here/gate_full_suite.sh" > "$dir/out.log" 2>&1 )
|
||||
local rc=$?
|
||||
if [ "$rc" -ne "$want" ]; then
|
||||
echo "FAIL: $name (exit $rc, want $want)"
|
||||
cat "$dir/out.log"
|
||||
fails=1
|
||||
elif [ -f "$dir/endpoint_error" ]; then
|
||||
echo "FAIL: $name (mock gh got an unexpected call)"
|
||||
cat "$dir/endpoint_error"
|
||||
fails=1
|
||||
elif [ -n "$want_log" ] && ! grep -Eq "$want_log" "$dir/out.log"; then
|
||||
echo "FAIL: $name (log does not match: $want_log)"
|
||||
cat "$dir/out.log"
|
||||
fails=1
|
||||
else
|
||||
echo "ok: $name"
|
||||
fi
|
||||
want_log="" pr_json="" raw_body=""
|
||||
}
|
||||
|
||||
expect "both green -> proceed" 0 "$PC_OK, $DOCS_OK, $OTHER, $NULL_NAME"
|
||||
expect "docs build failed -> blocked" 1 "$PC_OK, $DOCS_BAD"
|
||||
expect "pre-commit failed -> blocked" 1 "$PC_BAD"
|
||||
expect "pending then green -> proceed" 0 "$PC_PENDING" "$PC_OK, $DOCS_OK"
|
||||
want_log="never appeared.*Deploy Documentation"
|
||||
expect "docs run absent (path-filtered) -> proceed after grace" 0 "$PC_OK"
|
||||
expect "API outage -> fail open" 0
|
||||
want_log="FAILING OPEN"
|
||||
expect "pending past MAX_WAIT -> fail open" 0 "$PC_PENDING"
|
||||
want_log="FAILING OPEN"
|
||||
expect "unrelated runs only -> no grace, fail open at MAX_WAIT" 0 "$OTHER"
|
||||
expect "cancelled docs then green -> proceed" 0 \
|
||||
"$PC_OK, $DOCS_CANCELLED" "$PC_OK, $DOCS_OK"
|
||||
want_log="FAILING OPEN"
|
||||
expect "cancelled docs forever -> fail open at MAX_WAIT" 0 "$PC_OK, $DOCS_CANCELLED"
|
||||
want_log="FAILING OPEN"
|
||||
expect "pre-commit absent -> no grace, fail open at MAX_WAIT" 0 "$DOCS_OK"
|
||||
expect "duplicate run names -> latest wins" 0 "$PC_BAD, $PC_OK_RERUN, $DOCS_OK"
|
||||
raw_body=1
|
||||
expect "garbage response body -> fail open" 0 "this is not json"
|
||||
pr_json='{"labels": [{"name": "other"}]}'
|
||||
expect "ready label removed mid-gate -> blocked" 1 "$PC_OK, $DOCS_OK"
|
||||
|
||||
exit "$fails"
|
||||
@@ -0,0 +1,201 @@
|
||||
name: Build Image Template
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
python_version:
|
||||
required: true
|
||||
type: string
|
||||
dockerfile_path:
|
||||
required: true
|
||||
type: string
|
||||
tag_suffix:
|
||||
required: true
|
||||
type: string
|
||||
image_name:
|
||||
required: false
|
||||
type: string
|
||||
default: fastvideo-dev
|
||||
build_args:
|
||||
required: false
|
||||
type: string
|
||||
default: ''
|
||||
include_latest_tags:
|
||||
required: false
|
||||
type: boolean
|
||||
default: true
|
||||
mark_as_latest:
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
runner:
|
||||
required: false
|
||||
type: string
|
||||
default: ubuntu-latest
|
||||
architecture:
|
||||
required: false
|
||||
type: string
|
||||
default: amd64
|
||||
push_by_digest:
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
digest_artifact_name:
|
||||
required: false
|
||||
type: string
|
||||
default: ''
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
runs-on: ${{ inputs.runner }}
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
# The Docker context intentionally includes .git so the kernel build can
|
||||
# initialize its pinned submodules. Do not copy the checkout token with it.
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Free up disk space
|
||||
run: |
|
||||
# Display initial space
|
||||
echo "Initial disk space:"
|
||||
df -h
|
||||
|
||||
# Remove large directories directly
|
||||
sudo rm -rf /usr/share/dotnet
|
||||
sudo rm -rf /usr/local/lib/android
|
||||
sudo rm -rf /opt/ghc
|
||||
sudo rm -rf /usr/local/share/boost
|
||||
sudo rm -rf /usr/share/swift
|
||||
sudo rm -rf /usr/local/lib/node_modules
|
||||
sudo rm -rf /usr/local/share/powershell
|
||||
sudo rm -rf /usr/share/rust
|
||||
sudo rm -rf /usr/local/.ghcup
|
||||
|
||||
# Remove cached files
|
||||
sudo rm -rf /var/lib/apt/lists/*
|
||||
sudo rm -rf /var/cache/apt/archives/*
|
||||
|
||||
# Clean Docker
|
||||
docker system prune -af --volumes
|
||||
|
||||
# Display available space after cleanup
|
||||
echo "Disk space after cleanup:"
|
||||
df -h
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Normalize image reference
|
||||
id: image
|
||||
env:
|
||||
IMAGE: ghcr.io/${{ github.repository }}/${{ inputs.image_name }}
|
||||
run: echo "name=${IMAGE,,}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: Prepare tags
|
||||
id: prepare-tags
|
||||
run: |
|
||||
SHORT_SHA=$(echo ${{ github.sha }} | cut -c1-7)
|
||||
|
||||
TAGS="type=raw,value=${{ inputs.tag_suffix }}-sha-${SHORT_SHA}"
|
||||
|
||||
if [[ "${{ inputs.include_latest_tags }}" == "true" ]]; then
|
||||
TAGS="type=raw,value=${{ inputs.tag_suffix }}-latest\n${TAGS}"
|
||||
fi
|
||||
|
||||
# Tag the designated default variant as the global `latest` image
|
||||
if [[ "${{ inputs.include_latest_tags }}" == "true" && "${{ inputs.mark_as_latest }}" == "true" ]]; then
|
||||
TAGS="${TAGS}\ntype=raw,value=latest"
|
||||
fi
|
||||
|
||||
{
|
||||
echo "tags<<EOF"
|
||||
echo -e "$TAGS"
|
||||
echo "EOF"
|
||||
} >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Extract metadata for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ steps.image.outputs.name }}
|
||||
tags: ${{ steps.prepare-tags.outputs.tags }}
|
||||
|
||||
- name: Build and push Docker image
|
||||
if: ${{ !inputs.push_by_digest }}
|
||||
id: build-push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ${{ inputs.dockerfile_path }}
|
||||
platforms: linux/${{ inputs.architecture }}
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: ${{ inputs.build_args }}
|
||||
cache-from: type=gha,scope=${{ inputs.image_name }}-${{ inputs.tag_suffix }}-${{ inputs.architecture }}
|
||||
cache-to: type=gha,mode=max,scope=${{ inputs.image_name }}-${{ inputs.tag_suffix }}-${{ inputs.architecture }}
|
||||
|
||||
# Multi-architecture callers publish immutable manifests by digest here,
|
||||
# then create the shared user-facing tags in a single downstream job.
|
||||
# This prevents architecture jobs from racing to replace the same tag.
|
||||
- name: Build and push Docker image by digest
|
||||
if: ${{ inputs.push_by_digest }}
|
||||
id: build-push-digest
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ${{ inputs.dockerfile_path }}
|
||||
platforms: linux/${{ inputs.architecture }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: ${{ inputs.build_args }}
|
||||
outputs: type=image,name=${{ steps.image.outputs.name }},push-by-digest=true,name-canonical=true,push=true
|
||||
cache-from: type=gha,scope=${{ inputs.image_name }}-${{ inputs.tag_suffix }}-${{ inputs.architecture }}
|
||||
cache-to: type=gha,mode=max,scope=${{ inputs.image_name }}-${{ inputs.tag_suffix }}-${{ inputs.architecture }}
|
||||
|
||||
- name: Export digest
|
||||
if: ${{ inputs.push_by_digest }}
|
||||
env:
|
||||
DIGEST: ${{ steps.build-push-digest.outputs.digest }}
|
||||
run: |
|
||||
if [[ -z "${{ inputs.digest_artifact_name }}" ]]; then
|
||||
echo "digest_artifact_name is required when push_by_digest is true" >&2
|
||||
exit 1
|
||||
fi
|
||||
mkdir -p /tmp/digests
|
||||
touch "/tmp/digests/${DIGEST#sha256:}"
|
||||
|
||||
- name: Upload digest
|
||||
if: ${{ inputs.push_by_digest }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ inputs.digest_artifact_name }}
|
||||
path: /tmp/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
- name: Success message
|
||||
if: ${{ !inputs.push_by_digest }}
|
||||
run: |
|
||||
echo "✅ Python ${{ inputs.python_version }} image successfully built and pushed to ${{ steps.image.outputs.name }}:${{ inputs.tag_suffix }}-sha-${GITHUB_SHA::7}"
|
||||
echo "Digest: ${{ steps.build-push.outputs.digest }}"
|
||||
echo "To run tests with this image, manually trigger the 'Run Tests' workflow."
|
||||
|
||||
- name: Digest success message
|
||||
if: ${{ inputs.push_by_digest }}
|
||||
env:
|
||||
DIGEST: ${{ steps.build-push-digest.outputs.digest }}
|
||||
run: |
|
||||
echo "✅ Python ${{ inputs.python_version }} linux/${{ inputs.architecture }} image pushed as ${DIGEST}"
|
||||
@@ -1,106 +0,0 @@
|
||||
name: Build Image Template
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
python_version:
|
||||
required: true
|
||||
type: string
|
||||
dockerfile_path:
|
||||
required: true
|
||||
type: string
|
||||
tag_suffix:
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Free up disk space
|
||||
run: |
|
||||
# Display initial space
|
||||
echo "Initial disk space:"
|
||||
df -h
|
||||
|
||||
# Remove large directories directly
|
||||
sudo rm -rf /usr/share/dotnet
|
||||
sudo rm -rf /usr/local/lib/android
|
||||
sudo rm -rf /opt/ghc
|
||||
sudo rm -rf /usr/local/share/boost
|
||||
sudo rm -rf /usr/share/swift
|
||||
sudo rm -rf /usr/local/lib/node_modules
|
||||
sudo rm -rf /usr/local/share/powershell
|
||||
sudo rm -rf /usr/share/rust
|
||||
sudo rm -rf /usr/local/.ghcup
|
||||
|
||||
# Remove cached files
|
||||
sudo rm -rf /var/lib/apt/lists/*
|
||||
sudo rm -rf /var/cache/apt/archives/*
|
||||
|
||||
# Clean Docker
|
||||
docker system prune -af --volumes
|
||||
|
||||
# Display available space after cleanup
|
||||
echo "Disk space after cleanup:"
|
||||
df -h
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Prepare tags
|
||||
id: prepare-tags
|
||||
run: |
|
||||
SHORT_SHA=$(echo ${{ github.sha }} | cut -c1-7)
|
||||
|
||||
TAGS="type=raw,value=${{ inputs.tag_suffix }}-latest"
|
||||
TAGS="${TAGS}\ntype=raw,value=${{ inputs.tag_suffix }}-sha-${SHORT_SHA}"
|
||||
|
||||
# Set Python 3.10 as the default image
|
||||
if [[ "${{ inputs.python_version }}" == "3.10" ]]; then
|
||||
TAGS="${TAGS}\ntype=raw,value=latest"
|
||||
fi
|
||||
|
||||
{
|
||||
echo "tags<<EOF"
|
||||
echo -e "$TAGS"
|
||||
echo "EOF"
|
||||
} >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Extract metadata for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ghcr.io/${{ github.repository }}/fastvideo-dev
|
||||
tags: ${{ steps.prepare-tags.outputs.tags }}
|
||||
|
||||
- name: Build and push Docker image
|
||||
id: build-push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ${{ inputs.dockerfile_path }}
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
- name: Success message
|
||||
run: |
|
||||
echo "✅ Python ${{ inputs.python_version }} image successfully built and pushed to ghcr.io/${{ github.repository }}/fastvideo-dev:${{ inputs.tag_suffix }}-latest"
|
||||
echo "To run tests with this image, manually trigger the 'Run Tests' workflow."
|
||||
@@ -1,52 +0,0 @@
|
||||
name: Build and Push Docker Images
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
python_3_10:
|
||||
description: 'Build Python 3.10 image'
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
python_3_11:
|
||||
description: 'Build Python 3.11 image'
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
python_3_12:
|
||||
description: 'Build Python 3.12 image'
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
build-python-3-10:
|
||||
if: ${{ github.event.inputs.python_3_10 == 'true' }}
|
||||
uses: ./.github/workflows/build-image-template.yml
|
||||
with:
|
||||
python_version: '3.10'
|
||||
dockerfile_path: docker/Dockerfile.python3.10
|
||||
tag_suffix: py3.10
|
||||
secrets: inherit
|
||||
|
||||
build-python-3-11:
|
||||
if: ${{ github.event.inputs.python_3_11 == 'true' }}
|
||||
uses: ./.github/workflows/build-image-template.yml
|
||||
with:
|
||||
python_version: '3.11'
|
||||
dockerfile_path: docker/Dockerfile.python3.11
|
||||
tag_suffix: py3.11
|
||||
secrets: inherit
|
||||
|
||||
build-python-3-12:
|
||||
if: ${{ github.event.inputs.python_3_12 == 'true' }}
|
||||
uses: ./.github/workflows/build-image-template.yml
|
||||
with:
|
||||
python_version: '3.12'
|
||||
dockerfile_path: docker/Dockerfile.python3.12
|
||||
tag_suffix: py3.12
|
||||
secrets: inherit
|
||||
@@ -0,0 +1,100 @@
|
||||
name: Aggregate Test Status
|
||||
|
||||
on:
|
||||
status:
|
||||
|
||||
permissions:
|
||||
statuses: write
|
||||
|
||||
jobs:
|
||||
aggregate:
|
||||
if: >-
|
||||
github.event.context == 'direct-test-completed'
|
||||
&& github.event.state == 'success'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check and update aggregate status
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
with:
|
||||
script: |
|
||||
const sha = context.payload.sha;
|
||||
|
||||
const { data } = await github.rest.repos.getCombinedStatusForRef({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
ref: sha,
|
||||
per_page: 100,
|
||||
});
|
||||
|
||||
// Buildkite derives the GitHub context prefix from the label emoji.
|
||||
// Keep hard Full Suite lanes in test-tube/bar-chart namespaces and
|
||||
// Fastcheck lanes in microscope so targeted reruns cannot clear the
|
||||
// wrong aggregate status. Automatic PR jobs use pr-fastcheck while
|
||||
// slash-command and Full Suite jobs use ci; normalize the suffix
|
||||
// and keep the newest status for each logical lane.
|
||||
const FASTCHECK_PREFIXES = [
|
||||
'buildkite/pr-fastcheck/microscope-',
|
||||
'buildkite/ci/microscope-',
|
||||
];
|
||||
const FULL_SUITE_PREFIXES = [
|
||||
'buildkite/ci/test-tube-',
|
||||
'buildkite/ci/bar-chart-',
|
||||
];
|
||||
|
||||
function newestByLane(prefixes) {
|
||||
const statuses = new Map();
|
||||
for (const status of data.statuses) {
|
||||
const prefix = prefixes.find(p => status.context.startsWith(p));
|
||||
if (!prefix) continue;
|
||||
const lane = status.context.slice(prefix.length);
|
||||
const previous = statuses.get(lane);
|
||||
if (!previous || Date.parse(status.updated_at) > Date.parse(previous.updated_at)) {
|
||||
statuses.set(lane, status);
|
||||
}
|
||||
}
|
||||
return statuses;
|
||||
}
|
||||
|
||||
const fastcheck = newestByLane(FASTCHECK_PREFIXES);
|
||||
const fullSuiteOnly = newestByLane(FULL_SUITE_PREFIXES);
|
||||
const fastcheckPassed =
|
||||
fastcheck.size === 6
|
||||
&& [...fastcheck.values()].every(s => s.state === 'success');
|
||||
const fullSuitePassed =
|
||||
fastcheckPassed
|
||||
&& fullSuiteOnly.size === 14
|
||||
&& [...fullSuiteOnly.values()].every(s => s.state === 'success');
|
||||
|
||||
// Direct reruns may repair a failed suite, never create a gate for
|
||||
// a suite that did not run.
|
||||
const failedAggregate = context => data.statuses.some(
|
||||
s => s.context === context && s.state === 'failure'
|
||||
);
|
||||
|
||||
if (failedAggregate('fastcheck-passed') && fastcheckPassed) {
|
||||
core.info(
|
||||
`All ${fastcheck.size} fastcheck tests passed — updating fastcheck-passed`
|
||||
);
|
||||
await github.rest.repos.createCommitStatus({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
sha,
|
||||
state: 'success',
|
||||
context: 'fastcheck-passed',
|
||||
description: `All ${fastcheck.size} fastcheck tests passed`,
|
||||
});
|
||||
}
|
||||
|
||||
if (failedAggregate('full-suite-passed') && fullSuitePassed) {
|
||||
core.info(
|
||||
'All 20 full suite tests passed — updating full-suite-passed'
|
||||
);
|
||||
await github.rest.repos.createCommitStatus({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
sha,
|
||||
state: 'success',
|
||||
context: 'full-suite-passed',
|
||||
description: 'All 20 full suite tests passed',
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
name: macOS MLX Smoke
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
paths:
|
||||
- ".github/workflows/ci-macos-mlx.yml"
|
||||
- "fastvideo/mlx_runtime/**"
|
||||
- "fastvideo/tests/mlx/**"
|
||||
- "fastvideo/tests/platforms/test_mps_vsa_error.py"
|
||||
- "fastvideo/tests/platforms/test_cpu_sdpa.py"
|
||||
- "fastvideo/platforms/cpu.py"
|
||||
- "fastvideo/platforms/mps.py"
|
||||
- "fastvideo/platforms/__init__.py"
|
||||
- "fastvideo/__init__.py"
|
||||
- "examples/inference/basic/mlx_*.py"
|
||||
- "fastvideo/benchmarks/mlx_*.py"
|
||||
- "pyproject.toml"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: macos-mlx-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
mlx-smoke:
|
||||
if: github.event_name == 'workflow_dispatch' || github.event.pull_request.draft != true
|
||||
runs-on: macos-15
|
||||
timeout-minutes: 25
|
||||
env:
|
||||
FASTVIDEO_ATTENTION_BACKEND: TORCH_SDPA
|
||||
TOKENIZERS_PARALLELISM: "false"
|
||||
MASTER_ADDR: "127.0.0.1"
|
||||
MASTER_PORT: "29513"
|
||||
GLOO_SOCKET_IFNAME: lo0
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- uses: astral-sh/setup-uv@v3
|
||||
|
||||
- name: Install lightweight MLX smoke dependencies
|
||||
run: |
|
||||
uv pip install --system \
|
||||
--index-url https://download.pytorch.org/whl/cpu \
|
||||
torch==2.12.0 torchvision torchaudio
|
||||
uv pip install --system \
|
||||
pytest pytest-timeout numpy scipy pillow imageio einops cloudpickle filelock \
|
||||
PyYAML diffusers huggingface_hub remote-pdb safetensors loguru mlx \
|
||||
"ftfy>=6.3.1" "opencv-python>=4.10.0.84" psutil "transformers>=5.0.0"
|
||||
|
||||
- name: Show Apple runtime
|
||||
run: |
|
||||
python - <<'PY'
|
||||
import platform
|
||||
import mlx.core as mx
|
||||
import torch
|
||||
|
||||
print("machine:", platform.machine())
|
||||
print("processor:", platform.processor())
|
||||
print("mlx default device:", mx.default_device())
|
||||
device_info = mx.metal.device_info() if mx.metal.is_available() else "metal unavailable"
|
||||
print("mlx device_info:", device_info)
|
||||
print("torch:", torch.__version__)
|
||||
print("torch mps available:", torch.backends.mps.is_available())
|
||||
PY
|
||||
|
||||
- name: Run MLX smoke tests
|
||||
run: |
|
||||
python -m pytest \
|
||||
fastvideo/mlx_runtime/tests/ \
|
||||
fastvideo/tests/mlx/test_dmd_sampling.py \
|
||||
fastvideo/tests/mlx/test_memory_limits.py \
|
||||
fastvideo/tests/mlx/test_quant_capability.py \
|
||||
fastvideo/tests/mlx/test_mlx_dit_parity.py \
|
||||
fastvideo/tests/mlx/test_mlx_compile_parity.py \
|
||||
fastvideo/tests/mlx/test_mlx_checkpoint.py \
|
||||
fastvideo/tests/mlx/test_mlx_checkpoint_compat.py \
|
||||
fastvideo/tests/mlx/test_mlx_affine_dq_gemm.py \
|
||||
fastvideo/tests/mlx/test_mlx_minimax_h3_parity.py \
|
||||
fastvideo/tests/mlx/test_mlx_minimax_h3_vsa.py \
|
||||
fastvideo/tests/mlx/test_mlx_minimax_h3_vsa_regressions.py \
|
||||
fastvideo/tests/mlx/test_mlx_minimax_h3_fast_mode.py \
|
||||
fastvideo/tests/mlx/test_mlx_minimax_h3_fast_spatial.py \
|
||||
fastvideo/tests/mlx/test_mlx_fastwan_benchmark.py \
|
||||
fastvideo/tests/mlx/test_taehv_decode.py \
|
||||
fastvideo/tests/mlx/test_frame_upsample.py \
|
||||
fastvideo/tests/mlx/test_mlx_fast_spatial.py \
|
||||
fastvideo/tests/mlx/test_mlx_refine.py \
|
||||
fastvideo/tests/mlx/test_mlx_prompt_enhance.py \
|
||||
fastvideo/tests/mlx/test_mlx_prompt_to_video_decode.py \
|
||||
fastvideo/tests/mlx/test_mlx_wan22_prompt_cache_fingerprint.py \
|
||||
fastvideo/tests/mlx/test_wan22_sample.py \
|
||||
fastvideo/tests/mlx/test_windowed_attention.py \
|
||||
fastvideo/tests/mlx/test_mlx_rife_interpolation.py::test_rife_download_unavailable_has_specific_error \
|
||||
fastvideo/tests/mlx/test_mlx_rife_interpolation.py::test_rife_backend_regression_is_not_skip_eligible \
|
||||
fastvideo/tests/platforms/test_mps_vsa_error.py \
|
||||
fastvideo/tests/platforms/test_cpu_sdpa.py \
|
||||
-v -s --timeout=120 -o faulthandler_timeout=120
|
||||
|
||||
# Same tests on MLX's CPU backend. Hosted macOS runners are scarce and
|
||||
# slower to schedule; this Linux job gives fast PR signal on the identical
|
||||
# graph (the parity tests were designed to be backend-agnostic), while the
|
||||
# macOS job above stays the source of truth for Metal behavior.
|
||||
mlx-smoke-linux-cpu:
|
||||
if: github.event_name == 'workflow_dispatch' || github.event.pull_request.draft != true
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
env:
|
||||
FASTVIDEO_ATTENTION_BACKEND: TORCH_SDPA
|
||||
TOKENIZERS_PARALLELISM: "false"
|
||||
MASTER_ADDR: localhost
|
||||
MASTER_PORT: "29513"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- uses: astral-sh/setup-uv@v3
|
||||
|
||||
- name: Install lightweight MLX smoke dependencies (CPU backend)
|
||||
run: |
|
||||
uv pip install --system \
|
||||
--index-url https://download.pytorch.org/whl/cpu \
|
||||
torch==2.12.0 torchvision torchaudio
|
||||
uv pip install --system \
|
||||
pytest pytest-timeout numpy scipy pillow imageio einops cloudpickle filelock \
|
||||
PyYAML diffusers huggingface_hub remote-pdb safetensors loguru "mlx[cpu]" \
|
||||
"ftfy>=6.3.1" "opencv-python>=4.10.0.84" psutil "transformers>=5.0.0"
|
||||
|
||||
- name: Run MLX smoke tests (CPU backend)
|
||||
run: |
|
||||
python -m pytest \
|
||||
fastvideo/mlx_runtime/tests/ \
|
||||
fastvideo/tests/mlx/test_dmd_sampling.py \
|
||||
fastvideo/tests/mlx/test_memory_limits.py \
|
||||
fastvideo/tests/mlx/test_quant_capability.py \
|
||||
fastvideo/tests/mlx/test_mlx_dit_parity.py \
|
||||
fastvideo/tests/mlx/test_mlx_compile_parity.py \
|
||||
fastvideo/tests/mlx/test_mlx_checkpoint.py \
|
||||
fastvideo/tests/mlx/test_mlx_checkpoint_compat.py \
|
||||
fastvideo/tests/mlx/test_mlx_affine_dq_gemm.py \
|
||||
fastvideo/tests/mlx/test_mlx_minimax_h3_parity.py \
|
||||
fastvideo/tests/mlx/test_mlx_minimax_h3_vsa.py \
|
||||
fastvideo/tests/mlx/test_mlx_minimax_h3_vsa_regressions.py \
|
||||
fastvideo/tests/mlx/test_mlx_minimax_h3_fast_mode.py \
|
||||
fastvideo/tests/mlx/test_mlx_minimax_h3_fast_spatial.py \
|
||||
fastvideo/tests/mlx/test_mlx_fastwan_benchmark.py \
|
||||
fastvideo/tests/mlx/test_taehv_decode.py \
|
||||
fastvideo/tests/mlx/test_frame_upsample.py \
|
||||
fastvideo/tests/mlx/test_mlx_fast_spatial.py \
|
||||
fastvideo/tests/mlx/test_mlx_refine.py \
|
||||
fastvideo/tests/mlx/test_mlx_prompt_enhance.py \
|
||||
fastvideo/tests/mlx/test_mlx_prompt_to_video_decode.py \
|
||||
fastvideo/tests/mlx/test_mlx_wan22_prompt_cache_fingerprint.py \
|
||||
fastvideo/tests/mlx/test_wan22_sample.py \
|
||||
fastvideo/tests/mlx/test_windowed_attention.py \
|
||||
fastvideo/tests/mlx/test_mlx_rife_interpolation.py::test_rife_download_unavailable_has_specific_error \
|
||||
fastvideo/tests/mlx/test_mlx_rife_interpolation.py::test_rife_backend_regression_is_not_skip_eligible \
|
||||
fastvideo/tests/platforms/test_mps_vsa_error.py \
|
||||
fastvideo/tests/platforms/test_cpu_sdpa.py \
|
||||
-v -s --timeout=120 -o faulthandler_timeout=120
|
||||
@@ -0,0 +1,65 @@
|
||||
name: pre-commit
|
||||
|
||||
on:
|
||||
# pull_request_target instead of pull_request: the workflow definition and
|
||||
# the hook config are always taken from the BASE branch, so fork /
|
||||
# first-time-contributor PRs run immediately without a maintainer clicking
|
||||
# "Approve and run". The PR head is checked out as data only.
|
||||
pull_request_target:
|
||||
branches: [main]
|
||||
workflow_call:
|
||||
inputs:
|
||||
ref:
|
||||
description: 'Git ref to checkout (defaults to github.ref)'
|
||||
required: false
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
pre-commit:
|
||||
if: github.event.pull_request.draft != true
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.ref || '' }}
|
||||
# For PR events, lint the PR head — but keep the hook definitions from
|
||||
# the base branch so an untrusted PR cannot alter what gets executed.
|
||||
# The gate scripts are saved too: the self-test step below executes them,
|
||||
# so it must run the base-branch copies, not the PR head's.
|
||||
- name: Save trusted hook config and gate scripts
|
||||
if: github.event_name == 'pull_request_target'
|
||||
run: |
|
||||
cp .pre-commit-config.yaml "$RUNNER_TEMP/trusted-pre-commit-config.yaml"
|
||||
cp -a .github/scripts "$RUNNER_TEMP/trusted-scripts"
|
||||
echo "GATE_SCRIPTS_DIR=$RUNNER_TEMP/trusted-scripts" >> "$GITHUB_ENV"
|
||||
# allow-unsafe-pr-checkout acknowledges checkout's pull_request_target
|
||||
# guard: the head is data for the trusted hooks to lint; nothing from it
|
||||
# is executed (config and gate scripts are pinned to the base branch
|
||||
# above) and credentials are not persisted. SHA-pinned to v4.4.0 because
|
||||
# actionlint's action schema does not know the new input yet.
|
||||
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
||||
if: github.event_name == 'pull_request_target'
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
persist-credentials: false
|
||||
allow-unsafe-pr-checkout: true
|
||||
- name: Restore trusted hook config
|
||||
if: github.event_name == 'pull_request_target'
|
||||
run: cp "$RUNNER_TEMP/trusted-pre-commit-config.yaml" .pre-commit-config.yaml
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
- run: echo "::add-matcher::.github/workflows/matchers/actionlint.json"
|
||||
- run: echo "::add-matcher::.github/workflows/matchers/mypy.json"
|
||||
- run: echo "::add-matcher::.github/workflows/matchers/ruff.json"
|
||||
- uses: pre-commit/action@v3.0.1
|
||||
with:
|
||||
extra_args: --all-files --hook-stage manual
|
||||
# After pre-commit so a self-test failure cannot mask lint failures.
|
||||
# GATE_SCRIPTS_DIR points at the base-branch copy on fork PRs (set above);
|
||||
# push / workflow_call runs use the checked-out tree directly.
|
||||
- name: Full-suite gate self-test
|
||||
run: bash "${GATE_SCRIPTS_DIR:-.github/scripts}/test_gate_full_suite.sh"
|
||||
@@ -0,0 +1,44 @@
|
||||
name: Scheduled Full SSIM
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 5 * * 0"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
trigger:
|
||||
if: github.repository == 'hao-ai-lab/FastVideo'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Trigger weekly full SSIM on Slinky Slurm
|
||||
env:
|
||||
BUILDKITE_API_TOKEN: ${{ secrets.BUILDKITE_API_TOKEN }}
|
||||
SOURCE_SHA: ${{ github.sha }}
|
||||
SOURCE_BRANCH: ${{ github.event.repository.default_branch }}
|
||||
BK_ORG: ${{ vars.BUILDKITE_ORG_SLUG }}
|
||||
BK_PIPELINE: ${{ vars.BUILDKITE_PIPELINE_SLUG }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
curl -sS --fail-with-body -X POST \
|
||||
"https://api.buildkite.com/v2/organizations/${BK_ORG}/pipelines/${BK_PIPELINE}/builds" \
|
||||
-H "Authorization: Bearer $BUILDKITE_API_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data-raw "$(jq -n \
|
||||
--arg commit "$SOURCE_SHA" \
|
||||
--arg branch "$SOURCE_BRANCH" \
|
||||
'{
|
||||
commit: $commit,
|
||||
branch: $branch,
|
||||
message: "Weekly full SSIM on Slinky Slurm",
|
||||
ignore_pipeline_branch_filters: true,
|
||||
env: {
|
||||
TEST_SCOPE: "scheduled",
|
||||
FULL_SUITE: "false",
|
||||
TEST_TYPE: "ssim",
|
||||
PR_NUMBER: "false",
|
||||
PR_TITLE: "Scheduled full SSIM"
|
||||
}
|
||||
}')"
|
||||
@@ -0,0 +1,268 @@
|
||||
name: Slash Commands
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
statuses: write
|
||||
|
||||
jobs:
|
||||
handle-merge:
|
||||
if: >-
|
||||
github.event.issue.pull_request != null
|
||||
&& startsWith(github.event.comment.body, '/merge')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check write permission
|
||||
id: perm
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
with:
|
||||
script: |
|
||||
const { data: perm } = await github.rest.repos.getCollaboratorPermissionLevel({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
username: context.payload.comment.user.login,
|
||||
});
|
||||
const hasWrite = ['admin', 'write'].includes(perm.permission);
|
||||
if (!hasWrite) {
|
||||
core.setFailed(`User ${context.payload.comment.user.login} lacks write permission (has: ${perm.permission}).`);
|
||||
}
|
||||
core.setOutput('has_write', String(hasWrite));
|
||||
|
||||
- name: Add ready label
|
||||
if: steps.perm.outputs.has_write == 'true'
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
with:
|
||||
script: |
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
const prNumber = context.payload.issue.number;
|
||||
await github.rest.issues.addLabels({ owner, repo, issue_number: prNumber, labels: ['ready'] });
|
||||
|
||||
- name: React to comment
|
||||
if: steps.perm.outputs.has_write == 'true'
|
||||
continue-on-error: true
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
with:
|
||||
script: |
|
||||
await github.rest.reactions.createForIssueComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: context.payload.comment.id,
|
||||
content: 'rocket',
|
||||
});
|
||||
|
||||
trigger-merge-gate:
|
||||
needs: handle-merge
|
||||
if: needs.handle-merge.result == 'success'
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
pull-requests: read
|
||||
uses: ./.github/workflows/ci-trigger-full-suite.yml
|
||||
with:
|
||||
pr_number: ${{ github.event.issue.number }}
|
||||
secrets:
|
||||
BUILDKITE_API_TOKEN: ${{ secrets.BUILDKITE_API_TOKEN }}
|
||||
|
||||
parse-command:
|
||||
if: >-
|
||||
github.event.issue.pull_request != null
|
||||
&& startsWith(github.event.comment.body, '/test')
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
test_type: ${{ steps.parse.outputs.test_type }}
|
||||
test_scope: ${{ steps.parse.outputs.test_scope }}
|
||||
full_suite: ${{ steps.parse.outputs.full_suite }}
|
||||
pr_sha: ${{ steps.pr.outputs.sha }}
|
||||
pr_branch: ${{ steps.pr.outputs.branch }}
|
||||
has_write: ${{ steps.perm.outputs.has_write }}
|
||||
steps:
|
||||
- name: Check write permission
|
||||
id: perm
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
with:
|
||||
script: |
|
||||
const { data: perm } = await github.rest.repos.getCollaboratorPermissionLevel({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
username: context.payload.comment.user.login,
|
||||
});
|
||||
const hasWrite = ['admin', 'write'].includes(perm.permission);
|
||||
core.setOutput('has_write', String(hasWrite));
|
||||
if (!hasWrite) {
|
||||
core.info(`User ${context.payload.comment.user.login} lacks write permission — ignoring.`);
|
||||
}
|
||||
|
||||
- name: Parse /test command
|
||||
id: parse
|
||||
if: steps.perm.outputs.has_write == 'true'
|
||||
shell: bash
|
||||
env:
|
||||
COMMENT: ${{ github.event.comment.body }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
TEST_NAME=$(echo "$COMMENT" | grep -oP '(?<=/test\s)\S+' | head -1 || true)
|
||||
|
||||
VALID="encoder vae transformer kernel unit dreamverse ssim golden-gate training lora-inference lora-training lora-extraction distillation self-forcing vsa vmoba performance api train-framework eval unit-ci kernel-ci dreamverse-ci ssim-ci golden-gate-ci encoder-ci vae-ci transformer-ci lora-inference-ci lora-training-ci lora-extraction-ci training-ci distillation-ci self-forcing-ci vsa-ci vmoba-ci performance-ci api-ci train-framework-ci eval-ci full fastcheck pre-commit"
|
||||
if [ -z "$TEST_NAME" ] || ! echo "$VALID" | grep -qw "$TEST_NAME"; then
|
||||
echo "Unknown test: '$TEST_NAME'. Valid: $VALID"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
declare -A MAP=(
|
||||
[encoder]=encoder [vae]=vae [transformer]=transformer
|
||||
[kernel]=kernel_tests [unit]=unit_test [unit-ci]=unit_test_ci
|
||||
[kernel-ci]=kernel_tests_ci [dreamverse-ci]=dreamverse_app_ci
|
||||
[ssim-ci]=ssim_ci [vmoba-ci]=inference_vmoba_ci
|
||||
[golden-gate-ci]=golden_gate_ci [training-ci]=training_ci
|
||||
[encoder-ci]=encoder_ci [vae-ci]=vae_ci [transformer-ci]=transformer_ci
|
||||
[lora-inference-ci]=inference_lora_ci [lora-training-ci]=training_lora_ci
|
||||
[lora-extraction-ci]=lora_extraction_ci [distillation-ci]=distillation_dmd_ci
|
||||
[self-forcing-ci]=self_forcing_ci [vsa-ci]=training_vsa_ci
|
||||
[performance-ci]=performance_ci [api-ci]=api_server_ci
|
||||
[train-framework-ci]=train_framework_ci [eval-ci]=eval_ci
|
||||
[dreamverse]=dreamverse_app
|
||||
[ssim]=ssim [golden-gate]=golden_gate [training]=training
|
||||
[lora-inference]=inference_lora [lora-training]=training_lora
|
||||
[lora-extraction]=lora_extraction
|
||||
[distillation]=distillation_dmd [self-forcing]=self_forcing
|
||||
[vsa]=training_vsa [vmoba]=inference_vmoba
|
||||
[performance]=performance [api]=api_server
|
||||
[train-framework]=train_framework [eval]=eval
|
||||
)
|
||||
|
||||
if [ "$TEST_NAME" = "full" ]; then
|
||||
{
|
||||
echo "test_type=all"
|
||||
echo "test_scope=full"
|
||||
echo "full_suite=true"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
elif [ "$TEST_NAME" = "fastcheck" ]; then
|
||||
{
|
||||
echo "test_type=fastcheck"
|
||||
echo "test_scope=fastcheck"
|
||||
echo "full_suite=false"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
elif [ "$TEST_NAME" = "pre-commit" ]; then
|
||||
{
|
||||
echo "test_type="
|
||||
echo "test_scope=precommit"
|
||||
echo "full_suite=false"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
{
|
||||
echo "test_type=${MAP[$TEST_NAME]}"
|
||||
echo "test_scope=direct"
|
||||
echo "full_suite=false"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Get PR details
|
||||
id: pr
|
||||
if: steps.perm.outputs.has_write == 'true'
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
with:
|
||||
script: |
|
||||
const { data: pr } = await github.rest.pulls.get({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: context.payload.issue.number,
|
||||
});
|
||||
core.setOutput('sha', pr.head.sha);
|
||||
core.setOutput('branch', pr.head.ref);
|
||||
|
||||
- name: React to comment
|
||||
if: steps.perm.outputs.has_write == 'true'
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
with:
|
||||
script: |
|
||||
await github.rest.reactions.createForIssueComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: context.payload.comment.id,
|
||||
content: 'rocket',
|
||||
});
|
||||
|
||||
pre-commit:
|
||||
needs: parse-command
|
||||
if: >-
|
||||
needs.parse-command.outputs.has_write == 'true'
|
||||
&& needs.parse-command.outputs.test_scope == 'precommit'
|
||||
uses: ./.github/workflows/ci-precommit.yml
|
||||
with:
|
||||
ref: refs/pull/${{ github.event.issue.number }}/merge
|
||||
|
||||
post-precommit-status:
|
||||
needs: [parse-command, pre-commit]
|
||||
if: always() && needs.parse-command.outputs.test_scope == 'precommit'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
env:
|
||||
PR_SHA: ${{ needs.parse-command.outputs.pr_sha }}
|
||||
RESULT: ${{ needs.pre-commit.result }}
|
||||
with:
|
||||
script: |
|
||||
const state = process.env.RESULT === 'success' ? 'success' : 'failure';
|
||||
await github.rest.repos.createCommitStatus({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
sha: process.env.PR_SHA,
|
||||
state,
|
||||
context: 'pre-commit',
|
||||
description: `Triggered via /test pre-commit (${state})`,
|
||||
});
|
||||
|
||||
trigger-buildkite:
|
||||
needs: parse-command
|
||||
if: >-
|
||||
needs.parse-command.outputs.has_write == 'true'
|
||||
&& needs.parse-command.outputs.test_type != ''
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Trigger Buildkite
|
||||
env:
|
||||
BUILDKITE_API_TOKEN: ${{ secrets.BUILDKITE_API_TOKEN }}
|
||||
PR_SHA: ${{ needs.parse-command.outputs.pr_sha }}
|
||||
PR_BRANCH: ${{ needs.parse-command.outputs.pr_branch }}
|
||||
PR_NUMBER: ${{ github.event.issue.number }}
|
||||
TEST_SCOPE: ${{ needs.parse-command.outputs.test_scope }}
|
||||
FULL_SUITE: ${{ needs.parse-command.outputs.full_suite }}
|
||||
TEST_TYPE: ${{ needs.parse-command.outputs.test_type }}
|
||||
PR_TITLE: ${{ github.event.issue.title }}
|
||||
BK_ORG: ${{ vars.BUILDKITE_ORG_SLUG }}
|
||||
BK_PIPELINE: ${{ vars.BUILDKITE_PIPELINE_SLUG }}
|
||||
run: |
|
||||
curl -sS --fail-with-body -X POST \
|
||||
"https://api.buildkite.com/v2/organizations/${BK_ORG}/pipelines/${BK_PIPELINE}/builds" \
|
||||
-H "Authorization: Bearer $BUILDKITE_API_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data-raw "$(jq -n \
|
||||
--arg commit "$PR_SHA" \
|
||||
--arg branch "$PR_BRANCH" \
|
||||
--arg message "/test ${TEST_TYPE} on PR #${PR_NUMBER}" \
|
||||
--argjson pr_id "$PR_NUMBER" \
|
||||
--arg test_scope "$TEST_SCOPE" \
|
||||
--arg full_suite "$FULL_SUITE" \
|
||||
--arg test_type "$TEST_TYPE" \
|
||||
--arg pr_number "$PR_NUMBER" \
|
||||
--arg pr_title "$PR_TITLE" \
|
||||
'{
|
||||
commit: $commit,
|
||||
branch: $branch,
|
||||
message: $message,
|
||||
ignore_pipeline_branch_filters: true,
|
||||
pull_request_id: $pr_id,
|
||||
pull_request_base_branch: "main",
|
||||
env: {
|
||||
TEST_SCOPE: $test_scope,
|
||||
FULL_SUITE: $full_suite,
|
||||
TEST_TYPE: $test_type,
|
||||
PR_NUMBER: $pr_number,
|
||||
PR_TITLE: $pr_title
|
||||
}
|
||||
}')"
|
||||
@@ -0,0 +1,267 @@
|
||||
name: Trigger Merge Gate
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [labeled, synchronize]
|
||||
workflow_call:
|
||||
inputs:
|
||||
pr_number:
|
||||
description: Pull request number to enter into the merge gate
|
||||
required: true
|
||||
type: number
|
||||
secrets:
|
||||
BUILDKITE_API_TOKEN:
|
||||
required: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
actions: read
|
||||
|
||||
jobs:
|
||||
trigger:
|
||||
if: >-
|
||||
inputs.pr_number > 0
|
||||
|| (github.event.action == 'labeled' && github.event.label.name == 'ready')
|
||||
|| github.event.action == 'synchronize'
|
||||
runs-on: ubuntu-latest
|
||||
# Job-level concurrency: only this guarded job acquires the group, so an
|
||||
# unrelated `labeled` event (which skips the job) cannot cancel an in-flight
|
||||
# gate and then skip its replacement. The newest real trigger (`ready`,
|
||||
# push, or `/merge`) supersedes the in-flight run, whose Buildkite build the
|
||||
# cancel step below replaces.
|
||||
concurrency:
|
||||
group: merge-gate-${{ inputs.pr_number || github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
# Gate below may wait for cheap checks (up to MAX_WAIT_SECS = 25 min).
|
||||
timeout-minutes: 35
|
||||
steps:
|
||||
- name: Check ready label
|
||||
id: check
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
env:
|
||||
CALLED_PR_NUMBER: ${{ inputs.pr_number }}
|
||||
with:
|
||||
script: |
|
||||
const eventPrNumber = context.payload.pull_request?.number;
|
||||
const calledPrNumber = Number(process.env.CALLED_PR_NUMBER);
|
||||
const prNumber = eventPrNumber ?? calledPrNumber;
|
||||
if (!Number.isSafeInteger(prNumber) || prNumber <= 0) {
|
||||
core.setFailed(`Invalid pull request number: ${process.env.CALLED_PR_NUMBER}`);
|
||||
return;
|
||||
}
|
||||
const { data: pr } = await github.rest.pulls.get({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: prNumber,
|
||||
});
|
||||
if (pr.state !== 'open') {
|
||||
core.setFailed(`PR #${prNumber} is not open.`);
|
||||
return;
|
||||
}
|
||||
if (pr.base.repo.full_name !== context.payload.repository.full_name
|
||||
|| pr.base.ref !== context.payload.repository.default_branch) {
|
||||
core.setFailed(`PR #${prNumber} does not target this repository's default branch.`);
|
||||
return;
|
||||
}
|
||||
const hasReady = pr.labels.some(l => l.name === 'ready');
|
||||
core.setOutput('has_ready', String(hasReady));
|
||||
core.setOutput('changed_files', String(pr.changed_files));
|
||||
core.setOutput('pr_number', String(pr.number));
|
||||
core.setOutput('head_sha', pr.head.sha);
|
||||
core.setOutput('head_ref', pr.head.ref);
|
||||
core.setOutput('base_sha', pr.base.sha);
|
||||
core.setOutput('title', pr.title);
|
||||
if (!hasReady) core.info('No ready label — skipping merge-gate trigger.');
|
||||
|
||||
- name: Cancel previous Buildkite builds
|
||||
# Cancelling stale builds only saves agent time. If it cannot run, the
|
||||
# merge gate must still be triggered by the steps below, so a failure
|
||||
# here is reported and stepped over rather than ending the job.
|
||||
continue-on-error: true
|
||||
timeout-minutes: 3
|
||||
if: steps.check.outputs.has_ready == 'true'
|
||||
env:
|
||||
BK_ORG: ${{ vars.BUILDKITE_ORG_SLUG }}
|
||||
BK_PIPELINE: ${{ vars.BUILDKITE_PIPELINE_SLUG }}
|
||||
BUILDKITE_API_TOKEN: ${{ secrets.BUILDKITE_API_TOKEN }}
|
||||
PR_BRANCH: ${{ steps.check.outputs.head_ref }}
|
||||
PR_NUMBER: ${{ steps.check.outputs.pr_number }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
response_file=$(mktemp)
|
||||
builds_file=$(mktemp)
|
||||
trap 'rm -f "$response_file" "$builds_file"' EXIT
|
||||
|
||||
if [[ ! "$PR_NUMBER" =~ ^[1-9][0-9]*$ ]]; then
|
||||
echo "::warning::Invalid pull request number; stale Buildkite builds may continue."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! curl -sS --fail-with-body --connect-timeout 5 --max-time 20 --get \
|
||||
-H "Authorization: Bearer $BUILDKITE_API_TOKEN" \
|
||||
--data-urlencode "branch=$PR_BRANCH" \
|
||||
--data-urlencode "state[]=running" \
|
||||
--data-urlencode "state[]=scheduled" \
|
||||
--data-urlencode "state[]=failing" \
|
||||
--data-urlencode "exclude_jobs=true" \
|
||||
--data-urlencode "exclude_pipeline=true" \
|
||||
--output "$response_file" \
|
||||
"https://api.buildkite.com/v2/organizations/${BK_ORG}/pipelines/${BK_PIPELINE}/builds"; then
|
||||
echo "::warning::Could not list Buildkite builds; stale merge-gate builds may continue."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! jq -e '
|
||||
if type != "array" then false
|
||||
else all(.[];
|
||||
if type != "object" then false
|
||||
else
|
||||
(.number | if type == "number" then . > 0 and floor == . else false end)
|
||||
and (
|
||||
(.env? | if . == null then {} else . end) as $env
|
||||
| if ($env | type) != "object" then false
|
||||
else
|
||||
($env.TEST_SCOPE? | . == null or type == "string")
|
||||
and ($env.PR_NUMBER? | . == null or type == "string")
|
||||
end
|
||||
)
|
||||
end
|
||||
)
|
||||
end
|
||||
' "$response_file" >/dev/null 2>&1; then
|
||||
# Do not print the response body: it is remote data and may contain
|
||||
# multiline values that would be interpreted as workflow commands.
|
||||
echo "::warning::Buildkite returned an invalid build list; stale merge-gate builds may continue."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Match both branch and PR number: forks can reuse the same branch name.
|
||||
if ! jq -r --arg pr_number "$PR_NUMBER" '
|
||||
.[]
|
||||
| select((.env.TEST_SCOPE? == "merge") and (.env.PR_NUMBER? == $pr_number))
|
||||
| .number
|
||||
' "$response_file" > "$builds_file"; then
|
||||
echo "::warning::Could not select stale Buildkite builds; stale merge-gate builds may continue."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cancellation_failed=0
|
||||
while IFS= read -r build_num; do
|
||||
echo "Cancelling Buildkite build #$build_num"
|
||||
if ! curl -sS --fail-with-body --connect-timeout 5 --max-time 20 -o /dev/null -X PUT \
|
||||
-H "Authorization: Bearer $BUILDKITE_API_TOKEN" \
|
||||
"https://api.buildkite.com/v2/organizations/${BK_ORG}/pipelines/${BK_PIPELINE}/builds/${build_num}/cancel"; then
|
||||
echo "::warning::Could not cancel Buildkite build #$build_num; trying remaining builds."
|
||||
cancellation_failed=1
|
||||
fi
|
||||
done < "$builds_file"
|
||||
|
||||
if (( cancellation_failed != 0 )); then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check out the immutable BASE SHA: neither pull_request_target nor the
|
||||
# privileged slash-command call may run code from the untrusted PR head.
|
||||
- name: Checkout trusted merge planner
|
||||
if: steps.check.outputs.has_ready == 'true'
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ steps.check.outputs.base_sha }}
|
||||
persist-credentials: false
|
||||
|
||||
- name: Collect changed paths
|
||||
if: steps.check.outputs.has_ready == 'true'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
PR_NUMBER: ${{ steps.check.outputs.pr_number }}
|
||||
EXPECTED_CHANGED_FILES: ${{ steps.check.outputs.changed_files }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
changed_json="$RUNNER_TEMP/merge-changed-files.json"
|
||||
changed_paths="$RUNNER_TEMP/merge-changed-paths.txt"
|
||||
removed_paths="$RUNNER_TEMP/merge-removed-paths.txt"
|
||||
: > "$removed_paths"
|
||||
if gh api --paginate --slurp \
|
||||
"repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/files?per_page=100" \
|
||||
> "$changed_json"; then
|
||||
observed=$(jq '[.[][] | .filename] | unique | length' "$changed_json")
|
||||
if [ "$observed" = "$EXPECTED_CHANGED_FILES" ]; then
|
||||
jq -r '.[][] | .filename, (.previous_filename // empty)' "$changed_json" \
|
||||
| sort -u > "$changed_paths"
|
||||
# Paths absent from the PR head, so the planner never selects a deleted test.
|
||||
jq -r '.[][] | if .status == "removed" then .filename
|
||||
elif .status == "renamed" then (.previous_filename // empty) else empty end' \
|
||||
"$changed_json" | sort -u > "$removed_paths"
|
||||
else
|
||||
echo "::warning::Changed-file API returned $observed of $EXPECTED_CHANGED_FILES paths; selecting all merge lanes."
|
||||
echo '__FASTVIDEO_CI_PLAN_ALL__' > "$changed_paths"
|
||||
fi
|
||||
else
|
||||
echo "::warning::Changed-file API failed; selecting all merge lanes."
|
||||
echo '__FASTVIDEO_CI_PLAN_ALL__' > "$changed_paths"
|
||||
fi
|
||||
|
||||
- name: Select minimal merge tests
|
||||
id: plan
|
||||
if: steps.check.outputs.has_ready == 'true'
|
||||
run: |
|
||||
python3 .github/scripts/plan_merge_ci.py \
|
||||
--paths-file "$RUNNER_TEMP/merge-changed-paths.txt" \
|
||||
--removed-paths-file "$RUNNER_TEMP/merge-removed-paths.txt" \
|
||||
--github-output "$GITHUB_OUTPUT" \
|
||||
--summary-file "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Wait for pre-commit and docs build
|
||||
if: steps.check.outputs.has_ready == 'true'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
PR_SHA: ${{ steps.check.outputs.head_sha }}
|
||||
PR_NUMBER: ${{ steps.check.outputs.pr_number }}
|
||||
run: bash .github/scripts/gate_full_suite.sh
|
||||
|
||||
- name: Trigger Buildkite merge gate
|
||||
if: steps.check.outputs.has_ready == 'true'
|
||||
env:
|
||||
BUILDKITE_API_TOKEN: ${{ secrets.BUILDKITE_API_TOKEN }}
|
||||
PR_SHA: ${{ steps.check.outputs.head_sha }}
|
||||
PR_BRANCH: ${{ steps.check.outputs.head_ref }}
|
||||
PR_NUMBER: ${{ steps.check.outputs.pr_number }}
|
||||
PR_TITLE: ${{ steps.check.outputs.title }}
|
||||
BK_ORG: ${{ vars.BUILDKITE_ORG_SLUG }}
|
||||
BK_PIPELINE: ${{ vars.BUILDKITE_PIPELINE_SLUG }}
|
||||
MERGE_TEST_PLAN: ${{ steps.plan.outputs.merge_test_plan }}
|
||||
MERGE_GOLDEN_TESTS: ${{ steps.plan.outputs.merge_golden_tests }}
|
||||
MERGE_SSIM_TESTS: ${{ steps.plan.outputs.merge_ssim_tests }}
|
||||
MERGE_PLAN_LABEL: ${{ steps.plan.outputs.merge_plan_label }}
|
||||
run: |
|
||||
curl -sS --fail-with-body -X POST \
|
||||
"https://api.buildkite.com/v2/organizations/${BK_ORG}/pipelines/${BK_PIPELINE}/builds" \
|
||||
-H "Authorization: Bearer $BUILDKITE_API_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data-raw "$(jq -n \
|
||||
--arg commit "$PR_SHA" \
|
||||
--arg branch "$PR_BRANCH" \
|
||||
--arg message "Merge gate [${MERGE_PLAN_LABEL}] for PR #${PR_NUMBER}" \
|
||||
--arg pr_title "$PR_TITLE" \
|
||||
--arg merge_test_plan "$MERGE_TEST_PLAN" \
|
||||
--arg merge_golden_tests "$MERGE_GOLDEN_TESTS" \
|
||||
--arg merge_ssim_tests "$MERGE_SSIM_TESTS" \
|
||||
--argjson pr_id "$PR_NUMBER" \
|
||||
'{
|
||||
commit: $commit,
|
||||
branch: $branch,
|
||||
message: $message,
|
||||
ignore_pipeline_branch_filters: true,
|
||||
pull_request_id: $pr_id,
|
||||
pull_request_base_branch: "main",
|
||||
env: {
|
||||
TEST_SCOPE: "merge",
|
||||
FULL_SUITE: "true",
|
||||
MERGE_TEST_PLAN: $merge_test_plan,
|
||||
MERGE_GOLDEN_TESTS: $merge_golden_tests,
|
||||
MERGE_SSIM_TESTS: $merge_ssim_tests,
|
||||
PR_NUMBER: ($pr_id | tostring),
|
||||
PR_TITLE: $pr_title
|
||||
}
|
||||
}')"
|
||||
@@ -0,0 +1,65 @@
|
||||
name: Auto-Label Issues
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened, edited]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
label-issues:
|
||||
if: github.repository == 'hao-ai-lab/FastVideo'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Label by keywords
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
with:
|
||||
script: |
|
||||
const title = context.payload.issue.title.toLowerCase();
|
||||
const body = (context.payload.issue.body || '').toLowerCase();
|
||||
const text = title + ' ' + body;
|
||||
const labels = [];
|
||||
|
||||
const rules = [
|
||||
// scope labels (shared with PR labeling via Mergify)
|
||||
// Mapping: label → repo directories
|
||||
// scope: training → fastvideo/train/, fastvideo/training/, fastvideo/distillation/
|
||||
// scope: inference → fastvideo/pipelines/, fastvideo/entrypoints/, fastvideo/worker/
|
||||
// scope: attention → fastvideo/attention/
|
||||
// scope: kernel → fastvideo-kernel/, csrc/
|
||||
// scope: model → fastvideo/models/, fastvideo/layers/, fastvideo/configs/models/
|
||||
// scope: data → fastvideo/dataset/, fastvideo/pipelines/preprocess/
|
||||
// scope: distributed → fastvideo/distributed/
|
||||
// scope: docs → docs/
|
||||
{ keywords: ['training', 'finetune', 'fine-tune', 'lora', 'fsdp', 'distill'], label: 'scope: training' },
|
||||
{ keywords: ['inference', 'generate', 'pipeline', 'slow', 'latency'], label: 'scope: inference' },
|
||||
{ keywords: ['attention', 'vsa', 'flash', 'sta', 'vmoba', 'sparse attn'], label: 'scope: attention' },
|
||||
{ keywords: ['kernel', 'csrc', 'cuda kernel', 'thunderkittens'], label: 'scope: kernel' },
|
||||
{ keywords: ['wan', 'hunyuan', 'mochi', 'ltx', 'cogvideo', 'flux', 'sd3', 'cosmos'], label: 'scope: model' },
|
||||
{ keywords: ['dataset', 'dataloader', 'preprocessing', 'preprocess'], label: 'scope: data' },
|
||||
{ keywords: ['distributed', 'sequence parallel', 'fsdp', 'tensor parallel', 'multi-node', 'multi-gpu'], label: 'scope: distributed' },
|
||||
{ keywords: ['docs', 'documentation', 'tutorial', 'example'], label: 'scope: docs' },
|
||||
// issue-only labels (cross-module, no single repo directory)
|
||||
{ keywords: ['install', 'setup', 'pip', 'cuda', 'uv ', 'import error', 'modulenotfound'], label: 'installation' },
|
||||
{ keywords: ['memory', 'oom', 'out of memory', 'gpu memory', 'vram'], label: 'performance' },
|
||||
{ keywords: ['windows', 'macos', 'mac os', 'apple', 'mps', 'rocm', 'amd', 'npu'], label: 'platform' },
|
||||
];
|
||||
|
||||
for (const rule of rules) {
|
||||
if (rule.keywords.some(kw => text.includes(kw))) {
|
||||
labels.push(rule.label);
|
||||
}
|
||||
}
|
||||
|
||||
if (labels.length > 0) {
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.payload.issue.number,
|
||||
labels: labels,
|
||||
});
|
||||
console.log(`Added labels: ${labels.join(', ')}`);
|
||||
} else {
|
||||
console.log('No keyword matches found');
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
name: Close Stale Issues and PRs
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Daily at 1:30 AM UTC
|
||||
- cron: '30 1 * * *'
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
if: github.repository == 'hao-ai-lab/FastVideo'
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
actions: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1
|
||||
with:
|
||||
operations-per-run: 500
|
||||
|
||||
exempt-draft-pr: true
|
||||
exempt-issue-labels: 'keep-open,pinned,security,Bug,RFC'
|
||||
exempt-pr-labels: 'keep-open,pinned'
|
||||
|
||||
labels-to-add-when-unstale: 'unstale'
|
||||
labels-to-remove-when-stale: 'unstale'
|
||||
|
||||
days-before-issue-stale: 90
|
||||
days-before-issue-close: 30
|
||||
stale-issue-label: 'stale'
|
||||
|
||||
days-before-pr-stale: 60
|
||||
days-before-pr-close: 14
|
||||
stale-pr-label: 'stale'
|
||||
@@ -1,83 +0,0 @@
|
||||
# Sample workflow for building and deploying a Hugo site to GitHub Pages
|
||||
name: Deploy FastVideo Docs to Pages
|
||||
|
||||
on:
|
||||
# Runs on pushes targeting the default branch
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "docs/**/*.md"
|
||||
- "fastvideo/v1/examples/**/*.py"
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
types: [opened, ready_for_review, synchronize, reopened]
|
||||
paths:
|
||||
- "docs/**/*.md"
|
||||
- "fastvideo/v1/examples/**/*.py"
|
||||
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
|
||||
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
|
||||
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
|
||||
concurrency:
|
||||
group: "pages"
|
||||
cancel-in-progress: false
|
||||
|
||||
# Default to bash
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
jobs:
|
||||
pre-commit:
|
||||
uses: ./.github/workflows/pre-commit.yml
|
||||
|
||||
# Build job
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
needs: pre-commit
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Pages
|
||||
id: pages
|
||||
uses: actions/configure-pages@v5
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.10"
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
cd docs
|
||||
pip install -r requirements-docs.txt
|
||||
- name: Build docs
|
||||
run: |
|
||||
cd docs
|
||||
make clean
|
||||
make html
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: ./docs/build/html
|
||||
|
||||
# Deployment job
|
||||
deploy:
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
if: ${{ github.event_name == 'push' }}
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
@@ -1,71 +0,0 @@
|
||||
name: Publish FastVideo to PyPI on Version Change
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'pyproject.toml' # Trigger when pyproject.toml changes
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
check-version-change:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
version-changed: ${{ steps.check-version.outputs.changed }}
|
||||
new-version: ${{ steps.check-version.outputs.new-version }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Check if version changed
|
||||
id: check-version
|
||||
run: |
|
||||
# Get current commit's version
|
||||
NEW_VERSION=$(grep -oP "version\\s*=\\s*\"\\K[^\"]+\"" pyproject.toml)
|
||||
echo "New version: $NEW_VERSION"
|
||||
|
||||
# Get previous version from git history
|
||||
OLD_VERSION=$(git show HEAD~1:./pyproject.toml | grep -oP "version\\s*=\\s*\"\\K[^\"]+\"" || echo "0.0.0")
|
||||
echo "Old version: $OLD_VERSION"
|
||||
|
||||
if [ "$NEW_VERSION" != "$OLD_VERSION" ]; then
|
||||
echo "Version changed from $OLD_VERSION to $NEW_VERSION"
|
||||
echo "changed=true" >> $GITHUB_OUTPUT
|
||||
echo "new-version=$NEW_VERSION" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "Version did not change"
|
||||
echo "changed=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
build-publish-main:
|
||||
needs: check-version-change
|
||||
if: ${{ needs.check-version-change.outputs.version-changed == 'true' || github.event_name == 'workflow_dispatch' }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write # Needed for OIDC Trusted Publishing
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.10'
|
||||
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install build twine wheel
|
||||
|
||||
- name: Build package
|
||||
run: |
|
||||
python -m build
|
||||
|
||||
- name: Publish release distributions to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
packages-dir: dist/
|
||||
@@ -0,0 +1,257 @@
|
||||
name: Build and Push Docker Images
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
build_cuda_matrix:
|
||||
description: 'Build multi-arch CUDA images (12.6.3 default; 13.0.0 explicitly tagged)'
|
||||
required: false
|
||||
default: true
|
||||
type: boolean
|
||||
build_dreamverse_matrix:
|
||||
description: 'Build the amd64 Dreamverse matrix (backend + UI x CUDA 12.6.3/13.0.0)'
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
build_ci_runner_image:
|
||||
description: 'Build the ARM64 CUDA 13 CI runner image (sm_100)'
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
# Auto-rebuild the CUDA images when a repository-controlled image input
|
||||
# changes on main. This includes the trusted SM89 kernel artifact's source,
|
||||
# metadata/key helper, ABI dependency metadata, and build orchestration.
|
||||
# Dreamverse (apps/dreamverse/docker/Dockerfile) and the ROCm Dockerfile stay
|
||||
# manual-dispatch only.
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- '.dockerignore'
|
||||
- '.github/workflows/_template-build-image.yml'
|
||||
- '.github/workflows/infra-build-image.yml'
|
||||
- '.gitmodules'
|
||||
- 'docker/Dockerfile'
|
||||
- 'docker/uv-excludes'
|
||||
- 'fastvideo-kernel/**'
|
||||
- 'fastvideo/tests/modal/kernel_build_cache.py'
|
||||
- 'pyproject.toml'
|
||||
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
# One static group, no cancellation: every run of this workflow writes the same
|
||||
# mutable registry tags (latest, py3.12-latest, ...), so runs must serialize —
|
||||
# concurrent push/dispatch runs would race on those tags, and cancelling a run
|
||||
# mid-publish can strand the cu126/cu130 tag families at different commits. An
|
||||
# in-flight superseded build wastes its runner time, but its tags are then
|
||||
# overwritten by the newer queued run. GitHub keeps a single pending run per
|
||||
# group: the newest queued run replaces any older queued one.
|
||||
concurrency:
|
||||
group: infra-build-image
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
# CUDA matrix: Python 3.12 x {12.6.3, 13.0.0} x {amd64, arm64}. Each architecture
|
||||
# builds natively and pushes only by digest; publish-cuda-manifests is the sole
|
||||
# owner of the shared tags. CUDA 12.6 arm64 targets Hopper (sm_90a / GH200 class),
|
||||
# because CUDA 12.6 cannot compile sm_121. CUDA 13 arm64 targets GB10 (sm_121).
|
||||
# 12.6.3/cu126 owns the default `py3.12`/`latest` tags and keeps its versioned
|
||||
# aliases; 13.0.0/cu130 is published under explicit versioned tags. Flash-attn
|
||||
# 2.8.3 comes from the architecture-specific prebuilt releases.
|
||||
build-cuda-images:
|
||||
# Runs on a manual dispatch when build_cuda_matrix is set, or automatically
|
||||
# on an in-scope main push (inputs are null on push). The
|
||||
# repository guard keeps fork syncs from auto-building; manual dispatch
|
||||
# still works in forks.
|
||||
if: ${{ (github.event_name == 'push' && github.repository == 'hao-ai-lab/FastVideo') || github.event.inputs.build_cuda_matrix == 'true' }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
cuda:
|
||||
- version: '12.6.3'
|
||||
suffix: '-cuda12.6.3'
|
||||
torch_backend: 'cu126'
|
||||
fa_tag: 'cu126torch2.12'
|
||||
cmake_build_parallel_level: '4'
|
||||
torch_cuda_arch_list:
|
||||
amd64: '9.0a'
|
||||
arm64: '9.0a'
|
||||
- version: '13.0.0'
|
||||
suffix: '-cuda13.0.0'
|
||||
torch_backend: 'cu130'
|
||||
fa_tag: 'cu130torch2.12'
|
||||
cmake_build_parallel_level: '1'
|
||||
torch_cuda_arch_list:
|
||||
amd64: '9.0a'
|
||||
arm64: '12.1'
|
||||
architecture:
|
||||
- name: amd64
|
||||
runner: ubuntu-latest
|
||||
- name: arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
uses: ./.github/workflows/_template-build-image.yml
|
||||
with:
|
||||
python_version: '3.12'
|
||||
dockerfile_path: docker/Dockerfile
|
||||
tag_suffix: py3.12${{ matrix.cuda.suffix }}
|
||||
runner: ${{ matrix.architecture.runner }}
|
||||
architecture: ${{ matrix.architecture.name }}
|
||||
push_by_digest: true
|
||||
digest_artifact_name: fastvideo-dev-cuda${{ matrix.cuda.version }}-${{ matrix.architecture.name }}
|
||||
build_args: |
|
||||
PYTHON_VERSION=3.12
|
||||
CUDA_VERSION=${{ matrix.cuda.version }}
|
||||
UV_TORCH_BACKEND=${{ matrix.cuda.torch_backend }}
|
||||
TORCH_CUDA_ARCH_LIST=${{ matrix.cuda.torch_cuda_arch_list[matrix.architecture.name] }}
|
||||
CMAKE_BUILD_PARALLEL_LEVEL=${{ matrix.cuda.cmake_build_parallel_level }}
|
||||
FLASH_ATTN_WHEEL_TAG=${{ matrix.cuda.fa_tag }}
|
||||
FLASH_ATTN_WHEEL_RELEASE=https://github.com/mjun0812/flash-attention-prebuild-wheels/releases/download/v0.9.17
|
||||
FLASH_ATTN_WHEEL_RELEASE_ARM64=https://github.com/mjun0812/flash-attention-prebuild-wheels/releases/download/v0.9.22
|
||||
secrets: inherit
|
||||
|
||||
publish-cuda-manifests:
|
||||
# !cancelled(): publish lanes whose digests exist even if a sibling build
|
||||
# leg failed (the digest-count check fails incomplete lanes); it also
|
||||
# bypasses skipped-needs propagation, hence the explicit skipped check.
|
||||
if: ${{ !cancelled() && needs.build-cuda-images.result != 'skipped' }}
|
||||
needs: build-cuda-images
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
cuda:
|
||||
- version: '12.6.3'
|
||||
suffix: '-cuda12.6.3'
|
||||
is_default: true
|
||||
- version: '13.0.0'
|
||||
suffix: '-cuda13.0.0'
|
||||
is_default: false
|
||||
steps:
|
||||
- name: Download architecture digests
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: /tmp/digests
|
||||
pattern: fastvideo-dev-cuda${{ matrix.cuda.version }}-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Normalize image reference
|
||||
id: image
|
||||
env:
|
||||
IMAGE: ghcr.io/${{ github.repository }}/fastvideo-dev
|
||||
run: echo "name=${IMAGE,,}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: Prepare tags
|
||||
id: prepare-tags
|
||||
env:
|
||||
VERSIONED_TAG_PREFIX: py3.12${{ matrix.cuda.suffix }}
|
||||
PUBLISH_DEFAULT_TAGS: ${{ matrix.cuda.is_default }}
|
||||
run: |
|
||||
SHORT_SHA="${GITHUB_SHA::7}"
|
||||
TAGS="type=raw,value=${VERSIONED_TAG_PREFIX}-latest\ntype=raw,value=${VERSIONED_TAG_PREFIX}-sha-${SHORT_SHA}"
|
||||
if [[ "${PUBLISH_DEFAULT_TAGS}" == "true" ]]; then
|
||||
TAGS="type=raw,value=latest\ntype=raw,value=py3.12-latest\ntype=raw,value=py3.12-sha-${SHORT_SHA}\n${TAGS}"
|
||||
fi
|
||||
{
|
||||
echo "tags<<EOF"
|
||||
echo -e "${TAGS}"
|
||||
echo "EOF"
|
||||
} >> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: Extract metadata for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ steps.image.outputs.name }}
|
||||
tags: ${{ steps.prepare-tags.outputs.tags }}
|
||||
|
||||
- name: Create multi-architecture manifests
|
||||
env:
|
||||
IMAGE: ${{ steps.image.outputs.name }}
|
||||
run: |
|
||||
mapfile -t DIGESTS < <(find /tmp/digests -maxdepth 1 -type f -printf '%f\n' | sort)
|
||||
if [[ "${#DIGESTS[@]}" -ne 2 ]]; then
|
||||
echo "Expected exactly two architecture digests, found ${#DIGESTS[@]}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mapfile -t TAGS < <(jq -r '.tags[]' <<< "${DOCKER_METADATA_OUTPUT_JSON}")
|
||||
TAG_ARGS=()
|
||||
for TAG in "${TAGS[@]}"; do
|
||||
TAG_ARGS+=(--tag "${TAG}")
|
||||
done
|
||||
|
||||
IMAGE_REFS=()
|
||||
for DIGEST in "${DIGESTS[@]}"; do
|
||||
IMAGE_REFS+=("${IMAGE}@sha256:${DIGEST}")
|
||||
done
|
||||
|
||||
docker buildx imagetools create "${TAG_ARGS[@]}" "${IMAGE_REFS[@]}"
|
||||
docker buildx imagetools inspect "${TAGS[0]}"
|
||||
|
||||
# The CI runner is ARM64 like DGX Spark, but targets sm_100 rather than sm_121.
|
||||
# Publish a single-architecture variant so the self-hosted CI runner can reuse
|
||||
# the exact prebuilt kernel instead of compiling it in every job.
|
||||
build-ci-runner-image:
|
||||
if: ${{ (github.event_name == 'push' && github.repository == 'hao-ai-lab/FastVideo') || github.event.inputs.build_ci_runner_image == 'true' }}
|
||||
uses: ./.github/workflows/_template-build-image.yml
|
||||
with:
|
||||
python_version: '3.12'
|
||||
dockerfile_path: docker/Dockerfile
|
||||
tag_suffix: py3.12-cuda13.0.0-sm100
|
||||
runner: ubuntu-24.04-arm
|
||||
architecture: arm64
|
||||
build_args: |
|
||||
PYTHON_VERSION=3.12
|
||||
CUDA_VERSION=13.0.0
|
||||
UV_TORCH_BACKEND=cu130
|
||||
TORCH_CUDA_ARCH_LIST=10.0
|
||||
CMAKE_BUILD_PARALLEL_LEVEL=1
|
||||
FLASH_ATTN_WHEEL_TAG=cu130torch2.12
|
||||
FLASH_ATTN_WHEEL_RELEASE_ARM64=https://github.com/mjun0812/flash-attention-prebuild-wheels/releases/download/v0.9.22
|
||||
secrets: inherit
|
||||
|
||||
# Dreamverse matrix: {backend, UI} x {12.6.3, 13.0.0}, Python 3.12. Torch backend
|
||||
# matches the base CUDA (cu126 / cu130). Keep these images amd64-only until the
|
||||
# required FA4 dependency stack is available and validated on arm64.
|
||||
build-dreamverse:
|
||||
if: ${{ github.event.inputs.build_dreamverse_matrix == 'true' }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
cuda:
|
||||
- version: '12.6.3'
|
||||
torch_backend: 'cu126'
|
||||
- version: '13.0.0'
|
||||
torch_backend: 'cu130'
|
||||
variant:
|
||||
- name: backend
|
||||
ui: '0'
|
||||
- name: ui
|
||||
ui: '1'
|
||||
uses: ./.github/workflows/_template-build-image.yml
|
||||
with:
|
||||
python_version: '3.12'
|
||||
dockerfile_path: apps/dreamverse/docker/Dockerfile
|
||||
tag_suffix: dreamverse-${{ matrix.variant.name }}-cuda${{ matrix.cuda.version }}
|
||||
image_name: dreamverse
|
||||
build_args: |
|
||||
CUDA_VERSION=${{ matrix.cuda.version }}
|
||||
UV_TORCH_BACKEND=${{ matrix.cuda.torch_backend }}
|
||||
BUILD_DREAMVERSE_UI=${{ matrix.variant.ui }}
|
||||
include_latest_tags: false
|
||||
secrets: inherit
|
||||
@@ -0,0 +1,95 @@
|
||||
name: Deploy Documentation
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
paths:
|
||||
- 'docs/**'
|
||||
- 'examples/**'
|
||||
- 'scripts/inference/**'
|
||||
- 'mkdocs.yml'
|
||||
- 'requirements-mkdocs.in'
|
||||
- 'requirements-mkdocs.txt'
|
||||
- 'scripts/check_docs_links.py'
|
||||
- '.github/workflows/infra-docs.yml'
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
paths:
|
||||
- 'docs/**'
|
||||
- 'examples/**'
|
||||
- 'scripts/inference/**'
|
||||
- 'mkdocs.yml'
|
||||
- 'requirements-mkdocs.in'
|
||||
- 'requirements-mkdocs.txt'
|
||||
- 'scripts/check_docs_links.py'
|
||||
- '.github/workflows/infra-docs.yml'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
concurrency:
|
||||
group: "pages"
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v3
|
||||
|
||||
- name: Install dependencies
|
||||
run: uv pip install --system -r requirements-mkdocs.txt
|
||||
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
|
||||
- name: Install cookbook dependencies
|
||||
run: npm ci --prefix docs
|
||||
|
||||
- name: Validate and generate cookbook catalogs
|
||||
run: |
|
||||
npm run check:cookbook --prefix docs
|
||||
npm run test:cookbook --prefix docs
|
||||
python docs/tests/test_cookbook_assets.py
|
||||
npm run build:catalog --prefix docs
|
||||
|
||||
- name: Build documentation
|
||||
run: mkdocs build
|
||||
|
||||
- name: Check docs links
|
||||
run: python scripts/check_docs_links.py
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: ./site
|
||||
|
||||
deploy:
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
if: github.ref == 'refs/heads/main'
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
@@ -13,4 +13,4 @@
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"problemMatcher": [
|
||||
{
|
||||
"owner": "ruff",
|
||||
"pattern": [
|
||||
{
|
||||
"regexp": "^(.+):(\\d+):(\\d+): (\\w+) (.+)$",
|
||||
"file": 1,
|
||||
"line": 2,
|
||||
"column": 3,
|
||||
"code": 4,
|
||||
"message": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,376 +0,0 @@
|
||||
name: PR Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- "fastvideo/**/*.py"
|
||||
- ".github/workflows/pr-test.yml"
|
||||
pull_request:
|
||||
branches: [main]
|
||||
types: [opened, ready_for_review, synchronize, reopened]
|
||||
paths:
|
||||
- "fastvideo/**/*.py"
|
||||
- ".github/workflows/pr-test.yml"
|
||||
- "pyproject.toml"
|
||||
- "docker/Dockerfile.python3.12"
|
||||
- "csrc/**"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
run_encoder_test:
|
||||
description: "Run encoder-test"
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
run_vae_test:
|
||||
description: "Run vae-test"
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
run_transformer_test:
|
||||
description: "Run transformer-test"
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
run_ssim_test:
|
||||
description: "Run ssim-test"
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
run_training_test:
|
||||
description: "Run training-test"
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
run_training_test_VSA:
|
||||
description: "Run training-test-VSA"
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
run_inference_test_STA:
|
||||
description: "Run inference-test-STA"
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
run_precision_test_STA:
|
||||
description: "Run precision-test-STA"
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
run_precision_test_VSA:
|
||||
description: "Run precision-test-VSA"
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
run_nightly_test:
|
||||
description: "Run nightly-test"
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
|
||||
env:
|
||||
PYTHONUNBUFFERED: "1"
|
||||
|
||||
|
||||
concurrency:
|
||||
group: pr-test-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
pre-commit:
|
||||
uses: ./.github/workflows/pre-commit.yml
|
||||
|
||||
change-filter:
|
||||
runs-on: ubuntu-latest
|
||||
needs: pre-commit
|
||||
if: ${{ github.event.pull_request.draft == false || github.event_name == 'workflow_dispatch' }}
|
||||
outputs:
|
||||
encoder-test: ${{ steps.filter.outputs.encoder-test }}
|
||||
vae-test: ${{ steps.filter.outputs.vae-test }}
|
||||
transformer-test: ${{ steps.filter.outputs.transformer-test }}
|
||||
training-test: ${{ steps.filter.outputs.training-test }}
|
||||
training-test-VSA: ${{ steps.filter.outputs.training-test-VSA }}
|
||||
inference-test-STA: ${{ steps.filter.outputs.inference-test-STA }}
|
||||
precision-test-STA: ${{ steps.filter.outputs.precision-test-STA }}
|
||||
precision-test-VSA: ${{ steps.filter.outputs.precision-test-VSA }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dorny/paths-filter@v3
|
||||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
# Define reusable path patterns
|
||||
common-paths: &common-paths
|
||||
- 'pyproject.toml'
|
||||
- 'docker/Dockerfile.python3.12'
|
||||
sta-kernel-paths: &sta-kernel-paths
|
||||
- 'csrc/attn/st_attn/**'
|
||||
- 'csrc/attn/setup_sta.py'
|
||||
- 'csrc/attn/config_sta.py'
|
||||
- 'csrc/attn/st_attn.cpp'
|
||||
vsa-kernel-paths: &vsa-kernel-paths
|
||||
- 'csrc/attn/vsa/**'
|
||||
- 'csrc/attn/tk/**'
|
||||
- 'csrc/attn/setup_vsa.py'
|
||||
- 'csrc/attn/config_vsa.py'
|
||||
- 'csrc/attn/vsa.cpp'
|
||||
vsa-paths: &vsa-paths
|
||||
- 'fastvideo/v1/**'
|
||||
- *common-paths
|
||||
- *vsa-kernel-paths
|
||||
|
||||
# Actual tests
|
||||
encoder-test:
|
||||
- 'fastvideo/v1/models/encoders/**'
|
||||
- 'fastvideo/v1/models/loaders/**'
|
||||
- 'fastvideo/v1/tests/encoders/**'
|
||||
- *common-paths
|
||||
vae-test:
|
||||
- 'fastvideo/v1/models/vaes/**'
|
||||
- 'fastvideo/v1/models/loaders/**'
|
||||
- 'fastvideo/v1/tests/vaes/**'
|
||||
- *common-paths
|
||||
transformer-test:
|
||||
- 'fastvideo/v1/models/dits/**'
|
||||
- 'fastvideo/v1/models/loaders/**'
|
||||
- 'fastvideo/v1/tests/transformers/**'
|
||||
- 'fastvideo/v1/layers/**'
|
||||
- 'fastvideo/v1/attention/**'
|
||||
- *common-paths
|
||||
training-test:
|
||||
- 'fastvideo/v1/**'
|
||||
- *common-paths
|
||||
training-test-VSA:
|
||||
- 'fastvideo/v1/**'
|
||||
- *common-paths
|
||||
- *vsa-kernel-paths
|
||||
inference-test-STA:
|
||||
- 'fastvideo/v1/**'
|
||||
- *common-paths
|
||||
- *sta-kernel-paths
|
||||
precision-test-STA:
|
||||
- *common-paths
|
||||
- *sta-kernel-paths
|
||||
precision-test-VSA:
|
||||
- *common-paths
|
||||
- *vsa-kernel-paths
|
||||
|
||||
encoder-test:
|
||||
needs: change-filter
|
||||
if: >-
|
||||
(github.event_name != 'workflow_dispatch' && needs.change-filter.outputs.encoder-test == 'true') ||
|
||||
(github.event_name == 'workflow_dispatch' && github.event.inputs.run_encoder_test == 'true')
|
||||
uses: ./.github/workflows/runpod-test.yml
|
||||
with:
|
||||
job_id: "encoder-test"
|
||||
gpu_type: "NVIDIA A40"
|
||||
gpu_count: 1
|
||||
volume_size: 100
|
||||
image: "ghcr.io/${{ github.repository }}/fastvideo-dev:py3.12-latest"
|
||||
test_command: "uv pip install -e .[test] && pytest ./fastvideo/v1/tests/encoders -s"
|
||||
timeout_minutes: 30
|
||||
secrets:
|
||||
RUNPOD_API_KEY: ${{ secrets.RUNPOD_API_KEY }}
|
||||
RUNPOD_PRIVATE_KEY: ${{ secrets.RUNPOD_PRIVATE_KEY }}
|
||||
|
||||
vae-test:
|
||||
needs: change-filter
|
||||
if: >-
|
||||
(github.event_name != 'workflow_dispatch' && needs.change-filter.outputs.vae-test == 'true') ||
|
||||
(github.event_name == 'workflow_dispatch' && github.event.inputs.run_vae_test == 'true')
|
||||
uses: ./.github/workflows/runpod-test.yml
|
||||
with:
|
||||
job_id: "vae-test"
|
||||
gpu_type: "NVIDIA A40"
|
||||
gpu_count: 1
|
||||
volume_size: 100
|
||||
image: "ghcr.io/${{ github.repository }}/fastvideo-dev:py3.12-latest"
|
||||
test_command: "uv pip install -e .[test] && pytest ./fastvideo/v1/tests/vaes -s"
|
||||
timeout_minutes: 30
|
||||
secrets:
|
||||
RUNPOD_API_KEY: ${{ secrets.RUNPOD_API_KEY }}
|
||||
RUNPOD_PRIVATE_KEY: ${{ secrets.RUNPOD_PRIVATE_KEY }}
|
||||
|
||||
transformer-test:
|
||||
needs: change-filter
|
||||
if: >-
|
||||
(github.event_name != 'workflow_dispatch' && needs.change-filter.outputs.transformer-test == 'true') ||
|
||||
(github.event_name == 'workflow_dispatch' && github.event.inputs.run_transformer_test == 'true')
|
||||
uses: ./.github/workflows/runpod-test.yml
|
||||
with:
|
||||
job_id: "transformer-test"
|
||||
gpu_type: "NVIDIA L40S"
|
||||
gpu_count: 1
|
||||
volume_size: 100
|
||||
image: "ghcr.io/${{ github.repository }}/fastvideo-dev:py3.12-latest"
|
||||
test_command: "uv pip install -e .[test] && pytest ./fastvideo/v1/tests/transformers -s"
|
||||
timeout_minutes: 30
|
||||
secrets:
|
||||
RUNPOD_API_KEY: ${{ secrets.RUNPOD_API_KEY }}
|
||||
RUNPOD_PRIVATE_KEY: ${{ secrets.RUNPOD_PRIVATE_KEY }}
|
||||
|
||||
ssim-test:
|
||||
needs: change-filter
|
||||
if: >-
|
||||
(github.event_name != 'workflow_dispatch' && github.event.pull_request.draft == false) ||
|
||||
(github.event_name == 'workflow_dispatch' && github.event.inputs.run_ssim_test == 'true')
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: [
|
||||
# {version: "3.10", tag: "latest"},
|
||||
# {version: "3.11", tag: "py3.11-latest"},
|
||||
{version: "3.12", tag: "py3.12-latest"}
|
||||
]
|
||||
uses: ./.github/workflows/runpod-test.yml
|
||||
with:
|
||||
job_id: "ssim-test-py${{ matrix.python-version.version }}"
|
||||
gpu_type: "NVIDIA A40"
|
||||
gpu_count: 2
|
||||
volume_size: 200
|
||||
disk_size: 200
|
||||
image: "ghcr.io/${{ github.repository }}/fastvideo-dev:${{ matrix.python-version.tag }}"
|
||||
test_command: "uv pip install -e .[test] && pytest ./fastvideo/v1/tests/ssim -vs"
|
||||
timeout_minutes: 60
|
||||
secrets:
|
||||
RUNPOD_API_KEY: ${{ secrets.RUNPOD_API_KEY }}
|
||||
RUNPOD_PRIVATE_KEY: ${{ secrets.RUNPOD_PRIVATE_KEY }}
|
||||
|
||||
training-test:
|
||||
needs: change-filter
|
||||
if: >-
|
||||
(github.event_name != 'workflow_dispatch' && github.event.pull_request.draft == false) ||
|
||||
(github.event_name == 'workflow_dispatch' && github.event.inputs.run_training_test == 'true')
|
||||
uses: ./.github/workflows/runpod-test.yml
|
||||
with:
|
||||
job_id: "training-test"
|
||||
gpu_type: "NVIDIA A40"
|
||||
gpu_count: 4
|
||||
volume_size: 100
|
||||
disk_size: 100
|
||||
image: "ghcr.io/${{ github.repository }}/fastvideo-dev:py3.12-latest"
|
||||
test_command: "wandb login $WANDB_API_KEY && uv pip install -e .[test] && pytest ./fastvideo/v1/tests/training/Vanilla -srP"
|
||||
timeout_minutes: 30
|
||||
secrets:
|
||||
RUNPOD_API_KEY: ${{ secrets.RUNPOD_API_KEY }}
|
||||
RUNPOD_PRIVATE_KEY: ${{ secrets.RUNPOD_PRIVATE_KEY }}
|
||||
WANDB_API_KEY: ${{ secrets.WANDB_API_KEY }}
|
||||
|
||||
training-test-VSA:
|
||||
needs: change-filter
|
||||
if: >-
|
||||
(github.event_name != 'workflow_dispatch' && github.event.pull_request.draft == false) ||
|
||||
(github.event_name == 'workflow_dispatch' && github.event.inputs.run_training_test_VSA == 'true')
|
||||
uses: ./.github/workflows/runpod-test.yml
|
||||
with:
|
||||
job_id: "training-test-VSA"
|
||||
gpu_type: "NVIDIA H100 NVL"
|
||||
gpu_count: 1
|
||||
volume_size: 100
|
||||
disk_size: 100
|
||||
image: "ghcr.io/${{ github.repository }}/fastvideo-dev:py3.12-latest"
|
||||
test_command: "wandb login $WANDB_API_KEY && uv pip install -e .[test] && pytest ./fastvideo/v1/tests/training/VSA -srP"
|
||||
timeout_minutes: 30
|
||||
secrets:
|
||||
RUNPOD_API_KEY: ${{ secrets.RUNPOD_API_KEY }}
|
||||
RUNPOD_PRIVATE_KEY: ${{ secrets.RUNPOD_PRIVATE_KEY }}
|
||||
WANDB_API_KEY: ${{ secrets.WANDB_API_KEY }}
|
||||
|
||||
inference-test-STA:
|
||||
needs: change-filter
|
||||
if: >-
|
||||
(github.event_name != 'workflow_dispatch' && github.event.pull_request.draft == false) ||
|
||||
(github.event_name == 'workflow_dispatch' && github.event.inputs.run_inference_test_STA == 'true')
|
||||
uses: ./.github/workflows/runpod-test.yml
|
||||
with:
|
||||
job_id: "inference-test-STA"
|
||||
gpu_type: "NVIDIA H100 NVL"
|
||||
gpu_count: 1
|
||||
volume_size: 100
|
||||
disk_size: 100
|
||||
image: "ghcr.io/${{ github.repository }}/fastvideo-dev:py3.12-latest"
|
||||
test_command: "uv pip install -e .[test] && pytest ./fastvideo/v1/tests/inference/STA -srP"
|
||||
timeout_minutes: 30
|
||||
secrets:
|
||||
RUNPOD_API_KEY: ${{ secrets.RUNPOD_API_KEY }}
|
||||
RUNPOD_PRIVATE_KEY: ${{ secrets.RUNPOD_PRIVATE_KEY }}
|
||||
|
||||
precision-test-STA:
|
||||
needs: change-filter
|
||||
if: >-
|
||||
(github.event_name != 'workflow_dispatch' && github.event.pull_request.draft == false) ||
|
||||
(github.event_name == 'workflow_dispatch' && github.event.inputs.run_precision_test_STA == 'true')
|
||||
uses: ./.github/workflows/runpod-test.yml
|
||||
with:
|
||||
job_id: "precision-test-STA"
|
||||
gpu_type: "NVIDIA H100 NVL"
|
||||
gpu_count: 1
|
||||
volume_size: 100
|
||||
disk_size: 100
|
||||
image: "ghcr.io/${{ github.repository }}/fastvideo-dev:py3.12-latest"
|
||||
test_command: "uv pip install -e .[test] && python csrc/attn/tests/test_sta.py"
|
||||
timeout_minutes: 30
|
||||
secrets:
|
||||
RUNPOD_API_KEY: ${{ secrets.RUNPOD_API_KEY }}
|
||||
RUNPOD_PRIVATE_KEY: ${{ secrets.RUNPOD_PRIVATE_KEY }}
|
||||
|
||||
precision-test-VSA:
|
||||
needs: change-filter
|
||||
if: >-
|
||||
(github.event_name != 'workflow_dispatch' && github.event.pull_request.draft == false) ||
|
||||
(github.event_name == 'workflow_dispatch' && github.event.inputs.run_precision_test_VSA == 'true')
|
||||
uses: ./.github/workflows/runpod-test.yml
|
||||
with:
|
||||
job_id: "precision-test-VSA"
|
||||
gpu_type: "NVIDIA H100 NVL"
|
||||
gpu_count: 1
|
||||
volume_size: 100
|
||||
disk_size: 100
|
||||
image: "ghcr.io/${{ github.repository }}/fastvideo-dev:py3.12-latest"
|
||||
test_command: "uv pip install -e .[test] && python csrc/attn/tests/test_block_sparse.py"
|
||||
timeout_minutes: 30
|
||||
secrets:
|
||||
RUNPOD_API_KEY: ${{ secrets.RUNPOD_API_KEY }}
|
||||
RUNPOD_PRIVATE_KEY: ${{ secrets.RUNPOD_PRIVATE_KEY }}
|
||||
|
||||
nightly-test:
|
||||
if: >-
|
||||
(github.event_name == 'workflow_dispatch' && github.event.inputs.run_nightly_test == 'true')
|
||||
uses: ./.github/workflows/runpod-test.yml
|
||||
with:
|
||||
job_id: "nightly-test"
|
||||
gpu_type: "NVIDIA A40"
|
||||
gpu_count: 4
|
||||
volume_size: 100
|
||||
disk_size: 100
|
||||
image: "ghcr.io/${{ github.repository }}/fastvideo-dev:py3.12-latest"
|
||||
test_command: "wandb login $WANDB_API_KEY && uv pip install -e .[test] && pytest ./fastvideo/v1/tests/nightly/test_e2e_overfit_single_sample.py -vs"
|
||||
timeout_minutes: 30
|
||||
secrets:
|
||||
RUNPOD_API_KEY: ${{ secrets.RUNPOD_API_KEY }}
|
||||
RUNPOD_PRIVATE_KEY: ${{ secrets.RUNPOD_PRIVATE_KEY }}
|
||||
WANDB_API_KEY: ${{ secrets.WANDB_API_KEY }}
|
||||
|
||||
runpod-cleanup:
|
||||
# Add other jobs to this list as you create them
|
||||
needs: [encoder-test, vae-test, transformer-test, ssim-test, training-test, training-test-VSA, inference-test-STA, precision-test-STA, precision-test-VSA]
|
||||
if: ${{ always() && ((github.event_name != 'workflow_dispatch' && github.event.pull_request.draft == false) || github.event_name == 'workflow_dispatch') }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.10"
|
||||
|
||||
- name: Install dependencies
|
||||
run: pip install requests
|
||||
|
||||
- name: Cleanup all RunPod instances
|
||||
env:
|
||||
JOB_IDS: '["encoder-test", "vae-test", "transformer-test", "ssim-test-py3.10", "ssim-test-py3.11", "ssim-test-py3.12", "training-test", "training-test-VSA", "inference-test-STA", "precision-test-STA", "precision-test-VSA"]'
|
||||
RUNPOD_API_KEY: ${{ secrets.RUNPOD_API_KEY }}
|
||||
GITHUB_RUN_ID: ${{ github.run_id }}
|
||||
run: python .github/scripts/runpod_cleanup.py
|
||||
@@ -1,18 +0,0 @@
|
||||
name: pre-commit
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
pre-commit:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
- run: echo "::add-matcher::.github/workflows/matchers/actionlint.json"
|
||||
- run: echo "::add-matcher::.github/workflows/matchers/mypy.json"
|
||||
- uses: pre-commit/action@v3.0.1
|
||||
with:
|
||||
extra_args: --all-files --hook-stage manual
|
||||
@@ -0,0 +1,28 @@
|
||||
name: Publish to Comfy registry
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
paths:
|
||||
- "pyproject.toml"
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
publish-node:
|
||||
name: Publish Custom Node to registry
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.repository_owner == 'hao-ai-lab' }}
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
- name: Publish Custom Node
|
||||
uses: Comfy-Org/publish-node-action@v1
|
||||
with:
|
||||
## Add your own personal access token to your Github Repository secrets and reference it here.
|
||||
personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }}
|
||||
@@ -0,0 +1,72 @@
|
||||
name: Publish FastVideo to PyPI on Version Change
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'pyproject.toml' # Trigger when pyproject.toml changes
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
check-version-change:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
version-changed: ${{ steps.check-version.outputs.changed }}
|
||||
new-version: ${{ steps.check-version.outputs.new-version }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Check if version changed
|
||||
id: check-version
|
||||
run: |
|
||||
# Get current commit's version
|
||||
NEW_VERSION=$(grep -oP "version\\s*=\\s*\"\\K[^\"]+\"" pyproject.toml)
|
||||
echo "New version: $NEW_VERSION"
|
||||
|
||||
# Get previous version from git history
|
||||
OLD_VERSION=$(git show HEAD~1:./pyproject.toml | grep -oP "version\\s*=\\s*\"\\K[^\"]+\"" || echo "0.0.0")
|
||||
echo "Old version: $OLD_VERSION"
|
||||
|
||||
if [ "$NEW_VERSION" != "$OLD_VERSION" ]; then
|
||||
echo "Version changed from $OLD_VERSION to $NEW_VERSION"
|
||||
echo "changed=true" >> $GITHUB_OUTPUT
|
||||
echo "new-version=$NEW_VERSION" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "Version did not change"
|
||||
echo "changed=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
build-publish-main:
|
||||
needs: check-version-change
|
||||
if: ${{ needs.check-version-change.outputs.version-changed == 'true' || github.event_name == 'workflow_dispatch' }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write # Needed for OIDC Trusted Publishing
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.10'
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v3
|
||||
|
||||
- name: Install build dependencies
|
||||
run: uv pip install --system build twine wheel
|
||||
|
||||
- name: Build package
|
||||
run: |
|
||||
python -m build
|
||||
|
||||
- name: Publish release distributions to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
packages-dir: dist/
|
||||
@@ -0,0 +1,279 @@
|
||||
name: Publish FastVideo Kernel to PyPI on Version Change
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "fastvideo-kernel/pyproject.toml"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
check-version-change:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
version-changed: ${{ steps.check-version.outputs.changed }}
|
||||
new-version: ${{ steps.check-version.outputs.new-version }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Check if version changed
|
||||
id: check-version
|
||||
run: |
|
||||
cd fastvideo-kernel
|
||||
# Get current commit's version from pyproject.toml
|
||||
# Use ^ to match start of line to avoid matching minimum-version
|
||||
NEW_VERSION=$(grep -oP '^version\s*=\s*"\K[^"]+' pyproject.toml)
|
||||
echo "New version: $NEW_VERSION"
|
||||
|
||||
# Get previous version from git history
|
||||
# Note: git show expects path relative to repo root
|
||||
OLD_VERSION=$(git show HEAD~1:fastvideo-kernel/pyproject.toml | grep -oP '^version\s*=\s*"\K[^"]+' || echo "0.0.0")
|
||||
echo "Old version: $OLD_VERSION"
|
||||
|
||||
if [ "$NEW_VERSION" != "$OLD_VERSION" ]; then
|
||||
echo "Version changed from $OLD_VERSION to $NEW_VERSION"
|
||||
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||
echo "new-version=$NEW_VERSION" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "Version did not change"
|
||||
echo "changed=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
build_wheels:
|
||||
name: Build Wheel
|
||||
needs: check-version-change
|
||||
if: ${{ needs.check-version-change.outputs.version-changed == 'true' || github.event_name == 'workflow_dispatch' }}
|
||||
runs-on: ${{ matrix.platform.os }}
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ['3.12']
|
||||
torch-cuda:
|
||||
# torch 2.12 dropped cu128; cu130 (CUDA 13) is the default wheel, cu126 covers older drivers.
|
||||
- torch-version: '2.12.0'
|
||||
cuda-version: '12.6.3'
|
||||
torch-cuda-short: 'cu126'
|
||||
- torch-version: '2.12.0'
|
||||
cuda-version: '13.0.0'
|
||||
torch-cuda-short: 'cu130'
|
||||
platform:
|
||||
# x86_64 builds the full cu126 + cu130 set. cu130 ships the
|
||||
# data-center Blackwell sm_100a/sm_103a VSA and consumer sm_120a FP4
|
||||
# kernels.
|
||||
- os: ubuntu-22.04
|
||||
arch: x86_64
|
||||
wheel-plat: manylinux_2_35_x86_64
|
||||
# aarch64 is Blackwell (GB200 sm_100a/sm_103a + sm_120a + DGX Spark sm_121a), not
|
||||
# Hopper, and Blackwell needs CUDA >= 12.8 — so only the cu130 leg applies.
|
||||
# Added via include so x86 keeps cu126 + cu130 while aarch64 stays cu130-only.
|
||||
include:
|
||||
- python-version: '3.12'
|
||||
torch-cuda:
|
||||
torch-version: '2.12.0'
|
||||
cuda-version: '13.0.0'
|
||||
torch-cuda-short: 'cu130'
|
||||
platform:
|
||||
os: ubuntu-22.04-arm
|
||||
arch: aarch64
|
||||
wheel-plat: manylinux_2_35_aarch64
|
||||
|
||||
steps:
|
||||
- name: Free up disk space
|
||||
run: |
|
||||
echo "Initial disk space:"
|
||||
df -h
|
||||
|
||||
# Remove large directories
|
||||
sudo rm -rf /usr/share/dotnet
|
||||
sudo rm -rf /usr/local/lib/android
|
||||
sudo rm -rf /opt/ghc
|
||||
sudo rm -rf /usr/local/share/boost
|
||||
sudo rm -rf /usr/share/swift
|
||||
sudo rm -rf /usr/local/lib/node_modules
|
||||
sudo rm -rf /usr/local/share/powershell
|
||||
sudo rm -rf /usr/share/rust
|
||||
sudo rm -rf /usr/local/.ghcup
|
||||
|
||||
# Remove cached files
|
||||
sudo rm -rf /var/lib/apt/lists/*
|
||||
sudo rm -rf /var/cache/apt/archives/*
|
||||
|
||||
echo "Disk space after cleanup:"
|
||||
df -h
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Install CUDA ${{ matrix.torch-cuda.cuda-version }}
|
||||
uses: Jimver/cuda-toolkit@v0.2.35
|
||||
id: cuda-toolkit
|
||||
with:
|
||||
cuda: ${{ matrix.torch-cuda.cuda-version }}
|
||||
linux-local-args: '["--toolkit"]'
|
||||
method: 'network'
|
||||
|
||||
- name: Install dependencies (GCC, Clang, CUDA Paths, Git)
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt install -y git gcc-11 g++-11 clang-11
|
||||
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-11 100 --slave /usr/bin/g++ g++ /usr/bin/g++-11
|
||||
|
||||
# Allow Git to Access Safe Directory
|
||||
git config --global --add safe.directory /__w/FastVideo/FastVideo
|
||||
|
||||
# Set CUDA environment variables
|
||||
export CUDA_HOME=/usr/local/cuda-${{ matrix.torch-cuda.cuda-version }}
|
||||
export PATH=${CUDA_HOME}/bin:${PATH}
|
||||
export LD_LIBRARY_PATH=${CUDA_HOME}/lib64:$LD_LIBRARY_PATH
|
||||
|
||||
# Verify installation
|
||||
gcc --version
|
||||
g++ --version
|
||||
clang-11 --version
|
||||
nvcc --version
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v3
|
||||
|
||||
- name: Install PyTorch ${{ matrix.torch-cuda.torch-version }}+cu${{ matrix.torch-cuda.cuda-version }}
|
||||
run: |
|
||||
uv pip install --system typing-extensions==4.12.2
|
||||
uv pip install --system --no-cache-dir torch==${{ matrix.torch-cuda.torch-version }} --index-url https://download.pytorch.org/whl/${{matrix.torch-cuda.torch-cuda-short}}
|
||||
nvcc --version
|
||||
python --version
|
||||
python -c "import torch; print('PyTorch:', torch.__version__)"
|
||||
python -c "import torch; print('CUDA:', torch.version.cuda)"
|
||||
python -c "from torch.utils import cpp_extension; print (cpp_extension.CUDA_HOME)"
|
||||
|
||||
- name: Build wheel
|
||||
run: |
|
||||
export PYTHONPATH=$GITHUB_WORKSPACE:$PYTHONPATH
|
||||
|
||||
uv pip install --system setuptools ninja packaging wheel triton scikit-build-core cmake build
|
||||
|
||||
cd fastvideo-kernel
|
||||
git submodule update --init --recursive # Ensure ThunderKittens submodule is initialized
|
||||
# Release builds run on GPU-less runners, so set kernels + arch explicitly:
|
||||
# * aarch64 = Blackwell (GB200 sm_100a/sm_103a + sm_120a + DGX Spark sm_121a), NOT
|
||||
# Hopper, so TK (sm_90a wgmma) is OFF. The C++ FP4 (attn_qat_infer)
|
||||
# covers sm_120a+sm_121a; turbodiffusion covers every listed arch. The
|
||||
# sm_100 FP4 forward is the FA4 CuTe DSL path in the fastvideo package (PR #1221),
|
||||
# JIT-compiled at runtime — not built into this wheel.
|
||||
# * x86_64 cu130 = Hopper TK + data-center Blackwell sm_100a/sm_103a VSA
|
||||
# + consumer Blackwell sm_120a FP4.
|
||||
# * x86_64 cu126 = Hopper TK only (older drivers; CUDA < 12.8 has no FP4).
|
||||
# The per-arch split in CMakeLists pins the FP4 targets to requested
|
||||
# sm_120a/sm_121a and builds the main extension for the full arch list.
|
||||
# CMAKE_BUILD_PARALLEL_LEVEL caps
|
||||
# Ninja so heavy CUTLASS/TK template TUs don't OOM the 16 GB runner (exit 143).
|
||||
if [ "${{ matrix.platform.arch }}" = "aarch64" ]; then
|
||||
export TORCH_CUDA_ARCH_LIST="10.0a;10.3a;12.0a;12.1a"
|
||||
export CMAKE_ARGS="${CMAKE_ARGS:-} -DFASTVIDEO_KERNEL_BUILD_TK=OFF -DFASTVIDEO_KERNEL_BUILD_ATTN_QAT_INFER=ON"
|
||||
export CMAKE_BUILD_PARALLEL_LEVEL=1
|
||||
elif [ "${{ matrix.torch-cuda.torch-cuda-short }}" = "cu130" ]; then
|
||||
export TORCH_CUDA_ARCH_LIST="9.0a;10.0a;10.3a;12.0a"
|
||||
export CMAKE_ARGS="${CMAKE_ARGS:-} -DFASTVIDEO_KERNEL_BUILD_TK=ON -DFASTVIDEO_KERNEL_BUILD_ATTN_QAT_INFER=ON -DCMAKE_CUDA_ARCHITECTURES=90a"
|
||||
# A single FP4 TU (attn_qat_infer) can use ~8-12 GB on its own, so serialize.
|
||||
export CMAKE_BUILD_PARALLEL_LEVEL=1
|
||||
else
|
||||
export TORCH_CUDA_ARCH_LIST="9.0a"
|
||||
export CMAKE_ARGS="${CMAKE_ARGS:-} -DFASTVIDEO_KERNEL_BUILD_TK=ON -DCMAKE_CUDA_ARCHITECTURES=90a"
|
||||
# Hopper-only: the two TK TUs are the heavy ones (single-arch) and fit side
|
||||
# by side; -j4 overlaps the light TUs without OOMing (~12-14 GB peak).
|
||||
export CMAKE_BUILD_PARALLEL_LEVEL=4
|
||||
fi
|
||||
|
||||
# Build standard wheel (no local version suffix) for PyPI
|
||||
python -m build --wheel --outdir dist
|
||||
|
||||
# Fix the wheel to be manylinux compliant
|
||||
# Ubuntu 22.04 ships patchelf 0.14.3, while current auditwheel
|
||||
# requires at least 0.14.5. Use the stable PyPI binary on both
|
||||
# x86_64 and aarch64 release runners.
|
||||
uv pip install --system auditwheel patchelf==0.17.2.4
|
||||
patchelf --version
|
||||
# Point auditwheel at torch libs, but do not vendor them into the wheel.
|
||||
TORCH_LIB_DIR=$(python - <<'PY'
|
||||
import os
|
||||
import torch
|
||||
|
||||
print(os.path.join(os.path.dirname(torch.__file__), "lib"))
|
||||
PY
|
||||
)
|
||||
export LD_LIBRARY_PATH="${TORCH_LIB_DIR}:${LD_LIBRARY_PATH}"
|
||||
# Target manylinux_2_35 (Ubuntu 22.04 native), per-arch plat tag.
|
||||
auditwheel repair dist/*.whl --plat ${{ matrix.platform.wheel-plat }} -w fixed_dist \
|
||||
--exclude libtorch_cuda.so \
|
||||
--exclude libtorch_cpu.so \
|
||||
--exclude libtorch.so \
|
||||
--exclude libc10.so \
|
||||
--exclude libc10_cuda.so \
|
||||
--exclude libtorch_python.so \
|
||||
--exclude libnccl.so.2
|
||||
# Move fixed wheels back to dist for upload consistency
|
||||
rm dist/*.whl
|
||||
mv fixed_dist/*.whl dist/
|
||||
|
||||
- name: Upload wheel artifact
|
||||
# Upload every matrix leg as a distinct artifact (for inspection / GitHub releases).
|
||||
# The publish job below selects which CUDA build is pushed to PyPI.
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: fastvideo_kernel-py${{ matrix.python-version }}-${{ matrix.torch-cuda.torch-cuda-short }}-${{ matrix.platform.arch }}-torch${{ matrix.torch-cuda.torch-version }}
|
||||
path: fastvideo-kernel/dist/*.whl
|
||||
retention-days: 90
|
||||
|
||||
publish_package:
|
||||
name: Publish package
|
||||
needs: [build_wheels, check-version-change]
|
||||
if: ${{ needs.check-version-change.outputs.version-changed == 'true' || github.event_name == 'workflow_dispatch' }}
|
||||
runs-on: ubuntu-22.04
|
||||
permissions:
|
||||
id-token: write # Needed for OIDC Trusted Publishing
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
|
||||
- name: Download PyPI wheels
|
||||
# Publish the cu130 (CUDA 13) wheels to PyPI for both architectures:
|
||||
# x86_64 — Hopper sm_90a TK + consumer Blackwell sm_120a FP4
|
||||
# aarch64 — Blackwell: turbodiffusion (sm_100a/sm_103a/sm_120a/sm_121a)
|
||||
# + C++ FP4 (sm_120a/sm_121a);
|
||||
# no TK (Hopper). sm_100 FP4 forward is the FA4 CuTe DSL path in the
|
||||
# fastvideo package (#1221), shipped/JIT separately.
|
||||
# The x86_64 cu126 wheel stays available as a build artifact / GitHub-release asset.
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: fastvideo-kernel/dist/
|
||||
pattern: 'fastvideo_kernel-py*-cu130-*'
|
||||
merge-multiple: true
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v3
|
||||
|
||||
- name: Build source distribution
|
||||
run: |
|
||||
uv pip install --system build scikit-build-core cmake ninja
|
||||
|
||||
cd fastvideo-kernel
|
||||
# We don't need full CUDA/Torch to just package the source (sdist)
|
||||
python -m build --sdist --outdir dist
|
||||
|
||||
- name: Publish release distributions to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
packages-dir: fastvideo-kernel/dist/
|
||||
@@ -1,94 +0,0 @@
|
||||
name: RunPod Test
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
job_id:
|
||||
required: true
|
||||
type: string
|
||||
description: "Unique identifier for this test job"
|
||||
gpu_type:
|
||||
required: true
|
||||
type: string
|
||||
description: "GPU type to use (e.g. NVIDIA A40, NVIDIA L40S)"
|
||||
gpu_count:
|
||||
required: true
|
||||
type: number
|
||||
description: "Number of GPUs to use"
|
||||
volume_size:
|
||||
required: false
|
||||
type: number
|
||||
default: 20
|
||||
description: "Volume size in GB"
|
||||
disk_size:
|
||||
required: false
|
||||
type: number
|
||||
default: 20
|
||||
description: "Disk size in GB"
|
||||
image:
|
||||
required: true
|
||||
type: string
|
||||
description: "Docker image to use"
|
||||
test_command:
|
||||
required: true
|
||||
type: string
|
||||
description: "Command to run tests"
|
||||
timeout_minutes:
|
||||
required: false
|
||||
type: number
|
||||
default: 30
|
||||
description: "Timeout in minutes"
|
||||
secrets:
|
||||
RUNPOD_API_KEY:
|
||||
required: true
|
||||
RUNPOD_PRIVATE_KEY:
|
||||
required: true
|
||||
WANDB_API_KEY:
|
||||
required: false
|
||||
|
||||
jobs:
|
||||
run-test:
|
||||
runs-on: ubuntu-latest
|
||||
environment: runpod-runners
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Set up SSH key
|
||||
run: |
|
||||
mkdir -p ~/.ssh
|
||||
echo "${{ secrets.RUNPOD_PRIVATE_KEY }}" > ~/.ssh/id_rsa
|
||||
chmod 600 ~/.ssh/id_rsa
|
||||
ssh-keygen -y -f ~/.ssh/id_rsa > ~/.ssh/id_rsa.pub
|
||||
|
||||
- name: Install dependencies
|
||||
run: pip install requests
|
||||
|
||||
- name: Run tests on RunPod
|
||||
env:
|
||||
JOB_ID: ${{ inputs.job_id }}
|
||||
RUNPOD_API_KEY: ${{ secrets.RUNPOD_API_KEY }}
|
||||
GITHUB_RUN_ID: ${{ github.run_id }}
|
||||
WANDB_API_KEY: ${{ secrets.WANDB_API_KEY }}
|
||||
timeout-minutes: ${{ inputs.timeout_minutes }}
|
||||
run: >-
|
||||
python .github/scripts/runpod_api.py
|
||||
--gpu-type "${{ inputs.gpu_type }}"
|
||||
--gpu-count ${{ inputs.gpu_count }}
|
||||
--volume-size ${{ inputs.volume_size }}
|
||||
--disk-size ${{ inputs.disk_size }}
|
||||
--image "${{ inputs.image }}"
|
||||
--test-command "${{ inputs.test_command }}"
|
||||
|
||||
- name: Terminate RunPod Instances
|
||||
if: ${{ always() }}
|
||||
env:
|
||||
RUNPOD_API_KEY: ${{ secrets.RUNPOD_API_KEY }}
|
||||
GITHUB_RUN_ID: ${{ github.run_id }}
|
||||
JOB_ID: ${{ inputs.job_id }}
|
||||
run: python .github/scripts/runpod_cleanup.py
|
||||
@@ -1,249 +0,0 @@
|
||||
name: Publish Sliding Tile Attention Kernel to PyPI on Version Change
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "csrc/attn/setup_sta.py"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
check-version-change:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
version-changed: ${{ steps.check-version.outputs.changed }}
|
||||
new-version: ${{ steps.check-version.outputs.new-version }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Check if version changed
|
||||
id: check-version
|
||||
run: |
|
||||
cd csrc/attn
|
||||
# Get current commit's version
|
||||
NEW_VERSION=$(grep -oP 'VERSION\s*=\s*"\K[^"]+' setup_sta.py)
|
||||
echo "New version: $NEW_VERSION"
|
||||
|
||||
# Get previous version from git history
|
||||
OLD_VERSION=$(git show HEAD~1:./setup_sta.py | grep -oP 'VERSION\s*=\s*"\K[^"]+' || echo "0.0.0")
|
||||
echo "Old version: $OLD_VERSION"
|
||||
|
||||
if [ "$NEW_VERSION" != "$OLD_VERSION" ]; then
|
||||
echo "Version changed from $OLD_VERSION to $NEW_VERSION"
|
||||
echo "changed=true" >> $GITHUB_OUTPUT
|
||||
echo "new-version=$NEW_VERSION" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "Version did not change"
|
||||
echo "changed=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
build_wheels:
|
||||
name: Build Wheel
|
||||
needs: check-version-change
|
||||
if: ${{ needs.check-version-change.outputs.version-changed == 'true' || github.event_name == 'workflow_dispatch' }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
# Using ubuntu-20.04 instead of 22.04 for more compatibility (glibc). Ideally we'd use the
|
||||
# manylinux docker image, but I haven't figured out how to install CUDA on manylinux.
|
||||
os: [ubuntu-22.04]
|
||||
python-version: ['3.10', '3.11', '3.12', '3.13']
|
||||
torch-version: ['2.5.1', '2.6.0']
|
||||
cuda-version: ['12.4.1', '12.5.1', '12.6.3']
|
||||
|
||||
steps:
|
||||
- name: Free up disk space
|
||||
run: |
|
||||
echo "Initial disk space:"
|
||||
df -h
|
||||
|
||||
# Remove large directories
|
||||
sudo rm -rf /usr/share/dotnet
|
||||
sudo rm -rf /usr/local/lib/android
|
||||
sudo rm -rf /opt/ghc
|
||||
sudo rm -rf /usr/local/share/boost
|
||||
sudo rm -rf /usr/share/swift
|
||||
sudo rm -rf /usr/local/lib/node_modules
|
||||
sudo rm -rf /usr/local/share/powershell
|
||||
sudo rm -rf /usr/share/rust
|
||||
sudo rm -rf /usr/local/.ghcup
|
||||
|
||||
# Remove cached files
|
||||
sudo rm -rf /var/lib/apt/lists/*
|
||||
sudo rm -rf /var/cache/apt/archives/*
|
||||
|
||||
echo "Disk space after cleanup:"
|
||||
df -h
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Install CUDA ${{ matrix.cuda-version }}
|
||||
uses: Jimver/cuda-toolkit@v0.2.21
|
||||
id: cuda-toolkit
|
||||
with:
|
||||
cuda: ${{ matrix.cuda-version }}
|
||||
linux-local-args: '["--toolkit"]'
|
||||
method: 'network'
|
||||
|
||||
- name: Install dependencies (GCC, Clang, CUDA Paths, Git)
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt install -y git patchelf gcc-11 g++-11 clang-11
|
||||
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-11 100 --slave /usr/bin/g++ g++ /usr/bin/g++-11
|
||||
|
||||
# Allow Git to Access Safe Directory
|
||||
git config --global --add safe.directory /__w/FastVideo/FastVideo
|
||||
|
||||
# Set CUDA environment variables
|
||||
export CUDA_HOME=/usr/local/cuda-${{ matrix.cuda-version }}
|
||||
export PATH=${CUDA_HOME}/bin:${PATH}
|
||||
export LD_LIBRARY_PATH=${CUDA_HOME}/lib64:$LD_LIBRARY_PATH
|
||||
|
||||
# Verify installation
|
||||
gcc --version
|
||||
g++ --version
|
||||
clang-11 --version
|
||||
nvcc --version
|
||||
|
||||
- name: Install PyTorch ${{ matrix.torch-version }}+cu${{ matrix.cuda-version }}
|
||||
run: |
|
||||
pip install --upgrade pip
|
||||
# With python 3.13 and torch 2.5.1, unless we update typing-extensions, we get error
|
||||
# AttributeError: attribute '__default__' of 'typing.ParamSpec' objects is not writable
|
||||
pip install typing-extensions==4.12.2
|
||||
# We want to figure out the CUDA version to download pytorch
|
||||
# e.g. we can have system CUDA version being 11.7 but if torch==1.12 then we need to download the wheel from cu116
|
||||
# see https://github.com/pytorch/pytorch/blob/main/RELEASE.md#release-compatibility-matrix
|
||||
export TORCH_CUDA_VERSION=124
|
||||
pip install --no-cache-dir torch==${{ matrix.torch-version }} --index-url https://download.pytorch.org/whl/cu${TORCH_CUDA_VERSION}
|
||||
nvcc --version
|
||||
python --version
|
||||
python -c "import torch; print('PyTorch:', torch.__version__)"
|
||||
python -c "import torch; print('CUDA:', torch.version.cuda)"
|
||||
python -c "from torch.utils import cpp_extension; print (cpp_extension.CUDA_HOME)"
|
||||
|
||||
- name: Build wheel
|
||||
run: |
|
||||
export PYTHONPATH=$GITHUB_WORKSPACE:$PYTHONPATH
|
||||
|
||||
# We want setuptools >= 49.6.0 otherwise we can't compile the extension if system CUDA version is 11.7 and pytorch cuda version is 11.6
|
||||
# https://github.com/pytorch/pytorch/blob/664058fa83f1d8eede5d66418abff6e20bd76ca8/torch/utils/cpp_extension.py#L810
|
||||
# However this still fails so I'm using a newer version of setuptools
|
||||
pip install setuptools
|
||||
pip install ninja packaging wheel
|
||||
|
||||
cd csrc/attn # Move into the correct folder
|
||||
git submodule update --init --recursive # Ensure ThunderKittens submodule is initialized
|
||||
python setup_sta.py bdist_wheel --dist-dir=dist
|
||||
|
||||
- name: Rename wheel file
|
||||
run: |
|
||||
cd csrc/attn
|
||||
|
||||
CUDA_SHORT_VERSION=$(echo ${{ matrix.cuda-version }} | cut -d. -f1,2 | sed 's/\.//g')
|
||||
TORCH_SHORT_VERSION=$(echo ${{ matrix.torch-version }} | cut -d. -f1,2)
|
||||
# Get the correct version format
|
||||
tmpname=cu${CUDA_SHORT_VERSION}torch${TORCH_SHORT_VERSION}
|
||||
wheel_name=$(ls dist/*whl | xargs -n 1 basename | sed "s/-/+$tmpname-/2")
|
||||
# Rename with version information
|
||||
ls dist/*whl |xargs -I {} mv {} dist/${wheel_name}
|
||||
echo "wheel_name=${wheel_name}" >> $GITHUB_ENV
|
||||
|
||||
- name: Upload wheel artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ env.wheel_name }}
|
||||
path: csrc/attn/dist/*.whl
|
||||
retention-days: 90
|
||||
|
||||
publish_package:
|
||||
name: Publish package
|
||||
needs: [build_wheels, check-version-change]
|
||||
if: ${{ needs.check-version-change.outputs.version-changed == 'true' || github.event_name == 'workflow_dispatch' }}
|
||||
runs-on: ubuntu-22.04
|
||||
permissions:
|
||||
id-token: write # Needed for OIDC Trusted Publishing
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.10'
|
||||
|
||||
- name: Install CUDA 12.4.1
|
||||
uses: Jimver/cuda-toolkit@v0.2.21
|
||||
id: cuda-toolkit
|
||||
with:
|
||||
cuda: 12.4.1
|
||||
linux-local-args: '["--toolkit"]'
|
||||
method: 'network'
|
||||
sub-packages: '["nvcc"]'
|
||||
|
||||
- name: Install dependencies (GCC, Clang, CUDA Paths, Git)
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt install -y git patchelf gcc-11 g++-11 clang-11
|
||||
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-11 100 --slave /usr/bin/g++ g++ /usr/bin/g++-11
|
||||
|
||||
# Allow Git to Access Safe Directory
|
||||
git config --global --add safe.directory /__w/FastVideo/FastVideo
|
||||
|
||||
# Set CUDA environment variables
|
||||
export CUDA_HOME=/usr/local/cuda-12.4.1
|
||||
export PATH=${CUDA_HOME}/bin:${PATH}
|
||||
export LD_LIBRARY_PATH=${CUDA_HOME}/lib64:$LD_LIBRARY_PATH
|
||||
|
||||
# Verify installation
|
||||
gcc --version
|
||||
g++ --version
|
||||
clang-11 --version
|
||||
nvcc --version
|
||||
|
||||
- name: Install PyTorch 2.5.1+cu12.4.1
|
||||
run: |
|
||||
pip install --upgrade pip
|
||||
# With python 3.13 and torch 2.5.1, unless we update typing-extensions, we get error
|
||||
# AttributeError: attribute '__default__' of 'typing.ParamSpec' objects is not writable
|
||||
pip install typing-extensions==4.12.2
|
||||
# We want to figure out the CUDA version to download pytorch
|
||||
# e.g. we can have system CUDA version being 11.7 but if torch==1.12 then we need to download the wheel from cu116
|
||||
# see https://github.com/pytorch/pytorch/blob/main/RELEASE.md#release-compatibility-matrix
|
||||
export TORCH_CUDA_VERSION=124
|
||||
pip install --no-cache-dir torch==2.5.1 --index-url https://download.pytorch.org/whl/cu${TORCH_CUDA_VERSION}
|
||||
nvcc --version
|
||||
python --version
|
||||
python -c "import torch; print('PyTorch:', torch.__version__)"
|
||||
python -c "import torch; print('CUDA:', torch.version.cuda)"
|
||||
python -c "from torch.utils import cpp_extension; print (cpp_extension.CUDA_HOME)"
|
||||
|
||||
- name: Build source distribution
|
||||
run: |
|
||||
export PYTHONPATH=$GITHUB_WORKSPACE:$PYTHONPATH
|
||||
|
||||
# We want setuptools >= 49.6.0 otherwise we can't compile the extension if system CUDA version is 11.7 and pytorch cuda version is 11.6
|
||||
# https://github.com/pytorch/pytorch/blob/664058fa83f1d8eede5d66418abff6e20bd76ca8/torch/utils/cpp_extension.py#L810
|
||||
# However this still fails so I'm using a newer version of setuptools
|
||||
pip install setuptools
|
||||
pip install ninja packaging wheel
|
||||
|
||||
cd csrc/attn # Move into the correct folder
|
||||
git submodule update --init --recursive # Ensure ThunderKittens submodule is initialized
|
||||
python setup_sta.py sdist --dist-dir=dist
|
||||
|
||||
- name: Publish release distributions to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
packages-dir: csrc/attn/dist/
|
||||
@@ -1,31 +0,0 @@
|
||||
name: Run Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12' # or any version you need
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip setuptools wheel
|
||||
pip install torch
|
||||
pip install packaging ninja
|
||||
pip install -e .
|
||||
pip install pytest
|
||||
|
||||
- name: Run Pytest
|
||||
run: |
|
||||
pytest --ignore csrc/attn/test
|
||||
+96
-7
@@ -6,6 +6,7 @@ results/
|
||||
wandb/
|
||||
*.ipynb
|
||||
*.jpg
|
||||
!examples/datasets/lingbotworld2/image.jpg
|
||||
*.safetensors
|
||||
*.mp4
|
||||
*.png
|
||||
@@ -14,12 +15,17 @@ wandb/
|
||||
*.pt
|
||||
cache_dir/
|
||||
wandb/
|
||||
venv/
|
||||
.venv/
|
||||
runs/
|
||||
samples/
|
||||
Miniconda3-latest-Linux-x86_64.sh
|
||||
*validation/
|
||||
data/
|
||||
outputs/
|
||||
outputs_audio/
|
||||
outputs_video
|
||||
checkpoints/
|
||||
sbatch.sh
|
||||
*.out
|
||||
env
|
||||
@@ -27,6 +33,16 @@ env
|
||||
**/build/
|
||||
**.pyc
|
||||
**.txt
|
||||
*.log
|
||||
weights/
|
||||
logs/
|
||||
official_weights/
|
||||
converted_weights/
|
||||
|
||||
# SSIM test outputs
|
||||
fastvideo/tests/ssim/generated_videos/
|
||||
**/.cache/**
|
||||
|
||||
|
||||
# Distribution / packaging
|
||||
build/
|
||||
@@ -36,10 +52,18 @@ dist/
|
||||
eggs/
|
||||
.eggs/
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
docs/source/getting_started/examples/
|
||||
docs/source/inference/examples/
|
||||
# MkDocs documentation
|
||||
site/
|
||||
docs/assets/cookbook-serving.json
|
||||
docs/assets/cookbook-config/
|
||||
examples/serving/clients/node_modules/
|
||||
docs/node_modules/
|
||||
docs/getting_started/examples/
|
||||
docs/examples/
|
||||
docs/inference/examples/
|
||||
docs/training/examples/
|
||||
docs/distillation/examples/
|
||||
!requirements-mkdocs.txt
|
||||
|
||||
# VSCode
|
||||
.vscode/
|
||||
@@ -54,8 +78,73 @@ docs/source/inference/examples/
|
||||
# Python pickle files
|
||||
*.pkl
|
||||
|
||||
# Reference videos
|
||||
!fastvideo/v1/tests/ssim/reference_videos/**/*.mp4
|
||||
# Reference videos (negations must come after the catch-all on line below)
|
||||
!fastvideo/tests/nightly/reference_video_*.mp4
|
||||
|
||||
# Static images
|
||||
!docs/source/_static/images/**/*.png
|
||||
!docs/assets/images/**/*.png
|
||||
!comfyui/assets/**/*.png
|
||||
!comfyui/assets/**/*.gif
|
||||
!assets/images/**/*.png
|
||||
!assets/images/**/*.jpg
|
||||
!assets/images/**/*.jpeg
|
||||
!assets/images/**/*.gif
|
||||
!assets/videos/**/*.mp4
|
||||
|
||||
dmd_t2v_output/
|
||||
preprocess_output_text/
|
||||
|
||||
# SvelteKit / Node artifacts under apps/fastvideo_studio/: see apps/fastvideo_studio/.gitignore
|
||||
|
||||
# Next.js / Node artifacts under apps/dreamverse/web/
|
||||
apps/dreamverse/web/node_modules/
|
||||
apps/dreamverse/web/.next/
|
||||
apps/dreamverse/web/out/
|
||||
apps/dreamverse/web/coverage/
|
||||
apps/dreamverse/web/test-results/
|
||||
apps/dreamverse/web/playwright-report/
|
||||
apps/dreamverse/web/.env.local
|
||||
apps/dreamverse/web/.env.development.local
|
||||
apps/dreamverse/web/.env.test.local
|
||||
|
||||
# Generated by apps/dreamverse/scripts/install_native_ffmpeg.sh — host-specific
|
||||
apps/dreamverse/scripts/ffmpeg-env.sh
|
||||
apps/dreamverse/web/.env.production.local
|
||||
|
||||
# Unignore migrated Dreamverse product assets — root .gitignore globally
|
||||
# ignores *.png/*.jpg/*.mp4/*.gif, but apps/dreamverse/web/public/ MUST
|
||||
# be tracked (logo, icons, k2.png, etc.).
|
||||
!apps/dreamverse/web/public/**/*.png
|
||||
!apps/dreamverse/web/public/**/*.jpg
|
||||
!apps/dreamverse/web/public/**/*.jpeg
|
||||
!apps/dreamverse/web/public/**/*.mp4
|
||||
!apps/dreamverse/web/public/**/*.gif
|
||||
!apps/dreamverse/web/prompts/**/*.png
|
||||
!apps/dreamverse/web/prompts/**/*.jpg
|
||||
!apps/dreamverse/web/prompts/**/*.jpeg
|
||||
!apps/dreamverse/web/prompts/**/*.mp4
|
||||
!apps/dreamverse/web/prompts/**/*.gif
|
||||
!apps/dreamverse/gpu-pool.svg
|
||||
!apps/dreamverse/gpu-pool.drawio
|
||||
|
||||
.claude/
|
||||
.codex/
|
||||
.agents/tmp/
|
||||
.sisyphus/
|
||||
openspec/
|
||||
fastvideo/tests/ssim/reference_videos/**
|
||||
!fastvideo/tests/ssim/reference_videos/**/*.mp4
|
||||
!fastvideo/tests/ssim/reference_videos/**/*.png
|
||||
fastvideo/tests/ssim/.reference_videos_download.lock
|
||||
|
||||
# Local H3 MLX kernel / exactness benches (JSON, logs, frames, videos)
|
||||
.kernel_bench/
|
||||
|
||||
# Editor logs and local Python version pins (accidentally committed)
|
||||
*.nvimlog
|
||||
.nvimlog
|
||||
.python-version
|
||||
/LTX-2-Reference/
|
||||
/DFDReference/
|
||||
scripts/benchmarks/minimax_h3_pro6000/headline_results/
|
||||
fastvideo/tests/ssim/.reference_videos_download.lock
|
||||
|
||||
+11
-2
@@ -1,3 +1,12 @@
|
||||
[submodule "csrc/attn/tk"]
|
||||
path = csrc/attn/tk
|
||||
[submodule "fastvideo-kernel/include/tk"]
|
||||
path = fastvideo-kernel/include/tk
|
||||
url = https://github.com/HazyResearch/ThunderKittens.git
|
||||
[submodule "fastvideo-kernel/include/cutlass"]
|
||||
path = fastvideo-kernel/include/cutlass
|
||||
url = https://github.com/NVIDIA/cutlass.git
|
||||
[submodule "fastvideo/third_party/eval/vbench"]
|
||||
path = fastvideo/third_party/eval/vbench
|
||||
url = https://github.com/Vchitect/VBench.git
|
||||
[submodule "fastvideo/third_party/eval/vqeval"]
|
||||
path = fastvideo/third_party/eval/vqeval
|
||||
url = https://github.com/JiusiServe/LongVideoSparseAttention.git
|
||||
|
||||
+24
-24
@@ -3,27 +3,18 @@ default_stages:
|
||||
- manual # Run in CI
|
||||
exclude: |
|
||||
(?x)(
|
||||
fastvideo/v1/third_party/.*|
|
||||
csrc/.*|
|
||||
fastvideo/third_party/.*|
|
||||
fastvideo-kernel/.*|
|
||||
assets/.*|
|
||||
tests/.*|
|
||||
demo/.*|
|
||||
predict\.py|
|
||||
(?<!docs/)tests/.*|
|
||||
scripts/.*|
|
||||
fastvideo/data_preprocess/.*|
|
||||
fastvideo/dataset/.*|
|
||||
fastvideo/distill/.*|
|
||||
fastvideo/distill\.py|
|
||||
fastvideo/distill_adv\.py|
|
||||
fastvideo/models/.*|
|
||||
fastvideo/sample/.*|
|
||||
fastvideo/train\.py|
|
||||
fastvideo/utils/.*|
|
||||
fastvideo/models/(?!wan/(config|vae_config|pipeline_config|definition|__init__)\.py$).*|
|
||||
^apps/dreamverse/web/.*|
|
||||
examples/.*|
|
||||
.github/workflows/fastvideo-publish.yml|
|
||||
.github/workflows/sta-publish.yml|
|
||||
.github/workflows/build-image-template.yml|
|
||||
docs/source/inference/support_matrix.md
|
||||
\.agents/.*|
|
||||
.github/workflows/publish-fastvideo.yml|
|
||||
.github/workflows/_template-build-image.yml
|
||||
)
|
||||
repos:
|
||||
- repo: https://github.com/google/yapf
|
||||
@@ -31,6 +22,7 @@ repos:
|
||||
hooks:
|
||||
- id: yapf
|
||||
args: [--in-place, --verbose]
|
||||
language_version: python3.12
|
||||
additional_dependencies: [toml] # TODO: Remove when yapf is upgraded
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.11.12
|
||||
@@ -43,10 +35,10 @@ repos:
|
||||
- id: codespell
|
||||
additional_dependencies: ['tomli']
|
||||
args: ['--toml', 'pyproject.toml']
|
||||
- repo: https://github.com/PyCQA/isort
|
||||
rev: 6.0.1
|
||||
hooks:
|
||||
- id: isort
|
||||
# - repo: https://github.com/PyCQA/isort
|
||||
# rev: 6.0.1
|
||||
# hooks:
|
||||
# - id: isort
|
||||
- repo: https://github.com/jackdewinter/pymarkdown
|
||||
rev: v0.9.30
|
||||
hooks:
|
||||
@@ -60,8 +52,8 @@ repos:
|
||||
rev: v1.15.0
|
||||
hooks:
|
||||
- id: mypy
|
||||
args: [--python-version, '3.10', --follow-imports, "skip", ]
|
||||
additional_dependencies: [types-cachetools, types-setuptools, types-PyYAML, types-requests]
|
||||
args: [--python-version, '3.10', --follow-imports, "skip", "--disable-error-code", "union-attr", "--disable-error-code", "override" ]
|
||||
additional_dependencies: [types-aiofiles, types-cachetools, types-setuptools, types-PyYAML, types-requests]
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: check-filenames
|
||||
@@ -69,10 +61,18 @@ repos:
|
||||
entry: bash
|
||||
args:
|
||||
- -c
|
||||
- 'git ls-files | grep -v "^fastvideo/v1/tests/ssim/" | grep " " && echo "Filenames should not contain spaces!" && exit 1 || exit 0'
|
||||
- 'git ls-files | grep -v "^\"*fastvideo/tests/ssim/" | grep -v "^\"*fastvideo/tests/inference/lora/L40S_reference_videos/" | grep " " && echo "Filenames should not contain spaces!" && exit 1 || exit 0'
|
||||
language: system
|
||||
always_run: true
|
||||
pass_filenames: false
|
||||
- id: cookbook-javascript
|
||||
name: Cookbook JavaScript lint and format
|
||||
entry: npm run check:cookbook --prefix docs
|
||||
language: system
|
||||
# CI's trusted manual stage must not execute PR-authored npm scripts.
|
||||
stages: [pre-commit]
|
||||
files: ^docs/(js/(build-)?cookbook-.*\.mjs|tests/.*\.test\.mjs|eslint\.config\.mjs|prettier\.config\.mjs|package(-lock)?\.json)$
|
||||
pass_filenames: false
|
||||
# Keep `suggestion` last
|
||||
- id: suggestion
|
||||
name: Suggestion
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
# Repository Guidelines
|
||||
|
||||
## Project Structure & Module Organization
|
||||
- Core Python package: `fastvideo/` (models, pipelines, training, distributed runtime, CLI entrypoints).
|
||||
- CUDA/custom kernels: `fastvideo-kernel/` (separate build/test flow).
|
||||
- Tests:
|
||||
- `fastvideo/tests/` for package-level tests (dataset, encoders, inference, training, SSIM, workflow).
|
||||
- `tests/local_tests/` for additional local/component checks.
|
||||
- Docs and guides: `docs/` (MkDocs source), with contributor docs in `docs/contributing/`.
|
||||
- Runnable examples and scripts: `examples/` and `scripts/`.
|
||||
- Static assets: `assets/` (including `assets/images/`, `assets/videos/`, and `assets/prompts/`) and `comfyui/assets/`.
|
||||
|
||||
## Build, Test, and Development Commands
|
||||
- `UV_TORCH_BACKEND=cu126 uv pip install -e ".[dev]"`: editable CUDA 12 install with lint/test extras (`cu130` on CUDA 13).
|
||||
- `pre-commit install --hook-type pre-commit --hook-type commit-msg`: enable local hooks.
|
||||
- `pre-commit run --all-files`: run formatter/lint/type/spelling checks.
|
||||
- `pytest tests/`: run top-level test suite.
|
||||
- `pytest fastvideo/tests/ -v`: run package tests.
|
||||
- `pytest fastvideo/tests/ssim/ -vs`: run SSIM regression tests (GPU-heavy).
|
||||
- `cd fastvideo-kernel && ./build.sh`: build kernel extensions.
|
||||
|
||||
## Coding Style & Naming Conventions
|
||||
- Python 3.10+; 4-space indentation; keep code and imports readable and explicit.
|
||||
- Style tools are configured in `pyproject.toml` and `.pre-commit-config.yaml`:
|
||||
- `yapf` (format), `ruff` (lint, auto-fix), `mypy` (typing), `codespell`.
|
||||
- Lint via `pre-commit run --files <changed paths>` (or `pre-commit run --all-files` for a full sweep) before committing. Do not shell out to `yapf`/`ruff`/`codespell`/`mypy` directly — pre-commit chains them with the project's config and respects the `.pre-commit-config.yaml` excludes (e.g. `fastvideo/tests/` is intentionally skipped). If pre-commit reports `(no files to check)` for your paths, that exclude is deliberate — don't bypass it.
|
||||
- Target line length is 120 (configured in `pyproject.toml` for ruff, yapf, and isort).
|
||||
- Naming: `snake_case` for functions/files, `PascalCase` for classes, `UPPER_SNAKE_CASE` for constants.
|
||||
|
||||
## Testing Guidelines
|
||||
- Use `pytest` and place tests near relevant domains (e.g., `fastvideo/tests/encoders/`).
|
||||
- Prefer descriptive names like `test_<feature>_<expected_behavior>.py`.
|
||||
- For new pipelines/backends, include at least one regression-oriented test; add SSIM coverage when output quality must be preserved.
|
||||
- Document GPU assumptions in tests that require specific hardware.
|
||||
|
||||
## Commit & Pull Request Guidelines
|
||||
- Follow existing commit style: short subject with optional tag prefix, e.g. `[bugfix]: ...`, `[feat]: ...`, `[misc]: ...`, and include PR reference like `(#1234)` when applicable.
|
||||
- Keep commits focused by concern (feature, refactor, fix).
|
||||
- PRs should include:
|
||||
- clear problem/solution summary,
|
||||
- test evidence (`pytest`/SSIM outputs or rationale if skipped),
|
||||
- linked issue/PR context,
|
||||
- screenshots or sample outputs for UI/demo/docs changes.
|
||||
|
||||
## Agent Infrastructure
|
||||
|
||||
This repository is agent-friendly. Before doing any work:
|
||||
|
||||
1. Read the nearest in-scope `AGENTS.md` for every directory you may edit.
|
||||
2. Read the relevant user-facing design or contributor guide.
|
||||
3. Check `.agents/skills/*/SKILL.md` for a task-specific workflow.
|
||||
4. Search `.agents/lessons/` for relevant pitfalls.
|
||||
|
||||
Architecture, commands, and operational guidance belong beside the code or
|
||||
under `docs/`. Do not maintain static codebase maps, experiment journals,
|
||||
branch-state snapshots, or duplicate user-facing documentation under
|
||||
`.agents/`. Put a reusable procedure directly in a skill or contributor guide
|
||||
and capture durable failures in `.agents/lessons/`.
|
||||
|
||||
## Per-Directory AGENTS.md
|
||||
|
||||
Local guidance lives next to the code. Read the in-scope file before editing:
|
||||
|
||||
| Directory | What it covers |
|
||||
|-----------|----------------|
|
||||
| `fastvideo/AGENTS.md` | Core package map, public API, registry-driven model dispatch |
|
||||
| `fastvideo/configs/AGENTS.md` | Arch + pipeline config dataclasses, `param_names_mapping` |
|
||||
| `fastvideo/models/AGENTS.md` | DiT / VAE / encoder / scheduler / loader layout (pre-commit excluded) |
|
||||
| `fastvideo/models/wan/AGENTS.md` | Wan family-local transformers, VAE, configs, and the SP sharding invariant |
|
||||
| `fastvideo/layers/AGENTS.md` | Tensor-parallel linear/attention layer rules for ports |
|
||||
| `fastvideo/attention/AGENTS.md` | Backend registry + env-var override |
|
||||
| `fastvideo/pipelines/AGENTS.md` | Stage ABC, `basic/<model>/`, `preprocess/`, presets |
|
||||
| `fastvideo/pipelines/basic/wan/AGENTS.md` | Wan sampling stages, first-frame conditioning, DMD/causal boundaries |
|
||||
| `fastvideo/pipelines/basic/magi_human/AGENTS.md` | MagiHuman umbrella repo, lazy-loaded components, packing invariants |
|
||||
| `fastvideo/training/AGENTS.md` | Legacy monolithic pipelines (frozen for existing models) |
|
||||
| `fastvideo/train/AGENTS.md` | New modular trainer (methods × models × callbacks, YAML) |
|
||||
| `fastvideo/tests/AGENTS.md` | Test taxonomy, conftest, pre-commit-excluded path |
|
||||
| `fastvideo/tests/ssim/AGENTS.md` | GPU SSIM regression authoring + reference video sync |
|
||||
| `scripts/checkpoint_conversion/AGENTS.md` | Adding a converter for a new HF/official checkpoint |
|
||||
| `apps/dreamverse/AGENTS.md` | DreamVerse app structure and conventions |
|
||||
|
||||
## Critical: Two Training Stacks Coexist
|
||||
|
||||
- `fastvideo/training/` — legacy, monolithic per-model `*_training_pipeline.py` and
|
||||
`*_distillation_pipeline.py`. Still authoritative for shipped models.
|
||||
- `fastvideo/train/` — new modular framework (composable methods × models × callbacks
|
||||
driven by YAML). Preferred for new training work.
|
||||
|
||||
Pick the matching stack before editing. Do not migrate a pipeline between them
|
||||
without an explicit ask — the conventions and config surfaces differ.
|
||||
@@ -1,63 +1,142 @@
|
||||
<div align="center">
|
||||
<img src=assets/logo.jpg width="30%"/>
|
||||
<img src=assets/logos/logo.svg width="30%"/>
|
||||
</div>
|
||||
|
||||
**FastVideo is a unified framework for accelerated video generation.**
|
||||
|
||||
It features a clean, consistent API that works across popular video models, making it easier for developers to author new models and incorporate system- or kernel-level optimizations.
|
||||
With FastVideo's optimizations, you can achieve more than 3x inference improvement compared to other systems.
|
||||
|
||||
<p align="center">
|
||||
| <a href="https://hao-ai-lab.github.io/FastVideo"><b>Documentation</b></a> | <a href="https://hao-ai-lab.github.io/FastVideo/inference/inference_quick_start.html"><b> Quick Start</b></a> | 🤗 <a href="https://huggingface.co/FastVideo/FastHunyuan" target="_blank"><b>FastHunyuan</b></a> | 🤗 <a href="https://huggingface.co/FastVideo/FastMochi-diffusers" target="_blank"><b>FastMochi</b></a> | 🟣💬 <a href="https://join.slack.com/t/fastvideo/shared_invite/zt-2zf6ru791-sRwI9lPIUJQq1mIeB_yjJg" target="_blank"> <b>Slack</b> </a> |
|
||||
| <a href="https://hao-ai-lab.github.io/FastVideo"><b>Documentation</b></a> | <a href="https://haoailab.com/FastVideo/cookbook/"><b>Cookbook</b></a> | <a href="https://hao-ai-lab.github.io/FastVideo/inference/inference_quick_start/"><b> Quick Start</b></a> | <a href="https://github.com/hao-ai-lab/FastVideo/discussions/982" target="_blank"><b>Weekly Dev Meeting</b></a> | 🟣💬 <a href="https://join.slack.com/t/fastvideo/shared_invite/zt-3f4lao1uq-u~Ipx6Lt4J27AlD2y~IdLQ" target="_blank"> <b>Slack</b> </a> | 🟣💬 <a href="https://github.com/hao-ai-lab/FastVideo/discussions/1097" target="_blank"> <b> WeChat </b> </a> |
|
||||
</p>
|
||||
|
||||
<div align="center">
|
||||
<img src=assets/perf.png width="90%"/>
|
||||
</div>
|
||||
**FastVideo is a unified post-training and real-time inference framework for accelerated video generation.**
|
||||
|
||||
## NEWS
|
||||
- `2026/10/06`: FastH3 V2 now runs on a single consumer machine: NVIDIA RTX 5090, RTX 4090 and RTX PRO 6000 GPUs, DGX Spark and Apple Silicon. We also release [FastH3 Trim](https://huggingface.co/FastVideo/FastVideo-FastH3-Trim-8-Step-NVFP4), an experimental pruned model that is 4.2× smaller than base H3 and runs in as little as 8 GB of GPU memory. Get the [models](https://huggingface.co/collections/FastVideo/fastvideo-fasth3) and read the [Blog](https://haoailab.com/blogs/fasth3-rtx/).
|
||||
- `2026/10/06`: FastVideo now supports [Kandinsky 6](https://x.com/kandinskylab_ai/status/2107374635218055345) from Kandinsky Lab: text- and image-to-video with synchronized audio (base and 10-step distilled pi-Flow checkpoints) plus video super-resolution up to 4x. See the [Kandinsky 6 recipes](https://haoailab.com/FastVideo/cookbook/kandinsky6/).
|
||||
- `2026/09/15`: Release [FastH3 8-Step V2](https://huggingface.co/FastVideo/FastVideo-FastH3-8-Step-V2), an eight-forward data-free DMD2 checkpoint distilled from MiniMax-H3 with 80% Video Sparse Attention. Run it with `examples/inference/basic/basic_fasth3_8step.py` or the [FastH3 8-Step V2 recipe](https://haoailab.com/FastVideo/cookbook/minimax-h3/).
|
||||
- `2026/09/01`: FastH3 now runs locally on Apple Silicon through MLX and on NVIDIA DGX Spark through CUDA 13, including two-Spark inference. Follow the [FastH3 recipes](https://haoailab.com/FastVideo/cookbook/minimax-h3/) and read the [Blog](https://haoailab.com/blogs/fasth3-local/).
|
||||
- `2026/08/27`: [FastH3 Preview v1](https://haoailab.com/blogs/fasth3-preview/) is an open-weight 4-step sparse-distilled MiniMax-H3 model for synchronized video-and-audio generation, developed in collaboration with [Nuva Lab](https://nuvalab.ai/) and the [NVIDIA FastGen team](https://github.com/NVlabs/FastGen). Download the recommended [VSA / Data-Free weights](https://huggingface.co/FastVideo/FastVideo-FastH3-4-step-Preview-v1-VSA-DataFree), or see the [full FastH3 collection](https://huggingface.co/collections/FastVideo/fastvideo-fasth3).
|
||||
- `2026/08/19`: FastVideo now supports MLX on Apple Silicon with [FastMetal-QAD](https://huggingface.co/collections/FastVideo/fastmetal), a family of 1.3B, 5B, and 14B models optimized for Mac. Follow the [MLX install guide](https://hao-ai-lab.github.io/FastVideo/getting_started/installation/mlx/) and read the [Blog](https://haoailab.com/blogs/fastmetal/).
|
||||
- `2026/06/23`: Release FastWan-QAD: 5s of Video generated in 1.8s E2E. See the [FastWan-QAD models](https://huggingface.co/FastVideo/FastWan-QAD-FP8-1.3B), [Attn-QAT training guide](https://haoailab.com/FastVideo/training/attn_qat/), and [blog](https://haoailab.com/blogs/fastwan-qad/).
|
||||
- `2026/03/17`: Release demo: Into the Dreamverse: Vibe Directing in FastVideo, check out the [Blog](https://haoailab.com/blogs/dreamverse/).
|
||||
- `2026/03/13`: Release demo: Create a 5s 1080p Video in 4.5s with FastVideo on a Single GPU, check out the [Blog](https://haoailab.com/blogs/fastvideo_realtime_1080p/).
|
||||
- `2025/11/19`: Release [CausalWan2.2 I2V A14B Preview](https://huggingface.co/FastVideo/CausalWan2.2-I2V-A14B-Preview-Diffusers) models, [Blog](https://hao-ai-lab.github.io/blogs/fastvideo_causalwan_preview/) and [Inference Code!](https://github.com/hao-ai-lab/FastVideo/blob/main/examples/inference/basic/basic_self_forcing_causal_wan2_2_i2v.py).
|
||||
- `2025/08/04`: Release [FastWan](https://hao-ai-lab.github.io/FastVideo/distillation/dmd) models and [Sparse-Distillation](https://hao-ai-lab.github.io/blogs/fastvideo_post_training/).
|
||||
|
||||
### More News
|
||||
|
||||
- `2025/06/14`: Release finetuning and inference code for [VSA](https://arxiv.org/pdf/2505.13389).
|
||||
- `2025/04/24`: [FastVideo V1](https://hao-ai-lab.github.io/blogs/fastvideo/) is released!
|
||||
- `2025/02/18`: Release the inference code for [Sliding Tile Attention](https://hao-ai-lab.github.io/blogs/sta/).
|
||||
|
||||
## Key Features
|
||||
|
||||
FastVideo has the following features:
|
||||
|
||||
- End-to-end post-training support for bidirectional and autoregressive models:
|
||||
- Support full finetuning and LoRA finetuning for state-of-the-art open video DiTs
|
||||
- Data preprocessing pipeline for video, image, and text data
|
||||
- Distribution Matching Distillation (DMD2) stepwise distillation.
|
||||
- Sparse attention with [Video Sparse Attention](https://arxiv.org/pdf/2505.13389)
|
||||
- [Sparse distillation](https://hao-ai-lab.github.io/blogs/fastvideo_post_training/) to achieve >50x denoising speedup
|
||||
- Scalable training with FSDP2, sequence parallelism, and selective activation checkpointing.
|
||||
- Causal distillation through Self-Forcing
|
||||
- See this [page](https://hao-ai-lab.github.io/FastVideo/training/overview/) for the supported training workflows, and the [support matrix](https://hao-ai-lab.github.io/FastVideo/inference/support_matrix/) for supported models.
|
||||
- State-of-the-art performance optimizations for inference
|
||||
- [Sliding Tile Attention](https://arxiv.org/pdf/2502.04507)
|
||||
- [TeaCache](https://arxiv.org/pdf/2411.19108)
|
||||
- [Sage Attention](https://arxiv.org/abs/2410.02367)
|
||||
- Cutting edge models
|
||||
- Wan2.1 T2V, I2V
|
||||
- HunyuanVideo
|
||||
- FastHunyuan: consistency distilled video diffusion models for 8x inference speedup.
|
||||
- StepVideo T2V
|
||||
- Distillation support
|
||||
- Recipes for video DiT, based on [PCM](https://github.com/G-U-N/Phased-Consistency-Model).
|
||||
- Support distilling/finetuning/inferencing state-of-the-art open video DiTs: 1. Mochi 2. Hunyuan.
|
||||
- Scalable training with FSDP, sequence parallelism, and selective activation checkpointing, with near linear scaling to 64 GPUs.
|
||||
- Memory efficient finetuning with LoRA, precomputed latent, and precomputed text embeddings.
|
||||
- Sequence Parallelism for distributed inference
|
||||
- Multiple state-of-the-art attention backends
|
||||
- User-friendly CLI and Python API
|
||||
- See this [page](https://hao-ai-lab.github.io/FastVideo/inference/optimizations/) for full list of supported optimizations.
|
||||
- Diverse hardware and OS support
|
||||
- Support H100, A100, 4090
|
||||
- Support Linux, Windows, MacOS
|
||||
- See this [page](https://hao-ai-lab.github.io/FastVideo/inference/support_matrix/) for full list of supported models, hardware assumptions, and optimization compatibility.
|
||||
- Realtime video generation & editing
|
||||
- [Dreamverse](apps/dreamverse/README.md): stream and "vibe direct" video in realtime ([live demo](https://dreamverse.fastvideo.org/)), deployable on local GPU, a self-hosted B200 server, Docker, or serverless Modal
|
||||
|
||||
## Getting Started
|
||||
We recommend using an environment manager such as `Conda` to create a clean environment:
|
||||
|
||||
We recommend using [uv](https://docs.astral.sh/uv/) to create a clean environment. If you previously used Conda, switching to uv generally gives faster and more stable installs.
|
||||
|
||||
```bash
|
||||
# Create and activate a new conda environment
|
||||
conda create -n fastvideo python=3.12
|
||||
conda activate fastvideo
|
||||
# Create and activate a new uv environment
|
||||
uv venv --python 3.12 --seed
|
||||
source .venv/bin/activate
|
||||
|
||||
# Install FastVideo
|
||||
pip install fastvideo
|
||||
# Install FastVideo on NVIDIA CUDA 12
|
||||
UV_TORCH_BACKEND=cu126 uv pip install fastvideo
|
||||
```
|
||||
|
||||
Please see our [docs](https://hao-ai-lab.github.io/FastVideo/getting_started/installation.html) for more detailed installation instructions.
|
||||
Use `UV_TORCH_BACKEND=cu130` on CUDA 13. Apple silicon users should follow the
|
||||
[MLX install guide](https://hao-ai-lab.github.io/FastVideo/getting_started/installation/mlx/).
|
||||
|
||||
> **On an Apple Silicon Mac?** Install with `uv pip install -e '.[mlx]'` from
|
||||
> a clone, then pick a recipe in the
|
||||
> [cookbook](https://haoailab.com/FastVideo/cookbook/). See the
|
||||
> [MLX install guide](https://hao-ai-lab.github.io/FastVideo/getting_started/installation/mlx/).
|
||||
|
||||
Please see our [docs](https://hao-ai-lab.github.io/FastVideo/getting_started/installation/) for more detailed installation instructions.
|
||||
|
||||
> **On an NVIDIA DGX Spark (GB10 / ARM64 + CUDA 13)?** There's no prebuilt ARM wheel for the FastVideo CUDA kernel, so it's an editable from-source install (`UV_TORCH_BACKEND=cu130 uv pip install -e .`, which compiles that kernel for you) rather than `UV_TORCH_BACKEND=cu130 uv pip install fastvideo`. A compatible prebuilt ARM64 FlashAttention wheel is available separately. Follow the [DGX Spark install guide](https://hao-ai-lab.github.io/FastVideo/getting_started/installation/spark/).
|
||||
|
||||
### Install with an AI coding agent
|
||||
|
||||
FastVideo is a monorepo with rich agent guidance (see [`AGENTS.md`](AGENTS.md)). If you use Claude Code, Cursor, or another coding agent, paste the prompt below — it detects your platform and follows the matching guide:
|
||||
|
||||
```text
|
||||
Install FastVideo (https://github.com/hao-ai-lab/FastVideo) into a fresh uv virtual environment.
|
||||
|
||||
1. Detect the platform: run `uname -m`, `nvidia-smi`, and `nvcc --version`.
|
||||
2. Read and follow the matching install guide exactly (in this repo, or at
|
||||
https://hao-ai-lab.github.io/FastVideo/getting_started/installation/):
|
||||
- NVIDIA GPU, x86_64 -> docs/getting_started/installation/gpu.md
|
||||
- NVIDIA DGX Spark / GB10, aarch64, CUDA 13 -> docs/getting_started/installation/spark.md
|
||||
- Apple Silicon, macOS -> docs/getting_started/installation/mlx.md
|
||||
3. Use uv for every step. If a command fails, debug it and tell me what you changed.
|
||||
4. Verify the result:
|
||||
python -c "import fastvideo, torch; print('cuda', torch.cuda.is_available())"
|
||||
fastvideo --help
|
||||
5. Report which platform you detected and any deviations you had to make.
|
||||
```
|
||||
|
||||
## Sparse Distillation
|
||||
|
||||
For our sparse distillation techniques, please see our [distillation docs](https://hao-ai-lab.github.io/FastVideo/distillation/dmd/) and check out our [blog](https://hao-ai-lab.github.io/blogs/fastvideo_post_training/).
|
||||
|
||||
See below for recipes and datasets:
|
||||
|
||||
| Model | Sparse Distillation | Dataset |
|
||||
| ------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
|
||||
| [FastWan2.1-T2V-1.3B](https://huggingface.co/FastVideo/FastWan2.1-T2V-1.3B-Diffusers) | [Recipe](https://github.com/hao-ai-lab/FastVideo/tree/main/examples/distill/Wan2.1-T2V/Wan-Syn-Data-480P) | [FastVideo Synthetic Wan2.1 480P](https://huggingface.co/datasets/FastVideo/Wan-Syn_77x448x832_600k) |
|
||||
| [FastWan2.2-TI2V-5B](https://huggingface.co/FastVideo/FastWan2.2-TI2V-5B-Diffusers) | [Recipe](https://github.com/hao-ai-lab/FastVideo/tree/main/examples/distill/Wan2.2-TI2V-5B-Diffusers/Data-free) | [FastVideo Synthetic Wan2.2 720P](https://huggingface.co/datasets/FastVideo/Wan2.2-Syn-121x704x1280_32k) |
|
||||
|
||||
## Dreamverse — Realtime Video Generation & Editing
|
||||
|
||||
[Dreamverse](apps/dreamverse/README.md) is FastVideo's realtime video generation
|
||||
and editing platform — "vibe directing" a video as it streams. It lives in the
|
||||
monorepo under [`apps/dreamverse/`](apps/dreamverse/) and ships its own backend
|
||||
(`dreamverse-server`) plus a web UI.
|
||||
|
||||
Try the [live demo](https://dreamverse.fastvideo.org/), read the
|
||||
[blog](https://haoailab.com/blogs/dreamverse/), or run it yourself. Dreamverse
|
||||
deploys on a local GPU, a self-hosted B200 server over SSH, Docker, or
|
||||
serverless [Modal](apps/dreamverse/scripts/modal/README.md) — see the
|
||||
[Dreamverse README](apps/dreamverse/README.md).
|
||||
|
||||
## Inference
|
||||
|
||||
### Generating Your First Video
|
||||
Here's a minimal example to generate a video using the default settings. Create a file called `example.py` with the following code:
|
||||
|
||||
Here's a minimal example to generate a video using the default settings. Make sure VSA kernels are [installed](https://hao-ai-lab.github.io/FastVideo/attention/vsa/#installation). Create a file called `example.py` with the following code:
|
||||
|
||||
```python
|
||||
import os
|
||||
from fastvideo import VideoGenerator
|
||||
|
||||
def main():
|
||||
os.environ["FASTVIDEO_ATTENTION_BACKEND"] = "VIDEO_SPARSE_ATTN"
|
||||
|
||||
# Create a video generator with a pre-trained model
|
||||
generator = VideoGenerator.from_pretrained(
|
||||
"Wan-AI/Wan2.1-T2V-1.3B-Diffusers",
|
||||
"FastVideo/FastWan2.1-T2V-1.3B-Diffusers",
|
||||
num_gpus=1, # Adjust based on your hardware
|
||||
)
|
||||
|
||||
@@ -67,7 +146,6 @@ def main():
|
||||
# Generate the video
|
||||
video = generator.generate_video(
|
||||
prompt,
|
||||
return_frames=True, # Also return frames from this call (defaults to False)
|
||||
output_path="my_videos/", # Controls where videos are saved
|
||||
save_video=True
|
||||
)
|
||||
@@ -76,74 +154,58 @@ if __name__ == '__main__':
|
||||
main()
|
||||
```
|
||||
|
||||
`num_gpus=1` runs the worker in-process (weights load once, no extra Python process). On Colab/Kaggle-style machines with ~16GB host RAM, keep `num_gpus=1`; free-tier system memory does not grow with extra T4s, so `num_gpus>1` is likely to OOM.
|
||||
|
||||
Run the script with:
|
||||
|
||||
```bash
|
||||
python example.py
|
||||
```
|
||||
|
||||
For a more detailed guide, please see our [inference quick start](https://hao-ai-lab.github.io/FastVideo/inference/inference_quick_start.html).
|
||||
For a more detailed guide, please see our [inference quick start](https://hao-ai-lab.github.io/FastVideo/inference/inference_quick_start/).
|
||||
|
||||
### Other docs:
|
||||
## More Guides
|
||||
|
||||
- [Design Overview](https://hao-ai-lab.github.io/FastVideo/design/overview.html)
|
||||
- [Contribution Guide](https://hao-ai-lab.github.io/FastVideo/getting_started/installation.html)
|
||||
- [Design Overview](https://hao-ai-lab.github.io/FastVideo/design/overview/)
|
||||
- [Distillation Guide](https://hao-ai-lab.github.io/FastVideo/distillation/dmd/)
|
||||
- [Contribution Guide](https://hao-ai-lab.github.io/FastVideo/contributing/overview/)
|
||||
|
||||
## Distillation and Finetuning
|
||||
- [Distillation Guide](https://hao-ai-lab.github.io/FastVideo/training/distillation.html)
|
||||
- [Finetuning Guide](https://hao-ai-lab.github.io/FastVideo/training/finetune.html)
|
||||
## Awesome work using FastVideo or our research projects
|
||||
|
||||
## 📑 Development Plan
|
||||
|
||||
<!-- - More distillation methods -->
|
||||
<!-- - [ ] Add Distribution Matching Distillation -->
|
||||
- More models support
|
||||
<!-- - [ ] Add CogvideoX model -->
|
||||
- [x] Add StepVideo to V1
|
||||
- Optimization features
|
||||
- [x] Teacache in V1
|
||||
- [x] SageAttention in V1
|
||||
- Code updates
|
||||
- [x] V1 Configuration API
|
||||
- [ ] Support Training in V1
|
||||
<!-- - [ ] fp8 support -->
|
||||
<!-- - [ ] faster load model and save model support -->
|
||||
- [SGLang](https://github.com/sgl-project/sglang/tree/main/python/sglang/multimodal_gen): SGLang's diffusion inference functionality is based on a fork of FastVideo on Sept. 24, 2025.
|
||||
- [DanceGRPO](https://github.com/XueZeyue/DanceGRPO): A unified framework to adapt Group Relative Policy Optimization (GRPO) to visual generation paradigms. Code based on FastVideo.
|
||||
- [SRPO](https://github.com/Tencent-Hunyuan/SRPO): A method to directly align the full diffusion trajectory with fine-grained human preference. Code based on FastVideo.
|
||||
- [DCM](https://github.com/Vchitect/DCM): Dual-expert consistency model for efficient and high-quality video generation. Code based on FastVideo.
|
||||
- [HY-WorldPlay](https://github.com/Tencent-Hunyuan/HY-WorldPlay): An action-conditioned world model model trained using FastVideo framework.
|
||||
- [Hunyuan Video 1.5](https://github.com/Tencent-Hunyuan/HunyuanVideo-1.5): A leading lightweight video generation model, where they proposed SSTA based on Sliding Tile Attention.
|
||||
- [Kandinsky-5.0](https://github.com/kandinskylab/kandinsky-5): A family of diffusion models for video & image generation, where their NABLA attention includes a Sliding Tile Attention branch.
|
||||
- [LongCat Video](https://github.com/meituan-longcat/LongCat-Video): A foundational video generation model with 13.6B parameters with block-sparse attention similar to Video Sparse Attention.
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
We welcome all contributions. Please check out our guide [here](https://hao-ai-lab.github.io/FastVideo/contributing/overview.html)
|
||||
We welcome all contributions. Please check out our guide [here](https://hao-ai-lab.github.io/FastVideo/contributing/overview/).
|
||||
See details in [development roadmap](https://github.com/hao-ai-lab/FastVideo/issues/899).
|
||||
|
||||
## Acknowledgement
|
||||
We learned and reused code from the following projects:
|
||||
- [PCM](https://github.com/G-U-N/Phased-Consistency-Model)
|
||||
- [diffusers](https://github.com/huggingface/diffusers)
|
||||
- [OpenSoraPlan](https://github.com/PKU-YuanGroup/Open-Sora-Plan)
|
||||
- [xDiT](https://github.com/xdit-project/xDiT)
|
||||
- [vLLM](https://github.com/vllm-project/vllm)
|
||||
- [SGLang](https://github.com/sgl-project/sglang)
|
||||
|
||||
We thank MBZUAI and [Anyscale](https://www.anyscale.com/) for their support throughout this project.
|
||||
We learned the design and reused code from the following projects: [Wan-Video](https://github.com/Wan-Video), [ThunderKittens](https://github.com/HazyResearch/ThunderKittens), [DMD2](https://github.com/tianweiy/DMD2), [diffusers](https://github.com/huggingface/diffusers), [xDiT](https://github.com/xdit-project/xDiT), [vLLM](https://github.com/vllm-project/vllm), [SGLang](https://github.com/sgl-project/sglang). We thank [MBZUAI](https://ifm.mbzuai.ac.ae/), [Anyscale](https://www.anyscale.com/), and [GMI Cloud](https://www.gmicloud.ai/) for their support throughout this project.
|
||||
|
||||
## Citation
|
||||
If you use FastVideo for your research, please cite our paper:
|
||||
|
||||
If you find FastVideo useful, please consider citing our research work:
|
||||
|
||||
```bibtex
|
||||
@misc{zhang2025fastvideogenerationsliding,
|
||||
title={Fast Video Generation with Sliding Tile Attention},
|
||||
author={Peiyuan Zhang and Yongqi Chen and Runlong Su and Hangliang Ding and Ion Stoica and Zhenghong Liu and Hao Zhang},
|
||||
year={2025},
|
||||
eprint={2502.04507},
|
||||
archivePrefix={arXiv},
|
||||
primaryClass={cs.CV},
|
||||
url={https://arxiv.org/abs/2502.04507},
|
||||
@article{zhang2025vsa,
|
||||
title={Vsa: Faster video diffusion with trainable sparse attention},
|
||||
author={Zhang, Peiyuan and Chen, Yongqi and Huang, Haofeng and Lin, Will and Liu, Zhengzhong and Stoica, Ion and Xing, Eric and Zhang, Hao},
|
||||
journal={arXiv preprint arXiv:2505.13389},
|
||||
year={2025}
|
||||
}
|
||||
@misc{ding2025efficientvditefficientvideodiffusion,
|
||||
title={Efficient-vDiT: Efficient Video Diffusion Transformers With Attention Tile},
|
||||
author={Hangliang Ding and Dacheng Li and Runlong Su and Peiyuan Zhang and Zhijie Deng and Ion Stoica and Hao Zhang},
|
||||
year={2025},
|
||||
eprint={2502.06155},
|
||||
archivePrefix={arXiv},
|
||||
primaryClass={cs.CV},
|
||||
url={https://arxiv.org/abs/2502.06155},
|
||||
|
||||
@article{zhang2025fast,
|
||||
title={Fast video generation with sliding tile attention},
|
||||
author={Zhang, Peiyuan and Chen, Yongqi and Su, Runlong and Ding, Hangliang and Stoica, Ion and Liu, Zhengzhong and Zhang, Hao},
|
||||
journal={arXiv preprint arXiv:2502.04507},
|
||||
year={2025}
|
||||
}
|
||||
```
|
||||
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
try:
|
||||
from .comfyui.video_generator.nodes import (NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS)
|
||||
WEB_DIRECTORY = "./web"
|
||||
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS', 'WEB_DIRECTORY']
|
||||
except ImportError:
|
||||
# ComfyUI environment not available, skip comfyui imports
|
||||
NODE_CLASS_MAPPINGS = {}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {}
|
||||
WEB_DIRECTORY = "./web"
|
||||
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS', 'WEB_DIRECTORY']
|
||||
@@ -0,0 +1,215 @@
|
||||
# Dreamverse Agent Notes
|
||||
|
||||
## Repo-local workflows
|
||||
|
||||
- Use `.agents/skills/dreamverse-deploy/` to redeploy or stop the local
|
||||
backend/frontend stack on a chosen physical GPU.
|
||||
- Use `apps/dreamverse/scripts/modal/README.md` for Modal deployments and
|
||||
`apps/dreamverse/docker/README.md` for image builds. The local deploy skill
|
||||
intentionally does not manage remote deployments.
|
||||
|
||||
## Repo layout
|
||||
|
||||
Current paths:
|
||||
|
||||
- `apps/dreamverse/web/`: Next.js frontend, client-side stores, websocket
|
||||
event reduction, prompt-window editing, devtools UI.
|
||||
- `apps/dreamverse/dreamverse/`: current Python FastAPI runtime, websocket
|
||||
protocol, prompt enhancement, prompt rewrite orchestration, GPU worker
|
||||
lifecycle.
|
||||
- `apps/dreamverse/dreamverse/tests/`: backend unit and integration-oriented
|
||||
tests.
|
||||
- `apps/dreamverse/dreamverse/benchmarks/`: prompt-provider latency/token
|
||||
benchmarking scripts.
|
||||
|
||||
Planned paths during the OSS reorg:
|
||||
|
||||
- `controller/`: local control plane for provider credentials, compute
|
||||
lifecycle, and proxying.
|
||||
- `runtime/`: eventual rename of `apps/dreamverse/dreamverse/` once the
|
||||
controller/runtime split is stable.
|
||||
- `providers/`: provider adapters for local, Runpod, and Modal.
|
||||
|
||||
Important rule:
|
||||
|
||||
- Until the split lands, treat `apps/dreamverse/dreamverse/` as the
|
||||
authoritative runtime and keep provider orchestration out of
|
||||
`apps/dreamverse/web`.
|
||||
|
||||
## System split
|
||||
|
||||
Dreamverse is moving toward a three-part local-first architecture.
|
||||
|
||||
- Frontend owns local UI state, drafts, inspection tools, and user-triggered
|
||||
actions.
|
||||
- Controller will own local-only credentials, compute provisioning, runtime
|
||||
lifecycle, and HTTP/websocket proxying.
|
||||
- Runtime owns generation sessions, prompt rewrite, prompt safety, and
|
||||
websocket semantics.
|
||||
|
||||
The browser should only talk to the local Dreamverse process, never directly to
|
||||
Modal or Runpod.
|
||||
|
||||
## Current runtime responsibilities
|
||||
|
||||
The runtime in `apps/dreamverse/dreamverse/` is responsible for:
|
||||
|
||||
- websocket session lifecycle on `/ws`
|
||||
- queueing, GPU assignment, worker startup, and stream chunk emission
|
||||
- seed prompt memory and the active prompt window used for generation
|
||||
- prompt enhancement and prompt rewrite execution
|
||||
- prompt safety checks
|
||||
- persistence and reload of prompt system prompt files
|
||||
- curated preset append/read routes in devtools mode
|
||||
- health and readiness endpoints
|
||||
|
||||
Relevant files:
|
||||
|
||||
- `apps/dreamverse/dreamverse/main.py`: websocket protocol, session state
|
||||
machine, REST routes
|
||||
- `apps/dreamverse/dreamverse/gpu_pool.py`: FastVideo-backed generation
|
||||
workers
|
||||
- `apps/dreamverse/dreamverse/prompt_enhancer.py`: provider clients, prompt
|
||||
enhancement, rewrite execution
|
||||
- `apps/dreamverse/dreamverse/rewrite_prompt_payload.py`: canonical rewrite
|
||||
request body format
|
||||
- `apps/dreamverse/dreamverse/config.py`: prompt file paths, provider
|
||||
configuration, runtime flags
|
||||
|
||||
## Frontend responsibilities
|
||||
|
||||
The frontend in `apps/dreamverse/web/` is responsible for:
|
||||
|
||||
- collecting user input and deciding whether to send raw prompts or rewrite
|
||||
requests
|
||||
- maintaining client-side stores for session, prompt-window, stream, rewrite,
|
||||
and UI state
|
||||
- rendering prompt history, playback state, devtools controls, and rewrite
|
||||
inspection
|
||||
- building the prompt-window snapshot sent with rewrite requests
|
||||
- reducing websocket events into UI state
|
||||
- showing compute status and controller-driven errors once the controller lands
|
||||
|
||||
Relevant files:
|
||||
|
||||
- `apps/dreamverse/web/src/app/page.tsx`: main orchestration, websocket
|
||||
connect/send paths
|
||||
- `apps/dreamverse/web/src/lib/ws/reducer.ts`: applies normalized websocket
|
||||
events to stores
|
||||
- `apps/dreamverse/web/src/stores/promptWindow.ts`: prompt window and
|
||||
preset/editor state
|
||||
- `apps/dreamverse/web/src/stores/rewrite.ts`: rewrite activity timeline and
|
||||
flags
|
||||
- `apps/dreamverse/web/src/lib/prompts/promptWindowSnapshot.ts`: rewrite snapshot
|
||||
normalization and padding
|
||||
|
||||
## Planned controller responsibilities
|
||||
|
||||
The future local controller should own:
|
||||
|
||||
- local-only provider credential loading and storage
|
||||
- provider selection
|
||||
- runtime provisioning, reuse, shutdown, and health checks
|
||||
- proxying frontend HTTP and websocket traffic to the active runtime
|
||||
- surfacing provisioning, ready, failed, and idle states to the frontend
|
||||
- durable local settings that should survive ephemeral remote runtimes
|
||||
|
||||
The controller should not own:
|
||||
|
||||
- prompt rewrite logic
|
||||
- seed prompt memory
|
||||
- generation queue semantics
|
||||
- websocket event schemas
|
||||
|
||||
## Prompt rewrite contract
|
||||
|
||||
Prompt rewrite is a shared flow with a strict ownership split.
|
||||
|
||||
Frontend responsibilities:
|
||||
|
||||
- decide when a user action should trigger `rewrite_seed_prompts` instead of
|
||||
`append_prompt`
|
||||
- send `rewrite_instruction` and a snapshot of the current prompt window
|
||||
- pad the rewrite snapshot to the runtime-expected segment count using
|
||||
`buildRewritePromptWindowSnapshotFromPrompts(...)`
|
||||
- show rewrite activity and raw LLM output in local inspection UI
|
||||
|
||||
Runtime responsibilities:
|
||||
|
||||
- validate and normalize `prompt_window_prompts`
|
||||
- choose the rewrite system prompt and provider/model/temperature
|
||||
- build the canonical LLM request body in
|
||||
`apps/dreamverse/dreamverse/rewrite_prompt_payload.py`
|
||||
- run the rewrite through `PromptEnhancer.rewrite_prompt_sequence(...)`
|
||||
- apply safety filtering to rewritten prompts
|
||||
- replace the authoritative seed prompt memory when rewrite succeeds
|
||||
- emit `seed_prompts_updated` and `rewrite_seed_prompts_complete`
|
||||
|
||||
Controller responsibilities:
|
||||
|
||||
- proxy the request and response
|
||||
- surface runtime availability and provider lifecycle failures
|
||||
|
||||
Important rule:
|
||||
|
||||
- The frontend may suggest the prompt window to rewrite, but the runtime owns
|
||||
the actual rewritten rollout and the authoritative prompt window after
|
||||
acceptance.
|
||||
|
||||
## Rewrite modes
|
||||
|
||||
There are two runtime rewrite modes:
|
||||
|
||||
- edit existing rollout: when `prompt_window_prompts` is non-empty, rewrite the
|
||||
current rollout while preserving segment count and ordering
|
||||
- new rollout: when the prompt window is empty but there is a
|
||||
`rewrite_instruction`, generate a fresh rollout
|
||||
|
||||
The frontend should not emulate runtime rewrite behavior locally. It should
|
||||
prepare the snapshot, send it, and display the result.
|
||||
|
||||
## Prompt window ownership
|
||||
|
||||
- Frontend owns editable drafts, selected preset UI, and prompt-window
|
||||
inspection state.
|
||||
- Runtime owns the active seed prompt memory used for actual generation.
|
||||
- After any runtime event with reason `rewrite`, the frontend must replace its
|
||||
prompt window from the server payload instead of keeping a locally-derived
|
||||
version.
|
||||
|
||||
## Devtools and persistence ownership
|
||||
|
||||
Prompt config editing is runtime-owned persistence with frontend-owned forms
|
||||
today.
|
||||
|
||||
- Frontend loads and edits drafts through `/prompt-system-config`.
|
||||
- Runtime reads and writes prompt files and reloads runtime prompt config.
|
||||
|
||||
Curated presets follow the same pattern:
|
||||
|
||||
- frontend submits append requests and may update local UI optimistically from
|
||||
the response
|
||||
- runtime persists the JSON file and resolves overlay vs fallback file paths
|
||||
|
||||
During the controller reorg, avoid moving durable user settings into ephemeral
|
||||
remote runtimes. Controller-owned local persistence is preferred for anything
|
||||
that must survive provider restarts.
|
||||
|
||||
## Editing guidance
|
||||
|
||||
- Do not move rewrite logic into the frontend or controller.
|
||||
- Do not make the frontend the source of truth for the generated prompt window
|
||||
after rewrite.
|
||||
- If you change websocket message types or payload fields in
|
||||
`apps/dreamverse/dreamverse/main.py`, update the reducer in
|
||||
`apps/dreamverse/web/src/lib/ws/reducer.ts` in the same change.
|
||||
- If you change rewrite request shape, update both
|
||||
`apps/dreamverse/web/src/lib/prompts/promptWindowSnapshot.ts` and
|
||||
`apps/dreamverse/dreamverse/rewrite_prompt_payload.py`.
|
||||
- If you add controller-managed status or error payloads, keep them separate
|
||||
from runtime websocket events unless there is a strong reason to merge them.
|
||||
- If you change prompt file paths or devtools persistence, update
|
||||
`apps/dreamverse/dreamverse/`, the frontend devtools UI, and any
|
||||
controller-owned local persistence logic together.
|
||||
- Keep provider adapters focused on runtime lifecycle and reachability, not on
|
||||
prompt or session semantics.
|
||||
@@ -0,0 +1,349 @@
|
||||
# Dreamverse
|
||||
|
||||
Dreamverse is the FastVideo realtime video generation & editing platform. It lives in this monorepo under `apps/dreamverse/`.
|
||||
|
||||
**Deploy on:** [local GPU](#quick-start-local-gpu) · [self-hosted B200 (SSH)](#server-b200-deployment-ssh) · [Docker](docker/README.md) · [Modal](scripts/modal/README.md)
|
||||
|
||||
## Install Dreamverse
|
||||
|
||||
You can install Dreamverse using one of the methods below.
|
||||
|
||||
### Method 1: With uv pip
|
||||
|
||||
```bash
|
||||
pip install --upgrade pip
|
||||
pip install uv
|
||||
uv venv .venv --python 3.12
|
||||
source .venv/bin/activate
|
||||
UV_TORCH_BACKEND=cu126 uv pip install "fastvideo[dreamverse]"
|
||||
```
|
||||
|
||||
### Method 2: From source
|
||||
|
||||
```bash
|
||||
git clone https://github.com/hao-ai-lab/FastVideo.git
|
||||
cd FastVideo
|
||||
|
||||
pip install --upgrade pip
|
||||
pip install uv
|
||||
uv venv .venv --python 3.12
|
||||
source .venv/bin/activate
|
||||
UV_TORCH_BACKEND=cu126 uv pip install -e ".[dreamverse]"
|
||||
```
|
||||
|
||||
Use `UV_TORCH_BACKEND=cu130` instead on CUDA 13.
|
||||
|
||||
### Method 3: Using Docker
|
||||
|
||||
```bash
|
||||
git clone https://github.com/hao-ai-lab/FastVideo.git
|
||||
cd FastVideo
|
||||
|
||||
apps/dreamverse/docker/docker_build.sh
|
||||
```
|
||||
|
||||
See `apps/dreamverse/docker/README.md` for Docker build and run option details.
|
||||
|
||||
## Optional: Building FFmpeg For Better Performance
|
||||
|
||||
For full streaming performance in a non-Docker install, build a custom FFmpeg
|
||||
binary from a FastVideo source checkout. The command below is repo-relative,
|
||||
so run it from the repository root:
|
||||
|
||||
```bash
|
||||
bash apps/dreamverse/scripts/install_native_ffmpeg.sh
|
||||
```
|
||||
|
||||
The installer supports Linux `x86_64` and `aarch64`. It prefers conda-forge
|
||||
triplet compilers when those commands are on `PATH`, otherwise it falls back to
|
||||
system `gcc`/`g++` (plain venv). On `x86_64`, x264's hand-tuned SIMD also
|
||||
requires `nasm`; install via whichever path fits your host:
|
||||
|
||||
```bash
|
||||
sudo apt install nasm # Debian/Ubuntu
|
||||
conda install -c conda-forge nasm # inside an active conda env
|
||||
```
|
||||
|
||||
No sudo and no conda? Build `nasm` from source (~30s, installs into `$HOME`):
|
||||
|
||||
```bash
|
||||
(
|
||||
mkdir -p "$HOME/src" "$HOME/opt" && cd "$HOME/src"
|
||||
curl -fsSL -O https://www.nasm.us/pub/nasm/releasebuilds/2.16.03/nasm-2.16.03.tar.gz
|
||||
tar -xf nasm-2.16.03.tar.gz && cd nasm-2.16.03
|
||||
./configure --prefix="$HOME/opt/nasm" && make -j"$(nproc)" && make install
|
||||
)
|
||||
export PATH="$HOME/opt/nasm/bin:$PATH" # add to ~/.bashrc to persist
|
||||
```
|
||||
|
||||
The installer writes to `~/opt/ffmpeg-native/` and emits
|
||||
`apps/dreamverse/scripts/ffmpeg-env.sh`. Source it before starting the backend
|
||||
so Dreamverse uses the custom FFmpeg binary:
|
||||
|
||||
```bash
|
||||
source apps/dreamverse/scripts/ffmpeg-env.sh
|
||||
dreamverse-server
|
||||
```
|
||||
|
||||
Docker images already run this FFmpeg build during image creation and source the
|
||||
generated environment file at container startup.
|
||||
|
||||
## Launch Dreamverse
|
||||
|
||||
Start the backend with the installed Dreamverse commands:
|
||||
|
||||
```bash
|
||||
dreamverse-server --port 8009
|
||||
dreamverse-mock-server --port 8009
|
||||
```
|
||||
|
||||
### Run Dreamverse with FastH3
|
||||
|
||||
Select the VSA data-free FastH3 Preview profile when you start the backend:
|
||||
|
||||
```bash
|
||||
DREAMVERSE_MODEL_ID=fast-h3 dreamverse-server --port 8009
|
||||
```
|
||||
|
||||
The `fast-h3` profile uses four visible GPUs by default. It loads the `MiniMaxAI/MiniMax-H3` base checkpoint and the
|
||||
`vsa-datafree/adapter_model.safetensors` adapter from
|
||||
`FastVideo/FastVideo-FastH3-4-step-Preview-v1-LoRA`. Each request generates a 124-frame, 768×1344 video with
|
||||
synchronized audio and five sigma-grid points. Dreamverse uses the last frame of each segment as first-frame
|
||||
conditioning for the following segment.
|
||||
|
||||
Set `CUDA_VISIBLE_DEVICES` when you need to choose the four physical GPUs:
|
||||
|
||||
```bash
|
||||
CUDA_VISIBLE_DEVICES=0,1,2,3 DREAMVERSE_MODEL_ID=fast-h3 dreamverse-server --port 8009
|
||||
```
|
||||
|
||||
> **Expect a slow first boot.** With `torch.compile` and startup warmup enabled
|
||||
> (the default), the backend compiles the segment 1 and segment 2 inference
|
||||
> paths before it reports ready — this can take **tens of minutes on a cold
|
||||
> cache**, regardless of how you deploy (local, server, Docker, or Modal).
|
||||
> `/healthz` responds as soon as the process is up; `/readyz` stays `503` until
|
||||
> warmup finishes. To defer compilation until the first generated request while
|
||||
> testing, set `FASTVIDEO_ENABLE_STARTUP_WARMUP=0` before starting the backend.
|
||||
|
||||
## Frontend Setup
|
||||
|
||||
Install the web dependencies once from the FastVideo checkout:
|
||||
|
||||
```bash
|
||||
cd apps/dreamverse/web
|
||||
npm ci
|
||||
```
|
||||
|
||||
The frontend package uses `package-lock.json`; use npm for installs and scripts.
|
||||
|
||||
## Quick Start: Local GPU
|
||||
|
||||
### Start Backend
|
||||
|
||||
Export the API keys used for prompt rewrite and prompt enhancement:
|
||||
|
||||
```bash
|
||||
export CEREBRAS_API_KEY=...
|
||||
export GROQ_API_KEY=...
|
||||
```
|
||||
|
||||
If you built the optional native FFmpeg binary above, source its environment
|
||||
file in the same shell before starting the backend:
|
||||
|
||||
```bash
|
||||
source apps/dreamverse/scripts/ffmpeg-env.sh
|
||||
dreamverse-server --host 0.0.0.0 --port 8009
|
||||
```
|
||||
|
||||
The Dreamverse backend defaults to `0.0.0.0:8009` and starts one GPU worker on
|
||||
the first visible GPU by default.
|
||||
|
||||
### Cosmos Predict2.5 DFD continuation (experimental)
|
||||
|
||||
Dreamverse can combine two converted Cosmos Predict2.5 2B packages: the
|
||||
distilled Text2World student creates an unconditioned first segment, then the
|
||||
Data-Forcing Distillation (DFD) Video2World student conditions each later
|
||||
segment on the prior terminal frame. Point the runtime at both local converted
|
||||
packages:
|
||||
|
||||
```bash
|
||||
export DREAMVERSE_MODEL_ID=cosmos25-dfd
|
||||
export DREAMVERSE_MODEL_PATH=/path/to/Cosmos-Predict2.5-2B-Distilled-TrigFlow-FastVideo
|
||||
export DREAMVERSE_COSMOS25_DFD_MODEL_PATH=/path/to/Cosmos-Predict2.5-2B-DFD-FastVideo
|
||||
export ENABLE_TORCH_COMPILE=0
|
||||
dreamverse-server --host 0.0.0.0 --port 8009
|
||||
```
|
||||
|
||||
The backend loads and warms both model roles before reporting ready. Both use
|
||||
BF16, Torch SDPA, 704x1280 output, 24 FPS, and four steps. Bootstrap segments
|
||||
contain 77 frames. DFD segments contain 81 decoded frames, but Dreamverse drops
|
||||
the repeated conditioning frame before streaming, leaving 80 new frames. An
|
||||
initial user image selects DFD immediately without treating that first frame as
|
||||
a cross-segment overlap.
|
||||
|
||||
The profile uses a 30-minute session lease because sequential generation on
|
||||
GB10-class hardware can exceed Dreamverse's five-minute default while the GPU
|
||||
is still making progress. Deployments can override the lease with
|
||||
`FASTVIDEO_SESSION_TIMEOUT_SECONDS`.
|
||||
|
||||
Cosmos does not produce audio, so the backend supplies duration-matched silent
|
||||
24 kHz audio for the existing browser streaming contract and trims 1,000 audio
|
||||
samples with each repeated DFD boundary frame. Runtime LoRA changes are not
|
||||
supported. Full segments take roughly 145 seconds on GB10, so this profile is a
|
||||
continuation-quality integration rather than a real-time configuration.
|
||||
|
||||
### Check Readiness
|
||||
|
||||
In another shell, verify that the backend process is alive:
|
||||
|
||||
```bash
|
||||
curl http://localhost:8009/healthz
|
||||
```
|
||||
|
||||
Then wait for GPU workers and startup warmup to finish:
|
||||
|
||||
```bash
|
||||
curl http://localhost:8009/readyz
|
||||
```
|
||||
|
||||
You can also run the same readiness path with:
|
||||
|
||||
```bash
|
||||
BACKEND_HOST=localhost BACKEND_PORT=8009 apps/dreamverse/scripts/smoke_local.sh
|
||||
```
|
||||
|
||||
If a backend is already running and you only want the script to probe it:
|
||||
|
||||
```bash
|
||||
DREAMVERSE_SMOKE_START_BACKEND=0 apps/dreamverse/scripts/smoke_local.sh
|
||||
```
|
||||
|
||||
### Start Frontend
|
||||
|
||||
Start the frontend:
|
||||
|
||||
```bash
|
||||
cd apps/dreamverse/web
|
||||
BACKEND_HOST=localhost BACKEND_PORT=8009 npm run dev
|
||||
```
|
||||
|
||||
Open `http://localhost:5299`.
|
||||
|
||||
## Server B200 deployment (SSH)
|
||||
|
||||
Deploying on a remote GPU host (for example a B200 box) is a local install run
|
||||
over SSH, plus a few server-specific concerns. Two paths:
|
||||
|
||||
### Option A: Native (source install)
|
||||
|
||||
SSH in, then follow [Install → From source](#method-2-from-source) and
|
||||
(recommended) [Building FFmpeg](#optional-building-ffmpeg-for-better-performance),
|
||||
then start the backend as in [Quick Start: Local GPU](#quick-start-local-gpu).
|
||||
For a remote host, a few things differ from localhost:
|
||||
|
||||
- Bind all interfaces: `dreamverse-server --host 0.0.0.0 --port 8009`.
|
||||
- Point the frontend/client at the host: `BACKEND_HOST=<b200-host> BACKEND_PORT=8009 npm run dev`.
|
||||
- Keep the backend alive across SSH sessions (`tmux` / `systemd` / `nohup`).
|
||||
- Expose / firewall port `8009`, or front it with a reverse proxy + auth.
|
||||
|
||||
### Option B: Docker (on the server)
|
||||
|
||||
SSH in, then follow [Install → Using Docker](#method-3-using-docker) and the run
|
||||
steps in [`docker/README.md`](docker/README.md):
|
||||
|
||||
```bash
|
||||
CEREBRAS_API_KEY="<key>" GROQ_API_KEY="<key>" apps/dreamverse/docker/docker_run.sh
|
||||
```
|
||||
|
||||
## Quick Start: Mock Backend (For UI development)
|
||||
|
||||
The mock server emulates the Dreamverse backend protocol and streams a
|
||||
synthetic FFmpeg-generated fMP4 clip, so the frontend can run without a GPU.
|
||||
|
||||
```bash
|
||||
dreamverse-mock-server --latency 200 --port 8009
|
||||
```
|
||||
|
||||
## Tests
|
||||
|
||||
Run the focused backend tests that validate local startup wiring, config, GPU
|
||||
selection, and mock-server behavior:
|
||||
|
||||
```bash
|
||||
pytest apps/dreamverse/dreamverse/tests/test_config.py \
|
||||
apps/dreamverse/dreamverse/tests/test_entrypoints.py \
|
||||
apps/dreamverse/dreamverse/tests/test_gpu_pool.py \
|
||||
apps/dreamverse/dreamverse/tests/test_minimax_h3_generation.py \
|
||||
apps/dreamverse/dreamverse/tests/test_mock_server.py -q
|
||||
```
|
||||
|
||||
Run the broader Dreamverse backend suite:
|
||||
|
||||
```bash
|
||||
pytest apps/dreamverse/dreamverse/tests -q
|
||||
```
|
||||
|
||||
Run the frontend tests:
|
||||
|
||||
```bash
|
||||
cd apps/dreamverse/web
|
||||
npm test
|
||||
```
|
||||
|
||||
Run the frontend e2e tests:
|
||||
|
||||
```bash
|
||||
cd apps/dreamverse/web
|
||||
npm run e2e
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
`dreamverse-server` exits with an install hint
|
||||
|
||||
- install the Dreamverse extra with `UV_TORCH_BACKEND=cu126 uv pip install -e ".[dreamverse]"` from a
|
||||
source checkout, or `UV_TORCH_BACKEND=cu126 uv pip install "fastvideo[dreamverse]"` from PyPI
|
||||
(`cu130` on CUDA 13).
|
||||
|
||||
Prompt-provider environment variable errors
|
||||
|
||||
- set `CEREBRAS_API_KEY`
|
||||
- set `GROQ_API_KEY`
|
||||
- direct `dreamverse-server` launches do not source `~/.env`; export the keys
|
||||
in the shell or use the bundled launch scripts, which source `~/.env`.
|
||||
|
||||
`/readyz` stays at `503`
|
||||
|
||||
- wait for model loading and startup warmup to finish
|
||||
- confirm a compatible CUDA GPU is visible to the process
|
||||
- check backend logs for worker startup or warmup failures
|
||||
- for a startup/debug pass without warmup, set
|
||||
`FASTVIDEO_ENABLE_STARTUP_WARMUP=0` before starting the backend
|
||||
|
||||
Only one GPU is used
|
||||
|
||||
- this is the default local behavior
|
||||
- set `FASTVIDEO_GPU_COUNT=<N>` to start N GPU worker subprocesses inside one
|
||||
backend instance
|
||||
- set `FASTVIDEO_GPU_COUNT=all` to start one worker for every visible GPU
|
||||
- use `CUDA_VISIBLE_DEVICES` first if you need to pin the visible GPU set
|
||||
|
||||
Frontend cannot connect to backend
|
||||
|
||||
- confirm the backend is running on `8009`; if not, point the frontend at it
|
||||
with `BACKEND_HOST=<host> BACKEND_PORT=<port> npm run dev`
|
||||
- confirm `http://localhost:8009/healthz` responds before starting the frontend
|
||||
- confirm `http://localhost:8009/readyz` returns `200` before clicking Generate
|
||||
- use `apps/dreamverse/scripts/smoke_local.sh` for a repeatable local startup
|
||||
check
|
||||
|
||||
Mock backend fails during startup
|
||||
|
||||
- install FFmpeg or set `FASTVIDEO_FFMPEG_BIN` to an FFmpeg binary
|
||||
- for non-mock local GPU streaming performance, use the native FFmpeg installer
|
||||
described above
|
||||
|
||||
## Notes
|
||||
|
||||
Dreamverse owns its backend app under `apps/dreamverse/dreamverse/`. It expects
|
||||
`dreamverse-server`, not `fastvideo serve`.
|
||||
@@ -0,0 +1,365 @@
|
||||
# Dreamverse Architecture
|
||||
|
||||
## Overview
|
||||
|
||||
Dreamverse currently has two main runtime pieces:
|
||||
|
||||
- `apps/dreamverse/web/`: Next.js frontend
|
||||
- `apps/dreamverse/dreamverse/`: Python FastAPI runtime
|
||||
|
||||
Today, the browser talks directly to the Dreamverse runtime over HTTP and a
|
||||
single websocket on `/ws`. The frontend owns UI state and interaction flow. The
|
||||
server owns generation state, prompt rewrite, prompt safety, websocket session
|
||||
semantics, and GPU-backed execution.
|
||||
|
||||
Near-term OSS note:
|
||||
|
||||
- `apps/dreamverse/dreamverse/` is the current runtime implementation.
|
||||
- A future `controller/` layer is planned for local-only compute management and
|
||||
provider orchestration, but it does not exist yet.
|
||||
|
||||
## Repo Map
|
||||
|
||||
### Frontend
|
||||
|
||||
- `apps/dreamverse/web/src/app/page.tsx`: main client orchestration,
|
||||
websocket connect, init payloads, send paths, and top-level app behavior
|
||||
- `apps/dreamverse/web/src/lib/ws/reducer.ts`: reduces normalized websocket
|
||||
events into client stores
|
||||
- `apps/dreamverse/web/src/stores/session.ts`: connection, mode, and top-level
|
||||
session UI
|
||||
- `apps/dreamverse/web/src/stores/promptWindow.ts`: editable prompt window and
|
||||
seed prompt UI state
|
||||
- `apps/dreamverse/web/src/stores/rewrite.ts`: rewrite activity timeline and
|
||||
inspection state
|
||||
- `apps/dreamverse/web/src/stores/stream.ts`: playback and stream-related
|
||||
client state
|
||||
- `apps/dreamverse/web/src/lib/prompts/promptWindowSnapshot.ts`: prompt-window snapshot
|
||||
building for rewrite requests
|
||||
|
||||
### Server
|
||||
|
||||
- `apps/dreamverse/dreamverse/main.py`: websocket endpoint, request handling,
|
||||
session state machine, rewrite orchestration, REST routes, and stream relay
|
||||
- `apps/dreamverse/dreamverse/gpu_pool.py`: GPU worker processes, warmup, model
|
||||
loading, and `generate_video()` calls through FastVideo
|
||||
- `apps/dreamverse/dreamverse/prompt_enhancer.py`: prompt enhancement, rollout
|
||||
rewrite execution, provider selection, and timeout/fallback behavior
|
||||
- `apps/dreamverse/dreamverse/rewrite_prompt_payload.py`: canonical rewrite request payload
|
||||
building
|
||||
- `apps/dreamverse/dreamverse/config.py`: runtime flags, prompt file paths, provider settings, and
|
||||
warmup config
|
||||
- `apps/dreamverse/dreamverse/session_init_image.py`: validates and persists uploaded initial
|
||||
images for segment 1
|
||||
|
||||
## Current Split Of Responsibility
|
||||
|
||||
### Frontend owns
|
||||
|
||||
- local UI state and client-side stores
|
||||
- prompt drafts and prompt window editing
|
||||
- websocket connection management
|
||||
- deciding which user action to send:
|
||||
- `session_init_v2`
|
||||
- `project_init_v1`
|
||||
- `append_prompt`
|
||||
- `rewrite_seed_prompts`
|
||||
- `simple_generate`
|
||||
- showing rewrite progress, stream status, prompt history, and devtools views
|
||||
|
||||
### Server owns
|
||||
|
||||
- websocket session lifecycle and protocol
|
||||
- GPU assignment and worker lifecycle
|
||||
- the authoritative seed prompt memory used for generation
|
||||
- prompt rewrite execution and prompt safety
|
||||
- actual generation queue semantics
|
||||
- stream chunk emission and segment lifecycle events
|
||||
- prompt config and preset persistence routes
|
||||
- health and readiness endpoints
|
||||
|
||||
Important rule:
|
||||
|
||||
- The frontend may propose prompt-window state for rewrite, but the server is
|
||||
the source of truth for the rewritten rollout and the active prompt memory
|
||||
used for generation.
|
||||
|
||||
## End-To-End Flow
|
||||
|
||||
1. The frontend opens `/ws`.
|
||||
2. The frontend sends `session_init_v2` with the initial prompt-window state,
|
||||
preset metadata, and current toggles.
|
||||
3. The server validates init data, persists an optional initial image, acquires
|
||||
a GPU slot, and emits session status such as `gpu_assigned`.
|
||||
4. The server starts or resumes project generation and emits events like
|
||||
`ltx2_stream_start`, `ltx2_segment_start`, media init/chunks, and completion
|
||||
events.
|
||||
5. The frontend reduces those websocket events into its stores and updates the
|
||||
UI.
|
||||
6. User actions such as appending prompts, rewriting seed prompts, or starting
|
||||
a single custom clip go back to the server over the same websocket.
|
||||
|
||||
## Frontend Architecture
|
||||
|
||||
The frontend is store-driven.
|
||||
|
||||
- `page.tsx` wires together websocket setup, send helpers, reducer
|
||||
application, and top-level interaction flows.
|
||||
- Store modules separate concerns like session state, rewrite state, prompt
|
||||
window state, and stream state.
|
||||
- The websocket reducer is responsible for turning normalized runtime events
|
||||
into store updates. If the server event schema changes, the reducer must
|
||||
change with it.
|
||||
|
||||
The frontend is intentionally not responsible for:
|
||||
|
||||
- generating rewritten prompts locally
|
||||
- deciding final prompt safety outcomes
|
||||
- reconstructing server session state from scratch
|
||||
- inventing its own generation semantics independent of the runtime
|
||||
|
||||
## Server Architecture
|
||||
|
||||
The current runtime is a FastAPI app with a single long-lived websocket per
|
||||
session.
|
||||
|
||||
`apps/dreamverse/dreamverse/main.py` manages:
|
||||
|
||||
- websocket connect/init
|
||||
- prompt queues
|
||||
- project and segment state
|
||||
- prompt enhancement/rewrite triggers
|
||||
- stream relay from GPU workers to the browser
|
||||
- session logging and REST endpoints
|
||||
|
||||
`apps/dreamverse/dreamverse/gpu_pool.py` manages:
|
||||
|
||||
- model loading through FastVideo
|
||||
- one or more worker processes
|
||||
- startup warmup
|
||||
- user join/leave commands
|
||||
- `USER_STEP` execution for each segment
|
||||
- generation-command routing and stream-result delivery
|
||||
|
||||
Model generation has a separate ownership boundary inside each GPU process:
|
||||
|
||||
- `apps/dreamverse/dreamverse/generation_worker.py` selects the backend that the active model profile declares and owns
|
||||
the backend lifecycle.
|
||||
- `apps/dreamverse/dreamverse/ltx2_generation.py` owns LTX-2 generator configuration, video and audio continuation, and
|
||||
runtime LoRA application.
|
||||
- `apps/dreamverse/dreamverse/minimax_h3_generation.py` owns the VSA data-free FastH3 adapter, FastH3 generator and
|
||||
request configuration, and last-frame continuation through MiniMax H3 first-frame conditioning.
|
||||
- `apps/dreamverse/dreamverse/generation_contracts.py` defines the decoded media and stream-trimming result that both
|
||||
model backends return to `apps/dreamverse/dreamverse/gpu_pool.py`.
|
||||
|
||||
`apps/dreamverse/dreamverse/prompt_enhancer.py` manages:
|
||||
|
||||
- prompt enhancement for user-submitted prompts
|
||||
- rollout rewrite requests for the prompt window
|
||||
- provider selection and fallback across configured prompt providers
|
||||
- response normalization and safety-aware failure handling
|
||||
|
||||
## FastAPI Surface
|
||||
|
||||
The server is a single FastAPI application created in
|
||||
`apps/dreamverse/dreamverse/main.py`.
|
||||
|
||||
Current built-in FastAPI docs are enabled:
|
||||
|
||||
- `/docs`: Swagger UI
|
||||
- `/redoc`: ReDoc
|
||||
- `/openapi.json`: OpenAPI schema
|
||||
|
||||
The runtime also mounts the frontend static build at `/` when one of the
|
||||
configured frontend static directories exists. It does not expose the backend
|
||||
Python package as static content.
|
||||
|
||||
## HTTP API
|
||||
|
||||
The current HTTP API is small. Most realtime behavior still goes through the
|
||||
websocket.
|
||||
|
||||
### Core health and status routes
|
||||
|
||||
- `GET /healthz`
|
||||
- process liveness probe
|
||||
- returns a small payload with `status`, `service`, and timestamp
|
||||
- `GET /readyz`
|
||||
- readiness probe
|
||||
- returns `503` until prompt services are initialized and at least one GPU
|
||||
worker is ready
|
||||
- returns readiness and GPU pool summary fields such as ready workers, total
|
||||
GPUs, warmup counts, and queue size
|
||||
- `GET /status`
|
||||
- returns the current GPU pool status payload from `gpu_pool`
|
||||
- `GET /internal/monitor/sessions`
|
||||
- internal monitoring payload for session dashboards
|
||||
- includes pending session count, max available sessions, prompt provider
|
||||
success counts, and timestamp
|
||||
|
||||
### Prompt config routes
|
||||
|
||||
- `GET /prompt-system-config`
|
||||
- returns the editable prompt-system configuration currently loaded by
|
||||
`PromptEnhancer`
|
||||
- `POST /prompt-system-config`
|
||||
- saves prompt-system configuration to disk and reloads prompt config in the
|
||||
runtime
|
||||
- current editable fields include:
|
||||
- next-segment system prompt
|
||||
- auto-extension system prompt
|
||||
- rewrite-window system prompt
|
||||
- rewrite-user system prompt
|
||||
- rewrite model
|
||||
- rewrite temperature
|
||||
|
||||
### Devtools-only preset routes
|
||||
|
||||
These exist only when `DEVTOOLS_ENABLED` is true in
|
||||
`apps/dreamverse/dreamverse/config.py`.
|
||||
|
||||
- `GET /curated-presets`
|
||||
- returns merged curated presets, applying the local overlay file on top of
|
||||
the fallback file when both exist
|
||||
- `POST /curated-presets/append`
|
||||
- appends a new curated preset to the overlay presets file
|
||||
- validates non-empty label, normalized id, and at least two non-empty
|
||||
segment prompts
|
||||
|
||||
## Websocket API
|
||||
|
||||
`WS /ws` is the main runtime API.
|
||||
|
||||
The websocket owns:
|
||||
|
||||
- session init
|
||||
- project init and reset
|
||||
- prompt append
|
||||
- prompt rewrite
|
||||
- generation toggles
|
||||
- segment lifecycle events
|
||||
- media stream delivery
|
||||
- runtime error delivery
|
||||
|
||||
The websocket is the authoritative API for realtime Dreamverse behavior. The
|
||||
HTTP routes mainly support health checks, devtools persistence, and monitoring.
|
||||
|
||||
## Prompt Rewrite Architecture
|
||||
|
||||
Prompt rewrite is a shared flow with strict ownership boundaries.
|
||||
|
||||
### Frontend responsibilities
|
||||
|
||||
- collect the rewrite instruction
|
||||
- build the prompt-window snapshot from current client state
|
||||
- send `rewrite_seed_prompts`
|
||||
- show rewrite progress, raw output, fallback state, and resulting prompt list
|
||||
|
||||
### Server responsibilities
|
||||
|
||||
- validate and normalize the prompt-window payload
|
||||
- choose rewrite model, system prompt, timeout, and temperature
|
||||
- build the canonical prompt payload in
|
||||
`apps/dreamverse/dreamverse/rewrite_prompt_payload.py`
|
||||
- execute rewrite through `PromptEnhancer`
|
||||
- apply safety filtering to rewritten prompts
|
||||
- replace the authoritative seed prompt memory when rewrite succeeds
|
||||
- emit `seed_prompts_updated` and `rewrite_seed_prompts_complete`
|
||||
|
||||
Important rule:
|
||||
|
||||
- The frontend owns editable drafts.
|
||||
- The server owns the accepted rollout.
|
||||
|
||||
After a successful rewrite, the frontend should replace its prompt-window view
|
||||
from the server payload instead of preserving a locally-derived version.
|
||||
|
||||
## Prompt Modes
|
||||
|
||||
There are three related prompt paths in the current system:
|
||||
|
||||
### Initial rollout
|
||||
|
||||
- The frontend sends seed prompts during `session_init_v2`.
|
||||
- The server uses those prompts as the initial seed prompt memory.
|
||||
- If the rollout starts from an empty prompt window plus an initial rewrite
|
||||
instruction, the server can pause generation until rewrite completes.
|
||||
|
||||
### Live append
|
||||
|
||||
- The frontend sends `append_prompt`.
|
||||
- The server may enhance that prompt, safety-check it, enqueue it, and use it
|
||||
as the next generated segment.
|
||||
|
||||
### Rewrite
|
||||
|
||||
- The frontend sends `rewrite_seed_prompts`.
|
||||
- The server rewrites the entire seed prompt window or generates a new rollout,
|
||||
depending on the payload and current state.
|
||||
|
||||
## Initial Image And Segment Handling
|
||||
|
||||
The frontend currently sends `initial_image` as part of session init or
|
||||
`simple_generate`.
|
||||
|
||||
The server:
|
||||
|
||||
- validates and persists the image
|
||||
- uses it only for segment 1 when present
|
||||
- keeps continuation state for later segments in the GPU worker
|
||||
|
||||
This means the runtime, not the frontend, decides how segment 1 image
|
||||
conditioning and later continuation conditioning are applied.
|
||||
|
||||
## Websocket Contract
|
||||
|
||||
The websocket is the main integration surface between UI and runtime.
|
||||
|
||||
Typical incoming messages from the frontend:
|
||||
|
||||
- `session_init_v2`
|
||||
- `project_init_v1`
|
||||
- `append_prompt`
|
||||
- `rewrite_seed_prompts`
|
||||
- `simple_generate`
|
||||
- `set_enhancement`
|
||||
- `set_auto_extension`
|
||||
- `set_loop_generation`
|
||||
|
||||
Typical outgoing messages from the server:
|
||||
|
||||
- `gpu_assigned`
|
||||
- `ltx2_stream_start`
|
||||
- `ltx2_segment_start`
|
||||
- `segment_prompt_source`
|
||||
- `prompt_received`
|
||||
- `prompt_ready`
|
||||
- `prompt_enhancing`
|
||||
- `seed_prompts_updated`
|
||||
- `rewrite_seed_prompts_complete`
|
||||
- `media_init`
|
||||
- `media_segment_complete`
|
||||
- `project_idle`
|
||||
- `error`
|
||||
|
||||
Binary websocket frames carry media chunks for playback.
|
||||
|
||||
## Current And Planned Architecture
|
||||
|
||||
Current architecture:
|
||||
|
||||
- browser -> `apps/dreamverse/web`
|
||||
- `apps/dreamverse/web` -> `apps/dreamverse/dreamverse/main.py`
|
||||
- `apps/dreamverse/dreamverse/main.py` ->
|
||||
`apps/dreamverse/dreamverse/gpu_pool.py`
|
||||
- `gpu_pool.py` -> FastVideo runtime
|
||||
|
||||
Planned architecture:
|
||||
|
||||
- browser -> `apps/dreamverse/web`
|
||||
- `apps/dreamverse/web` -> local `controller/`
|
||||
- `controller/` -> local or remote Dreamverse runtime
|
||||
- runtime -> FastVideo runtime
|
||||
|
||||
That future controller split should not move prompt rewrite, session state, or
|
||||
generation semantics out of the runtime.
|
||||
@@ -0,0 +1,600 @@
|
||||
# Dreamverse OSS Design
|
||||
|
||||
## Overview
|
||||
|
||||
Dreamverse should ship as a local-first open source application.
|
||||
|
||||
- The browser talks only to a local Dreamverse control plane on the user's
|
||||
machine.
|
||||
- Provider credentials stay local to that machine.
|
||||
- Dreamverse may provision compute on the user's behalf, but Dreamverse does
|
||||
not host that control path as a service.
|
||||
|
||||
This keeps the UX simple without turning Dreamverse into a credential-holding
|
||||
hosted platform.
|
||||
|
||||
## Goals
|
||||
|
||||
- Support three compute modes behind one product surface:
|
||||
- local GPU
|
||||
- managed remote GPU via Runpod
|
||||
- managed remote GPU via Modal
|
||||
- Keep prompt rewrite, websocket session state, and generation behavior
|
||||
consistent across providers.
|
||||
- Keep provider API keys out of browser state and out of any hosted service.
|
||||
- Make the existing runtime reusable as the common serving contract.
|
||||
- Minimize provider-specific code and isolate it behind a narrow interface.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Do not make the frontend call provider APIs directly.
|
||||
- Do not unify providers at the level of SSH, VM, serverless, or pod
|
||||
semantics.
|
||||
- Do not move prompt rewrite logic into the frontend or controller.
|
||||
- Do not require remote compute for the basic product path.
|
||||
|
||||
## Current State
|
||||
|
||||
Today the repo contains two major pieces:
|
||||
|
||||
- `apps/dreamverse/web/`: Next.js frontend
|
||||
- `apps/dreamverse/dreamverse/`: FastAPI runtime that owns websocket state,
|
||||
prompt rewrite, prompt safety, and GPU-backed generation
|
||||
|
||||
The current runtime already exposes useful health and streaming surfaces such
|
||||
as `/healthz`, `/readyz`, `/status`, and `/ws`.
|
||||
|
||||
## Target Architecture
|
||||
|
||||
The target open source structure should be:
|
||||
|
||||
```text
|
||||
Dreamverse/
|
||||
├── apps/dreamverse/
|
||||
│ ├── web/ # browser UI
|
||||
│ ├── dreamverse/ # current FastAPI websocket/generation runtime
|
||||
│ ├── controller/ # local control plane and provider lifecycle
|
||||
│ ├── providers/ # provider adapters
|
||||
│ ├── tests/
|
||||
│ │ ├── contract/
|
||||
│ │ ├── controller/
|
||||
│ │ └── smoke/
|
||||
│ └── design.md
|
||||
└── ...
|
||||
```
|
||||
|
||||
Near-term note:
|
||||
|
||||
- `apps/dreamverse/dreamverse/` is the current runtime implementation.
|
||||
- We can keep the code there initially and rename it to `runtime/` only after
|
||||
the controller lands.
|
||||
|
||||
## Trust Model
|
||||
|
||||
Dreamverse is local-only for control and secrets.
|
||||
|
||||
- The user launches Dreamverse on their own machine.
|
||||
- Provider API keys are entered into the local app or local CLI.
|
||||
- The controller uses those credentials to provision or connect to compute.
|
||||
- The browser never talks to Modal or Runpod directly.
|
||||
- Dreamverse-hosted infrastructure is not involved.
|
||||
|
||||
This is the key reason the provider-based path is acceptable for OSS.
|
||||
|
||||
## Responsibility Split
|
||||
|
||||
### `apps/dreamverse/web`
|
||||
|
||||
The frontend should own:
|
||||
|
||||
- UI state, drafts, and local interaction state
|
||||
- websocket event reduction into client stores
|
||||
- selection of compute mode and display of cost/health/status
|
||||
- local forms for provider configuration
|
||||
- sending prompt requests and rewrite requests to the local controller
|
||||
|
||||
The frontend should not own:
|
||||
|
||||
- provider credentials after submission
|
||||
- provider API calls
|
||||
- runtime lifecycle
|
||||
- authoritative prompt window after rewrite
|
||||
- prompt safety or generation policy
|
||||
|
||||
### `controller`
|
||||
|
||||
The local controller should own:
|
||||
|
||||
- provider credential loading and local-only storage
|
||||
- compute mode selection
|
||||
- provisioning, reuse, shutdown, and health monitoring of runtimes
|
||||
- reverse proxying HTTP and websocket traffic from the frontend to the active
|
||||
runtime
|
||||
- user-visible status such as provisioning, ready, failed, and idle shutdown
|
||||
- local persistence for user settings that must survive ephemeral runtimes
|
||||
|
||||
The controller should not own:
|
||||
|
||||
- prompt rewrite logic
|
||||
- seed prompt memory semantics
|
||||
- generation queue behavior
|
||||
- provider-specific UI state
|
||||
|
||||
### `runtime`
|
||||
|
||||
The runtime should remain the authoritative owner of:
|
||||
|
||||
- `/ws` session state
|
||||
- prompt rewrite execution
|
||||
- prompt safety
|
||||
- seed prompt memory and prompt-window state used for generation
|
||||
- generation orchestration and GPU worker lifecycle
|
||||
- websocket event schemas
|
||||
|
||||
This preserves the current model and avoids splitting state across layers.
|
||||
|
||||
## Runtime Contract
|
||||
|
||||
Provider abstraction should happen around a stable Dreamverse runtime contract,
|
||||
not around infrastructure details.
|
||||
|
||||
Minimum runtime surface:
|
||||
|
||||
- `GET /healthz`
|
||||
- `GET /readyz`
|
||||
- `GET /status`
|
||||
- `GET/POST /prompt-system-config` if devtools persists config through the
|
||||
runtime
|
||||
- curated preset routes if those remain runtime-backed
|
||||
- `WS /ws`
|
||||
|
||||
Important rule:
|
||||
|
||||
- The controller only needs to know how to reach a healthy runtime.
|
||||
- The runtime remains provider-agnostic.
|
||||
|
||||
## Provider Abstraction
|
||||
|
||||
Use a narrow provider interface:
|
||||
|
||||
```python
|
||||
class ComputeProvider(Protocol):
|
||||
async def ensure_runtime(self, spec: RuntimeSpec) -> RuntimeHandle: ...
|
||||
async def wait_until_ready(self, handle: RuntimeHandle) -> None: ...
|
||||
async def stop_runtime(self, handle: RuntimeHandle) -> None: ...
|
||||
```
|
||||
|
||||
`RuntimeHandle` should include:
|
||||
|
||||
- `provider`
|
||||
- `runtime_id`
|
||||
- `base_url`
|
||||
- `ws_url`
|
||||
- runtime auth headers or tokens if needed
|
||||
- lifecycle metadata
|
||||
- cost or hardware metadata for UI display
|
||||
|
||||
The controller should work only with `RuntimeHandle`, never with raw SSH hosts
|
||||
or provider-specific payloads after resolution.
|
||||
|
||||
## Provider Notes
|
||||
|
||||
### Local
|
||||
|
||||
Local mode should be the reference implementation.
|
||||
|
||||
- Start the runtime as a local subprocess or connect to an already-running
|
||||
local runtime URL.
|
||||
- Reuse the same runtime contract as remote providers.
|
||||
- Make this the first supported path and the main smoke-test target.
|
||||
|
||||
### Runpod
|
||||
|
||||
Runpod should be treated as pod lifecycle plus runtime reachability.
|
||||
|
||||
- Prefer prepared images or templates that auto-start the Dreamverse runtime.
|
||||
- Prefer exposed HTTP/TCP ports for steady-state traffic.
|
||||
- Use SSH only for bootstrap fallback, diagnostics, or repair.
|
||||
- Avoid a design where the controller shells into the pod for every action.
|
||||
|
||||
### Modal
|
||||
|
||||
Modal should be treated as deployment-based runtime hosting.
|
||||
|
||||
- Wrap the Dreamverse runtime in a thin Modal entrypoint if needed.
|
||||
- Reuse the same runtime behavior behind that wrapper.
|
||||
- Do not model Modal as a machine that Dreamverse logs into.
|
||||
- Do not force the websocket runtime into a per-request serverless handler
|
||||
shape.
|
||||
|
||||
## Config and Persistence
|
||||
|
||||
Remote compute may be ephemeral, so mutable user configuration should not live
|
||||
only inside remote runtimes.
|
||||
|
||||
Keep durable state local to the user's machine unless there is a strong reason
|
||||
otherwise:
|
||||
|
||||
- provider selection
|
||||
- provider credentials or credential references
|
||||
- default hardware preferences
|
||||
- editable prompt presets
|
||||
- prompt system prompt overrides
|
||||
- idle shutdown policy
|
||||
|
||||
Runtime-local state should be treated as disposable unless explicitly synced.
|
||||
|
||||
## Prompt Rewrite Ownership
|
||||
|
||||
Prompt rewrite remains runtime-owned even after the controller is added.
|
||||
|
||||
Frontend responsibilities:
|
||||
|
||||
- collect the rewrite instruction
|
||||
- build the prompt-window snapshot
|
||||
- display rewrite activity and results
|
||||
|
||||
Runtime responsibilities:
|
||||
|
||||
- validate and normalize the prompt window
|
||||
- choose the rewrite system prompt and model settings
|
||||
- execute rewrite
|
||||
- apply safety filtering
|
||||
- replace authoritative seed prompt memory
|
||||
- emit the canonical completion events
|
||||
|
||||
Controller responsibilities:
|
||||
|
||||
- proxy the request and response
|
||||
- surface runtime availability and failure state
|
||||
|
||||
This boundary should not move.
|
||||
|
||||
## Recommended Rollout
|
||||
|
||||
1. Finish the path reorg so docs and code agree on `apps/dreamverse/web`.
|
||||
2. Introduce `controller/` as a local-only API/proxy process.
|
||||
3. Keep `apps/dreamverse/dreamverse/` as the runtime and adapt it behind the
|
||||
controller.
|
||||
4. Add `local` provider first.
|
||||
5. Add "bring your own runtime URL" as an escape hatch.
|
||||
6. Add automated Runpod provisioning.
|
||||
7. Add Modal deployment support.
|
||||
8. Rename `apps/dreamverse/dreamverse/` to `runtime/` once the split is stable.
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
The implementation should start with the smallest milestone that gives users a
|
||||
working local GPU setup without forcing the controller/provider architecture
|
||||
into the first patch series.
|
||||
|
||||
### Milestone 0: Make local GPU the official baseline
|
||||
|
||||
Goal:
|
||||
|
||||
- A user with a working `fastvideo` install can run the Dreamverse backend on a
|
||||
local GPU and connect to it from `apps/dreamverse/web`.
|
||||
|
||||
Non-goals for this milestone:
|
||||
|
||||
- no controller process yet
|
||||
- no provider abstraction yet
|
||||
- no Runpod or Modal support yet
|
||||
- no secret-management UI yet
|
||||
|
||||
Reasoning:
|
||||
|
||||
- `apps/dreamverse/dreamverse/` already is the real local GPU runtime.
|
||||
- `apps/dreamverse/web` already knows how to talk to a backend over `/ws` and
|
||||
REST rewrites.
|
||||
- The shortest path is to make the existing local path explicit, reliable, and
|
||||
tested before adding another layer.
|
||||
|
||||
### Milestone 0 work items
|
||||
|
||||
#### 0.1 Fix repo path assumptions after the frontend move
|
||||
|
||||
Current issue:
|
||||
|
||||
- Some paths still assume `prod-ui/`, but the frontend now lives at
|
||||
`apps/dreamverse/web/`.
|
||||
|
||||
Required changes:
|
||||
|
||||
- update prompt/preset path resolution in
|
||||
`apps/dreamverse/dreamverse/config.py`
|
||||
- update docs that still mention `prod-ui`
|
||||
- audit any frontend build settings that assume the old repo root
|
||||
|
||||
This is prerequisite cleanup. Local GPU mode should not depend on stale
|
||||
monorepo paths.
|
||||
|
||||
#### 0.2 Make local runtime startup the primary supported entrypoint
|
||||
|
||||
Required outcome:
|
||||
|
||||
- one documented backend command
|
||||
- one documented frontend command
|
||||
- one clear env contract for local development
|
||||
|
||||
Expected shape:
|
||||
|
||||
```bash
|
||||
uv pip install -e ".[dreamverse]"
|
||||
dreamverse-server --host 0.0.0.0 --port 8009
|
||||
|
||||
cd apps/dreamverse/web
|
||||
npm ci
|
||||
BACKEND_HOST=localhost BACKEND_PORT=8009 npm run dev
|
||||
```
|
||||
|
||||
Optional but useful:
|
||||
|
||||
- add a small root helper script or Make target for local startup
|
||||
- add a `dreamverse-doctor` or lightweight startup check later
|
||||
|
||||
#### 0.3 Define the minimum local runtime contract
|
||||
|
||||
For Milestone 0, the frontend should rely only on the current runtime surface:
|
||||
|
||||
- `/ws`
|
||||
- `/status`
|
||||
- `/healthz`
|
||||
- `/readyz`
|
||||
- existing prompt/devtools routes
|
||||
|
||||
Do not add a second local API layer yet unless the current runtime surface is
|
||||
proven insufficient.
|
||||
|
||||
#### 0.4 Make failure states explicit in the UI
|
||||
|
||||
Local GPU mode fails in a few predictable ways:
|
||||
|
||||
- backend not reachable
|
||||
- backend reachable but not ready
|
||||
- `fastvideo` or model runtime missing
|
||||
- no compatible GPU available
|
||||
|
||||
Minimum implementation:
|
||||
|
||||
- show a clear connection error when `/ws` or `/status` fails
|
||||
- surface readiness failures in a human-readable way
|
||||
- avoid silent retry loops that hide backend startup failures
|
||||
|
||||
This is a small UI pass, not a controller project.
|
||||
|
||||
#### 0.5 Add a minimal local smoke test path
|
||||
|
||||
At this milestone, local GPU support is "done" only if there is a repeatable
|
||||
test path for the local runtime contract.
|
||||
|
||||
Minimum test additions:
|
||||
|
||||
- backend tests for `/healthz`, `/readyz`, and `/status`
|
||||
- a frontend integration test that assumes a reachable backend URL and verifies
|
||||
connection lifecycle behavior
|
||||
- one local smoke script that starts the backend and verifies readiness before
|
||||
the frontend is launched
|
||||
|
||||
### Milestone 1: Introduce a thin local controller
|
||||
|
||||
Goal:
|
||||
|
||||
- Preserve the same local GPU behavior, but place a stable local control-plane
|
||||
API in front of the runtime.
|
||||
|
||||
This should happen only after Milestone 0 is stable.
|
||||
|
||||
Scope:
|
||||
|
||||
- add `controller/`
|
||||
- proxy `/ws` and the needed REST routes to `apps/dreamverse/dreamverse/`
|
||||
- expose controller-owned status for "backend starting", "runtime ready", and
|
||||
"runtime failed"
|
||||
- optionally spawn the local runtime as a subprocess
|
||||
|
||||
Non-goal:
|
||||
|
||||
- do not add remote provider logic yet
|
||||
|
||||
Reasoning:
|
||||
|
||||
- the controller earns its complexity only once it stabilizes the local
|
||||
contract that future providers will share
|
||||
|
||||
### Milestone 2: Provider abstraction on top of the controller
|
||||
|
||||
Goal:
|
||||
|
||||
- Keep the same frontend contract while allowing the controller to resolve a
|
||||
runtime via `local`, then later `runpod` and `modal`.
|
||||
|
||||
At this point:
|
||||
|
||||
- define `ComputeProvider`
|
||||
- implement `providers/local.py`
|
||||
- move local-runtime subprocess management behind the provider interface
|
||||
|
||||
The first provider should be `local`, because it is cheapest to debug and
|
||||
matches the runtime most closely.
|
||||
|
||||
## Minimal Code Change Order
|
||||
|
||||
If we want the shortest path to a working local GPU milestone, the change order
|
||||
should be:
|
||||
|
||||
1. Fix `apps/dreamverse/dreamverse/config.py` and any remaining path
|
||||
assumptions from `prod-ui` to `apps/dreamverse/web`.
|
||||
2. Update `README.md` to document the real local GPU startup flow.
|
||||
3. Confirm `apps/dreamverse/web` connects cleanly to the local wrapper-backed
|
||||
backend.
|
||||
4. Improve frontend error handling for backend-not-ready and backend-missing
|
||||
cases.
|
||||
5. Add a local smoke test and keep existing backend/frontend tests green.
|
||||
6. Only then introduce `controller/`.
|
||||
|
||||
## Test Plan for the Local GPU Milestone
|
||||
|
||||
### Backend
|
||||
|
||||
Keep the current Python test suite as the base:
|
||||
|
||||
- `apps/dreamverse/dreamverse/tests/test_health_endpoints.py`
|
||||
- `apps/dreamverse/dreamverse/tests/test_mock_server.py`
|
||||
- `apps/dreamverse/dreamverse/tests/test_prompt_enhancer.py`
|
||||
- `apps/dreamverse/dreamverse/tests/test_rewrite_prompt_payload.py`
|
||||
- related config and logging tests
|
||||
|
||||
Add or tighten tests for:
|
||||
|
||||
- path resolution in `apps/dreamverse/dreamverse/config.py`
|
||||
- readiness behavior when GPU pool initialization fails
|
||||
- startup error messaging when `fastvideo` is unavailable
|
||||
|
||||
### Frontend
|
||||
|
||||
Keep the current Vitest suite as the base:
|
||||
|
||||
- websocket reducer tests
|
||||
- prompt-window snapshot tests
|
||||
- integration tests under `apps/dreamverse/web/src/app/`
|
||||
|
||||
Add or tighten tests for:
|
||||
|
||||
- connection failure UX when backend is down
|
||||
- readiness failure UX when backend returns non-ready status
|
||||
- backend routing configuration through `BACKEND_HOST` and `BACKEND_PORT`
|
||||
|
||||
### Manual smoke path
|
||||
|
||||
The first manual smoke checklist should be:
|
||||
|
||||
1. start `dreamverse-server`
|
||||
2. confirm `GET /healthz` returns 200
|
||||
3. confirm `GET /readyz` returns 200 after warmup
|
||||
4. start `apps/dreamverse/web`
|
||||
5. confirm the UI opens and the websocket connects
|
||||
6. submit a prompt and verify the first generation starts
|
||||
|
||||
This checklist should be written down in the README once the milestone is
|
||||
implemented.
|
||||
|
||||
## Test Strategy
|
||||
|
||||
The test suite should preserve one rule: provider changes must not be able to
|
||||
break prompt rewrite, websocket semantics, or runtime behavior silently.
|
||||
|
||||
### 1. Runtime unit and integration tests
|
||||
|
||||
Keep and expand the current `pytest` coverage in
|
||||
`apps/dreamverse/dreamverse/tests/test_*.py`.
|
||||
|
||||
Focus areas:
|
||||
|
||||
- config loading
|
||||
- prompt rewrite payload normalization
|
||||
- prompt enhancement and prompt safety
|
||||
- health/readiness endpoints
|
||||
- websocket session behavior
|
||||
- session logging
|
||||
- mock runtime behavior
|
||||
|
||||
These tests should remain provider-agnostic.
|
||||
|
||||
### 2. Controller unit tests
|
||||
|
||||
Add a Python test suite for the controller state machine.
|
||||
|
||||
Key cases:
|
||||
|
||||
- provider selection and validation
|
||||
- credential loading from local config or env
|
||||
- runtime lifecycle transitions:
|
||||
- idle
|
||||
- provisioning
|
||||
- ready
|
||||
- failed
|
||||
- stopping
|
||||
- idle timeout and cleanup behavior
|
||||
- retry and backoff behavior
|
||||
- HTTP and websocket proxy routing
|
||||
|
||||
These tests should use fake providers and fake runtimes by default.
|
||||
|
||||
### 3. Provider contract tests
|
||||
|
||||
Each provider should pass the same contract tests.
|
||||
|
||||
Examples:
|
||||
|
||||
- `ensure_runtime()` returns a usable `RuntimeHandle`
|
||||
- `wait_until_ready()` surfaces timeout vs readiness correctly
|
||||
- `stop_runtime()` is safe to call twice
|
||||
- provider errors are mapped into stable controller error types
|
||||
|
||||
Use recorded fixtures or fakes wherever possible to avoid spend in CI.
|
||||
|
||||
### 4. Web contract tests
|
||||
|
||||
The frontend already has useful Vitest coverage under
|
||||
`apps/dreamverse/web/src`.
|
||||
Preserve that and expand around the controller split.
|
||||
|
||||
Priority areas:
|
||||
|
||||
- websocket event reduction
|
||||
- prompt-window snapshot construction
|
||||
- rewrite request shaping
|
||||
- compute-status UI
|
||||
- failure and reconnect UX
|
||||
|
||||
The frontend should mock the local controller API, not provider APIs.
|
||||
|
||||
### 5. Cross-layer protocol tests
|
||||
|
||||
Add contract fixtures that validate shared payloads across layers.
|
||||
|
||||
Important fixtures:
|
||||
|
||||
- websocket event payloads
|
||||
- rewrite request payloads
|
||||
- runtime status payloads
|
||||
- controller status payloads
|
||||
|
||||
These can be simple JSON fixtures validated by both Python and TypeScript
|
||||
tests. They will catch drift earlier than end-to-end tests.
|
||||
|
||||
### 6. Smoke tests
|
||||
|
||||
Add a small number of high-signal smoke tests:
|
||||
|
||||
- local provider + mock runtime
|
||||
- local provider + real runtime when GPU is available
|
||||
- controller startup + frontend health path
|
||||
|
||||
These should be cheap enough for routine local use.
|
||||
|
||||
### 7. Provider-backed manual or nightly tests
|
||||
|
||||
Real Modal and Runpod tests should be opt-in.
|
||||
|
||||
- Do not run them in default CI.
|
||||
- Gate them behind explicit credentials and flags.
|
||||
- Keep them focused on provisioning and reachability, not full product
|
||||
regression.
|
||||
|
||||
This avoids flaky and expensive CI while still validating real provider flows.
|
||||
|
||||
## Testing Recommendations for the Next Step
|
||||
|
||||
The next practical additions should be:
|
||||
|
||||
1. A controller test suite in Python using fake providers.
|
||||
2. Shared contract fixtures for websocket and rewrite payloads.
|
||||
3. A smoke test that starts the local controller against the existing mock
|
||||
runtime.
|
||||
|
||||
I would not add Playwright yet. The current web stack already has Vitest and
|
||||
integration-style component tests, which are cheaper and better aligned with
|
||||
the immediate reorg. Add browser automation only after the controller path is
|
||||
stable.
|
||||
@@ -0,0 +1,103 @@
|
||||
# syntax=docker/dockerfile:1.7
|
||||
# CUDA base. CUDA_VERSION/UBUNTU_VERSION feed the default tag (matches the
|
||||
# unified docker/Dockerfile); override BUILD_BASE_IMAGE wholesale for a mirror.
|
||||
ARG CUDA_VERSION=13.0.0
|
||||
ARG UBUNTU_VERSION=22.04
|
||||
ARG BUILD_BASE_IMAGE=nvidia/cuda:${CUDA_VERSION}-cudnn-devel-ubuntu${UBUNTU_VERSION}
|
||||
FROM ${BUILD_BASE_IMAGE}
|
||||
|
||||
ARG BUILD_FASTVIDEO_KERNEL_FROM_SOURCE=0
|
||||
ARG BUILD_DREAMVERSE_UI=0
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
UV_LINK_MODE=copy \
|
||||
UV_CACHE_DIR=/opt/uv/cache
|
||||
|
||||
# pyproject no longer pins a PyTorch index; GPU-less build host -> pin explicitly
|
||||
# (auto would fall back to CPU). Matches the base CUDA: cu130 (13.0) / cu126 (12.6).
|
||||
ARG UV_TORCH_BACKEND=cu130
|
||||
ENV UV_TORCH_BACKEND=${UV_TORCH_BACKEND}
|
||||
|
||||
SHELL ["/bin/bash", "-c"]
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
gcc-11 g++-11 clang-11 \
|
||||
make cmake ninja-build pkg-config nasm \
|
||||
git curl wget ca-certificates \
|
||||
libssl-dev zlib1g-dev \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-11 100 \
|
||||
--slave /usr/bin/g++ g++ /usr/bin/g++-11
|
||||
|
||||
# Version-agnostic /usr/local/cuda symlink so this works for any CUDA_VERSION.
|
||||
ENV CUDA_HOME=/usr/local/cuda
|
||||
ENV PATH=/root/.local/bin:/opt/venv/bin:${CUDA_HOME}/bin:${PATH}
|
||||
ENV LD_LIBRARY_PATH=${CUDA_HOME}/lib64:${LD_LIBRARY_PATH}
|
||||
ENV VIRTUAL_ENV=/opt/venv
|
||||
|
||||
RUN curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
|
||||
RUN uv venv --python 3.12 --seed /opt/venv \
|
||||
&& echo 'source /opt/venv/bin/activate' >> /root/.bashrc
|
||||
|
||||
WORKDIR /opt/FastVideo
|
||||
|
||||
COPY . /opt/FastVideo
|
||||
|
||||
RUN --mount=type=cache,target=/opt/uv/cache \
|
||||
source /opt/venv/bin/activate \
|
||||
&& uv pip install "/opt/FastVideo[dreamverse]"
|
||||
|
||||
# Standard docker build does not expose GPUs, while fastvideo-kernel/build.sh
|
||||
# detects the CUDA architecture with torch at build time. The FastVideo package
|
||||
# install above brings in the pinned fastvideo-kernel package; rebuild from the
|
||||
# copied source only on hosts configured for build-time GPU access.
|
||||
RUN --mount=type=cache,target=/opt/uv/cache \
|
||||
if [[ "${BUILD_FASTVIDEO_KERNEL_FROM_SOURCE}" == "1" ]]; then \
|
||||
source /opt/venv/bin/activate \
|
||||
&& cd /opt/FastVideo/fastvideo-kernel \
|
||||
&& ./build.sh; \
|
||||
else \
|
||||
echo "Skipping source fastvideo-kernel build; using installed fastvideo-kernel package."; \
|
||||
fi
|
||||
|
||||
RUN if [[ "${BUILD_DREAMVERSE_UI}" == "1" ]]; then \
|
||||
curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
|
||||
&& apt-get install -y --no-install-recommends nodejs \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& cd /opt/FastVideo/apps/dreamverse/web \
|
||||
&& npm ci --ignore-scripts \
|
||||
&& NEXT_OUTPUT_EXPORT=1 npm run build \
|
||||
&& rm -rf node_modules .next; \
|
||||
else \
|
||||
echo "Skipping Dreamverse UI build; Building backend-only image."; \
|
||||
fi
|
||||
|
||||
# The monorepo ffmpeg installer force-selects conda compiler triplets for
|
||||
# local dev shells. Inside this image we explicitly opt into the system
|
||||
# gcc/g++ toolchain.
|
||||
RUN source /opt/venv/bin/activate \
|
||||
&& INSTALL_PREFIX=/opt/ffmpeg-native \
|
||||
SOURCE_DIR=/tmp/ffmpeg-native-src \
|
||||
FFMPEG_NATIVE_CC=/usr/bin/gcc \
|
||||
FFMPEG_NATIVE_CXX=/usr/bin/g++ \
|
||||
bash /opt/FastVideo/apps/dreamverse/scripts/install_native_ffmpeg.sh
|
||||
|
||||
# FASTVIDEO_FA4: FA4 (flash_attn.cute) is opt-in; this image installs it via
|
||||
# the dreamverse extra and is validated with it, so enable it here.
|
||||
ENV FASTVIDEO_DREAMVERSE_HOME=/var/lib/dreamverse \
|
||||
STREAM_MODE=av_fmp4 \
|
||||
FASTVIDEO_ENABLE_PROMPT_SAFETY=0 \
|
||||
FASTVIDEO_FA4=1 \
|
||||
HF_HOME=/root/.cache/huggingface
|
||||
|
||||
RUN mkdir -p /var/lib/dreamverse
|
||||
|
||||
EXPOSE 8009
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=2700s --retries=3 \
|
||||
CMD curl -fsS http://127.0.0.1:8009/healthz || exit 1
|
||||
|
||||
ENTRYPOINT ["/opt/FastVideo/apps/dreamverse/docker/docker_entrypoint.sh"]
|
||||
CMD ["dreamverse-server", "--host", "0.0.0.0", "--port", "8009"]
|
||||
@@ -0,0 +1,46 @@
|
||||
.git/
|
||||
**/.git/
|
||||
.venv/
|
||||
**/.venv/
|
||||
.*dreamverse*/
|
||||
**/__pycache__/
|
||||
**/*.pyc
|
||||
**/*.pyo
|
||||
**/*.egg-info/
|
||||
.pytest_cache/
|
||||
**/.pytest_cache/
|
||||
.mypy_cache/
|
||||
.ruff_cache/
|
||||
.cache/
|
||||
|
||||
apps/dreamverse/web/node_modules/
|
||||
apps/dreamverse/web/.next/
|
||||
apps/dreamverse/web/out/
|
||||
apps/dreamverse/web/dist/
|
||||
apps/dreamverse/web/test-results/
|
||||
apps/dreamverse/web/playwright-report/
|
||||
|
||||
apps/dreamverse/outputs/
|
||||
apps/dreamverse/dreamverse/outputs/
|
||||
apps/dreamverse/dreamverse/prompts.local/
|
||||
apps/dreamverse/logs/
|
||||
outputs/
|
||||
outputs_video/
|
||||
quality_check_outputs/
|
||||
data/
|
||||
logs/
|
||||
slurm-logs/
|
||||
wandb/
|
||||
|
||||
.env
|
||||
.env.*
|
||||
**/prompts.local/
|
||||
.codex/
|
||||
.agents/
|
||||
.opencode/
|
||||
.agent_tmp/
|
||||
.vscode/
|
||||
.idea/
|
||||
*.log
|
||||
*.tmp
|
||||
*.pdf
|
||||
@@ -0,0 +1,97 @@
|
||||
# Dreamverse Docker Image
|
||||
|
||||
This folder contains the Docker image for Dreamverse inside the FastVideo
|
||||
monorepo. Build commands use the FastVideo repository root as the Docker
|
||||
context, so run the helper scripts from this folder or from any path in the
|
||||
checkout.
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
apps/dreamverse/docker/docker_build.sh
|
||||
```
|
||||
|
||||
The image defaults to `dreamverse:dev`. Override it with:
|
||||
|
||||
```bash
|
||||
DREAMVERSE_IMAGE=dreamverse:local apps/dreamverse/docker/docker_build.sh
|
||||
```
|
||||
|
||||
Backend-only remains the default image. To include the static Dreamverse UI
|
||||
served by the backend, set `BUILD_DREAMVERSE_UI=1` and choose a specific image
|
||||
tag:
|
||||
|
||||
```bash
|
||||
BUILD_DREAMVERSE_UI=1 DREAMVERSE_IMAGE=<image-tag> apps/dreamverse/docker/docker_build.sh
|
||||
```
|
||||
|
||||
Prefer SHA-specific tags for deployable images; avoid `latest`.
|
||||
|
||||
The Dockerfile defaults to CUDA 13.0.0 with the cu130 PyTorch backend. CI also
|
||||
builds a CUDA 12.6.3 / cu126 image. Select that local build explicitly with:
|
||||
|
||||
```bash
|
||||
CUDA_VERSION=12.6.3 apps/dreamverse/docker/docker_build.sh
|
||||
```
|
||||
|
||||
The helper derives `UV_TORCH_BACKEND=cu126` for CUDA 12.x and `cu130` for CUDA
|
||||
13.x. Set `UV_TORCH_BACKEND` explicitly for a custom CUDA version. The legacy
|
||||
complete-image-tag override remains supported and selects the same matching
|
||||
backend:
|
||||
|
||||
```bash
|
||||
CUDA_TAG=12.6.3-cudnn-devel-ubuntu22.04 apps/dreamverse/docker/docker_build.sh
|
||||
```
|
||||
|
||||
Do not set `CUDA_TAG` and `CUDA_VERSION` together. The image installs FastVideo
|
||||
from this checkout with the `dreamverse` extra, including FA4 flash-attention
|
||||
and FlashInfer for NVFP4 quantization, and builds native FFmpeg.
|
||||
|
||||
FastVideo's pinned `fastvideo-kernel==0.3.2` package is installed by default.
|
||||
To rebuild `fastvideo-kernel` from this checkout during the image build, set:
|
||||
|
||||
```bash
|
||||
BUILD_FASTVIDEO_KERNEL_FROM_SOURCE=1 apps/dreamverse/docker/docker_build.sh
|
||||
```
|
||||
|
||||
That source build detects the GPU architecture with torch during `docker
|
||||
build`. On hosts where Docker does not expose GPUs during build, leave the
|
||||
default package install path enabled.
|
||||
|
||||
## Run
|
||||
|
||||
```bash
|
||||
CEREBRAS_API_KEY="<your-key>" \
|
||||
GROQ_API_KEY="<your-key>" \
|
||||
apps/dreamverse/docker/docker_run.sh
|
||||
```
|
||||
|
||||
The container serves Dreamverse on host port `8009` by default and mounts:
|
||||
|
||||
```text
|
||||
$HOME/.cache/huggingface -> /root/.cache/huggingface
|
||||
apps/dreamverse/outputs -> /var/lib/dreamverse/outputs
|
||||
```
|
||||
|
||||
Override the host port and output directory with `BACKEND_PORT` and
|
||||
`DREAMVERSE_OUTPUTS_DIR`.
|
||||
|
||||
To pin the container to a specific host GPU, pass Docker's GPU request syntax:
|
||||
|
||||
```bash
|
||||
DREAMVERSE_DOCKER_GPUS=device=4 FASTVIDEO_GPU_COUNT=1 \
|
||||
CEREBRAS_API_KEY="<your-key>" \
|
||||
GROQ_API_KEY="<your-key>" \
|
||||
apps/dreamverse/docker/docker_run.sh
|
||||
```
|
||||
|
||||
## Smoke
|
||||
|
||||
```bash
|
||||
CEREBRAS_API_KEY=placeholder \
|
||||
GROQ_API_KEY=placeholder \
|
||||
apps/dreamverse/docker/docker_smoke.sh
|
||||
```
|
||||
|
||||
The smoke script starts the container, polls `/healthz`, then polls `/readyz`.
|
||||
It removes the container on exit unless `DREAMVERSE_KEEP_CONTAINER=1` is set.
|
||||
Executable
+88
@@ -0,0 +1,88 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd -- "${SCRIPT_DIR}/../../.." && pwd)"
|
||||
IMAGE="${DREAMVERSE_IMAGE:-dreamverse:dev}"
|
||||
ROOT_DOCKERIGNORE="${REPO_ROOT}/.dockerignore"
|
||||
DOCKERFILE_DOCKERIGNORE="${SCRIPT_DIR}/Dockerfile.dockerignore"
|
||||
CREATED_ROOT_DOCKERIGNORE_SYMLINK=0
|
||||
|
||||
cleanup_root_dockerignore_symlink() {
|
||||
if [[ "${CREATED_ROOT_DOCKERIGNORE_SYMLINK}" == "1" && -L "${ROOT_DOCKERIGNORE}" ]] && \
|
||||
[[ "$(readlink "${ROOT_DOCKERIGNORE}")" == "${DOCKERFILE_DOCKERIGNORE}" ]]; then
|
||||
rm -- "${ROOT_DOCKERIGNORE}"
|
||||
fi
|
||||
}
|
||||
trap cleanup_root_dockerignore_symlink EXIT INT TERM
|
||||
|
||||
if [[ -z "${DOCKER_BUILDKIT:-}" ]] && docker buildx version >/dev/null 2>&1; then
|
||||
export DOCKER_BUILDKIT=1
|
||||
fi
|
||||
|
||||
build_args=()
|
||||
cuda_version="${CUDA_VERSION:-}"
|
||||
torch_backend="${UV_TORCH_BACKEND:-}"
|
||||
|
||||
# CUDA_TAG was the Dockerfile's original override and contains the complete
|
||||
# nvidia/cuda tag (for example, 12.6.3-cudnn-devel-ubuntu22.04). Keep accepting
|
||||
# it while translating it to the parameterized Dockerfile inputs.
|
||||
if [[ -n "${CUDA_TAG:-}" ]]; then
|
||||
if [[ -n "${CUDA_VERSION:-}" ]]; then
|
||||
printf 'CUDA_TAG and CUDA_VERSION cannot both be set. Use CUDA_VERSION for new builds.\n' >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
cuda_version="${CUDA_TAG%%-*}"
|
||||
if [[ ! "${cuda_version}" =~ ^[0-9]+\.[0-9]+(\.[0-9]+)?$ ]]; then
|
||||
printf 'Cannot infer CUDA_VERSION from CUDA_TAG=%s. Use CUDA_VERSION and UV_TORCH_BACKEND instead.\n' \
|
||||
"${CUDA_TAG}" >&2
|
||||
exit 2
|
||||
fi
|
||||
build_args+=(--build-arg "BUILD_BASE_IMAGE=nvidia/cuda:${CUDA_TAG}")
|
||||
fi
|
||||
|
||||
if [[ -n "${cuda_version}" ]]; then
|
||||
build_args+=(--build-arg "CUDA_VERSION=${cuda_version}")
|
||||
fi
|
||||
|
||||
if [[ -z "${torch_backend}" && -n "${cuda_version}" ]]; then
|
||||
case "${cuda_version}" in
|
||||
12.*) torch_backend=cu126 ;;
|
||||
13.*) torch_backend=cu130 ;;
|
||||
*)
|
||||
printf 'No default UV_TORCH_BACKEND for CUDA_VERSION=%s. Set UV_TORCH_BACKEND explicitly.\n' \
|
||||
"${cuda_version}" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
if [[ -n "${torch_backend}" ]]; then
|
||||
build_args+=(--build-arg "UV_TORCH_BACKEND=${torch_backend}")
|
||||
fi
|
||||
|
||||
[[ -n "${BUILD_FASTVIDEO_KERNEL_FROM_SOURCE:-}" ]] && \
|
||||
build_args+=(--build-arg "BUILD_FASTVIDEO_KERNEL_FROM_SOURCE=${BUILD_FASTVIDEO_KERNEL_FROM_SOURCE}")
|
||||
build_args+=(--build-arg "BUILD_DREAMVERSE_UI=${BUILD_DREAMVERSE_UI:-0}")
|
||||
|
||||
if [[ "${DOCKER_BUILDKIT:-}" == "1" ]]; then
|
||||
if [[ -L "${ROOT_DOCKERIGNORE}" ]] && \
|
||||
[[ "$(readlink "${ROOT_DOCKERIGNORE}")" == "${DOCKERFILE_DOCKERIGNORE}" ]]; then
|
||||
rm -- "${ROOT_DOCKERIGNORE}"
|
||||
printf 'Removed stale temporary root .dockerignore symlink: %s\n' "${ROOT_DOCKERIGNORE}"
|
||||
fi
|
||||
elif [[ -e "${ROOT_DOCKERIGNORE}" || -L "${ROOT_DOCKERIGNORE}" ]]; then
|
||||
printf 'Using existing root .dockerignore: %s\n' "${ROOT_DOCKERIGNORE}"
|
||||
else
|
||||
ln -s -- "${DOCKERFILE_DOCKERIGNORE}" "${ROOT_DOCKERIGNORE}"
|
||||
CREATED_ROOT_DOCKERIGNORE_SYMLINK=1
|
||||
printf 'Created temporary root .dockerignore symlink for legacy Docker builder: %s -> %s\n' \
|
||||
"${ROOT_DOCKERIGNORE}" "${DOCKERFILE_DOCKERIGNORE}"
|
||||
fi
|
||||
|
||||
docker build \
|
||||
-f "${SCRIPT_DIR}/Dockerfile" \
|
||||
-t "${IMAGE}" \
|
||||
"${build_args[@]}" \
|
||||
"${REPO_ROOT}"
|
||||
Executable
+13
@@ -0,0 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
source /opt/venv/bin/activate
|
||||
|
||||
if [[ -f /opt/FastVideo/apps/dreamverse/scripts/ffmpeg-env.sh ]]; then
|
||||
source /opt/FastVideo/apps/dreamverse/scripts/ffmpeg-env.sh
|
||||
fi
|
||||
|
||||
: "${CEREBRAS_API_KEY:?CEREBRAS_API_KEY must be set (pass with -e CEREBRAS_API_KEY=...)}"
|
||||
: "${GROQ_API_KEY:?GROQ_API_KEY must be set (pass with -e GROQ_API_KEY=...)}"
|
||||
|
||||
exec "$@"
|
||||
Executable
+30
@@ -0,0 +1,30 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
: "${CEREBRAS_API_KEY:?CEREBRAS_API_KEY not set on host}"
|
||||
: "${GROQ_API_KEY:?GROQ_API_KEY not set on host}"
|
||||
|
||||
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
DREAMVERSE_ROOT="$(cd -- "${SCRIPT_DIR}/.." && pwd)"
|
||||
|
||||
IMAGE="${DREAMVERSE_IMAGE:-dreamverse:dev}"
|
||||
PORT="${BACKEND_PORT:-8009}"
|
||||
HF_CACHE="${HF_HOME:-$HOME/.cache/huggingface}"
|
||||
OUTPUTS_DIR="${DREAMVERSE_OUTPUTS_DIR:-${DREAMVERSE_ROOT}/outputs}"
|
||||
GPU_REQUEST="${DREAMVERSE_DOCKER_GPUS:-all}"
|
||||
|
||||
mkdir -p "${HF_CACHE}" "${OUTPUTS_DIR}"
|
||||
|
||||
env_args=(
|
||||
-e "CEREBRAS_API_KEY=${CEREBRAS_API_KEY}"
|
||||
-e "GROQ_API_KEY=${GROQ_API_KEY}"
|
||||
)
|
||||
[[ -n "${ENABLE_TORCH_COMPILE:-}" ]] && env_args+=(-e "ENABLE_TORCH_COMPILE=${ENABLE_TORCH_COMPILE}")
|
||||
[[ -n "${FASTVIDEO_GPU_COUNT:-}" ]] && env_args+=(-e "FASTVIDEO_GPU_COUNT=${FASTVIDEO_GPU_COUNT}")
|
||||
|
||||
exec docker run --rm --gpus "${GPU_REQUEST}" --init \
|
||||
-p "${PORT}:8009" \
|
||||
"${env_args[@]}" \
|
||||
-v "${HF_CACHE}:/root/.cache/huggingface" \
|
||||
-v "${OUTPUTS_DIR}:/var/lib/dreamverse/outputs" \
|
||||
"${IMAGE}"
|
||||
Executable
+75
@@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
: "${CEREBRAS_API_KEY:?CEREBRAS_API_KEY not set on host}"
|
||||
: "${GROQ_API_KEY:?GROQ_API_KEY not set on host}"
|
||||
|
||||
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
DREAMVERSE_ROOT="$(cd -- "${SCRIPT_DIR}/.." && pwd)"
|
||||
|
||||
IMAGE="${DREAMVERSE_IMAGE:-dreamverse:dev}"
|
||||
PORT="${BACKEND_PORT:-8009}"
|
||||
HF_CACHE="${HF_HOME:-$HOME/.cache/huggingface}"
|
||||
OUTPUTS_DIR="${DREAMVERSE_OUTPUTS_DIR:-${DREAMVERSE_ROOT}/outputs}"
|
||||
NAME="${DREAMVERSE_NAME:-dreamverse}"
|
||||
TIMEOUT_SECONDS="${DREAMVERSE_SMOKE_TIMEOUT_SECONDS:-1200}"
|
||||
POLL_SECONDS="${DREAMVERSE_SMOKE_POLL_SECONDS:-5}"
|
||||
GPU_REQUEST="${DREAMVERSE_DOCKER_GPUS:-all}"
|
||||
|
||||
mkdir -p "${HF_CACHE}" "${OUTPUTS_DIR}"
|
||||
|
||||
docker rm -f "${NAME}" >/dev/null 2>&1 || true
|
||||
|
||||
env_args=(
|
||||
-e "CEREBRAS_API_KEY=${CEREBRAS_API_KEY}"
|
||||
-e "GROQ_API_KEY=${GROQ_API_KEY}"
|
||||
-e "ENABLE_TORCH_COMPILE=${ENABLE_TORCH_COMPILE:-0}"
|
||||
)
|
||||
[[ -n "${FASTVIDEO_GPU_COUNT:-}" ]] && env_args+=(-e "FASTVIDEO_GPU_COUNT=${FASTVIDEO_GPU_COUNT}")
|
||||
|
||||
container_id="$(
|
||||
docker run -d --rm --gpus "${GPU_REQUEST}" --init \
|
||||
-p "${PORT}:8009" \
|
||||
"${env_args[@]}" \
|
||||
-v "${HF_CACHE}:/root/.cache/huggingface" \
|
||||
-v "${OUTPUTS_DIR}:/var/lib/dreamverse/outputs" \
|
||||
--name "${NAME}" \
|
||||
"${IMAGE}"
|
||||
)"
|
||||
|
||||
cleanup() {
|
||||
if [[ "${DREAMVERSE_KEEP_CONTAINER:-0}" != "1" ]]; then
|
||||
docker rm -f "${NAME}" >/dev/null 2>&1 || true
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
wait_for_endpoint() {
|
||||
local path="$1"
|
||||
local label="$2"
|
||||
local deadline=$((SECONDS + TIMEOUT_SECONDS))
|
||||
local url="http://127.0.0.1:${PORT}${path}"
|
||||
|
||||
echo "Waiting for ${label} at ${url}"
|
||||
while (( SECONDS < deadline )); do
|
||||
if curl -fsS "${url}" >/dev/null 2>&1; then
|
||||
echo "${label} ok"
|
||||
return 0
|
||||
fi
|
||||
if ! docker ps --format '{{.Names}}' | grep -qx "${NAME}"; then
|
||||
echo "Container exited before ${label} became healthy." >&2
|
||||
docker logs "${container_id}" >&2 || true
|
||||
return 1
|
||||
fi
|
||||
sleep "${POLL_SECONDS}"
|
||||
done
|
||||
|
||||
echo "Timed out waiting for ${label}." >&2
|
||||
docker logs "${container_id}" >&2 || true
|
||||
return 1
|
||||
}
|
||||
|
||||
wait_for_endpoint "/healthz" "healthz"
|
||||
wait_for_endpoint "/readyz" "readyz"
|
||||
|
||||
echo "Dreamverse Docker smoke passed for ${IMAGE} on host port ${PORT}."
|
||||
@@ -0,0 +1,15 @@
|
||||
from __future__ import annotations
|
||||
|
||||
DREAMVERSE_RUNTIME_DEPS_MESSAGE = (
|
||||
"Dreamverse runtime deps missing — install with pip install 'fastvideo[dreamverse]'.")
|
||||
|
||||
|
||||
def require_dreamverse_runtime_deps() -> None:
|
||||
try:
|
||||
import cerebras.cloud.sdk # noqa: F401
|
||||
import openai # noqa: F401
|
||||
except ModuleNotFoundError as exc:
|
||||
missing_root = (exc.name or "").split(".", 1)[0]
|
||||
if missing_root in {"cerebras", "openai"}:
|
||||
raise SystemExit(DREAMVERSE_RUNTIME_DEPS_MESSAGE) from exc
|
||||
raise
|
||||
@@ -0,0 +1,184 @@
|
||||
"""Bounded, runtime-local media library shared by the HTTP and generation APIs."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import threading
|
||||
import uuid
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
from PIL import Image, UnidentifiedImageError
|
||||
|
||||
IMAGE_LIMIT = 15 * 1024 * 1024
|
||||
MEDIA_LIMIT = 100 * 1024 * 1024
|
||||
STORE_LIMIT = 2 * 1024 * 1024 * 1024
|
||||
ASSET_LIMIT = 100
|
||||
MAX_MEDIA_SECONDS = 30
|
||||
MIME_TYPES = {
|
||||
"image/png": ("image", ".png"),
|
||||
"image/jpeg": ("image", ".jpg"),
|
||||
"image/webp": ("image", ".webp"),
|
||||
"video/mp4": ("video", ".mp4"),
|
||||
"video/quicktime": ("video", ".mov"),
|
||||
"video/webm": ("video", ".webm"),
|
||||
"audio/mpeg": ("audio", ".mp3"),
|
||||
"audio/mp4": ("audio", ".m4a"),
|
||||
"audio/x-m4a": ("audio", ".m4a"),
|
||||
"audio/wav": ("audio", ".wav"),
|
||||
"audio/x-wav": ("audio", ".wav"),
|
||||
"audio/flac": ("audio", ".flac"),
|
||||
"audio/x-flac": ("audio", ".flac"),
|
||||
"audio/ogg": ("audio", ".ogg"),
|
||||
"audio/webm": ("audio", ".webm"),
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class StoredAsset:
|
||||
asset_id: str
|
||||
kind: str
|
||||
path: str
|
||||
name: str
|
||||
mime_type: str
|
||||
size: int
|
||||
|
||||
def public(self) -> dict:
|
||||
return {
|
||||
"asset_id": self.asset_id,
|
||||
"kind": self.kind,
|
||||
"name": self.name,
|
||||
"mime_type": self.mime_type,
|
||||
"size": self.size,
|
||||
"url": f"/assets/{self.asset_id}",
|
||||
}
|
||||
|
||||
|
||||
def validate_media(path: Path, mime_type: str) -> None:
|
||||
"""Inspect content, not filenames; refuse playlists and non-media uploads."""
|
||||
kind = MIME_TYPES[mime_type][0]
|
||||
if kind == "image":
|
||||
try:
|
||||
with Image.open(path) as img:
|
||||
expected = {"image/png": "PNG", "image/jpeg": "JPEG", "image/webp": "WEBP"}[mime_type]
|
||||
if img.format != expected:
|
||||
raise ValueError("The image content does not match its file type.")
|
||||
if img.width * img.height > 16_777_216:
|
||||
raise ValueError("Images must contain at most 16 megapixels.")
|
||||
if getattr(img, "is_animated", False):
|
||||
raise ValueError("Use a still image or upload the animation as a video.")
|
||||
img.verify()
|
||||
except (UnidentifiedImageError, OSError, Image.DecompressionBombError) as exc:
|
||||
raise ValueError("The image could not be decoded. Use PNG, JPEG, or WebP.") from exc
|
||||
return
|
||||
|
||||
probe = shutil.which(os.getenv("FASTVIDEO_FFPROBE_BIN", "ffprobe"))
|
||||
if not probe:
|
||||
raise ValueError("This runtime needs ffprobe installed to accept video and audio assets.")
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[
|
||||
probe, "-v", "error", "-protocol_whitelist", "file,pipe", "-format_whitelist",
|
||||
"mov,matroska,webm,mp3,wav,flac,ogg", "-show_format", "-show_streams", "-of", "json",
|
||||
str(path)
|
||||
],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
timeout=15,
|
||||
)
|
||||
info = json.loads(result.stdout)
|
||||
formats = set(info.get("format", {}).get("format_name", "").split(","))
|
||||
if not formats.intersection({"mov", "mp4", "matroska", "webm", "mp3", "wav", "flac", "ogg"}):
|
||||
raise ValueError("Upload a media file, not a playlist or external reference.")
|
||||
streams = [stream for stream in info.get("streams", []) if stream.get("codec_type") == kind]
|
||||
if not streams:
|
||||
raise ValueError(f"The file contains no {kind} stream.")
|
||||
for stream in info.get("streams", []):
|
||||
if stream.get("codec_type") == "audio" and int(stream.get("channels", 0)) not in (1, 2):
|
||||
raise ValueError("H3 references require mono or stereo audio, including video soundtracks.")
|
||||
duration = float(info.get("format", {}).get("duration", "nan"))
|
||||
if not math.isfinite(duration) or not 0 < duration <= MAX_MEDIA_SECONDS:
|
||||
raise ValueError(f"Reference video and audio must be between 0 and {MAX_MEDIA_SECONDS} seconds long.")
|
||||
for stream in streams:
|
||||
if kind == "video" and int(stream.get("width", 0)) * int(stream.get("height", 0)) > 8_294_400:
|
||||
raise ValueError("Reference videos must be 4K or smaller.")
|
||||
except (subprocess.SubprocessError, json.JSONDecodeError, OSError) as exc:
|
||||
raise ValueError("The media file could not be decoded. Check its format and try again.") from exc
|
||||
|
||||
|
||||
class AssetStore:
|
||||
"""Assets live until deletion or runtime exit; pinned generation inputs cannot be deleted."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._directory: tempfile.TemporaryDirectory | None = None
|
||||
self._assets: dict[str, StoredAsset] = {}
|
||||
self._pins: dict[str, int] = {}
|
||||
self._lock = threading.RLock()
|
||||
|
||||
def staging_path(self, mime_type: str) -> Path:
|
||||
with self._lock:
|
||||
if mime_type not in MIME_TYPES:
|
||||
raise ValueError("Unsupported media type. Use PNG/JPEG/WebP, MP4/WebM/MOV, or WAV/MP3/M4A/FLAC/OGG.")
|
||||
if len(self._assets) >= ASSET_LIMIT or sum(item.size for item in self._assets.values()) >= STORE_LIMIT:
|
||||
raise ValueError("The runtime asset library is full. Remove unused assets before uploading more.")
|
||||
if self._directory is None:
|
||||
self._directory = tempfile.TemporaryDirectory(prefix="dreamverse-assets-")
|
||||
return Path(self._directory.name) / f"{uuid.uuid4().hex}{MIME_TYPES[mime_type][1]}"
|
||||
|
||||
def add(self, path: Path, name: str, mime_type: str) -> StoredAsset:
|
||||
validate_media(path, mime_type)
|
||||
size = path.stat().st_size
|
||||
if size == 0 or size > (IMAGE_LIMIT if MIME_TYPES[mime_type][0] == "image" else MEDIA_LIMIT):
|
||||
raise ValueError("The asset is empty or exceeds its upload size limit.")
|
||||
with self._lock:
|
||||
if len(self._assets) >= ASSET_LIMIT or size + sum(item.size
|
||||
for item in self._assets.values()) > STORE_LIMIT:
|
||||
raise ValueError("The runtime asset library is full. Remove unused assets before uploading more.")
|
||||
if self._directory is None or path.parent != Path(self._directory.name):
|
||||
raise ValueError("The asset must be uploaded to this runtime.")
|
||||
display_name = re.sub(r"[\x00-\x1f\x7f/\\]", "_", name).strip()[:200] or "Untitled asset"
|
||||
asset = StoredAsset(path.stem, MIME_TYPES[mime_type][0], str(path), display_name, mime_type, size)
|
||||
self._assets[asset.asset_id] = asset
|
||||
return asset
|
||||
|
||||
def get(self, asset_id: str) -> StoredAsset:
|
||||
with self._lock:
|
||||
if not isinstance(asset_id, str) or not re.fullmatch(r"[a-f0-9]{32}", asset_id):
|
||||
raise ValueError("Invalid asset ID. Upload or select an asset from the library.")
|
||||
asset = self._assets.get(asset_id)
|
||||
if asset is None or not Path(asset.path).is_file():
|
||||
raise ValueError("An asset is no longer available. Upload it again and reselect it.")
|
||||
return asset
|
||||
|
||||
def pin(self, asset_ids: list[str]) -> None:
|
||||
with self._lock:
|
||||
for asset_id in asset_ids:
|
||||
self.get(asset_id)
|
||||
for asset_id in asset_ids:
|
||||
self._pins[asset_id] = self._pins.get(asset_id, 0) + 1
|
||||
|
||||
def release(self, asset_ids: list[str]) -> None:
|
||||
with self._lock:
|
||||
for asset_id in asset_ids:
|
||||
count = self._pins.get(asset_id, 0)
|
||||
if count > 1:
|
||||
self._pins[asset_id] = count - 1
|
||||
else:
|
||||
self._pins.pop(asset_id, None)
|
||||
|
||||
def delete(self, asset_id: str) -> None:
|
||||
with self._lock:
|
||||
asset = self.get(asset_id)
|
||||
if self._pins.get(asset_id, 0):
|
||||
raise ValueError("This asset is in use by a generation session. End the session before deleting it.")
|
||||
Path(asset.path).unlink(missing_ok=True)
|
||||
del self._assets[asset_id]
|
||||
|
||||
|
||||
asset_store = AssetStore()
|
||||
@@ -0,0 +1,445 @@
|
||||
# pyright: reportMissingTypeArgument=false, reportArgumentType=false, reportOptionalSubscript=false, reportOptionalMemberAccess=false, reportConstantRedefinition=false, reportCallIssue=false
|
||||
# ruff: noqa: UP007, SIM108, SIM105
|
||||
# mypy: ignore-errors
|
||||
"""ffmpeg fMP4 muxing with chunk-level event emission.
|
||||
|
||||
Self-contained: spawns ffmpeg as a subprocess, pipes raw frames into
|
||||
its stdin, reads fragmented-MP4 chunks from stdout, and publishes each
|
||||
chunk as a ``StreamEvent`` via the caller-supplied ``publish``
|
||||
callback. Knows nothing about multiprocessing queues, the GPU pool,
|
||||
or individual users — the caller decides what "publish" means.
|
||||
"""
|
||||
import fcntl
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
import uuid
|
||||
import wave
|
||||
from dataclasses import dataclass
|
||||
from typing import Union
|
||||
from collections.abc import Callable
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
|
||||
FFMPEG_BIN = shutil.which(os.getenv("FASTVIDEO_FFMPEG_BIN", "ffmpeg"))
|
||||
AV_MEDIA_MIME = os.getenv(
|
||||
"STREAM_MIME_TYPE",
|
||||
'video/mp4; codecs="avc1.42E01E,mp4a.40.2"',
|
||||
)
|
||||
AV_CHUNK_SIZE_BYTES = 1048576
|
||||
TARGET_FPS = 24
|
||||
AV_FRAGMENT_DURATION_US = int(os.getenv("FASTVIDEO_FRAG_US", "250000"))
|
||||
X264_GOP_FRAMES = int(os.getenv("FASTVIDEO_X264_GOP", "12"))
|
||||
X264_PROFILE = os.getenv("FASTVIDEO_X264_PROFILE", "baseline").strip().lower()
|
||||
if X264_PROFILE not in {"baseline", "main", "high", "main10", "high10"}:
|
||||
print(f"[WARN] Unsupported FASTVIDEO_X264_PROFILE={X264_PROFILE}; using baseline")
|
||||
X264_PROFILE = "baseline"
|
||||
USE_SHARED_STREAM_BUFFER = (os.getenv("FASTVIDEO_USE_SHARED_STREAM_BUFFER", "1").strip().lower()
|
||||
not in {"0", "false", "no"})
|
||||
SHARED_STREAM_BUFFER_BYTES = int(os.getenv("FASTVIDEO_SHARED_STREAM_BUFFER_BYTES", str(256 * 1024 * 1024)))
|
||||
|
||||
|
||||
@dataclass
|
||||
class StreamInit:
|
||||
"""First event emitted — tells the consumer the stream is starting."""
|
||||
stream_id: str
|
||||
mime: str
|
||||
uses_shared_buffer: bool
|
||||
|
||||
|
||||
@dataclass
|
||||
class StreamChunk:
|
||||
"""One fMP4 chunk. Either ``chunk`` (raw bytes) or
|
||||
``chunk_offset``+``chunk_length`` (read from the shared buffer)
|
||||
will be populated, never both."""
|
||||
stream_id: str
|
||||
chunk: bytes | None = None
|
||||
chunk_offset: int | None = None
|
||||
chunk_length: int | None = None
|
||||
uses_shared_buffer: bool = False
|
||||
|
||||
|
||||
@dataclass
|
||||
class StreamComplete:
|
||||
"""Final event emitted — muxing finished successfully."""
|
||||
stream_id: str
|
||||
chunks: int
|
||||
|
||||
|
||||
StreamEvent = Union[StreamInit, StreamChunk, StreamComplete]
|
||||
|
||||
|
||||
def generate_stream_id(segment_idx: int) -> str:
|
||||
"""Convenience: build a stream id of the form ``seg007-abcd1234``."""
|
||||
return f"seg{segment_idx:03d}-{uuid.uuid4().hex[:8]}"
|
||||
|
||||
|
||||
def _normalize_audio_tensor(audio: object) -> tuple[np.ndarray, int] | None:
|
||||
"""Convert audio tensor/array into int16 ndarray [samples, channels]."""
|
||||
if audio is None:
|
||||
return None
|
||||
|
||||
if torch.is_tensor(audio):
|
||||
audio_np = audio.detach().cpu().float().numpy()
|
||||
else:
|
||||
audio_np = np.asarray(audio, dtype=np.float32)
|
||||
|
||||
if audio_np.ndim == 1:
|
||||
audio_np = audio_np[:, None]
|
||||
elif audio_np.ndim == 2:
|
||||
if audio_np.shape[0] <= 8 and audio_np.shape[1] > audio_np.shape[0]:
|
||||
audio_np = audio_np.T
|
||||
else:
|
||||
return None
|
||||
|
||||
audio_np = np.clip(audio_np, -1.0, 1.0)
|
||||
audio_int16 = (audio_np * 32767.0).astype(np.int16)
|
||||
num_channels = audio_int16.shape[1]
|
||||
return audio_int16, num_channels
|
||||
|
||||
|
||||
def _write_audio_wav(
|
||||
audio_int16: np.ndarray,
|
||||
num_channels: int,
|
||||
sample_rate: int,
|
||||
) -> str:
|
||||
"""Write normalized int16 audio to a temporary WAV file."""
|
||||
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as f:
|
||||
wav_path = f.name
|
||||
try:
|
||||
with wave.open(wav_path, "wb") as wav_file:
|
||||
wav_file.setnchannels(num_channels)
|
||||
wav_file.setsampwidth(2)
|
||||
wav_file.setframerate(sample_rate)
|
||||
wav_file.writeframes(audio_int16.tobytes())
|
||||
except Exception:
|
||||
try:
|
||||
os.unlink(wav_path)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
raise
|
||||
return wav_path
|
||||
|
||||
|
||||
def stream_fmp4(
|
||||
*,
|
||||
frames: list[np.ndarray],
|
||||
audio: object,
|
||||
audio_sample_rate: int | None,
|
||||
stream_id: str,
|
||||
timings: dict,
|
||||
head_trim_frames: int = 0,
|
||||
head_trim_audio_frames: int | None = None,
|
||||
shared_buffer=None,
|
||||
shared_buffer_bytes: int = 0,
|
||||
publish: Callable[[StreamEvent], None],
|
||||
log_prefix: str = "",
|
||||
) -> tuple[bool, str | None]:
|
||||
"""Encode frames+audio with ffmpeg, publish each fMP4 chunk as an event.
|
||||
|
||||
Args:
|
||||
frames: RGB24 video frames as HxWx3 uint8 arrays.
|
||||
audio: 1D/2D tensor or ndarray, float values in [-1, 1].
|
||||
audio_sample_rate: sample rate of ``audio``.
|
||||
stream_id: caller-supplied identifier carried on every event.
|
||||
timings: dict mutated in place with ffmpeg/stream timing metrics.
|
||||
head_trim_frames: video frames to drop from the start
|
||||
(conditioning overlap).
|
||||
head_trim_audio_frames: video-frame-equivalent audio to drop.
|
||||
Defaults to ``head_trim_frames``.
|
||||
shared_buffer: optional ``mp.RawArray``-compatible object; when
|
||||
provided, chunks are written into it and emitted by offset
|
||||
rather than by bytes (avoids IPC copies).
|
||||
shared_buffer_bytes: size of ``shared_buffer`` in bytes.
|
||||
publish: callback invoked once per stream event.
|
||||
log_prefix: prepended to warning prints (e.g. ``"[GPU 0]"``).
|
||||
|
||||
Returns:
|
||||
``(True, None)`` on success, ``(False, error_message)`` on
|
||||
failure. On mid-stream failure, a ``StreamInit`` may have
|
||||
already been published — the caller is responsible for
|
||||
handling that.
|
||||
"""
|
||||
if not frames:
|
||||
return False, "no frames returned"
|
||||
if audio is None:
|
||||
return False, "audio is None"
|
||||
if audio_sample_rate is None:
|
||||
return False, "audio_sample_rate is None"
|
||||
if FFMPEG_BIN is None:
|
||||
return False, "ffmpeg not found"
|
||||
|
||||
if head_trim_audio_frames is None:
|
||||
head_trim_audio_frames = head_trim_frames
|
||||
|
||||
normalized_audio = _normalize_audio_tensor(audio)
|
||||
if normalized_audio is None:
|
||||
shape_hint = getattr(audio, "shape", None)
|
||||
return False, f"unsupported audio shape={shape_hint}"
|
||||
audio_int16, num_channels = normalized_audio
|
||||
|
||||
if head_trim_frames < 0:
|
||||
return False, (f"head_trim_frames must be >= 0, "
|
||||
f"got {head_trim_frames}")
|
||||
if head_trim_frames >= len(frames):
|
||||
return False, (f"head_trim_frames={head_trim_frames} removes "
|
||||
f"all {len(frames)} frames in segment")
|
||||
|
||||
out_frames = (frames[head_trim_frames:] if head_trim_frames > 0 else frames)
|
||||
sample_rate = int(audio_sample_rate)
|
||||
if head_trim_audio_frames > 0:
|
||||
trim_start_samples = int(round((head_trim_audio_frames / float(TARGET_FPS)) * sample_rate))
|
||||
if trim_start_samples >= audio_int16.shape[0]:
|
||||
return False, ("audio too short after overlap trim: "
|
||||
f"trim_start_samples={trim_start_samples}"
|
||||
f", audio_samples={audio_int16.shape[0]}")
|
||||
keep_samples = int(round((len(out_frames) / float(TARGET_FPS)) * sample_rate))
|
||||
trim_end_samples = min(
|
||||
audio_int16.shape[0],
|
||||
trim_start_samples + keep_samples,
|
||||
)
|
||||
if trim_end_samples <= trim_start_samples:
|
||||
return False, ("invalid audio trim range: "
|
||||
f"start={trim_start_samples}, "
|
||||
f"end={trim_end_samples}")
|
||||
audio_int16 = audio_int16[trim_start_samples:trim_end_samples]
|
||||
|
||||
height = int(out_frames[0].shape[0])
|
||||
width = int(out_frames[0].shape[1])
|
||||
codec = os.getenv("FASTVIDEO_VIDEO_CODEC", "libx264")
|
||||
t_wav_start = time.perf_counter()
|
||||
wav_path = _write_audio_wav(audio_int16, num_channels, sample_rate)
|
||||
wav_write_ms = (time.perf_counter() - t_wav_start) * 1000
|
||||
|
||||
cmd = [
|
||||
FFMPEG_BIN,
|
||||
"-hide_banner",
|
||||
"-loglevel",
|
||||
"error",
|
||||
"-y",
|
||||
"-f",
|
||||
"rawvideo",
|
||||
"-pix_fmt",
|
||||
"rgb24",
|
||||
"-s:v",
|
||||
f"{width}x{height}",
|
||||
"-r",
|
||||
str(TARGET_FPS),
|
||||
"-i",
|
||||
"pipe:0",
|
||||
"-i",
|
||||
wav_path,
|
||||
"-c:v",
|
||||
codec,
|
||||
]
|
||||
if codec.endswith("_nvenc"):
|
||||
cmd += [
|
||||
"-preset",
|
||||
os.getenv("FASTVIDEO_NVENC_PRESET", "p1"),
|
||||
"-tune",
|
||||
os.getenv("FASTVIDEO_NVENC_TUNE", "ull"),
|
||||
"-rc",
|
||||
os.getenv("FASTVIDEO_NVENC_RC", "constqp"),
|
||||
"-qp",
|
||||
os.getenv("FASTVIDEO_NVENC_QP", "28"),
|
||||
"-bf",
|
||||
os.getenv("FASTVIDEO_NVENC_BF", "0"),
|
||||
]
|
||||
else:
|
||||
cmd += [
|
||||
"-preset",
|
||||
os.getenv("FASTVIDEO_X264_PRESET", "ultrafast"),
|
||||
"-tune",
|
||||
"zerolatency",
|
||||
"-profile:v",
|
||||
X264_PROFILE,
|
||||
# Emit frequent keyframes so fragments are independently playable.
|
||||
"-g",
|
||||
str(X264_GOP_FRAMES),
|
||||
"-keyint_min",
|
||||
str(X264_GOP_FRAMES),
|
||||
"-x264-params",
|
||||
"scenecut=0",
|
||||
]
|
||||
cmd += [
|
||||
"-c:a",
|
||||
"aac",
|
||||
"-pix_fmt",
|
||||
os.getenv("FASTVIDEO_OUTPUT_PIX_FMT", "yuv420p"),
|
||||
"-shortest",
|
||||
"-movflags",
|
||||
"+frag_keyframe+empty_moov+default_base_moof",
|
||||
"-frag_duration",
|
||||
str(AV_FRAGMENT_DURATION_US),
|
||||
"-flush_packets",
|
||||
"1",
|
||||
"-muxdelay",
|
||||
"0",
|
||||
"-muxpreload",
|
||||
"0",
|
||||
"-f",
|
||||
"mp4",
|
||||
"pipe:1",
|
||||
]
|
||||
|
||||
proc: subprocess.Popen | None = None
|
||||
stderr_chunks: list[bytes] = []
|
||||
writer_error: list[Exception | None] = [None]
|
||||
t_stream_start = time.perf_counter()
|
||||
use_shared_buffer = (USE_SHARED_STREAM_BUFFER and shared_buffer is not None and shared_buffer_bytes > 0)
|
||||
shared_write_offset = 0
|
||||
shared_buffer_fallback = False
|
||||
shared_np = (np.frombuffer(
|
||||
shared_buffer,
|
||||
dtype=np.uint8,
|
||||
count=shared_buffer_bytes,
|
||||
) if use_shared_buffer else None)
|
||||
chunk_intervals_ms: list[float] = []
|
||||
chunk_publish_ms: list[float] = []
|
||||
chunk_read_ms: list[float] = []
|
||||
|
||||
try:
|
||||
t_proc_spawn_start = time.perf_counter()
|
||||
proc = subprocess.Popen(
|
||||
cmd,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
bufsize=0,
|
||||
)
|
||||
assert proc.stdin is not None
|
||||
assert proc.stdout is not None
|
||||
assert proc.stderr is not None
|
||||
if hasattr(fcntl, "F_SETPIPE_SZ"):
|
||||
fcntl.fcntl(proc.stdin.fileno(), fcntl.F_SETPIPE_SZ, 1048576)
|
||||
fcntl.fcntl(proc.stdout.fileno(), fcntl.F_SETPIPE_SZ, 1048576)
|
||||
ffmpeg_spawn_ms = (time.perf_counter() - t_proc_spawn_start) * 1000
|
||||
|
||||
def _write_frames():
|
||||
try:
|
||||
for frame in out_frames:
|
||||
proc.stdin.write(np.ascontiguousarray(frame).tobytes())
|
||||
proc.stdin.close()
|
||||
except Exception as exc:
|
||||
writer_error[0] = exc
|
||||
try:
|
||||
proc.stdin.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _read_stderr():
|
||||
try:
|
||||
while True:
|
||||
data = proc.stderr.read(4096)
|
||||
if not data:
|
||||
break
|
||||
stderr_chunks.append(data)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
writer_thread = threading.Thread(target=_write_frames, daemon=True)
|
||||
stderr_thread = threading.Thread(target=_read_stderr, daemon=True)
|
||||
writer_thread.start()
|
||||
stderr_thread.start()
|
||||
|
||||
publish(StreamInit(
|
||||
stream_id=stream_id,
|
||||
mime=AV_MEDIA_MIME,
|
||||
uses_shared_buffer=use_shared_buffer,
|
||||
))
|
||||
|
||||
chunk_count = 0
|
||||
total_bytes = 0
|
||||
first_chunk_ms: float | None = None
|
||||
last_chunk_emit_t = time.perf_counter()
|
||||
while True:
|
||||
t_read_start = time.perf_counter()
|
||||
chunk = proc.stdout.read(AV_CHUNK_SIZE_BYTES)
|
||||
t_read_end = time.perf_counter()
|
||||
if not chunk:
|
||||
break
|
||||
chunk_read_ms.append((t_read_end - t_read_start) * 1000)
|
||||
chunk_count += 1
|
||||
total_bytes += len(chunk)
|
||||
if first_chunk_ms is None:
|
||||
first_chunk_ms = (t_read_end - t_stream_start) * 1000
|
||||
chunk_intervals_ms.append((t_read_end - last_chunk_emit_t) * 1000)
|
||||
t_publish_start = time.perf_counter()
|
||||
if use_shared_buffer and not shared_buffer_fallback:
|
||||
chunk_len = len(chunk)
|
||||
write_end = shared_write_offset + chunk_len
|
||||
if write_end <= shared_buffer_bytes:
|
||||
shared_np[shared_write_offset:write_end] = np.frombuffer(chunk, dtype=np.uint8)
|
||||
publish(
|
||||
StreamChunk(
|
||||
stream_id=stream_id,
|
||||
chunk_offset=shared_write_offset,
|
||||
chunk_length=chunk_len,
|
||||
uses_shared_buffer=True,
|
||||
))
|
||||
shared_write_offset = write_end
|
||||
chunk_publish_ms.append((time.perf_counter() - t_publish_start) * 1000)
|
||||
last_chunk_emit_t = time.perf_counter()
|
||||
continue
|
||||
shared_buffer_fallback = True
|
||||
print(f"{log_prefix} Shared stream buffer exhausted at "
|
||||
f"{shared_write_offset / (1024 * 1024):.1f}MB; "
|
||||
"falling back to queue chunk bytes")
|
||||
publish(StreamChunk(
|
||||
stream_id=stream_id,
|
||||
chunk=chunk,
|
||||
))
|
||||
chunk_publish_ms.append((time.perf_counter() - t_publish_start) * 1000)
|
||||
last_chunk_emit_t = time.perf_counter()
|
||||
|
||||
writer_thread.join(timeout=5.0)
|
||||
rc = proc.wait()
|
||||
stderr_thread.join(timeout=1.0)
|
||||
|
||||
if rc != 0:
|
||||
stderr_tail = b"".join(stderr_chunks).decode(errors="ignore")[-1200:]
|
||||
return False, f"ffmpeg av_fmp4 stream failed (rc={rc}): {stderr_tail}"
|
||||
if writer_error[0] is not None:
|
||||
return False, f"ffmpeg frame writer failed: {writer_error[0]}"
|
||||
|
||||
timings["av_encode_stream_ms"] = (time.perf_counter() - t_stream_start) * 1000
|
||||
timings["av_stream_bytes"] = total_bytes
|
||||
timings["av_trim_head_frames"] = head_trim_frames
|
||||
timings["av_trim_head_audio_frames"] = head_trim_audio_frames
|
||||
timings["av_frames_encoded"] = len(out_frames)
|
||||
timings["av_shared_buffer_used"] = (bool(use_shared_buffer and not shared_buffer_fallback))
|
||||
timings["av_wav_write_ms"] = wav_write_ms
|
||||
timings["av_ffmpeg_spawn_ms"] = ffmpeg_spawn_ms
|
||||
timings["av_first_chunk_ms"] = first_chunk_ms or 0.0
|
||||
if chunk_intervals_ms:
|
||||
timings["av_chunk_interval_ms_min"] = min(chunk_intervals_ms)
|
||||
timings["av_chunk_interval_ms_median"] = float(np.median(chunk_intervals_ms))
|
||||
timings["av_chunk_interval_ms_p95"] = (float(np.percentile(chunk_intervals_ms, 95)))
|
||||
timings["av_chunk_interval_ms_max"] = max(chunk_intervals_ms)
|
||||
if chunk_publish_ms:
|
||||
timings["av_chunk_publish_ms_median"] = float(np.median(chunk_publish_ms))
|
||||
timings["av_chunk_publish_ms_p95"] = float(np.percentile(chunk_publish_ms, 95))
|
||||
if chunk_read_ms:
|
||||
timings["av_chunk_read_ms_median"] = float(np.median(chunk_read_ms))
|
||||
timings["av_chunk_read_ms_p95"] = float(np.percentile(chunk_read_ms, 95))
|
||||
publish(StreamComplete(
|
||||
stream_id=stream_id,
|
||||
chunks=chunk_count,
|
||||
))
|
||||
return True, None
|
||||
except Exception as exc:
|
||||
return False, str(exc)
|
||||
finally:
|
||||
if proc is not None and proc.poll() is None:
|
||||
try:
|
||||
proc.kill()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
os.remove(wav_path)
|
||||
except OSError:
|
||||
pass
|
||||
@@ -0,0 +1,288 @@
|
||||
"""Benchmark the AV streaming hot-path used by dreamverse-server.
|
||||
|
||||
Measures wall-time, encoded byte volume, and chunk count for
|
||||
``av_streaming.stream_fmp4`` over synthetic frames + audio at
|
||||
production resolution. Sweeps codecs (default: ``libx264`` and
|
||||
``h264_nvenc`` if the active ffmpeg supports it) and ffmpeg presets so
|
||||
the deploy can pick a configuration that achieves a >=1.0 realtime
|
||||
ratio (5.04s of generated video produced in <=5.04s wall-time).
|
||||
|
||||
Usage::
|
||||
|
||||
python -m dreamverse.benchmarks.benchmark_av_streaming
|
||||
python -m dreamverse.benchmarks.benchmark_av_streaming \\
|
||||
--frames 121 --width 1920 --height 1088 --runs 3 \\
|
||||
--codecs libx264 h264_nvenc --x264-preset ultrafast \\
|
||||
--nvenc-preset p1
|
||||
FASTVIDEO_FFMPEG_BIN=$HOME/opt/ffmpeg-native/bin/ffmpeg \\
|
||||
python -m dreamverse.benchmarks.benchmark_av_streaming
|
||||
|
||||
Skips ``h264_nvenc`` automatically if the binary lacks the encoder. This is
|
||||
the regression guard for software-encoding overhead that can drain the
|
||||
inter-segment playback buffer.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import shutil
|
||||
import statistics
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from collections.abc import Iterable
|
||||
from dataclasses import dataclass
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
|
||||
_HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
_SERVER_ROOT = os.path.dirname(_HERE)
|
||||
if _SERVER_ROOT not in sys.path:
|
||||
sys.path.insert(0, _SERVER_ROOT)
|
||||
|
||||
from dreamverse.av_streaming import stream_fmp4 # noqa: E402
|
||||
|
||||
|
||||
@dataclass
|
||||
class BenchResult:
|
||||
codec: str
|
||||
preset: str
|
||||
runs: int
|
||||
wall_ms_min: float
|
||||
wall_ms_median: float
|
||||
wall_ms_p95: float
|
||||
wall_ms_max: float
|
||||
bytes_median: int
|
||||
chunks_median: float
|
||||
realtime_ratio_median: float
|
||||
error: str | None = None
|
||||
|
||||
|
||||
def _make_synthetic_frames(num: int, width: int, height: int, seed: int) -> list[np.ndarray]:
|
||||
rng = np.random.default_rng(seed)
|
||||
base = rng.integers(0, 255, size=(height, width, 3), dtype=np.uint8)
|
||||
out: list[np.ndarray] = []
|
||||
for i in range(num):
|
||||
f = base.copy()
|
||||
f[:, :, 0] = (f[:, :, 0].astype(np.int32) + i * 2) % 256
|
||||
out.append(f)
|
||||
return out
|
||||
|
||||
|
||||
def _make_synthetic_audio(num_frames: int, fps: int, sample_rate: int, seed: int) -> torch.Tensor:
|
||||
duration_s = num_frames / fps
|
||||
samples = int(round(duration_s * sample_rate))
|
||||
rng = np.random.default_rng(seed + 1)
|
||||
audio = (rng.uniform(-0.1, 0.1, (2, samples))).astype(np.float32)
|
||||
return torch.from_numpy(audio)
|
||||
|
||||
|
||||
def _ffmpeg_supports(codec: str, ffmpeg_bin: str) -> bool:
|
||||
try:
|
||||
out = subprocess.run(
|
||||
[ffmpeg_bin, "-hide_banner", "-encoders"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
timeout=10,
|
||||
).stdout
|
||||
except Exception:
|
||||
return False
|
||||
needle = f" {codec} "
|
||||
return any(needle in line for line in out.splitlines())
|
||||
|
||||
|
||||
def _run_one(frames: list[np.ndarray], audio: torch.Tensor, sample_rate: int, codec: str,
|
||||
preset: str) -> tuple[float, int, int, str | None]:
|
||||
timings: dict = {}
|
||||
chunks: list = []
|
||||
|
||||
def _publish(event):
|
||||
chunks.append(event)
|
||||
|
||||
prev_codec = os.environ.get("FASTVIDEO_VIDEO_CODEC")
|
||||
prev_preset = os.environ.get("FASTVIDEO_X264_PRESET")
|
||||
prev_nvenc_preset = os.environ.get("FASTVIDEO_NVENC_PRESET")
|
||||
os.environ["FASTVIDEO_VIDEO_CODEC"] = codec
|
||||
if codec.endswith("_nvenc"):
|
||||
os.environ["FASTVIDEO_NVENC_PRESET"] = preset
|
||||
else:
|
||||
os.environ["FASTVIDEO_X264_PRESET"] = preset
|
||||
|
||||
t0 = time.perf_counter()
|
||||
try:
|
||||
ok, err = stream_fmp4(
|
||||
frames=frames,
|
||||
audio=audio,
|
||||
audio_sample_rate=sample_rate,
|
||||
stream_id="bench",
|
||||
timings=timings,
|
||||
head_trim_frames=0,
|
||||
head_trim_audio_frames=0,
|
||||
shared_buffer=None,
|
||||
shared_buffer_bytes=0,
|
||||
publish=_publish,
|
||||
log_prefix="[bench]",
|
||||
)
|
||||
finally:
|
||||
if prev_codec is None:
|
||||
os.environ.pop("FASTVIDEO_VIDEO_CODEC", None)
|
||||
else:
|
||||
os.environ["FASTVIDEO_VIDEO_CODEC"] = prev_codec
|
||||
if prev_preset is None:
|
||||
os.environ.pop("FASTVIDEO_X264_PRESET", None)
|
||||
else:
|
||||
os.environ["FASTVIDEO_X264_PRESET"] = prev_preset
|
||||
if prev_nvenc_preset is None:
|
||||
os.environ.pop("FASTVIDEO_NVENC_PRESET", None)
|
||||
else:
|
||||
os.environ["FASTVIDEO_NVENC_PRESET"] = prev_nvenc_preset
|
||||
wall_ms = (time.perf_counter() - t0) * 1000.0
|
||||
if not ok:
|
||||
return wall_ms, 0, 0, err or "stream_fmp4 returned False"
|
||||
total_bytes = int(timings.get("av_stream_bytes", 0))
|
||||
return wall_ms, total_bytes, len(chunks), None
|
||||
|
||||
|
||||
def benchmark(codecs: Iterable[str], runs: int, frames_n: int, width: int, height: int, fps: int, sample_rate: int,
|
||||
x264_preset: str, nvenc_preset: str, seed: int, ffmpeg_bin: str) -> list[BenchResult]:
|
||||
print(f"[bench] ffmpeg_bin={ffmpeg_bin}")
|
||||
print(f"[bench] frames={frames_n} {width}x{height} fps={fps} "
|
||||
f"audio_sr={sample_rate} runs/codec={runs}")
|
||||
frames = _make_synthetic_frames(frames_n, width, height, seed)
|
||||
audio = _make_synthetic_audio(frames_n, fps, sample_rate, seed)
|
||||
playable_s = frames_n / fps
|
||||
print(f"[bench] playable={playable_s:.3f}s "
|
||||
f"(realtime_ratio = playable / wall_time; >= 1.0 means no "
|
||||
f"buffer drain)")
|
||||
|
||||
results: list[BenchResult] = []
|
||||
for codec in codecs:
|
||||
preset = nvenc_preset if codec.endswith("_nvenc") else x264_preset
|
||||
if not _ffmpeg_supports(codec, ffmpeg_bin):
|
||||
results.append(
|
||||
BenchResult(codec=codec,
|
||||
preset=preset,
|
||||
runs=0,
|
||||
wall_ms_min=0,
|
||||
wall_ms_median=0,
|
||||
wall_ms_p95=0,
|
||||
wall_ms_max=0,
|
||||
bytes_median=0,
|
||||
chunks_median=0,
|
||||
realtime_ratio_median=0,
|
||||
error=f"{codec} not in ffmpeg"))
|
||||
continue
|
||||
walls: list[float] = []
|
||||
sizes: list[int] = []
|
||||
chunkcounts: list[int] = []
|
||||
last_err: str | None = None
|
||||
for run in range(runs):
|
||||
wall_ms, total_bytes, chunk_count, err = _run_one(frames, audio, sample_rate, codec, preset)
|
||||
print(f"[bench] codec={codec:12s} preset={preset:9s} "
|
||||
f"run={run + 1}/{runs} wall={wall_ms:7.1f}ms "
|
||||
f"bytes={total_bytes:>9d} chunks={chunk_count:>3d} "
|
||||
f"realtime={playable_s / (wall_ms / 1000.0):5.2f}x"
|
||||
f"{' ERR=' + err if err else ''}")
|
||||
if err is not None:
|
||||
last_err = err
|
||||
continue
|
||||
walls.append(wall_ms)
|
||||
sizes.append(total_bytes)
|
||||
chunkcounts.append(chunk_count)
|
||||
if not walls:
|
||||
results.append(
|
||||
BenchResult(codec=codec,
|
||||
preset=preset,
|
||||
runs=0,
|
||||
wall_ms_min=0,
|
||||
wall_ms_median=0,
|
||||
wall_ms_p95=0,
|
||||
wall_ms_max=0,
|
||||
bytes_median=0,
|
||||
chunks_median=0,
|
||||
realtime_ratio_median=0,
|
||||
error=last_err or "all runs failed"))
|
||||
continue
|
||||
walls_sorted = sorted(walls)
|
||||
p95_idx = max(0, int(round(0.95 * (len(walls_sorted) - 1))))
|
||||
wall_med = statistics.median(walls)
|
||||
results.append(
|
||||
BenchResult(
|
||||
codec=codec,
|
||||
preset=preset,
|
||||
runs=len(walls),
|
||||
wall_ms_min=min(walls),
|
||||
wall_ms_median=wall_med,
|
||||
wall_ms_p95=walls_sorted[p95_idx],
|
||||
wall_ms_max=max(walls),
|
||||
bytes_median=int(statistics.median(sizes)),
|
||||
chunks_median=statistics.median(chunkcounts),
|
||||
realtime_ratio_median=playable_s / (wall_med / 1000.0),
|
||||
))
|
||||
return results
|
||||
|
||||
|
||||
def _print_summary(results: list[BenchResult]) -> None:
|
||||
print()
|
||||
print("=== summary ===")
|
||||
header = (f"{'codec':14s} {'preset':10s} {'runs':>4s} "
|
||||
f"{'wall_med_ms':>11s} {'wall_p95_ms':>11s} "
|
||||
f"{'bytes_med':>10s} {'realtime':>8s} notes")
|
||||
print(header)
|
||||
print("-" * len(header))
|
||||
for r in results:
|
||||
if r.error is not None:
|
||||
print(f"{r.codec:14s} {r.preset:10s} {r.runs:>4d} "
|
||||
f"{'-':>11s} {'-':>11s} {'-':>10s} {'-':>8s} "
|
||||
f"ERR: {r.error}")
|
||||
continue
|
||||
print(f"{r.codec:14s} {r.preset:10s} {r.runs:>4d} "
|
||||
f"{r.wall_ms_median:>11.1f} {r.wall_ms_p95:>11.1f} "
|
||||
f"{r.bytes_median:>10d} {r.realtime_ratio_median:>7.2f}x "
|
||||
f"{'OK' if r.realtime_ratio_median >= 1.0 else 'BUFFER DRAINS'}")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
p.add_argument("--frames", type=int, default=121, help="num frames (default: 121, matches NUM_FRAMES)")
|
||||
p.add_argument("--width", type=int, default=1920)
|
||||
p.add_argument("--height", type=int, default=1088)
|
||||
p.add_argument("--fps", type=int, default=24)
|
||||
p.add_argument("--sample-rate", type=int, default=24000)
|
||||
p.add_argument("--runs", type=int, default=3, help="runs per codec (default: 3 — 1 warmup + 2 timed in median)")
|
||||
p.add_argument("--codecs",
|
||||
nargs="+",
|
||||
default=["libx264", "h264_nvenc"],
|
||||
help="codecs to benchmark; missing ones are skipped")
|
||||
p.add_argument("--x264-preset", default="ultrafast")
|
||||
p.add_argument("--nvenc-preset", default="p1")
|
||||
p.add_argument("--seed", type=int, default=0)
|
||||
args = p.parse_args()
|
||||
|
||||
ffmpeg_bin = shutil.which(os.getenv("FASTVIDEO_FFMPEG_BIN", "ffmpeg"))
|
||||
if ffmpeg_bin is None:
|
||||
print("ffmpeg not found", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
results = benchmark(
|
||||
codecs=args.codecs,
|
||||
runs=args.runs,
|
||||
frames_n=args.frames,
|
||||
width=args.width,
|
||||
height=args.height,
|
||||
fps=args.fps,
|
||||
sample_rate=args.sample_rate,
|
||||
x264_preset=args.x264_preset,
|
||||
nvenc_preset=args.nvenc_preset,
|
||||
seed=args.seed,
|
||||
ffmpeg_bin=ffmpeg_bin,
|
||||
)
|
||||
_print_summary(results)
|
||||
any_below = any(r.error is None and r.realtime_ratio_median < 1.0 for r in results)
|
||||
return 1 if any_below else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,368 @@
|
||||
"""Benchmark the LTX-2 generation pipeline driven by the dreamverse Python SDK path.
|
||||
|
||||
Mirrors how ``apps/dreamverse/dreamverse/ltx2_generation.py`` constructs
|
||||
``GeneratorConfig`` and calls ``VideoGenerator.generate()``, then
|
||||
captures per-stage timings via the ``FASTVIDEO_STAGE_LOGGING=1`` log
|
||||
hooks (same mechanism as ``FastVideo-internal/examples/inference/basic/
|
||||
basic_ltx2_distilled_i2v_two_stage_time.py``).
|
||||
|
||||
Reports for each scenario:
|
||||
* Total wall-time (median, p95)
|
||||
* Per-stage execution_time (input_validation_stage,
|
||||
prompt_encoding_stage, ltx2_refine_init_stage, latent_preparation_stage,
|
||||
denoising_stage, ltx2_upsample_stage, ltx2_refine_lora_stage,
|
||||
ltx2_refine_denoising_stage, audio_decoding_stage, decoding_stage)
|
||||
* Realtime ratio (frames / fps / wall_time; >= 1.0 = no buffer drain)
|
||||
* Peak GPU memory
|
||||
|
||||
Sweep scenarios (default):
|
||||
* compile=False, warmup=False → cold inference baseline
|
||||
* compile=True, warmup=False → JIT compile mid-run
|
||||
* compile=True, warmup=True → fully warmed (production mode)
|
||||
|
||||
Skips compile / NVENC if the host lacks support. Does NOT exercise the
|
||||
AV streaming path (use ``benchmark_av_streaming.py`` for that).
|
||||
|
||||
Usage::
|
||||
|
||||
python -m dreamverse.benchmarks.benchmark_pipeline
|
||||
python -m dreamverse.benchmarks.benchmark_pipeline \\
|
||||
--runs 3 --scenarios compile_warm cold --gpu 4
|
||||
|
||||
This benchmark is the source of truth for the pipeline timing breakdown used
|
||||
when investigating inter-segment buffer drain.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import contextlib
|
||||
import json
|
||||
import os
|
||||
import statistics
|
||||
import time
|
||||
from collections import OrderedDict
|
||||
from dataclasses import asdict, dataclass
|
||||
|
||||
if "FASTVIDEO_STAGE_LOGGING" not in os.environ:
|
||||
os.environ["FASTVIDEO_STAGE_LOGGING"] = "1"
|
||||
if "FASTVIDEO_ATTENTION_BACKEND" not in os.environ:
|
||||
os.environ["FASTVIDEO_ATTENTION_BACKEND"] = "FLASH_ATTN"
|
||||
|
||||
import torch # noqa: E402
|
||||
|
||||
from fastvideo import VideoGenerator # noqa: E402
|
||||
from fastvideo.api import ( # noqa: E402
|
||||
ComponentConfig, CompileConfig, EngineConfig, GeneratorConfig, OffloadConfig, PipelineSelection, QuantizationConfig,
|
||||
)
|
||||
|
||||
DEFAULT_PROMPT = ("A cinematic drone shot over coastal cliffs at sunrise, golden "
|
||||
"light, gentle ocean waves, ultra detailed")
|
||||
DEFAULT_MODEL = "FastVideo/LTX2-Distilled-Diffusers"
|
||||
|
||||
|
||||
@dataclass
|
||||
class ScenarioConfig:
|
||||
name: str
|
||||
enable_compile: bool
|
||||
do_warmup: bool
|
||||
nvenc: bool = False
|
||||
|
||||
|
||||
DEFAULT_SCENARIOS: list[ScenarioConfig] = [
|
||||
ScenarioConfig(name="cold", enable_compile=False, do_warmup=False),
|
||||
ScenarioConfig(name="compile_cold", enable_compile=True, do_warmup=False),
|
||||
ScenarioConfig(name="compile_warm", enable_compile=True, do_warmup=True),
|
||||
]
|
||||
|
||||
|
||||
@dataclass
|
||||
class RunResult:
|
||||
wall_ms: float
|
||||
stage_times_ms: OrderedDict[str, float]
|
||||
peak_gpu_mb: float
|
||||
error: str | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class ScenarioResult:
|
||||
name: str
|
||||
enable_compile: bool
|
||||
do_warmup: bool
|
||||
runs: int
|
||||
wall_ms_median: float
|
||||
wall_ms_p95: float
|
||||
stage_means_ms: OrderedDict[str, float]
|
||||
realtime_ratio_median: float
|
||||
peak_gpu_mb_max: float
|
||||
error: str | None = None
|
||||
|
||||
|
||||
def _build_generator_config(model_path: str, enable_compile: bool, num_gpus: int) -> GeneratorConfig:
|
||||
components = ComponentConfig(config_root=model_path)
|
||||
return GeneratorConfig(
|
||||
model_path=model_path,
|
||||
engine=EngineConfig(
|
||||
num_gpus=num_gpus,
|
||||
offload=OffloadConfig(dit=False, dit_layerwise=False, text_encoder=False, vae=False, pin_cpu_memory=True),
|
||||
compile=CompileConfig(enabled=enable_compile,
|
||||
text_encoder_enabled=enable_compile,
|
||||
backend="inductor",
|
||||
fullgraph=True,
|
||||
mode="max-autotune-no-cudagraphs",
|
||||
dynamic=False),
|
||||
use_fsdp_inference=False,
|
||||
# The bundled LTX2 model enables a refinement LoRA during the
|
||||
# first request. NVFP4 otherwise purges the dense weights that
|
||||
# FastVideo's LoRA merge path requires.
|
||||
quantization=QuantizationConfig(
|
||||
transformer_quant="NVFP4",
|
||||
transformer_retain_original_weights=True,
|
||||
),
|
||||
),
|
||||
pipeline=PipelineSelection(
|
||||
components=components,
|
||||
vae_tiling=False,
|
||||
preset_overrides={
|
||||
"refine": {
|
||||
"enabled": True,
|
||||
"num_inference_steps": 2,
|
||||
"guidance_scale": 1.0,
|
||||
"add_noise": True,
|
||||
},
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _extract_stage_times(result: dict) -> OrderedDict[str, float]:
|
||||
out: OrderedDict[str, float] = OrderedDict()
|
||||
info = result.get("logging_info") if isinstance(result, dict) else None
|
||||
if info is None:
|
||||
return out
|
||||
stages = getattr(info, "stages", None)
|
||||
if not stages:
|
||||
return out
|
||||
for name, metrics in stages.items():
|
||||
exec_time = float(metrics.get("execution_time", 0.0))
|
||||
out[name] = exec_time * 1000.0
|
||||
return out
|
||||
|
||||
|
||||
def _peak_gpu_mb() -> float:
|
||||
if not torch.cuda.is_available():
|
||||
return 0.0
|
||||
try:
|
||||
return torch.cuda.max_memory_allocated() / (1024 * 1024)
|
||||
except Exception:
|
||||
return 0.0
|
||||
|
||||
|
||||
def _reset_peak_gpu() -> None:
|
||||
if torch.cuda.is_available():
|
||||
with contextlib.suppress(Exception):
|
||||
torch.cuda.reset_peak_memory_stats()
|
||||
|
||||
|
||||
def _do_one_run(generator: VideoGenerator, prompt: str, *, height: int, width: int, num_frames: int, seed: int,
|
||||
num_inference_steps: int) -> RunResult:
|
||||
_reset_peak_gpu()
|
||||
t0 = time.perf_counter()
|
||||
try:
|
||||
result = generator.generate_video(
|
||||
prompt=prompt,
|
||||
negative_prompt="",
|
||||
save_video=False,
|
||||
height=height,
|
||||
width=width,
|
||||
num_frames=num_frames,
|
||||
fps=24,
|
||||
num_inference_steps=num_inference_steps,
|
||||
guidance_scale=1.0,
|
||||
seed=seed,
|
||||
ltx2_image_crf=0.0,
|
||||
)
|
||||
if torch.cuda.is_available():
|
||||
torch.cuda.synchronize()
|
||||
except Exception as exc:
|
||||
return RunResult(wall_ms=(time.perf_counter() - t0) * 1000.0,
|
||||
stage_times_ms=OrderedDict(),
|
||||
peak_gpu_mb=0.0,
|
||||
error=f"{type(exc).__name__}: {exc}")
|
||||
wall_ms = (time.perf_counter() - t0) * 1000.0
|
||||
return RunResult(
|
||||
wall_ms=wall_ms,
|
||||
stage_times_ms=_extract_stage_times(result),
|
||||
peak_gpu_mb=_peak_gpu_mb(),
|
||||
)
|
||||
|
||||
|
||||
def benchmark_scenario(scenario: ScenarioConfig, model_path: str, num_gpus: int, prompt: str, num_runs: int,
|
||||
num_frames: int, height: int, width: int, num_inference_steps: int, seed: int) -> ScenarioResult:
|
||||
print()
|
||||
print(f"=== scenario: {scenario.name} "
|
||||
f"(compile={scenario.enable_compile} warmup={scenario.do_warmup}) ===")
|
||||
|
||||
config = _build_generator_config(model_path, scenario.enable_compile, num_gpus)
|
||||
generator = VideoGenerator.from_config(config)
|
||||
|
||||
if scenario.do_warmup:
|
||||
print(f"[{scenario.name}] warmup: 2 generate calls "
|
||||
"(triggers compile + first-shape graphs)")
|
||||
for warmup_idx in range(2):
|
||||
t0 = time.perf_counter()
|
||||
_do_one_run(generator,
|
||||
prompt,
|
||||
height=height,
|
||||
width=width,
|
||||
num_frames=num_frames,
|
||||
seed=seed + 100 + warmup_idx,
|
||||
num_inference_steps=num_inference_steps)
|
||||
print(f"[{scenario.name}] warmup {warmup_idx + 1}: "
|
||||
f"{(time.perf_counter() - t0) * 1000:.0f}ms")
|
||||
|
||||
runs: list[RunResult] = []
|
||||
last_error: str | None = None
|
||||
for run_idx in range(num_runs):
|
||||
result = _do_one_run(generator,
|
||||
prompt,
|
||||
height=height,
|
||||
width=width,
|
||||
num_frames=num_frames,
|
||||
seed=seed + run_idx,
|
||||
num_inference_steps=num_inference_steps)
|
||||
if result.error is not None:
|
||||
print(f"[{scenario.name}] run {run_idx + 1}: "
|
||||
f"ERROR {result.error}")
|
||||
last_error = result.error
|
||||
continue
|
||||
playable_s = num_frames / 24.0
|
||||
rt = playable_s / (result.wall_ms / 1000.0)
|
||||
print(f"[{scenario.name}] run {run_idx + 1}/{num_runs}: "
|
||||
f"wall={result.wall_ms:.0f}ms peak={result.peak_gpu_mb:.0f}MB "
|
||||
f"realtime={rt:.2f}x stages={len(result.stage_times_ms)}")
|
||||
runs.append(result)
|
||||
|
||||
if not runs:
|
||||
return ScenarioResult(name=scenario.name,
|
||||
enable_compile=scenario.enable_compile,
|
||||
do_warmup=scenario.do_warmup,
|
||||
runs=0,
|
||||
wall_ms_median=0.0,
|
||||
wall_ms_p95=0.0,
|
||||
stage_means_ms=OrderedDict(),
|
||||
realtime_ratio_median=0.0,
|
||||
peak_gpu_mb_max=0.0,
|
||||
error=last_error or "all runs failed")
|
||||
|
||||
walls = [r.wall_ms for r in runs]
|
||||
walls_sorted = sorted(walls)
|
||||
p95_idx = max(0, int(round(0.95 * (len(walls_sorted) - 1))))
|
||||
stage_means: OrderedDict[str, float] = OrderedDict()
|
||||
if runs:
|
||||
for stage_name in runs[0].stage_times_ms:
|
||||
vals = [r.stage_times_ms.get(stage_name, 0.0) for r in runs]
|
||||
stage_means[stage_name] = sum(vals) / len(vals)
|
||||
return ScenarioResult(
|
||||
name=scenario.name,
|
||||
enable_compile=scenario.enable_compile,
|
||||
do_warmup=scenario.do_warmup,
|
||||
runs=len(runs),
|
||||
wall_ms_median=statistics.median(walls),
|
||||
wall_ms_p95=walls_sorted[p95_idx],
|
||||
stage_means_ms=stage_means,
|
||||
realtime_ratio_median=(num_frames / 24.0) / (statistics.median(walls) / 1000.0),
|
||||
peak_gpu_mb_max=max(r.peak_gpu_mb for r in runs),
|
||||
)
|
||||
|
||||
|
||||
def _print_summary(results: list[ScenarioResult]) -> None:
|
||||
print()
|
||||
print("=== summary ===")
|
||||
header = (f"{'scenario':14s} {'runs':>4s} "
|
||||
f"{'wall_med_ms':>11s} {'wall_p95_ms':>11s} "
|
||||
f"{'peak_mb':>8s} {'realtime':>8s}")
|
||||
print(header)
|
||||
print("-" * len(header))
|
||||
for r in results:
|
||||
if r.error is not None:
|
||||
print(f"{r.name:14s} {r.runs:>4d} ERR: {r.error}")
|
||||
continue
|
||||
print(f"{r.name:14s} {r.runs:>4d} "
|
||||
f"{r.wall_ms_median:>11.0f} {r.wall_ms_p95:>11.0f} "
|
||||
f"{r.peak_gpu_mb_max:>8.0f} {r.realtime_ratio_median:>7.2f}x")
|
||||
for r in results:
|
||||
if r.error is not None or not r.stage_means_ms:
|
||||
continue
|
||||
print()
|
||||
print(f"=== {r.name} per-stage means (ms) ===")
|
||||
for stage, mean_ms in r.stage_means_ms.items():
|
||||
pct = 100.0 * mean_ms / r.wall_ms_median if r.wall_ms_median > 0 \
|
||||
else 0.0
|
||||
print(f" {stage:35s} {mean_ms:>9.1f}ms ({pct:5.1f}%)")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
p.add_argument("--model", default=DEFAULT_MODEL)
|
||||
p.add_argument("--prompt", default=DEFAULT_PROMPT)
|
||||
p.add_argument("--scenarios",
|
||||
nargs="+",
|
||||
default=[s.name for s in DEFAULT_SCENARIOS],
|
||||
choices=[s.name for s in DEFAULT_SCENARIOS],
|
||||
help="which scenarios to run")
|
||||
p.add_argument("--runs", type=int, default=3)
|
||||
p.add_argument("--num-frames", type=int, default=121)
|
||||
p.add_argument("--height", type=int, default=1088)
|
||||
p.add_argument("--width", type=int, default=1920)
|
||||
p.add_argument("--num-inference-steps", type=int, default=5)
|
||||
p.add_argument("--num-gpus", type=int, default=1)
|
||||
p.add_argument("--gpu", type=int, default=None, help="set CUDA_VISIBLE_DEVICES to this single GPU index")
|
||||
p.add_argument("--seed", type=int, default=10)
|
||||
p.add_argument("--output-json", default=None, help="write structured results to this path")
|
||||
args = p.parse_args()
|
||||
|
||||
if args.gpu is not None:
|
||||
os.environ["CUDA_VISIBLE_DEVICES"] = str(args.gpu)
|
||||
|
||||
selected = [s for s in DEFAULT_SCENARIOS if s.name in args.scenarios]
|
||||
print(f"[bench] model={args.model} num_gpus={args.num_gpus}")
|
||||
print(f"[bench] frames={args.num_frames} {args.width}x{args.height} "
|
||||
f"steps={args.num_inference_steps} runs/scenario={args.runs}")
|
||||
print(f"[bench] scenarios={[s.name for s in selected]}")
|
||||
|
||||
results: list[ScenarioResult] = []
|
||||
for scenario in selected:
|
||||
try:
|
||||
result = benchmark_scenario(scenario, args.model, args.num_gpus, args.prompt, args.runs, args.num_frames,
|
||||
args.height, args.width, args.num_inference_steps, args.seed)
|
||||
except Exception as exc:
|
||||
result = ScenarioResult(name=scenario.name,
|
||||
enable_compile=scenario.enable_compile,
|
||||
do_warmup=scenario.do_warmup,
|
||||
runs=0,
|
||||
wall_ms_median=0.0,
|
||||
wall_ms_p95=0.0,
|
||||
stage_means_ms=OrderedDict(),
|
||||
realtime_ratio_median=0.0,
|
||||
peak_gpu_mb_max=0.0,
|
||||
error=f"{type(exc).__name__}: {exc}")
|
||||
results.append(result)
|
||||
|
||||
_print_summary(results)
|
||||
|
||||
if args.output_json:
|
||||
out = []
|
||||
for r in results:
|
||||
entry = asdict(r)
|
||||
entry["stage_means_ms"] = dict(r.stage_means_ms)
|
||||
out.append(entry)
|
||||
with open(args.output_json, "w") as f:
|
||||
json.dump({"args": vars(args), "results": out}, f, indent=2)
|
||||
print(f"[bench] wrote {args.output_json}")
|
||||
|
||||
any_below = any(r.error is None and r.realtime_ratio_median < 1.0 for r in results)
|
||||
return 1 if any_below else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user