Compare commits
532
Commits
pq_dev
...
khluu-patch-2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5bdc2fa345 | ||
|
|
881c675087 | ||
|
|
d05212cd5f | ||
|
|
66457c39fa | ||
|
|
c9786c5c0c | ||
|
|
3eb3aa54ad | ||
|
|
bc26de834e | ||
|
|
ed08a216c1 | ||
|
|
9913c00144 | ||
|
|
a2d5b61703 | ||
|
|
caed95fb45 | ||
|
|
904b97d5bd | ||
|
|
986b356077 | ||
|
|
22c8cde328 | ||
|
|
1ae2e1e4c4 | ||
|
|
5e7974c15b | ||
|
|
919ad28da5 | ||
|
|
315daeba8c | ||
|
|
8e950daf6a | ||
|
|
97f4c5b1e7 | ||
|
|
1eebb374f1 | ||
|
|
466a7f7a62 | ||
|
|
43939df2a5 | ||
|
|
1e86404456 | ||
|
|
bf9754a8b1 | ||
|
|
296d995ec2 | ||
|
|
4de077ea62 | ||
|
|
82e1bf2804 | ||
|
|
c0908dd3fa | ||
|
|
a8cec43ce6 | ||
|
|
36b8f80266 | ||
|
|
06d347b47f | ||
|
|
af11b02081 | ||
|
|
1589931fa6 | ||
|
|
7b02b856a6 | ||
|
|
d6a3551a62 | ||
|
|
40f72bb870 | ||
|
|
0ff443f069 | ||
|
|
5722333fba | ||
|
|
b42287d14b | ||
|
|
a3f2d4cbfe | ||
|
|
267170115b | ||
|
|
d8a32a6a87 | ||
|
|
6c85ddcb96 | ||
|
|
efbe4116ff | ||
|
|
5295f0f81b | ||
|
|
4f9731c920 | ||
|
|
d2f015edce | ||
|
|
0b577a7263 | ||
|
|
a42b7487b0 | ||
|
|
75770c9309 | ||
|
|
630e84ef93 | ||
|
|
f4867ec175 | ||
|
|
00bd07b82b | ||
|
|
36a79717c6 | ||
|
|
c6f314df4f | ||
|
|
b16965dfa7 | ||
|
|
9994594782 | ||
|
|
e427da022b | ||
|
|
b0a5ffa100 | ||
|
|
17e238c7ea | ||
|
|
8bf849c399 | ||
|
|
f117a07271 | ||
|
|
ab1acb27ff | ||
|
|
71c9e77606 | ||
|
|
9c00852d85 | ||
|
|
a1aed18b80 | ||
|
|
0b56fba7b3 | ||
|
|
20a8fe1dcc | ||
|
|
82fd2c3637 | ||
|
|
830d261f9d | ||
|
|
f0bca6f66b | ||
|
|
5fb7d137c7 | ||
|
|
8e3dce781e | ||
|
|
dd302b63c4 | ||
|
|
a854f0b7ff | ||
|
|
963b64e7ae | ||
|
|
dc8b28e0aa | ||
|
|
8a9644c821 | ||
|
|
8228b5a8fe | ||
|
|
e192b0a6c5 | ||
|
|
a05aa1dd6a | ||
|
|
43e8747c00 | ||
|
|
f55ce9c793 | ||
|
|
ee624d0686 | ||
|
|
109bb97b2c | ||
|
|
acfe99e4d9 | ||
|
|
9327cccde6 | ||
|
|
f73fa1fbe7 | ||
|
|
4968702950 | ||
|
|
5fea4823b7 | ||
|
|
26ba1e4de8 | ||
|
|
b81e0a8abf | ||
|
|
9af6fb9a1f | ||
|
|
ac397ae2a0 | ||
|
|
d3ea94309d | ||
|
|
cdde401813 | ||
|
|
29e6a29e54 | ||
|
|
c6b526bc2e | ||
|
|
2538ae6332 | ||
|
|
59024bee62 | ||
|
|
01e0a8f284 | ||
|
|
2f97db17a3 | ||
|
|
accf334112 | ||
|
|
53a44c56fe | ||
|
|
a43dbdc064 | ||
|
|
87847a214a | ||
|
|
10f841bb98 | ||
|
|
11a5a914ca | ||
|
|
285d71b26a | ||
|
|
9f00a37437 | ||
|
|
ffbc1305e6 | ||
|
|
78a5aae299 | ||
|
|
c4933ec2aa | ||
|
|
398ae95b96 | ||
|
|
8135ee81e6 | ||
|
|
d1cf4c4c0f | ||
|
|
f1960d8dfd | ||
|
|
38ec7eda90 | ||
|
|
ab657ce374 | ||
|
|
959b3ca8c8 | ||
|
|
f95f8e936d | ||
|
|
176fa4bc6e | ||
|
|
efce272cb4 | ||
|
|
117d9bd122 | ||
|
|
78830562db | ||
|
|
7672298075 | ||
|
|
5304bc1e16 | ||
|
|
0aa610d82a | ||
|
|
6c19f3e386 | ||
|
|
4d8e29075a | ||
|
|
3bc3c95708 | ||
|
|
ce1ae26084 | ||
|
|
d6f93b0f37 | ||
|
|
7fcbaa794c | ||
|
|
3fb3c8aba4 | ||
|
|
70ef548448 | ||
|
|
9494d691bf | ||
|
|
47b11c6b13 | ||
|
|
dc6177520e | ||
|
|
df3b347cd9 | ||
|
|
de2cac95e8 | ||
|
|
81774fd933 | ||
|
|
de0a8b057b | ||
|
|
0f0e94760c | ||
|
|
ed89c8b043 | ||
|
|
70a5de9dd4 | ||
|
|
722a69e8d1 | ||
|
|
2e6895ff0e | ||
|
|
07f4c3fdd6 | ||
|
|
e503a41941 | ||
|
|
425952e7d3 | ||
|
|
23cf48d7bb | ||
|
|
8ad39570b8 | ||
|
|
f6cfc0d1e3 | ||
|
|
4eeea68cff | ||
|
|
be835c4483 | ||
|
|
e6b9d52905 | ||
|
|
2923d5a91e | ||
|
|
be580d5c24 | ||
|
|
2c294a750b | ||
|
|
790e34e375 | ||
|
|
7bfd3c069d | ||
|
|
c7fb59fd28 | ||
|
|
0e07eb6ff9 | ||
|
|
47a0969976 | ||
|
|
2c4974c6ea | ||
|
|
3eecd2057b | ||
|
|
bfbf3e5b69 | ||
|
|
c7f89ef085 | ||
|
|
a909d36415 | ||
|
|
f8dd566abd | ||
|
|
c48a9f33ad | ||
|
|
ee0f7a7797 | ||
|
|
0c8b5ac2d5 | ||
|
|
dd518973de | ||
|
|
5b8ade8446 | ||
|
|
def39561ae | ||
|
|
cac6504adf | ||
|
|
8306c91ea9 | ||
|
|
43c6f520ca | ||
|
|
2ac141026b | ||
|
|
d4190115f7 | ||
|
|
144fd6e1e0 | ||
|
|
20e7310eb1 | ||
|
|
741f7e2826 | ||
|
|
c9392dff01 | ||
|
|
b11d436e19 | ||
|
|
c0beebbfe3 | ||
|
|
cb48968294 | ||
|
|
91a3d465ee | ||
|
|
6fb4058566 | ||
|
|
ae96aa8a8b | ||
|
|
d64bbde561 | ||
|
|
9ece725fc5 | ||
|
|
c4220f0979 | ||
|
|
1c8fcf436f | ||
|
|
e8aa32b82b | ||
|
|
40cbd8ef7f | ||
|
|
9bca8880fe | ||
|
|
d2b3a24af3 | ||
|
|
2f5e32477c | ||
|
|
16be03b909 | ||
|
|
2cc4375df8 | ||
|
|
46e96e18e3 | ||
|
|
cadd77262d | ||
|
|
8efd912d32 | ||
|
|
50b44303f1 | ||
|
|
d12d3d711b | ||
|
|
77ff875170 | ||
|
|
4a3a1b6de8 | ||
|
|
17e79336aa | ||
|
|
1fe8e71433 | ||
|
|
5037af1033 | ||
|
|
4f3fd4de37 | ||
|
|
978a290d6e | ||
|
|
52bf1e2f40 | ||
|
|
578ed1967e | ||
|
|
7f0825832a | ||
|
|
06c9d9b463 | ||
|
|
f9c69a8c40 | ||
|
|
3d9cd16df7 | ||
|
|
28a6597c4c | ||
|
|
21ee1ff6e6 | ||
|
|
5d9918fa67 | ||
|
|
34c9d8f2a2 | ||
|
|
8f4f091387 | ||
|
|
28d13e0dee | ||
|
|
b8499e4839 | ||
|
|
75dddcb2b5 | ||
|
|
e3b6254090 | ||
|
|
7f821be57b | ||
|
|
a54c323952 | ||
|
|
0df8e80574 | ||
|
|
f266b04534 | ||
|
|
c9d7cd189f | ||
|
|
4869311f2f | ||
|
|
58efc5dde3 | ||
|
|
0aa72b952f | ||
|
|
c3894585b5 | ||
|
|
b564132e44 | ||
|
|
6d5ceaebcc | ||
|
|
72a8553310 | ||
|
|
25b08b3371 | ||
|
|
334d306d9a | ||
|
|
d2a72f380f | ||
|
|
9f552d010b | ||
|
+13 |
776c3a794e | ||
|
|
a578a376d5 | ||
|
|
8825730b98 | ||
|
|
430c60b9b5 | ||
|
|
a1c37cc6e3 | ||
|
|
3fc4f988ea | ||
|
|
5e7035e183 | ||
|
|
36c28767cd | ||
|
|
bb24e07a7a | ||
|
|
0888520875 | ||
|
|
4e23bfff9e | ||
|
|
3fb6adc40a | ||
|
|
a7f9926013 | ||
|
|
43ec7f701b | ||
|
|
35f994e255 | ||
|
|
b5cf8806f0 | ||
|
|
4203bfa350 | ||
|
|
aaea77a180 | ||
|
|
2d5faf3634 | ||
|
|
314bb4e324 | ||
|
|
1fe64e85a6 | ||
|
|
e213bdd0a4 | ||
|
|
1444e1fc45 | ||
|
|
e9a1beec77 | ||
|
|
d7cd00e26c | ||
|
|
f7611198ae | ||
|
|
8e20b33b38 | ||
|
|
9cdf592782 | ||
|
|
7e14c943d5 | ||
|
|
779f5987d9 | ||
|
|
2e13bddf87 | ||
|
|
8c7f704c23 | ||
|
|
2c92b63cce | ||
|
|
61ddeef9f9 | ||
|
|
19887997eb | ||
|
|
36c7b0148f | ||
|
|
f25cd37f93 | ||
|
|
d0836d8459 | ||
|
|
67c0a17a0e | ||
|
|
dde96c2468 | ||
|
|
0ec2da08de | ||
|
|
f3741ac2b9 | ||
|
|
9c2f7467a0 | ||
|
|
65b55d1c83 | ||
|
|
f26219162b | ||
|
|
978e3ad7d4 | ||
|
|
1467b3e2c7 | ||
|
|
c784b3e07d | ||
|
|
cddcc400d3 | ||
|
|
4c72b67444 | ||
|
|
bb13467adc | ||
|
|
8c1259383c | ||
|
|
4df794bdaa | ||
|
|
89f47c910a | ||
|
|
27b804cea7 | ||
|
|
0cb7ec62da | ||
|
|
75cdf1cb4e | ||
|
|
aaba243832 | ||
|
|
14c04d0fd4 | ||
|
|
c32235c4e0 | ||
|
|
b7e63e07d7 | ||
|
|
aa5b628b6a | ||
|
|
b60030092c | ||
|
|
695e62e2b9 | ||
|
|
c3a638e8b6 | ||
|
|
026f613e57 | ||
|
|
b414a4d15e | ||
|
|
c9a4683a1a | ||
|
|
4a26b2eca9 | ||
|
|
535b1a93f5 | ||
|
|
e7eeb54e08 | ||
|
|
52afcb6055 | ||
|
|
927d952405 | ||
|
|
f9b8bfd7bb | ||
|
|
4cc28275f9 | ||
|
|
ec8ff95e22 | ||
|
|
d430beef76 | ||
|
|
cd623598b1 | ||
|
|
ad3011dcaf | ||
|
|
6b18d31aaa | ||
|
|
cb7361f021 | ||
|
|
b076941a9d | ||
|
|
be0fca9a4c | ||
|
|
0a2b5e87b6 | ||
|
|
3c2661da16 | ||
|
|
61ac92a84d | ||
|
|
1c9e58ee19 | ||
|
|
7961eb6a02 | ||
|
|
4321b42e47 | ||
|
|
fe509c0c10 | ||
|
|
fd52647028 | ||
|
|
667f6cf25d | ||
|
|
d7062550d7 | ||
|
|
c1ebb60747 | ||
|
|
4f8cb95571 | ||
|
|
680742bc33 | ||
|
|
9c7de5373a | ||
|
|
c94ce4d85f | ||
|
|
9850f60743 | ||
|
|
46afa046ce | ||
|
|
ad0224f2e6 | ||
|
|
6f8deb4a5a | ||
|
|
7c7636f6bb | ||
|
|
23bf317cc5 | ||
|
|
5842a74815 | ||
|
|
5b90ddf099 | ||
|
|
ef01223c42 | ||
|
|
380c43eb86 | ||
|
|
2eebd22e32 | ||
|
|
194fa159a3 | ||
|
|
f0b8d3db82 | ||
|
|
51c0303945 | ||
|
|
1ae0c64fe0 | ||
|
|
1753a878e0 | ||
|
|
59ae174bba | ||
|
|
099a36dfdc | ||
|
|
ebe37e9f57 | ||
|
|
0aedbf4a18 | ||
|
|
60143adc52 | ||
|
|
be81443c8a | ||
|
|
df4909630c | ||
|
|
c9eace8371 | ||
|
|
2f69748ee4 | ||
|
|
fb57a6162f | ||
|
|
6dd948d5a2 | ||
|
|
4913f9cc18 | ||
|
|
d456721d81 | ||
|
|
595e7c0032 | ||
|
|
1f875f06d9 | ||
|
|
52d15c4c04 | ||
|
|
abde501dfa | ||
|
|
8ddf052a95 | ||
|
|
fe3d9a6a81 | ||
|
|
51eefbfebc | ||
|
|
4f1c34099a | ||
|
|
a500ca1f28 | ||
|
|
b3d212e985 | ||
|
|
beb474b65c | ||
|
|
e1f9691120 | ||
|
|
15523a3909 | ||
|
|
1a7dcdf65c | ||
|
|
3ec35e5409 | ||
|
|
d51f4c086d | ||
|
|
e9e6b3711c | ||
|
|
341457a121 | ||
|
|
da7f25e14b | ||
|
|
cbaf508665 | ||
|
|
cfc356f01d | ||
|
|
c78c326eb3 | ||
|
|
2a3c50147a | ||
|
|
2d42bdcc40 | ||
|
|
259fe1150c | ||
|
|
edbd610060 | ||
|
|
a16476bebf | ||
|
|
d9faeb29c0 | ||
|
|
74c142460d | ||
|
|
0e878a92d2 | ||
|
|
ef827022b8 | ||
|
|
f363108425 | ||
|
|
89e0836f78 | ||
|
|
ee2b78e826 | ||
|
|
b06a5148f6 | ||
|
|
36a20de5af | ||
|
|
92c20afdc1 | ||
|
|
97a5860681 | ||
|
|
a7fe4fba4e | ||
|
|
01f28217b1 | ||
|
|
6b9944f11d | ||
|
|
13584fd98e | ||
|
|
85bc8bfa6a | ||
|
|
96e4690013 | ||
|
|
4586efe651 | ||
|
|
6ec427f82f | ||
|
|
c591ea124f | ||
|
|
094de95548 | ||
|
|
628c0a9217 | ||
|
|
8939eb835d | ||
|
|
9464e1479d | ||
|
|
5216cf4ae6 | ||
|
|
d6310e2ea9 | ||
|
|
b5e50d552a | ||
|
|
eb41000851 | ||
|
|
a97f979ec4 | ||
|
|
61b6010765 | ||
|
|
cf8d297af0 | ||
|
|
1f4e1badc6 | ||
|
|
c4cd502d82 | ||
|
|
acf66ee2cc | ||
|
|
8722c6374c | ||
|
|
2b6d8f17ba | ||
|
|
85f0c1177f | ||
|
|
f785a5d401 | ||
|
|
bd53347615 | ||
|
|
4f25803289 | ||
|
|
01549fb953 | ||
|
|
c091a03d82 | ||
|
|
31fedcfa2b | ||
|
|
06c2b7f6e4 | ||
|
|
5a9e312a2e | ||
|
|
bd5c81cb7e | ||
|
|
2b4a9fe729 | ||
|
|
63f1e347ad | ||
|
|
9c03b80428 | ||
|
|
6c333161a4 | ||
|
|
c52270cfee | ||
|
|
c32f52fcc0 | ||
|
|
7c8e5be627 | ||
|
|
8654a25565 | ||
|
|
f22944eea4 | ||
|
|
50dcc5086b | ||
|
|
65ca13102d | ||
|
|
7aaec2d7f4 | ||
|
|
0548674d30 | ||
|
|
cc716e139a | ||
|
|
4128d63c51 | ||
|
|
3e4f7eb9ba | ||
|
|
3f699a7b4d | ||
|
|
f995211da0 | ||
|
|
cafe52e8ec | ||
|
|
71f9f9a9a7 | ||
|
|
9a7f7b7de5 | ||
|
|
75a84c8127 | ||
|
|
c45cdea512 | ||
|
|
811b5f919d | ||
|
|
83c2723113 | ||
|
|
3bf21396c6 | ||
|
|
baf1086d55 | ||
|
|
892f837c5a | ||
|
|
e8ea4784c6 | ||
|
|
4ac3e6e7d0 | ||
|
|
45dc189d8d | ||
|
|
bbb5a72871 | ||
|
|
aa689e6af0 | ||
|
|
02b1b36c6d | ||
|
|
cfe7ad47c1 | ||
|
|
059f45e48d | ||
|
|
08fb978c46 | ||
|
|
f75c21bcd0 | ||
|
|
98fe15762d | ||
|
|
6079b14040 | ||
|
|
a3f5cd55ee | ||
|
|
c70b167f70 | ||
|
|
f3c69df419 | ||
|
|
128248650c | ||
|
|
522ce30a85 | ||
|
|
1406c6ec71 | ||
|
|
630cbe1ee6 | ||
|
|
c0b88a87ba | ||
|
|
863cdb82b6 | ||
|
|
f73791d246 | ||
|
|
0e07a0ae9e | ||
|
|
598d26580e | ||
|
|
574e1fb68d | ||
|
|
8ed9716d5b | ||
|
|
7a6d2fe3f6 | ||
|
|
5a0d7882c5 | ||
|
|
55ea6ef7e9 | ||
|
|
9fe730a44b | ||
|
|
00b1a9a1a0 | ||
|
|
7e60c49ce4 | ||
|
|
98d94e0e3d | ||
|
|
510aeb7da4 | ||
|
|
5939089c98 | ||
|
|
1a23d70a97 | ||
|
|
a6c5a6aca4 | ||
|
|
d53155f45c | ||
|
|
e8fb85d38c | ||
|
|
f378f80430 | ||
|
|
a1914e1176 | ||
|
|
3b12ed9cf4 | ||
|
|
c852267a1a | ||
|
|
2c5d0481b3 | ||
|
|
7391db62d1 | ||
|
|
90ba189232 | ||
|
|
a33bbcedea | ||
|
|
04987ffa7a | ||
|
|
3427fb4774 | ||
|
|
0c245dd1f9 | ||
|
|
bfd3cad5ee | ||
|
|
0d110312df | ||
|
|
3ea7fd280b | ||
|
|
ec9e04027d | ||
|
|
88c00fb271 | ||
|
|
228f4e5070 | ||
|
|
f77e44d5bf |
No files matched your search
Executable
+238
@@ -0,0 +1,238 @@
|
||||
#!/bin/bash
|
||||
# vllm-omni customized version
|
||||
# Based on: https://github.com/vllm-project/ci-infra/blob/main/buildkite/bootstrap-amd.sh
|
||||
# Last synced: 2025-12-15
|
||||
# Modifications: Use local template file instead of downloading from ci-infra
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
if [[ -z "${RUN_ALL:-}" ]]; then
|
||||
RUN_ALL=0
|
||||
fi
|
||||
|
||||
if [[ -z "${NIGHTLY:-}" ]]; then
|
||||
NIGHTLY=0
|
||||
fi
|
||||
|
||||
if [[ -z "${VLLM_CI_BRANCH:-}" ]]; then
|
||||
VLLM_CI_BRANCH="main"
|
||||
fi
|
||||
|
||||
if [[ -z "${AMD_MIRROR_HW:-}" ]]; then
|
||||
AMD_MIRROR_HW="amdproduction"
|
||||
fi
|
||||
|
||||
if [[ -z "${DOCS_ONLY_DISABLE:-}" ]]; then
|
||||
DOCS_ONLY_DISABLE=0
|
||||
fi
|
||||
|
||||
fail_fast() {
|
||||
DISABLE_LABEL="ci-no-fail-fast"
|
||||
# If BUILDKITE_PULL_REQUEST != "false", then we check the PR labels using curl and jq
|
||||
if [ "$BUILDKITE_PULL_REQUEST" != "false" ]; then
|
||||
PR_LABELS=$(curl -s "https://api.github.com/repos/vllm-project/vllm-omni/pulls/$BUILDKITE_PULL_REQUEST" | jq -r '.labels[].name')
|
||||
if [[ $PR_LABELS == *"$DISABLE_LABEL"* ]]; then
|
||||
echo false
|
||||
else
|
||||
echo true
|
||||
fi
|
||||
else
|
||||
echo false # not a PR or BUILDKITE_PULL_REQUEST not set
|
||||
fi
|
||||
}
|
||||
|
||||
check_run_all_label() {
|
||||
RUN_ALL_LABEL="ready-run-all-tests"
|
||||
# If BUILDKITE_PULL_REQUEST != "false", then we check the PR labels using curl and jq
|
||||
if [ "$BUILDKITE_PULL_REQUEST" != "false" ]; then
|
||||
PR_LABELS=$(curl -s "https://api.github.com/repos/vllm-project/vllm-omni/pulls/$BUILDKITE_PULL_REQUEST" | jq -r '.labels[].name')
|
||||
if [[ $PR_LABELS == *"$RUN_ALL_LABEL"* ]]; then
|
||||
echo true
|
||||
else
|
||||
echo false
|
||||
fi
|
||||
else
|
||||
echo false # not a PR or BUILDKITE_PULL_REQUEST not set
|
||||
fi
|
||||
}
|
||||
|
||||
if [[ -z "${COV_ENABLED:-}" ]]; then
|
||||
COV_ENABLED=0
|
||||
fi
|
||||
|
||||
upload_pipeline() {
|
||||
echo "Uploading pipeline..."
|
||||
# Install minijinja
|
||||
ls .buildkite || buildkite-agent annotate --style error 'Please merge upstream main branch for buildkite CI'
|
||||
curl -sSfL https://github.com/mitsuhiko/minijinja/releases/download/2.3.1/minijinja-cli-installer.sh | sh
|
||||
source /var/lib/buildkite-agent/.cargo/env
|
||||
|
||||
if [[ $BUILDKITE_PIPELINE_SLUG == "fastcheck" ]]; then
|
||||
AMD_MIRROR_HW="amdtentative"
|
||||
fi
|
||||
|
||||
# Use local template file for vllm-omni
|
||||
cp .buildkite/test-template-amd-omni.j2 .buildkite/test-template.j2
|
||||
|
||||
|
||||
# (WIP) Use pipeline generator instead of jinja template
|
||||
if [ -e ".buildkite/pipeline_generator/pipeline_generator.py" ]; then
|
||||
python -m pip install click pydantic
|
||||
python .buildkite/pipeline_generator/pipeline_generator.py --run_all=$RUN_ALL --list_file_diff="$LIST_FILE_DIFF" --nightly="$NIGHTLY" --mirror_hw="$AMD_MIRROR_HW"
|
||||
buildkite-agent pipeline upload .buildkite/pipeline.yaml
|
||||
exit 0
|
||||
fi
|
||||
echo "List file diff: $LIST_FILE_DIFF"
|
||||
echo "Run all: $RUN_ALL"
|
||||
echo "Nightly: $NIGHTLY"
|
||||
echo "AMD Mirror HW: $AMD_MIRROR_HW"
|
||||
|
||||
FAIL_FAST=$(fail_fast)
|
||||
|
||||
cd .buildkite
|
||||
(
|
||||
set -x
|
||||
# Output pipeline.yaml with all blank lines removed
|
||||
minijinja-cli test-template.j2 test-amd.yaml \
|
||||
-D branch="$BUILDKITE_BRANCH" \
|
||||
-D list_file_diff="$LIST_FILE_DIFF" \
|
||||
-D run_all="$RUN_ALL" \
|
||||
-D nightly="$NIGHTLY" \
|
||||
-D mirror_hw="$AMD_MIRROR_HW" \
|
||||
-D fail_fast="$FAIL_FAST" \
|
||||
-D vllm_use_precompiled="$VLLM_USE_PRECOMPILED" \
|
||||
-D vllm_merge_base_commit="$(git merge-base origin/main HEAD)" \
|
||||
-D cov_enabled="$COV_ENABLED" \
|
||||
-D vllm_ci_branch="$VLLM_CI_BRANCH" \
|
||||
| sed '/^[[:space:]]*$/d' \
|
||||
> pipeline.yaml
|
||||
)
|
||||
cat pipeline.yaml
|
||||
buildkite-agent artifact upload pipeline.yaml
|
||||
buildkite-agent pipeline upload pipeline.yaml
|
||||
exit 0
|
||||
}
|
||||
|
||||
get_diff() {
|
||||
$(git add .)
|
||||
echo $(git diff --name-only --diff-filter=ACMDR $(git merge-base origin/main HEAD))
|
||||
}
|
||||
|
||||
get_diff_main() {
|
||||
$(git add .)
|
||||
echo $(git diff --name-only --diff-filter=ACMDR HEAD~1)
|
||||
}
|
||||
|
||||
file_diff=$(get_diff)
|
||||
if [[ $BUILDKITE_BRANCH == "main" ]]; then
|
||||
file_diff=$(get_diff_main)
|
||||
fi
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Early exit start: skip pipeline if conditions are met
|
||||
# ----------------------------------------------------------------------
|
||||
|
||||
# skip pipeline if all changed files are under docs/
|
||||
if [[ "${DOCS_ONLY_DISABLE}" != "1" ]]; then
|
||||
if [[ -n "${file_diff:-}" ]]; then
|
||||
docs_only=1
|
||||
# Robust iteration over newline-separated file_diff
|
||||
while IFS= read -r f; do
|
||||
[[ -z "$f" ]] && continue
|
||||
# **Policy:** only skip if *every* path starts with docs/
|
||||
if [[ "$f" != docs/* ]]; then
|
||||
docs_only=0
|
||||
break
|
||||
fi
|
||||
done < <(printf '%s\n' "$file_diff" | tr ' ' '\n' | tr -d '\r')
|
||||
|
||||
if [[ "$docs_only" -eq 1 ]]; then
|
||||
buildkite-agent annotate ":memo: CI skipped — docs/** only changes detected
|
||||
|
||||
\`\`\`
|
||||
${file_diff}
|
||||
\`\`\`" --style "info" || true
|
||||
echo "[docs-only] All changes are under docs/. Exiting before pipeline upload."
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Early exit end
|
||||
# ----------------------------------------------------------------------
|
||||
|
||||
patterns=(
|
||||
"docker/Dockerfile"
|
||||
"CMakeLists.txt"
|
||||
"requirements/common.txt"
|
||||
"requirements/cuda.txt"
|
||||
"requirements/build.txt"
|
||||
"requirements/test.txt"
|
||||
"setup.py"
|
||||
"csrc/"
|
||||
"cmake/"
|
||||
)
|
||||
|
||||
ignore_patterns=(
|
||||
"docker/Dockerfile."
|
||||
"csrc/cpu"
|
||||
"csrc/rocm"
|
||||
"cmake/hipify.py"
|
||||
"cmake/cpu_extension.cmake"
|
||||
)
|
||||
|
||||
for file in $file_diff; do
|
||||
# First check if file matches any pattern
|
||||
matches_pattern=0
|
||||
for pattern in "${patterns[@]}"; do
|
||||
if [[ $file == $pattern* ]] || [[ $file == $pattern ]]; then
|
||||
matches_pattern=1
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# If file matches pattern, check it's not in ignore patterns
|
||||
if [[ $matches_pattern -eq 1 ]]; then
|
||||
matches_ignore=0
|
||||
for ignore in "${ignore_patterns[@]}"; do
|
||||
if [[ $file == $ignore* ]] || [[ $file == $ignore ]]; then
|
||||
matches_ignore=1
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ $matches_ignore -eq 0 ]]; then
|
||||
RUN_ALL=1
|
||||
echo "Found changes: $file. Run all tests"
|
||||
break
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
# Check for ready-run-all-tests label
|
||||
LABEL_RUN_ALL=$(check_run_all_label)
|
||||
if [[ $LABEL_RUN_ALL == true ]]; then
|
||||
RUN_ALL=1
|
||||
NIGHTLY=1
|
||||
echo "Found 'ready-run-all-tests' label. Running all tests including optional tests."
|
||||
fi
|
||||
|
||||
# Decide whether to use precompiled wheels
|
||||
# Relies on existing patterns array as a basis.
|
||||
if [[ -n "${VLLM_USE_PRECOMPILED:-}" ]]; then
|
||||
echo "VLLM_USE_PRECOMPILED is already set to: $VLLM_USE_PRECOMPILED"
|
||||
elif [[ $RUN_ALL -eq 1 ]]; then
|
||||
export VLLM_USE_PRECOMPILED=0
|
||||
echo "Detected critical changes, building wheels from source"
|
||||
else
|
||||
export VLLM_USE_PRECOMPILED=1
|
||||
echo "No critical changes, using precompiled wheels"
|
||||
fi
|
||||
|
||||
|
||||
LIST_FILE_DIFF=$(get_diff | tr ' ' '|')
|
||||
if [[ $BUILDKITE_BRANCH == "main" ]]; then
|
||||
LIST_FILE_DIFF=$(get_diff_main | tr ' ' '|')
|
||||
fi
|
||||
upload_pipeline
|
||||
+36
-2
@@ -1,6 +1,40 @@
|
||||
steps:
|
||||
- label: "Simple Unit Test"
|
||||
- label: ":docker: Build image"
|
||||
key: image-build
|
||||
commands:
|
||||
- ".buildkite/scripts/simple_test.sh"
|
||||
- "aws ecr-public get-login-password --region us-east-1 | docker login --username AWS --password-stdin public.ecr.aws/q9t5s3a7"
|
||||
- "docker build --file docker/Dockerfile.ci -t vllm-omni-ci ."
|
||||
- "docker tag vllm-omni-ci public.ecr.aws/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT"
|
||||
- "docker push public.ecr.aws/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT"
|
||||
agents:
|
||||
queue: "cpu_queue_premerge"
|
||||
|
||||
# L2 Test
|
||||
- label: "Upload Ready Pipeline"
|
||||
depends_on: image-build
|
||||
key: upload-ready-pipeline
|
||||
if: build.branch != "main"
|
||||
commands:
|
||||
- buildkite-agent pipeline upload .buildkite/test-ready.yml
|
||||
agents:
|
||||
queue: "cpu_queue_premerge"
|
||||
|
||||
# L3 Test
|
||||
- label: "Upload Merge Pipeline"
|
||||
depends_on: image-build
|
||||
key: upload-merge-pipeline
|
||||
if: build.branch == "main"
|
||||
commands:
|
||||
- buildkite-agent pipeline upload .buildkite/test-merge.yml
|
||||
agents:
|
||||
queue: "cpu_queue_premerge"
|
||||
|
||||
# L4 Test
|
||||
- label: "Upload Nightly Pipeline"
|
||||
depends_on: image-build
|
||||
key: upload-nightly-pipeline
|
||||
if: build.env("NIGHTLY") == "1"
|
||||
commands:
|
||||
- buildkite-agent pipeline upload .buildkite/test-nightly.yml
|
||||
agents:
|
||||
queue: "cpu_queue_premerge"
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
#!/bin/bash
|
||||
# Helper function to safely login to ECR Public with per-job config isolation
|
||||
# Uses DOCKER_CONFIG environment variable to prevent race conditions
|
||||
#
|
||||
# This script prevents the "device or resource busy" error by giving each
|
||||
# Buildkite job its own isolated Docker config directory.
|
||||
#
|
||||
# Usage:
|
||||
# source docker_login_ecr_public.sh && safe_docker_login_ecr_public
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Configuration
|
||||
ECR_REGISTRY="public.ecr.aws"
|
||||
|
||||
setup_isolated_docker_config() {
|
||||
# Use BUILDKITE_JOB_ID for job-specific isolation
|
||||
# Fallback to PID if running outside Buildkite
|
||||
local job_id="${BUILDKITE_JOB_ID:-$$}"
|
||||
|
||||
# Set Docker config to job-specific directory
|
||||
export DOCKER_CONFIG="/tmp/docker-config-${job_id}"
|
||||
|
||||
# Create directory if it doesn't exist
|
||||
mkdir -p "$DOCKER_CONFIG"
|
||||
|
||||
echo "[docker-config] Using isolated Docker config: $DOCKER_CONFIG"
|
||||
}
|
||||
|
||||
check_docker_auth() {
|
||||
# Check if already authenticated to the given registry
|
||||
# Returns 0 if authenticated, 1 if not
|
||||
local registry="$1"
|
||||
|
||||
# Check if credentials exist in the isolated config
|
||||
if [[ -f "$DOCKER_CONFIG/config.json" ]]; then
|
||||
# Check if registry is present in config
|
||||
if grep -q "$registry" "$DOCKER_CONFIG/config.json" 2>/dev/null; then
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
safe_docker_login_ecr_public() {
|
||||
# Setup isolated config first
|
||||
setup_isolated_docker_config
|
||||
|
||||
local registry="$ECR_REGISTRY"
|
||||
|
||||
# Check if already authenticated (within this job)
|
||||
if check_docker_auth "$registry"; then
|
||||
echo "[docker-login] Already authenticated to $registry in this job"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Perform login to isolated config directory
|
||||
echo "[docker-login] Logging in to $ECR_REGISTRY (isolated config)..."
|
||||
if aws ecr-public get-login-password --region us-east-1 | docker login --username AWS --password-stdin "$ECR_REGISTRY"; then
|
||||
echo "[docker-login] Login successful (config: $DOCKER_CONFIG)"
|
||||
return 0
|
||||
else
|
||||
local exit_code=$?
|
||||
echo "[docker-login] ERROR: Login failed with exit code $exit_code" >&2
|
||||
return $exit_code
|
||||
fi
|
||||
}
|
||||
|
||||
# Execute if run as script (not sourced)
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
safe_docker_login_ecr_public
|
||||
fi
|
||||
+166
@@ -0,0 +1,166 @@
|
||||
#!/bin/bash
|
||||
# vllm-omni customized version
|
||||
# Based on: vllm/.buildkite/scripts/hardware_ci/run-amd-test.sh
|
||||
# Last synced: 2025-12-15
|
||||
# Modifications: docker image name for vllm-omni
|
||||
|
||||
# This script runs test inside the corresponding ROCm docker container.
|
||||
set -o pipefail
|
||||
|
||||
# Export Python path
|
||||
export PYTHONPATH=".."
|
||||
|
||||
# Print ROCm version
|
||||
echo "--- Confirming Clean Initial State"
|
||||
while true; do
|
||||
sleep 3
|
||||
if grep -q clean /opt/amdgpu/etc/gpu_state; then
|
||||
echo "GPUs state is \"clean\""
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
echo "--- ROCm info"
|
||||
rocminfo
|
||||
|
||||
# cleanup older docker images
|
||||
cleanup_docker() {
|
||||
# Get Docker's root directory
|
||||
docker_root=$(docker info -f '{{.DockerRootDir}}')
|
||||
if [ -z "$docker_root" ]; then
|
||||
echo "Failed to determine Docker root directory."
|
||||
exit 1
|
||||
fi
|
||||
echo "Docker root directory: $docker_root"
|
||||
# Check disk usage of the filesystem where Docker's root directory is located
|
||||
disk_usage=$(df "$docker_root" | tail -1 | awk '{print $5}' | sed 's/%//')
|
||||
# Define the threshold
|
||||
threshold=70
|
||||
if [ "$disk_usage" -gt "$threshold" ]; then
|
||||
echo "Disk usage is above $threshold%. Cleaning up Docker images and volumes..."
|
||||
# Remove dangling images (those that are not tagged and not used by any container)
|
||||
docker image prune -f
|
||||
# Remove unused volumes / force the system prune for old images as well.
|
||||
docker volume prune -f && docker system prune --force --filter "until=72h" --all
|
||||
echo "Docker images and volumes cleanup completed."
|
||||
else
|
||||
echo "Disk usage is below $threshold%. No cleanup needed."
|
||||
fi
|
||||
}
|
||||
|
||||
# Call the cleanup docker function
|
||||
cleanup_docker
|
||||
|
||||
echo "--- Resetting GPUs"
|
||||
|
||||
echo "reset" > /opt/amdgpu/etc/gpu_state
|
||||
|
||||
while true; do
|
||||
sleep 3
|
||||
if grep -q clean /opt/amdgpu/etc/gpu_state; then
|
||||
echo "GPUs state is \"clean\""
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
echo "--- Pulling container"
|
||||
image_name="public.ecr.aws/q9t5s3a7/vllm-ci-test-repo:${BUILDKITE_COMMIT}-rocm-omni"
|
||||
container_name="rocm_${BUILDKITE_COMMIT}_$(tr -dc A-Za-z0-9 < /dev/urandom | head -c 10; echo)"
|
||||
|
||||
# Install AWS CLI to authenticate to ECR Public Gallery to get higher rate limit for pulling images
|
||||
sudo apt-get update && sudo apt-get install -y awscli
|
||||
# Use safe docker login helper to prevent race conditions
|
||||
source "$(dirname "${BASH_SOURCE[0]}")/../docker_login_ecr_public.sh"
|
||||
safe_docker_login_ecr_public
|
||||
# Pull the container from ECR Public Gallery
|
||||
|
||||
docker pull "${image_name}"
|
||||
|
||||
remove_docker_container() {
|
||||
docker rm -f "${container_name}" || docker image rm -f "${image_name}" || true
|
||||
}
|
||||
trap remove_docker_container EXIT
|
||||
|
||||
echo "--- Running container"
|
||||
|
||||
HF_CACHE="$(realpath ~)/huggingface"
|
||||
mkdir -p "${HF_CACHE}"
|
||||
HF_MOUNT="/root/.cache/huggingface"
|
||||
|
||||
commands=$@
|
||||
echo "Commands:$commands"
|
||||
|
||||
PARALLEL_JOB_COUNT=8
|
||||
MYPYTHONPATH=".."
|
||||
|
||||
# Test that we're launching on the machine that has
|
||||
# proper access to GPUs
|
||||
render_gid=$(getent group render | cut -d: -f3)
|
||||
if [[ -z "$render_gid" ]]; then
|
||||
echo "Error: 'render' group not found. This is required for GPU access." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# check if the command contains shard flag, we will run all shards in parallel because the host have 8 GPUs.
|
||||
if [[ $commands == *"--shard-id="* ]]; then
|
||||
# assign job count as the number of shards used
|
||||
commands=$(echo "$commands" | sed -E "s/--num-shards[[:blank:]]*=[[:blank:]]*[0-9]*/--num-shards=${PARALLEL_JOB_COUNT} /g" | sed 's/ \\ / /g')
|
||||
for GPU in $(seq 0 $(($PARALLEL_JOB_COUNT-1))); do
|
||||
# assign shard-id for each shard
|
||||
commands_gpu=$(echo "$commands" | sed -E "s/--shard-id[[:blank:]]*=[[:blank:]]*[0-9]*/--shard-id=${GPU} /g" | sed 's/ \\ / /g')
|
||||
echo "Shard ${GPU} commands:$commands_gpu"
|
||||
echo "Render devices: $BUILDKITE_AGENT_META_DATA_RENDER_DEVICES"
|
||||
docker run \
|
||||
--device /dev/kfd $BUILDKITE_AGENT_META_DATA_RENDER_DEVICES \
|
||||
--network=host \
|
||||
--shm-size=16gb \
|
||||
--group-add "$render_gid" \
|
||||
--rm \
|
||||
-e MIOPEN_DEBUG_CONV_DIRECT=0 \
|
||||
-e MIOPEN_DEBUG_CONV_GEMM=0 \
|
||||
-e VLLM_ROCM_USE_AITER=1 \
|
||||
-e HIP_VISIBLE_DEVICES="${GPU}" \
|
||||
-e HF_TOKEN \
|
||||
-e AWS_ACCESS_KEY_ID \
|
||||
-e AWS_SECRET_ACCESS_KEY \
|
||||
-v "${HF_CACHE}:${HF_MOUNT}" \
|
||||
-e "HF_HOME=${HF_MOUNT}" \
|
||||
-e "PYTHONPATH=${MYPYTHONPATH}" \
|
||||
--name "${container_name}_${GPU}" \
|
||||
"${image_name}" \
|
||||
/bin/bash -c "${commands_gpu}" \
|
||||
|& while read -r line; do echo ">>Shard $GPU: $line"; done &
|
||||
PIDS+=($!)
|
||||
done
|
||||
#wait for all processes to finish and collect exit codes
|
||||
for pid in "${PIDS[@]}"; do
|
||||
wait "${pid}"
|
||||
STATUS+=($?)
|
||||
done
|
||||
for st in "${STATUS[@]}"; do
|
||||
if [[ ${st} -ne 0 ]]; then
|
||||
echo "One of the processes failed with $st"
|
||||
exit "${st}"
|
||||
fi
|
||||
done
|
||||
else
|
||||
echo "Render devices: $BUILDKITE_AGENT_META_DATA_RENDER_DEVICES"
|
||||
docker run \
|
||||
--device /dev/kfd $BUILDKITE_AGENT_META_DATA_RENDER_DEVICES \
|
||||
--network=host \
|
||||
--shm-size=16gb \
|
||||
--group-add "$render_gid" \
|
||||
--rm \
|
||||
-e MIOPEN_DEBUG_CONV_DIRECT=0 \
|
||||
-e MIOPEN_DEBUG_CONV_GEMM=0 \
|
||||
-e VLLM_ROCM_USE_AITER=1 \
|
||||
-e HF_TOKEN \
|
||||
-e AWS_ACCESS_KEY_ID \
|
||||
-e AWS_SECRET_ACCESS_KEY \
|
||||
-v "${HF_CACHE}:${HF_MOUNT}" \
|
||||
-e "HF_HOME=${HF_MOUNT}" \
|
||||
-e "PYTHONPATH=${MYPYTHONPATH}" \
|
||||
--name "${container_name}" \
|
||||
"${image_name}" \
|
||||
/bin/bash -c "${commands}"
|
||||
fi
|
||||
@@ -0,0 +1,145 @@
|
||||
#!/bin/bash
|
||||
|
||||
# This script build the Ascend NPU docker image and run the offline inference inside the container.
|
||||
# It serves a sanity check for compilation and basic model usage.
|
||||
set -ex
|
||||
|
||||
# Base ubuntu image with basic ascend development libraries and python installed
|
||||
VLLM_OMNI_REPO="https://github.com/vllm-project/vllm-omni.git"
|
||||
BASE_IMAGE_NAME="quay.nju.edu.cn/ascend/vllm-ascend:v0.11.0rc2"
|
||||
image_name="npu/vllm-omni-ci:${BUILDKITE_COMMIT}_${EPOCHSECONDS}"
|
||||
# image_name="npu/vllm-ci:${BUILDKITE_COMMIT}_${EPOCHSECONDS}"
|
||||
container_name="npu_${BUILDKITE_COMMIT}_$(tr -dc A-Za-z0-9 < /dev/urandom | head -c 10; echo)"
|
||||
|
||||
# BUILDKITE_AGENT_NAME format is {hostname}-{agent_idx}-{npu_card_num}cards
|
||||
agent_idx=$(echo "${BUILDKITE_AGENT_NAME}" | awk -F'-' '{print $(NF-1)}')
|
||||
echo "agent_idx: ${agent_idx}"
|
||||
builder_name="cachebuilder${agent_idx}"
|
||||
builder_cache_dir="/mnt/docker-cache${agent_idx}"
|
||||
mkdir -p ${builder_cache_dir}
|
||||
|
||||
# Try building the docker image
|
||||
cat <<EOF | DOCKER_BUILDKIT=1 docker build \
|
||||
--add-host pypi-cache:${PYPI_CACHE_HOST} \
|
||||
--builder ${builder_name} --cache-from type=local,src=${builder_cache_dir} \
|
||||
--cache-to type=local,dest=${builder_cache_dir},mode=max \
|
||||
--build-arg BUILDKITE_PULL_REQUEST="${BUILDKITE_PULL_REQUEST}" \
|
||||
--build-arg BUILDKITE_PULL_REQUEST_REPO="${BUILDKITE_PULL_REQUEST_REPO}" \
|
||||
--progress=plain --load -t ${image_name} -f - .
|
||||
FROM ${BASE_IMAGE_NAME}
|
||||
|
||||
# Define environments
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
RUN pip config set global.index-url http://pypi-cache:${PYPI_CACHE_PORT}/pypi/simple && \
|
||||
pip config set global.trusted-host pypi-cache && \
|
||||
apt-get update -y && \
|
||||
apt-get install -y python3-pip git vim wget net-tools gcc g++ cmake libnuma-dev && \
|
||||
rm -rf /var/cache/apt/* && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install for pytest to make the docker build cache layer always valid
|
||||
RUN --mount=type=cache,target=/root/.cache/pip \
|
||||
pip install pytest>=6.0 pytest-cov modelscope
|
||||
|
||||
COPY . .
|
||||
|
||||
# Install vllm-omni
|
||||
WORKDIR /workspace
|
||||
ARG VLLM_OMNI_REPO=https://github.com/vllm-project/vllm-omni.git
|
||||
ARG VLLM_OMNI_TAG=main
|
||||
ARG BUILDKITE_PULL_REQUEST
|
||||
ARG BUILDKITE_PULL_REQUEST_REPO
|
||||
RUN git config --global url."https://gh-proxy.test.osinfra.cn/https://github.com/".insteadOf "https://github.com/" && \
|
||||
if [ "\$BUILDKITE_PULL_REQUEST" != "false" ] && [ -n "\$BUILDKITE_PULL_REQUEST" ]; then \
|
||||
echo "Cloning and checking out PR #\$BUILDKITE_PULL_REQUEST..." && \
|
||||
git clone \$VLLM_OMNI_REPO /workspace/vllm-omni && \
|
||||
cd /workspace/vllm-omni && \
|
||||
git fetch origin pull/\$BUILDKITE_PULL_REQUEST/head:pr-\$BUILDKITE_PULL_REQUEST && \
|
||||
git checkout pr-\$BUILDKITE_PULL_REQUEST; \
|
||||
else \
|
||||
echo "Not a PR build, using main branch" && \
|
||||
git clone --depth 1 \$VLLM_OMNI_REPO /workspace/vllm-omni; \
|
||||
fi
|
||||
|
||||
RUN --mount=type=cache,target=/root/.cache/pip \
|
||||
export PIP_EXTRA_INDEX_URL=https://mirrors.huaweicloud.com/ascend/repos/pypi && \
|
||||
source /usr/local/Ascend/ascend-toolkit/set_env.sh && \
|
||||
source /usr/local/Ascend/nnal/atb/set_env.sh && \
|
||||
export LD_LIBRARY_PATH=\$LD_LIBRARY_PATH:/usr/local/Ascend/ascend-toolkit/latest/`uname -i`-linux/devlib && \
|
||||
python3 -m pip install -v -e /workspace/vllm-omni/
|
||||
|
||||
ENV VLLM_WORKER_MULTIPROC_METHOD=spawn
|
||||
|
||||
WORKDIR /workspace/vllm-omni
|
||||
CMD ["/bin/bash"]
|
||||
|
||||
EOF
|
||||
|
||||
# Setup cleanup
|
||||
remove_docker_container() {
|
||||
docker rm -f "${container_name}" || true;
|
||||
docker image rm -f "${image_name}" || true;
|
||||
docker system prune -f || true;
|
||||
}
|
||||
trap remove_docker_container EXIT
|
||||
|
||||
# Generate corresponding --device args based on BUILDKITE_AGENT_NAME
|
||||
# Ascend NPU BUILDKITE_AGENT_NAME format is {hostname}-{agent_idx}-{npu_card_num}cards, and agent_idx starts from 1.
|
||||
# e.g. atlas-a2-001-1-2cards means this is the 1-th agent on atlas-a2-001 host, and it has 2 NPU cards.
|
||||
# returns --device /dev/davinci0 --device /dev/davinci1
|
||||
parse_and_gen_devices() {
|
||||
local input="$1"
|
||||
local index cards_num
|
||||
if [[ "$input" =~ ([0-9]+)-([0-9]+)cards$ ]]; then
|
||||
index="${BASH_REMATCH[1]}"
|
||||
cards_num="${BASH_REMATCH[2]}"
|
||||
else
|
||||
echo "parse error" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
local devices=""
|
||||
local i=0
|
||||
while (( i < cards_num )); do
|
||||
local dev_idx=$(((index - 1)*cards_num + i ))
|
||||
devices="$devices --device /dev/davinci${dev_idx}"
|
||||
((i++))
|
||||
done
|
||||
|
||||
# trim leading space
|
||||
devices="${devices#"${devices%%[![:space:]]*}"}"
|
||||
# Output devices: assigned to the caller variable
|
||||
printf '%s' "$devices"
|
||||
}
|
||||
|
||||
devices=$(parse_and_gen_devices "${BUILDKITE_AGENT_NAME}") || exit 1
|
||||
|
||||
# Run the image and execute the Out-Of-Tree (OOT) platform interface test case on Ascend NPU hardware.
|
||||
# This test checks whether the OOT platform interface is functioning properly in conjunction with
|
||||
# the hardware plugin vllm-ascend.
|
||||
hf_model_cache_dir=/mnt/hf_cache${agent_idx}
|
||||
ms_model_cache_dir=/mnt/modelscope${agent_idx}
|
||||
mkdir -p ${hf_model_cache_dir}
|
||||
mkdir -p ${ms_model_cache_dir}
|
||||
docker run \
|
||||
--init \
|
||||
${devices} \
|
||||
--device /dev/davinci_manager \
|
||||
--device /dev/devmm_svm \
|
||||
--device /dev/hisi_hdc \
|
||||
-v /usr/local/dcmi:/usr/local/dcmi \
|
||||
-v /usr/local/bin/npu-smi:/usr/local/bin/npu-smi \
|
||||
-v /usr/local/Ascend/driver/lib64/:/usr/local/Ascend/driver/lib64/ \
|
||||
-v /usr/local/Ascend/driver/version.info:/usr/local/Ascend/driver/version.info \
|
||||
-v /etc/ascend_install.info:/etc/ascend_install.info \
|
||||
-v ${hf_model_cache_dir}:/root/.cache/huggingface \
|
||||
-v ${ms_model_cache_dir}:/root/.cache/modelscope \
|
||||
--network host \
|
||||
--entrypoint="" \
|
||||
--name "${container_name}" \
|
||||
"${image_name}" \
|
||||
bash -c '
|
||||
set -e
|
||||
VLLM_USE_MODELSCOPE=True pytest -s -v tests/e2e/offline_inference/test_qwen2_5_omni.py
|
||||
'
|
||||
@@ -1,52 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd -- "${SCRIPT_DIR}/../.." && pwd)"
|
||||
UV_BIN="uv"
|
||||
PYTHON_BOOTSTRAP="${PYTHON:-python3}"
|
||||
VENV_DIR="${VENV_DIR:-${REPO_ROOT}/.venv-simple-test}"
|
||||
TARGET_PY_VERSION="3.12"
|
||||
|
||||
if ! command -v "${PYTHON_BOOTSTRAP}" >/dev/null 2>&1; then
|
||||
PYTHON_BOOTSTRAP="python"
|
||||
fi
|
||||
|
||||
cd "${REPO_ROOT}"
|
||||
|
||||
# Ensure pip-installed scripts (including uv) are on PATH
|
||||
PYTHON_SCRIPTS="$(${PYTHON_BOOTSTRAP} -c 'import sysconfig; print(sysconfig.get_path("scripts"))')"
|
||||
USER_BASE="$(${PYTHON_BOOTSTRAP} -m site --user-base 2>/dev/null || true)"
|
||||
PATH="${PYTHON_SCRIPTS}:${PATH}"
|
||||
if [[ -n "${USER_BASE}" ]]; then
|
||||
PATH="${USER_BASE}/bin:${PATH}"
|
||||
fi
|
||||
|
||||
if ! command -v "${UV_BIN}" >/dev/null 2>&1; then
|
||||
"${PYTHON_BOOTSTRAP}" -m pip install --upgrade pip
|
||||
"${PYTHON_BOOTSTRAP}" -m pip install uv
|
||||
hash -r
|
||||
fi
|
||||
|
||||
TARGET_PY_PATH="$(${UV_BIN} python find "${TARGET_PY_VERSION}" 2>/dev/null | head -n1 || true)"
|
||||
if [[ -z "${TARGET_PY_PATH}" ]]; then
|
||||
"${UV_BIN}" python install "${TARGET_PY_VERSION}"
|
||||
TARGET_PY_PATH="$(${UV_BIN} python find "${TARGET_PY_VERSION}" 2>/dev/null | head -n1 || true)"
|
||||
fi
|
||||
|
||||
if [[ -z "${TARGET_PY_PATH}" ]]; then
|
||||
echo "Failed to locate python ${TARGET_PY_VERSION}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -d "${VENV_DIR}" ]]; then
|
||||
"${UV_BIN}" venv --python "${TARGET_PY_PATH}" "${VENV_DIR}"
|
||||
fi
|
||||
|
||||
VENV_PYTHON="${VENV_DIR}/bin/python"
|
||||
[[ -x "${VENV_PYTHON}" ]] || { echo "Python not found in ${VENV_DIR}"; exit 1; }
|
||||
|
||||
"${UV_BIN}" pip install --python "${VENV_PYTHON}" vllm==0.11.0
|
||||
"${UV_BIN}" pip install --python "${VENV_PYTHON}" -e ".[dev]"
|
||||
"${VENV_PYTHON}" -m pytest -v -s tests/test_omni_llm.py
|
||||
@@ -0,0 +1,116 @@
|
||||
steps:
|
||||
|
||||
- label: "Diffusion Model Test"
|
||||
timeout_in_minutes: 20
|
||||
agent_pool: mi325_2
|
||||
depends_on: amd-build
|
||||
mirror_hardwares: [amdproduction]
|
||||
grade: Blocking
|
||||
commands:
|
||||
- export GPU_ARCHS=gfx942
|
||||
- pytest -s -v tests/e2e/offline_inference/test_t2i_model.py
|
||||
|
||||
- label: "Diffusion Images API LoRA E2E"
|
||||
timeout_in_minutes: 20
|
||||
agent_pool: mi325_1
|
||||
depends_on: amd-build
|
||||
mirror_hardwares: [amdproduction]
|
||||
grade: Blocking
|
||||
commands:
|
||||
- export GPU_ARCHS=gfx942
|
||||
- export VLLM_LOGGING_LEVEL=DEBUG
|
||||
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
|
||||
- pytest -s -v tests/e2e/online_serving/test_images_generations_lora.py
|
||||
|
||||
- label: "Diffusion Model CPU offloading Test"
|
||||
timeout_in_minutes: 20
|
||||
agent_pool: mi325_1
|
||||
depends_on: amd-build
|
||||
mirror_hardwares: [amdproduction]
|
||||
grade: Blocking
|
||||
commands:
|
||||
- export GPU_ARCHS=gfx942
|
||||
- export VLLM_LOGGING_LEVEL=DEBUG
|
||||
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
|
||||
- pytest -s -v tests/e2e/offline_inference/test_diffusion_cpu_offload.py
|
||||
|
||||
- label: "Diffusion Cache Backend Test"
|
||||
timeout_in_minutes: 15
|
||||
agent_pool: mi325_1
|
||||
depends_on: amd-build
|
||||
mirror_hardwares: [amdproduction]
|
||||
grade: Blocking
|
||||
commands:
|
||||
- export GPU_ARCHS=gfx942
|
||||
- export VLLM_LOGGING_LEVEL=DEBUG
|
||||
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
|
||||
- pytest -s -v -m 'core_model and cache and diffusion and not distributed_rocm and MI325'
|
||||
|
||||
- label: "Diffusion Sequence Parallelism Test"
|
||||
timeout_in_minutes: 20
|
||||
agent_pool: mi325_2
|
||||
depends_on: amd-build
|
||||
mirror_hardwares: [amdproduction]
|
||||
grade: Blocking
|
||||
commands:
|
||||
- export GPU_ARCHS=gfx942
|
||||
- export VLLM_LOGGING_LEVEL=DEBUG
|
||||
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
|
||||
- pytest -s -v tests/e2e/offline_inference/test_sequence_parallel.py -m core_model
|
||||
|
||||
- label: "Diffusion Tensor Parallelism Test"
|
||||
timeout_in_minutes: 20
|
||||
agent_pool: mi325_2
|
||||
depends_on: amd-build
|
||||
commands:
|
||||
- export GPU_ARCHS=gfx942
|
||||
- export VLLM_LOGGING_LEVEL=DEBUG
|
||||
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
|
||||
- pytest -s -v tests/e2e/offline_inference/test_zimage_parallelism.py
|
||||
|
||||
- label: "Diffusion GPU Worker Test"
|
||||
timeout_in_minutes: 20
|
||||
agent_pool: mi325_2
|
||||
depends_on: amd-build
|
||||
mirror_hardwares: [amdproduction]
|
||||
grade: Blocking
|
||||
commands:
|
||||
- pytest -s -v tests/diffusion/test_diffusion_worker.py
|
||||
|
||||
- label: "Omni Model Test Qwen2-5-Omni"
|
||||
timeout_in_minutes: 15
|
||||
agent_pool: mi325_2
|
||||
depends_on: amd-build
|
||||
mirror_hardwares: [amdproduction]
|
||||
grade: Blocking
|
||||
commands:
|
||||
- export GPU_ARCHS=gfx942
|
||||
- export VLLM_LOGGING_LEVEL=DEBUG
|
||||
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
|
||||
- pytest -s -v tests/e2e/offline_inference/test_qwen2_5_omni.py
|
||||
|
||||
- label: "Omni Model Test Qwen3-Omni"
|
||||
timeout_in_minutes: 15
|
||||
agent_pool: mi325_2
|
||||
depends_on: amd-build
|
||||
mirror_hardwares: [amdproduction]
|
||||
grade: Blocking
|
||||
commands:
|
||||
- export VLLM_LOGGING_LEVEL=DEBUG
|
||||
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
|
||||
- export VLLM_TEST_CLEAN_GPU_MEMORY="1"
|
||||
- pytest -s -v tests/e2e/offline_inference/test_qwen3_omni.py
|
||||
- pytest -s -v tests/e2e/online_serving/test_qwen3_omni.py
|
||||
- pytest -s -v tests/engine/test_async_omni_engine_abort.py
|
||||
|
||||
- label: "Diffusion Image Edit Test"
|
||||
timeout_in_minutes: 15
|
||||
agent_pool: mi325_1
|
||||
depends_on: amd-build
|
||||
mirror_hardwares: [amdproduction]
|
||||
grade: Blocking
|
||||
commands:
|
||||
- export GPU_ARCHS=gfx942
|
||||
- export VLLM_LOGGING_LEVEL=DEBUG
|
||||
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
|
||||
- pytest -s -v tests/e2e/online_serving/test_image_gen_edit.py
|
||||
@@ -0,0 +1,318 @@
|
||||
steps:
|
||||
- label: "Simple Unit Test"
|
||||
depends_on: upload-merge-pipeline
|
||||
commands:
|
||||
- "pytest -v -s -m 'core_model and cpu' --cov=vllm_omni --cov-branch --cov-report=term-missing --cov-report=html --cov-report=xml"
|
||||
agents:
|
||||
queue: "gpu_1_queue"
|
||||
plugins:
|
||||
- docker#v5.2.0:
|
||||
image: public.ecr.aws/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
|
||||
always-pull: true
|
||||
propagate-environment: true
|
||||
environment:
|
||||
- "HF_HOME=/fsx/hf_cache"
|
||||
volumes:
|
||||
- "/fsx/hf_cache:/fsx/hf_cache"
|
||||
|
||||
- label: "Diffusion Model Test"
|
||||
timeout_in_minutes: 20
|
||||
depends_on: upload-merge-pipeline
|
||||
commands:
|
||||
- pytest -s -v tests/e2e/offline_inference/test_t2i_model.py -m "advanced_model and diffusion" --run-level "advanced_model"
|
||||
agents:
|
||||
queue: "gpu_1_queue" # g6.4xlarge instance on AWS, has 1 L4 GPU
|
||||
plugins:
|
||||
- docker#v5.2.0:
|
||||
image: public.ecr.aws/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
|
||||
always-pull: true
|
||||
propagate-environment: true
|
||||
environment:
|
||||
- "HF_HOME=/fsx/hf_cache"
|
||||
volumes:
|
||||
- "/fsx/hf_cache:/fsx/hf_cache"
|
||||
|
||||
- label: "Diffusion Images API LoRA E2E"
|
||||
timeout_in_minutes: 20
|
||||
depends_on: upload-merge-pipeline
|
||||
commands:
|
||||
- pytest -s -v tests/e2e/online_serving/test_images_generations_lora.py
|
||||
agents:
|
||||
queue: "gpu_1_queue" # g6.4xlarge instance on AWS, has 1 L4 GPU
|
||||
plugins:
|
||||
- docker#v5.2.0:
|
||||
image: public.ecr.aws/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
|
||||
always-pull: true
|
||||
propagate-environment: true
|
||||
environment:
|
||||
- "HF_HOME=/fsx/hf_cache"
|
||||
volumes:
|
||||
- "/fsx/hf_cache:/fsx/hf_cache"
|
||||
|
||||
- label: "Diffusion Model CPU offloading Test"
|
||||
timeout_in_minutes: 20
|
||||
depends_on: upload-merge-pipeline
|
||||
commands:
|
||||
- pytest -s -v tests/e2e/offline_inference/test_diffusion_cpu_offload.py
|
||||
- pytest -s -v tests/e2e/offline_inference/test_diffusion_layerwise_offload.py
|
||||
agents:
|
||||
queue: "gpu_1_queue" # g6.4xlarge instance on AWS, has 1 L4 GPU
|
||||
plugins:
|
||||
- docker#v5.2.0:
|
||||
image: public.ecr.aws/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
|
||||
always-pull: true
|
||||
propagate-environment: true
|
||||
environment:
|
||||
- "HF_HOME=/fsx/hf_cache"
|
||||
volumes:
|
||||
- "/fsx/hf_cache:/fsx/hf_cache"
|
||||
|
||||
- label: "Audio Generation Model Test"
|
||||
timeout_in_minutes: 20
|
||||
depends_on: upload-merge-pipeline
|
||||
commands:
|
||||
- pytest -s -v tests/e2e/offline_inference/test_stable_audio_model.py
|
||||
agents:
|
||||
queue: "gpu_1_queue" # g6.4xlarge instance on AWS, has 1 L4 GPU
|
||||
plugins:
|
||||
- docker#v5.2.0:
|
||||
image: public.ecr.aws/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
|
||||
always-pull: true
|
||||
propagate-environment: true
|
||||
environment:
|
||||
- "HF_HOME=/fsx/hf_cache"
|
||||
volumes:
|
||||
- "/fsx/hf_cache:/fsx/hf_cache"
|
||||
|
||||
- label: "Diffusion Cache Backend Test"
|
||||
timeout_in_minutes: 15
|
||||
depends_on: upload-merge-pipeline
|
||||
commands:
|
||||
- pytest -s -v -m 'core_model and cache and diffusion and not distributed_cuda and L4'
|
||||
agents:
|
||||
queue: "gpu_1_queue" # g6.4xlarge instance on AWS, has 1 L4 GPU
|
||||
plugins:
|
||||
- docker#v5.2.0:
|
||||
image: public.ecr.aws/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
|
||||
always-pull: true
|
||||
propagate-environment: true
|
||||
environment:
|
||||
- "HF_HOME=/fsx/hf_cache"
|
||||
volumes:
|
||||
- "/fsx/hf_cache:/fsx/hf_cache"
|
||||
|
||||
- label: "Diffusion Sequence Parallelism Test"
|
||||
timeout_in_minutes: 20
|
||||
depends_on: upload-merge-pipeline
|
||||
commands:
|
||||
- pytest -s -v tests/e2e/offline_inference/test_sequence_parallel.py
|
||||
agents:
|
||||
queue: "gpu_4_queue" # g6.12xlarge instance on AWS, has 4 L4 GPU
|
||||
plugins:
|
||||
- docker#v5.2.0:
|
||||
image: public.ecr.aws/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
|
||||
always-pull: true
|
||||
propagate-environment: true
|
||||
shm-size: "8gb"
|
||||
environment:
|
||||
- "HF_HOME=/fsx/hf_cache"
|
||||
volumes:
|
||||
- "/fsx/hf_cache:/fsx/hf_cache"
|
||||
|
||||
- label: "Diffusion Tensor Parallelism Test"
|
||||
timeout_in_minutes: 20
|
||||
depends_on: upload-merge-pipeline
|
||||
commands:
|
||||
- pytest -s -v tests/e2e/offline_inference/test_zimage_parallelism.py
|
||||
agents:
|
||||
queue: "gpu_4_queue" # g6.12xlarge instance on AWS, has 4 L4 GPU
|
||||
plugins:
|
||||
- docker#v5.2.0:
|
||||
image: public.ecr.aws/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
|
||||
always-pull: true
|
||||
propagate-environment: true
|
||||
shm-size: "8gb"
|
||||
environment:
|
||||
- "HF_HOME=/fsx/hf_cache"
|
||||
volumes:
|
||||
- "/fsx/hf_cache:/fsx/hf_cache"
|
||||
|
||||
- label: "Diffusion GPU Worker Test"
|
||||
timeout_in_minutes: 20
|
||||
depends_on: upload-merge-pipeline
|
||||
commands:
|
||||
- pytest -s -v tests/diffusion/test_diffusion_worker.py
|
||||
agents:
|
||||
queue: "gpu_4_queue" # g6.12xlarge instance on AWS, has 4 L4 GPU
|
||||
plugins:
|
||||
- docker#v5.2.0:
|
||||
image: public.ecr.aws/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
|
||||
always-pull: true
|
||||
propagate-environment: true
|
||||
shm-size: "8gb"
|
||||
environment:
|
||||
- "HF_HOME=/fsx/hf_cache"
|
||||
volumes:
|
||||
- "/fsx/hf_cache:/fsx/hf_cache"
|
||||
|
||||
- label: "Benchmark & Engine Test with H100"
|
||||
timeout_in_minutes: 15
|
||||
depends_on: upload-merge-pipeline
|
||||
commands:
|
||||
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
|
||||
- pytest -s -v tests/benchmarks/test_serve_cli.py
|
||||
- pytest -s -v tests/engine/test_async_omni_engine_abort.py
|
||||
agents:
|
||||
queue: "mithril-h100-pool"
|
||||
plugins:
|
||||
- kubernetes:
|
||||
podSpec:
|
||||
containers:
|
||||
- image: 936637512419.dkr.ecr.us-west-2.amazonaws.com/vllm-ci-pull-through-cache/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
|
||||
resources:
|
||||
limits:
|
||||
nvidia.com/gpu: 2
|
||||
volumeMounts:
|
||||
- name: devshm
|
||||
mountPath: /dev/shm
|
||||
- name: hf-cache
|
||||
mountPath: /root/.cache/huggingface
|
||||
env:
|
||||
- name: HF_HOME
|
||||
value: /root/.cache/huggingface
|
||||
nodeSelector:
|
||||
node.kubernetes.io/instance-type: gpu-h100-sxm
|
||||
volumes:
|
||||
- name: devshm
|
||||
emptyDir:
|
||||
medium: Memory
|
||||
- name: hf-cache
|
||||
hostPath:
|
||||
path: /mnt/hf-cache
|
||||
type: DirectoryOrCreate
|
||||
|
||||
- label: "Omni Model Test"
|
||||
timeout_in_minutes: 15
|
||||
depends_on: upload-merge-pipeline
|
||||
commands:
|
||||
- export VLLM_LOGGING_LEVEL=DEBUG
|
||||
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
|
||||
- pytest -s -v tests/e2e/offline_inference/test_qwen2_5_omni.py
|
||||
agents:
|
||||
queue: "gpu_4_queue" # g6.12xlarge instance on AWS, has 4 L4 GPU
|
||||
plugins:
|
||||
- docker#v5.2.0:
|
||||
image: public.ecr.aws/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
|
||||
always-pull: true
|
||||
propagate-environment: true
|
||||
environment:
|
||||
- "HF_HOME=/fsx/hf_cache"
|
||||
volumes:
|
||||
- "/fsx/hf_cache:/fsx/hf_cache"
|
||||
|
||||
- label: "Omni Model Test with H100"
|
||||
timeout_in_minutes: 30
|
||||
depends_on: upload-merge-pipeline
|
||||
commands:
|
||||
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
|
||||
- export VLLM_TEST_CLEAN_GPU_MEMORY="1"
|
||||
- pytest -s -v tests/e2e/offline_inference/test_qwen3_omni.py
|
||||
- pytest -s -v tests/e2e/online_serving/test_qwen3_omni.py -m "advanced_model" --run-level "advanced_model"
|
||||
agents:
|
||||
queue: "mithril-h100-pool"
|
||||
plugins:
|
||||
- kubernetes:
|
||||
podSpec:
|
||||
containers:
|
||||
- image: 936637512419.dkr.ecr.us-west-2.amazonaws.com/vllm-ci-pull-through-cache/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
|
||||
resources:
|
||||
limits:
|
||||
nvidia.com/gpu: 2
|
||||
volumeMounts:
|
||||
- name: devshm
|
||||
mountPath: /dev/shm
|
||||
- name: hf-cache
|
||||
mountPath: /root/.cache/huggingface
|
||||
env:
|
||||
- name: HF_HOME
|
||||
value: /root/.cache/huggingface
|
||||
nodeSelector:
|
||||
node.kubernetes.io/instance-type: gpu-h100-sxm
|
||||
volumes:
|
||||
- name: devshm
|
||||
emptyDir:
|
||||
medium: Memory
|
||||
- name: hf-cache
|
||||
hostPath:
|
||||
path: /mnt/hf-cache
|
||||
type: DirectoryOrCreate
|
||||
|
||||
- label: "Diffusion Image Edit Test with H100 (1 GPU)"
|
||||
timeout_in_minutes: 20
|
||||
depends_on: upload-merge-pipeline
|
||||
commands:
|
||||
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
|
||||
- pytest -s -v tests/e2e/online_serving/test_image_gen_edit.py
|
||||
agents:
|
||||
queue: "mithril-h100-pool"
|
||||
plugins:
|
||||
- kubernetes:
|
||||
podSpec:
|
||||
containers:
|
||||
- image: 936637512419.dkr.ecr.us-west-2.amazonaws.com/vllm-ci-pull-through-cache/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
|
||||
resources:
|
||||
limits:
|
||||
nvidia.com/gpu: 1
|
||||
volumeMounts:
|
||||
- name: devshm
|
||||
mountPath: /dev/shm
|
||||
- name: hf-cache
|
||||
mountPath: /root/.cache/huggingface
|
||||
env:
|
||||
- name: HF_HOME
|
||||
value: /root/.cache/huggingface
|
||||
nodeSelector:
|
||||
node.kubernetes.io/instance-type: gpu-h100-sxm
|
||||
volumes:
|
||||
- name: devshm
|
||||
emptyDir:
|
||||
medium: Memory
|
||||
- name: hf-cache
|
||||
hostPath:
|
||||
path: /mnt/hf-cache
|
||||
type: DirectoryOrCreate
|
||||
|
||||
# - label: "Bagel Text2Img Model Test with H100"
|
||||
# timeout_in_minutes: 30
|
||||
# depends_on: upload-merge-pipeline
|
||||
# commands:
|
||||
# - export VLLM_WORKER_MULTIPROC_METHOD=spawn
|
||||
# - pytest -s -v tests/e2e/offline_inference/test_bagel_text2img.py
|
||||
# agents:
|
||||
# queue: "mithril-h100-pool"
|
||||
# plugins:
|
||||
# - kubernetes:
|
||||
# podSpec:
|
||||
# containers:
|
||||
# - image: 936637512419.dkr.ecr.us-west-2.amazonaws.com/vllm-ci-pull-through-cache/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
|
||||
# resources:
|
||||
# limits:
|
||||
# nvidia.com/gpu: 1
|
||||
# volumeMounts:
|
||||
# - name: devshm
|
||||
# mountPath: /dev/shm
|
||||
# - name: hf-cache
|
||||
# mountPath: /root/.cache/huggingface
|
||||
# env:
|
||||
# - name: HF_HOME
|
||||
# value: /root/.cache/huggingface
|
||||
# nodeSelector:
|
||||
# node.kubernetes.io/instance-type: gpu-h100-sxm
|
||||
# volumes:
|
||||
# - name: devshm
|
||||
# emptyDir:
|
||||
# medium: Memory
|
||||
# - name: hf-cache
|
||||
# hostPath:
|
||||
# path: /mnt/hf-cache
|
||||
# type: DirectoryOrCreate
|
||||
@@ -0,0 +1,93 @@
|
||||
steps:
|
||||
- label: "Omni Model Test with H100"
|
||||
timeout_in_minutes: 90
|
||||
depends_on: upload-nightly-pipeline
|
||||
if: build.env("NIGHTLY") == "1"
|
||||
commands:
|
||||
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
|
||||
- pytest -s -v tests/e2e/online_serving/test_qwen3_omni_expansion.py -m "advanced_model" --run-level "advanced_model"
|
||||
- pytest -s -v tests/examples/online_serving/test_qwen3_omni.py -m "advanced_model" --run-level "advanced_model"
|
||||
agents:
|
||||
queue: "mithril-h100-pool"
|
||||
plugins:
|
||||
- kubernetes:
|
||||
podSpec:
|
||||
containers:
|
||||
- image: public.ecr.aws/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
|
||||
resources:
|
||||
limits:
|
||||
nvidia.com/gpu: 2
|
||||
volumeMounts:
|
||||
- name: devshm
|
||||
mountPath: /dev/shm
|
||||
- name: hf-cache
|
||||
mountPath: /root/.cache/huggingface
|
||||
env:
|
||||
- name: HF_HOME
|
||||
value: /root/.cache/huggingface
|
||||
nodeSelector:
|
||||
node.kubernetes.io/instance-type: gpu-h100-sxm
|
||||
volumes:
|
||||
- name: devshm
|
||||
emptyDir:
|
||||
medium: Memory
|
||||
- name: hf-cache
|
||||
hostPath:
|
||||
path: /mnt/hf-cache
|
||||
type: DirectoryOrCreate
|
||||
|
||||
- label: "Omni Model Test"
|
||||
timeout_in_minutes: 60
|
||||
depends_on: upload-nightly-pipeline
|
||||
if: build.env("NIGHTLY") == "1"
|
||||
commands:
|
||||
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
|
||||
- pytest -s -v tests/examples/online_serving/test_qwen2_5_omni.py -m "advanced_model" --run-level "advanced_model"
|
||||
agents:
|
||||
queue: "gpu_4_queue" # g6.12xlarge instance on AWS, has 4 L4 GPU
|
||||
plugins:
|
||||
- docker#v5.2.0:
|
||||
image: public.ecr.aws/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
|
||||
always-pull: true
|
||||
shm-size: "8gb"
|
||||
propagate-environment: true
|
||||
environment:
|
||||
- "HF_HOME=/fsx/hf_cache"
|
||||
volumes:
|
||||
- "/fsx/hf_cache:/fsx/hf_cache"
|
||||
|
||||
- label: "Omni Model Perf Test"
|
||||
timeout_in_minutes: 120
|
||||
depends_on: upload-nightly-pipeline
|
||||
if: build.env("NIGHTLY") == "1"
|
||||
commands:
|
||||
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
|
||||
- pytest -s -v tests/perf/scripts/run_benchmark.py
|
||||
agents:
|
||||
queue: "mithril-h100-pool"
|
||||
plugins:
|
||||
- kubernetes:
|
||||
podSpec:
|
||||
containers:
|
||||
- image: public.ecr.aws/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
|
||||
resources:
|
||||
limits:
|
||||
nvidia.com/gpu: 2
|
||||
volumeMounts:
|
||||
- name: devshm
|
||||
mountPath: /dev/shm
|
||||
- name: hf-cache
|
||||
mountPath: /root/.cache/huggingface
|
||||
env:
|
||||
- name: HF_HOME
|
||||
value: /root/.cache/huggingface
|
||||
nodeSelector:
|
||||
node.kubernetes.io/instance-type: gpu-h100-sxm
|
||||
volumes:
|
||||
- name: devshm
|
||||
emptyDir:
|
||||
medium: Memory
|
||||
- name: hf-cache
|
||||
hostPath:
|
||||
path: /mnt/hf-cache
|
||||
type: DirectoryOrCreate
|
||||
@@ -0,0 +1,303 @@
|
||||
steps:
|
||||
- label: "Simple Unit Test"
|
||||
depends_on: upload-ready-pipeline
|
||||
commands:
|
||||
- "pytest -v -s -m 'core_model and cpu' --cov=vllm_omni --cov-branch --cov-report=term-missing --cov-report=html --cov-report=xml"
|
||||
agents:
|
||||
queue: "gpu_1_queue"
|
||||
plugins:
|
||||
- docker#v5.2.0:
|
||||
image: public.ecr.aws/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
|
||||
always-pull: true
|
||||
propagate-environment: true
|
||||
environment:
|
||||
- "HF_HOME=/fsx/hf_cache"
|
||||
volumes:
|
||||
- "/fsx/hf_cache:/fsx/hf_cache"
|
||||
|
||||
- label: "Diffusion Model Test"
|
||||
timeout_in_minutes: 20
|
||||
depends_on: upload-ready-pipeline
|
||||
commands:
|
||||
- pytest -s -v tests/e2e/offline_inference/test_t2i_model.py -m "core_model and diffusion" --run-level "core_model"
|
||||
agents:
|
||||
queue: "gpu_1_queue" # g6.4xlarge instance on AWS, has 1 L4 GPU
|
||||
plugins:
|
||||
- docker#v5.2.0:
|
||||
image: public.ecr.aws/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
|
||||
always-pull: true
|
||||
propagate-environment: true
|
||||
environment:
|
||||
- "HF_HOME=/fsx/hf_cache"
|
||||
volumes:
|
||||
- "/fsx/hf_cache:/fsx/hf_cache"
|
||||
|
||||
- label: "Diffusion Model CPU offloading Test"
|
||||
timeout_in_minutes: 20
|
||||
depends_on: upload-ready-pipeline
|
||||
commands:
|
||||
- pytest -s -v tests/e2e/offline_inference/test_diffusion_cpu_offload.py
|
||||
- pytest -s -v tests/e2e/offline_inference/test_diffusion_layerwise_offload.py
|
||||
agents:
|
||||
queue: "gpu_1_queue" # g6.4xlarge instance on AWS, has 1 L4 GPU
|
||||
plugins:
|
||||
- docker#v5.2.0:
|
||||
image: public.ecr.aws/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
|
||||
always-pull: true
|
||||
propagate-environment: true
|
||||
environment:
|
||||
- "HF_HOME=/fsx/hf_cache"
|
||||
volumes:
|
||||
- "/fsx/hf_cache:/fsx/hf_cache"
|
||||
|
||||
- label: "Audio Generation Model Test"
|
||||
timeout_in_minutes: 20
|
||||
depends_on: upload-ready-pipeline
|
||||
commands:
|
||||
- pytest -s -v tests/e2e/offline_inference/test_stable_audio_model.py
|
||||
agents:
|
||||
queue: "gpu_1_queue" # g6.4xlarge instance on AWS, has 1 L4 GPU
|
||||
plugins:
|
||||
- docker#v5.2.0:
|
||||
image: public.ecr.aws/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
|
||||
always-pull: true
|
||||
propagate-environment: true
|
||||
environment:
|
||||
- "HF_HOME=/fsx/hf_cache"
|
||||
volumes:
|
||||
- "/fsx/hf_cache:/fsx/hf_cache"
|
||||
|
||||
- label: "Diffusion Cache Backend Test"
|
||||
timeout_in_minutes: 15
|
||||
depends_on: upload-ready-pipeline
|
||||
commands:
|
||||
- pytest -s -v -m 'core_model and cache and diffusion and not distributed_cuda and L4'
|
||||
agents:
|
||||
queue: "gpu_1_queue" # g6.4xlarge instance on AWS, has 1 L4 GPU
|
||||
plugins:
|
||||
- docker#v5.2.0:
|
||||
image: public.ecr.aws/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
|
||||
always-pull: true
|
||||
propagate-environment: true
|
||||
environment:
|
||||
- "HF_HOME=/fsx/hf_cache"
|
||||
volumes:
|
||||
- "/fsx/hf_cache:/fsx/hf_cache"
|
||||
|
||||
- label: "Diffusion Sequence Parallelism Test"
|
||||
timeout_in_minutes: 20
|
||||
depends_on: upload-ready-pipeline
|
||||
commands:
|
||||
- pytest -s -v tests/e2e/offline_inference/test_sequence_parallel.py -m core_model
|
||||
agents:
|
||||
queue: "gpu_4_queue" # g6.12xlarge instance on AWS, has 4 L4 GPU
|
||||
plugins:
|
||||
- docker#v5.2.0:
|
||||
image: public.ecr.aws/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
|
||||
always-pull: true
|
||||
propagate-environment: true
|
||||
shm-size: "8gb"
|
||||
environment:
|
||||
- "HF_HOME=/fsx/hf_cache"
|
||||
volumes:
|
||||
- "/fsx/hf_cache:/fsx/hf_cache"
|
||||
|
||||
- label: "Diffusion GPU Worker Test"
|
||||
timeout_in_minutes: 20
|
||||
depends_on: upload-ready-pipeline
|
||||
commands:
|
||||
- pytest -s -v tests/diffusion/test_diffusion_worker.py
|
||||
agents:
|
||||
queue: "gpu_4_queue" # g6.12xlarge instance on AWS, has 4 L4 GPU
|
||||
plugins:
|
||||
- docker#v5.2.0:
|
||||
image: public.ecr.aws/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
|
||||
always-pull: true
|
||||
propagate-environment: true
|
||||
shm-size: "8gb"
|
||||
environment:
|
||||
- "HF_HOME=/fsx/hf_cache"
|
||||
volumes:
|
||||
- "/fsx/hf_cache:/fsx/hf_cache"
|
||||
|
||||
# - label: "Benchmark & Engine Test with H100"
|
||||
# timeout_in_minutes: 15
|
||||
# depends_on: upload-ready-pipeline
|
||||
# commands:
|
||||
# - export VLLM_WORKER_MULTIPROC_METHOD=spawn
|
||||
# - pytest -s -v tests/benchmarks/test_serve_cli.py
|
||||
# - pytest -s -v tests/engine/test_async_omni_engine_abort.py
|
||||
# agents:
|
||||
# queue: "mithril-h100-pool"
|
||||
# plugins:
|
||||
# - kubernetes:
|
||||
# podSpec:
|
||||
# containers:
|
||||
# - image: public.ecr.aws/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
|
||||
# resources:
|
||||
# limits:
|
||||
# nvidia.com/gpu: 2
|
||||
# volumeMounts:
|
||||
# - name: devshm
|
||||
# mountPath: /dev/shm
|
||||
# - name: hf-cache
|
||||
# mountPath: /root/.cache/huggingface
|
||||
# env:
|
||||
# - name: HF_HOME
|
||||
# value: /root/.cache/huggingface
|
||||
# nodeSelector:
|
||||
# node.kubernetes.io/instance-type: gpu-h100-sxm
|
||||
# volumes:
|
||||
# - name: devshm
|
||||
# emptyDir:
|
||||
# medium: Memory
|
||||
# - name: hf-cache
|
||||
# hostPath:
|
||||
# path: /mnt/hf-cache
|
||||
# type: DirectoryOrCreate
|
||||
|
||||
- label: "Omni Model Test"
|
||||
timeout_in_minutes: 17
|
||||
depends_on: upload-ready-pipeline
|
||||
commands:
|
||||
- export VLLM_LOGGING_LEVEL=DEBUG
|
||||
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
|
||||
- pytest -s -v tests/e2e/offline_inference/test_qwen2_5_omni.py
|
||||
agents:
|
||||
queue: "gpu_4_queue" # g6.12xlarge instance on AWS, has 4 L4 GPU
|
||||
plugins:
|
||||
- docker#v5.2.0:
|
||||
image: public.ecr.aws/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
|
||||
always-pull: true
|
||||
propagate-environment: true
|
||||
environment:
|
||||
- "HF_HOME=/fsx/hf_cache"
|
||||
volumes:
|
||||
- "/fsx/hf_cache:/fsx/hf_cache"
|
||||
|
||||
# - label: "Omni Model Test with H100"
|
||||
# timeout_in_minutes: 20
|
||||
# depends_on: upload-ready-pipeline
|
||||
# commands:
|
||||
# - export VLLM_WORKER_MULTIPROC_METHOD=spawn
|
||||
# - export VLLM_TEST_CLEAN_GPU_MEMORY="1"
|
||||
# # - pytest -s -v tests/e2e/offline_inference/test_qwen3_omni.py
|
||||
# - pytest -s -v tests/e2e/online_serving/test_qwen3_omni.py -m "core_model" --run-level "core_model"
|
||||
# agents:
|
||||
# queue: "mithril-h100-pool"
|
||||
# plugins:
|
||||
# - kubernetes:
|
||||
# podSpec:
|
||||
# containers:
|
||||
# - image: public.ecr.aws/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
|
||||
# resources:
|
||||
# limits:
|
||||
# nvidia.com/gpu: 2
|
||||
# volumeMounts:
|
||||
# - name: devshm
|
||||
# mountPath: /dev/shm
|
||||
# - name: hf-cache
|
||||
# mountPath: /root/.cache/huggingface
|
||||
# env:
|
||||
# - name: HF_HOME
|
||||
# value: /root/.cache/huggingface
|
||||
# nodeSelector:
|
||||
# node.kubernetes.io/instance-type: gpu-h100-sxm
|
||||
# volumes:
|
||||
# - name: devshm
|
||||
# emptyDir:
|
||||
# medium: Memory
|
||||
# - name: hf-cache
|
||||
# hostPath:
|
||||
# path: /mnt/hf-cache
|
||||
# type: DirectoryOrCreate
|
||||
|
||||
- label: "Qwen3-TTS E2E Test"
|
||||
timeout_in_minutes: 20
|
||||
depends_on: upload-ready-pipeline
|
||||
commands:
|
||||
- export VLLM_LOGGING_LEVEL=DEBUG
|
||||
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
|
||||
- pytest -s -v tests/e2e/online_serving/test_qwen3_tts.py
|
||||
agents:
|
||||
queue: "gpu_4_queue"
|
||||
plugins:
|
||||
- docker#v5.2.0:
|
||||
image: public.ecr.aws/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
|
||||
always-pull: true
|
||||
propagate-environment: true
|
||||
shm-size: "8gb"
|
||||
environment:
|
||||
- "HF_HOME=/fsx/hf_cache"
|
||||
volumes:
|
||||
- "/fsx/hf_cache:/fsx/hf_cache"
|
||||
|
||||
# - label: "Diffusion Image Edit Test with H100 (1 GPU)"
|
||||
# timeout_in_minutes: 20
|
||||
# depends_on: upload-ready-pipeline
|
||||
# commands:
|
||||
# - export VLLM_WORKER_MULTIPROC_METHOD=spawn
|
||||
# - pytest -s -v tests/e2e/online_serving/test_image_gen_edit.py
|
||||
# agents:
|
||||
# queue: "mithril-h100-pool"
|
||||
# plugins:
|
||||
# - kubernetes:
|
||||
# podSpec:
|
||||
# containers:
|
||||
# - image: public.ecr.aws/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
|
||||
# resources:
|
||||
# limits:
|
||||
# nvidia.com/gpu: 1
|
||||
# volumeMounts:
|
||||
# - name: devshm
|
||||
# mountPath: /dev/shm
|
||||
# - name: hf-cache
|
||||
# mountPath: /root/.cache/huggingface
|
||||
# env:
|
||||
# - name: HF_HOME
|
||||
# value: /root/.cache/huggingface
|
||||
# nodeSelector:
|
||||
# node.kubernetes.io/instance-type: gpu-h100-sxm
|
||||
# volumes:
|
||||
# - name: devshm
|
||||
# emptyDir:
|
||||
# medium: Memory
|
||||
# - name: hf-cache
|
||||
# hostPath:
|
||||
# path: /mnt/hf-cache
|
||||
# type: DirectoryOrCreate
|
||||
|
||||
# - label: "Bagel Text2Img Model Test with H100"
|
||||
# timeout_in_minutes: 30
|
||||
# depends_on: upload-ready-pipeline
|
||||
# commands:
|
||||
# - export VLLM_WORKER_MULTIPROC_METHOD=spawn
|
||||
# - pytest -s -v tests/e2e/offline_inference/test_bagel_text2img.py
|
||||
# agents:
|
||||
# queue: "mithril-h100-pool"
|
||||
# plugins:
|
||||
# - kubernetes:
|
||||
# podSpec:
|
||||
# containers:
|
||||
# - image: public.ecr.aws/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
|
||||
# resources:
|
||||
# limits:
|
||||
# nvidia.com/gpu: 1
|
||||
# volumeMounts:
|
||||
# - name: devshm
|
||||
# mountPath: /dev/shm
|
||||
# - name: hf-cache
|
||||
# mountPath: /root/.cache/huggingface
|
||||
# env:
|
||||
# - name: HF_HOME
|
||||
# value: /root/.cache/huggingface
|
||||
# nodeSelector:
|
||||
# node.kubernetes.io/instance-type: gpu-h100-sxm
|
||||
# volumes:
|
||||
# - name: devshm
|
||||
# emptyDir:
|
||||
# medium: Memory
|
||||
# - name: hf-cache
|
||||
# hostPath:
|
||||
# path: /mnt/hf-cache
|
||||
# type: DirectoryOrCreate
|
||||
@@ -0,0 +1,53 @@
|
||||
{# vllm-omni customized version
|
||||
Based on: https://github.com/vllm-project/ci-infra/blob/main/buildkite/test-template-amd.j2
|
||||
Last synced: 2025-12-15
|
||||
Modifications: Removed unused CUDA/NVIDIA logic, keeping only AMD tests
|
||||
#}
|
||||
{% set docker_image_amd = "public.ecr.aws/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT-rocm-omni" %}
|
||||
{% set default_working_dir = "/app/vllm-omni" %}
|
||||
|
||||
- group: "AMD Tests"
|
||||
depends_on: ~
|
||||
steps:
|
||||
- label: "AMD: :docker: build image"
|
||||
depends_on: ~
|
||||
soft_fail: false
|
||||
commands:
|
||||
- "source .buildkite/scripts/docker_login_ecr_public.sh && safe_docker_login_ecr_public"
|
||||
- "docker build -f docker/Dockerfile.rocm -t {{ docker_image_amd }} --target test --build-arg ARG_PYTORCH_ROCM_ARCH=gfx942 --progress plain ."
|
||||
- "docker push {{ docker_image_amd }}"
|
||||
key: "amd-build"
|
||||
env:
|
||||
DOCKER_BUILDKIT: "1"
|
||||
retry:
|
||||
automatic:
|
||||
- exit_status: -1 # Agent was lost
|
||||
limit: 1
|
||||
- exit_status: -10 # Agent was lost
|
||||
limit: 1
|
||||
- exit_status: 1 # Machine occasionally fail
|
||||
limit: 1
|
||||
agents:
|
||||
queue: cpu_queue_premerge
|
||||
|
||||
{% for step in steps %}
|
||||
{% if step.mirror_hardwares and mirror_hw in step.mirror_hardwares %}
|
||||
- label: "{{ step.agent_pool }}: {{ step.label }}"
|
||||
depends_on: amd-build
|
||||
agents:
|
||||
{% if step.agent_pool %}
|
||||
queue: amd_{{ step.agent_pool }}
|
||||
{% else %}
|
||||
queue: amd_mi325_1
|
||||
{% endif %}
|
||||
command: bash .buildkite/scripts/hardware_ci/run-amd-test.sh "(command rocm-smi || true) && cd {{ (step.working_dir or default_working_dir) | safe }} ; {{ step.command or (step.commands | join(" && ")) | safe }}"
|
||||
env:
|
||||
DOCKER_BUILDKIT: "1"
|
||||
priority: 100
|
||||
{% if step.grade and step.grade == "Blocking" %}
|
||||
soft_fail: false
|
||||
{% else %}
|
||||
soft_fail: true
|
||||
{% endif%}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
@@ -24,7 +24,7 @@ body:
|
||||
description: |
|
||||
Please run the following and paste the output below.
|
||||
```sh
|
||||
wget https://raw.githubusercontent.com/vllm-project/vllm-omni/main/vllm_omni/collect_env.py
|
||||
wget https://raw.githubusercontent.com/vllm-project/vllm-omni/main/collect_env.py
|
||||
# For security purposes, please feel free to check the contents of collect_env.py before running it.
|
||||
python collect_env.py
|
||||
```
|
||||
@@ -40,6 +40,31 @@ body:
|
||||
</details>
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Your code version
|
||||
description: |
|
||||
Please provide a description of which commit id or version the code is.
|
||||
|
||||
value: |
|
||||
<details>
|
||||
<summary>The commit id or version of vllm</summary>
|
||||
|
||||
```text
|
||||
|
||||
```
|
||||
</details>
|
||||
<details>
|
||||
<summary>The commit id or version of vllm-omni</summary>
|
||||
|
||||
```text
|
||||
|
||||
```
|
||||
</details>
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: 🐛 Describe the bug
|
||||
|
||||
@@ -20,6 +20,8 @@ body:
|
||||
label: Proposed Change.
|
||||
description: >
|
||||
The proposed change of the RFC.
|
||||
value: |
|
||||
Please provide the detailed design document of the RFC using the [template](https://docs.google.com/document/d/12YxSsVeD1jvL-InClkeAEnZyWFDndz_65JmXvsamuV4/edit?tab=t.0#heading=h.4ef4szrsgspp).
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Questions
|
||||
url: https://github.com/vllm-project/vllm-omni/discussions
|
||||
about: Ask questions and discuss with other vLLM-Omni community members
|
||||
url: https://slack.vllm.ai
|
||||
about: Ask questions and discuss with other vLLM-Omni community members in our #sig-omni slack channel.
|
||||
@@ -12,9 +12,9 @@ PLEASE FILL IN THE PR DESCRIPTION HERE ENSURING ALL CHECKLIST ITEMS (AT THE BOTT
|
||||
<summary> Essential Elements of an Effective PR Description Checklist </summary>
|
||||
|
||||
- [ ] The purpose of the PR, such as "Fix some issue (link existing issues this PR will resolve)".
|
||||
- [ ] The test plan, such as providing test command.
|
||||
- [ ] The test results, such as pasting the results comparison before and after, or e2e results
|
||||
- [ ] (Optional) The necessary documentation update, such as updating `supported_models.md` and `examples` for a new model.
|
||||
- [ ] The test plan. Please providing the test scripts & test commands. Please state the reasons if your codes don't require additional test scripts. For test file guidelines, please check the [test style doc](https://docs.vllm.ai/projects/vllm-omni/en/latest/contributing/ci/tests_style/)
|
||||
- [ ] The test results. Please pasting the results comparison before and after, or e2e results.
|
||||
- [ ] (Optional) The necessary documentation update, such as updating `supported_models.md` and `examples` for a new model. **Please run `mkdocs serve` to sync the documentation editions to `./docs`.**
|
||||
- [ ] (Optional) Release notes update. If your change is user facing, please update the release notes draft.
|
||||
</details>
|
||||
|
||||
|
||||
Executable
+629
@@ -0,0 +1,629 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
PR Reviewer using GLM API for vllm-omni project.
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, TypedDict
|
||||
|
||||
import requests
|
||||
|
||||
|
||||
# Type definitions for API responses
|
||||
class PRDetails(TypedDict):
|
||||
"""Type definition for GitHub PR details response."""
|
||||
|
||||
title: str
|
||||
body: str
|
||||
number: int
|
||||
state: str
|
||||
user: dict[str, Any]
|
||||
|
||||
|
||||
class GLMMessage(TypedDict):
|
||||
"""Type definition for GLM API message."""
|
||||
|
||||
role: str
|
||||
content: str
|
||||
|
||||
|
||||
class GLMChoice(TypedDict):
|
||||
"""Type definition for GLM API choice."""
|
||||
|
||||
message: GLMMessage
|
||||
finish_reason: str
|
||||
|
||||
|
||||
class GLMResponse(TypedDict):
|
||||
"""Type definition for GLM API response."""
|
||||
|
||||
choices: list[GLMChoice]
|
||||
usage: dict[str, int] | None
|
||||
|
||||
|
||||
class GitHubComment(TypedDict):
|
||||
"""Type definition for GitHub comment."""
|
||||
|
||||
id: int
|
||||
body: str
|
||||
created_at: str
|
||||
user: dict[str, Any]
|
||||
|
||||
|
||||
# Configuration
|
||||
TRIGGER_PHRASE: str = "@vllm-omni-reviewer"
|
||||
DEFAULT_GLM_API_URL: str = "https://open.bigmodel.cn/api/paas/v4/chat/completions" # noqa: E501
|
||||
DEFAULT_GLM_MODEL: str = "glm-5"
|
||||
DEFAULT_COOLDOWN_MINUTES: int = 5
|
||||
DEFAULT_MAX_RETRIES: int = 3
|
||||
DEFAULT_RETRY_DELAY: float = 1.0
|
||||
MAX_DIFF_SIZE: int = 100_000 # Maximum diff size in characters
|
||||
|
||||
|
||||
@dataclass
|
||||
class Config:
|
||||
"""Configuration for the PR reviewer."""
|
||||
|
||||
glm_api_url: str
|
||||
glm_model: str
|
||||
cooldown_minutes: int
|
||||
max_retries: int
|
||||
retry_delay: float
|
||||
max_diff_size: int
|
||||
|
||||
|
||||
# Setup logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="[PR Reviewer] %(asctime)s - %(levelname)s - %(message)s",
|
||||
datefmt="%Y-%m-%d %H:%M:%S",
|
||||
)
|
||||
logger: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_config() -> Config:
|
||||
"""Load configuration from environment variables with defaults."""
|
||||
return Config(
|
||||
glm_api_url=os.getenv("GLM_API_URL", DEFAULT_GLM_API_URL),
|
||||
glm_model=os.getenv("GLM_MODEL", DEFAULT_GLM_MODEL),
|
||||
cooldown_minutes=int(
|
||||
os.getenv(
|
||||
"PR_REVIEWER_COOLDOWN_MINUTES",
|
||||
str(DEFAULT_COOLDOWN_MINUTES),
|
||||
)
|
||||
),
|
||||
max_retries=int(
|
||||
os.getenv(
|
||||
"PR_REVIEWER_MAX_RETRIES",
|
||||
str(DEFAULT_MAX_RETRIES),
|
||||
)
|
||||
),
|
||||
retry_delay=float(os.getenv("PR_REVIEWER_RETRY_DELAY", str(DEFAULT_RETRY_DELAY))),
|
||||
max_diff_size=int(os.getenv("PR_REVIEWER_MAX_DIFF_SIZE", str(MAX_DIFF_SIZE))), # noqa: E501
|
||||
)
|
||||
|
||||
|
||||
def get_env_var(name: str) -> str:
|
||||
"""
|
||||
Get an environment variable or raise an error.
|
||||
|
||||
Args:
|
||||
name: Name of the environment variable.
|
||||
|
||||
Returns:
|
||||
The value of the environment variable.
|
||||
|
||||
Raises:
|
||||
SystemExit: If the environment variable is not set.
|
||||
"""
|
||||
value = os.environ.get(name)
|
||||
if not value:
|
||||
logger.error(f"Environment variable {name} is not set")
|
||||
sys.exit(1)
|
||||
return value
|
||||
|
||||
|
||||
def check_trigger(comment_body: str) -> bool:
|
||||
"""
|
||||
Check if the comment contains the trigger phrase.
|
||||
|
||||
Args:
|
||||
comment_body: The body of the comment to check.
|
||||
|
||||
Returns:
|
||||
True if the trigger phrase is found, False otherwise.
|
||||
"""
|
||||
return TRIGGER_PHRASE in comment_body
|
||||
|
||||
|
||||
def fetch_pr_diff(
|
||||
repo_name: str,
|
||||
pr_number: int,
|
||||
token: str,
|
||||
max_size: int = MAX_DIFF_SIZE,
|
||||
) -> str | None:
|
||||
"""
|
||||
Fetch the diff for a pull request.
|
||||
|
||||
Args:
|
||||
repo_name: The repository name in format "owner/repo".
|
||||
pr_number: The pull request number.
|
||||
token: GitHub authentication token.
|
||||
max_size: Maximum diff size in characters.
|
||||
|
||||
Returns:
|
||||
The diff content as a string, or None if fetching failed.
|
||||
Returns empty string if diff is larger than max_size.
|
||||
"""
|
||||
url: str = f"https://api.github.com/repos/{repo_name}/pulls/{pr_number}"
|
||||
headers: dict[str, str] = {
|
||||
"Authorization": f"Bearer {token}",
|
||||
"Accept": "application/vnd.github.v3.diff",
|
||||
}
|
||||
|
||||
logger.info(f"Fetching PR diff from {url}")
|
||||
response = requests.get(url, headers=headers, timeout=30)
|
||||
|
||||
if response.status_code == 200:
|
||||
diff: str = response.text
|
||||
if len(diff) > max_size:
|
||||
logger.warning(
|
||||
f"Diff size ({len(diff)} bytes) exceeds maximum "
|
||||
f"({max_size} bytes), truncating to first "
|
||||
f"{max_size} bytes"
|
||||
)
|
||||
return diff[:max_size] + "\n\n... [Diff truncated due to size] ..."
|
||||
logger.info(f"Successfully fetched diff ({len(diff)} bytes)")
|
||||
return diff
|
||||
else:
|
||||
logger.error(f"Failed to fetch PR diff: {response.status_code}")
|
||||
logger.error(f"Response: {response.text}")
|
||||
return None
|
||||
|
||||
|
||||
def fetch_pr_details(
|
||||
repo_name: str,
|
||||
pr_number: int,
|
||||
token: str,
|
||||
) -> PRDetails | None:
|
||||
"""
|
||||
Fetch PR details including title and description.
|
||||
|
||||
Args:
|
||||
repo_name: The repository name in format "owner/repo".
|
||||
pr_number: The pull request number.
|
||||
token: GitHub authentication token.
|
||||
|
||||
Returns:
|
||||
A dictionary containing PR details, or None if fetching failed.
|
||||
"""
|
||||
url: str = f"https://api.github.com/repos/{repo_name}/pulls/{pr_number}"
|
||||
headers: dict[str, str] = {
|
||||
"Authorization": f"Bearer {token}",
|
||||
"Accept": "application/vnd.github.v3+json",
|
||||
}
|
||||
|
||||
logger.info(f"Fetching PR details from {url}")
|
||||
response = requests.get(url, headers=headers, timeout=30)
|
||||
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
else:
|
||||
logger.error(f"Failed to fetch PR details: {response.status_code}")
|
||||
return None
|
||||
|
||||
|
||||
def build_review_prompt(pr_title: str, pr_description: str, diff: str) -> str:
|
||||
"""
|
||||
Build the prompt for the GLM-4.7 API.
|
||||
|
||||
Args:
|
||||
pr_title: The title of the pull request.
|
||||
pr_description: The description/body of the pull request.
|
||||
diff: The diff content of the pull request.
|
||||
|
||||
Returns:
|
||||
The formatted prompt string for the API.
|
||||
"""
|
||||
return f"""You are an expert code reviewer for the VLLM-Omni project. \
|
||||
Please review the following pull request:
|
||||
|
||||
## Pull Request Details
|
||||
**Title:** {pr_title}
|
||||
|
||||
**Description:**
|
||||
{pr_description if pr_description else "No description provided."}
|
||||
|
||||
## Code Changes (Diff)
|
||||
{diff}
|
||||
|
||||
## Review Guidelines
|
||||
|
||||
Please provide a comprehensive code review with the following sections:
|
||||
|
||||
### 1. Overview
|
||||
- Brief summary of the changes
|
||||
- Overall assessment (positive, neutral, or concerns)
|
||||
|
||||
### 2. Code Quality
|
||||
- Code style and consistency
|
||||
- Potential bugs or edge cases
|
||||
- Performance considerations
|
||||
- Error handling
|
||||
|
||||
### 3. Architecture & Design
|
||||
- Integration with existing codebase
|
||||
- Design patterns and best practices
|
||||
- Potential improvements
|
||||
|
||||
### 4. Security & Safety
|
||||
- Security concerns (if any)
|
||||
- Resource management
|
||||
- Input validation
|
||||
|
||||
### 5. Testing & Documentation
|
||||
- Test coverage considerations
|
||||
- Documentation completeness
|
||||
- Examples and usage clarity
|
||||
|
||||
### 6. Specific Suggestions
|
||||
- Line-by-line specific feedback (use `file:line` format)
|
||||
- Concrete actionable suggestions
|
||||
- Code examples for improvements (if applicable)
|
||||
|
||||
### 7. Approval Status
|
||||
- **LGTM** (Looks Good To Me) if the PR is ready to merge
|
||||
- **LGTM with suggestions** if the PR is good but has minor suggestions
|
||||
- **Changes requested** if significant changes are needed
|
||||
|
||||
## Important Notes
|
||||
- Be constructive and helpful
|
||||
- Focus on objective technical feedback
|
||||
- Acknowledge good practices when you see them
|
||||
- Prioritize critical issues over nitpicks
|
||||
- If the diff is empty or minimal, acknowledge this and provide
|
||||
any relevant context-specific guidance
|
||||
|
||||
Please format your response in Markdown with clear section headers.
|
||||
"""
|
||||
|
||||
|
||||
def validate_glm_response(data: dict[str, Any]) -> str | None:
|
||||
"""
|
||||
Validate and extract content from GLM API response.
|
||||
|
||||
Args:
|
||||
data: The response data from GLM API.
|
||||
|
||||
Returns:
|
||||
The review content string if valid, None otherwise.
|
||||
"""
|
||||
# Check if choices exists and is a non-empty list
|
||||
if "choices" not in data:
|
||||
logger.error("GLM API response missing 'choices' field")
|
||||
logger.error(f"Response structure: {json.dumps(data, indent=2)}")
|
||||
return None
|
||||
|
||||
choices = data["choices"]
|
||||
if not isinstance(choices, list):
|
||||
logger.error(f"GLM API 'choices' is not a list: {type(choices)}")
|
||||
return None
|
||||
|
||||
if len(choices) == 0:
|
||||
logger.error("GLM API 'choices' is an empty list")
|
||||
return None
|
||||
|
||||
# Check if first choice has message
|
||||
try:
|
||||
first_choice = choices[0]
|
||||
if not isinstance(first_choice, dict):
|
||||
logger.error(f"GLM API choice is not a dict: {type(first_choice)}")
|
||||
return None
|
||||
|
||||
if "message" not in first_choice:
|
||||
logger.error("GLM API choice missing 'message' field")
|
||||
logger.error(f"Choice structure: {json.dumps(first_choice, indent=2)}") # noqa: E501
|
||||
return None
|
||||
|
||||
message = first_choice["message"]
|
||||
if not isinstance(message, dict):
|
||||
logger.error(f"GLM API message is not a dict: {type(message)}")
|
||||
return None
|
||||
|
||||
if "content" not in message:
|
||||
logger.error("GLM API message missing 'content' field")
|
||||
logger.error(f"Message structure: {json.dumps(message, indent=2)}")
|
||||
return None
|
||||
|
||||
content = message["content"]
|
||||
if not isinstance(content, str):
|
||||
logger.error(f"GLM API content is not a string: {type(content)}")
|
||||
return None
|
||||
|
||||
return content
|
||||
|
||||
except (KeyError, IndexError, TypeError) as e:
|
||||
logger.error(f"Failed to parse GLM API response: {e}")
|
||||
logger.error(f"Response: {json.dumps(data, indent=2)}")
|
||||
return None
|
||||
|
||||
|
||||
def call_glm_api(prompt: str, api_key: str, config: Config) -> str | None:
|
||||
"""
|
||||
Call the GLM-4.7 API to get code review with retry logic.
|
||||
|
||||
Args:
|
||||
prompt: The prompt to send to the API.
|
||||
api_key: The GLM API key.
|
||||
config: Configuration object.
|
||||
|
||||
Returns:
|
||||
The review content as a string, or None if all retries failed.
|
||||
"""
|
||||
headers: dict[str, str] = {
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
payload: dict[str, Any] = {
|
||||
"model": config.glm_model,
|
||||
"messages": [{"role": "user", "content": prompt}],
|
||||
"temperature": 0.3,
|
||||
"max_tokens": 32000,
|
||||
"top_p": 0.9,
|
||||
}
|
||||
|
||||
last_error: str | None = None
|
||||
|
||||
for attempt in range(config.max_retries):
|
||||
try:
|
||||
logger.info(f"Calling GLM API ({config.glm_model}) - Attempt {attempt + 1}/{config.max_retries}")
|
||||
response = requests.post(
|
||||
config.glm_api_url,
|
||||
headers=headers,
|
||||
json=payload,
|
||||
timeout=120,
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
review = validate_glm_response(data)
|
||||
if review:
|
||||
logger.info(f"Successfully received review ({len(review)} chars)") # noqa: E501
|
||||
return review
|
||||
else:
|
||||
last_error = "Failed to validate API response structure"
|
||||
logger.error(last_error)
|
||||
else:
|
||||
last_error = f"GLM API request failed: {response.status_code} - {response.text}"
|
||||
logger.error(last_error)
|
||||
|
||||
except requests.exceptions.Timeout:
|
||||
last_error = f"GLM API request timed out (attempt {attempt + 1})"
|
||||
logger.error(last_error)
|
||||
except requests.exceptions.RequestException as e:
|
||||
last_error = f"GLM API request exception: {e}"
|
||||
logger.error(last_error)
|
||||
except json.JSONDecodeError as e:
|
||||
last_error = f"Failed to decode GLM API response as JSON: {e}"
|
||||
logger.error(last_error)
|
||||
|
||||
# Exponential backoff before retry
|
||||
if attempt < config.max_retries - 1:
|
||||
wait_time: float = config.retry_delay * (2**attempt)
|
||||
logger.info(f"Waiting {wait_time}s before retry...") # noqa: E501
|
||||
time.sleep(wait_time)
|
||||
|
||||
logger.error(
|
||||
f"All {config.max_retries} attempts failed. Last error: {last_error}" # noqa: E501
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def check_cooldown( # noqa: E501
|
||||
repo_name: str,
|
||||
pr_number: int,
|
||||
token: str,
|
||||
cooldown_minutes: int,
|
||||
) -> bool:
|
||||
"""
|
||||
Check if the PR is within the cooldown period.
|
||||
|
||||
Args:
|
||||
repo_name: The repository name in format "owner/repo".
|
||||
pr_number: The pull request number.
|
||||
token: GitHub authentication token.
|
||||
cooldown_minutes: Cooldown period in minutes.
|
||||
|
||||
Returns:
|
||||
True if within cooldown period (should skip), False otherwise.
|
||||
"""
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
url: str = (
|
||||
f"https://api.github.com/repos/{repo_name}/issues/"
|
||||
f"{pr_number}/comments" # noqa: E501
|
||||
)
|
||||
headers: dict[str, str] = {
|
||||
"Authorization": f"Bearer {token}",
|
||||
"Accept": "application/vnd.github.v3+json",
|
||||
}
|
||||
|
||||
logger.info(f"Checking cooldown period ({cooldown_minutes} minutes)")
|
||||
response = requests.get(url, headers=headers, timeout=30)
|
||||
|
||||
if response.status_code != 200:
|
||||
logger.warning(f"Failed to check cooldown: {response.status_code}, proceeding with review")
|
||||
return False
|
||||
|
||||
comments: list[dict[str, Any]] = response.json()
|
||||
cutoff_time: datetime = datetime.utcnow() - timedelta(minutes=cooldown_minutes) # noqa: E501
|
||||
|
||||
for comment in reversed(comments):
|
||||
# Check if this is a bot comment
|
||||
body: str = comment.get("body", "")
|
||||
if "VLLM-Omni PR Review" in body or "PR Reviewer Bot" in body:
|
||||
created_at_str: str = comment.get("created_at", "")
|
||||
try:
|
||||
# Parse GitHub timestamp format
|
||||
created_at = datetime.fromisoformat(created_at_str.replace("Z", "+00:00"))
|
||||
created_at = created_at.replace(tzinfo=None)
|
||||
if created_at > cutoff_time:
|
||||
logger.info(f"PR is within cooldown period (last review: {created_at_str})")
|
||||
return True
|
||||
except ValueError:
|
||||
logger.warning(f"Failed to parse comment timestamp: {created_at_str}") # noqa: E501
|
||||
continue
|
||||
|
||||
logger.info("PR is outside cooldown period, proceeding with review")
|
||||
return False
|
||||
|
||||
|
||||
def post_review_comment( # noqa: E501
|
||||
repo_name: str,
|
||||
pr_number: int,
|
||||
token: str,
|
||||
review: str,
|
||||
) -> bool:
|
||||
"""
|
||||
Post the review as a comment on the PR.
|
||||
|
||||
Args:
|
||||
repo_name: The repository name in format "owner/repo".
|
||||
pr_number: The pull request number.
|
||||
token: GitHub authentication token.
|
||||
review: The review content to post.
|
||||
|
||||
Returns:
|
||||
True if posting succeeded, False otherwise.
|
||||
"""
|
||||
url: str = (
|
||||
f"https://api.github.com/repos/{repo_name}/issues/"
|
||||
f"{pr_number}/comments" # noqa: E501
|
||||
)
|
||||
headers: dict[str, str] = {
|
||||
"Authorization": f"Bearer {token}",
|
||||
"Accept": "application/vnd.github.v3+json",
|
||||
}
|
||||
|
||||
# Format the review comment
|
||||
comment_body: str = f"""## 🤖 VLLM-Omni PR Review
|
||||
|
||||
{review}
|
||||
|
||||
---
|
||||
*This review was generated automatically by the VLLM-Omni PR Reviewer Bot
|
||||
using {os.getenv("GLM_MODEL", DEFAULT_GLM_MODEL)}.*
|
||||
"""
|
||||
|
||||
payload: dict[str, str] = {"body": comment_body}
|
||||
|
||||
logger.info(f"Posting review comment to PR #{pr_number}")
|
||||
response = requests.post(url, headers=headers, json=payload, timeout=30)
|
||||
|
||||
if response.status_code == 201:
|
||||
logger.info("Successfully posted review comment")
|
||||
return True
|
||||
else:
|
||||
logger.error(f"Failed to post comment: {response.status_code}")
|
||||
logger.error(f"Response: {response.text}")
|
||||
return False
|
||||
|
||||
|
||||
def main() -> int:
|
||||
"""
|
||||
Main entry point for the PR reviewer bot.
|
||||
|
||||
Returns:
|
||||
0 on success, 1 on error.
|
||||
"""
|
||||
logger.info("VLLM-Omni PR Reviewer Bot starting...")
|
||||
|
||||
# Load configuration
|
||||
config: Config = get_config()
|
||||
logger.info(
|
||||
f"Configuration: model={config.glm_model}, "
|
||||
f"cooldown={config.cooldown_minutes}min, "
|
||||
f"max_retries={config.max_retries}"
|
||||
)
|
||||
|
||||
# Get environment variables
|
||||
token: str = get_env_var("GITHUB_TOKEN")
|
||||
api_key: str = get_env_var("GLM_API_KEY")
|
||||
repo_name: str = get_env_var("REPO_NAME")
|
||||
pr_number_str: str = get_env_var("PR_NUMBER")
|
||||
comment_body: str = get_env_var("COMMENT_BODY")
|
||||
|
||||
try:
|
||||
pr_number: int = int(pr_number_str)
|
||||
except ValueError:
|
||||
logger.error(f"Invalid PR number: {pr_number_str}")
|
||||
return 1
|
||||
|
||||
logger.info(f"Repository: {repo_name}")
|
||||
logger.info(f"PR Number: {pr_number}")
|
||||
|
||||
# Check if the comment contains the trigger phrase
|
||||
if not check_trigger(comment_body):
|
||||
logger.info(
|
||||
f"Comment does not contain trigger phrase '{TRIGGER_PHRASE}', exiting" # noqa: E501
|
||||
)
|
||||
return 0
|
||||
|
||||
logger.info("Trigger phrase detected! Starting review process...")
|
||||
|
||||
# Check cooldown period
|
||||
if check_cooldown(repo_name, pr_number, token, config.cooldown_minutes):
|
||||
logger.info("Skipping review due to cooldown period")
|
||||
return 0
|
||||
|
||||
# Fetch PR details
|
||||
logger.info("Step 1/4: Fetching PR details...")
|
||||
pr_details: PRDetails | None = fetch_pr_details(repo_name, pr_number, token) # noqa: E501
|
||||
if not pr_details:
|
||||
logger.error("Failed to fetch PR details")
|
||||
return 1
|
||||
|
||||
pr_title: str = pr_details.get("title", "Unknown")
|
||||
pr_description: str = pr_details.get("body", "")
|
||||
|
||||
logger.info(f"PR Title: {pr_title}")
|
||||
|
||||
# Fetch PR diff
|
||||
logger.info("Step 2/4: Fetching PR diff...")
|
||||
diff: str | None = fetch_pr_diff(repo_name, pr_number, token, config.max_diff_size)
|
||||
if diff is None:
|
||||
logger.error("Failed to fetch PR diff")
|
||||
return 1
|
||||
|
||||
if not diff:
|
||||
logger.warning("Warning: Empty diff - this might be a draft PR or no code changes")
|
||||
|
||||
# Build prompt
|
||||
logger.info("Step 3/4: Building review prompt...")
|
||||
prompt: str = build_review_prompt(pr_title, pr_description, diff)
|
||||
|
||||
# Call GLM API
|
||||
logger.info("Step 4/4: Calling GLM API...")
|
||||
review: str | None = call_glm_api(prompt, api_key, config)
|
||||
if not review:
|
||||
logger.error("Failed to get review from GLM API")
|
||||
return 1
|
||||
|
||||
# Post review comment
|
||||
logger.info("Posting review comment...")
|
||||
if not post_review_comment(repo_name, pr_number, token, review):
|
||||
logger.error("Failed to post review comment")
|
||||
return 1
|
||||
|
||||
logger.info("PR review completed successfully!")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,63 @@
|
||||
name: Build Wheel
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths-ignore:
|
||||
- 'docs/**'
|
||||
- 'examples/**'
|
||||
- 'apps/**'
|
||||
- '.github/**'
|
||||
- '.buildkite/**'
|
||||
- '**.md'
|
||||
- '*.yml'
|
||||
- '.gitignore'
|
||||
- '.gitattributes'
|
||||
- 'LICENSE'
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
paths-ignore:
|
||||
- 'docs/**'
|
||||
- 'examples/**'
|
||||
- 'apps/**'
|
||||
- '.github/**'
|
||||
- '.buildkite/**'
|
||||
- '*.yml'
|
||||
- '**.md'
|
||||
- '.gitignore'
|
||||
- '.gitattributes'
|
||||
- 'LICENSE'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.11", "3.12"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Install uv
|
||||
run: |
|
||||
pip install uv
|
||||
|
||||
- name: Build Wheel
|
||||
env:
|
||||
UV_SYSTEM_PYTHON: 1
|
||||
run: |
|
||||
bash scripts/build_wheel.sh --python python
|
||||
|
||||
- name: Upload Wheel Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: vllm-omni-wheel-py${{ matrix.python-version }}
|
||||
path: dist/*.whl
|
||||
@@ -0,0 +1,27 @@
|
||||
name: ComfyUI Integration -- Publish to Comfy registry
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- "*"
|
||||
paths:
|
||||
- "apps/ComfyUI-vLLM-Omni/**"
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: apps/ComfyUI-vLLM-Omni
|
||||
|
||||
jobs:
|
||||
publish-node:
|
||||
name: Publish Custom Node to registry
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: ♻️ Check out code
|
||||
uses: actions/checkout@v4
|
||||
- name: 📦 Publish Custom Node
|
||||
uses: Comfy-Org/publish-node-action@main
|
||||
with:
|
||||
personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }}
|
||||
@@ -0,0 +1,40 @@
|
||||
name: ComfyUI Integration -- Validate backwards compatibility
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- main
|
||||
paths:
|
||||
- "apps/ComfyUI-vLLM-Omni/**"
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: apps/ComfyUI-vLLM-Omni
|
||||
|
||||
jobs:
|
||||
check-base-path:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
exists: ${{ steps.check.outputs.exists }}
|
||||
defaults:
|
||||
run:
|
||||
working-directory: .
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.base.sha }}
|
||||
fetch-depth: 1
|
||||
- id: check
|
||||
run: |
|
||||
if [ -d "apps/ComfyUI-vLLM-Omni" ]; then
|
||||
echo "exists=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "exists=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
validate:
|
||||
needs: check-base-path
|
||||
if: needs.check-base-path.outputs.exists == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: comfy-org/node-diff@main
|
||||
@@ -0,0 +1,62 @@
|
||||
name: VLLM-Omni PR Reviewer
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
pr-reviewer:
|
||||
name: Review Pull Request
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
# Only run when the comment is from a collaborator/owner/member
|
||||
if: |
|
||||
github.event_name == 'issue_comment' &&
|
||||
github.event.issue.pull_request != null &&
|
||||
contains(github.event.comment.body, '@vllm-omni-reviewer') &&
|
||||
(github.event.comment.author_association == 'MEMBER' ||
|
||||
github.event.comment.author_association == 'COLLABORATOR' ||
|
||||
github.event.comment.author_association == 'OWNER')
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4.2.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5.3.0
|
||||
with:
|
||||
python-version: '3.11'
|
||||
cache: 'pip'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip==24.0
|
||||
pip install requests==2.31.0 pyyaml==6.0.1
|
||||
|
||||
- name: Run PR Reviewer
|
||||
id: reviewer
|
||||
env:
|
||||
GLM_API_KEY: ${{ secrets.GLM_API_KEY }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PR_NUMBER: ${{ github.event.issue.number || github.event.pull_request.number }}
|
||||
COMMENT_BODY: ${{ github.event.comment.body }}
|
||||
REPO_NAME: ${{ github.repository }}
|
||||
PR_REVIEWER_COOLDOWN_MINUTES: 5
|
||||
PR_REVIEWER_MAX_RETRIES: 3
|
||||
run: |
|
||||
python .github/scripts/pr_reviewer.py 2>&1 | tee "/tmp/pr_review_${PR_NUMBER}.log"
|
||||
|
||||
- name: Upload review logs
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4.4.3
|
||||
with:
|
||||
name: pr-review-logs-${{ github.event.issue.number || github.event.pull_request.number }}
|
||||
path: /tmp/pr_review_*.log
|
||||
retention-days: 7
|
||||
if-no-files-found: ignore
|
||||
+16
-6
@@ -153,18 +153,20 @@ dmypy.json
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# Claude
|
||||
CLAUDE.md
|
||||
.claude/
|
||||
|
||||
# Codex
|
||||
AGENTS.md
|
||||
.codex/
|
||||
|
||||
# cursor
|
||||
.cursor/
|
||||
|
||||
# docker
|
||||
docker/
|
||||
|
||||
# scripts
|
||||
/scripts/
|
||||
|
||||
# tests
|
||||
tests/
|
||||
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be added to the global gitignore or merged into this project gitignore. For a PyCharm
|
||||
@@ -185,6 +187,7 @@ checkpoints/
|
||||
|
||||
# Cache directories
|
||||
cache/
|
||||
!vllm_omni/diffusion/cache/
|
||||
.cache/
|
||||
diffusion_cache/
|
||||
kv_cache/
|
||||
@@ -219,6 +222,7 @@ data/
|
||||
datasets/
|
||||
*.csv
|
||||
*.json
|
||||
!apps/ComfyUI-vLLM-Omni/example_workflows/*.json
|
||||
*.jsonl
|
||||
*.parquet
|
||||
|
||||
@@ -226,6 +230,7 @@ datasets/
|
||||
outputs/
|
||||
results/
|
||||
generated/
|
||||
output_*/
|
||||
|
||||
# Configuration overrides
|
||||
configs/local.yaml
|
||||
@@ -235,3 +240,8 @@ configs/development.yaml
|
||||
# Docker
|
||||
.dockerignore
|
||||
Dockerfile.dev
|
||||
discussion
|
||||
tmp_test
|
||||
|
||||
# output files
|
||||
*.wav
|
||||
+13
-3
@@ -3,7 +3,7 @@ default_install_hook_types:
|
||||
- commit-msg
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.6.0
|
||||
rev: v6.0.0
|
||||
hooks:
|
||||
# list of supported hooks: https://pre-commit.com/hooks.html
|
||||
- id: check-yaml
|
||||
@@ -16,19 +16,22 @@ repos:
|
||||
args: ["--markdown-linebreak-ext=md"]
|
||||
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.14.0
|
||||
rev: v0.14.10
|
||||
hooks:
|
||||
- id: ruff-check
|
||||
args: [--output-format, github, --fix]
|
||||
- id: ruff-format
|
||||
|
||||
- repo: https://github.com/crate-ci/typos
|
||||
rev: v1.35.5
|
||||
rev: typos-dict-v0.13.13
|
||||
hooks:
|
||||
- id: typos
|
||||
# only for staged files
|
||||
|
||||
- repo: https://github.com/rhysd/actionlint
|
||||
# v1.7.8+ sets `go 1.24.0` in go.mod, which older Go toolchains (and most
|
||||
# current CI images) cannot parse. Pin to v1.7.7 until actionlint fixes the
|
||||
# go.mod directive.
|
||||
rev: v1.7.7
|
||||
hooks:
|
||||
- id: actionlint
|
||||
@@ -58,3 +61,10 @@ repos:
|
||||
verbose: true
|
||||
pass_filenames: false
|
||||
# Insert new entries above the `suggestion` entry
|
||||
|
||||
- id: check-pickle-imports
|
||||
name: Prevent new pickle/cloudpickle imports
|
||||
entry: python tools/pre_commit/check_pickle_imports.py
|
||||
language: python
|
||||
types: [python]
|
||||
additional_dependencies: [regex]
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
# Contributing to vLLM-Omni
|
||||
|
||||
You may find information about contributing to vLLM-Omni on [Contributing](docs/contributing/)
|
||||
You may find information about contributing to vLLM-Omni on [Contributing](https://vllm-omni.readthedocs.io/en/latest/contributing/)
|
||||
@@ -1,7 +1,7 @@
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" src="./docs/source/logos/vllm-omni-logo-text-dark.png">
|
||||
<img alt="vllm-omni" src="./docs/source/logos/vllm-omni-logo-text-dark.png" width=55%>
|
||||
<source media="(prefers-color-scheme: dark)" src="https://raw.githubusercontent.com/vllm-project/vllm-omni/refs/heads/main/docs/source/logos/vllm-omni-logo.png">
|
||||
<img alt="vllm-omni" src="https://raw.githubusercontent.com/vllm-project/vllm-omni/refs/heads/main/docs/source/logos/vllm-omni-logo.png" width=55%>
|
||||
</picture>
|
||||
</p>
|
||||
<h3 align="center">
|
||||
@@ -9,13 +9,16 @@ Easy, fast, and cheap omni-modality model serving for everyone
|
||||
</h3>
|
||||
|
||||
<p align="center">
|
||||
| <a href="https://vllm-omni.readthedocs.io/en/latest/"><b>Documentation</b></a> | <a href="https://tinyurl.com/vllm-omni-meeting"><b>Weekly Meeting</b></a> |
|
||||
| <a href="https://vllm-omni.readthedocs.io/en/latest/"><b>Documentation</b></a> | <a href="https://discuss.vllm.ai"><b>User Forum</b></a> | <a href="https://slack.vllm.ai"><b>Developer Slack</b></a> | <a href="docs/assets/WeChat.jpg"><b>WeChat</b></a> | <a href="https://arxiv.org/abs/2602.02204"><b>Paper</b></a> | <a href="https://docs.google.com/presentation/d/1qv4qMW1rKAqDREMXiUDLIgqqHQe7TDPj/edit?usp=sharing&ouid=110473603432222024453&rtpof=true&sd=true"><b>Slides</b></a> |
|
||||
</p>
|
||||
|
||||
|
||||
---
|
||||
|
||||
*Latest News* 🔥
|
||||
|
||||
- [2026/02] We released [0.14.0](https://github.com/vllm-project/vllm-omni/releases/tag/v0.14.0) - This is the first **stable release** of vLLM-Omni that expands Omni’s diffusion / image-video generation and audio / TTS stack, improves distributed execution and memory efficiency, and broadens platform/backend coverage (GPU/ROCm/NPU/XPU). It also brings meaningful upgrades to serving APIs, profiling & benchmarking, and overall stability. Please check our latest [paper](https://arxiv.org/abs/2602.02204) for architecture design and performance results.
|
||||
- [2026/01] We released [0.12.0rc1](https://github.com/vllm-project/vllm-omni/releases/tag/v0.12.0rc1) - a major RC milestone focused on maturing the diffusion stack, strengthening OpenAI-compatible serving, expanding omni-model coverage, and improving stability across platforms (GPU/NPU/ROCm).
|
||||
- [2025/11] vLLM community officially released [vllm-project/vllm-omni](https://github.com/vllm-project/vllm-omni) in order to support omni-modality models serving.
|
||||
|
||||
---
|
||||
@@ -30,7 +33,7 @@ Easy, fast, and cheap omni-modality model serving for everyone
|
||||
|
||||
<p align="center">
|
||||
<picture>
|
||||
<img alt="vllm-omni" src="./docs/source/architecture/omni-modality model architecture.png" width=55%>
|
||||
<img alt="vllm-omni" src="https://raw.githubusercontent.com/vllm-project/vllm-omni/refs/heads/main/docs/source/architecture/omni-modality-model-architecture.png" width=55%>
|
||||
</picture>
|
||||
</p>
|
||||
|
||||
@@ -57,8 +60,6 @@ vLLM-Omni seamlessly supports most popular open-source models on HuggingFace, in
|
||||
|
||||
Visit our [documentation](https://vllm-omni.readthedocs.io/en/latest/) to learn more.
|
||||
|
||||
Visit our documentation to learn more.
|
||||
|
||||
- [Installation](https://vllm-omni.readthedocs.io/en/latest/getting_started/installation/)
|
||||
- [Quickstart](https://vllm-omni.readthedocs.io/en/latest/getting_started/quickstart/)
|
||||
- [List of Supported Models](https://vllm-omni.readthedocs.io/en/latest/models/supported_models/)
|
||||
@@ -68,10 +69,25 @@ Visit our documentation to learn more.
|
||||
We welcome and value any contributions and collaborations.
|
||||
Please check out [Contributing to vLLM-Omni](https://vllm-omni.readthedocs.io/en/latest/contributing/) for how to get involved.
|
||||
|
||||
## Weekly Meeting
|
||||
## Citation
|
||||
|
||||
- vLLM Omni Weekly Meeting: https://tinyurl.com/vllm-omni-meeting
|
||||
- Wednesday, 11:30 - 12:30 (UTC+8, [Convert to your timezone](https://dateful.com/convert/gmt8?t=15))
|
||||
If you use vLLM-Omni for your research, please cite our [paper](https://arxiv.org/abs/2602.02204):
|
||||
|
||||
```bibtex
|
||||
@article{yin2026vllmomni,
|
||||
title={vLLM-Omni: Fully Disaggregated Serving for Any-to-Any Multimodal Models},
|
||||
author={Peiqi Yin, Jiangyun Zhu, Han Gao, Chenguang Zheng, Yongxiang Huang, Taichang Zhou, Ruirui Yang, Weizhi Liu, Weiqing Chen, Canlin Guo, Didan Deng, Zifeng Mo, Cong Wang, James Cheng, Roger Wang, Hongsheng Liu},
|
||||
journal={arXiv preprint arXiv:2602.02204},
|
||||
year={2026}
|
||||
}
|
||||
```
|
||||
|
||||
## Join the Community
|
||||
Feel free to ask questions, provide feedbacks and discuss with fellow users of vLLM-Omni in `#sig-omni` slack channel at [slack.vllm.ai](https://slack.vllm.ai) or vLLM user forum at [discuss.vllm.ai](https://discuss.vllm.ai).
|
||||
|
||||
## Star History
|
||||
|
||||
[](https://www.star-history.com/#vllm-project/vllm-omni&type=date&legend=top-left)
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# OSX useful to ignore
|
||||
*.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
# Thumbnails
|
||||
._*
|
||||
|
||||
# Files that might appear in the root of a volume
|
||||
.DocumentRevisions-V100
|
||||
.fseventsd
|
||||
.Spotlight-V100
|
||||
.TemporaryItems
|
||||
.Trashes
|
||||
.VolumeIcon.icns
|
||||
.com.apple.timemachine.donotpresent
|
||||
|
||||
# Directories potentially created on remote AFP share
|
||||
.AppleDB
|
||||
.AppleDesktop
|
||||
Network Trash Folder
|
||||
Temporary Items
|
||||
.apdisk
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
env/
|
||||
venv/
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# IntelliJ Idea
|
||||
.idea
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# Cookiecutter
|
||||
output/
|
||||
python_boilerplate/
|
||||
cookiecutter-pypackage-env/
|
||||
|
||||
# vscode settings
|
||||
.history/
|
||||
*.code-workspace
|
||||
|
||||
# Frontend extension
|
||||
node_modules/
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
node.zip
|
||||
.vscode/
|
||||
.claude/
|
||||
.codemate/
|
||||
@@ -0,0 +1,15 @@
|
||||
Apache Software License 2.0
|
||||
|
||||
Copyright (c) 2026, Zeyu Huang
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -0,0 +1,170 @@
|
||||
# vLLM-Omni
|
||||
|
||||
vLLM-Omni offers a ComfyUI integration on top of its online serving API.
|
||||
It can send model inference requests to either a locally running vLLM-Omni service or a remote one.
|
||||
|
||||
## Requirement
|
||||
|
||||
- Python 3.12 or above
|
||||
- [ComfyUI installed](https://docs.comfy.org/installation/system_requirements)
|
||||
- [vLLM-Omni installed](https://docs.vllm.ai/projects/vllm-omni/en/latest/getting_started/installation/) on either the same device or another device discoverable via the internet.
|
||||
- No need to install additional packages apart from those already required by ComfyUI.
|
||||
|
||||
> [!TIP]
|
||||
> If you run both ComfyUI and vLLM-Omni on the same device, you can create separate virtual environments and use different Python versions for them.
|
||||
|
||||
|
||||
## Installation
|
||||
|
||||
Copy this folder to the `custom_nodes` subfolder of your ComfyUI installation. Your directory should look like `ComfyUI/custom_nodes/ComfyUI-vLLM-Omni`.
|
||||
|
||||
If you are running ComfyUI during copying, you should restart ComfyUI to load this extension.
|
||||
|
||||
> [!TIP]
|
||||
> You can use utility websites such as https://download-directory.github.io/ to download a subdirectory of a repo. Also checkout community discussions (e.g., https://stackoverflow.com/questions/7106012/download-a-single-folder-or-directory-from-a-github-repository) for more info.
|
||||
|
||||
On the device and virtual environment you run ComfyUI, launch ComfyUI with
|
||||
```bash
|
||||
cd ComfyUI
|
||||
|
||||
# The regular way
|
||||
python main.py
|
||||
|
||||
# If you are mainly using this node, launch it faster with
|
||||
python main.py --cpu
|
||||
```
|
||||
|
||||
On the device and virtual environment you run vLLM-Omni, start a model service with
|
||||
```bash
|
||||
vllm serve The_Model_ID_to_Serve --omni --port 8000
|
||||
```
|
||||
|
||||
Check **ComfyUI's sidebar -> Node Library**. There should be a new folder named **vLLM-Omni**.
|
||||
If no, check your shell running the ComfyUI process. There may be some error messages before the line `Import times for custom nodes:` and the line `To see the GUI go to: http://127.0.0.1:8188`.
|
||||
|
||||
## Quickstart
|
||||
|
||||
This extension offers the following nodes based on the output modalities (at **ComfyUI sidebar -> Node Library**):
|
||||
|
||||
- **Generate Image** for text-to-image and image-to-image tasks
|
||||
- **Multimodality Comprehension** for multimodality-to-text and multimodality-to-audio tasks
|
||||
- **TTS** and **TTS Voice Clone** for TTS tasks
|
||||
|
||||
This extension also offers example workflows (at **ComfyUI sidebar -> Templates -> vLLM-Omni**)
|
||||
|
||||
> [!INFO]
|
||||
> The node UI and feature designs are intended to match vLLM-Omni online serving interfaces. It cannot offer more than what the interfaces support.
|
||||
|
||||
To build a simple workflow yourself,
|
||||
|
||||
- Drag a generation node onto the canvas.
|
||||
- Depending on your need, grab built-in multimedia file loader nodes, such as **image->Load Image**, **image->video->Load Video**, **audio->Load Audio**
|
||||
- Depending on your need, grab built-in multimedia file preview nodes, such as **image->Preview Image**, **image->video->Save Video**, **audio->Preview Audio**. For text output, you can install [ComfyUI-Custom-Scripts plugin](https://github.com/pythongosssss/ComfyUI-Custom-Scripts/) and grab its **utils->Show Text 🐍** node.
|
||||
- If you want to tune sampling parameters, grab corresponding nodes from **vLLM-Omni-> Sampling Params**.
|
||||
- For multi-stage models, you can connect multiple **AR Sampling Params** and **Diffusion Sampling Params** nodes to a **Multi-Stage Sampling Params List** node, and connect this node to the generation node.
|
||||
- For some multi-stage models like BAGEL, [only one stage's sampling parameters are exposed and tunable via vLLM-Omni's online serving API](https://docs.vllm.ai/projects/vllm-omni/en/latest/user_guide/examples/online_serving/bagel/). Thus, these models are treated as single-stage ones. Please check the vLLM-Omni documentation on how to correctly set each model's sampling parameters.
|
||||
- For multi-stage models where all stages are either autoregression or diffusion, you can also connect only a single Sampling Params node, indicating that this set of sampling parameters will be used for all stages.
|
||||
|
||||
**The following features are tested**:
|
||||
|
||||
- Single-node workflows for
|
||||
- Multimodal Comprehension (e.g., Qwen Omni, BAGEL)
|
||||
- Text-to-Image Generation (e.g., Qwen-Image)
|
||||
- Image-to-Image Generation (e.g., Qwen-Image-Edit)
|
||||
- TTS (e.g., Qwen TTS, including VoiceDesign, VoiceClone, CustomVoice)
|
||||
|
||||
**The following features are not currently tested**. They may work or break. You are welcomed to test it out and offer comments.
|
||||
|
||||
- Multi-node workflow that connects multiple model services together.
|
||||
|
||||
## Screenshots and Examples
|
||||
|
||||
### Multimodal comprehension (e.g., Qwen Omni series, BAGEL)
|
||||
|
||||
(Also available at **ComfyUI sidebar->Template->vLLM-Omni->vLLM-Omni Annotated Example**)
|
||||
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" src="https://raw.githubusercontent.com/vllm-project/vllm-omni/refs/heads/main/apps/ComfyUI-vLLM-Omni/docs/images/comfyui-comprehension.jpg">
|
||||
<img alt="vLLM-Omni Main Architecture" src="https://raw.githubusercontent.com/vllm-project/vllm-omni/refs/heads/main/apps/ComfyUI-vLLM-Omni/docs/images/comfyui-comprehension.jpg" width=55%>
|
||||
</picture>
|
||||
</p>
|
||||
|
||||
> [!TIP]
|
||||
> Although this node enables all-modality input, you should check whether the specific model you host and request for supports the modalities you connect to the node.
|
||||
|
||||
You can configure per-stage sampling parameters for multi-stage models.
|
||||
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" src="https://raw.githubusercontent.com/vllm-project/vllm-omni/refs/heads/main/apps/ComfyUI-vLLM-Omni/docs/images/comfyui-multi-stage.jpg">
|
||||
<img alt="vLLM-Omni Main Architecture" src="https://raw.githubusercontent.com/vllm-project/vllm-omni/refs/heads/main/apps/ComfyUI-vLLM-Omni/docs/images/comfyui-multi-stage.jpg" width=55%>
|
||||
</picture>
|
||||
</p>
|
||||
|
||||
### Text-to-image and image-to-image generation (e.g., Z-Image-Turbo, Qwen-Image-Edit, BAGEL)
|
||||
|
||||
(Also available at **ComfyUI sidebar->Template->vLLM-Omni->vLLM-Omni Image Generation**)
|
||||
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" src="https://raw.githubusercontent.com/vllm-project/vllm-omni/refs/heads/main/apps/ComfyUI-vLLM-Omni/docs/images/comfyui-image-generation.jpg">
|
||||
<img alt="vLLM-Omni Main Architecture" src="https://raw.githubusercontent.com/vllm-project/vllm-omni/refs/heads/main/apps/ComfyUI-vLLM-Omni/docs/images/comfyui-image-generation.jpg" width=55%>
|
||||
</picture>
|
||||
</p>
|
||||
|
||||
> [!TIP]
|
||||
> The node automatically choose text-to-image or image-to-image API endpoints depending on whether you connect an image input or not.
|
||||
|
||||
### TTS (e.g., Qwen TTS series)
|
||||
|
||||
(Also available at **ComfyUI sidebar->Template->vLLM-Omni->vLLM-Omni TTS**)
|
||||
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" src="https://raw.githubusercontent.com/vllm-project/vllm-omni/refs/heads/main/apps/ComfyUI-vLLM-Omni/docs/images/comfyui-tts.jpg">
|
||||
<img alt="vLLM-Omni Main Architecture" src="https://raw.githubusercontent.com/vllm-project/vllm-omni/refs/heads/main/apps/ComfyUI-vLLM-Omni/docs/images/comfyui-tts.jpg" width=55%>
|
||||
</picture>
|
||||
</p>
|
||||
|
||||
> [!TIP]
|
||||
> There is a dedicated node for VoiceClone tasks with reference audio input. Other simple text-to-speech tasks should use the regular TTS node.
|
||||
|
||||
## Develop
|
||||
|
||||
Follow the [development convention and rules of vLLM-Omni](https://docs.vllm.ai/projects/vllm-omni/en/latest/contributing/).
|
||||
|
||||
## Limitation and Non-Goals
|
||||
|
||||
- Single server mode only. No automatic load balancing or failover.
|
||||
- Features set is bounded to vLLM-Omni's online service capability, including
|
||||
- The types of models supported in online mode,
|
||||
- The types of sampling parameters supported in the online mode,
|
||||
- The ways to send files (primarily through full-length base64 in JSON payload),
|
||||
- Figuring out errors in the payload (such as unsupported fields by a specific model) if the endpoint does not explicitly return an error,
|
||||
- (The lack of) Authentication
|
||||
- (The lack of) Progress indicator
|
||||
|
||||
## Support
|
||||
|
||||
If you are new to ComfyUI, please check out [its documentation](https://docs.comfy.org/) for usage instructions.
|
||||
|
||||
If you are new to vLLM-Omni, please also check out [its documentation](https://docs.vllm.ai/projects/vllm-omni/en/latest/) for usage instructions.
|
||||
|
||||
Whenever you find an issue or problem, please
|
||||
|
||||
- First find out if this is an upstream limitation of vLLM-Omni's online serving mode, by [checking their documentation](https://docs.vllm.ai/projects/vllm-omni/en/latest/examples/).
|
||||
- [Open an issue](https://github.com/vllm-project/vllm-omni/issues) that clearly describes this ComfyUI or online service problem.
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
Features
|
||||
|
||||
- https://github.com/dougbtv/comfyui-vllm-omni/ The official reference implementation for ComfyUI integration with vLLM-Omni's DALL-E compatible image generation API.
|
||||
- https://github.com/Comfy-Org/ComfyUI/tree/master/comfy_extras ComfyUI's built-in node implementations.
|
||||
|
||||
UI/UX design references
|
||||
|
||||
- https://github.com/sgl-project/sglang/pull/15271 SGLang Diffusion's official ComfyUI integration for image and video generation.
|
||||
- https://github.com/SXQBW/ComfyUI-Qwen-Omni A third party ComfyUI integration for Qwen Omni series.
|
||||
- https://github.com/flybirdxx/ComfyUI-Qwen-TTS https://github.com/DarioFT/ComfyUI-Qwen3-TTS Third party ComfyUI integrations for Qwen TTS series.
|
||||
@@ -0,0 +1,52 @@
|
||||
"""Top-level package for comfyui_vllm_omni.""" # noqa: N999 # This is not a python library intended to be imported
|
||||
|
||||
__all__ = [
|
||||
"NODE_CLASS_MAPPINGS",
|
||||
"NODE_DISPLAY_NAME_MAPPINGS",
|
||||
"WEB_DIRECTORY",
|
||||
]
|
||||
|
||||
__author__ = """Zeyu Huang"""
|
||||
__email__ = "11222265+fhfuih@users.noreply.github.com"
|
||||
__version__ = "0.0.1"
|
||||
|
||||
from .comfyui_vllm_omni.nodes import (
|
||||
VLLMOmniARSampling,
|
||||
VLLMOmniComprehension,
|
||||
VLLMOmniDiffusionSampling,
|
||||
VLLMOmniGenerateImage,
|
||||
VLLMOmniQwenTTSParams,
|
||||
VLLMOmniSamplingParamsList,
|
||||
VLLMOmniTTS,
|
||||
VLLMOmniVoiceClone,
|
||||
)
|
||||
|
||||
# A dictionary that contains all nodes you want to export with their names
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
# === Generation ===
|
||||
"VLLMOmniGenerateImage": VLLMOmniGenerateImage,
|
||||
"VLLMOmniComprehension": VLLMOmniComprehension,
|
||||
"VLLMOmniTTS": VLLMOmniTTS,
|
||||
"VLLMOmniVoiceClone": VLLMOmniVoiceClone,
|
||||
# === Params ===
|
||||
"VLLMOmniARSampling": VLLMOmniARSampling,
|
||||
"VLLMOmniDiffusionSampling": VLLMOmniDiffusionSampling,
|
||||
"VLLMOmniSamplingParamsList": VLLMOmniSamplingParamsList,
|
||||
"VLLMOmniQwenTTSParams": VLLMOmniQwenTTSParams,
|
||||
}
|
||||
|
||||
# A dictionary that contains the friendly/humanly readable titles for the nodes
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
# === Generation ===
|
||||
"VLLMOmniGenerateImage": "Generate Image",
|
||||
"VLLMOmniComprehension": "Multimodality Comprehension",
|
||||
"VLLMOmniTTS": "TTS (Text to Speech)",
|
||||
"VLLMOmniVoiceClone": "TTS Voice Cloning",
|
||||
# === Params ===
|
||||
"VLLMOmniARSampling": "AR Sampling Params",
|
||||
"VLLMOmniDiffusionSampling": "Diffusion Sampling Params",
|
||||
"VLLMOmniSamplingParamsList": "Multi-Stage Sampling Params List",
|
||||
"VLLMOmniQwenTTSParams": "Qwen TTS Params",
|
||||
}
|
||||
|
||||
WEB_DIRECTORY = "./web"
|
||||
@@ -0,0 +1 @@
|
||||
# noqa: N999 # This is not a python library intended to be imported
|
||||
@@ -0,0 +1,569 @@
|
||||
from typing import Literal, cast
|
||||
|
||||
import torch
|
||||
from comfy_api.input import AudioInput, VideoInput
|
||||
|
||||
from .utils.api_client import VLLMOmniClient
|
||||
from .utils.logger import get_logger
|
||||
from .utils.models import lookup_model_spec
|
||||
from .utils.types import AudioFormat
|
||||
from .utils.validators import (
|
||||
add_sampling_parameters_to_stage,
|
||||
validate_model_and_sampling_params_types,
|
||||
)
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class _VLLMOmniGenerateBase:
|
||||
"""Base class for vLLM-Omni generation nodes with shared functionality."""
|
||||
|
||||
CATEGORY = "vLLM-Omni"
|
||||
|
||||
@classmethod
|
||||
def VALIDATE_INPUTS(cls, url, model) -> str | Literal[True]:
|
||||
"""
|
||||
Can only validate this model's own input. Cannot check inputs from other nodes.
|
||||
See: https://docs.comfy.org/custom-nodes/backend/server_overview#validate_inputs
|
||||
"""
|
||||
if not url:
|
||||
return "URL must not be empty"
|
||||
if not model:
|
||||
return "Model must not be empty"
|
||||
return True
|
||||
|
||||
|
||||
class VLLMOmniGenerateImage(_VLLMOmniGenerateBase):
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"url": ("STRING", {"default": "http://localhost:8000/v1"}),
|
||||
"model": ("STRING", {"default": "Tongyi-MAI/Z-Image-Turbo"}),
|
||||
"prompt": ("STRING", {"multiline": True}),
|
||||
"negative_prompt": ("STRING", {"multiline": True, "default": ""}),
|
||||
"width": ("INT", {"default": 512, "min": 64, "max": 2048}),
|
||||
"height": ("INT", {"default": 512, "min": 64, "max": 2048}),
|
||||
},
|
||||
"optional": {
|
||||
"image": ("IMAGE",),
|
||||
"mask": ("MASK",),
|
||||
# "video": ("VIDEO",),
|
||||
# "audio": ("AUDIO",),
|
||||
"sampling_params": ("SAMPLING_PARAMS",),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
RETURN_NAMES = ("image",)
|
||||
FUNCTION = "generate"
|
||||
|
||||
async def generate(
|
||||
self,
|
||||
url: str,
|
||||
model: str,
|
||||
prompt: str,
|
||||
width: int,
|
||||
height: int,
|
||||
negative_prompt: str | None = None,
|
||||
image: torch.Tensor | None = None,
|
||||
mask: torch.Tensor | None = None,
|
||||
audio: AudioInput | None = None, # Hidden & unused
|
||||
video: VideoInput | None = None, # Hidden & unused
|
||||
sampling_params: dict | list[dict] | None = None,
|
||||
**kwargs,
|
||||
):
|
||||
logger.info("Uncaught kwargs: %s", kwargs)
|
||||
logger.debug("Got sampling params: %s", sampling_params)
|
||||
validate_model_and_sampling_params_types(model, sampling_params)
|
||||
if image is None and mask is not None:
|
||||
raise ValueError("Mask input provided without an image input.")
|
||||
|
||||
client = VLLMOmniClient(url)
|
||||
|
||||
spec, pattern = lookup_model_spec(model)
|
||||
is_bagel = pattern is not None and "bagel" in pattern.lower()
|
||||
|
||||
# Prefer DALL-E compatible API for simple (one-stage) diffusion models
|
||||
if (spec is None or spec["stages"] == ["diffusion"]) and not is_bagel:
|
||||
sampling_params = cast(dict | None, sampling_params)
|
||||
if audio is None and image is None and video is None:
|
||||
# No multimodal input --- use DALL-E image generation
|
||||
logger.info("Using DALL-E image generation endpoint")
|
||||
output = await client.generate_image(
|
||||
model=model,
|
||||
prompt=prompt,
|
||||
width=width,
|
||||
height=height,
|
||||
negative_prompt=negative_prompt,
|
||||
sampling_params=sampling_params,
|
||||
)
|
||||
return (output,)
|
||||
elif image is not None and audio is None and video is None:
|
||||
# Image and text input --- use DALL-E image edit
|
||||
logger.info("Using DALL-E image edit endpoint")
|
||||
output = await client.edit_image(
|
||||
model=model,
|
||||
prompt=prompt,
|
||||
image=image,
|
||||
width=width,
|
||||
height=height,
|
||||
negative_prompt=negative_prompt,
|
||||
mask=mask,
|
||||
sampling_params=sampling_params,
|
||||
)
|
||||
return (output,)
|
||||
|
||||
logger.info("Using chat completion endpoint")
|
||||
sampling_params = add_sampling_parameters_to_stage(
|
||||
model, sampling_params, "diffusion", width=width, height=height
|
||||
)
|
||||
logger.debug("Edited sampling params: %s", sampling_params)
|
||||
|
||||
output = await client.generate_image_chat_completion(
|
||||
model=model,
|
||||
prompt=prompt,
|
||||
negative_prompt=negative_prompt,
|
||||
image=image,
|
||||
audio=audio,
|
||||
video=video,
|
||||
sampling_params=sampling_params,
|
||||
)
|
||||
|
||||
return (output,)
|
||||
|
||||
|
||||
class VLLMOmniComprehension(_VLLMOmniGenerateBase):
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"url": ("STRING", {"default": "http://localhost:8000/v1"}),
|
||||
"model": ("STRING", {"default": "Qwen/Qwen2.5-Omni-7B"}),
|
||||
"prompt": ("STRING", {"multiline": True}),
|
||||
"output_text": ("BOOLEAN", {"default": True}),
|
||||
"output_audio": ("BOOLEAN", {"default": True}),
|
||||
"use_audio_in_video": ("BOOLEAN", {"default": True}),
|
||||
},
|
||||
"optional": {
|
||||
"image": ("IMAGE",),
|
||||
"video": ("VIDEO",),
|
||||
"audio": ("AUDIO",),
|
||||
"sampling_params": ("SAMPLING_PARAMS",),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING", "AUDIO")
|
||||
RETURN_NAMES = ("text_response", "audio_response")
|
||||
FUNCTION = "generate"
|
||||
|
||||
@classmethod
|
||||
def VALIDATE_INPUTS(cls, url, model, output_text, output_audio) -> str | Literal[True]: # type: ignore[reportIncompatibleMethodOverride]
|
||||
super_validation = super().VALIDATE_INPUTS(url, model)
|
||||
if isinstance(super_validation, str):
|
||||
return super_validation
|
||||
if not output_text and not output_audio:
|
||||
return "At least one of output_text or output_audio must be True."
|
||||
return True
|
||||
|
||||
async def generate(
|
||||
self,
|
||||
url: str,
|
||||
model: str,
|
||||
prompt: str,
|
||||
image: torch.Tensor | None = None,
|
||||
audio: AudioInput | None = None,
|
||||
video: VideoInput | None = None,
|
||||
sampling_params: dict | list[dict] | None = None,
|
||||
output_text: bool = True,
|
||||
output_audio: bool = True,
|
||||
use_audio_in_video: bool = True,
|
||||
**kwargs,
|
||||
) -> tuple[str, AudioInput]:
|
||||
logger.info("Uncaught kwargs: %s", kwargs)
|
||||
logger.debug("Got sampling params: %s", sampling_params)
|
||||
validate_model_and_sampling_params_types(model, sampling_params)
|
||||
|
||||
client = VLLMOmniClient(url)
|
||||
spec, pattern = lookup_model_spec(model)
|
||||
is_bagel = pattern is not None and "bagel" in pattern.lower()
|
||||
|
||||
if is_bagel:
|
||||
# A lot of special handlings here...
|
||||
if output_audio:
|
||||
raise ValueError("BAGEL models do not support audio output.")
|
||||
if audio is not None or video is not None:
|
||||
raise ValueError("BAGEL models do not support audio or video input.")
|
||||
(
|
||||
text_response,
|
||||
_,
|
||||
) = await client.generate_comprehension_chat_completion(
|
||||
model=model,
|
||||
prompt=prompt,
|
||||
image=image,
|
||||
audio=None,
|
||||
video=None,
|
||||
sampling_params=sampling_params,
|
||||
modalities=["text"],
|
||||
)
|
||||
else:
|
||||
modalities = []
|
||||
if output_text:
|
||||
modalities.append("text")
|
||||
if output_audio:
|
||||
modalities.append("audio")
|
||||
|
||||
if use_audio_in_video and video is not None:
|
||||
use_audio_in_video = True
|
||||
else:
|
||||
use_audio_in_video = False
|
||||
|
||||
(
|
||||
text_response,
|
||||
audio,
|
||||
) = await client.generate_comprehension_chat_completion(
|
||||
model=model,
|
||||
prompt=prompt,
|
||||
image=image,
|
||||
audio=audio,
|
||||
video=video,
|
||||
sampling_params=sampling_params,
|
||||
modalities=modalities,
|
||||
# == extra kwargs ==
|
||||
mm_processor_kwargs={"use_audio_in_video": use_audio_in_video},
|
||||
)
|
||||
|
||||
if text_response is None:
|
||||
text_response = ""
|
||||
if audio is None:
|
||||
channels = 1
|
||||
duration = 1
|
||||
sample_rate = 44100
|
||||
num_samples = int(round(duration * sample_rate))
|
||||
waveform = torch.zeros((1, channels, num_samples), dtype=torch.float32)
|
||||
audio = {"waveform": waveform, "sample_rate": sample_rate}
|
||||
|
||||
return (text_response, audio)
|
||||
|
||||
|
||||
class VLLMOmniTTS(_VLLMOmniGenerateBase):
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"url": ("STRING", {"default": "http://localhost:8000/v1"}),
|
||||
"model": (
|
||||
"STRING",
|
||||
{"default": "Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice"},
|
||||
),
|
||||
"input": ("STRING", {"multiline": True}),
|
||||
"voice": ("STRING", {"default": "Vivian"}),
|
||||
"response_format": (["mp3", "opus", "aac", "flac", "wav", "pcm"],),
|
||||
"speed": (
|
||||
"FLOAT",
|
||||
{"default": 1.0, "min": 0.25, "max": 4.0, "step": 0.01},
|
||||
),
|
||||
},
|
||||
"optional": {
|
||||
"model_specific_params": ("TTS_PARAMS",),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("AUDIO",)
|
||||
RETURN_NAMES = ("audio",)
|
||||
FUNCTION = "generate"
|
||||
|
||||
async def generate(
|
||||
self,
|
||||
url: str,
|
||||
model: str,
|
||||
input: str,
|
||||
voice: str,
|
||||
response_format: AudioFormat,
|
||||
speed: float,
|
||||
model_specific_params: dict | None,
|
||||
**kwargs,
|
||||
) -> tuple[AudioInput]:
|
||||
logger.info("Got extra kwargs in TTS: %s", kwargs)
|
||||
|
||||
is_qwen_tts = "qwen3-tts" in model.lower()
|
||||
extra_params_type = None if model_specific_params is None else model_specific_params["type"]
|
||||
if not is_qwen_tts and extra_params_type == "qwen-tts":
|
||||
raise ValueError(
|
||||
"You have provided Qwen-specific TTS params."
|
||||
"However, the model appears to not be a Qwen TTS model (no 'Qwen3-TTS' in model name)."
|
||||
)
|
||||
|
||||
combined_params = {**kwargs, **(model_specific_params or {})}
|
||||
combined_params.pop("type", None) # Internal fields in model_specific_params
|
||||
|
||||
client = VLLMOmniClient(url)
|
||||
|
||||
audio = await client.generate_speech(
|
||||
model=model,
|
||||
input=input,
|
||||
voice=voice,
|
||||
response_format=response_format,
|
||||
speed=speed,
|
||||
**combined_params,
|
||||
)
|
||||
return (audio,)
|
||||
|
||||
|
||||
class VLLMOmniVoiceClone(_VLLMOmniGenerateBase):
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"url": ("STRING", {"default": "http://localhost:8000/v1"}),
|
||||
"model": ("STRING", {"default": "Qwen/Qwen3-TTS-12Hz-1.7B-Base"}),
|
||||
"input": ("STRING", {"multiline": True}),
|
||||
"voice": ("STRING", {"default": "Vivian"}),
|
||||
"response_format": (["mp3", "opus", "aac", "flac", "wav", "pcm"],),
|
||||
"speed": (
|
||||
"FLOAT",
|
||||
{"default": 1.0, "min": 0.25, "max": 4.0, "step": 0.01},
|
||||
),
|
||||
"ref_audio": ("AUDIO",),
|
||||
"ref_text": ("STRING", {"multiline": True}),
|
||||
"x_vector_only_mode": ("BOOLEAN", {"default": False}),
|
||||
},
|
||||
"optional": {
|
||||
"model_specific_params": ("TTS_PARAMS",),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("AUDIO",)
|
||||
RETURN_NAMES = ("audio",)
|
||||
FUNCTION = "generate"
|
||||
|
||||
async def generate(
|
||||
self,
|
||||
url: str,
|
||||
model: str,
|
||||
input: str,
|
||||
voice: str,
|
||||
response_format: AudioFormat,
|
||||
speed: float,
|
||||
ref_audio: AudioInput,
|
||||
ref_text: str,
|
||||
x_vector_only_mode: bool,
|
||||
model_specific_params: dict | None,
|
||||
**kwargs,
|
||||
):
|
||||
is_qwen_tts = "qwen3-tts" in model.lower()
|
||||
extra_params_type = None if model_specific_params is None else model_specific_params["type"]
|
||||
if not is_qwen_tts and extra_params_type == "qwen-tts":
|
||||
raise ValueError(
|
||||
"You have provided Qwen-specific TTS params."
|
||||
"However, the model appears to not be a Qwen TTS model (no 'Qwen3-TTS' in model name)."
|
||||
)
|
||||
|
||||
combined_params = {
|
||||
"ref_audio": ref_audio,
|
||||
"ref_text": ref_text,
|
||||
"x_vector_only_mode": x_vector_only_mode,
|
||||
**kwargs,
|
||||
**(model_specific_params or {}),
|
||||
}
|
||||
combined_params.pop("type", None) # Internal fields in model_specific_params
|
||||
|
||||
client = VLLMOmniClient(url)
|
||||
|
||||
audio = await client.generate_speech(
|
||||
model=model,
|
||||
input=input,
|
||||
voice=voice,
|
||||
response_format=response_format,
|
||||
speed=speed,
|
||||
**combined_params,
|
||||
)
|
||||
return (audio,)
|
||||
|
||||
|
||||
class VLLMOmniARSampling:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"max_tokens": ("INT", {"default": 100, "min": 1, "max": 10000}),
|
||||
"temperature": (
|
||||
"FLOAT",
|
||||
{"default": 1.0, "min": 0.0, "max": 2.0, "step": 0.01},
|
||||
),
|
||||
"top_p": (
|
||||
"FLOAT",
|
||||
{"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01},
|
||||
),
|
||||
"repetition_penalty": (
|
||||
"FLOAT",
|
||||
{"default": 1.0, "min": 0.0, "max": 5.0, "step": 0.01},
|
||||
),
|
||||
# === Put seed at last. ===
|
||||
# Whenever a field named "seed" is present, ComfyUI adds another field called "control after generate"
|
||||
"seed": (
|
||||
"INT",
|
||||
{
|
||||
"default": -1,
|
||||
"min": -1,
|
||||
"step": 1,
|
||||
"tooltip": "-1 means to not provide a seed.",
|
||||
},
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("SAMPLING_PARAMS",)
|
||||
RETURN_NAMES = ("AR sampling params",)
|
||||
FUNCTION = "get_params"
|
||||
CATEGORY = "vLLM-Omni/Sampling Params"
|
||||
|
||||
def get_params(self, seed, **kwargs):
|
||||
params = {
|
||||
"type": "autoregression", # for internal use, removed before sending the request
|
||||
**kwargs,
|
||||
}
|
||||
if seed >= 0:
|
||||
params["seed"] = seed
|
||||
return (params,)
|
||||
|
||||
|
||||
class VLLMOmniDiffusionSampling:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"n": (
|
||||
"INT",
|
||||
{
|
||||
"default": 1,
|
||||
"min": 0,
|
||||
"max": 10,
|
||||
"step": 1,
|
||||
"tooltip": "Number of images to generate",
|
||||
},
|
||||
),
|
||||
"num_inference_steps": (
|
||||
"INT",
|
||||
{
|
||||
"default": 50,
|
||||
"min": 1,
|
||||
"max": 1000,
|
||||
"tooltip": "Number of denoising steps (higher = better quality, slower).",
|
||||
},
|
||||
),
|
||||
"guidance_scale": (
|
||||
"FLOAT",
|
||||
{
|
||||
"default": 7.5,
|
||||
"min": 0.0,
|
||||
"max": 20.0,
|
||||
"step": 0.1,
|
||||
"tooltip": "Classifier-free guidance scale (higher = more prompt adherence).",
|
||||
},
|
||||
),
|
||||
"true_cfg_scale": (
|
||||
"FLOAT",
|
||||
{
|
||||
"default": 1.0,
|
||||
"min": 0.0,
|
||||
"max": 20.0,
|
||||
"step": 0.5,
|
||||
"tooltip": "True CFG scale for advanced control (model-specific).",
|
||||
},
|
||||
),
|
||||
"vae_use_slicing": (
|
||||
"BOOLEAN",
|
||||
{
|
||||
"default": False,
|
||||
"tooltip": "Enable VAE slicing for reduced memory usage (slight quality trade-off)",
|
||||
},
|
||||
),
|
||||
# === Put seed at last. ===
|
||||
# Whenever a field named "seed" is present, ComfyUI adds another field called "control after generate"
|
||||
"seed": (
|
||||
"INT",
|
||||
{
|
||||
"default": -1,
|
||||
"min": -1,
|
||||
"step": 1,
|
||||
"tooltip": "-1 means to not provide a seed.",
|
||||
},
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("SAMPLING_PARAMS",)
|
||||
RETURN_NAMES = ("diffusion sampling params",)
|
||||
FUNCTION = "get_params"
|
||||
CATEGORY = "vLLM-Omni/Sampling Params"
|
||||
|
||||
def get_params(self, seed, **kwargs):
|
||||
params = {
|
||||
"type": "diffusion", # for internal use, removed before sending the request
|
||||
**kwargs,
|
||||
}
|
||||
if seed >= 0:
|
||||
params["seed"] = seed
|
||||
return (params,)
|
||||
|
||||
|
||||
class VLLMOmniSamplingParamsList:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"param1": ("SAMPLING_PARAMS",),
|
||||
},
|
||||
"optional": {
|
||||
"param2": ("SAMPLING_PARAMS",),
|
||||
"param3": ("SAMPLING_PARAMS",),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("SAMPLING_PARAMS",)
|
||||
RETURN_NAMES = ("param list",)
|
||||
FUNCTION = "aggregate"
|
||||
CATEGORY = "vLLM-Omni/Sampling Params"
|
||||
|
||||
def aggregate(self, param1: dict, param2: dict | None = None, param3: dict | None = None):
|
||||
for i, p in enumerate((param1, param2, param3)):
|
||||
if isinstance(p, list):
|
||||
raise ValueError(
|
||||
f"Input {i} is a Multi-Stage Sampling Params List. "
|
||||
f"Expected a single sampling parameters node (either AR or Diffusion)."
|
||||
)
|
||||
|
||||
params = [param1]
|
||||
if param2 is not None:
|
||||
params.append(param2)
|
||||
if param3 is not None:
|
||||
params.append(param3)
|
||||
return (params,)
|
||||
|
||||
|
||||
class VLLMOmniQwenTTSParams:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"task_type": (
|
||||
["CustomVoice", "VoiceDesign", "Base"],
|
||||
{"default": "CustomVoice"},
|
||||
),
|
||||
"language": (
|
||||
["Auto", "Chinese", "English", "Japanese", "Korean"],
|
||||
{"default": "Auto"},
|
||||
),
|
||||
"instructions": ("STRING", {"multiline": True}),
|
||||
"max_new_tokens": ("INT", {"default": 2048, "min": 1}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("TTS_PARAMS",)
|
||||
RETURN_NAMES = ("Qwen TTS params",)
|
||||
FUNCTION = "get_params"
|
||||
CATEGORY = "vLLM-Omni/TTS Params"
|
||||
|
||||
def get_params(self, **kwargs):
|
||||
return ({"type": "qwen-tts", **kwargs},)
|
||||
@@ -0,0 +1,473 @@
|
||||
"""
|
||||
An high-level API client adapter that forwards ComfyUI inputs to vLLM-Omni's REST API,
|
||||
and transforms the API responses back to ComfyUI formats.
|
||||
|
||||
The image generation part is derived from dougbtv/comfyui-vllm-omni by Doug (@dougbtv).
|
||||
Original source at https://github.com/dougbtv/comfyui-vllm-omni, distributed under the MIT License.
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
import aiohttp
|
||||
import av.error
|
||||
import torch
|
||||
from comfy_api.input import AudioInput, VideoInput
|
||||
|
||||
from .format import (
|
||||
audio_to_base64,
|
||||
base64_to_audio,
|
||||
base64_to_image_tensor,
|
||||
bytes_to_audio,
|
||||
image_tensor_to_base64,
|
||||
image_tensor_to_png_bytes,
|
||||
video_to_base64,
|
||||
)
|
||||
from .logger import get_logger, pretty_printer
|
||||
from .models import lookup_model_spec
|
||||
from .types import AudioFormat
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class VLLMOmniClient:
|
||||
def __init__(self, base_url: str, timeout: float = 300.0):
|
||||
self.base_url = base_url
|
||||
self.timeout = aiohttp.ClientTimeout(total=timeout)
|
||||
|
||||
async def generate_image(
|
||||
self,
|
||||
*,
|
||||
model: str,
|
||||
prompt: str,
|
||||
width: int,
|
||||
height: int,
|
||||
negative_prompt: str | None = None,
|
||||
sampling_params: dict | None = None,
|
||||
) -> torch.Tensor:
|
||||
"""Run text-to-image generation via DALLE API"""
|
||||
await self._check_model_exist(model)
|
||||
|
||||
size = f"{width}x{height}"
|
||||
payload = {
|
||||
"model": model,
|
||||
"prompt": prompt,
|
||||
"size": size,
|
||||
"response_format": "b64_json",
|
||||
}
|
||||
if negative_prompt:
|
||||
payload["negative_prompt"] = negative_prompt
|
||||
if sampling_params is not None:
|
||||
# Only select specific sampling params
|
||||
for k in (
|
||||
"n",
|
||||
"num_inference_steps",
|
||||
"guidance_scale",
|
||||
"true_cfg_scale",
|
||||
"vae_use_slicing",
|
||||
):
|
||||
if k in sampling_params and sampling_params[k] is not None:
|
||||
payload[k] = sampling_params[k]
|
||||
logger.debug("img gen payload: %s", payload)
|
||||
|
||||
url = self.base_url + "/images/generations"
|
||||
async with aiohttp.ClientSession(timeout=self.timeout) as session:
|
||||
try:
|
||||
async with session.post(
|
||||
url,
|
||||
json=payload,
|
||||
headers={"Content-Type": "application/json"},
|
||||
) as response:
|
||||
if not response.ok:
|
||||
error_text = await response.text()
|
||||
raise (ValueError if response.status < 500 else RuntimeError)(
|
||||
f"vLLM-Omni API returned status {response.status}: {error_text}"
|
||||
)
|
||||
|
||||
try:
|
||||
data = await response.json()
|
||||
except aiohttp.ContentTypeError as e:
|
||||
raise RuntimeError(f"Invalid JSON response from vLLM-Omni: {e}")
|
||||
if "data" not in data:
|
||||
raise RuntimeError("API response missing 'data' field - expected OpenAI DALL-E format")
|
||||
if not data["data"]:
|
||||
raise RuntimeError("API returned empty data array")
|
||||
|
||||
image_tensors = []
|
||||
for idx, img in enumerate(data["data"]):
|
||||
if "b64_json" not in img:
|
||||
raise RuntimeError(f"API returned image #{idx} without 'b64_json' field")
|
||||
base64_str = img["b64_json"]
|
||||
tensor = base64_to_image_tensor(base64_str)
|
||||
image_tensors.append(tensor)
|
||||
logger.debug("Image #%d has shape %s", idx, tensor.shape)
|
||||
|
||||
batch_tensor = torch.stack(image_tensors, dim=0)
|
||||
logger.debug("batch_tensor output has shape: %s", batch_tensor.shape)
|
||||
return batch_tensor
|
||||
|
||||
except aiohttp.ClientError as e:
|
||||
raise RuntimeError(f"Network error connecting to vLLM-Omni at {url}: {e}")
|
||||
|
||||
async def edit_image(
|
||||
self,
|
||||
*,
|
||||
model: str,
|
||||
prompt: str,
|
||||
image: torch.Tensor,
|
||||
width: int,
|
||||
height: int,
|
||||
negative_prompt: str | None = None,
|
||||
mask: torch.Tensor | None = None,
|
||||
sampling_params: dict | None = None,
|
||||
) -> torch.Tensor:
|
||||
"""Run image editing via DALLE API"""
|
||||
await self._check_model_exist(model)
|
||||
|
||||
size = f"{width}x{height}"
|
||||
image_filename = "image.png" # Required for multipart form
|
||||
form = aiohttp.FormData()
|
||||
form.add_field("model", model)
|
||||
form.add_field(
|
||||
"image",
|
||||
image_tensor_to_png_bytes(image, image_filename),
|
||||
filename=image_filename,
|
||||
content_type="image/png",
|
||||
)
|
||||
form.add_field("prompt", prompt)
|
||||
form.add_field("size", size)
|
||||
if negative_prompt:
|
||||
form.add_field("negative_prompt", negative_prompt)
|
||||
if sampling_params is not None:
|
||||
# Only select specific sampling params
|
||||
for k in ("n", "num_inference_steps", "guidance_scale", "true_cfg_scale"):
|
||||
if k in sampling_params and sampling_params[k] is not None:
|
||||
form.add_field(k, str(sampling_params[k]))
|
||||
if mask is not None:
|
||||
mask_filename = "mask.png"
|
||||
form.add_field(
|
||||
"mask",
|
||||
image_tensor_to_png_bytes(mask, mask_filename),
|
||||
filename=mask_filename,
|
||||
content_type="image/png",
|
||||
)
|
||||
|
||||
url = self.base_url + "/images/edits"
|
||||
async with aiohttp.ClientSession(timeout=self.timeout) as session:
|
||||
try:
|
||||
async with session.post(url, data=form) as response:
|
||||
if not response.ok:
|
||||
error_text = await response.text()
|
||||
raise (ValueError if response.status < 500 else RuntimeError)(
|
||||
f"vLLM-Omni API returned status {response.status}: {error_text}"
|
||||
)
|
||||
|
||||
try:
|
||||
data = await response.json()
|
||||
except aiohttp.ContentTypeError as e:
|
||||
raise RuntimeError(f"Invalid JSON response from vLLM-Omni: {e}")
|
||||
|
||||
if "data" not in data:
|
||||
raise RuntimeError("API response missing 'data' field - expected OpenAI DALL-E format")
|
||||
if not data["data"]:
|
||||
raise RuntimeError("API returned empty data array")
|
||||
|
||||
image_tensors = []
|
||||
for idx, img in enumerate(data["data"]):
|
||||
if "b64_json" not in img:
|
||||
raise RuntimeError(f"API returned image #{idx} without 'b64_json' field")
|
||||
base64_str = img["b64_json"]
|
||||
tensor = base64_to_image_tensor(base64_str)
|
||||
image_tensors.append(tensor)
|
||||
|
||||
return torch.stack(image_tensors, dim=0)
|
||||
|
||||
except aiohttp.ClientError as e:
|
||||
raise RuntimeError(f"Network error connecting to vLLM-Omni at {url}: {e}")
|
||||
|
||||
async def generate_image_chat_completion(
|
||||
self,
|
||||
*,
|
||||
model: str,
|
||||
prompt: str,
|
||||
negative_prompt: str | None = None,
|
||||
image: torch.Tensor | None = None,
|
||||
audio: AudioInput | None = None,
|
||||
video: VideoInput | None = None,
|
||||
sampling_params: dict | list[dict] | None = None,
|
||||
) -> torch.Tensor:
|
||||
payload = VLLMOmniClient._prepare_chat_completion_messages(
|
||||
model=model,
|
||||
prompt=prompt,
|
||||
negative_prompt=negative_prompt,
|
||||
image=image,
|
||||
audio=audio,
|
||||
video=video,
|
||||
sampling_params=sampling_params,
|
||||
modalities=["image"],
|
||||
)
|
||||
choices = await self._generate_base_chat_completion(model, payload)
|
||||
|
||||
image_tensors = []
|
||||
for idx, img_content in enumerate(choices[0]["message"]["content"]):
|
||||
base64_str = img_content.get("image_url", {}).get("url", "")
|
||||
if not base64_str:
|
||||
raise RuntimeError(f"API returned image #{idx} without image url")
|
||||
tensor = base64_to_image_tensor(base64_str)
|
||||
image_tensors.append(tensor)
|
||||
|
||||
return torch.stack(image_tensors, dim=0)
|
||||
|
||||
async def generate_comprehension_chat_completion(
|
||||
self,
|
||||
*,
|
||||
model: str,
|
||||
prompt: str,
|
||||
image: torch.Tensor | None = None,
|
||||
audio: AudioInput | None = None,
|
||||
video: VideoInput | None = None,
|
||||
sampling_params: dict | list[dict] | None = None,
|
||||
modalities: list[str] = ["text", "audio"],
|
||||
**extra_body,
|
||||
) -> tuple[str | None, AudioInput | None]:
|
||||
# Response may contain two choices: one with text, one with audio
|
||||
payload = VLLMOmniClient._prepare_chat_completion_messages(
|
||||
model=model,
|
||||
prompt=prompt,
|
||||
negative_prompt=None,
|
||||
image=image,
|
||||
audio=audio,
|
||||
video=video,
|
||||
sampling_params=sampling_params,
|
||||
modalities=modalities,
|
||||
**extra_body,
|
||||
)
|
||||
|
||||
choices = await self._generate_base_chat_completion(model, payload)
|
||||
text_response = None
|
||||
audio_base64 = None
|
||||
for choice in choices:
|
||||
try:
|
||||
text_response = choice["message"]["content"]
|
||||
except (KeyError, TypeError):
|
||||
# Either this case (text response) or the audio response case will be hit. Checking None's later.
|
||||
pass
|
||||
try:
|
||||
audio_base64 = choice["message"]["audio"]["data"]
|
||||
except (KeyError, TypeError):
|
||||
# Either this case (text response) or the audio response case will be hit. Checking None's later.
|
||||
pass
|
||||
if audio_base64 is None and text_response is None:
|
||||
raise RuntimeError(
|
||||
"API response missing both '.message.audio' and 'message.content' fields."
|
||||
f"The choices object is {choices}"
|
||||
)
|
||||
if audio_base64 is not None:
|
||||
audio = base64_to_audio(audio_base64)
|
||||
logger.debug(
|
||||
"audio sample rate %d, audio shape %s, duration in second %f",
|
||||
audio["sample_rate"],
|
||||
audio["waveform"].shape,
|
||||
audio["waveform"].shape[2] / audio["sample_rate"],
|
||||
)
|
||||
else:
|
||||
audio = None
|
||||
return text_response, audio
|
||||
|
||||
async def generate_speech(
|
||||
self,
|
||||
*,
|
||||
model: str,
|
||||
input: str,
|
||||
voice: str,
|
||||
response_format: AudioFormat,
|
||||
speed: float,
|
||||
**extra_params,
|
||||
) -> AudioInput:
|
||||
await self._check_model_exist(model)
|
||||
|
||||
ref_audio: AudioInput | None = extra_params.pop("ref_audio", None)
|
||||
|
||||
payload = {
|
||||
"model": model,
|
||||
"input": input,
|
||||
"voice": voice,
|
||||
"response_format": response_format,
|
||||
"speed": speed,
|
||||
**extra_params,
|
||||
}
|
||||
|
||||
if ref_audio is not None:
|
||||
audio_base64 = audio_to_base64(ref_audio)
|
||||
payload["ref_audio"] = audio_base64
|
||||
|
||||
logger.debug("Omni TTS payload: %s", pretty_printer.pformat(payload))
|
||||
|
||||
url = self.base_url + "/audio/speech"
|
||||
async with aiohttp.ClientSession(timeout=self.timeout) as session:
|
||||
try:
|
||||
async with session.post(
|
||||
url,
|
||||
json=payload,
|
||||
headers={"Content-Type": "application/json"},
|
||||
) as response:
|
||||
if not response.ok:
|
||||
error_text = await response.text()
|
||||
raise (ValueError if response.status < 500 else RuntimeError)(
|
||||
f"vLLM-Omni API returned status {response.status}: {error_text}"
|
||||
)
|
||||
|
||||
try:
|
||||
audio_bytes = await response.read()
|
||||
except aiohttp.ContentTypeError as e:
|
||||
raise RuntimeError(f"Invalid JSON response from vLLM-Omni: {e}")
|
||||
|
||||
try:
|
||||
audio = bytes_to_audio(audio_bytes)
|
||||
except av.error.InvalidDataError as e:
|
||||
raise ValueError(
|
||||
f"Invalid audio data received from vLLM-Omni: {e}"
|
||||
"Check if you have input unsupported arguments (such as 'voice')"
|
||||
)
|
||||
return audio
|
||||
|
||||
except aiohttp.ClientError as e:
|
||||
raise RuntimeError(f"Network error connecting to vLLM-Omni at {url}: {e}")
|
||||
|
||||
async def _generate_base_chat_completion(self, model: str, payload: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
logger.debug("Omni payload: %s", pretty_printer.pformat(payload))
|
||||
await self._check_model_exist(model)
|
||||
|
||||
url = self.base_url + "/chat/completions"
|
||||
async with aiohttp.ClientSession(timeout=self.timeout) as session:
|
||||
try:
|
||||
async with session.post(
|
||||
url,
|
||||
json=payload,
|
||||
headers={"Content-Type": "application/json"},
|
||||
) as response:
|
||||
if not response.ok:
|
||||
error_text = await response.text()
|
||||
raise (ValueError if response.status < 500 else RuntimeError)(
|
||||
f"vLLM-Omni API returned status {response.status}: {error_text}"
|
||||
)
|
||||
|
||||
try:
|
||||
data = await response.json()
|
||||
except aiohttp.ContentTypeError as e:
|
||||
raise RuntimeError(f"Invalid JSON response from vLLM-Omni: {e}")
|
||||
|
||||
logger.debug(
|
||||
"chat completion response: %s",
|
||||
pretty_printer.pformat(data),
|
||||
)
|
||||
|
||||
try:
|
||||
return data["choices"]
|
||||
except (KeyError, TypeError):
|
||||
raise RuntimeError("Invalid JSON response from vLLM-Omni: missing 'choices' field")
|
||||
|
||||
except aiohttp.ClientError as e:
|
||||
raise RuntimeError(f"Network error connecting to vLLM-Omni at {self.base_url}: {e}")
|
||||
|
||||
async def _check_model_exist(self, model: str):
|
||||
url = self.base_url + "/models"
|
||||
async with aiohttp.ClientSession(timeout=self.timeout) as session:
|
||||
try:
|
||||
async with session.get(
|
||||
url,
|
||||
headers={"Content-Type": "application/json"},
|
||||
) as response:
|
||||
if not response.ok:
|
||||
error_text = await response.text()
|
||||
raise (ValueError if response.status < 500 else RuntimeError)(
|
||||
f"vLLM-Omni API returned status {response.status} "
|
||||
f"when getting hosted model list: {error_text}"
|
||||
)
|
||||
|
||||
try:
|
||||
data = await response.json()
|
||||
except aiohttp.ContentTypeError as e:
|
||||
raise RuntimeError(f"Invalid JSON response when getting hosted model list from vLLM-Omni: {e}")
|
||||
|
||||
except aiohttp.ClientError as e:
|
||||
raise RuntimeError(f"Network error connecting to vLLM-Omni at {self.base_url}: {e}")
|
||||
try:
|
||||
model_list = data["data"]
|
||||
model_found = next((True for m in model_list if m["id"] == model), False)
|
||||
except (KeyError, TypeError):
|
||||
raise RuntimeError(f"Invalid JSON response of the hosted model list: {data}")
|
||||
|
||||
if not model_found:
|
||||
raise ValueError(f"Model {model} not served at {self.base_url}.")
|
||||
|
||||
@staticmethod
|
||||
def _prepare_chat_completion_messages(
|
||||
*,
|
||||
model: str,
|
||||
prompt: str,
|
||||
negative_prompt: str | None,
|
||||
image: torch.Tensor | None = None,
|
||||
audio: AudioInput | None = None,
|
||||
video: VideoInput | None = None,
|
||||
sampling_params: dict | list[dict] | None = None,
|
||||
modalities: list[str] | None = None, # diffusion don't have this field
|
||||
**extra_body,
|
||||
):
|
||||
message_content: list[dict] = [{"type": "text", "text": prompt}]
|
||||
if image is not None:
|
||||
message_content.append(
|
||||
{
|
||||
"type": "image_url",
|
||||
"image_url": {"url": image_tensor_to_base64(image)},
|
||||
}
|
||||
)
|
||||
if audio is not None:
|
||||
message_content.append({"type": "audio_url", "audio_url": {"url": audio_to_base64(audio)}})
|
||||
if video is not None:
|
||||
message_content.append({"type": "video_url", "video_url": {"url": video_to_base64(video)}})
|
||||
messages = [{"role": "user", "content": message_content}]
|
||||
|
||||
combined_extra_body: dict[str, Any] = {}
|
||||
if sampling_params is not None:
|
||||
spec, _ = lookup_model_spec(model)
|
||||
is_single_sampling_param = isinstance(sampling_params, dict) or len(sampling_params) == 1
|
||||
|
||||
# Exclude internal key
|
||||
if isinstance(sampling_params, dict):
|
||||
sampling_params = {k: v for k, v in sampling_params.items() if k != "type"}
|
||||
else:
|
||||
sampling_params = [{k: v for k, v in sp.items() if k != "type"} for sp in sampling_params]
|
||||
|
||||
if (spec is None and is_single_sampling_param) or (spec is not None and spec["stages"] == ["diffusion"]):
|
||||
# Diffusion format: extra_body directly contains sampling params.
|
||||
# Validation should have taken care of matching sampling params' types.
|
||||
# * Use this mode if the model is a simple one-stage diffusion model.
|
||||
# * Fallback to this mode if model is not registered and a single sampling param is provided.
|
||||
sampling_params = sampling_params if isinstance(sampling_params, dict) else sampling_params[0]
|
||||
combined_extra_body: dict[str, Any] = {**sampling_params}
|
||||
if "n" in combined_extra_body:
|
||||
combined_extra_body["num_outputs_per_prompt"] = combined_extra_body["n"]
|
||||
del combined_extra_body["n"]
|
||||
else:
|
||||
# Use AR style payload, extra_body has a sampling_params_list field
|
||||
combined_extra_body: dict[str, Any] = {"sampling_params_list": sampling_params}
|
||||
|
||||
if negative_prompt:
|
||||
combined_extra_body["negative_prompt"] = negative_prompt
|
||||
|
||||
if extra_body:
|
||||
combined_extra_body.update(extra_body)
|
||||
|
||||
payload: dict[str, Any] = {"messages": messages, "model": model}
|
||||
if combined_extra_body:
|
||||
payload["extra_body"] = combined_extra_body
|
||||
if modalities:
|
||||
payload["modalities"] = modalities
|
||||
|
||||
spec, _ = lookup_model_spec(model)
|
||||
if spec:
|
||||
preprocessor = spec.get("payload_preprocessor", None)
|
||||
if preprocessor is not None:
|
||||
payload = preprocessor(payload)
|
||||
|
||||
return payload
|
||||
@@ -0,0 +1,230 @@
|
||||
"""Image/tensor format helpers.
|
||||
|
||||
The image generation part is derived from dougbtv/comfyui-vllm-omni by Doug (@dougbtv).
|
||||
Original source at https://github.com/dougbtv/comfyui-vllm-omni, distributed under the MIT License.
|
||||
"""
|
||||
|
||||
import base64
|
||||
import mimetypes
|
||||
from io import BytesIO
|
||||
|
||||
import av
|
||||
import numpy as np
|
||||
import torch
|
||||
from comfy_api.input import AudioInput, VideoInput
|
||||
from comfy_extras import nodes_audio
|
||||
from PIL import Image
|
||||
|
||||
from .logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
def base64_to_image_tensor(base64_str: str, mode: str = "RGB") -> torch.Tensor:
|
||||
"""
|
||||
Convert base64-encoded image to ComfyUI image tensor.
|
||||
|
||||
Args:
|
||||
base64_str: Base64-encoded image string
|
||||
mode: PIL image mode (default RGB for transparency support)
|
||||
|
||||
Returns:
|
||||
torch.Tensor with shape (1, H, W, C) in float32 [0, 1] range
|
||||
|
||||
Raises:
|
||||
ValueError: If base64 string is invalid or image cannot be decoded
|
||||
"""
|
||||
if base64_str.startswith("data:image"):
|
||||
_, base64_str = base64_str.split(",", 1)
|
||||
|
||||
try:
|
||||
# Decode base64 to bytes
|
||||
image_bytes = base64.b64decode(base64_str)
|
||||
except Exception as e:
|
||||
raise ValueError(f"Invalid base64 string: {e}")
|
||||
|
||||
# Create BytesIO object for PIL
|
||||
image_bytesio = BytesIO(image_bytes)
|
||||
|
||||
# Open with PIL and convert to desired mode
|
||||
try:
|
||||
pil_image = Image.open(image_bytesio)
|
||||
pil_image = pil_image.convert(mode)
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"Failed to open image: {e}")
|
||||
|
||||
image_array = np.asarray(pil_image).astype(np.float32) / 255.0
|
||||
image_tensor = torch.from_numpy(image_array)
|
||||
return image_tensor
|
||||
|
||||
|
||||
def image_tensor_to_png_bytes(tensor: torch.Tensor, filename: str = "image.png") -> BytesIO:
|
||||
"""
|
||||
Convert ComfyUI image tensor to PNG BytesIO for multipart upload.
|
||||
|
||||
This function converts a ComfyUI IMAGE tensor to a PNG-encoded BytesIO object
|
||||
suitable for multipart/form-data upload. The BytesIO object has its .name
|
||||
attribute set, which is required by aiohttp for file uploads.
|
||||
|
||||
Args:
|
||||
tensor: ComfyUI IMAGE tensor with shape (B, H, W, C), dtype float32, range [0, 1]
|
||||
filename: Name attribute to set on BytesIO (default: "image.png")
|
||||
|
||||
Returns:
|
||||
BytesIO object containing PNG-encoded image with .name attribute set
|
||||
|
||||
Raises:
|
||||
ValueError: If tensor format is invalid (not 4D, wrong dtype, etc.)
|
||||
"""
|
||||
if tensor.ndim != 4:
|
||||
raise ValueError(f"Expected 4D tensor with shape (B, H, W, C), got {tensor.ndim}D tensor")
|
||||
|
||||
image_tensor = tensor[0] # Shape: (H, W, C)
|
||||
image_np = (image_tensor.cpu().numpy() * 255.0).astype(np.uint8)
|
||||
pil_image = Image.fromarray(image_np)
|
||||
|
||||
# Save to BytesIO as image file
|
||||
img_bytes = BytesIO()
|
||||
# Set name attribute (required for multipart upload and mimetype detection)
|
||||
img_bytes.name = filename
|
||||
try:
|
||||
pil_image.save(img_bytes)
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"Failed to save image as file: {e}")
|
||||
|
||||
# Reset position to beginning
|
||||
img_bytes.seek(0)
|
||||
|
||||
return img_bytes
|
||||
|
||||
|
||||
def image_tensor_to_base64(tensor: torch.Tensor, filename: str = "image.png") -> str:
|
||||
"""
|
||||
Convert ComfyUI image tensor to base64-encoded image string.
|
||||
|
||||
Args:
|
||||
tensor: ComfyUI IMAGE tensor with shape (B, H, W, C), dtype float32, range [0, 1]
|
||||
filename: Name attribute to set on BytesIO (default: "image.png")
|
||||
format: File format of the output image file buffer (default: "PNG")
|
||||
|
||||
Returns:
|
||||
Base64-encoded image string
|
||||
|
||||
Raises:
|
||||
ValueError: If tensor format is invalid (not 4D, wrong dtype, etc.)
|
||||
"""
|
||||
img_bytes = image_tensor_to_png_bytes(tensor, filename)
|
||||
img_bytes.seek(0)
|
||||
byte_data = img_bytes.read()
|
||||
base64_str = base64.b64encode(byte_data).decode("utf-8")
|
||||
mime_type = mimetypes.guess_type(filename)[0] or "application/octet-stream"
|
||||
return f"data:{mime_type};base64,{base64_str}"
|
||||
|
||||
|
||||
def video_to_bytes(video: VideoInput, filename: str = "video.mp4") -> BytesIO:
|
||||
output_buffer = BytesIO()
|
||||
output_buffer.name = filename
|
||||
video.save_to(output_buffer)
|
||||
output_buffer.seek(0)
|
||||
return output_buffer
|
||||
|
||||
|
||||
def video_to_base64(video: VideoInput, filename: str = "video.mp4") -> str:
|
||||
video_buffer = video_to_bytes(video, filename)
|
||||
video_buffer.seek(0)
|
||||
byte_data = video_buffer.read()
|
||||
base64_str = base64.b64encode(byte_data).decode("utf-8")
|
||||
mime_type = mimetypes.guess_type(filename)[0] or "application/octet-stream"
|
||||
return f"data:{mime_type};base64,{base64_str}"
|
||||
|
||||
|
||||
def audio_to_bytes(audio: AudioInput, filename: str = "audio.mp3", quality: str = "128k") -> BytesIO:
|
||||
waveform = audio["waveform"][0] # Shape: (C, T)
|
||||
sample_rate = audio["sample_rate"]
|
||||
format = filename.rsplit(".", maxsplit=1)[1]
|
||||
layout = "mono" if waveform.shape[0] == 1 else "stereo"
|
||||
|
||||
output_buffer = BytesIO()
|
||||
output_buffer.name = filename
|
||||
output_container = av.open(output_buffer, mode="w", format=format)
|
||||
if format == "opus":
|
||||
out_stream = output_container.add_stream("libopus", rate=sample_rate, layout=layout)
|
||||
if quality == "64k":
|
||||
out_stream.bit_rate = 64000 # type: ignore # copy from ComfyUI comfy_api/latest/_ui.py
|
||||
elif quality == "96k":
|
||||
out_stream.bit_rate = 96000 # type: ignore # copy from ComfyUI comfy_api/latest/_ui.py
|
||||
elif quality == "128k":
|
||||
out_stream.bit_rate = 128000 # type: ignore # copy from ComfyUI comfy_api/latest/_ui.py
|
||||
elif quality == "192k":
|
||||
out_stream.bit_rate = 192000 # type: ignore # copy from ComfyUI comfy_api/latest/_ui.py
|
||||
elif quality == "320k":
|
||||
out_stream.bit_rate = 320000 # type: ignore # copy from ComfyUI comfy_api/latest/_ui.py
|
||||
elif format == "mp3":
|
||||
out_stream = output_container.add_stream("libmp3lame", rate=sample_rate, layout=layout)
|
||||
if quality == "V0":
|
||||
out_stream.codec_context.qscale = 1 # type: ignore # copy from ComfyUI comfy_api/latest/_ui.py
|
||||
elif quality == "128k":
|
||||
out_stream.bit_rate = 128000 # type: ignore # copy from ComfyUI comfy_api/latest/_ui.py
|
||||
elif quality == "320k":
|
||||
out_stream.bit_rate = 320000 # type: ignore # copy from ComfyUI comfy_api/latest/_ui.py
|
||||
else: # format == "flac":
|
||||
out_stream = output_container.add_stream("flac", rate=sample_rate, layout=layout)
|
||||
|
||||
frame = av.AudioFrame.from_ndarray(
|
||||
waveform.movedim(0, 1).reshape(1, -1).float().numpy(),
|
||||
format="flt",
|
||||
layout=layout,
|
||||
)
|
||||
frame.sample_rate = sample_rate
|
||||
frame.pts = 0
|
||||
output_container.mux(out_stream.encode(frame)) # type: ignore # copy from ComfyUI comfy_api/latest/_ui.py
|
||||
# Flush encoder
|
||||
output_container.mux(out_stream.encode(None)) # type: ignore # copy from ComfyUI comfy_api/latest/_ui.py
|
||||
output_container.close()
|
||||
output_buffer.seek(0)
|
||||
|
||||
return output_buffer
|
||||
|
||||
|
||||
def audio_to_base64(audio: AudioInput, filename: str = "audio.mp3", quality: str = "128k") -> str:
|
||||
audio_buffer = audio_to_bytes(audio, filename, quality)
|
||||
audio_buffer.seek(0)
|
||||
byte_data = audio_buffer.read()
|
||||
base64_str = base64.b64encode(byte_data).decode("utf-8")
|
||||
mime_type = mimetypes.guess_type(filename)[0] or "application/octet-stream"
|
||||
return f"data:{mime_type};base64,{base64_str}"
|
||||
|
||||
|
||||
def bytes_to_audio(audio_bytes: bytes) -> AudioInput:
|
||||
"""
|
||||
Convert audio bytes to ComfyUI audio tensor.
|
||||
|
||||
Args:
|
||||
audio_bytes: Audio file bytes
|
||||
Returns:
|
||||
torch.Tensor with shape (B, C, T) in float32 range [-1, 1]
|
||||
"""
|
||||
audio_buffer = BytesIO(audio_bytes)
|
||||
waveform, sample_rate = nodes_audio.load(audio_buffer) # type: ignore # Although expect string argument, it calls av.open underneath, which supports BytesIO (file-like)
|
||||
return {"waveform": waveform.unsqueeze(0), "sample_rate": sample_rate}
|
||||
|
||||
|
||||
def base64_to_audio(base64_str: str) -> AudioInput:
|
||||
"""
|
||||
Convert base64-encoded audio to ComfyUI audio tensor.
|
||||
|
||||
Args:
|
||||
base64_str: Base64-encoded audio string
|
||||
Returns:
|
||||
torch.Tensor with shape (B, C, T) in float32 range [-1, 1]
|
||||
"""
|
||||
if base64_str.startswith("data:audio"):
|
||||
_, base64_str = base64_str.split(",", 1)
|
||||
|
||||
try:
|
||||
# Decode base64 to bytes
|
||||
audio_bytes = base64.b64decode(base64_str)
|
||||
except Exception as e:
|
||||
raise ValueError(f"Invalid base64 string: {e}")
|
||||
|
||||
return bytes_to_audio(audio_bytes)
|
||||
@@ -0,0 +1,125 @@
|
||||
"""Centralized logger configuration for vLLM-Omni ComfyUI."""
|
||||
|
||||
import logging
|
||||
import pprint
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
|
||||
def get_logger(name: str) -> logging.Logger:
|
||||
"""
|
||||
Get or create a logger with proper formatting.
|
||||
|
||||
Args:
|
||||
name: Logger name (typically __name__ of the calling module)
|
||||
|
||||
Returns:
|
||||
Configured logger instance
|
||||
"""
|
||||
logger = logging.getLogger(name)
|
||||
|
||||
# Only configure if not already configured
|
||||
if not logger.handlers:
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
# Create console handler
|
||||
handler = logging.StreamHandler(sys.stdout)
|
||||
handler.setLevel(logging.INFO)
|
||||
|
||||
# Create formatter
|
||||
formatter = logging.Formatter(
|
||||
fmt="(ComfyUI-vLLM-Omni) [%(levelname)s] %(asctime)s [%(filename)s:%(lineno)s] %(message)s",
|
||||
datefmt="%Y-%m-%d %H:%M:%S",
|
||||
)
|
||||
handler.setFormatter(formatter)
|
||||
|
||||
# Add handler to logger
|
||||
logger.addHandler(handler)
|
||||
|
||||
# Prevent propagation to root logger
|
||||
logger.propagate = False
|
||||
|
||||
return logger
|
||||
|
||||
|
||||
class OmitBase64PrettyPrinter(pprint.PrettyPrinter):
|
||||
"""
|
||||
A PrettyPrinter that redacts specific field names with '...'
|
||||
wherever they appear in nested structures.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def _format(self, obj: Any, stream, indent: int, allowance: int, context, level: int) -> None:
|
||||
# Check if this is a dict with redacted keys
|
||||
if isinstance(obj, dict):
|
||||
# Create a copy with redacted values
|
||||
display_obj = {}
|
||||
for key, value in obj.items():
|
||||
if key == "data" or key == "url":
|
||||
if value.startswith("data:"):
|
||||
base64_header = value.split(",", 1)[0]
|
||||
display_obj[key] = f"{base64_header},***"
|
||||
elif value.startswith("http://") or value.startswith("https://"):
|
||||
display_obj[key] = value
|
||||
elif len(value) > 10:
|
||||
display_obj[key] = f"{value[:10]}***"
|
||||
else:
|
||||
display_obj[key] = value
|
||||
else:
|
||||
display_obj[key] = value
|
||||
obj = display_obj
|
||||
|
||||
# Handle list/tuple/set containing dicts that might have redacted keys
|
||||
# (pprint will recursively call _format on nested items, so this
|
||||
# handles arbitrary nesting automatically)
|
||||
|
||||
super()._format(obj, stream, indent, allowance, context, level)
|
||||
|
||||
|
||||
pretty_printer = OmitBase64PrettyPrinter()
|
||||
|
||||
|
||||
# ========== PPrint EXAMPLES ==========
|
||||
|
||||
if __name__ == "__main__":
|
||||
data = {
|
||||
"messages": [
|
||||
{
|
||||
"role": "system",
|
||||
"content": (
|
||||
"You are Qwen, a virtual human developed by the Qwen Team, Alibaba Group,"
|
||||
"capable of perceiving auditory and visual inputs, as well as generating text and speech."
|
||||
),
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "What sound is it, and what is the drawing about?",
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"text": "What sound is it, and what is the drawing about?",
|
||||
},
|
||||
{
|
||||
"type": "image_url",
|
||||
"image_url": {"url": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAwAAAA"},
|
||||
},
|
||||
{
|
||||
"type": "audio_url",
|
||||
"audio_url": {"url": "data:audio/mpeg;base64,SUQzBAAAAAAAIlRTU0UAAAAOAAADT="},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
"extra_body": {"mm_processor_kwargs": {"use_audio_in_video": False}},
|
||||
"modalities": ["text"],
|
||||
}
|
||||
|
||||
# Create printer that redacts 'password' and 'token' fields
|
||||
|
||||
print("\nRedactingPrettyPrinter (hides secrets):")
|
||||
pretty_printer.pprint(data)
|
||||
@@ -0,0 +1,119 @@
|
||||
import re
|
||||
|
||||
from .types import Modality, ModelMode, Spec
|
||||
|
||||
|
||||
def _bagel_payload_preprocessor(payload: dict) -> dict:
|
||||
try:
|
||||
for message in payload["messages"]:
|
||||
for content in message["content"]:
|
||||
if content["type"] == "text":
|
||||
content["text"] = "<|im_start|>" + content["text"] + "<|im_end|>"
|
||||
except (KeyError, TypeError):
|
||||
raise RuntimeError("Internal Error: malformatted BAGEL payload")
|
||||
extra_body = payload.pop("extra_body", {})
|
||||
return {**payload, **extra_body}
|
||||
|
||||
|
||||
def _qwen25_payload_preprocessor(payload: dict) -> dict:
|
||||
if payload["messages"][0]["role"] != "system":
|
||||
payload["messages"] = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": (
|
||||
"You are Qwen, a virtual human developed by the Qwen Team, Alibaba Group,"
|
||||
"capable of perceiving auditory and visual inputs, as well as generating text and speech."
|
||||
),
|
||||
},
|
||||
*payload["messages"],
|
||||
]
|
||||
return payload
|
||||
|
||||
|
||||
_MODEL_PIPELINE_SPECS: dict[str, Spec] = {
|
||||
r"BAGEL-7B-MoT": {
|
||||
"stages": [
|
||||
"diffusion" # The vLLM-Omni interface treats it as a single-stage diffusion model
|
||||
],
|
||||
"modes": [
|
||||
{
|
||||
"mode": ModelMode.COMPREHENSION,
|
||||
"input_modalities": [Modality.TEXT, Modality.IMAGE],
|
||||
}
|
||||
],
|
||||
"payload_preprocessor": _bagel_payload_preprocessor,
|
||||
},
|
||||
r"Qwen2.5-Omni*": {
|
||||
"stages": ["autoregression", "autoregression", "autoregression"],
|
||||
"payload_preprocessor": _qwen25_payload_preprocessor,
|
||||
"modes": [
|
||||
{
|
||||
"mode": ModelMode.COMPREHENSION,
|
||||
"input_modalities": [
|
||||
Modality.TEXT,
|
||||
Modality.IMAGE,
|
||||
Modality.VIDEO,
|
||||
Modality.AUDIO,
|
||||
],
|
||||
}
|
||||
],
|
||||
},
|
||||
r"Qwen3-Omni*": {
|
||||
"stages": ["autoregression", "autoregression", "autoregression"],
|
||||
"modes": [
|
||||
{
|
||||
"mode": ModelMode.COMPREHENSION,
|
||||
"input_modalities": [
|
||||
Modality.TEXT,
|
||||
Modality.IMAGE,
|
||||
Modality.VIDEO,
|
||||
Modality.AUDIO,
|
||||
],
|
||||
}
|
||||
],
|
||||
},
|
||||
}
|
||||
# Convert dict keys to regex patterns
|
||||
MODEL_PIPELINE_SPECS: dict[re.Pattern, Spec] = {}
|
||||
for k, v in _MODEL_PIPELINE_SPECS.items():
|
||||
MODEL_PIPELINE_SPECS[re.compile(k)] = v
|
||||
del _MODEL_PIPELINE_SPECS
|
||||
|
||||
|
||||
def lookup_model_spec(model: str) -> tuple[Spec | None, str | None]:
|
||||
try:
|
||||
last_component = model.rstrip("/").rsplit("/", 1)[-1]
|
||||
except IndexError:
|
||||
last_component = model
|
||||
for pattern, spec in MODEL_PIPELINE_SPECS.items():
|
||||
if pattern.search(last_component):
|
||||
return spec, pattern.pattern
|
||||
return None, None
|
||||
|
||||
|
||||
# ============== DEMONSTRATION ==============
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_paths = [
|
||||
"Qwen/Qwen2.5-Omni-7B",
|
||||
"MyModels/Qwen2.5-Omni-3B",
|
||||
"/root/home/Qwen2.5-Omni-7B",
|
||||
"Qwen/Qwen3-Omni",
|
||||
"Qwen/Qwen3-Omni-30B-A3B-Instruct",
|
||||
"Custom/Path/UnknownModel-Instruct",
|
||||
"Not/Matching/Anything",
|
||||
]
|
||||
|
||||
test_payload = {"messages": [{"role": "user", "content": "prompt"}]}
|
||||
|
||||
print("Testing registry lookups:\n")
|
||||
for path in test_paths:
|
||||
spec, _ = lookup_model_spec(path)
|
||||
if spec:
|
||||
if preprocessor := spec.get("payload_preprocessor"):
|
||||
result = preprocessor(test_payload)
|
||||
print(f"✓ {path:<40} → {result}")
|
||||
else:
|
||||
print(f"✓ {path:<40} → No preprocessor")
|
||||
else:
|
||||
print(f"✗ {path:<40} → No match")
|
||||
@@ -0,0 +1,39 @@
|
||||
from collections.abc import Callable
|
||||
from enum import Enum, auto
|
||||
from typing import (
|
||||
Any,
|
||||
Literal,
|
||||
NotRequired,
|
||||
TypeAlias,
|
||||
TypedDict,
|
||||
)
|
||||
|
||||
AudioFormat: TypeAlias = Literal["mp3", "opus", "aac", "flac", "wav", "pcm"]
|
||||
|
||||
|
||||
class ModelMode(Enum):
|
||||
IMAGE_GENERATION = auto()
|
||||
VIDEO_GENERATION = auto()
|
||||
AUDIO_GENERATION = auto()
|
||||
COMPREHENSION = auto()
|
||||
|
||||
|
||||
class Modality(Enum):
|
||||
TEXT = auto() # maybe not useful. Prompt is always required
|
||||
IMAGE = auto()
|
||||
VIDEO = auto()
|
||||
AUDIO = auto()
|
||||
|
||||
|
||||
class ModelModeSpec(TypedDict):
|
||||
mode: ModelMode
|
||||
input_modalities: list[Modality]
|
||||
|
||||
|
||||
PayloadPreprocessor: TypeAlias = Callable[[dict[str, Any]], dict[str, Any]]
|
||||
|
||||
|
||||
class Spec(TypedDict):
|
||||
stages: list[Literal["diffusion", "autoregression"]]
|
||||
modes: list[ModelModeSpec]
|
||||
payload_preprocessor: NotRequired[PayloadPreprocessor]
|
||||
@@ -0,0 +1,84 @@
|
||||
from .logger import get_logger
|
||||
from .models import lookup_model_spec
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
def validate_model_and_sampling_params_types(
|
||||
model_name: str,
|
||||
sampling_param_list: dict | list[dict] | None = None,
|
||||
):
|
||||
# Check if model name exists
|
||||
if not model_name:
|
||||
raise ValueError("Model name must not be empty.")
|
||||
|
||||
# Skip if no spec or no sampling params
|
||||
pipeline_spec, _ = lookup_model_spec(model_name)
|
||||
if pipeline_spec is None:
|
||||
logger.info("skipping sampling params check because spec is not found")
|
||||
return
|
||||
if sampling_param_list is None:
|
||||
return
|
||||
|
||||
# Check the number of stages and their data types
|
||||
stages = pipeline_spec["stages"]
|
||||
if isinstance(sampling_param_list, list):
|
||||
# Check that the lengths match
|
||||
if len(stages) != len(sampling_param_list):
|
||||
raise ValueError(
|
||||
f"Sampling parameter list length {len(sampling_param_list)} does not match "
|
||||
f"number of stages {len(stages)} for model {model_name}."
|
||||
)
|
||||
# Check that each stage's type match
|
||||
for i, sp in enumerate(sampling_param_list):
|
||||
if "type" not in sp:
|
||||
raise RuntimeError("Internal error: unknown sampling parameter type")
|
||||
if sp["type"] != stages[i]:
|
||||
raise ValueError(
|
||||
f"Sampling parameter type ({sp['type']}) does not match "
|
||||
f"stage type ({stages[i]}) at index {i} for model {model_name}."
|
||||
)
|
||||
elif isinstance(sampling_param_list, dict):
|
||||
if "type" not in sampling_param_list:
|
||||
raise RuntimeError("Internal error: unknown sampling parameter type")
|
||||
# Check that the provided single sampling param matches all stages
|
||||
elif any(stage != sampling_param_list["type"] for stage in stages):
|
||||
raise ValueError(
|
||||
f"When passing a single sampling parameter node, all stages of the model must match "
|
||||
f"the provided sampling parameter's type. "
|
||||
f"However, the stages of model {model_name} are: {stages}. "
|
||||
f"The provided sampling parameter is {sampling_param_list['type']}"
|
||||
)
|
||||
|
||||
|
||||
def add_sampling_parameters_to_stage(
|
||||
model_name: str,
|
||||
sampling_param_list: dict | list[dict] | None,
|
||||
stage_type: str,
|
||||
/,
|
||||
**params_to_add,
|
||||
) -> dict | list[dict]:
|
||||
"""
|
||||
Given a model's name and the sampling parameter list to query this model,
|
||||
add arbitrary additional parameters to the sampling parameters of all stages of the given type.
|
||||
"""
|
||||
pipeline_spec, _ = lookup_model_spec(model_name)
|
||||
if not pipeline_spec:
|
||||
logger.warning(
|
||||
f"Since the model {model_name} is not in our list, we cannot ensure if "
|
||||
f"the fields ({tuple(params_to_add.keys())}) are added to the correct stage's sampling params. "
|
||||
f"We will do it heuristically."
|
||||
)
|
||||
pipeline_spec = {"stages": ["diffusion"]}
|
||||
|
||||
stages = pipeline_spec["stages"]
|
||||
if isinstance(sampling_param_list, dict):
|
||||
sampling_param_list.update(params_to_add)
|
||||
elif sampling_param_list is None:
|
||||
sampling_param_list = params_to_add.copy()
|
||||
else:
|
||||
for i, stage in enumerate(stages):
|
||||
if stage == stage_type:
|
||||
sampling_param_list[i].update(params_to_add)
|
||||
|
||||
return sampling_param_list
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 413 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 132 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 212 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 303 KiB |
@@ -0,0 +1 @@
|
||||
{"id":"1c99f525-0a37-45ba-a28a-7df7c3af66b4","revision":0,"last_node_id":12,"last_link_id":14,"nodes":[{"id":1,"type":"VLLMOmniComprehension","pos":[1191.2177053682556,144.66829928181377],"size":[400,268],"flags":{},"order":8,"mode":0,"inputs":[{"localized_name":"image","name":"image","shape":7,"type":"IMAGE","link":1},{"localized_name":"video","name":"video","shape":7,"type":"VIDEO","link":2},{"localized_name":"audio","name":"audio","shape":7,"type":"AUDIO","link":3},{"localized_name":"sampling_params","name":"sampling_params","shape":7,"type":"SAMPLING_PARAMS","link":14},{"localized_name":"url","name":"url","type":"STRING","widget":{"name":"url"},"link":null},{"localized_name":"model","name":"model","type":"STRING","widget":{"name":"model"},"link":null},{"localized_name":"prompt","name":"prompt","type":"STRING","widget":{"name":"prompt"},"link":null},{"localized_name":"output_text","name":"output_text","type":"BOOLEAN","widget":{"name":"output_text"},"link":null},{"localized_name":"output_audio","name":"output_audio","type":"BOOLEAN","widget":{"name":"output_audio"},"link":null},{"localized_name":"use_audio_in_video","name":"use_audio_in_video","type":"BOOLEAN","widget":{"name":"use_audio_in_video"},"link":null}],"outputs":[{"localized_name":"text_response","name":"text_response","type":"STRING","links":[8]},{"localized_name":"audio_response","name":"audio_response","type":"AUDIO","links":[9]}],"properties":{"Node name for S&R":"VLLMOmniComprehension"},"widgets_values":["http://localhost:8000/v1","Qwen/Qwen2.5-Omni-7B","",true,true,true]},{"id":3,"type":"LoadVideo","pos":[729.5984141255855,-198.631920454299],"size":[282.798828125,233.0743408203125],"flags":{},"order":0,"mode":0,"inputs":[{"localized_name":"file","name":"file","type":"COMBO","widget":{"name":"file"},"link":null},{"localized_name":"choose file to upload","name":"upload","type":"IMAGEUPLOAD","widget":{"name":"upload"},"link":null}],"outputs":[{"localized_name":"VIDEO","name":"VIDEO","type":"VIDEO","links":[2]}],"properties":{"Node name for S&R":"LoadVideo"},"widgets_values":["draw.mp4","image"]},{"id":4,"type":"LoadAudio","pos":[729.8037086965753,99.86963519703949],"size":[282.798828125,136],"flags":{},"order":1,"mode":0,"inputs":[{"localized_name":"audio","name":"audio","type":"COMBO","widget":{"name":"audio"},"link":null},{"localized_name":"audioUI","name":"audioUI","type":"AUDIO_UI","widget":{"name":"audioUI"},"link":null},{"localized_name":"choose file to upload","name":"upload","type":"AUDIOUPLOAD","widget":{"name":"upload"},"link":null}],"outputs":[{"localized_name":"AUDIO","name":"AUDIO","type":"AUDIO","links":[3]}],"properties":{"Node name for S&R":"LoadAudio"},"widgets_values":["Megan-Fox.mp3",null,null]},{"id":5,"type":"VLLMOmniARSampling","pos":[510.3517536642828,658.073751009259],"size":[270,178],"flags":{},"order":2,"mode":0,"inputs":[{"localized_name":"max_tokens","name":"max_tokens","type":"INT","widget":{"name":"max_tokens"},"link":null},{"localized_name":"temperature","name":"temperature","type":"FLOAT","widget":{"name":"temperature"},"link":null},{"localized_name":"top_p","name":"top_p","type":"FLOAT","widget":{"name":"top_p"},"link":null},{"localized_name":"repetition_penalty","name":"repetition_penalty","type":"FLOAT","widget":{"name":"repetition_penalty"},"link":null},{"localized_name":"seed","name":"seed","type":"INT","widget":{"name":"seed"},"link":null}],"outputs":[{"localized_name":"AR sampling params","name":"AR sampling params","type":"SAMPLING_PARAMS","links":[12]}],"properties":{"Node name for S&R":"VLLMOmniARSampling"},"widgets_values":[100,1,1,1,-1,"randomize"]},{"id":7,"type":"VLLMOmniARSampling","pos":[503.33235181647115,419.34158016181806],"size":[270,178],"flags":{},"order":3,"mode":0,"inputs":[{"localized_name":"max_tokens","name":"max_tokens","type":"INT","widget":{"name":"max_tokens"},"link":null},{"localized_name":"temperature","name":"temperature","type":"FLOAT","widget":{"name":"temperature"},"link":null},{"localized_name":"top_p","name":"top_p","type":"FLOAT","widget":{"name":"top_p"},"link":null},{"localized_name":"repetition_penalty","name":"repetition_penalty","type":"FLOAT","widget":{"name":"repetition_penalty"},"link":null},{"localized_name":"seed","name":"seed","type":"INT","widget":{"name":"seed"},"link":null}],"outputs":[{"localized_name":"AR sampling params","name":"AR sampling params","type":"SAMPLING_PARAMS","links":[5,13]}],"properties":{"Node name for S&R":"VLLMOmniARSampling"},"widgets_values":[100,1,1,1,-1,"randomize"]},{"id":8,"type":"VLLMOmniSamplingParamsList","pos":[820.6056617389042,426.38372037182273],"size":[263.066015625,66],"flags":{},"order":7,"mode":0,"inputs":[{"localized_name":"param1","name":"param1","type":"SAMPLING_PARAMS","link":5},{"localized_name":"param2","name":"param2","shape":7,"type":"SAMPLING_PARAMS","link":12},{"localized_name":"param3","name":"param3","shape":7,"type":"SAMPLING_PARAMS","link":13}],"outputs":[{"localized_name":"param list","name":"param list"Line truncated
|
||||
@@ -0,0 +1 @@
|
||||
{"id":"91f75acc-8040-40f6-865a-2e8a7cfd6672","revision":0,"last_node_id":11,"last_link_id":23,"nodes":[{"id":3,"type":"PreviewImage","pos":[1281.8455167767304,-69.02638461454333],"size":[305.6628685610174,307.0336884172169],"flags":{},"order":4,"mode":0,"inputs":[{"localized_name":"images","name":"images","type":"IMAGE","link":22}],"outputs":[],"properties":{"Node name for S&R":"PreviewImage"},"widgets_values":[]},{"id":11,"type":"VLLMOmniGenerateImage","pos":[816.0962515873642,-137.51112584854445],"size":[400,278],"flags":{},"order":3,"mode":0,"inputs":[{"localized_name":"image","name":"image","shape":7,"type":"IMAGE","link":21},{"localized_name":"mask","name":"mask","shape":7,"type":"MASK","link":null},{"localized_name":"sampling_params","name":"sampling_params","shape":7,"type":"SAMPLING_PARAMS","link":23},{"localized_name":"url","name":"url","type":"STRING","widget":{"name":"url"},"link":null},{"localized_name":"model","name":"model","type":"STRING","widget":{"name":"model"},"link":null},{"localized_name":"prompt","name":"prompt","type":"STRING","widget":{"name":"prompt"},"link":null},{"localized_name":"negative_prompt","name":"negative_prompt","type":"STRING","widget":{"name":"negative_prompt"},"link":null},{"localized_name":"width","name":"width","type":"INT","widget":{"name":"width"},"link":null},{"localized_name":"height","name":"height","type":"INT","widget":{"name":"height"},"link":null}],"outputs":[{"localized_name":"image","name":"image","type":"IMAGE","links":[22]}],"properties":{"Node name for S&R":"VLLMOmniGenerateImage"},"widgets_values":["http://localhost:8000/v1","Qwen/Qwen-Image-Edit","Put this figure in a realistic mountain view","",512,512]},{"id":8,"type":"VLLMOmniDiffusionSampling","pos":[478.59266934006774,183.67711984955648],"size":[284.205078125,202],"flags":{},"order":0,"mode":0,"inputs":[{"localized_name":"n","name":"n","type":"INT","widget":{"name":"n"},"link":null},{"localized_name":"num_inference_steps","name":"num_inference_steps","type":"INT","widget":{"name":"num_inference_steps"},"link":null},{"localized_name":"guidance_scale","name":"guidance_scale","type":"FLOAT","widget":{"name":"guidance_scale"},"link":null},{"localized_name":"true_cfg_scale","name":"true_cfg_scale","type":"FLOAT","widget":{"name":"true_cfg_scale"},"link":null},{"localized_name":"vae_use_slicing","name":"vae_use_slicing","type":"BOOLEAN","widget":{"name":"vae_use_slicing"},"link":null},{"localized_name":"seed","name":"seed","type":"INT","widget":{"name":"seed"},"link":null}],"outputs":[{"localized_name":"diffusion sampling params","name":"diffusion sampling params","type":"SAMPLING_PARAMS","links":[23]}],"properties":{"Node name for S&R":"VLLMOmniDiffusionSampling"},"widgets_values":[4,50,1,1,false,42,"fixed"]},{"id":10,"type":"MarkdownNote","pos":[227.99306819575278,-231.24143306069843],"size":[240.3326478922474,136.3505820791881],"flags":{},"order":2,"mode":0,"inputs":[],"outputs":[],"title":"Note: Task and Input","properties":{},"widgets_values":["vLLM-Omni nodes are categorized based on the output modality. The \"Generate Image\" node supports both text-to-image generation or image-to-image generation (a.k.a. image editing). The node will route to the correct endpoint depending on whether an input image is present or not."],"color":"#432","bgcolor":"#000"},{"id":4,"type":"LoadImage","pos":[496.31859627609606,-229.71277089860084],"size":[270,314],"flags":{},"order":1,"mode":0,"inputs":[{"localized_name":"image","name":"image","type":"COMBO","widget":{"name":"image"},"link":null},{"localized_name":"choose file to upload","name":"upload","type":"IMAGEUPLOAD","widget":{"name":"upload"},"link":null}],"outputs":[{"localized_name":"IMAGE","name":"IMAGE","type":"IMAGE","links":[21]},{"localized_name":"MASK","name":"MASK","type":"MASK","links":null}],"properties":{"Node name for S&R":"LoadImage"},"widgets_values":["example.png","image"]}],"links":[[21,4,0,11,0,"IMAGE"],[22,11,0,3,0,"IMAGE"],[23,8,0,11,2,"SAMPLING_PARAMS"]],"groups":[{"id":1,"title":"Input","bounding":[213.1706010087147,-313.20095750667554,560.8246471437027,407.4246532472182],"color":"#3f789e","font_size":24,"flags":{}}],"config":{},"extra":{"workflowRendererVersion":"LG","ds":{"scale":1.1469075819486894,"offset":[276.8427259980847,549.4885226647582]}},"version":0.4}
|
||||
@@ -0,0 +1 @@
|
||||
{"id":"c0a11241-f0e6-4f5c-8488-b3d344a9df3d","revision":0,"last_node_id":7,"last_link_id":3,"nodes":[{"id":4,"type":"PreviewAudio","pos":[690.1759063763212,226.76444957981545],"size":[270,88],"flags":{},"order":5,"mode":0,"inputs":[{"localized_name":"audio","name":"audio","type":"AUDIO","link":2},{"localized_name":"audioUI","name":"audioUI","type":"AUDIO_UI","widget":{"name":"audioUI"},"link":null}],"outputs":[],"properties":{"Node name for S&R":"PreviewAudio"},"widgets_values":[]},{"id":5,"type":"LoadAudio","pos":[-83.61093919682321,103.80510898465587],"size":[282.798828125,136],"flags":{},"order":0,"mode":0,"inputs":[{"localized_name":"audio","name":"audio","type":"COMBO","widget":{"name":"audio"},"link":null},{"localized_name":"audioUI","name":"audioUI","type":"AUDIO_UI","widget":{"name":"audioUI"},"link":null},{"localized_name":"choose file to upload","name":"upload","type":"AUDIOUPLOAD","widget":{"name":"upload"},"link":null}],"outputs":[{"localized_name":"AUDIO","name":"AUDIO","type":"AUDIO","links":[3]}],"properties":{"Node name for S&R":"LoadAudio"},"widgets_values":["Megan-Fox.mp3",null,null]},{"id":3,"type":"VLLMOmniVoiceClone","pos":[248.63466280064262,168.13112893706375],"size":[400,306],"flags":{},"order":4,"mode":0,"inputs":[{"localized_name":"ref_audio","name":"ref_audio","type":"AUDIO","link":3},{"localized_name":"model_specific_params","name":"model_specific_params","shape":7,"type":"TTS_PARAMS","link":1},{"localized_name":"url","name":"url","type":"STRING","widget":{"name":"url"},"link":null},{"localized_name":"model","name":"model","type":"STRING","widget":{"name":"model"},"link":null},{"localized_name":"input","name":"input","type":"STRING","widget":{"name":"input"},"link":null},{"localized_name":"voice","name":"voice","type":"STRING","widget":{"name":"voice"},"link":null},{"localized_name":"response_format","name":"response_format","type":"COMBO","widget":{"name":"response_format"},"link":null},{"localized_name":"speed","name":"speed","type":"FLOAT","widget":{"name":"speed"},"link":null},{"localized_name":"ref_text","name":"ref_text","type":"STRING","widget":{"name":"ref_text"},"link":null},{"localized_name":"x_vector_only_mode","name":"x_vector_only_mode","type":"BOOLEAN","widget":{"name":"x_vector_only_mode"},"link":null}],"outputs":[{"localized_name":"audio","name":"audio","type":"AUDIO","links":[2]}],"properties":{"Node name for S&R":"VLLMOmniVoiceClone"},"widgets_values":["http://localhost:8000/v1","Qwen/Qwen3-TTS-12Hz-1.7B-Base","Someone just spilled a cup of coffee on my jacket this morning in the subway. Now I have to wear this stained jacket for a whole day!","","mp3",1,"",true]},{"id":6,"type":"MarkdownNote","pos":[255.52667408769662,27.26842775454861],"size":[381.58204108637847,88],"flags":{},"order":2,"mode":0,"inputs":[],"outputs":[],"title":"Note: TTS Nodes","properties":{},"widgets_values":["Apart from the Voice Cloning node, there is also another TTS node for simple text-to-speech tasks (without extra inputs for reference audio)."],"color":"#432","bgcolor":"#000"},{"id":7,"type":"MarkdownNote","pos":[-196.99453003423977,598.3693693727904],"size":[382.4539509209102,88],"flags":{},"order":3,"mode":0,"inputs":[],"outputs":[],"title":"Note: Model-Specific Parameters","properties":{},"widgets_values":["TTS models often require some tailor-made parameters. If you need to customize these parameters, grab one that matches the model you are requesting from \"TTS Params\" subfolder."],"color":"#432","bgcolor":"#000"},{"id":2,"type":"VLLMOmniQwenTTSParams","pos":[-207.8925310560497,342.7942867650348],"size":[400,200],"flags":{},"order":1,"mode":0,"inputs":[{"localized_name":"task_type","name":"task_type","type":"COMBO","widget":{"name":"task_type"},"link":null},{"localized_name":"language","name":"language","type":"COMBO","widget":{"name":"language"},"link":null},{"localized_name":"instructions","name":"instructions","type":"STRING","widget":{"name":"instructions"},"link":null},{"localized_name":"max_new_tokens","name":"max_new_tokens","type":"INT","widget":{"name":"max_new_tokens"},"link":null}],"outputs":[{"localized_name":"Qwen TTS params","name":"Qwen TTS params","type":"TTS_PARAMS","links":[1]}],"properties":{"Node name for S&R":"VLLMOmniQwenTTSParams"},"widgets_values":["Base","Auto","Super angry",2048]}],"links":[[1,2,0,3,1,"TTS_PARAMS"],[2,3,0,4,0,"AUDIO"],[3,5,0,3,0,"AUDIO"]],"groups":[{"id":1,"title":"Model-Specific Parameters","bounding":[-228.38328407738007,264.42790274716276,443.9887293164586,439.6853171248897],"color":"#3f789e","font_size":24,"flags":{}}],"config":{},"extra":{"workflowRendererVersion":"LG","ds":{"scale":1.1469075819486911,"offset":[898.8819468322333,258.7179979718389]}},"version":0.4}
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* @file This file is intended to add dynamic fields to vLLM-Omni nodes
|
||||
* based on widget (in-node form fields) and input (connection link) values and changes.
|
||||
* However, this functionality is currently disabled/commented out.
|
||||
* Because it introduces too much complexity,
|
||||
* and it may even conflict with the current backend (Python) validation for unknown reasons (pending ComfyUI upstream fixes).
|
||||
*/
|
||||
|
||||
import { app } from "../../scripts/app.js";
|
||||
app.registerExtension({
|
||||
name: "vllm.vllm_omni",
|
||||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||
if (!nodeData.name.startsWith("VLLMOmni")) {
|
||||
return
|
||||
}
|
||||
// Stub frontend plugin for now
|
||||
},
|
||||
async setup() {
|
||||
console.info("vLLM-Omni Setup complete!")
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,42 @@
|
||||
# Benchmarks Overview and Architecture
|
||||
|
||||
This document explains the benchmark architecture across all benchmark assets in this repo. It describes what we measure, and where to find or plug in new scenarios. Per-task details remain in subfolder READMEs (e.g., `benchmarks/<model>/README.md`).
|
||||
|
||||
## Scope and goals
|
||||
- Establish repeatable latency/throughput measurements for multimodal LLM pipelines.
|
||||
- Provide both HF Transformers (offline) and vLLM-Omni (multi-stage/pipeline) baselines.
|
||||
- Make it easy to plug in new datasets and models with minimal changes to the runner scripts.
|
||||
|
||||
## Dataset and inputs
|
||||
- Default example: SeedTTS top-100 prompts (`benchmarks/build_dataset/top100.txt`) via `benchmarks/build_dataset/`.
|
||||
- Extensible: drop in new prompt files or modality-aligned payloads; keep the expected format for the consuming scripts (e.g., one prompt per line).
|
||||
- If you add a new dataset, document it under `benchmarks/<model>/README.md` and point scripts to your data path.
|
||||
|
||||
## Directory layout
|
||||
- `benchmarks/build_dataset/` — dataset prep utilities (e.g., SeedTTS top100).
|
||||
- `benchmarks/<model>/vllm_omni/` — vLLM-Omni pipeline benchmarks, logs, outputs.
|
||||
- Add new tasks under `benchmarks/<model>/...` with the same pattern: `transformers/`, `vllm_omni/`, task-specific README, and (optionally) dataset prep notes.
|
||||
|
||||
## Reference workflows
|
||||
- **HF Transformers (offline, single process)**
|
||||
Script (example): `benchmarks/<model>/transformers/eval_qwen3_moe_omni_transformers.sh`
|
||||
Outputs: `benchmark_results/perf_stats.json`, `benchmark_results/results.json`, `benchmark_results/audio/` (if audio is produced).
|
||||
|
||||
- **vLLM-Omni end-to-end pipeline**
|
||||
Script (example): `benchmarks/<model>/vllm_omni/eval_qwen3_moe_omni.sh`
|
||||
Outputs: `vllm_omni/logs/*.stats.jsonl` (per-stage/overall latency & TPS), `vllm_omni/logs/stage*.log`, `vllm_omni/outputs/` (text/audio artifacts).
|
||||
|
||||
- **Adding a new task/model**
|
||||
1) Create `benchmarks/<model>/transformers/` and/or `benchmarks/<model>/vllm_omni/` with scripts referencing your model and dataset.
|
||||
2) Add a task README describing dataset, configs, and expected outputs.
|
||||
3) Keep the output/log structure similar for easy comparison (perf_stats/results/audio or text outputs; stats.jsonl/logs for pipeline).
|
||||
|
||||
## Metrics to watch
|
||||
- **Throughput**: `overall_tps`, `*_tps_avg` per stage.
|
||||
- **Latency distribution**: look for long tails in `*.stats.jsonl`.
|
||||
- **Quality/completeness**: missing outputs or errors in stage logs indicate pipeline failures or misconfigurations.
|
||||
|
||||
## Troubleshooting
|
||||
- Verify GPU/driver/FlashAttention2 requirements for your chosen model/config.
|
||||
- Ensure network access for dataset/model downloads (Google Drive, Hugging Face, etc.).
|
||||
- If outputs are missing or slow, inspect per-stage logs and `*.stats.jsonl` for errors, stragglers, or contention.
|
||||
@@ -0,0 +1,82 @@
|
||||
# Benchmark Dataset Preparation Guide
|
||||
|
||||
This guide describes how to download and prepare the SeedTTS test dataset for benchmarking Qwen-Omni models.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Python 3.8+
|
||||
- `gdown` for downloading from Google Drive
|
||||
- Access to the benchmark scripts
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Navigate to the Dataset Directory
|
||||
|
||||
```bash
|
||||
cd benchmarks/build_dataset
|
||||
```
|
||||
|
||||
### 2. Install Dependencies
|
||||
|
||||
```bash
|
||||
pip install gdown
|
||||
```
|
||||
|
||||
### 3. Download the SeedTTS Test Dataset
|
||||
|
||||
Download the dataset from Google Drive:
|
||||
|
||||
```bash
|
||||
gdown --id 1GlSjVfSHkW3-leKKBlfrjuuTGqQ_xaLP
|
||||
```
|
||||
|
||||
### 4. Extract the Dataset
|
||||
|
||||
```bash
|
||||
tar -xf seedtts_testset.tar
|
||||
```
|
||||
|
||||
### 5. Prepare the Metadata File
|
||||
|
||||
Copy the English metadata file to the working directory:
|
||||
|
||||
```bash
|
||||
cp seedtts_testset/en/meta.lst meta.lst
|
||||
```
|
||||
|
||||
### 6. Extract Prompts
|
||||
|
||||
Extract the first N prompts from the metadata file:
|
||||
|
||||
```bash
|
||||
# Extract top 100 prompts (adjust -n for different amounts)
|
||||
python extract_tts_prompts.py -i meta.lst -o top100.txt -n 100
|
||||
```
|
||||
|
||||
**Options:**
|
||||
- `-i, --input`: Input metadata file (default: `meta.lst`)
|
||||
- `-o, --output`: Output prompts file (default: `prompts.txt`)
|
||||
- `-n, --num-lines`: Number of prompts to extract (required)
|
||||
|
||||
### 7. Clean Up (Optional)
|
||||
|
||||
Remove temporary files to save disk space:
|
||||
|
||||
```bash
|
||||
rm -rf seedtts_testset
|
||||
rm seedtts_testset.tar
|
||||
rm meta.lst
|
||||
```
|
||||
|
||||
## Quick Start (All-in-One)
|
||||
|
||||
```bash
|
||||
# Full setup and benchmark
|
||||
cd benchmarks/build_dataset
|
||||
pip install gdown
|
||||
gdown --id 1GlSjVfSHkW3-leKKBlfrjuuTGqQ_xaLP
|
||||
tar -xf seedtts_testset.tar
|
||||
cp seedtts_testset/en/meta.lst meta.lst
|
||||
python extract_tts_prompts.py -i meta.lst -o top100.txt -n 100
|
||||
rm -rf seedtts_testset seedtts_testset.tar meta.lst
|
||||
```
|
||||
@@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Extract prompts from meta.lst and save them to a txt file.
|
||||
|
||||
Each line in meta.lst has the format:
|
||||
ID|prompt_text|audio_path|target_text
|
||||
|
||||
This script extracts the prompt_text (second field) from the first N lines.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def extract_prompts(input_file: str, output_file: str, num_lines: int) -> None:
|
||||
"""
|
||||
Extract prompts from meta.lst and save to output file.
|
||||
|
||||
Args:
|
||||
input_file: Path to the meta.lst file
|
||||
output_file: Path to the output txt file
|
||||
num_lines: Number of lines to process
|
||||
"""
|
||||
prompts = []
|
||||
|
||||
with open(input_file, encoding="utf-8") as f:
|
||||
for i, line in enumerate(f):
|
||||
if i >= num_lines:
|
||||
break
|
||||
|
||||
line = line.strip()
|
||||
if not line: # Skip empty lines
|
||||
continue
|
||||
|
||||
parts = line.split("|")
|
||||
if len(parts) >= 2:
|
||||
prompt = parts[1] # The prompt is the second field
|
||||
prompts.append(prompt)
|
||||
|
||||
# Write prompts to output file
|
||||
with open(output_file, "w", encoding="utf-8") as f:
|
||||
for prompt in prompts:
|
||||
f.write(prompt + "\n")
|
||||
|
||||
# Print result stats
|
||||
print(f"Extracted {len(prompts)} prompts from first {num_lines} lines")
|
||||
print(f"Saved to: {output_file}")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Extract prompts from meta.lst file")
|
||||
parser.add_argument(
|
||||
"-i", "--input", type=str, default="meta.lst", help="Input meta.lst file path (default: meta.lst)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-o", "--output", type=str, default="prompts.txt", help="Output txt file path (default: prompts.txt)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-n", "--num-lines", type=int, required=True, help="Number of lines to extract from the beginning"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Check if input file exists
|
||||
if not Path(args.input).exists():
|
||||
print(f"Error: Input file '{args.input}' not found")
|
||||
return
|
||||
|
||||
extract_prompts(args.input, args.output, args.num_lines)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,117 @@
|
||||
|
||||
# Diffusion Serving Benchmark (Image/Video)
|
||||
|
||||
This folder contains an online-serving benchmark script for diffusion models.
|
||||
It sends requests to a vLLM OpenAI-compatible endpoint and reports throughput,
|
||||
latency percentiles, and optional SLO attainment.
|
||||
|
||||
The main entrypoint is:
|
||||
|
||||
- `benchmarks/diffusion/diffusion_benchmark_serving.py`
|
||||
|
||||
## 1. Quick Start
|
||||
|
||||
1. Start the server:
|
||||
|
||||
```bash
|
||||
vllm serve Qwen/Qwen-Image --omni --port 8099
|
||||
```
|
||||
|
||||
2. Run a minimal benchmark:
|
||||
|
||||
```bash
|
||||
python3 benchmarks/diffusion/diffusion_benchmark_serving.py \
|
||||
--base-url http://localhost:8099 \
|
||||
--model Qwen/Qwen-Image \
|
||||
--task t2i \
|
||||
--dataset vbench \
|
||||
--num-prompts 5
|
||||
```
|
||||
|
||||
**Notes**
|
||||
|
||||
- The benchmark talks to `http://<host>:<port>/v1/chat/completions`.
|
||||
- If you run the server on another host or port, pass `--base-url` accordingly.
|
||||
|
||||
## 2. Supported Datasets
|
||||
|
||||
The benchmark supports three dataset modes via `--dataset`:
|
||||
|
||||
- `vbench`: Built-in prompt/data loader.
|
||||
- `trace`: Heterogeneous request traces (each request can have different resolution/frames/steps).
|
||||
- `random`: Synthetic prompts for quick smoke tests.
|
||||
|
||||
### VBench dataset
|
||||
|
||||
If you use i2v/i2i bench datasets and need auto-download support, you may need:
|
||||
|
||||
```bash
|
||||
uv pip install gdown
|
||||
```
|
||||
|
||||
### Trace dataset
|
||||
|
||||
Use `--dataset trace` to replay a trace file. The trace can specify per-request fields such as:
|
||||
|
||||
- `width`, `height`
|
||||
- `num_frames` (video)
|
||||
- `num_inference_steps`
|
||||
- `seed`, `fps`
|
||||
- optional `slo_ms` (per-request SLO target)
|
||||
|
||||
By default (when `--dataset-path` is not provided), the script downloads a default trace from
|
||||
the HuggingFace dataset repo `asukaqaqzz/Dit_Trace`. The default filename can depend on `--task`
|
||||
(e.g., `t2v` uses a video trace).
|
||||
|
||||
Current defaults:
|
||||
|
||||
- `--task t2i` -> `sd3_trace.txt`
|
||||
- `--task t2v` -> `cogvideox_trace.txt`
|
||||
|
||||
You can point to your own trace using `--dataset-path`.
|
||||
|
||||
## 3. Benchmark Parameters
|
||||
|
||||
### Basic flags
|
||||
|
||||
- `--base-url`: Server address (the script calls `.../v1/chat/completions`).
|
||||
- `--model`: The OpenAI-compatible `model` field.
|
||||
- `--task`: Task type (e.g., `t2i`, `t2v`, `i2i`, `i2v`).
|
||||
- `--dataset`: Dataset mode (`vbench` / `trace` / `random`).
|
||||
- `--num-prompts`: Number of requests to send.
|
||||
|
||||
Common optional flags:
|
||||
|
||||
- `--output-file`: Write metrics to a JSON file.
|
||||
- `--disable-tqdm`: Disable the progress bar.
|
||||
|
||||
### Resolution / frames / steps: CLI defaults vs dataset fields
|
||||
|
||||
Related flags: `--width`, `--height`, `--num-frames`, `--fps`, `--num-inference-steps`.
|
||||
|
||||
- For `vbench` / `random`: these CLI flags act as global defaults for all generated requests.
|
||||
- For `trace`: each request can carry its own fields (e.g., `width/height/num_frames/num_inference_steps`).
|
||||
|
||||
Precedence rules for `trace` (i.e., what actually gets sent):
|
||||
|
||||
- `width/height`: if either `--width` or `--height` is explicitly set, it overrides per-request values from the trace; otherwise per-request values are used when present.
|
||||
- `num_frames`: per-request `num_frames` takes precedence; otherwise fall back to `--num-frames`.
|
||||
- `num_inference_steps`: per-request `num_inference_steps` takes precedence; otherwise fall back to `--num-inference-steps`.
|
||||
|
||||
### SLO, warmup, and max concurrency
|
||||
|
||||
Enable SLO evaluation with `--slo`.
|
||||
|
||||
- If a request in the trace already has `slo_ms`, that value is used.
|
||||
- Otherwise, the script runs warmup requests to infer a base unit time, estimates `expected_ms` by linearly scaling with area/frames/steps, and then sets `slo_ms = expected_ms * --slo-scale`.
|
||||
|
||||
Warmup flags:
|
||||
|
||||
- `--warmup-requests`: Number of warmup requests.
|
||||
- `--warmup-num-inference-steps`: Steps used during warmup.
|
||||
- For `--task t2v`: warmup requests are forced to use `num_frames=1` to make warmup faster and less noisy.
|
||||
|
||||
Traffic / concurrency flags:
|
||||
|
||||
- `--request-rate`: Target request rate (requests/second). If set to `inf`, the script sends all requests immediately.
|
||||
- `--max-concurrency`: Max number of in-flight requests (default: `1`). This can hard-cap the achieved QPS: if it is too small, requests will queue behind the semaphore, and both achieved throughput and observed SLO attainment can be skewed.
|
||||
@@ -0,0 +1,198 @@
|
||||
import base64
|
||||
import mimetypes
|
||||
import os
|
||||
import time
|
||||
import uuid
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
import aiohttp
|
||||
from tqdm import tqdm
|
||||
|
||||
|
||||
@dataclass
|
||||
class RequestFuncInput:
|
||||
prompt: str
|
||||
api_url: str
|
||||
model: str
|
||||
width: int | None = None
|
||||
height: int | None = None
|
||||
num_frames: int | None = None
|
||||
num_inference_steps: int | None = None
|
||||
seed: int | None = None
|
||||
fps: int | None = None
|
||||
timestamp: float | None = None
|
||||
slo_ms: float | None = None
|
||||
extra_body: dict[str, Any] = field(default_factory=dict)
|
||||
image_paths: list[str] | None = None
|
||||
request_id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
||||
|
||||
|
||||
@dataclass
|
||||
class RequestFuncOutput:
|
||||
success: bool = False
|
||||
latency: float = 0.0
|
||||
error: str = ""
|
||||
start_time: float = 0.0
|
||||
response_body: dict[str, Any] = field(default_factory=dict)
|
||||
peak_memory_mb: float = 0.0
|
||||
slo_achieved: bool | None = None
|
||||
|
||||
|
||||
def _guess_mime_type(path: str) -> str:
|
||||
mime, _ = mimetypes.guess_type(path)
|
||||
return mime or "application/octet-stream"
|
||||
|
||||
|
||||
def _encode_image_as_data_url(path: str) -> str:
|
||||
with open(path, "rb") as f:
|
||||
encoded = base64.b64encode(f.read()).decode("utf-8")
|
||||
mime = _guess_mime_type(path)
|
||||
return f"data:{mime};base64,{encoded}"
|
||||
|
||||
|
||||
async def async_request_chat_completions(
|
||||
input: RequestFuncInput,
|
||||
session: aiohttp.ClientSession,
|
||||
pbar: tqdm | None = None,
|
||||
) -> RequestFuncOutput:
|
||||
output = RequestFuncOutput()
|
||||
output.start_time = time.perf_counter()
|
||||
|
||||
extra_body = dict(input.extra_body)
|
||||
if input.width and input.height:
|
||||
extra_body.setdefault("height", input.height)
|
||||
extra_body.setdefault("width", input.width)
|
||||
if input.num_frames:
|
||||
extra_body.setdefault("num_frames", input.num_frames)
|
||||
if input.num_inference_steps:
|
||||
extra_body.setdefault("num_inference_steps", input.num_inference_steps)
|
||||
if input.seed is not None:
|
||||
extra_body.setdefault("seed", input.seed)
|
||||
if input.fps:
|
||||
extra_body.setdefault("fps", input.fps)
|
||||
|
||||
if input.image_paths and len(input.image_paths) > 0:
|
||||
content = []
|
||||
if input.prompt:
|
||||
content.append({"type": "text", "text": input.prompt})
|
||||
for img_path in input.image_paths:
|
||||
if not os.path.exists(img_path):
|
||||
output.error = f"Image file not found: {img_path}"
|
||||
output.success = False
|
||||
if pbar:
|
||||
pbar.update(1)
|
||||
return output
|
||||
content.append(
|
||||
{
|
||||
"type": "image_url",
|
||||
"image_url": {"url": _encode_image_as_data_url(img_path)},
|
||||
}
|
||||
)
|
||||
messages = [{"role": "user", "content": content}]
|
||||
else:
|
||||
messages = [{"role": "user", "content": input.prompt}]
|
||||
|
||||
payload = {
|
||||
"model": input.model,
|
||||
"messages": messages,
|
||||
}
|
||||
if extra_body:
|
||||
payload["extra_body"] = extra_body
|
||||
|
||||
try:
|
||||
async with session.post(input.api_url, json=payload) as response:
|
||||
if response.status == 200:
|
||||
resp_json = await response.json()
|
||||
output.response_body = resp_json
|
||||
output.success = True
|
||||
if "peak_memory_mb" in resp_json:
|
||||
output.peak_memory_mb = resp_json["peak_memory_mb"]
|
||||
else:
|
||||
output.error = f"HTTP {response.status}: {await response.text()}"
|
||||
output.success = False
|
||||
except Exception as e:
|
||||
output.error = str(e)
|
||||
output.success = False
|
||||
|
||||
output.latency = time.perf_counter() - output.start_time
|
||||
|
||||
if output.success and input.slo_ms is not None:
|
||||
output.slo_achieved = (output.latency * 1000.0) <= float(input.slo_ms)
|
||||
|
||||
if pbar:
|
||||
pbar.update(1)
|
||||
return output
|
||||
|
||||
|
||||
async def async_request_openai_images(
|
||||
input: RequestFuncInput,
|
||||
session: aiohttp.ClientSession,
|
||||
pbar: tqdm | None = None,
|
||||
) -> RequestFuncOutput:
|
||||
"""
|
||||
Send request to OpenAI's /v1/images/generations endpoint.
|
||||
"""
|
||||
output = RequestFuncOutput()
|
||||
output.start_time = time.perf_counter()
|
||||
|
||||
# Build size string from width/height
|
||||
width = input.width or 1024
|
||||
height = input.height or 1024
|
||||
size = f"{width}x{height}"
|
||||
|
||||
payload: dict[str, Any] = {
|
||||
"model": input.model,
|
||||
"prompt": input.prompt,
|
||||
"n": 1,
|
||||
"size": size,
|
||||
"response_format": "b64_json",
|
||||
}
|
||||
|
||||
# Add optional parameters
|
||||
if input.seed is not None:
|
||||
payload["seed"] = input.seed
|
||||
if input.num_inference_steps is not None:
|
||||
payload["num_inference_steps"] = input.num_inference_steps
|
||||
|
||||
# Add any extra body parameters
|
||||
if input.extra_body:
|
||||
for key, value in input.extra_body.items():
|
||||
if key not in payload:
|
||||
payload[key] = value
|
||||
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": "Bearer EMPTY",
|
||||
}
|
||||
|
||||
try:
|
||||
async with session.post(input.api_url, json=payload, headers=headers) as response:
|
||||
if response.status == 200:
|
||||
resp_json = await response.json()
|
||||
output.response_body = resp_json
|
||||
output.success = True
|
||||
# Check for usage/memory info if available
|
||||
if "usage" in resp_json and "peak_memory_mb" in resp_json.get("usage", {}):
|
||||
output.peak_memory_mb = resp_json["usage"]["peak_memory_mb"]
|
||||
else:
|
||||
output.error = f"HTTP {response.status}: {await response.text()}"
|
||||
output.success = False
|
||||
except Exception as e:
|
||||
output.error = str(e)
|
||||
output.success = False
|
||||
|
||||
output.latency = time.perf_counter() - output.start_time
|
||||
|
||||
if output.success and input.slo_ms is not None:
|
||||
output.slo_achieved = (output.latency * 1000.0) <= float(input.slo_ms)
|
||||
|
||||
if pbar:
|
||||
pbar.update(1)
|
||||
return output
|
||||
|
||||
|
||||
backends_function_mapping = {
|
||||
"vllm-omni": (async_request_chat_completions, "/v1/chat/completions"),
|
||||
"openai": (async_request_openai_images, "/v1/images/generations"),
|
||||
}
|
||||
@@ -0,0 +1,978 @@
|
||||
# adapted from sglang and fastvideo
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
"""
|
||||
Benchmark online serving for diffusion models (Image/Video Generation).
|
||||
If you want to use i2v, i2i dataset, you should `uv pip install gdown` first
|
||||
|
||||
Supports multiple backends:
|
||||
- vllm-omni: Uses /v1/chat/completions endpoint (default)
|
||||
- openai: Uses /v1/images/generations endpoint
|
||||
|
||||
Usage:
|
||||
# Video (vllm-omni backend)
|
||||
t2v:
|
||||
python3 benchmarks/diffusion/diffusion_benchmark_serving.py \
|
||||
--backend vllm-omni --dataset vbench --task t2v --num-prompts 10 \
|
||||
--height 480 --width 640 --fps 16 --num-frames 80
|
||||
|
||||
i2v:
|
||||
python3 benchmarks/diffusion/diffusion_benchmark_serving.py \
|
||||
--backend vllm-omni --dataset vbench --task i2v --num-prompts 10
|
||||
|
||||
|
||||
# Image (vllm-omni backend)
|
||||
t2i:
|
||||
python3 benchmarks/diffusion/diffusion_benchmark_serving.py \
|
||||
--backend vllm-omni --dataset vbench --task t2i --num-prompts 10 \
|
||||
--height 1024 --width 1024
|
||||
|
||||
i2i:
|
||||
python3 benchmarks/diffusion/diffusion_benchmark_serving.py \
|
||||
--backend vllm-omni --dataset vbench --task i2i --num-prompts 10
|
||||
|
||||
# Image (openai backend)
|
||||
t2i:
|
||||
python3 benchmarks/diffusion/diffusion_benchmark_serving.py \
|
||||
--backend openai --dataset vbench --task t2i --num-prompts 10 \
|
||||
--height 1024 --width 1024 --port 3000
|
||||
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import ast
|
||||
import asyncio
|
||||
import glob
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
import uuid
|
||||
from abc import ABC, abstractmethod
|
||||
from collections.abc import AsyncGenerator
|
||||
from dataclasses import replace
|
||||
from typing import Any
|
||||
|
||||
import aiohttp
|
||||
import numpy as np
|
||||
import requests
|
||||
from backends import RequestFuncInput, RequestFuncOutput, backends_function_mapping
|
||||
from tqdm.asyncio import tqdm
|
||||
|
||||
|
||||
class BaseDataset(ABC):
|
||||
def __init__(self, args, api_url: str, model: str):
|
||||
self.args = args
|
||||
self.api_url = api_url
|
||||
self.model = model
|
||||
|
||||
@abstractmethod
|
||||
def __len__(self) -> int:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def __getitem__(self, idx: int) -> RequestFuncInput:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_requests(self) -> list[RequestFuncInput]:
|
||||
pass
|
||||
|
||||
|
||||
class VBenchDataset(BaseDataset):
|
||||
"""
|
||||
Dataset loader for VBench prompts.
|
||||
Supports t2v, i2v.
|
||||
"""
|
||||
|
||||
T2V_PROMPT_URL = (
|
||||
"https://raw.githubusercontent.com/Vchitect/VBench/master/prompts/prompts_per_dimension/subject_consistency.txt"
|
||||
)
|
||||
I2V_DOWNLOAD_SCRIPT_URL = (
|
||||
"https://raw.githubusercontent.com/Vchitect/VBench/master/vbench2_beta_i2v/download_data.sh"
|
||||
)
|
||||
|
||||
def __init__(self, args, api_url: str, model: str):
|
||||
super().__init__(args, api_url, model)
|
||||
self.cache_dir = os.path.join(os.path.expanduser("~"), ".cache", "vllm-omni")
|
||||
self.items = self._load_data()
|
||||
|
||||
def _load_data(self) -> list[dict[str, Any]]:
|
||||
if self.args.task == "t2v":
|
||||
return self._load_t2v_prompts()
|
||||
elif self.args.task in ["i2v", "ti2v", "ti2i", "i2i"]:
|
||||
return self._load_i2v_data()
|
||||
else:
|
||||
return self._load_t2v_prompts()
|
||||
|
||||
def _download_file(self, url: str, dest_path: str) -> None:
|
||||
"""Download a file from URL to destination path."""
|
||||
os.makedirs(os.path.dirname(dest_path), exist_ok=True)
|
||||
resp = requests.get(url)
|
||||
resp.raise_for_status()
|
||||
with open(dest_path, "w") as f:
|
||||
f.write(resp.text)
|
||||
|
||||
def _load_t2v_prompts(self) -> list[dict[str, Any]]:
|
||||
path = self.args.dataset_path
|
||||
|
||||
if not path:
|
||||
path = os.path.join(self.cache_dir, "vbench_subject_consistency.txt")
|
||||
if not os.path.exists(path):
|
||||
print(f"Downloading VBench T2V prompts to {path}...")
|
||||
try:
|
||||
self._download_file(self.T2V_PROMPT_URL, path)
|
||||
except Exception as e:
|
||||
print(f"Failed to download VBench prompts: {e}")
|
||||
return [{"prompt": "A cat sitting on a bench"}] * 50
|
||||
|
||||
prompts = []
|
||||
with open(path) as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line:
|
||||
prompts.append({"prompt": line})
|
||||
|
||||
return self._resize_data(prompts)
|
||||
|
||||
def _auto_download_i2v_dataset(self) -> str:
|
||||
"""Auto-download VBench I2V dataset and return the dataset directory."""
|
||||
vbench_i2v_dir = os.path.join(self.cache_dir, "vbench_i2v", "vbench2_beta_i2v")
|
||||
info_json_path = os.path.join(vbench_i2v_dir, "data", "i2v-bench-info.json")
|
||||
|
||||
if os.path.exists(info_json_path):
|
||||
return vbench_i2v_dir
|
||||
|
||||
print(f"Downloading VBench I2V dataset to {vbench_i2v_dir}...")
|
||||
try:
|
||||
cache_root = os.path.join(self.cache_dir, "vbench_i2v")
|
||||
script_path = os.path.join(cache_root, "download_data.sh")
|
||||
|
||||
self._download_file(self.I2V_DOWNLOAD_SCRIPT_URL, script_path)
|
||||
os.chmod(script_path, 0o755)
|
||||
|
||||
print("Executing download_data.sh (this may take a while)...")
|
||||
import subprocess
|
||||
|
||||
result = subprocess.run(
|
||||
["bash", script_path],
|
||||
cwd=cache_root,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(f"Download script failed: {result.stderr}")
|
||||
|
||||
print(f"Successfully downloaded VBench I2V dataset to {vbench_i2v_dir}")
|
||||
except Exception as e:
|
||||
print(f"Failed to download VBench I2V dataset: {e}")
|
||||
print("Please manually download following instructions at:")
|
||||
print("https://github.com/Vchitect/VBench/tree/master/vbench2_beta_i2v#22-download")
|
||||
return None
|
||||
|
||||
return vbench_i2v_dir if os.path.exists(info_json_path) else None
|
||||
|
||||
def _load_from_i2v_json(self, json_path: str) -> list[dict[str, Any]]:
|
||||
"""Load I2V data from i2v-bench-info.json format."""
|
||||
with open(json_path) as f:
|
||||
items = json.load(f)
|
||||
|
||||
base_dir = os.path.dirname(os.path.dirname(json_path)) # Go up to vbench2_beta_i2v
|
||||
origin_dir = os.path.join(base_dir, "data", "origin")
|
||||
|
||||
data = []
|
||||
for item in items:
|
||||
img_path = os.path.join(origin_dir, item.get("file_name", ""))
|
||||
if os.path.exists(img_path):
|
||||
data.append({"prompt": item.get("caption", ""), "image_path": img_path})
|
||||
else:
|
||||
print(f"Warning: Image not found: {img_path}")
|
||||
|
||||
print(f"Loaded {len(data)} I2V samples from VBench I2V dataset")
|
||||
return data
|
||||
|
||||
def _scan_directory_for_images(self, path: str) -> list[dict[str, Any]]:
|
||||
"""Scan directory for image files."""
|
||||
exts = ["*.jpg", "*.jpeg", "*.png", "*.webp"]
|
||||
files = []
|
||||
|
||||
for ext in exts:
|
||||
files.extend(glob.glob(os.path.join(path, ext)))
|
||||
files.extend(glob.glob(os.path.join(path, ext.upper())))
|
||||
|
||||
# Also check in data/origin subdirectory
|
||||
origin_dir = os.path.join(path, "data", "origin")
|
||||
if os.path.exists(origin_dir):
|
||||
files.extend(glob.glob(os.path.join(origin_dir, ext)))
|
||||
files.extend(glob.glob(os.path.join(origin_dir, ext.upper())))
|
||||
|
||||
return [{"prompt": os.path.splitext(os.path.basename(f))[0], "image_path": f} for f in files]
|
||||
|
||||
def _create_dummy_data(self) -> list[dict[str, Any]]:
|
||||
"""Create dummy data with a placeholder image in cache directory."""
|
||||
print("No I2V data found. Using dummy placeholders.")
|
||||
|
||||
dummy_image = os.path.join(self.cache_dir, "dummy_image.jpg")
|
||||
if not os.path.exists(dummy_image):
|
||||
try:
|
||||
from PIL import Image
|
||||
|
||||
os.makedirs(self.cache_dir, exist_ok=True)
|
||||
img = Image.new("RGB", (100, 100), color="red")
|
||||
img.save(dummy_image)
|
||||
print(f"Created dummy image at {dummy_image}")
|
||||
except ImportError:
|
||||
print("PIL not installed, cannot create dummy image.")
|
||||
return []
|
||||
|
||||
return [{"prompt": "A moving cat", "image_path": dummy_image}] * 10
|
||||
|
||||
def _load_i2v_data(self) -> list[dict[str, Any]]:
|
||||
"""Load I2V data from VBench I2V dataset or user-provided path."""
|
||||
path = self.args.dataset_path
|
||||
|
||||
# Auto-download if no path provided
|
||||
if not path:
|
||||
path = self._auto_download_i2v_dataset()
|
||||
if not path:
|
||||
return self._resize_data(self._create_dummy_data())
|
||||
|
||||
# Try to load from i2v-bench-info.json
|
||||
info_json_candidates = [
|
||||
os.path.join(path, "data", "i2v-bench-info.json"),
|
||||
path if path.endswith(".json") else None,
|
||||
]
|
||||
|
||||
for json_path in info_json_candidates:
|
||||
if json_path and os.path.exists(json_path):
|
||||
try:
|
||||
return self._resize_data(self._load_from_i2v_json(json_path))
|
||||
except Exception as e:
|
||||
print(f"Failed to load {json_path}: {e}")
|
||||
|
||||
# Fallback: scan directory for images
|
||||
if os.path.isdir(path):
|
||||
data = self._scan_directory_for_images(path)
|
||||
if data:
|
||||
return self._resize_data(data)
|
||||
|
||||
# Last resort: dummy data
|
||||
return self._resize_data(self._create_dummy_data())
|
||||
|
||||
def _resize_data(self, data: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
"""Resize data to match num_prompts."""
|
||||
if not self.args.num_prompts:
|
||||
return data
|
||||
|
||||
if len(data) < self.args.num_prompts:
|
||||
factor = (self.args.num_prompts // len(data)) + 1
|
||||
data = data * factor
|
||||
|
||||
return data[: self.args.num_prompts]
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(self.items)
|
||||
|
||||
def __getitem__(self, idx: int) -> RequestFuncInput:
|
||||
item = self.items[idx]
|
||||
image_paths = [item["image_path"]] if "image_path" in item else None
|
||||
|
||||
return RequestFuncInput(
|
||||
prompt=item.get("prompt", ""),
|
||||
api_url=self.api_url,
|
||||
model=self.model,
|
||||
width=self.args.width,
|
||||
height=self.args.height,
|
||||
num_frames=self.args.num_frames,
|
||||
num_inference_steps=self.args.num_inference_steps,
|
||||
seed=self.args.seed,
|
||||
fps=self.args.fps,
|
||||
image_paths=image_paths,
|
||||
)
|
||||
|
||||
def get_requests(self) -> list[RequestFuncInput]:
|
||||
return [self[i] for i in range(len(self))]
|
||||
|
||||
|
||||
class TraceDataset(BaseDataset):
|
||||
"""Trace-based dataset loader for heterogeneous diffusion requests."""
|
||||
|
||||
DEFAULT_REPO_ID = "asukaqaqzz/Dit_Trace"
|
||||
DEFAULT_FILENAME = "sd3_trace.txt"
|
||||
DEFAULT_FILENAME_BY_TASK: dict[str, str] = {
|
||||
# Text-to-image traces (e.g., SD3)
|
||||
"t2i": "sd3_trace.txt",
|
||||
# Text-to-video traces (e.g., CogVideoX)
|
||||
"t2v": "cogvideox_trace.txt",
|
||||
}
|
||||
|
||||
def __init__(self, args, api_url: str, model: str):
|
||||
super().__init__(args, api_url, model)
|
||||
self.cache_dir = os.path.join(os.path.expanduser("~"), ".cache", "vllm-omni", "trace")
|
||||
self.default_filename = self.DEFAULT_FILENAME_BY_TASK.get(getattr(args, "task", ""), self.DEFAULT_FILENAME)
|
||||
dataset_root = args.dataset_path
|
||||
if not dataset_root:
|
||||
dataset_root = self._download_default_trace()
|
||||
self.items = self._load_items(dataset_root)
|
||||
|
||||
@staticmethod
|
||||
def _coerce_int(x: Any) -> int | None:
|
||||
if x is None:
|
||||
return None
|
||||
if isinstance(x, bool):
|
||||
return None
|
||||
if isinstance(x, int):
|
||||
return x
|
||||
try:
|
||||
s = str(x).strip()
|
||||
if not s:
|
||||
return None
|
||||
return int(float(s))
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _coerce_float(x: Any) -> float | None:
|
||||
if x is None:
|
||||
return None
|
||||
if isinstance(x, float):
|
||||
return x
|
||||
if isinstance(x, int):
|
||||
return float(x)
|
||||
try:
|
||||
s = str(x).strip()
|
||||
if not s:
|
||||
return None
|
||||
return float(s)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def _download_default_trace(self) -> str:
|
||||
"""Download default trace file from HuggingFace Hub if not provided."""
|
||||
|
||||
try:
|
||||
from huggingface_hub import hf_hub_download
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"huggingface_hub is required to download the default trace dataset. "
|
||||
"Install via `pip install huggingface_hub`."
|
||||
) from exc
|
||||
|
||||
os.makedirs(self.cache_dir, exist_ok=True)
|
||||
return hf_hub_download(
|
||||
repo_id=self.DEFAULT_REPO_ID,
|
||||
filename=self.default_filename,
|
||||
repo_type="dataset",
|
||||
local_dir=self.cache_dir,
|
||||
local_dir_use_symlinks=False,
|
||||
)
|
||||
|
||||
def _expand_paths(self, dataset_path: str | None) -> list[str]:
|
||||
if not dataset_path:
|
||||
return []
|
||||
|
||||
parts = [p.strip() for p in str(dataset_path).split(",") if p.strip()]
|
||||
paths: list[str] = []
|
||||
for p in parts:
|
||||
if any(ch in p for ch in ["*", "?", "["]):
|
||||
paths.extend(sorted(glob.glob(p)))
|
||||
elif os.path.isdir(p):
|
||||
paths.extend(sorted(glob.glob(os.path.join(p, "**", "*.txt"), recursive=True)))
|
||||
else:
|
||||
paths.append(p)
|
||||
|
||||
seen = set()
|
||||
unique_paths = []
|
||||
for p in paths:
|
||||
if p not in seen:
|
||||
seen.add(p)
|
||||
unique_paths.append(p)
|
||||
return unique_paths
|
||||
|
||||
def _parse_trace_file(self, path: str) -> list[dict[str, Any]]:
|
||||
rows: list[dict[str, Any]] = []
|
||||
|
||||
def parse_request_repr_line(line: str) -> dict[str, Any] | None:
|
||||
text = line.strip()
|
||||
if not text:
|
||||
return None
|
||||
if not (text.startswith("Request(") and text.endswith(")")):
|
||||
return None
|
||||
inner = text[len("Request(") : -1]
|
||||
try:
|
||||
expr = ast.parse(f"f({inner})", mode="eval")
|
||||
if not isinstance(expr.body, ast.Call):
|
||||
return None
|
||||
call = expr.body
|
||||
out: dict[str, Any] = {}
|
||||
for kw in call.keywords:
|
||||
if kw.arg is None:
|
||||
continue
|
||||
out[kw.arg] = ast.literal_eval(kw.value)
|
||||
return out
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
# detect first non-empty line to pick parser
|
||||
first_non_empty = None
|
||||
with open(path, encoding="utf-8") as f:
|
||||
for _ in range(50):
|
||||
pos = f.tell()
|
||||
line = f.readline()
|
||||
if not line:
|
||||
break
|
||||
if line.strip():
|
||||
first_non_empty = line.strip()
|
||||
f.seek(pos)
|
||||
break
|
||||
|
||||
if first_non_empty is None:
|
||||
return rows
|
||||
|
||||
if first_non_empty.startswith("Request("):
|
||||
with open(path, encoding="utf-8") as f:
|
||||
for line in f:
|
||||
parsed = parse_request_repr_line(line)
|
||||
if isinstance(parsed, dict):
|
||||
rows.append(parsed)
|
||||
return rows
|
||||
|
||||
# txt fallback: parse Request(...) lines only
|
||||
with open(path, encoding="utf-8") as f:
|
||||
for line in f:
|
||||
parsed = parse_request_repr_line(line)
|
||||
if isinstance(parsed, dict):
|
||||
rows.append(parsed)
|
||||
return rows
|
||||
|
||||
def _load_items(self, dataset_root: str) -> list[dict[str, Any]]:
|
||||
paths = self._expand_paths(dataset_root)
|
||||
if not paths:
|
||||
raise ValueError("No trace files found. Provide --dataset-path or rely on default HuggingFace download.")
|
||||
|
||||
items: list[dict[str, Any]] = []
|
||||
for p in paths:
|
||||
if not os.path.exists(p):
|
||||
continue
|
||||
for row in self._parse_trace_file(p):
|
||||
if isinstance(row, dict):
|
||||
row = dict(row)
|
||||
row.setdefault("_source", p)
|
||||
items.append(row)
|
||||
|
||||
if not items:
|
||||
raise ValueError("Trace dataset is empty after parsing provided paths.")
|
||||
|
||||
if self.args.num_prompts is not None:
|
||||
items = items[: self.args.num_prompts]
|
||||
|
||||
return items
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(self.items)
|
||||
|
||||
def __getitem__(self, idx: int) -> RequestFuncInput:
|
||||
row = self.items[idx]
|
||||
prompt = row.get("prompt") or row.get("text") or ""
|
||||
|
||||
row_height = self._coerce_int(row.get("height"))
|
||||
row_width = self._coerce_int(row.get("width"))
|
||||
num_frames = self._coerce_int(row.get("num_frames"))
|
||||
num_steps = self._coerce_int(row.get("num_inference_steps"))
|
||||
seed = self._coerce_int(row.get("seed"))
|
||||
fps = self._coerce_int(row.get("fps"))
|
||||
timestamp = self._coerce_float(row.get("timestamp"))
|
||||
slo_ms = self._coerce_float(row.get("slo_ms"))
|
||||
image_paths = row.get("image_paths")
|
||||
|
||||
override_w = self.args.width
|
||||
override_h = self.args.height
|
||||
if override_w is not None or override_h is not None:
|
||||
width = override_w
|
||||
height = override_h
|
||||
else:
|
||||
width = row_width
|
||||
height = row_height
|
||||
|
||||
return RequestFuncInput(
|
||||
prompt=str(prompt),
|
||||
api_url=self.api_url,
|
||||
model=self.model,
|
||||
width=width,
|
||||
height=height,
|
||||
num_frames=num_frames if num_frames is not None else self.args.num_frames,
|
||||
num_inference_steps=num_steps if num_steps is not None else self.args.num_inference_steps,
|
||||
seed=seed if seed is not None else self.args.seed,
|
||||
fps=fps if fps is not None else self.args.fps,
|
||||
timestamp=timestamp,
|
||||
slo_ms=slo_ms,
|
||||
image_paths=image_paths,
|
||||
request_id=str(row.get("request_id")) if row.get("request_id") is not None else str(uuid.uuid4()),
|
||||
)
|
||||
|
||||
def get_requests(self) -> list[RequestFuncInput]:
|
||||
return [self[i] for i in range(len(self))]
|
||||
|
||||
|
||||
class RandomDataset(BaseDataset):
|
||||
def __init__(self, args, api_url: str, model: str):
|
||||
self.args = args
|
||||
self.api_url = api_url
|
||||
self.model = model
|
||||
self.num_prompts = args.num_prompts
|
||||
|
||||
def __len__(self) -> int:
|
||||
return self.num_prompts
|
||||
|
||||
def __getitem__(self, idx: int) -> RequestFuncInput:
|
||||
return RequestFuncInput(
|
||||
prompt=f"Random prompt {idx} for benchmarking diffusion models",
|
||||
api_url=self.api_url,
|
||||
model=self.model,
|
||||
width=self.args.width,
|
||||
height=self.args.height,
|
||||
num_frames=self.args.num_frames,
|
||||
num_inference_steps=self.args.num_inference_steps,
|
||||
seed=self.args.seed,
|
||||
fps=self.args.fps,
|
||||
)
|
||||
|
||||
def get_requests(self) -> list[RequestFuncInput]:
|
||||
return [self[i] for i in range(len(self))]
|
||||
|
||||
|
||||
def _compute_expected_latency_ms_from_base(req: RequestFuncInput, args, base_time_ms: float | None) -> float | None:
|
||||
"""Compute expected execution time (ms) based on a base per-step-per-frame unit time.
|
||||
|
||||
Assumes linear scaling with pixel area, frame count, and num_inference_steps.
|
||||
The base unit represents latency for a 16x16 resolution, single frame, single step.
|
||||
"""
|
||||
|
||||
if base_time_ms is None:
|
||||
return None
|
||||
|
||||
width = req.width if req.width is not None else args.width
|
||||
height = req.height if req.height is not None else args.height
|
||||
if width is None or height is None:
|
||||
return None
|
||||
|
||||
frames = req.num_frames if req.num_frames is not None else args.num_frames
|
||||
steps = req.num_inference_steps if req.num_inference_steps is not None else args.num_inference_steps
|
||||
|
||||
frame_scale = frames if isinstance(frames, int) and frames > 0 else 1
|
||||
step_scale = steps if isinstance(steps, int) and steps > 0 else 1
|
||||
|
||||
area_units = max((float(width) * float(height)) / float(16 * 16), 1.0)
|
||||
return float(base_time_ms) * area_units * frame_scale * step_scale
|
||||
|
||||
|
||||
def _infer_slo_base_time_ms_from_warmups(
|
||||
warmup_pairs: list[tuple[RequestFuncInput, RequestFuncOutput]],
|
||||
args,
|
||||
) -> float | None:
|
||||
"""Infer base SLO unit time from warmup requests.
|
||||
|
||||
Returns the median base latency (ms) for a 16x16 resolution, single-frame,
|
||||
single-step request. Only uses warmups that succeeded and have resolvable
|
||||
width/height.
|
||||
"""
|
||||
|
||||
candidates_ms: list[float] = []
|
||||
for req, out in warmup_pairs:
|
||||
if not out.success or out.latency <= 0:
|
||||
continue
|
||||
|
||||
width = req.width if req.width is not None else args.width
|
||||
height = req.height if req.height is not None else args.height
|
||||
if width is None or height is None:
|
||||
continue
|
||||
|
||||
frames = req.num_frames if req.num_frames is not None else args.num_frames
|
||||
steps = req.num_inference_steps if req.num_inference_steps is not None else args.num_inference_steps
|
||||
|
||||
frame_scale = int(frames) if isinstance(frames, int) and frames > 0 else 1
|
||||
step_scale = int(steps) if isinstance(steps, int) and steps > 0 else 1
|
||||
|
||||
area_units = max((float(width) * float(height)) / float(16 * 16), 1.0)
|
||||
denom = area_units * float(frame_scale) * float(step_scale)
|
||||
if denom <= 0:
|
||||
continue
|
||||
|
||||
candidates_ms.append((out.latency * 1000.0) / denom)
|
||||
|
||||
if not candidates_ms:
|
||||
return None
|
||||
return float(np.median(candidates_ms))
|
||||
|
||||
|
||||
def _populate_slo_ms_from_warmups(
|
||||
requests_list: list[RequestFuncInput],
|
||||
warmup_pairs: list[tuple[RequestFuncInput, RequestFuncOutput]],
|
||||
args,
|
||||
) -> list[RequestFuncInput]:
|
||||
"""Populate missing RequestFuncInput.slo_ms using warmup outputs.
|
||||
|
||||
- If a request already has slo_ms (e.g., trace-provided), it is kept as-is.
|
||||
- If any request has slo_ms is None and we can infer base time from warmups,
|
||||
we estimate each missing request's expected execution time and set:
|
||||
req.slo_ms = expected_latency_ms * args.slo_scale
|
||||
|
||||
Returns updated requests_list.
|
||||
"""
|
||||
|
||||
if not any(req.slo_ms is None for req in requests_list):
|
||||
return requests_list
|
||||
|
||||
base_time_ms = _infer_slo_base_time_ms_from_warmups(warmup_pairs, args)
|
||||
if base_time_ms is None:
|
||||
return requests_list
|
||||
|
||||
slo_scale = float(getattr(args, "slo_scale", 3.0))
|
||||
if slo_scale <= 0:
|
||||
raise ValueError(f"slo_scale must be positive, got {slo_scale}.")
|
||||
|
||||
updated: list[RequestFuncInput] = []
|
||||
for req in requests_list:
|
||||
if req.slo_ms is not None:
|
||||
updated.append(req)
|
||||
continue
|
||||
expected_ms = _compute_expected_latency_ms_from_base(req, args, base_time_ms)
|
||||
updated.append(replace(req, slo_ms=(expected_ms * slo_scale) if expected_ms is not None else None))
|
||||
|
||||
return updated
|
||||
|
||||
|
||||
async def iter_requests(
|
||||
requests_list: list[RequestFuncInput],
|
||||
request_rate: float,
|
||||
) -> AsyncGenerator[RequestFuncInput, None]:
|
||||
"""Yield requests using a fixed interval if request_rate is set.
|
||||
|
||||
- If request_rate is inf, all requests are yielded immediately (no sleep).
|
||||
- Otherwise, requests are emitted at a fixed cadence of 1 / request_rate seconds.
|
||||
"""
|
||||
|
||||
if request_rate != float("inf"):
|
||||
if request_rate <= 0:
|
||||
raise ValueError(f"request_rate must be positive or inf, got {request_rate}.")
|
||||
interval_s = 1.0 / float(request_rate)
|
||||
|
||||
for i, req in enumerate(requests_list):
|
||||
if request_rate != float("inf") and i > 0:
|
||||
await asyncio.sleep(interval_s)
|
||||
yield req
|
||||
|
||||
|
||||
def calculate_metrics(
|
||||
outputs: list[RequestFuncOutput],
|
||||
total_duration: float,
|
||||
requests_list: list[RequestFuncInput],
|
||||
args,
|
||||
slo_enabled: bool,
|
||||
):
|
||||
success_outputs = [o for o in outputs if o.success]
|
||||
error_outputs = [o for o in outputs if not o.success]
|
||||
|
||||
num_success = len(success_outputs)
|
||||
latencies = [o.latency for o in success_outputs]
|
||||
peak_memories = [o.peak_memory_mb for o in success_outputs if o.peak_memory_mb > 0]
|
||||
|
||||
metrics = {
|
||||
"duration": total_duration,
|
||||
"completed_requests": num_success,
|
||||
"failed_requests": len(error_outputs),
|
||||
"throughput_qps": num_success / total_duration if total_duration > 0 else 0,
|
||||
"latency_mean": np.mean(latencies) if latencies else 0,
|
||||
"latency_median": np.median(latencies) if latencies else 0,
|
||||
"latency_p99": np.percentile(latencies, 99) if latencies else 0,
|
||||
"latency_p50": np.percentile(latencies, 50) if latencies else 0,
|
||||
"peak_memory_mb_max": max(peak_memories) if peak_memories else 0,
|
||||
"peak_memory_mb_mean": np.mean(peak_memories) if peak_memories else 0,
|
||||
"peak_memory_mb_median": np.median(peak_memories) if peak_memories else 0,
|
||||
}
|
||||
|
||||
if slo_enabled:
|
||||
slo_defined_total = 0
|
||||
slo_met_success = 0
|
||||
|
||||
for req, out in zip(requests_list, outputs):
|
||||
if req.slo_ms is None:
|
||||
continue
|
||||
slo_defined_total += 1
|
||||
if out.slo_achieved is None:
|
||||
continue
|
||||
if out.slo_achieved:
|
||||
slo_met_success += 1
|
||||
|
||||
slo_attain_all = (slo_met_success / slo_defined_total) if slo_defined_total > 0 else 0.0
|
||||
|
||||
metrics.update(
|
||||
{
|
||||
"slo_attainment_rate": slo_attain_all,
|
||||
"slo_met_success": slo_met_success,
|
||||
"slo_scale": getattr(args, "slo_scale", 3.0),
|
||||
}
|
||||
)
|
||||
|
||||
return metrics
|
||||
|
||||
|
||||
def wait_for_service(base_url: str, timeout: int = 120) -> None:
|
||||
print(f"Waiting for service at {base_url}...")
|
||||
start_time = time.time()
|
||||
while True:
|
||||
try:
|
||||
# Try /health endpoint first
|
||||
resp = requests.get(f"{base_url}/health", timeout=1)
|
||||
if resp.status_code == 200:
|
||||
print("Service is ready.")
|
||||
break
|
||||
except requests.exceptions.RequestException:
|
||||
pass
|
||||
|
||||
if time.time() - start_time > timeout:
|
||||
raise TimeoutError(f"Service at {base_url} did not start within {timeout} seconds.")
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
|
||||
async def benchmark(args):
|
||||
# Construct base_url if not provided
|
||||
if args.base_url is None:
|
||||
args.base_url = f"http://{args.host}:{args.port}"
|
||||
|
||||
# Setup API URL and request function based on backend
|
||||
request_func, api_url = backends_function_mapping[args.backend]
|
||||
api_url = f"{args.base_url}{api_url}"
|
||||
|
||||
if args.dataset == "vbench":
|
||||
dataset = VBenchDataset(args, api_url, args.model)
|
||||
elif args.dataset == "trace":
|
||||
dataset = TraceDataset(args, api_url, args.model)
|
||||
elif args.dataset == "random":
|
||||
dataset = RandomDataset(args, api_url, args.model)
|
||||
else:
|
||||
raise ValueError(f"Unknown dataset: {args.dataset}")
|
||||
|
||||
print("Loading requests...")
|
||||
requests_list = dataset.get_requests()
|
||||
print(f"Prepared {len(requests_list)} requests from {args.dataset} dataset.")
|
||||
|
||||
# Limit concurrency
|
||||
if args.max_concurrency is not None:
|
||||
semaphore = asyncio.Semaphore(args.max_concurrency)
|
||||
else:
|
||||
semaphore = None
|
||||
|
||||
async def limited_request_func(req, session, pbar):
|
||||
if semaphore:
|
||||
async with semaphore:
|
||||
return await request_func(req, session, pbar)
|
||||
else:
|
||||
return await request_func(req, session, pbar)
|
||||
|
||||
# Run benchmark
|
||||
pbar = tqdm(total=len(requests_list), disable=args.disable_tqdm)
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
warmup_pairs: list[tuple[RequestFuncInput, RequestFuncOutput]] = []
|
||||
if args.warmup_requests and requests_list:
|
||||
print(
|
||||
f"Running {args.warmup_requests} warmup request(s) \
|
||||
with num_inference_steps={args.warmup_num_inference_steps}..."
|
||||
)
|
||||
for i in range(args.warmup_requests):
|
||||
warm_req = requests_list[i % len(requests_list)]
|
||||
if args.warmup_num_inference_steps is not None:
|
||||
warm_req = replace(
|
||||
warm_req,
|
||||
num_inference_steps=args.warmup_num_inference_steps,
|
||||
)
|
||||
warm_out = await limited_request_func(warm_req, session, None)
|
||||
warmup_pairs.append((warm_req, warm_out))
|
||||
|
||||
if args.slo:
|
||||
# Prefer trace-provided per-request slo_ms. Only populate when missing.
|
||||
requests_list = _populate_slo_ms_from_warmups(
|
||||
requests_list=requests_list,
|
||||
warmup_pairs=warmup_pairs,
|
||||
args=args,
|
||||
)
|
||||
|
||||
start_time = time.perf_counter()
|
||||
tasks = []
|
||||
async for req in iter_requests(requests_list=requests_list, request_rate=args.request_rate):
|
||||
task = asyncio.create_task(limited_request_func(req, session, pbar))
|
||||
tasks.append(task)
|
||||
|
||||
outputs = await asyncio.gather(*tasks)
|
||||
total_duration = time.perf_counter() - start_time
|
||||
|
||||
pbar.close()
|
||||
|
||||
# Calculate metrics
|
||||
metrics = calculate_metrics(outputs, total_duration, requests_list, args, args.slo)
|
||||
|
||||
# Add configuration info to metrics for JSON output
|
||||
metrics["backend"] = args.backend
|
||||
metrics["model"] = args.model
|
||||
metrics["dataset"] = args.dataset
|
||||
metrics["task"] = args.task
|
||||
|
||||
print("\n{s:{c}^{n}}".format(s=" Serving Benchmark Result ", n=60, c="="))
|
||||
|
||||
# Section 1: Configuration
|
||||
print("{:<40} {:<15}".format("Backend:", args.backend))
|
||||
print("{:<40} {:<15}".format("Model:", args.model))
|
||||
print("{:<40} {:<15}".format("Dataset:", args.dataset))
|
||||
print("{:<40} {:<15}".format("Task:", args.task))
|
||||
|
||||
# Section 2: Execution & Traffic
|
||||
print(f"{'-' * 50}")
|
||||
print("{:<40} {:<15.2f}".format("Benchmark duration (s):", metrics["duration"]))
|
||||
print("{:<40} {:<15}".format("Request rate:", str(args.request_rate)))
|
||||
print(
|
||||
"{:<40} {:<15}".format(
|
||||
"Max request concurrency:",
|
||||
str(args.max_concurrency) if args.max_concurrency else "not set",
|
||||
)
|
||||
)
|
||||
print("{:<40} {}/{:<15}".format("Successful requests:", metrics["completed_requests"], len(requests_list)))
|
||||
|
||||
# Section 3: Performance Metrics
|
||||
print(f"{'-' * 50}")
|
||||
|
||||
print("{:<40} {:<15.2f}".format("Request throughput (req/s):", metrics["throughput_qps"]))
|
||||
print("{:<40} {:<15.4f}".format("Latency Mean (s):", metrics["latency_mean"]))
|
||||
print("{:<40} {:<15.4f}".format("Latency Median (s):", metrics["latency_median"]))
|
||||
print("{:<40} {:<15.4f}".format("Latency P99 (s):", metrics["latency_p99"]))
|
||||
|
||||
if args.slo:
|
||||
print(f"{'-' * 50}")
|
||||
print("{:<40} {:<15.2%}".format("SLO Attainment Rate (all):", metrics.get("slo_attainment_rate", 0.0)))
|
||||
print("{:<40} {:<15}".format("SLO Met (success count):", str(metrics.get("slo_met_success", 0))))
|
||||
print("{:<40} {:<15}".format("SLO Scale:", str(metrics.get("slo_scale", 3.0))))
|
||||
|
||||
if metrics["peak_memory_mb_max"] > 0:
|
||||
print(f"{'-' * 50}")
|
||||
print("{:<40} {:<15.2f}".format("Peak Memory Max (MB):", metrics["peak_memory_mb_max"]))
|
||||
print("{:<40} {:<15.2f}".format("Peak Memory Mean (MB):", metrics["peak_memory_mb_mean"]))
|
||||
print("{:<40} {:<15.2f}".format("Peak Memory Median (MB):", metrics["peak_memory_mb_median"]))
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
|
||||
if args.output_file:
|
||||
with open(args.output_file, "w") as f:
|
||||
json.dump(metrics, f, indent=2)
|
||||
print(f"Metrics saved to {args.output_file}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Benchmark serving for diffusion models.")
|
||||
parser.add_argument(
|
||||
"--base-url",
|
||||
type=str,
|
||||
default=None,
|
||||
help="Base URL of the server (e.g., http://localhost:8091). Overrides host/port.",
|
||||
)
|
||||
parser.add_argument("--host", type=str, default="localhost", help="Server host.")
|
||||
parser.add_argument("--port", type=int, default=8091, help="Server port.")
|
||||
parser.add_argument("--model", type=str, default="default", help="Model name.")
|
||||
parser.add_argument(
|
||||
"--backend",
|
||||
type=str,
|
||||
default="vllm-omni",
|
||||
choices=["vllm-omni", "openai"],
|
||||
help="Backend to target the benchmark to.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dataset",
|
||||
type=str,
|
||||
default="vbench",
|
||||
choices=["vbench", "trace", "random"],
|
||||
help="Dataset to use.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--task",
|
||||
type=str,
|
||||
default="t2v",
|
||||
choices=["t2v", "i2v", "ti2v", "ti2i", "i2i", "t2i"],
|
||||
help="Task type.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dataset-path",
|
||||
type=str,
|
||||
default=None,
|
||||
help="Path to local dataset file (optional).",
|
||||
)
|
||||
parser.add_argument("--num-prompts", type=int, default=10, help="Number of prompts to benchmark.")
|
||||
parser.add_argument(
|
||||
"--max-concurrency",
|
||||
type=int,
|
||||
default=1,
|
||||
help="Maximum number of concurrent requests, default to `1`. This can be used "
|
||||
"to help simulate an environment where a higher level component "
|
||||
"is enforcing a maximum number of concurrent requests. While the "
|
||||
"--request-rate argument controls the rate at which requests are "
|
||||
"initiated, this argument will control how many are actually allowed "
|
||||
"to execute at a time. This means that when used in combination, the "
|
||||
"actual request rate may be lower than specified with --request-rate, "
|
||||
"if the server is not processing requests fast enough to keep up.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--request-rate",
|
||||
type=float,
|
||||
default=float("inf"),
|
||||
help="Number of requests per second. If this is inf, then all the requests are sent at time 0. "
|
||||
"Otherwise, we use Poisson process to synthesize the request arrival times. Default is inf.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--warmup-requests",
|
||||
type=int,
|
||||
default=1,
|
||||
help="Number of warmup requests to run before measurement.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--warmup-num-inference-steps",
|
||||
type=int,
|
||||
default=1,
|
||||
help="num_inference_steps used for warmup requests.",
|
||||
)
|
||||
parser.add_argument("--width", type=int, default=None, help="Image/Video width.")
|
||||
parser.add_argument("--height", type=int, default=None, help="Image/Video height.")
|
||||
parser.add_argument("--num-frames", type=int, default=None, help="Number of frames (for video).")
|
||||
parser.add_argument(
|
||||
"--num-inference-steps",
|
||||
type=int,
|
||||
default=50,
|
||||
help="Number of inference steps (for diffusion models).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--seed",
|
||||
type=int,
|
||||
default=None,
|
||||
help="Random seed (for diffusion models).",
|
||||
)
|
||||
parser.add_argument("--fps", type=int, default=None, help="FPS (for video).")
|
||||
parser.add_argument("--output-file", type=str, default=None, help="Output JSON file for metrics.")
|
||||
parser.add_argument(
|
||||
"--slo",
|
||||
action="store_true",
|
||||
help=(
|
||||
"Enable SLO calculation and reporting. If trace provides per-request slo_ms, it is used. "
|
||||
"Otherwise, warmup request(s) are used to infer expected execution time assuming linear "
|
||||
"scaling by resolution, frames, and steps, then slo_ms = expected_time * --slo-scale."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--slo-scale",
|
||||
type=float,
|
||||
default=3.0,
|
||||
help="SLO target multiplier: slo_ms = estimated_exec_time_ms * slo_scale (default: 3).",
|
||||
)
|
||||
parser.add_argument("--disable-tqdm", action="store_true", help="Disable progress bar.")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
asyncio.run(benchmark(args))
|
||||
@@ -0,0 +1,397 @@
|
||||
# RDMA Test Configuration Guide
|
||||
|
||||
This document explains how to configure the RDMA environment and run tests for `MooncakeTransferEngineConnector`.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Docker Container Permissions](#docker-container-permissions)
|
||||
- [Single-Node Testing](#single-node-testing)
|
||||
- [Multi-Node Testing](#multi-node-testing)
|
||||
- [Running Tests](#running-tests)
|
||||
- [Cross-Node Testing](#cross-node-testing)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
|
||||
---
|
||||
|
||||
## Docker Container Permissions
|
||||
|
||||
RDMA tests require access to InfiniBand/RoCE devices and system topology. Add the following permissions when running `docker run`.
|
||||
|
||||
### Option 1: Minimal Permissions (Recommended)
|
||||
|
||||
```bash
|
||||
docker run -it \
|
||||
--cap-add=SYS_PTRACE \
|
||||
--cap-add=IPC_LOCK \
|
||||
--security-opt seccomp=unconfined \
|
||||
--network=host \
|
||||
--device=/dev/infiniband \
|
||||
-v /sys/class/infiniband:/sys/class/infiniband:ro \
|
||||
your-image:tag
|
||||
```
|
||||
|
||||
Parameter explanation:
|
||||
- `--cap-add=SYS_PTRACE`: Allow reading system topology information
|
||||
- `--cap-add=IPC_LOCK`: Allow memory locking (required for RDMA memory registration)
|
||||
- `--security-opt seccomp=unconfined`: Disable seccomp restrictions
|
||||
- `--network=host`: Use host network (required for RDMA)
|
||||
- `--device=/dev/infiniband`: Mount InfiniBand devices
|
||||
- `-v /sys/class/infiniband`: Mount IB device info (read-only)
|
||||
|
||||
### Option 2: Full Permissions (Quick but not recommended for production)
|
||||
|
||||
```bash
|
||||
docker run -it \
|
||||
--privileged \
|
||||
--network=host \
|
||||
your-image:tag
|
||||
```
|
||||
|
||||
`--privileged` grants full host permissions. Suitable for quick testing but not recommended for production.
|
||||
|
||||
---
|
||||
|
||||
## Single-Node Testing
|
||||
|
||||
When running single-node tests (producer and consumer on the same machine), ensure they use the **same RDMA device**.
|
||||
|
||||
### Problem Background
|
||||
|
||||
InfiniBand devices use LID (Local Identifier) for routing. Different devices have different LIDs and cannot communicate directly. If no device is specified, Mooncake may assign different devices to connectors, causing handshake failures.
|
||||
|
||||
Common error:
|
||||
```
|
||||
[Handshake] Failed to modify QP to RTR, check mtu, gid, peer lid, peer qp num: Invalid argument [22]
|
||||
```
|
||||
|
||||
### Solution
|
||||
|
||||
**Method 1: Set Environment Variable (Recommended)**
|
||||
|
||||
```bash
|
||||
# List available RDMA devices
|
||||
ibstat
|
||||
|
||||
# Select a device (e.g., mlx5_0)
|
||||
export RDMA_DEVICE_NAME='mlx5_0'
|
||||
|
||||
# Run tests
|
||||
pytest test_mooncake_transfer_engine_rdma.py -v -s
|
||||
```
|
||||
|
||||
**Method 2: Use RoCE Devices**
|
||||
|
||||
If the system has RoCE devices (using IPv4 routing), the test code will automatically detect and prefer them. RoCE device GIDs start with `00:00:00:00:00:00:00:00:00:00:ff:ff` (IPv4-mapped).
|
||||
|
||||
**Method 3: Ensure MTU Consistency**
|
||||
|
||||
Make sure both endpoints use the same MTU:
|
||||
|
||||
```bash
|
||||
# Check device MTU
|
||||
ibstatus mlx5_0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Multi-Node Testing
|
||||
|
||||
For multi-node tests, producer and consumer run on different machines connected via InfiniBand switch.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
1. Both machines have Mooncake and RDMA drivers installed
|
||||
2. Both machines are in the same InfiniBand subnet
|
||||
3. Switch is properly configured
|
||||
|
||||
### Configuration
|
||||
|
||||
**Machine A (Producer):**
|
||||
|
||||
```bash
|
||||
# Set RDMA host IP (InfiniBand interface IP)
|
||||
export RDMA_TEST_HOST='10.0.0.1'
|
||||
|
||||
# Optional: Specify device
|
||||
export RDMA_DEVICE_NAME='mlx5_0'
|
||||
```
|
||||
|
||||
**Machine B (Consumer):**
|
||||
|
||||
```bash
|
||||
# Set RDMA host IP
|
||||
export RDMA_TEST_HOST='10.0.0.2'
|
||||
|
||||
# Optional: Specify device
|
||||
export RDMA_DEVICE_NAME='mlx5_0'
|
||||
```
|
||||
|
||||
### Verify Connectivity
|
||||
|
||||
```bash
|
||||
# Ping IB interface
|
||||
ping 10.0.0.2
|
||||
|
||||
# Test RDMA connectivity with ibping
|
||||
# On Machine B (server)
|
||||
ibping -S
|
||||
|
||||
# On Machine A (client)
|
||||
ibping -G <Machine_B_GID>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Running Tests
|
||||
|
||||
### Run All RDMA Tests (Single-Node, fast suite)
|
||||
|
||||
Slow tests (large payloads, stress, concurrency integrity) are marked `@pytest.mark.slow`. Use `-m "not slow"` to skip them in quick CI or local fast iteration.
|
||||
|
||||
```bash
|
||||
cd tests/distributed/omni_connectors
|
||||
|
||||
# Fast suite only (excludes slow/stress tests)
|
||||
pytest test_mooncake_transfer_engine_rdma.py test_mooncake_transfer_engine_buffer.py -v -s -m "not slow"
|
||||
```
|
||||
|
||||
### Run Including Slow Tests
|
||||
|
||||
```bash
|
||||
# Run ALL tests including slow/stress tests
|
||||
pytest test_mooncake_transfer_engine_rdma.py test_mooncake_transfer_engine_buffer.py -v -s
|
||||
|
||||
# Run ONLY the slow/stress tests
|
||||
pytest test_mooncake_transfer_engine_rdma.py test_mooncake_transfer_engine_buffer.py -v -s -m slow
|
||||
```
|
||||
|
||||
### Run Buffer Management Tests
|
||||
|
||||
```bash
|
||||
# Fast only
|
||||
pytest test_mooncake_transfer_engine_buffer.py -v -s -m "not slow"
|
||||
|
||||
# Including allocator invariant tests (double-free, overlap, merge)
|
||||
pytest test_mooncake_transfer_engine_buffer.py -v -s
|
||||
```
|
||||
|
||||
### Run Specific Test Classes
|
||||
|
||||
```bash
|
||||
# Basic connector tests
|
||||
pytest test_mooncake_transfer_engine_rdma.py::TestBasicConnector -v -s
|
||||
|
||||
# End-to-end RDMA transfer tests
|
||||
pytest test_mooncake_transfer_engine_rdma.py::TestEndToEnd -v -s
|
||||
|
||||
# Lifecycle & resource management tests
|
||||
pytest test_mooncake_transfer_engine_rdma.py::TestLifecycle -v -s
|
||||
|
||||
# GPU memory pool tests (requires CUDA)
|
||||
pytest test_mooncake_transfer_engine_rdma.py::TestGPUPool -v -s
|
||||
|
||||
# Stress / correctness tests (slow)
|
||||
pytest test_mooncake_transfer_engine_rdma.py::TestStressCorrectness -v -s
|
||||
```
|
||||
|
||||
### RDMA Environment Diagnostics
|
||||
|
||||
For quick diagnostics (device status, Mooncake availability, env vars, etc.),
|
||||
see the [Troubleshooting section](../../../docs/design/feature/omni_connectors/mooncake_transfer_engine_connector.md#troubleshooting)
|
||||
in the connector documentation.
|
||||
|
||||
---
|
||||
|
||||
## Cross-Node Testing
|
||||
|
||||
The `cross_node_mooncake_transfer_engine.py` script enables testing RDMA transfers between two separate physical machines. This script is **not** auto-discovered by `pytest` (it does not start with `test_`) — it must be run manually on each node.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
1. Both machines have Mooncake installed
|
||||
2. Both machines are connected via InfiniBand/RoCE switch
|
||||
3. Firewall allows ZMQ ports (default: 15500, 15501)
|
||||
4. Same RDMA device name on both nodes (if multiple devices exist)
|
||||
|
||||
### Running Cross-Node Tests
|
||||
|
||||
**On Machine A (Producer) — start first:**
|
||||
|
||||
```bash
|
||||
cd benchmarks/distributed/omni_connectors/
|
||||
|
||||
# Optional: specify device if multiple exist
|
||||
export RDMA_DEVICE_NAME='mlx5_0'
|
||||
|
||||
python cross_node_mooncake_transfer_engine.py \
|
||||
--role producer \
|
||||
--local-host <PRODUCER_IP> \
|
||||
--remote-host <CONSUMER_IP> \
|
||||
--tensor-size-mb 100 \
|
||||
--num-transfers 3
|
||||
```
|
||||
|
||||
**On Machine B (Consumer) — start after producer:**
|
||||
|
||||
```bash
|
||||
cd benchmarks/distributed/omni_connectors/
|
||||
|
||||
export RDMA_DEVICE_NAME='mlx5_0'
|
||||
|
||||
python cross_node_mooncake_transfer_engine.py \
|
||||
--role consumer \
|
||||
--local-host <CONSUMER_IP> \
|
||||
--remote-host <PRODUCER_IP> \
|
||||
--tensor-size-mb 100 \
|
||||
--num-transfers 3
|
||||
```
|
||||
|
||||
### Transfer Modes
|
||||
|
||||
| Mode | Description | Example |
|
||||
|------|-------------|---------|
|
||||
| `copy` | Normal path — tensor copied to RDMA pool (default) | `--mode copy` |
|
||||
| `zerocopy` | Zero-copy path — data created directly in RDMA pool | `--mode zerocopy` |
|
||||
| `gpu` | GPU transfer — RDMA pool on GPU, uses GPUDirect | `--mode gpu --gpu-id 0` |
|
||||
|
||||
### Benchmark Mode
|
||||
|
||||
Skip MD5 verification and measure pure RDMA throughput:
|
||||
|
||||
```bash
|
||||
# Producer
|
||||
python cross_node_mooncake_transfer_engine.py \
|
||||
--role producer \
|
||||
--local-host <PRODUCER_IP> \
|
||||
--remote-host <CONSUMER_IP> \
|
||||
--tensor-size-mb 1024 \
|
||||
--num-transfers 20 \
|
||||
--benchmark
|
||||
|
||||
# Consumer
|
||||
python cross_node_mooncake_transfer_engine.py \
|
||||
--role consumer \
|
||||
--local-host <CONSUMER_IP> \
|
||||
--remote-host <PRODUCER_IP> \
|
||||
--tensor-size-mb 1024 \
|
||||
--num-transfers 20 \
|
||||
--benchmark
|
||||
```
|
||||
|
||||
### Cross-Node Test Options
|
||||
|
||||
| Option | Description | Default |
|
||||
|--------|-------------|---------|
|
||||
| `--role` | `producer` or `consumer` | Required |
|
||||
| `--local-host` | Local RDMA IP address | Required |
|
||||
| `--remote-host` | Remote RDMA IP address | Required |
|
||||
| `--local-port` | Local ZMQ port for RDMA data | 15500 |
|
||||
| `--remote-port` | Remote ZMQ port for RDMA data | 15500 |
|
||||
| `--ctrl-port` | Control channel port | 15501 |
|
||||
| `--tensor-size-mb` | Tensor size in MB | 100 |
|
||||
| `--num-transfers` | Number of transfers | 3 |
|
||||
| `--mode` | `copy`, `zerocopy`, or `gpu` | `copy` |
|
||||
| `--gpu-id` | GPU ID for GPU mode | 0 |
|
||||
| `--benchmark` | Skip MD5, pure performance test | off |
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### 1. "Failed to modify QP to RTR" Error
|
||||
|
||||
**Cause**: QP handshake failed, usually due to device configuration mismatch.
|
||||
|
||||
**Solution**:
|
||||
```bash
|
||||
# Force using the same device
|
||||
export RDMA_DEVICE_NAME='mlx5_0'
|
||||
```
|
||||
|
||||
### 2. "Mooncake TransferEngine is not available"
|
||||
|
||||
**Cause**: Mooncake not installed or import failed.
|
||||
|
||||
**Solution**:
|
||||
```bash
|
||||
# Check Mooncake installation
|
||||
python -c "from mooncake.engine import TransferEngine; print('OK')"
|
||||
|
||||
# Reinstall if needed
|
||||
pip install mooncake-transfer-engine
|
||||
# Or using uv
|
||||
uv pip install mooncake-transfer-engine
|
||||
|
||||
```
|
||||
|
||||
### 3. "Permission denied" accessing /dev/infiniband
|
||||
|
||||
**Cause**: Container lacks IB device access permissions.
|
||||
|
||||
**Solution**:
|
||||
```bash
|
||||
docker run --device=/dev/infiniband --cap-add=IPC_LOCK ...
|
||||
```
|
||||
|
||||
### 4. Test Timeout
|
||||
|
||||
**Cause**: RDMA connection establishment failed or network latency.
|
||||
|
||||
**Solution**:
|
||||
```bash
|
||||
# Check network status
|
||||
ibstat
|
||||
ibstatus
|
||||
```
|
||||
|
||||
### 5. GPU Test Failed "CUDA is not available"
|
||||
|
||||
**Cause**: CUDA environment not configured or GPU unavailable.
|
||||
|
||||
**Solution**:
|
||||
```bash
|
||||
# Check CUDA
|
||||
python -c "import torch; print(torch.cuda.is_available())"
|
||||
|
||||
# Docker needs NVIDIA runtime
|
||||
docker run --gpus all ...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Environment Variables Reference
|
||||
|
||||
| Variable | Description | Example |
|
||||
|----------|-------------|---------|
|
||||
| `RDMA_DEVICE_NAME` | Specify RDMA device name | `mlx5_0` |
|
||||
| `RDMA_TEST_HOST` | Specify test host IP | `10.0.0.1` |
|
||||
| `MC_TE_METRIC` | Enable Mooncake metrics | `1` |
|
||||
| `MC_IB_PCI_RELAXED_ORDERING` | Enable PCIe relaxed ordering | `1` |
|
||||
|
||||
---
|
||||
|
||||
## Test Files Overview
|
||||
|
||||
| File | Description | Auto-discovered by pytest |
|
||||
|------|-------------|--------------------------|
|
||||
| `test_mooncake_transfer_engine_rdma.py` | Integration tests for MooncakeTransferEngineConnector (basic, E2E, lifecycle, GPU) | Yes |
|
||||
| `test_mooncake_transfer_engine_buffer.py` | Memory pool and buffer management unit tests | Yes |
|
||||
| `cross_node_mooncake_transfer_engine.py` | Cross-node (multi-machine) testing script — run manually | No (filename does not start with `test_`) |
|
||||
|
||||
### test_mooncake_transfer_engine_rdma.py — Test Classes
|
||||
|
||||
| Test Class | Memory Pool | Marker | Description |
|
||||
|------------|-------------|--------|-------------|
|
||||
| `TestBasicConnector` | CPU | — | Initialization, put tensor/bytes/object, cleanup, pool exhaustion |
|
||||
| `TestEndToEnd` | CPU | — | E2E RDMA transfer: tensor, bytes, object, zero-copy, large payload (100MB), mixed types, concurrency |
|
||||
| `TestLifecycle` | CPU | — | Close, context manager, double-close safety |
|
||||
| `TestGPUPool` | GPU | — | GPU pool init, put CPU/GPU tensor, GPU E2E transfer |
|
||||
| `TestStressCorrectness` | CPU | `slow` | Concurrent put+get with MD5 integrity, bidirectional concurrency, edge cases (1-element tensor, empty bytes), 500MB payload, rapid alloc/free cycles |
|
||||
|
||||
### test_mooncake_transfer_engine_buffer.py — Test Classes
|
||||
|
||||
| Test Class | Marker | Description |
|
||||
|------------|--------|-------------|
|
||||
| `TestBufferAllocator` | — | Basic alloc/free, alignment, exhaustion/recovery, thread safety |
|
||||
| `TestAllocatorInvariants` | `slow` | Double-free safety, overlap corruption detection, adjacent-block merging, fragmentation/defrag |
|
||||
| `TestManagedBuffer` | — | Tensor views, context manager |
|
||||
@@ -0,0 +1,644 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
"""
|
||||
Cross-Node RDMA Test Script (Automated Version)
|
||||
|
||||
This script enables testing RDMA transfers between two separate machines.
|
||||
Supports three transfer modes:
|
||||
- copy: Normal path - tensor copied to RDMA pool (default)
|
||||
- zerocopy: Zero-copy path - data created directly in RDMA pool
|
||||
- gpu: GPU transfer - RDMA pool on GPU, uses GPUDirect
|
||||
|
||||
Usage:
|
||||
# On Machine A (Producer) - start first:
|
||||
python cross_node_mooncake_transfer_engine.py --role producer --local-host hostname_A --remote-host hostname_B
|
||||
|
||||
# On Machine B (Consumer) - start after producer:
|
||||
python cross_node_mooncake_transfer_engine.py --role consumer --local-host hostname_B --remote-host hostname_A
|
||||
|
||||
# Zero-copy mode:
|
||||
python cross_node_mooncake_transfer_engine.py --role producer ... --mode zerocopy
|
||||
|
||||
# GPU mode (requires GPUDirect RDMA support):
|
||||
python cross_node_mooncake_transfer_engine.py --role producer ... --mode gpu --gpu-id 0
|
||||
|
||||
# Benchmark mode (skip random data generation and MD5 verification,
|
||||
# measures pure RDMA throughput):
|
||||
python cross_node_mooncake_transfer_engine.py --role producer ... --benchmark
|
||||
|
||||
Environment Variables:
|
||||
RDMA_DEVICE_NAME: Specify RDMA device (e.g., mlx5_0)
|
||||
MC_IB_PCI_RELAXED_ORDERING: Set to 1 to enable PCIe relaxed ordering
|
||||
for higher RDMA throughput
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
import msgspec
|
||||
import torch
|
||||
import zmq
|
||||
|
||||
# Add parent path for imports
|
||||
sys.path.insert(
|
||||
0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))))
|
||||
)
|
||||
|
||||
from vllm_omni.distributed.omni_connectors.connectors.mooncake_transfer_engine_connector import (
|
||||
ManagedBuffer,
|
||||
MooncakeTransferEngineConnector,
|
||||
TransferEngine,
|
||||
)
|
||||
|
||||
|
||||
def compute_md5(tensor: torch.Tensor) -> str:
|
||||
"""Compute MD5 checksum of a tensor."""
|
||||
if tensor.is_cuda:
|
||||
tensor = tensor.cpu()
|
||||
data = tensor.contiguous().view(torch.uint8).numpy().tobytes()
|
||||
return hashlib.md5(data).hexdigest()
|
||||
|
||||
|
||||
# Control channel message types
|
||||
class CtrlMsg(msgspec.Struct):
|
||||
"""Control channel message."""
|
||||
|
||||
msg_type: str # "READY", "TRANSFER", "ACK", "DONE", "ERROR"
|
||||
request_id: str = ""
|
||||
md5: str = ""
|
||||
data_size: int = 0
|
||||
error: str = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class TransferConfig:
|
||||
"""Configuration for cross-node transfer test."""
|
||||
|
||||
local_host: str
|
||||
remote_host: str
|
||||
local_port: int
|
||||
remote_port: int
|
||||
ctrl_port: int
|
||||
num_transfers: int
|
||||
tensor_size_mb: int
|
||||
mode: str # "copy", "zerocopy", "gpu"
|
||||
gpu_id: int = 0
|
||||
benchmark: bool = False # Skip MD5 verification for pure performance test
|
||||
|
||||
|
||||
@dataclass
|
||||
class TransferStats:
|
||||
"""Statistics for transfer operations."""
|
||||
|
||||
success_count: int = 0
|
||||
fail_count: int = 0
|
||||
total_bytes: int = 0
|
||||
elapsed_time: float = 0.0
|
||||
|
||||
@property
|
||||
def throughput_mbps(self) -> float:
|
||||
if self.elapsed_time > 0:
|
||||
return (self.total_bytes / (1024 * 1024)) / self.elapsed_time
|
||||
return 0.0
|
||||
|
||||
def print_summary(self, role: str):
|
||||
print(f"\n{'=' * 60}")
|
||||
print(f" {role.upper()} SUMMARY")
|
||||
print(f" Successful: {self.success_count}/{self.success_count + self.fail_count}")
|
||||
print(f" Failed: {self.fail_count}/{self.success_count + self.fail_count}")
|
||||
print(f" Total: {self.total_bytes / (1024 * 1024):.2f} MB")
|
||||
print(f" Time: {self.elapsed_time:.2f} s")
|
||||
print(f" Throughput: {self.throughput_mbps:.2f} MB/s")
|
||||
print(f"{'=' * 60}")
|
||||
|
||||
|
||||
class CrossNodeTester(ABC):
|
||||
"""Abstract base class for cross-node RDMA testing."""
|
||||
|
||||
def __init__(self, config: TransferConfig):
|
||||
self.config = config
|
||||
self.connector: MooncakeTransferEngineConnector | None = None
|
||||
self.zmq_ctx: zmq.Context | None = None
|
||||
self.ctrl_socket: zmq.Socket | None = None
|
||||
self.stats = TransferStats()
|
||||
|
||||
def get_connector_config(self) -> dict:
|
||||
"""Get connector configuration based on mode."""
|
||||
pool_size = int(self.config.tensor_size_mb * 1.5) * 1024 * 1024
|
||||
pool_size = max(pool_size, 128 * 1024 * 1024)
|
||||
|
||||
conn_config = {
|
||||
"host": self.config.local_host,
|
||||
"zmq_port": self.config.local_port,
|
||||
"protocol": "rdma",
|
||||
"memory_pool_size": pool_size,
|
||||
}
|
||||
|
||||
# Set device based on mode
|
||||
if self.config.mode == "gpu":
|
||||
conn_config["memory_pool_device"] = f"cuda:{self.config.gpu_id}"
|
||||
else:
|
||||
conn_config["memory_pool_device"] = "cpu"
|
||||
|
||||
# RDMA device name from environment
|
||||
device_name = os.environ.get("RDMA_DEVICE_NAME")
|
||||
if device_name:
|
||||
conn_config["device_name"] = device_name
|
||||
print(f"[CONFIG] Using RDMA device: {device_name}")
|
||||
|
||||
return conn_config
|
||||
|
||||
def initialize(self):
|
||||
"""Initialize connector and ZMQ context."""
|
||||
print(f"[{self.role}] Initializing connector...")
|
||||
conn_config = self.get_connector_config()
|
||||
self.connector = MooncakeTransferEngineConnector(conn_config)
|
||||
self.zmq_ctx = zmq.Context()
|
||||
print(f"[{self.role}] Ready at {self.config.local_host}:{self.config.local_port}")
|
||||
|
||||
def cleanup(self):
|
||||
"""Cleanup resources."""
|
||||
if self.ctrl_socket:
|
||||
self.ctrl_socket.close()
|
||||
if self.zmq_ctx:
|
||||
self.zmq_ctx.term()
|
||||
if self.connector:
|
||||
self.connector.close()
|
||||
print(f"[{self.role}] Closed.")
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def role(self) -> str:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def run(self):
|
||||
pass
|
||||
|
||||
|
||||
class Producer(CrossNodeTester):
|
||||
"""Producer node - sends data to consumer."""
|
||||
|
||||
@property
|
||||
def role(self) -> str:
|
||||
return "PRODUCER"
|
||||
|
||||
def print_header(self):
|
||||
print(f"\n{'=' * 60}")
|
||||
print(f" PRODUCER MODE ({self.config.mode.upper()})")
|
||||
print(f" Local: {self.config.local_host}:{self.config.local_port}")
|
||||
print(f" Remote: {self.config.remote_host}:{self.config.remote_port}")
|
||||
print(f" Control Port: {self.config.ctrl_port}")
|
||||
print(f" Transfer Mode: {self.config.mode}")
|
||||
if self.config.mode == "gpu":
|
||||
print(f" GPU ID: {self.config.gpu_id}")
|
||||
print(f"{'=' * 60}\n")
|
||||
|
||||
def setup_control_channel(self):
|
||||
"""Setup ZMQ control channel as server (REP socket)."""
|
||||
self.ctrl_socket = self.zmq_ctx.socket(zmq.REP)
|
||||
self.ctrl_socket.bind(f"tcp://*:{self.config.ctrl_port}")
|
||||
print(f"[PRODUCER] Control channel listening on port {self.config.ctrl_port}")
|
||||
print("[PRODUCER] Waiting for consumer to connect...")
|
||||
|
||||
def wait_for_consumer(self) -> bool:
|
||||
"""Wait for consumer READY signal."""
|
||||
msg_data = self.ctrl_socket.recv()
|
||||
msg = msgspec.msgpack.decode(msg_data, type=CtrlMsg)
|
||||
if msg.msg_type != "READY":
|
||||
print(f"[PRODUCER] Unexpected message: {msg.msg_type}")
|
||||
return False
|
||||
print("[PRODUCER] Consumer connected!")
|
||||
self.ctrl_socket.send(msgspec.msgpack.encode(CtrlMsg(msg_type="ACK")))
|
||||
return True
|
||||
|
||||
def create_test_data(self, transfer_idx: int) -> tuple[Any, str, int]:
|
||||
"""
|
||||
Create test data based on transfer mode.
|
||||
|
||||
Returns:
|
||||
(data, md5, size) tuple
|
||||
"""
|
||||
num_elements = (self.config.tensor_size_mb * 1024 * 1024) // 4
|
||||
data_size = num_elements * 4
|
||||
|
||||
if self.config.mode == "zerocopy":
|
||||
# Zero-Copy Path: Allocate directly from connector's pool
|
||||
offset = self.connector.allocator.alloc(data_size)
|
||||
managed_buf = ManagedBuffer(self.connector.allocator, offset, data_size, self.connector.pool)
|
||||
|
||||
if self.config.benchmark:
|
||||
# In benchmark mode, skip random data generation (use uninitialized memory)
|
||||
return managed_buf, "", data_size
|
||||
else:
|
||||
# Fill buffer with random data using tensor view
|
||||
tensor_view = managed_buf.as_tensor(dtype=torch.float32, shape=(num_elements,))
|
||||
random_data = torch.randn(num_elements, dtype=torch.float32)
|
||||
if tensor_view.is_cuda:
|
||||
tensor_view.copy_(random_data.to(tensor_view.device))
|
||||
else:
|
||||
tensor_view.copy_(random_data)
|
||||
md5 = compute_md5(tensor_view)
|
||||
return managed_buf, md5, data_size
|
||||
|
||||
elif self.config.mode == "gpu":
|
||||
# GPU Path: Create tensor on GPU
|
||||
device = f"cuda:{self.config.gpu_id}"
|
||||
if self.config.benchmark:
|
||||
# In benchmark mode, use empty tensor (no random generation)
|
||||
gpu_tensor = torch.empty(num_elements, dtype=torch.float32, device=device)
|
||||
return gpu_tensor, "", data_size
|
||||
else:
|
||||
cpu_tensor = torch.randn(num_elements, dtype=torch.float32)
|
||||
md5 = compute_md5(cpu_tensor)
|
||||
gpu_tensor = cpu_tensor.to(device)
|
||||
return gpu_tensor, md5, data_size
|
||||
|
||||
else:
|
||||
# Copy Path (default): Create regular CPU tensor
|
||||
if self.config.benchmark:
|
||||
# In benchmark mode, use empty tensor (no random generation)
|
||||
tensor = torch.empty(num_elements, dtype=torch.float32)
|
||||
return tensor, "", data_size
|
||||
else:
|
||||
tensor = torch.randn(num_elements, dtype=torch.float32)
|
||||
md5 = compute_md5(tensor)
|
||||
return tensor, md5, data_size
|
||||
|
||||
def do_transfer(self, transfer_idx: int) -> bool:
|
||||
"""Perform a single transfer."""
|
||||
req_id = f"cross_node_transfer_{transfer_idx}"
|
||||
|
||||
if not self.config.benchmark:
|
||||
print(f"\n[PRODUCER] Transfer {transfer_idx + 1}/{self.config.num_transfers}")
|
||||
|
||||
# Create test data
|
||||
t0 = time.time()
|
||||
data, md5, data_size = self.create_test_data(transfer_idx)
|
||||
t_create = time.time() - t0
|
||||
|
||||
if not self.config.benchmark:
|
||||
print(f" Mode: {self.config.mode}")
|
||||
print(f" Size: {self.config.tensor_size_mb} MB")
|
||||
if md5:
|
||||
print(f" MD5: {md5[:16]}...")
|
||||
print(f" Create time: {t_create * 1000:.1f} ms")
|
||||
|
||||
# Put data
|
||||
t1 = time.time()
|
||||
success, size, metadata = self.connector.put("producer", "consumer", req_id, data)
|
||||
t_put = time.time() - t1
|
||||
|
||||
if not success:
|
||||
print(" [FAIL] Put failed")
|
||||
return False
|
||||
|
||||
if not self.config.benchmark:
|
||||
print(f" [OK] Put successful, {size} bytes ({t_put * 1000:.1f} ms)")
|
||||
|
||||
# Wait for consumer to request transfer info
|
||||
msg_data = self.ctrl_socket.recv()
|
||||
msg = msgspec.msgpack.decode(msg_data, type=CtrlMsg)
|
||||
|
||||
if msg.msg_type != "READY":
|
||||
print(f" [ERROR] Unexpected message: {msg.msg_type}")
|
||||
return False
|
||||
|
||||
# Send transfer metadata to consumer
|
||||
transfer_msg = CtrlMsg(
|
||||
msg_type="TRANSFER",
|
||||
request_id=req_id,
|
||||
md5=md5,
|
||||
data_size=data_size,
|
||||
)
|
||||
self.ctrl_socket.send(msgspec.msgpack.encode(transfer_msg))
|
||||
|
||||
# Wait for consumer ACK (this includes RDMA transfer time)
|
||||
t2 = time.time()
|
||||
msg_data = self.ctrl_socket.recv()
|
||||
t_rdma = time.time() - t2
|
||||
msg = msgspec.msgpack.decode(msg_data, type=CtrlMsg)
|
||||
|
||||
success = msg.msg_type == "ACK"
|
||||
if success:
|
||||
if not self.config.benchmark:
|
||||
print(f" [OK] RDMA transfer complete ({t_rdma * 1000:.1f} ms)")
|
||||
self.stats.success_count += 1
|
||||
self.stats.total_bytes += size
|
||||
else:
|
||||
print(f" [WARN] Consumer reported error: {msg.error}")
|
||||
self.stats.fail_count += 1
|
||||
|
||||
# Send ACK to allow consumer to continue
|
||||
self.ctrl_socket.send(msgspec.msgpack.encode(CtrlMsg(msg_type="ACK")))
|
||||
|
||||
# Cleanup buffer
|
||||
self.connector.cleanup(req_id)
|
||||
|
||||
return success
|
||||
|
||||
def run(self):
|
||||
"""Run the producer."""
|
||||
self.print_header()
|
||||
self.initialize()
|
||||
self.setup_control_channel()
|
||||
|
||||
try:
|
||||
if not self.wait_for_consumer():
|
||||
return
|
||||
|
||||
if self.config.benchmark:
|
||||
print(
|
||||
f"[BENCHMARK] Running {self.config.num_transfers} "
|
||||
f"transfers of {self.config.tensor_size_mb} MB each..."
|
||||
)
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
for i in range(self.config.num_transfers):
|
||||
self.do_transfer(i)
|
||||
if self.config.benchmark and (i + 1) % 10 == 0:
|
||||
elapsed = time.time() - start_time
|
||||
current_throughput = (self.stats.total_bytes / (1024 * 1024)) / elapsed
|
||||
print(f" Progress: {i + 1}/{self.config.num_transfers}, Throughput: {current_throughput:.2f} MB/s")
|
||||
|
||||
self.stats.elapsed_time = time.time() - start_time
|
||||
self.stats.print_summary("PRODUCER")
|
||||
|
||||
# Wait for final consumer message and send DONE
|
||||
self.ctrl_socket.recv()
|
||||
self.ctrl_socket.send(msgspec.msgpack.encode(CtrlMsg(msg_type="DONE")))
|
||||
|
||||
finally:
|
||||
self.cleanup()
|
||||
|
||||
|
||||
class Consumer(CrossNodeTester):
|
||||
"""Consumer node - receives data from producer."""
|
||||
|
||||
@property
|
||||
def role(self) -> str:
|
||||
return "CONSUMER"
|
||||
|
||||
def print_header(self):
|
||||
print(f"\n{'=' * 60}")
|
||||
print(f" CONSUMER MODE ({self.config.mode.upper()})")
|
||||
print(f" Local: {self.config.local_host}:{self.config.local_port}")
|
||||
print(f" Remote: {self.config.remote_host}:{self.config.remote_port}")
|
||||
print(f" Control Port: {self.config.ctrl_port}")
|
||||
print(f" Transfer Mode: {self.config.mode}")
|
||||
if self.config.mode == "gpu":
|
||||
print(f" GPU ID: {self.config.gpu_id}")
|
||||
print(f"{'=' * 60}\n")
|
||||
|
||||
def setup_control_channel(self):
|
||||
"""Setup ZMQ control channel as client (REQ socket)."""
|
||||
self.ctrl_socket = self.zmq_ctx.socket(zmq.REQ)
|
||||
ctrl_addr = f"tcp://{self.config.remote_host}:{self.config.ctrl_port}"
|
||||
print(f"[CONSUMER] Connecting to producer control channel at {ctrl_addr}...")
|
||||
self.ctrl_socket.connect(ctrl_addr)
|
||||
|
||||
def connect_to_producer(self) -> bool:
|
||||
"""Connect to producer and send READY signal."""
|
||||
self.ctrl_socket.send(msgspec.msgpack.encode(CtrlMsg(msg_type="READY")))
|
||||
msg_data = self.ctrl_socket.recv()
|
||||
msg = msgspec.msgpack.decode(msg_data, type=CtrlMsg)
|
||||
if msg.msg_type != "ACK":
|
||||
print(f"[CONSUMER] Unexpected response: {msg.msg_type}")
|
||||
return False
|
||||
print("[CONSUMER] Connected to producer! Starting transfers...")
|
||||
return True
|
||||
|
||||
def do_transfer(self, transfer_idx: int) -> bool:
|
||||
"""Perform a single transfer."""
|
||||
if not self.config.benchmark:
|
||||
print(f"\n[CONSUMER] Transfer {transfer_idx + 1}/{self.config.num_transfers}")
|
||||
|
||||
# Request next transfer info
|
||||
self.ctrl_socket.send(msgspec.msgpack.encode(CtrlMsg(msg_type="READY")))
|
||||
msg_data = self.ctrl_socket.recv()
|
||||
msg = msgspec.msgpack.decode(msg_data, type=CtrlMsg)
|
||||
|
||||
if msg.msg_type == "DONE":
|
||||
print("[CONSUMER] Producer signaled completion")
|
||||
return False
|
||||
|
||||
if msg.msg_type != "TRANSFER":
|
||||
print(f"[CONSUMER] Unexpected message: {msg.msg_type}")
|
||||
return False
|
||||
|
||||
req_id = msg.request_id
|
||||
expected_md5 = msg.md5
|
||||
data_size = msg.data_size
|
||||
num_elements = data_size // 4
|
||||
|
||||
if not self.config.benchmark:
|
||||
print(f" Request ID: {req_id}")
|
||||
if expected_md5:
|
||||
print(f" Expected MD5: {expected_md5[:16]}...")
|
||||
print(f" Data Size: {data_size / (1024 * 1024):.2f} MB")
|
||||
|
||||
# Build metadata for get
|
||||
metadata = {
|
||||
"request_id": req_id,
|
||||
"source_host": self.config.remote_host,
|
||||
"source_port": self.config.remote_port,
|
||||
"data_size": data_size,
|
||||
"dtype": "float32",
|
||||
"shape": [num_elements],
|
||||
"is_fast_path": True,
|
||||
}
|
||||
|
||||
if not self.config.benchmark:
|
||||
print(f" [INFO] Requesting from {self.config.remote_host}:{self.config.remote_port}")
|
||||
|
||||
# Get data with timing
|
||||
t0 = time.time()
|
||||
result = self.connector.get("producer", "consumer", req_id, metadata)
|
||||
t_get = time.time() - t0
|
||||
|
||||
response_msg = CtrlMsg(msg_type="ERROR", error="Get failed")
|
||||
|
||||
if result is not None:
|
||||
recv_buffer, recv_size = result
|
||||
if not self.config.benchmark:
|
||||
print(f" [OK] Get successful, {recv_size} bytes ({t_get * 1000:.1f} ms)")
|
||||
|
||||
if isinstance(recv_buffer, ManagedBuffer):
|
||||
# In benchmark mode, skip MD5 verification
|
||||
if self.config.benchmark or not expected_md5:
|
||||
response_msg = CtrlMsg(msg_type="ACK")
|
||||
self.stats.success_count += 1
|
||||
self.stats.total_bytes += recv_size
|
||||
else:
|
||||
# Verify data
|
||||
t1 = time.time()
|
||||
reconstructed = recv_buffer.as_tensor(dtype=torch.float32, shape=(num_elements,))
|
||||
recv_md5 = compute_md5(reconstructed)
|
||||
t_md5 = time.time() - t1
|
||||
print(f" MD5: {recv_md5[:16]}... ({t_md5 * 1000:.1f} ms)")
|
||||
|
||||
if recv_md5 == expected_md5:
|
||||
print(" [PASS] MD5 checksum verified!")
|
||||
response_msg = CtrlMsg(msg_type="ACK")
|
||||
self.stats.success_count += 1
|
||||
self.stats.total_bytes += recv_size
|
||||
else:
|
||||
print(" [FAIL] MD5 mismatch!")
|
||||
response_msg = CtrlMsg(msg_type="ERROR", error="MD5 mismatch")
|
||||
self.stats.fail_count += 1
|
||||
|
||||
recv_buffer.release()
|
||||
else:
|
||||
response_msg = CtrlMsg(msg_type="ACK")
|
||||
self.stats.success_count += 1
|
||||
self.stats.total_bytes += recv_size
|
||||
else:
|
||||
print(" [FAIL] Get failed")
|
||||
self.stats.fail_count += 1
|
||||
|
||||
# Send response to producer
|
||||
self.ctrl_socket.send(msgspec.msgpack.encode(response_msg))
|
||||
# Wait for ACK
|
||||
self.ctrl_socket.recv()
|
||||
|
||||
return response_msg.msg_type == "ACK"
|
||||
|
||||
def run(self):
|
||||
"""Run the consumer."""
|
||||
self.print_header()
|
||||
self.initialize()
|
||||
self.setup_control_channel()
|
||||
|
||||
try:
|
||||
if not self.connect_to_producer():
|
||||
return
|
||||
|
||||
if self.config.benchmark:
|
||||
print(
|
||||
f"[BENCHMARK] Running {self.config.num_transfers} "
|
||||
f"transfers of {self.config.tensor_size_mb} MB each..."
|
||||
)
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
for i in range(self.config.num_transfers):
|
||||
if not self.do_transfer(i):
|
||||
break
|
||||
if self.config.benchmark and (i + 1) % 10 == 0:
|
||||
elapsed = time.time() - start_time
|
||||
current_throughput = (self.stats.total_bytes / (1024 * 1024)) / elapsed
|
||||
print(f" Progress: {i + 1}/{self.config.num_transfers}, Throughput: {current_throughput:.2f} MB/s")
|
||||
|
||||
self.stats.elapsed_time = time.time() - start_time
|
||||
self.stats.print_summary("CONSUMER")
|
||||
|
||||
# Send final READY and wait for DONE
|
||||
self.ctrl_socket.send(msgspec.msgpack.encode(CtrlMsg(msg_type="READY")))
|
||||
self.ctrl_socket.recv()
|
||||
|
||||
finally:
|
||||
self.cleanup()
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Cross-Node RDMA Test (Automated)",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Transfer Modes:
|
||||
copy - Normal path: tensor copied to RDMA pool (default)
|
||||
zerocopy - Zero-copy path: data created directly in RDMA pool
|
||||
gpu - GPU transfer: RDMA pool on GPU, uses GPUDirect
|
||||
|
||||
Examples:
|
||||
# Copy mode (default):
|
||||
python cross_node_mooncake_transfer_engine.py --role producer \
|
||||
--local-host hostA --remote-host hostB
|
||||
|
||||
# Zero-copy mode:
|
||||
python cross_node_mooncake_transfer_engine.py --role producer \
|
||||
--local-host hostA --remote-host hostB --mode zerocopy
|
||||
|
||||
# GPU mode:
|
||||
python cross_node_mooncake_transfer_engine.py --role producer \
|
||||
--local-host hostA --remote-host hostB --mode gpu --gpu-id 0
|
||||
|
||||
# Benchmark mode (skip MD5, measure pure RDMA performance):
|
||||
python cross_node_mooncake_transfer_engine.py --role producer \
|
||||
--local-host hostA --remote-host hostB --benchmark
|
||||
|
||||
# With specific RDMA device:
|
||||
RDMA_DEVICE_NAME=mlx5_0 python cross_node_mooncake_transfer_engine.py --role producer ...
|
||||
""",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--role", required=True, choices=["producer", "consumer"], help="Role: producer (sends) or consumer (receives)"
|
||||
)
|
||||
parser.add_argument("--local-host", required=True, help="Local hostname or IP address")
|
||||
parser.add_argument("--remote-host", required=True, help="Remote hostname or IP address")
|
||||
parser.add_argument("--local-port", type=int, default=15500, help="Local ZMQ port for RDMA data (default: 15500)")
|
||||
parser.add_argument("--remote-port", type=int, default=15500, help="Remote ZMQ port for RDMA data (default: 15500)")
|
||||
parser.add_argument("--ctrl-port", type=int, default=15501, help="Control channel port (default: 15501)")
|
||||
parser.add_argument("--num-transfers", type=int, default=20, help="Number of transfers to perform (default: 3)")
|
||||
parser.add_argument("--tensor-size-mb", type=int, default=100, help="Tensor size in MB (default: 100)")
|
||||
parser.add_argument(
|
||||
"--mode",
|
||||
choices=["copy", "zerocopy", "gpu"],
|
||||
default="copy",
|
||||
help="Transfer mode: copy, zerocopy, or gpu (default: copy)",
|
||||
)
|
||||
parser.add_argument("--gpu-id", type=int, default=0, help="GPU ID for GPU mode (default: 0)")
|
||||
parser.add_argument(
|
||||
"--benchmark", action="store_true", help="Benchmark mode: skip MD5 verification for pure performance test"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Check Mooncake
|
||||
if TransferEngine is None:
|
||||
print("[ERROR] Mooncake TransferEngine is not available.")
|
||||
print("Install with: pip install mooncake")
|
||||
sys.exit(1)
|
||||
|
||||
# Check CUDA for GPU mode
|
||||
if args.mode == "gpu":
|
||||
if not torch.cuda.is_available():
|
||||
print("[ERROR] CUDA is not available but GPU mode was requested.")
|
||||
sys.exit(1)
|
||||
if args.gpu_id >= torch.cuda.device_count():
|
||||
print(f"[ERROR] GPU {args.gpu_id} not available. Found {torch.cuda.device_count()} GPUs.")
|
||||
sys.exit(1)
|
||||
print(f"[INFO] Using GPU {args.gpu_id}: {torch.cuda.get_device_name(args.gpu_id)}")
|
||||
|
||||
config = TransferConfig(
|
||||
local_host=args.local_host,
|
||||
remote_host=args.remote_host,
|
||||
local_port=args.local_port,
|
||||
remote_port=args.remote_port,
|
||||
ctrl_port=args.ctrl_port,
|
||||
num_transfers=args.num_transfers,
|
||||
tensor_size_mb=args.tensor_size_mb,
|
||||
mode=args.mode,
|
||||
gpu_id=args.gpu_id,
|
||||
benchmark=args.benchmark,
|
||||
)
|
||||
|
||||
if args.role == "producer":
|
||||
tester = Producer(config)
|
||||
else:
|
||||
tester = Consumer(config)
|
||||
|
||||
tester.run()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,86 @@
|
||||
# Benchmarks Guide
|
||||
|
||||
This README explains how to (1) prepare benchmark datasets and (2) run the provided Qwen3-Omni benchmarks.
|
||||
|
||||
## 1) Prepare the dataset (SeedTTS top100)
|
||||
|
||||
```bash
|
||||
cd benchmarks/build_dataset
|
||||
pip install gdown
|
||||
|
||||
# Download SeedTTS test set from Google Drive
|
||||
gdown --id 1GlSjVfSHkW3-leKKBlfrjuuTGqQ_xaLP
|
||||
|
||||
# Extract
|
||||
tar -xf seedtts_testset.tar
|
||||
|
||||
# Copy metadata and extract top-100 prompts
|
||||
cp seedtts_testset/en/meta.lst meta.lst
|
||||
python extract_prompts.py -i meta.lst -o top100.txt -n 100
|
||||
|
||||
# (Optional) clean up to save space
|
||||
rm -rf seedtts_testset seedtts_testset.tar meta.lst
|
||||
```
|
||||
|
||||
Artifacts:
|
||||
- `benchmarks/build_dataset/top100.txt` — 100 text prompts (one per line).
|
||||
|
||||
## 2) Run benchmarks
|
||||
|
||||
All commands assume repo root (`vllm-omni`).
|
||||
|
||||
### A. Transformers benchmark (offline, HF Transformers)
|
||||
|
||||
```
|
||||
bash benchmarks/qwen3-omni/transformers/eval_qwen3_moe_omni_transformers.sh
|
||||
```
|
||||
|
||||
What it does:
|
||||
- Runs `qwen3_omni_moe_transformers.py` over `top100.txt` with `--num-prompts 100`.
|
||||
- Outputs to `benchmarks/qwen3-omni/transformers/benchmark_results/`:
|
||||
- `perf_stats.json` — aggregated & per-prompt TPS/latency (thinker/talker/code2wav/overall).
|
||||
- `results.json` — per-prompt outputs and audio paths.
|
||||
- `audio/` — ~100 generated `.wav` files.
|
||||
|
||||
Key checks:
|
||||
- `overall_tps` and `*_tps_avg` should be non-zero and reasonably stable.
|
||||
- Investigate any 0/NaN or unusually low TPS / long-tail latency.
|
||||
|
||||
### B. vLLM Omni end-to-end benchmark (pipeline)
|
||||
|
||||
```
|
||||
bash benchmarks/qwen3-omni/vllm_omni/eval_qwen3_moe_omni.sh
|
||||
```
|
||||
|
||||
What it does:
|
||||
- Runs `examples/offline_inference/qwen3_omni/end2end.py` with `--log-stats`.
|
||||
- Uses `benchmarks/build_dataset/top100.txt` and writes to:
|
||||
- Logs: `benchmarks/qwen3-omni/vllm_omni/logs/`
|
||||
- `omni_llm_pipeline_text.orchestrator.stats.jsonl` — per-stage latency stats.
|
||||
- `omni_llm_pipeline_text.overall.stats.jsonl` — end-to-end latency/TPS.
|
||||
- `omni_llm_pipeline_text.stage{0,1,2}.log` — per-stage detailed logs/errors.
|
||||
- Outputs: `benchmarks/qwen3-omni/vllm_omni/outputs/` — ~100 text and `.wav` files.
|
||||
|
||||
Key checks:
|
||||
- Overall stats: end-to-end latency/TPS should be reasonable.
|
||||
- Orchestrator stats: per-stage latency should be stable; investigate long tails.
|
||||
- Stage logs: ensure no errors and no unusually slow stages.
|
||||
|
||||
|
||||
## Performance snapshot
|
||||
|
||||
The chart below summarizes our measured Qwen3-Omni MoE end-to-end benchmark, comparing vLLM-Omni against HF Transformers. It shows the overall throughput advantage for vLLM-Omni. These are actual experiment results—please refer to this performance when evaluating or reproducing the benchmark.
|
||||
|
||||

|
||||
|
||||
## Directory layout
|
||||
- `benchmarks/build_dataset/` — dataset prep utilities (e.g., SeedTTS top100).
|
||||
- `benchmarks/<model>/vllm_omni/` — vLLM-Omni pipeline benchmarks, logs, outputs.
|
||||
- Add new tasks under `benchmarks/<model>/...` with the same pattern: `transformers/`, `vllm_omni/`, task-specific README, and (optionally) dataset prep notes.
|
||||
- `benchmarks/<model>/vllm-omni-vs-hf.png` — current performance snapshot (overall throughput comparison).
|
||||
- `benchmarks/<model>/transformers/` — HF Transformers benchmarks (offline reference).
|
||||
|
||||
## Troubleshooting
|
||||
- Make sure GPU/driver/FlashAttention2 requirements are met for the chosen model.
|
||||
- If downloads fail, confirm network access to Google Drive (`gdown`) and Hugging Face.
|
||||
- If audio files are missing, check for errors in stage logs or model generation.***
|
||||
@@ -0,0 +1,26 @@
|
||||
#!/bin/bash
|
||||
# Qwen3-Omni Transformers Benchmark Evaluation Script
|
||||
# This script must be run from the vllm-omni root directory
|
||||
|
||||
# Get the directory where this script is located
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
# Navigate to vllm-omni root directory (4 levels up from script location)
|
||||
VLLM_OMNI_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
||||
cd "$VLLM_OMNI_ROOT" || { echo "Error: Failed to navigate to vllm-omni directory"; exit 1; }
|
||||
|
||||
echo "Working directory: $(pwd)"
|
||||
# Verify we're in the correct directory and run benchmark
|
||||
if [[ ! -f "benchmarks/qwen3-omni/transformers/qwen3_omni_moe_transformers.py" ]]; then
|
||||
echo "Error: Not in vllm-omni root directory. Please run from vllm-omni folder."
|
||||
else
|
||||
cd benchmarks/qwen3-omni/transformers
|
||||
|
||||
python qwen3_omni_moe_transformers.py --prompts-file ../../build_dataset/top100.txt --num-prompts 100
|
||||
|
||||
echo "Logs and outputs are saved to $(pwd)/benchmark_results:"
|
||||
echo " - perf_stats.json Aggregated/per-prompt TPS and latency (thinker/talker/code2wav/overall)"
|
||||
echo " - results.json Per-prompt outputs and audio paths"
|
||||
echo " - audio/ Generated wav files, there should be 100 wav file generated"
|
||||
echo "Key checks: overall_tps and *_tps_avg should be non-zero and stable; investigate 0/NaN or unusually low TPS/long-tail latency."
|
||||
fi
|
||||
@@ -0,0 +1,265 @@
|
||||
import time
|
||||
|
||||
import torch
|
||||
from transformers import Qwen3OmniMoeForConditionalGeneration
|
||||
|
||||
|
||||
class Qwen3OmniMoeForConditionalGenerationWithLogging(Qwen3OmniMoeForConditionalGeneration):
|
||||
@torch.no_grad()
|
||||
def generate(
|
||||
self,
|
||||
input_ids: torch.Tensor | None = None,
|
||||
speaker: str = "Ethan",
|
||||
use_audio_in_video: bool = False,
|
||||
return_audio: bool | None = None,
|
||||
thinker_max_new_tokens: int = 1024,
|
||||
thinker_eos_token_id: int = 151645,
|
||||
talker_max_new_tokens: int = 4096,
|
||||
talker_do_sample: bool = True,
|
||||
talker_top_k: int = 50,
|
||||
talker_top_p: float = 1.0,
|
||||
talker_temperature: float = 0.9,
|
||||
talker_repetition_penalty: float = 1.05,
|
||||
**kwargs,
|
||||
):
|
||||
total_t0 = time.time()
|
||||
perf_stats = {
|
||||
"thinker_tokens": 0,
|
||||
"thinker_time_s": 0.0,
|
||||
"thinker_tps": 0.0,
|
||||
"talker_tokens": 0,
|
||||
"talker_time_s": 0.0,
|
||||
"talker_tps": 0.0,
|
||||
"code2wav_tokens": 0,
|
||||
"code2wav_time_s": 0.0,
|
||||
"code2wav_tps": 0.0,
|
||||
"total_tokens": 0,
|
||||
"total_time_s": 0.0,
|
||||
"total_tps": 0.0,
|
||||
}
|
||||
if return_audio and not self.has_talker:
|
||||
raise ValueError(
|
||||
"Cannot use talker when talker module not initialized. "
|
||||
"Use `enable_talker` method or set enable_talker in config "
|
||||
"to enable talker."
|
||||
)
|
||||
if return_audio is None:
|
||||
return_audio = self.has_talker
|
||||
|
||||
shared_kwargs = {"use_audio_in_video": use_audio_in_video}
|
||||
thinker_kwargs = {
|
||||
"max_new_tokens": thinker_max_new_tokens,
|
||||
"eos_token_id": thinker_eos_token_id,
|
||||
}
|
||||
|
||||
talker_kwargs = {}
|
||||
token2wav_kwargs = {}
|
||||
if return_audio:
|
||||
speaker_id = self.config.talker_config.speaker_id.get(speaker.lower())
|
||||
if speaker_id is None:
|
||||
raise NotImplementedError(f"Speaker {speaker} not implemented")
|
||||
if input_ids.shape[0] != 1:
|
||||
raise NotImplementedError("Qwen3-Omni currently does not support batched inference with audio output")
|
||||
talker_suppressed_tokens = [
|
||||
i
|
||||
for i in range(
|
||||
self.config.talker_config.text_config.vocab_size - 1024,
|
||||
self.config.talker_config.text_config.vocab_size,
|
||||
)
|
||||
if i != self.config.talker_config.codec_eos_token_id
|
||||
] # Suppress additional special tokens, should not be predicted
|
||||
talker_kwargs = {
|
||||
"max_new_tokens": talker_max_new_tokens,
|
||||
"do_sample": talker_do_sample,
|
||||
"top_k": talker_top_k,
|
||||
"top_p": talker_top_p,
|
||||
"temperature": talker_temperature,
|
||||
"eos_token_id": self.config.talker_config.codec_eos_token_id,
|
||||
"repetition_penalty": talker_repetition_penalty,
|
||||
"suppress_tokens": talker_suppressed_tokens,
|
||||
"output_hidden_states": True,
|
||||
"return_dict_in_generate": True,
|
||||
}
|
||||
token2wav_kwargs = {}
|
||||
|
||||
for key, value in kwargs.items():
|
||||
if key.startswith("thinker_"):
|
||||
thinker_kwargs[key[len("thinker_") :]] = value
|
||||
elif key.startswith("talker_"):
|
||||
talker_kwargs[key[len("talker_") :]] = value
|
||||
elif key.startswith("token2wav_"):
|
||||
token2wav_kwargs[key[len("token2wav_") :]] = value
|
||||
# Process special input values
|
||||
elif key == "feature_attention_mask":
|
||||
thinker_kwargs[key] = value
|
||||
talker_kwargs["audio_feature_lengths"] = torch.sum(value, dim=1)
|
||||
elif key in ("input_features", "attention_mask"):
|
||||
thinker_kwargs[key] = value
|
||||
# Put other key to shared kwargs
|
||||
else:
|
||||
shared_kwargs[key] = value
|
||||
|
||||
# Merge kwargs
|
||||
for key, value in shared_kwargs.items():
|
||||
if key not in thinker_kwargs:
|
||||
thinker_kwargs[key] = value
|
||||
if key not in talker_kwargs and key in ["image_grid_thw", "video_grid_thw", "video_second_per_grid"]:
|
||||
talker_kwargs[key] = value
|
||||
if key not in token2wav_kwargs:
|
||||
token2wav_kwargs[key] = value
|
||||
|
||||
# 1. Generate from thinker module
|
||||
generate_audio = return_audio and self.has_talker
|
||||
if generate_audio:
|
||||
thinker_kwargs["output_hidden_states"] = True
|
||||
thinker_kwargs["return_dict_in_generate"] = True
|
||||
|
||||
t0 = time.time()
|
||||
thinker_result = self.thinker.generate(input_ids=input_ids, **thinker_kwargs)
|
||||
t1 = time.time()
|
||||
perf_stats["thinker_time_s"] = max(0.0, t1 - t0)
|
||||
try:
|
||||
prompt_len = int(input_ids.shape[1]) if input_ids is not None else 0
|
||||
total_len = int(thinker_result.sequences.shape[-1])
|
||||
thinker_out_len = max(0, total_len - prompt_len)
|
||||
except Exception:
|
||||
thinker_out_len = 0
|
||||
perf_stats["thinker_tokens"] = thinker_out_len
|
||||
perf_stats["thinker_tps"] = (
|
||||
(thinker_out_len / perf_stats["thinker_time_s"]) if perf_stats["thinker_time_s"] > 0 else 0.0
|
||||
)
|
||||
|
||||
if not generate_audio:
|
||||
perf_stats["total_tokens"] = perf_stats["thinker_tokens"]
|
||||
perf_stats["total_time_s"] = time.time() - total_t0
|
||||
perf_stats["total_tps"] = (
|
||||
(perf_stats["total_tokens"] / perf_stats["total_time_s"]) if perf_stats["total_time_s"] > 0 else 0.0
|
||||
)
|
||||
# attach stats to self
|
||||
setattr(self, "_perf_stats_last", perf_stats)
|
||||
if not hasattr(self, "_perf_stats_history"):
|
||||
setattr(self, "_perf_stats_history", [])
|
||||
self._perf_stats_history.append(perf_stats)
|
||||
return thinker_result, None
|
||||
|
||||
# 2. Prepare talker input
|
||||
thinker_embed = torch.cat([hidden_states[0] for hidden_states in thinker_result.hidden_states], dim=1).to(
|
||||
self.talker.device
|
||||
) # [1 t d]
|
||||
thinker_hidden = torch.cat(
|
||||
[
|
||||
hidden_states[self.config.talker_config.accept_hidden_layer]
|
||||
for hidden_states in thinker_result.hidden_states
|
||||
],
|
||||
dim=1,
|
||||
).to(self.talker.device) # [1 t d]
|
||||
|
||||
im_start_indexes = torch.cat(
|
||||
(
|
||||
torch.nonzero(input_ids[0] == self.config.im_start_token_id).squeeze(),
|
||||
torch.tensor([thinker_result.sequences.shape[-1]], device=input_ids.device, dtype=input_ids.dtype),
|
||||
),
|
||||
dim=-1,
|
||||
).to(self.talker.device) # Shape [n_starts + 1]; Take batch 0 since batched inference is not supported here.
|
||||
multimodal_mask = (
|
||||
(thinker_result.sequences == self.config.thinker_config.audio_token_id) |
|
||||
(thinker_result.sequences == self.config.thinker_config.image_token_id) |
|
||||
(thinker_result.sequences == self.config.thinker_config.video_token_id)
|
||||
).to(self.talker.device) # [1 t] # fmt: skip
|
||||
|
||||
talker_special_tokens = torch.tensor(
|
||||
[[self.config.tts_bos_token_id, self.config.tts_eos_token_id, self.config.tts_pad_token_id]],
|
||||
device=self.thinker.device,
|
||||
dtype=input_ids.dtype,
|
||||
)
|
||||
tts_bos_embed, tts_eos_embed, tts_pad_embed = (
|
||||
self.talker.text_projection(self.thinker.get_input_embeddings()(talker_special_tokens))
|
||||
.to(self.talker.device)
|
||||
.chunk(3, dim=1)
|
||||
) # 3 * [1 1 d]
|
||||
|
||||
talker_input_embeds = [] # [1 t d]
|
||||
talker_input_ids = []
|
||||
# For every chatml parts
|
||||
for i in range(len(im_start_indexes) - 1):
|
||||
im_start_index = im_start_indexes[i]
|
||||
segment_end_index = im_start_indexes[i + 1]
|
||||
role_token = input_ids[0][im_start_index + 1]
|
||||
# Talker should ignore thinker system prompt
|
||||
if role_token == self.config.system_token_id:
|
||||
continue
|
||||
# Talker takes word embeddings for tokens and hidden state from `accept_hidden_layer` for multimodal inputs
|
||||
elif role_token == self.config.user_token_id:
|
||||
talker_user_part = self._get_talker_user_parts(
|
||||
im_start_index, segment_end_index, multimodal_mask, thinker_hidden, thinker_embed
|
||||
)
|
||||
talker_input_embeds.append(talker_user_part)
|
||||
talker_input_ids.append(thinker_result.sequences[:, im_start_index:segment_end_index])
|
||||
# Take assistant output (for now)
|
||||
elif role_token == self.config.assistant_token_id and i == len(im_start_indexes) - 2:
|
||||
talker_assistant_embeds, talker_assistant_ids, trailing_text_hidden = self._get_talker_assistant_parts(
|
||||
im_start_index,
|
||||
segment_end_index,
|
||||
speaker_id,
|
||||
thinker_embed,
|
||||
tts_pad_embed,
|
||||
tts_bos_embed,
|
||||
tts_eos_embed,
|
||||
)
|
||||
talker_input_embeds.append(talker_assistant_embeds)
|
||||
talker_input_ids.append(talker_assistant_ids)
|
||||
# History assistant output (ignore for now)
|
||||
elif role_token == self.config.assistant_token_id and i != len(im_start_indexes) - 2:
|
||||
continue
|
||||
else:
|
||||
raise AssertionError("Expect role id after <|im_start|> (assistant, user, system)")
|
||||
talker_input_embed = torch.cat([embed.to(self.talker.device) for embed in talker_input_embeds], dim=1)
|
||||
talker_input_id = torch.cat([embed.to(self.talker.device) for embed in talker_input_ids], dim=1)
|
||||
t2 = time.time()
|
||||
talker_result = self.talker.generate(
|
||||
inputs_embeds=talker_input_embed,
|
||||
trailing_text_hidden=trailing_text_hidden,
|
||||
tts_pad_embed=tts_pad_embed,
|
||||
talker_input_ids=talker_input_id, # Not use input_ids to prevent repetition penalty out of bound
|
||||
**talker_kwargs,
|
||||
)
|
||||
t3 = time.time()
|
||||
perf_stats["talker_time_s"] = max(0.0, t3 - t2)
|
||||
talker_codes = (
|
||||
torch.stack([hid[-1] for hid in talker_result.hidden_states if hid[-1] is not None], dim=1)
|
||||
.transpose(1, 2)
|
||||
.to(self.code2wav.device)
|
||||
)
|
||||
try:
|
||||
# codes shape: (B, num_quantizers, T). We log T as token length.
|
||||
perf_stats["talker_tokens"] = int(talker_codes.shape[-1])
|
||||
except Exception:
|
||||
perf_stats["talker_tokens"] = 0
|
||||
perf_stats["talker_tps"] = (
|
||||
(perf_stats["talker_tokens"] / perf_stats["talker_time_s"]) if perf_stats["talker_time_s"] > 0 else 0.0
|
||||
)
|
||||
t4 = time.time()
|
||||
talker_wavs = self.code2wav.chunked_decode(talker_codes, chunk_size=300, left_context_size=25).float()
|
||||
t5 = time.time()
|
||||
perf_stats["code2wav_time_s"] = max(0.0, t5 - t4)
|
||||
perf_stats["code2wav_tokens"] = perf_stats["talker_tokens"] # same T, not times 16
|
||||
perf_stats["code2wav_tps"] = (
|
||||
(perf_stats["code2wav_tokens"] / perf_stats["code2wav_time_s"])
|
||||
if perf_stats["code2wav_time_s"] > 0
|
||||
else 0.0
|
||||
)
|
||||
perf_stats["total_tokens"] = perf_stats["thinker_tokens"] + perf_stats["talker_tokens"]
|
||||
perf_stats["total_time_s"] = time.time() - total_t0
|
||||
perf_stats["total_tps"] = (
|
||||
(perf_stats["total_tokens"] / perf_stats["total_time_s"]) if perf_stats["total_time_s"] > 0 else 0.0
|
||||
)
|
||||
setattr(self, "_perf_stats_last", perf_stats)
|
||||
if not hasattr(self, "_perf_stats_history"):
|
||||
setattr(self, "_perf_stats_history", [])
|
||||
self._perf_stats_history.append(perf_stats)
|
||||
return thinker_result, talker_wavs.float()
|
||||
|
||||
|
||||
__all__ = [
|
||||
"Qwen3OmniMoeForConditionalGenerationWithLogging",
|
||||
]
|
||||
@@ -0,0 +1,275 @@
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
|
||||
import soundfile as sf
|
||||
from qwen3_omni_moe_model import Qwen3OmniMoeForConditionalGenerationWithLogging
|
||||
from qwen_omni_utils import process_mm_info
|
||||
from tqdm import tqdm
|
||||
from transformers import Qwen3OmniMoeProcessor
|
||||
|
||||
MODEL_PATH = "Qwen/Qwen3-Omni-30B-A3B-Instruct"
|
||||
# MODEL_PATH = "Qwen/Qwen3-Omni-30B-A3B-Thinking"
|
||||
|
||||
|
||||
def load_prompts(prompts_file: str) -> list[str]:
|
||||
"""Load prompts from a text file, one prompt per line."""
|
||||
prompts = []
|
||||
with open(prompts_file, encoding="utf-8") as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line:
|
||||
prompts.append(line)
|
||||
return prompts
|
||||
|
||||
|
||||
def run_benchmark(
|
||||
model,
|
||||
processor,
|
||||
prompts: list[str],
|
||||
output_dir: str = "benchmark_results",
|
||||
speaker: str = "Ethan",
|
||||
use_audio_in_video: bool = True,
|
||||
):
|
||||
"""
|
||||
Run benchmark on a list of prompts and collect performance stats.
|
||||
|
||||
Args:
|
||||
model: The Qwen3OmniMoe model
|
||||
processor: The Qwen3OmniMoe processor
|
||||
prompts: List of text prompts to process
|
||||
output_dir: Directory to save results
|
||||
speaker: Speaker voice for audio output
|
||||
use_audio_in_video: Whether to use audio in video
|
||||
|
||||
Returns:
|
||||
tuple: (aggregated_stats, results, audio_outputs)
|
||||
- aggregated_stats: dict with aggregated performance statistics
|
||||
- results: list of dicts with per-prompt results
|
||||
- audio_outputs: list of audio tensors/arrays (or None if no audio)
|
||||
"""
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
audio_dir = os.path.join(output_dir, "audio")
|
||||
os.makedirs(audio_dir, exist_ok=True)
|
||||
|
||||
all_stats = []
|
||||
results = []
|
||||
audio_outputs = []
|
||||
|
||||
for idx, prompt in enumerate(tqdm(prompts, desc="Processing prompts")):
|
||||
conversation = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [{"type": "text", "text": prompt}],
|
||||
},
|
||||
]
|
||||
|
||||
# Preparation for inference
|
||||
text = processor.apply_chat_template(conversation, add_generation_prompt=True, tokenize=False)
|
||||
audios, images, videos = process_mm_info(conversation, use_audio_in_video=use_audio_in_video)
|
||||
inputs = processor(
|
||||
text=text,
|
||||
audio=audios,
|
||||
images=images,
|
||||
videos=videos,
|
||||
return_tensors="pt",
|
||||
padding=True,
|
||||
use_audio_in_video=use_audio_in_video,
|
||||
)
|
||||
inputs = inputs.to(model.device).to(model.dtype)
|
||||
|
||||
# Inference: Generation of the output text and audio
|
||||
text_ids, audio = model.generate(
|
||||
**inputs, speaker=speaker, thinker_return_dict_in_generate=True, use_audio_in_video=use_audio_in_video
|
||||
)
|
||||
|
||||
# Decode output text
|
||||
output_text = processor.batch_decode(
|
||||
text_ids.sequences[:, inputs["input_ids"].shape[1] :],
|
||||
skip_special_tokens=True,
|
||||
clean_up_tokenization_spaces=False,
|
||||
)[0]
|
||||
|
||||
# Collect performance stats
|
||||
perf_stats = None
|
||||
if hasattr(model, "_perf_stats_last"):
|
||||
perf_stats = model._perf_stats_last.copy()
|
||||
perf_stats["prompt_idx"] = idx
|
||||
perf_stats["prompt"] = prompt
|
||||
all_stats.append(perf_stats)
|
||||
|
||||
# Save audio and collect audio output
|
||||
audio_path = None
|
||||
audio_data = None
|
||||
if audio is not None:
|
||||
audio_data = audio.reshape(-1).detach().cpu().numpy()
|
||||
audio_path = os.path.join(audio_dir, f"output_{idx:04d}.wav")
|
||||
sf.write(
|
||||
audio_path,
|
||||
audio_data,
|
||||
samplerate=24000,
|
||||
)
|
||||
audio_outputs.append(audio_data)
|
||||
else:
|
||||
audio_outputs.append(None)
|
||||
|
||||
# Save result
|
||||
result = {
|
||||
"idx": idx,
|
||||
"prompt": prompt,
|
||||
"output": output_text,
|
||||
"audio_path": audio_path,
|
||||
"perf_stats": perf_stats,
|
||||
}
|
||||
results.append(result)
|
||||
|
||||
# Aggregate statistics
|
||||
aggregated_stats = aggregate_stats(all_stats)
|
||||
|
||||
# Save all results
|
||||
results_path = os.path.join(output_dir, "results.json")
|
||||
with open(results_path, "w", encoding="utf-8") as f:
|
||||
json.dump(results, f, ensure_ascii=False, indent=2)
|
||||
|
||||
# Save aggregated stats
|
||||
stats_path = os.path.join(output_dir, "perf_stats.json")
|
||||
with open(stats_path, "w", encoding="utf-8") as f:
|
||||
json.dump({"aggregated": aggregated_stats, "per_prompt": all_stats}, f, ensure_ascii=False, indent=2)
|
||||
|
||||
# Count saved audio files
|
||||
num_audio_saved = sum(1 for a in audio_outputs if a is not None)
|
||||
print(f"\nSaved {num_audio_saved} audio files to {audio_dir}/")
|
||||
|
||||
return aggregated_stats, results, audio_outputs
|
||||
|
||||
|
||||
def aggregate_stats(all_stats: list[dict]) -> dict:
|
||||
"""Aggregate performance statistics from multiple runs."""
|
||||
if not all_stats:
|
||||
return {}
|
||||
|
||||
keys = [
|
||||
"thinker_tokens",
|
||||
"thinker_time_s",
|
||||
"thinker_tps",
|
||||
"talker_tokens",
|
||||
"talker_time_s",
|
||||
"talker_tps",
|
||||
"code2wav_tokens",
|
||||
"code2wav_time_s",
|
||||
"code2wav_tps",
|
||||
"total_tokens",
|
||||
"total_time_s",
|
||||
"total_tps",
|
||||
]
|
||||
|
||||
aggregated = {
|
||||
"num_samples": len(all_stats),
|
||||
}
|
||||
|
||||
for key in keys:
|
||||
values = [s.get(key, 0) for s in all_stats if key in s]
|
||||
if values:
|
||||
aggregated[f"{key}_sum"] = sum(values)
|
||||
aggregated[f"{key}_avg"] = sum(values) / len(values)
|
||||
aggregated[f"{key}_min"] = min(values)
|
||||
aggregated[f"{key}_max"] = max(values)
|
||||
|
||||
# Calculate overall throughput
|
||||
total_tokens = aggregated.get("total_tokens_sum", 0)
|
||||
total_time = aggregated.get("total_time_s_sum", 0)
|
||||
if total_time > 0:
|
||||
aggregated["overall_tps"] = total_tokens / total_time
|
||||
|
||||
return aggregated
|
||||
|
||||
|
||||
def print_stats(stats: dict):
|
||||
"""Print performance statistics in a formatted way."""
|
||||
print("\n" + "=" * 60)
|
||||
print("Performance Statistics Summary")
|
||||
print("=" * 60)
|
||||
|
||||
print(f"\nNumber of samples: {stats.get('num_samples', 0)}")
|
||||
|
||||
print("\n--- Thinker ---")
|
||||
print(f" Total tokens: {stats.get('thinker_tokens_sum', 0):.0f}")
|
||||
print(f" Total time: {stats.get('thinker_time_s_sum', 0):.2f}s")
|
||||
print(f" Avg TPS: {stats.get('thinker_tps_avg', 0):.2f}")
|
||||
print(f" Min TPS: {stats.get('thinker_tps_min', 0):.2f}")
|
||||
print(f" Max TPS: {stats.get('thinker_tps_max', 0):.2f}")
|
||||
|
||||
print("\n--- Talker ---")
|
||||
print(f" Total tokens: {stats.get('talker_tokens_sum', 0):.0f}")
|
||||
print(f" Total time: {stats.get('talker_time_s_sum', 0):.2f}s")
|
||||
print(f" Avg TPS: {stats.get('talker_tps_avg', 0):.2f}")
|
||||
print(f" Min TPS: {stats.get('talker_tps_min', 0):.2f}")
|
||||
print(f" Max TPS: {stats.get('talker_tps_max', 0):.2f}")
|
||||
|
||||
print("\n--- Code2Wav ---")
|
||||
print(f" Total tokens: {stats.get('code2wav_tokens_sum', 0):.0f}")
|
||||
print(f" Total time: {stats.get('code2wav_time_s_sum', 0):.2f}s")
|
||||
print(f" Avg TPS: {stats.get('code2wav_tps_avg', 0):.2f}")
|
||||
print(f" Min TPS: {stats.get('code2wav_tps_min', 0):.2f}")
|
||||
print(f" Max TPS: {stats.get('code2wav_tps_max', 0):.2f}")
|
||||
|
||||
print("\n--- Overall ---")
|
||||
print(f" Total tokens: {stats.get('total_tokens_sum', 0):.0f}")
|
||||
print(f" Total time: {stats.get('total_time_s_sum', 0):.2f}s")
|
||||
print(f" Overall TPS: {stats.get('overall_tps', 0):.2f}")
|
||||
print(f" Avg TPS: {stats.get('total_tps_avg', 0):.2f}")
|
||||
print(f" Min TPS: {stats.get('total_tps_min', 0):.2f}")
|
||||
print(f" Max TPS: {stats.get('total_tps_max', 0):.2f}")
|
||||
|
||||
print("=" * 60 + "\n")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Qwen3-Omni Benchmark Script")
|
||||
parser.add_argument(
|
||||
"--prompts-file",
|
||||
type=str,
|
||||
default="benchmark/build_dataset/top100.txt",
|
||||
help="Path to the prompts file (one prompt per line)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output-dir", type=str, default="benchmark_results", help="Directory to save benchmark results"
|
||||
)
|
||||
parser.add_argument("--model-path", type=str, default=MODEL_PATH, help="Path to the model")
|
||||
parser.add_argument("--speaker", type=str, default="Ethan", help="Speaker voice for audio output")
|
||||
parser.add_argument("--num-prompts", type=int, default=None, help="Number of prompts to process (default: all)")
|
||||
args = parser.parse_args()
|
||||
|
||||
# Load model and processor
|
||||
print(f"Loading model from {args.model_path}...")
|
||||
model = Qwen3OmniMoeForConditionalGenerationWithLogging.from_pretrained(
|
||||
args.model_path,
|
||||
dtype="auto",
|
||||
device_map="auto",
|
||||
attn_implementation="flash_attention_2",
|
||||
)
|
||||
processor = Qwen3OmniMoeProcessor.from_pretrained(args.model_path)
|
||||
|
||||
# Benchmark mode
|
||||
print(f"Loading prompts from {args.prompts_file}...")
|
||||
prompts = load_prompts(args.prompts_file)
|
||||
|
||||
if args.num_prompts:
|
||||
prompts = prompts[: args.num_prompts]
|
||||
|
||||
print(f"Running benchmark on {len(prompts)} prompts...")
|
||||
|
||||
aggregated_stats, results, audio_outputs = run_benchmark(
|
||||
model=model,
|
||||
processor=processor,
|
||||
prompts=prompts,
|
||||
output_dir=args.output_dir,
|
||||
speaker=args.speaker,
|
||||
)
|
||||
|
||||
print_stats(aggregated_stats)
|
||||
print(f"\nResults saved to {args.output_dir}/")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 115 KiB |
@@ -0,0 +1,37 @@
|
||||
#!/bin/bash
|
||||
# Qwen3-Omni Benchmark Evaluation Script
|
||||
# This script must be run from the vllm-omni root directory
|
||||
|
||||
# Get the directory where this script is located
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
# Navigate to vllm-omni root directory (4 levels up from script location)
|
||||
VLLM_OMNI_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
||||
cd "$VLLM_OMNI_ROOT" || { echo "Error: Failed to navigate to vllm-omni directory"; exit 1; }
|
||||
|
||||
echo "Working directory: $(pwd)"
|
||||
|
||||
# Verify we're in the correct directory and run benchmark
|
||||
if [[ ! -d "benchmarks/qwen3-omni/vllm_omni" ]]; then
|
||||
echo "Error: Not in vllm-omni root directory. Please run from vllm-omni folder."
|
||||
else
|
||||
log_dir=benchmarks/qwen3-omni/vllm_omni/logs
|
||||
outputs_dir=benchmarks/qwen3-omni/vllm_omni/outputs
|
||||
end2end_script_path=examples/offline_inference/qwen3_omni/end2end.py
|
||||
build_dataset_path=benchmarks/build_dataset/top100.txt
|
||||
|
||||
python $end2end_script_path --output-wav $outputs_dir \
|
||||
--query-type text \
|
||||
--txt-prompts $build_dataset_path \
|
||||
--log-stats \
|
||||
--log-dir $log_dir
|
||||
echo "Logs and outputs are saved in ${log_dir} and ${outputs_dir} respectively:"
|
||||
echo " - omni_llm_pipeline_text run dir/base name"
|
||||
echo " - omni_llm_pipeline_text.orchestrator.stats.jsonl orchestrator-stage latency stats"
|
||||
echo " - omni_llm_pipeline_text.overall.stats.jsonl overall latency/TPS stats"
|
||||
echo " - omni_llm_pipeline_text.stage0.log per-stage detailed logs"
|
||||
echo " - omni_llm_pipeline_text.stage1.log"
|
||||
echo " - omni_llm_pipeline_text.stage2.log"
|
||||
echo "Key checks: overall.stats.jsonl for end-to-end latency/TPS; orchestrator.stats.jsonl for stable per-stage latency; stage*.log for errors or long tails."
|
||||
echo " - outputs/ Generated txt and wav files, there should be 100 text and wav files generated respectively"
|
||||
fi
|
||||
+760
@@ -0,0 +1,760 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
# ruff: noqa
|
||||
# code borrowed from https://github.com/pytorch/pytorch/blob/main/torch/utils/collect_env.py
|
||||
|
||||
import datetime
|
||||
import locale
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
# Unlike the rest of the PyTorch this file must be python2 compliant.
|
||||
# This script outputs relevant system environment info
|
||||
# Run it with `python collect_env.py` or `python -m torch.utils.collect_env`
|
||||
from collections import namedtuple
|
||||
|
||||
import regex as re
|
||||
|
||||
from vllm.envs import environment_variables
|
||||
|
||||
try:
|
||||
import torch
|
||||
|
||||
TORCH_AVAILABLE = True
|
||||
except (ImportError, NameError, AttributeError, OSError):
|
||||
TORCH_AVAILABLE = False
|
||||
|
||||
# System Environment Information
|
||||
SystemEnv = namedtuple(
|
||||
"SystemEnv",
|
||||
[
|
||||
"torch_version",
|
||||
"is_debug_build",
|
||||
"cuda_compiled_version",
|
||||
"gcc_version",
|
||||
"clang_version",
|
||||
"cmake_version",
|
||||
"os",
|
||||
"libc_version",
|
||||
"python_version",
|
||||
"python_platform",
|
||||
"is_cuda_available",
|
||||
"cuda_runtime_version",
|
||||
"cuda_module_loading",
|
||||
"nvidia_driver_version",
|
||||
"nvidia_gpu_models",
|
||||
"cudnn_version",
|
||||
"pip_version", # 'pip' or 'pip3'
|
||||
"pip_packages",
|
||||
"conda_packages",
|
||||
"hip_compiled_version",
|
||||
"hip_runtime_version",
|
||||
"miopen_runtime_version",
|
||||
"caching_allocator_config",
|
||||
"is_xnnpack_available",
|
||||
"cpu_info",
|
||||
"rocm_version", # vllm specific field
|
||||
"vllm_version", # vllm specific field
|
||||
"vllm_omni_version", # vllm-omni specific field
|
||||
"vllm_build_flags", # vllm specific field
|
||||
"gpu_topo", # vllm specific field
|
||||
"env_vars",
|
||||
],
|
||||
)
|
||||
|
||||
DEFAULT_CONDA_PATTERNS = {
|
||||
"torch",
|
||||
"numpy",
|
||||
"cudatoolkit",
|
||||
"soumith",
|
||||
"mkl",
|
||||
"magma",
|
||||
"triton",
|
||||
"optree",
|
||||
"nccl",
|
||||
"transformers",
|
||||
"zmq",
|
||||
"nvidia",
|
||||
"pynvml",
|
||||
"flashinfer-python",
|
||||
}
|
||||
|
||||
DEFAULT_PIP_PATTERNS = {
|
||||
"torch",
|
||||
"numpy",
|
||||
"mypy",
|
||||
"flake8",
|
||||
"triton",
|
||||
"optree",
|
||||
"onnx",
|
||||
"nccl",
|
||||
"transformers",
|
||||
"zmq",
|
||||
"nvidia",
|
||||
"pynvml",
|
||||
"flashinfer-python",
|
||||
}
|
||||
|
||||
|
||||
def run(command):
|
||||
"""Return (return-code, stdout, stderr)."""
|
||||
shell = True if type(command) is str else False
|
||||
try:
|
||||
p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=shell)
|
||||
raw_output, raw_err = p.communicate()
|
||||
rc = p.returncode
|
||||
if get_platform() == "win32":
|
||||
enc = "oem"
|
||||
else:
|
||||
enc = locale.getpreferredencoding()
|
||||
output = raw_output.decode(enc)
|
||||
if command == "nvidia-smi topo -m":
|
||||
# don't remove the leading whitespace of `nvidia-smi topo -m`
|
||||
# because they are meaningful
|
||||
output = output.rstrip()
|
||||
else:
|
||||
output = output.strip()
|
||||
err = raw_err.decode(enc)
|
||||
return rc, output, err.strip()
|
||||
|
||||
except FileNotFoundError:
|
||||
cmd_str = command if isinstance(command, str) else command[0]
|
||||
return 127, "", f"Command not found: {cmd_str}"
|
||||
|
||||
|
||||
def run_and_read_all(run_lambda, command):
|
||||
"""Run command using run_lambda; reads and returns entire output if rc is 0."""
|
||||
rc, out, _ = run_lambda(command)
|
||||
if rc != 0:
|
||||
return None
|
||||
return out
|
||||
|
||||
|
||||
def run_and_parse_first_match(run_lambda, command, regex):
|
||||
"""Run command using run_lambda, returns the first regex match if it exists."""
|
||||
rc, out, _ = run_lambda(command)
|
||||
if rc != 0:
|
||||
return None
|
||||
match = re.search(regex, out)
|
||||
if match is None:
|
||||
return None
|
||||
return match.group(1)
|
||||
|
||||
|
||||
def run_and_return_first_line(run_lambda, command):
|
||||
"""Run command using run_lambda and returns first line if output is not empty."""
|
||||
rc, out, _ = run_lambda(command)
|
||||
if rc != 0:
|
||||
return None
|
||||
return out.split("\n")[0]
|
||||
|
||||
|
||||
def get_conda_packages(run_lambda, patterns=None):
|
||||
if patterns is None:
|
||||
patterns = DEFAULT_CONDA_PATTERNS
|
||||
conda = os.environ.get("CONDA_EXE", "conda")
|
||||
out = run_and_read_all(run_lambda, [conda, "list"])
|
||||
if out is None:
|
||||
return out
|
||||
|
||||
return "\n".join(
|
||||
line for line in out.splitlines() if not line.startswith("#") and any(name in line for name in patterns)
|
||||
)
|
||||
|
||||
|
||||
def get_gcc_version(run_lambda):
|
||||
return run_and_parse_first_match(run_lambda, "gcc --version", r"gcc (.*)")
|
||||
|
||||
|
||||
def get_clang_version(run_lambda):
|
||||
return run_and_parse_first_match(run_lambda, "clang --version", r"clang version (.*)")
|
||||
|
||||
|
||||
def get_cmake_version(run_lambda):
|
||||
return run_and_parse_first_match(run_lambda, "cmake --version", r"cmake (.*)")
|
||||
|
||||
|
||||
def get_nvidia_driver_version(run_lambda):
|
||||
if get_platform() == "darwin":
|
||||
cmd = "kextstat | grep -i cuda"
|
||||
return run_and_parse_first_match(run_lambda, cmd, r"com[.]nvidia[.]CUDA [(](.*?)[)]")
|
||||
smi = get_nvidia_smi()
|
||||
return run_and_parse_first_match(run_lambda, smi, r"Driver Version: (.*?) ")
|
||||
|
||||
|
||||
def get_gpu_info(run_lambda):
|
||||
if get_platform() == "darwin" or (
|
||||
TORCH_AVAILABLE and hasattr(torch.version, "hip") and torch.version.hip is not None
|
||||
):
|
||||
if TORCH_AVAILABLE and torch.cuda.is_available():
|
||||
if torch.version.hip is not None:
|
||||
prop = torch.cuda.get_device_properties(0)
|
||||
if hasattr(prop, "gcnArchName"):
|
||||
gcnArch = " ({})".format(prop.gcnArchName)
|
||||
else:
|
||||
gcnArch = "NoGCNArchNameOnOldPyTorch"
|
||||
else:
|
||||
gcnArch = ""
|
||||
return torch.cuda.get_device_name(None) + gcnArch
|
||||
return None
|
||||
smi = get_nvidia_smi()
|
||||
uuid_regex = re.compile(r" \(UUID: .+?\)")
|
||||
rc, out, _ = run_lambda(smi + " -L")
|
||||
if rc != 0:
|
||||
return None
|
||||
# Anonymize GPUs by removing their UUID
|
||||
return re.sub(uuid_regex, "", out)
|
||||
|
||||
|
||||
def get_running_cuda_version(run_lambda):
|
||||
return run_and_parse_first_match(run_lambda, "nvcc --version", r"release .+ V(.*)")
|
||||
|
||||
|
||||
def get_cudnn_version(run_lambda):
|
||||
"""Return a list of libcudnn.so; it's hard to tell which one is being used."""
|
||||
if get_platform() == "win32":
|
||||
system_root = os.environ.get("SYSTEMROOT", "C:\\Windows")
|
||||
cuda_path = os.environ.get("CUDA_PATH", "%CUDA_PATH%")
|
||||
where_cmd = os.path.join(system_root, "System32", "where")
|
||||
cudnn_cmd = '{} /R "{}\\bin" cudnn*.dll'.format(where_cmd, cuda_path)
|
||||
elif get_platform() == "darwin":
|
||||
# CUDA libraries and drivers can be found in /usr/local/cuda/. See
|
||||
# https://docs.nvidia.com/cuda/cuda-installation-guide-mac-os-x/index.html#install
|
||||
# https://docs.nvidia.com/deeplearning/sdk/cudnn-install/index.html#installmac
|
||||
# Use CUDNN_LIBRARY when cudnn library is installed elsewhere.
|
||||
cudnn_cmd = "ls /usr/local/cuda/lib/libcudnn*"
|
||||
else:
|
||||
cudnn_cmd = 'ldconfig -p | grep libcudnn | rev | cut -d" " -f1 | rev'
|
||||
rc, out, _ = run_lambda(cudnn_cmd)
|
||||
# find will return 1 if there are permission errors or if not found
|
||||
if len(out) == 0 or (rc != 1 and rc != 0):
|
||||
l = os.environ.get("CUDNN_LIBRARY")
|
||||
if l is not None and os.path.isfile(l):
|
||||
return os.path.realpath(l)
|
||||
return None
|
||||
files_set = set()
|
||||
for fn in out.split("\n"):
|
||||
fn = os.path.realpath(fn) # eliminate symbolic links
|
||||
if os.path.isfile(fn):
|
||||
files_set.add(fn)
|
||||
if not files_set:
|
||||
return None
|
||||
# Alphabetize the result because the order is non-deterministic otherwise
|
||||
files = sorted(files_set)
|
||||
if len(files) == 1:
|
||||
return files[0]
|
||||
result = "\n".join(files)
|
||||
return "Probably one of the following:\n{}".format(result)
|
||||
|
||||
|
||||
def get_nvidia_smi():
|
||||
# Note: nvidia-smi is currently available only on Windows and Linux
|
||||
smi = "nvidia-smi"
|
||||
if get_platform() == "win32":
|
||||
system_root = os.environ.get("SYSTEMROOT", "C:\\Windows")
|
||||
program_files_root = os.environ.get("PROGRAMFILES", "C:\\Program Files")
|
||||
legacy_path = os.path.join(program_files_root, "NVIDIA Corporation", "NVSMI", smi)
|
||||
new_path = os.path.join(system_root, "System32", smi)
|
||||
smis = [new_path, legacy_path]
|
||||
for candidate_smi in smis:
|
||||
if os.path.exists(candidate_smi):
|
||||
smi = '"{}"'.format(candidate_smi)
|
||||
break
|
||||
return smi
|
||||
|
||||
|
||||
def get_rocm_version(run_lambda):
|
||||
"""Returns the ROCm version if available, otherwise 'N/A'."""
|
||||
return run_and_parse_first_match(run_lambda, "hipcc --version", r"HIP version: (\S+)")
|
||||
|
||||
|
||||
def get_vllm_version():
|
||||
from vllm import __version__, __version_tuple__
|
||||
|
||||
if __version__ == "dev":
|
||||
return "N/A (dev)"
|
||||
version_str = __version_tuple__[-1]
|
||||
if isinstance(version_str, str) and version_str.startswith("g"):
|
||||
# it's a dev build
|
||||
if "." in version_str:
|
||||
# it's a dev build containing local changes
|
||||
git_sha = version_str.split(".")[0][1:]
|
||||
date = version_str.split(".")[-1][1:]
|
||||
return f"{__version__} (git sha: {git_sha}, date: {date})"
|
||||
else:
|
||||
# it's a dev build without local changes
|
||||
git_sha = version_str[1:] # type: ignore
|
||||
return f"{__version__} (git sha: {git_sha})"
|
||||
return __version__
|
||||
|
||||
|
||||
def get_vllm_omni_version(run_lambda):
|
||||
try:
|
||||
import vllm_omni
|
||||
from vllm_omni import __version__, __version_tuple__
|
||||
|
||||
version_str = __version_tuple__[-1]
|
||||
if isinstance(version_str, str) and version_str.startswith("g"):
|
||||
if "." in version_str:
|
||||
git_sha = version_str.split(".")[0][1:]
|
||||
date = version_str.split(".")[-1][1:]
|
||||
return f"{__version__} (git sha: {git_sha}, date: {date})"
|
||||
else:
|
||||
git_sha = version_str[1:]
|
||||
return f"{__version__} (git sha: {git_sha})"
|
||||
|
||||
package_dir = os.path.dirname(os.path.abspath(vllm_omni.__file__))
|
||||
git_sha = run_and_read_all(run_lambda, f"git -C {package_dir} rev-parse --short HEAD")
|
||||
if git_sha:
|
||||
return f"{__version__} (git sha: {git_sha})"
|
||||
|
||||
return __version__
|
||||
except ImportError:
|
||||
return "N/A (vllm_omni not installed)"
|
||||
|
||||
|
||||
def summarize_vllm_build_flags():
|
||||
# This could be a static method if the flags are constant, or dynamic if you need to check environment variables, etc.
|
||||
return "CUDA Archs: {}; ROCm: {}".format(
|
||||
os.environ.get("TORCH_CUDA_ARCH_LIST", "Not Set"),
|
||||
"Enabled" if os.environ.get("ROCM_HOME") else "Disabled",
|
||||
)
|
||||
|
||||
|
||||
def get_gpu_topo(run_lambda):
|
||||
output = None
|
||||
|
||||
if get_platform() == "linux":
|
||||
output = run_and_read_all(run_lambda, "nvidia-smi topo -m")
|
||||
if output is None:
|
||||
output = run_and_read_all(run_lambda, "rocm-smi --showtopo")
|
||||
|
||||
return output
|
||||
|
||||
|
||||
def get_cpu_info(run_lambda):
|
||||
rc, out, err = 0, "", ""
|
||||
if get_platform() == "linux":
|
||||
rc, out, err = run_lambda("lscpu")
|
||||
elif get_platform() == "win32":
|
||||
rc, out, err = run_lambda(
|
||||
"wmic cpu get Name,Manufacturer,Family,Architecture,ProcessorType,DeviceID, \
|
||||
CurrentClockSpeed,MaxClockSpeed,L2CacheSize,L2CacheSpeed,Revision /VALUE"
|
||||
)
|
||||
elif get_platform() == "darwin":
|
||||
rc, out, err = run_lambda("sysctl -n machdep.cpu.brand_string")
|
||||
cpu_info = "None"
|
||||
if rc == 0:
|
||||
cpu_info = out
|
||||
else:
|
||||
cpu_info = err
|
||||
return cpu_info
|
||||
|
||||
|
||||
def get_platform():
|
||||
if sys.platform.startswith("linux"):
|
||||
return "linux"
|
||||
elif sys.platform.startswith("win32"):
|
||||
return "win32"
|
||||
elif sys.platform.startswith("cygwin"):
|
||||
return "cygwin"
|
||||
elif sys.platform.startswith("darwin"):
|
||||
return "darwin"
|
||||
else:
|
||||
return sys.platform
|
||||
|
||||
|
||||
def get_mac_version(run_lambda):
|
||||
return run_and_parse_first_match(run_lambda, "sw_vers -productVersion", r"(.*)")
|
||||
|
||||
|
||||
def get_windows_version(run_lambda):
|
||||
system_root = os.environ.get("SYSTEMROOT", "C:\\Windows")
|
||||
wmic_cmd = os.path.join(system_root, "System32", "Wbem", "wmic")
|
||||
findstr_cmd = os.path.join(system_root, "System32", "findstr")
|
||||
return run_and_read_all(run_lambda, "{} os get Caption | {} /v Caption".format(wmic_cmd, findstr_cmd))
|
||||
|
||||
|
||||
def get_lsb_version(run_lambda):
|
||||
return run_and_parse_first_match(run_lambda, "lsb_release -a", r"Description:\t(.*)")
|
||||
|
||||
|
||||
def check_release_file(run_lambda):
|
||||
return run_and_parse_first_match(run_lambda, "cat /etc/*-release", r'PRETTY_NAME="(.*)"')
|
||||
|
||||
|
||||
def get_os(run_lambda):
|
||||
from platform import machine
|
||||
|
||||
platform = get_platform()
|
||||
|
||||
if platform == "win32" or platform == "cygwin":
|
||||
return get_windows_version(run_lambda)
|
||||
|
||||
if platform == "darwin":
|
||||
version = get_mac_version(run_lambda)
|
||||
if version is None:
|
||||
return None
|
||||
return "macOS {} ({})".format(version, machine())
|
||||
|
||||
if platform == "linux":
|
||||
# Ubuntu/Debian based
|
||||
desc = get_lsb_version(run_lambda)
|
||||
if desc is not None:
|
||||
return "{} ({})".format(desc, machine())
|
||||
|
||||
# Try reading /etc/*-release
|
||||
desc = check_release_file(run_lambda)
|
||||
if desc is not None:
|
||||
return "{} ({})".format(desc, machine())
|
||||
|
||||
return "{} ({})".format(platform, machine())
|
||||
|
||||
# Unknown platform
|
||||
return platform
|
||||
|
||||
|
||||
def get_python_platform():
|
||||
import platform
|
||||
|
||||
return platform.platform()
|
||||
|
||||
|
||||
def get_libc_version():
|
||||
import platform
|
||||
|
||||
if get_platform() != "linux":
|
||||
return "N/A"
|
||||
return "-".join(platform.libc_ver())
|
||||
|
||||
|
||||
def is_uv_venv():
|
||||
if os.environ.get("UV"):
|
||||
return True
|
||||
pyvenv_cfg_path = os.path.join(sys.prefix, "pyvenv.cfg")
|
||||
if os.path.exists(pyvenv_cfg_path):
|
||||
with open(pyvenv_cfg_path, "r") as f:
|
||||
return any(line.startswith("uv = ") for line in f)
|
||||
return False
|
||||
|
||||
|
||||
def get_pip_packages(run_lambda, patterns=None):
|
||||
"""Return `pip list` output. Note: will also find conda-installed pytorch and numpy packages."""
|
||||
if patterns is None:
|
||||
patterns = DEFAULT_PIP_PATTERNS
|
||||
|
||||
def run_with_pip():
|
||||
try:
|
||||
import importlib.util
|
||||
|
||||
pip_spec = importlib.util.find_spec("pip")
|
||||
pip_available = pip_spec is not None
|
||||
except ImportError:
|
||||
pip_available = False
|
||||
|
||||
if pip_available:
|
||||
cmd = [sys.executable, "-mpip", "list", "--format=freeze"]
|
||||
elif is_uv_venv():
|
||||
print("uv is set")
|
||||
cmd = ["uv", "pip", "list", "--format=freeze"]
|
||||
else:
|
||||
raise RuntimeError("Could not collect pip list output (pip or uv module not available)")
|
||||
|
||||
out = run_and_read_all(run_lambda, cmd)
|
||||
return "\n".join(line for line in out.splitlines() if any(name in line for name in patterns))
|
||||
|
||||
pip_version = "pip3" if sys.version[0] == "3" else "pip"
|
||||
out = run_with_pip()
|
||||
return pip_version, out
|
||||
|
||||
|
||||
def get_cachingallocator_config():
|
||||
ca_config = os.environ.get("PYTORCH_CUDA_ALLOC_CONF", "")
|
||||
return ca_config
|
||||
|
||||
|
||||
def get_cuda_module_loading_config():
|
||||
if TORCH_AVAILABLE and torch.cuda.is_available():
|
||||
torch.cuda.init()
|
||||
config = os.environ.get("CUDA_MODULE_LOADING", "")
|
||||
return config
|
||||
else:
|
||||
return "N/A"
|
||||
|
||||
|
||||
def is_xnnpack_available():
|
||||
if TORCH_AVAILABLE:
|
||||
import torch.backends.xnnpack
|
||||
|
||||
return str(torch.backends.xnnpack.enabled) # type: ignore[attr-defined]
|
||||
else:
|
||||
return "N/A"
|
||||
|
||||
|
||||
def get_env_vars():
|
||||
env_vars = ""
|
||||
secret_terms = ("secret", "token", "api", "access", "password")
|
||||
report_prefix = (
|
||||
"TORCH",
|
||||
"NCCL",
|
||||
"PYTORCH",
|
||||
"CUDA",
|
||||
"CUBLAS",
|
||||
"CUDNN",
|
||||
"OMP_",
|
||||
"MKL_",
|
||||
"NVIDIA",
|
||||
)
|
||||
for k, v in os.environ.items():
|
||||
if any(term in k.lower() for term in secret_terms):
|
||||
continue
|
||||
if k in environment_variables:
|
||||
env_vars = env_vars + "{}={}".format(k, v) + "\n"
|
||||
if k.startswith(report_prefix):
|
||||
env_vars = env_vars + "{}={}".format(k, v) + "\n"
|
||||
|
||||
return env_vars
|
||||
|
||||
|
||||
def get_env_info():
|
||||
run_lambda = run
|
||||
pip_version, pip_list_output = get_pip_packages(run_lambda)
|
||||
|
||||
if TORCH_AVAILABLE:
|
||||
version_str = torch.__version__
|
||||
debug_mode_str = str(torch.version.debug)
|
||||
cuda_available_str = str(torch.cuda.is_available())
|
||||
cuda_version_str = torch.version.cuda
|
||||
if not hasattr(torch.version, "hip") or torch.version.hip is None: # cuda version
|
||||
hip_compiled_version = hip_runtime_version = miopen_runtime_version = "N/A"
|
||||
else: # HIP version
|
||||
|
||||
def get_version_or_na(cfg, prefix):
|
||||
_lst = [s.rsplit(None, 1)[-1] for s in cfg if prefix in s]
|
||||
return _lst[0] if _lst else "N/A"
|
||||
|
||||
cfg = torch._C._show_config().split("\n")
|
||||
hip_runtime_version = get_version_or_na(cfg, "HIP Runtime")
|
||||
miopen_runtime_version = get_version_or_na(cfg, "MIOpen")
|
||||
cuda_version_str = "N/A"
|
||||
hip_compiled_version = torch.version.hip
|
||||
else:
|
||||
version_str = debug_mode_str = cuda_available_str = cuda_version_str = "N/A"
|
||||
hip_compiled_version = hip_runtime_version = miopen_runtime_version = "N/A"
|
||||
|
||||
sys_version = sys.version.replace("\n", " ")
|
||||
|
||||
conda_packages = get_conda_packages(run_lambda)
|
||||
|
||||
rocm_version = get_rocm_version(run_lambda)
|
||||
vllm_version = get_vllm_version()
|
||||
vllm_omni_version = get_vllm_omni_version(run_lambda)
|
||||
vllm_build_flags = summarize_vllm_build_flags()
|
||||
gpu_topo = get_gpu_topo(run_lambda)
|
||||
|
||||
return SystemEnv(
|
||||
torch_version=version_str,
|
||||
is_debug_build=debug_mode_str,
|
||||
python_version="{} ({}-bit runtime)".format(sys_version, sys.maxsize.bit_length() + 1),
|
||||
python_platform=get_python_platform(),
|
||||
is_cuda_available=cuda_available_str,
|
||||
cuda_compiled_version=cuda_version_str,
|
||||
cuda_runtime_version=get_running_cuda_version(run_lambda),
|
||||
cuda_module_loading=get_cuda_module_loading_config(),
|
||||
nvidia_gpu_models=get_gpu_info(run_lambda),
|
||||
nvidia_driver_version=get_nvidia_driver_version(run_lambda),
|
||||
cudnn_version=get_cudnn_version(run_lambda),
|
||||
hip_compiled_version=hip_compiled_version,
|
||||
hip_runtime_version=hip_runtime_version,
|
||||
miopen_runtime_version=miopen_runtime_version,
|
||||
pip_version=pip_version,
|
||||
pip_packages=pip_list_output,
|
||||
conda_packages=conda_packages,
|
||||
os=get_os(run_lambda),
|
||||
libc_version=get_libc_version(),
|
||||
gcc_version=get_gcc_version(run_lambda),
|
||||
clang_version=get_clang_version(run_lambda),
|
||||
cmake_version=get_cmake_version(run_lambda),
|
||||
caching_allocator_config=get_cachingallocator_config(),
|
||||
is_xnnpack_available=is_xnnpack_available(),
|
||||
cpu_info=get_cpu_info(run_lambda),
|
||||
rocm_version=rocm_version,
|
||||
vllm_version=vllm_version,
|
||||
vllm_omni_version=vllm_omni_version,
|
||||
vllm_build_flags=vllm_build_flags,
|
||||
gpu_topo=gpu_topo,
|
||||
env_vars=get_env_vars(),
|
||||
)
|
||||
|
||||
|
||||
env_info_fmt = """
|
||||
==============================
|
||||
System Info
|
||||
==============================
|
||||
OS : {os}
|
||||
GCC version : {gcc_version}
|
||||
Clang version : {clang_version}
|
||||
CMake version : {cmake_version}
|
||||
Libc version : {libc_version}
|
||||
|
||||
==============================
|
||||
PyTorch Info
|
||||
==============================
|
||||
PyTorch version : {torch_version}
|
||||
Is debug build : {is_debug_build}
|
||||
CUDA used to build PyTorch : {cuda_compiled_version}
|
||||
ROCM used to build PyTorch : {hip_compiled_version}
|
||||
|
||||
==============================
|
||||
Python Environment
|
||||
==============================
|
||||
Python version : {python_version}
|
||||
Python platform : {python_platform}
|
||||
|
||||
==============================
|
||||
CUDA / GPU Info
|
||||
==============================
|
||||
Is CUDA available : {is_cuda_available}
|
||||
CUDA runtime version : {cuda_runtime_version}
|
||||
CUDA_MODULE_LOADING set to : {cuda_module_loading}
|
||||
GPU models and configuration : {nvidia_gpu_models}
|
||||
Nvidia driver version : {nvidia_driver_version}
|
||||
cuDNN version : {cudnn_version}
|
||||
HIP runtime version : {hip_runtime_version}
|
||||
MIOpen runtime version : {miopen_runtime_version}
|
||||
Is XNNPACK available : {is_xnnpack_available}
|
||||
|
||||
==============================
|
||||
CPU Info
|
||||
==============================
|
||||
{cpu_info}
|
||||
|
||||
==============================
|
||||
Versions of relevant libraries
|
||||
==============================
|
||||
{pip_packages}
|
||||
{conda_packages}
|
||||
""".strip()
|
||||
|
||||
# both the above code and the following code use `strip()` to
|
||||
# remove leading/trailing whitespaces, so we need to add a newline
|
||||
# in between to separate the two sections
|
||||
env_info_fmt += "\n\n"
|
||||
|
||||
env_info_fmt += """
|
||||
==============================
|
||||
vLLM Info
|
||||
==============================
|
||||
ROCM Version : {rocm_version}
|
||||
vLLM Version : {vllm_version}
|
||||
vLLM-Omni Version : {vllm_omni_version}
|
||||
vLLM Build Flags:
|
||||
{vllm_build_flags}
|
||||
GPU Topology:
|
||||
{gpu_topo}
|
||||
|
||||
==============================
|
||||
Environment Variables
|
||||
==============================
|
||||
{env_vars}
|
||||
""".strip()
|
||||
|
||||
|
||||
def pretty_str(envinfo):
|
||||
def replace_nones(dct, replacement="Could not collect"):
|
||||
for key in dct.keys():
|
||||
if dct[key] is not None:
|
||||
continue
|
||||
dct[key] = replacement
|
||||
return dct
|
||||
|
||||
def replace_bools(dct, true="Yes", false="No"):
|
||||
for key in dct.keys():
|
||||
if dct[key] is True:
|
||||
dct[key] = true
|
||||
elif dct[key] is False:
|
||||
dct[key] = false
|
||||
return dct
|
||||
|
||||
def prepend(text, tag="[prepend]"):
|
||||
lines = text.split("\n")
|
||||
updated_lines = [tag + line for line in lines]
|
||||
return "\n".join(updated_lines)
|
||||
|
||||
def replace_if_empty(text, replacement="No relevant packages"):
|
||||
if text is not None and len(text) == 0:
|
||||
return replacement
|
||||
return text
|
||||
|
||||
def maybe_start_on_next_line(string):
|
||||
# If `string` is multiline, prepend a \n to it.
|
||||
if string is not None and len(string.split("\n")) > 1:
|
||||
return "\n{}\n".format(string)
|
||||
return string
|
||||
|
||||
mutable_dict = envinfo._asdict()
|
||||
|
||||
# If nvidia_gpu_models is multiline, start on the next line
|
||||
mutable_dict["nvidia_gpu_models"] = maybe_start_on_next_line(envinfo.nvidia_gpu_models)
|
||||
|
||||
# If the machine doesn't have CUDA, report some fields as 'No CUDA'
|
||||
dynamic_cuda_fields = [
|
||||
"cuda_runtime_version",
|
||||
"nvidia_gpu_models",
|
||||
"nvidia_driver_version",
|
||||
]
|
||||
all_cuda_fields = dynamic_cuda_fields + ["cudnn_version"]
|
||||
all_dynamic_cuda_fields_missing = all(mutable_dict[field] is None for field in dynamic_cuda_fields)
|
||||
if TORCH_AVAILABLE and not torch.cuda.is_available() and all_dynamic_cuda_fields_missing:
|
||||
for field in all_cuda_fields:
|
||||
mutable_dict[field] = "No CUDA"
|
||||
if envinfo.cuda_compiled_version is None:
|
||||
mutable_dict["cuda_compiled_version"] = "None"
|
||||
|
||||
# Replace True with Yes, False with No
|
||||
mutable_dict = replace_bools(mutable_dict)
|
||||
|
||||
# Replace all None objects with 'Could not collect'
|
||||
mutable_dict = replace_nones(mutable_dict)
|
||||
|
||||
# If either of these are '', replace with 'No relevant packages'
|
||||
mutable_dict["pip_packages"] = replace_if_empty(mutable_dict["pip_packages"])
|
||||
mutable_dict["conda_packages"] = replace_if_empty(mutable_dict["conda_packages"])
|
||||
|
||||
# Tag conda and pip packages with a prefix
|
||||
# If they were previously None, they'll show up as ie '[conda] Could not collect'
|
||||
if mutable_dict["pip_packages"]:
|
||||
mutable_dict["pip_packages"] = prepend(mutable_dict["pip_packages"], "[{}] ".format(envinfo.pip_version))
|
||||
if mutable_dict["conda_packages"]:
|
||||
mutable_dict["conda_packages"] = prepend(mutable_dict["conda_packages"], "[conda] ")
|
||||
mutable_dict["cpu_info"] = envinfo.cpu_info
|
||||
return env_info_fmt.format(**mutable_dict)
|
||||
|
||||
|
||||
def get_pretty_env_info():
|
||||
return pretty_str(get_env_info())
|
||||
|
||||
|
||||
def main():
|
||||
print("Collecting environment information...")
|
||||
output = get_pretty_env_info()
|
||||
print(output)
|
||||
|
||||
if TORCH_AVAILABLE and hasattr(torch, "utils") and hasattr(torch.utils, "_crash_handler"):
|
||||
minidump_dir = torch.utils._crash_handler.DEFAULT_MINIDUMP_DIR
|
||||
if sys.platform == "linux" and os.path.exists(minidump_dir):
|
||||
dumps = [os.path.join(minidump_dir, dump) for dump in os.listdir(minidump_dir)]
|
||||
latest = max(dumps, key=os.path.getctime)
|
||||
ctime = os.path.getctime(latest)
|
||||
creation_time = datetime.datetime.fromtimestamp(ctime).strftime("%Y-%m-%d %H:%M:%S")
|
||||
msg = (
|
||||
"\n*** Detected a minidump at {} created on {}, ".format(latest, creation_time)
|
||||
+ "if this is related to your bug please include it when you file a report ***"
|
||||
)
|
||||
print(msg, file=sys.stderr)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,21 @@
|
||||
ARG VLLM_BASE_IMAGE=vllm/vllm-openai
|
||||
ARG VLLM_BASE_TAG=v0.16.0
|
||||
FROM ${VLLM_BASE_IMAGE}:${VLLM_BASE_TAG}
|
||||
ARG APP_DIR=/workspace/vllm-omni
|
||||
WORKDIR ${APP_DIR}
|
||||
|
||||
COPY . .
|
||||
|
||||
# Install system dependencies
|
||||
RUN apt-get update && \
|
||||
apt-get install -y ffmpeg sox libsox-fmt-all jq && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install vllm-omni into the same uv-managed Python environment used by the base image.
|
||||
# Use bash -c so that $(python3 -c ...) is expanded inside the container.
|
||||
RUN uv pip install --system --no-cache-dir ".[dev]"
|
||||
|
||||
RUN ln -sf /usr/bin/python3 /usr/bin/python
|
||||
|
||||
ENTRYPOINT []
|
||||
@@ -0,0 +1,16 @@
|
||||
ARG VLLM_ASCEND_IMAGE=quay.io/ascend/vllm-ascend
|
||||
ARG VLLM_ASCEND_TAG=v0.14.0rc1
|
||||
FROM ${VLLM_ASCEND_IMAGE}:${VLLM_ASCEND_TAG}
|
||||
|
||||
ARG APP_DIR=/vllm-workspace/vllm-omni
|
||||
WORKDIR ${APP_DIR}
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN sed -i -E 's/^([[:space:]]*)"fa3-fwd==0\.0\.1",/\1# "fa3-fwd==0.0.1",/' pyproject.toml \
|
||||
&& sed -i -E 's/\bonnxruntime\b/onnxruntime-cann/g' pyproject.toml
|
||||
RUN pip install -v -e .
|
||||
|
||||
ENV VLLM_WORKER_MULTIPROC_METHOD=spawn
|
||||
|
||||
ENTRYPOINT []
|
||||
@@ -0,0 +1,16 @@
|
||||
ARG VLLM_ASCEND_IMAGE=quay.io/ascend/vllm-ascend
|
||||
ARG VLLM_ASCEND_TAG=v0.14.0rc1-a3
|
||||
FROM ${VLLM_ASCEND_IMAGE}:${VLLM_ASCEND_TAG}
|
||||
|
||||
ARG APP_DIR=/vllm-workspace/vllm-omni
|
||||
WORKDIR ${APP_DIR}
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN sed -i -E 's/^([[:space:]]*)"fa3-fwd==0\.0\.1",/\1# "fa3-fwd==0.0.1",/' pyproject.toml \
|
||||
&& sed -i -E 's/\bonnxruntime\b/onnxruntime-cann/g' pyproject.toml
|
||||
RUN pip install -v -e .
|
||||
|
||||
ENV VLLM_WORKER_MULTIPROC_METHOD=spawn
|
||||
|
||||
ENTRYPOINT []
|
||||
@@ -0,0 +1,53 @@
|
||||
ARG BASE_IMAGE=vllm/vllm-openai-rocm:v0.16.0
|
||||
FROM ${BASE_IMAGE} AS base
|
||||
|
||||
# Declare a variable to know if we want to use the nightly build or the stable build.
|
||||
# NOTE: REMINDER to vLLM-Omni rebase maintainer
|
||||
# Remember to set `USE_NIGHTLY_BUILD` to 0, when switching back to stable vLLM docker image
|
||||
# 1. If vLLM-omni maintainer is forced to use custom commits
|
||||
# during rebasing, they can change the two variables
|
||||
# 2. Whenever vLLM upstream has released a stable version,
|
||||
# we should swap over to use stable release ASAP.
|
||||
# We should avoid relying on custom commits.
|
||||
ARG USE_NIGHTLY_BUILD=0
|
||||
ARG VLLM_VERSION_OR_COMMIT_HASH=2d5be1dd5ce2e44dfea53ea03ff61143da5137eb
|
||||
ARG ARG_PYTORCH_ROCM_ARCH
|
||||
ENV PYTORCH_ROCM_ARCH=${ARG_PYTORCH_ROCM_ARCH:-${PYTORCH_ROCM_ARCH}}
|
||||
|
||||
ARG COMMON_WORKDIR=/app
|
||||
WORKDIR ${COMMON_WORKDIR}
|
||||
|
||||
# Step 1: Setup - Install system dependencies
|
||||
RUN apt-get update && \
|
||||
apt-get install -y ffmpeg && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Step 2: Conditionally reinstall vllm from source for nightly builds
|
||||
RUN if [ "${USE_NIGHTLY_BUILD}" = "1" ]; then \
|
||||
python3 -m pip uninstall -y vllm && rm -rf vllm && \
|
||||
git clone https://github.com/vllm-project/vllm.git && \
|
||||
cd vllm && \
|
||||
git checkout ${VLLM_VERSION_OR_COMMIT_HASH} && \
|
||||
python3 -m pip install -r requirements/rocm.txt && \
|
||||
python3 setup.py clean --all && \
|
||||
PYTORCH_ROCM_ARCH=${PYTORCH_ROCM_ARCH} python3 setup.py develop && \
|
||||
cd ../ && \
|
||||
rm -rf vllm/.git; \
|
||||
fi
|
||||
|
||||
# Step 3: Copy vllm-omni code and install without uv
|
||||
RUN mkdir -p ${COMMON_WORKDIR}/vllm-omni
|
||||
COPY . ${COMMON_WORKDIR}/vllm-omni
|
||||
RUN cd ${COMMON_WORKDIR}/vllm-omni && uv pip install --python "$(python3 -c 'import sys; print(sys.executable)')" --no-cache-dir ".[dev]" --no-build-isolation
|
||||
|
||||
RUN ln -sf /usr/bin/python3 /usr/bin/python
|
||||
|
||||
FROM base AS test
|
||||
|
||||
CMD ["/bin/bash"]
|
||||
ENTRYPOINT []
|
||||
|
||||
# Set entrypoint for vllm-openai official images
|
||||
FROM base AS vllm-openai
|
||||
ENTRYPOINT ["vllm", "serve", "--omni"]
|
||||
@@ -0,0 +1,139 @@
|
||||
# Argument to configure vllm base image if pre-built
|
||||
ARG VLLM_BASE=vllm-base
|
||||
|
||||
FROM intel/deep-learning-essentials:2025.3.2-0-devel-ubuntu24.04 AS vllm-base
|
||||
|
||||
WORKDIR /workspace/
|
||||
|
||||
ARG PYTHON_VERSION=3.12
|
||||
ARG PIP_EXTRA_INDEX_URL="https://download.pytorch.org/whl/xpu"
|
||||
|
||||
RUN wget -O- https://apt.repos.intel.com/intel-gpg-keys/GPG-PUB-KEY-INTEL-SW-PRODUCTS.PUB | gpg --dearmor | tee /usr/share/keyrings/oneapi-archive-keyring.gpg > /dev/null && \
|
||||
echo "deb [signed-by=/usr/share/keyrings/oneapi-archive-keyring.gpg] https://apt.repos.intel.com/oneapi all main" | tee /etc/apt/sources.list.d/oneAPI.list && \
|
||||
add-apt-repository -y ppa:kobuk-team/intel-graphics
|
||||
|
||||
RUN apt clean && apt-get update -y && \
|
||||
apt-get install -y --no-install-recommends --fix-missing \
|
||||
curl \
|
||||
ffmpeg \
|
||||
git \
|
||||
libsndfile1 \
|
||||
libsm6 \
|
||||
libxext6 \
|
||||
libgl1 \
|
||||
lsb-release \
|
||||
libaio-dev \
|
||||
numactl \
|
||||
wget \
|
||||
vim \
|
||||
python3.12 \
|
||||
python3.12-dev \
|
||||
python3-pip
|
||||
|
||||
RUN apt update && apt upgrade -y && \
|
||||
apt install -y libze1 libze-dev libze-intel-gpu1 intel-opencl-icd libze-intel-gpu-raytracing intel-ocloc && \
|
||||
apt install -y intel-oneapi-compiler-dpcpp-cpp-2025.3
|
||||
|
||||
ENV PATH="/root/.local/bin:$PATH"
|
||||
ENV VIRTUAL_ENV="/opt/venv"
|
||||
ENV UV_PYTHON_INSTALL_DIR=/opt/uv/python
|
||||
RUN curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
RUN uv venv --python ${PYTHON_VERSION} --seed ${VIRTUAL_ENV}
|
||||
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
|
||||
|
||||
# This oneccl contains the BMG support which is not the case for default version of oneapi 2025.2.
|
||||
ARG ONECCL_INSTALLER="intel-oneccl-2021.15.7.8_offline.sh"
|
||||
RUN wget "https://github.com/uxlfoundation/oneCCL/releases/download/2021.15.7/${ONECCL_INSTALLER}" && \
|
||||
bash "${ONECCL_INSTALLER}" -a --silent --eula accept && \
|
||||
rm "${ONECCL_INSTALLER}" && \
|
||||
echo "source /opt/intel/oneapi/setvars.sh --force" >> /root/.bashrc && \
|
||||
echo "source /opt/intel/oneapi/ccl/2021.15/env/vars.sh --force" >> /root/.bashrc
|
||||
RUN rm -f /opt/intel/oneapi/ccl/latest && \
|
||||
ln -s /opt/intel/oneapi/ccl/2021.15 /opt/intel/oneapi/ccl/latest
|
||||
|
||||
SHELL ["bash", "-c"]
|
||||
CMD ["bash", "-c", "source /root/.bashrc && exec bash"]
|
||||
|
||||
WORKDIR /workspace/
|
||||
ENV UV_HTTP_TIMEOUT=500
|
||||
|
||||
# Configure package index for XPU
|
||||
ENV PIP_EXTRA_INDEX_URL=${PIP_EXTRA_INDEX_URL}
|
||||
ENV UV_EXTRA_INDEX_URL=${PIP_EXTRA_INDEX_URL}
|
||||
ENV UV_INDEX_STRATEGY="unsafe-best-match"
|
||||
ENV UV_LINK_MODE="copy"
|
||||
|
||||
ARG VLLM_VERSION=v0.16.0
|
||||
RUN git clone -b ${VLLM_VERSION} https://github.com/vllm-project/vllm
|
||||
WORKDIR /workspace/vllm
|
||||
|
||||
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||
uv pip install --upgrade pip && \
|
||||
uv pip install -r requirements/xpu.txt
|
||||
|
||||
# used for suffix method speculative decoding
|
||||
# build deps for proto + nanobind-based extensions to set up the build environment
|
||||
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||
uv pip install grpcio-tools protobuf nanobind
|
||||
# arctic-inference is built from source which needs torch-xpu properly installed first
|
||||
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||
source /opt/intel/oneapi/setvars.sh --force && \
|
||||
source /opt/intel/oneapi/ccl/2021.15/env/vars.sh --force && \
|
||||
export CMAKE_PREFIX_PATH="$(python -c 'import site; print(site.getsitepackages()[0])'):${CMAKE_PREFIX_PATH}" && \
|
||||
uv pip install --no-build-isolation arctic-inference==0.1.1
|
||||
|
||||
ENV LD_LIBRARY_PATH="$LD_LIBRARY_PATH:/usr/local/lib/"
|
||||
|
||||
ENV VLLM_TARGET_DEVICE=xpu
|
||||
ENV VLLM_WORKER_MULTIPROC_METHOD=spawn
|
||||
|
||||
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||
uv pip install --no-build-isolation .
|
||||
|
||||
CMD ["/bin/bash"]
|
||||
|
||||
FROM vllm-base AS vllm-openai
|
||||
|
||||
# install additional dependencies for openai api server
|
||||
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||
uv pip install accelerate hf_transfer pytest pytest_asyncio lm_eval[api] modelscope
|
||||
|
||||
# install development dependencies (for testing)
|
||||
RUN uv pip install -e tests/vllm_test_utils
|
||||
|
||||
# install nixl from source code
|
||||
ENV NIXL_VERSION=0.7.0
|
||||
RUN python /workspace/vllm/tools/install_nixl_from_source_ubuntu.py
|
||||
|
||||
# ensure vllm is properly installed
|
||||
RUN python -c "import vllm, inspect; print(vllm.__file__)"
|
||||
RUN uv pip show vllm
|
||||
|
||||
CMD ["/bin/bash"]
|
||||
|
||||
ENTRYPOINT []
|
||||
|
||||
FROM ${VLLM_BASE} AS vllm-omni
|
||||
|
||||
WORKDIR /workspace/vllm-omni
|
||||
COPY . .
|
||||
|
||||
ENV VLLM_OMNI_TARGET_DEVICE=xpu
|
||||
RUN uv pip install --no-cache-dir ".[dev]" --no-build-isolation
|
||||
|
||||
# FIX triton
|
||||
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||
uv pip uninstall triton triton-xpu && \
|
||||
uv pip install triton-xpu==3.6.0
|
||||
|
||||
# remove torch bundled oneccl to avoid conflicts
|
||||
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||
uv pip uninstall oneccl oneccl-devel
|
||||
|
||||
FROM vllm-omni AS vllm-omni-openai
|
||||
|
||||
RUN ln -sf /usr/bin/python3 /usr/bin/python
|
||||
|
||||
ENV VLLM_WORKER_MULTIPROC_METHOD=spawn
|
||||
|
||||
ENTRYPOINT ["vllm", "serve", "--omni"]
|
||||
+87
-43
@@ -1,44 +1,88 @@
|
||||
nav:
|
||||
- Home: README.md
|
||||
- User Guide:
|
||||
- Getting Started:
|
||||
- getting_started/quickstart.md
|
||||
- getting_started/installation
|
||||
- Examples:
|
||||
- examples/README.md
|
||||
- Offline Inference:
|
||||
- Qwen2.5-Omni: user_guide/examples/offline_inference/qwen2_5_omni.md
|
||||
- Online Serving:
|
||||
- Qwen2.5-Omni: user_guide/examples/online_serving/qwen2_5_omni.md
|
||||
- Qwen2.5-Image: user_guide/examples/offline_inference/qwen_image.md
|
||||
- General:
|
||||
- usage/*
|
||||
- Configuration:
|
||||
- configuration/README.md
|
||||
- configuration/*
|
||||
- Models:
|
||||
- models/supported_models.md
|
||||
- Developer Guide:
|
||||
- General:
|
||||
- contributing/README.md
|
||||
- glob: contributing/*
|
||||
flatten_single_child_sections: true
|
||||
- Model Implementation:
|
||||
- contributing/model/README.md
|
||||
- CI: contributing/ci
|
||||
- Design Documents:
|
||||
- design/index.md
|
||||
- design/architecture_overview.md
|
||||
- design/vllm_omni_design.md
|
||||
- design/mrs_design.md
|
||||
- design/api_design_doc.md
|
||||
- Docs Guide: contributing/DOCS_GUIDE.md
|
||||
- API Reference:
|
||||
- api/README.md
|
||||
- api/vllm_omni
|
||||
- CLI Reference: cli
|
||||
- Community:
|
||||
- community/*
|
||||
- Slack: https://slack.vllm.ai
|
||||
- Blog: https://blog.vllm.ai
|
||||
- Forum: https://discuss.vllm.ai
|
||||
- Home: README.md
|
||||
- User Guide:
|
||||
- Getting Started:
|
||||
- getting_started/quickstart.md
|
||||
- getting_started/installation/*
|
||||
- Serving:
|
||||
- OpenAI-Compatible API:
|
||||
- Image Generation: serving/image_generation_api.md
|
||||
- Image Edit: serving/image_edit_api.md
|
||||
- Text to Speech: serving/speech_api.md
|
||||
- Examples:
|
||||
- examples/README.md
|
||||
- Offline Inference:
|
||||
- BAGEL-7B-MoT: user_guide/examples/offline_inference/bagel.md
|
||||
- Image-To-Image: user_guide/examples/offline_inference/image_to_image.md
|
||||
- Image-To-Video: user_guide/examples/offline_inference/image_to_video.md
|
||||
- LoRA Inference(Diffusion): user_guide/examples/offline_inference/lora_inference.md
|
||||
- Qwen2.5-Omni: user_guide/examples/offline_inference/qwen2_5_omni.md
|
||||
- Qwen3-Omni: user_guide/examples/offline_inference/qwen3_omni.md
|
||||
- Qwen3-TTS: user_guide/examples/offline_inference/qwen3_tts.md
|
||||
- Text-To-Audio: user_guide/examples/offline_inference/text_to_audio.md
|
||||
- Text-To-Image: user_guide/examples/offline_inference/text_to_image.md
|
||||
- Text-To-Video: user_guide/examples/offline_inference/text_to_video.md
|
||||
- Online Serving:
|
||||
- BAGEL-7B-MoT: user_guide/examples/online_serving/bagel.md
|
||||
- Image-To-Image: user_guide/examples/online_serving/image_to_image.md
|
||||
- LoRA Inference(Diffusion): user_guide/examples/online_serving/lora_inference.md
|
||||
- Qwen2.5-Omni: user_guide/examples/online_serving/qwen2_5_omni.md
|
||||
- Qwen3-Omni: user_guide/examples/online_serving/qwen3_omni.md
|
||||
- Qwen3-TTS: user_guide/examples/online_serving/qwen3_tts.md
|
||||
- Text-To-Image: user_guide/examples/online_serving/text_to_image.md
|
||||
- General:
|
||||
- usage/*
|
||||
- Configuration:
|
||||
- configuration/README.md
|
||||
- configuration/*
|
||||
- Models:
|
||||
- models/supported_models.md
|
||||
- Features:
|
||||
- Sleep Mode: features/sleep_mode.md
|
||||
- Diffusion Features:
|
||||
- Acceleration Overview: user_guide/diffusion_acceleration.md
|
||||
- TeaCache: user_guide/diffusion/teacache.md
|
||||
- Cache-DiT: user_guide/diffusion/cache_dit_acceleration.md
|
||||
- Quantization:
|
||||
- Overview: user_guide/diffusion/quantization/overview.md
|
||||
- FP8: user_guide/diffusion/quantization/fp8.md
|
||||
- Parallelism Acceleration: user_guide/diffusion/parallelism_acceleration.md
|
||||
- CPU Offloading: user_guide/diffusion/cpu_offload_diffusion.md
|
||||
- LoRA: user_guide/diffusion/lora.md
|
||||
- ComfyUI: features/comfyui.md
|
||||
- Developer Guide:
|
||||
- General:
|
||||
- contributing/README.md
|
||||
- glob: contributing/*
|
||||
flatten_single_child_sections: true
|
||||
- Model Implementation:
|
||||
- contributing/model/README.md
|
||||
- contributing/model/adding_omni_model.md
|
||||
- contributing/model/adding_diffusion_model.md
|
||||
- CI: contributing/ci
|
||||
- Design Documents:
|
||||
- design/index.md
|
||||
- design/architecture_overview.md
|
||||
- Feature Design:
|
||||
- design/feature/disaggregated_inference.md
|
||||
- design/feature/ray_based_execution.md
|
||||
- design/feature/omni_connectors/
|
||||
- design/feature/cfg_parallel.md
|
||||
- design/feature/sequence_parallel.md
|
||||
- design/feature/tensor_parallel.md
|
||||
- design/feature/cache_dit.md
|
||||
- design/feature/teacache.md
|
||||
- Module Design:
|
||||
- design/module/ar_module.md
|
||||
- design/module/dit_module.md
|
||||
- design/module/entrypoint_module.md
|
||||
- Docs Guide: contributing/DOCS_GUIDE.md
|
||||
- API Reference:
|
||||
- api/README.md
|
||||
- api/vllm_omni
|
||||
- CLI Reference: cli
|
||||
- Community:
|
||||
- community/*
|
||||
- Slack: https://slack.vllm.ai
|
||||
- Blog: https://blog.vllm.ai
|
||||
- Forum: https://discuss.vllm.ai
|
||||
+7
-4
@@ -8,8 +8,8 @@ hide:
|
||||
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" src="./source/logos/vllm-omni-logo-text-dark.png">
|
||||
<img alt="vllm-omni" src="./source/logos/vllm-omni-logo-text-dark.png" width=55%>
|
||||
<source media="(prefers-color-scheme: dark)" src="./source/logos/vllm-omni-logo.png">
|
||||
<img alt="vllm-omni" src="./source/logos/vllm-omni-logo.png" width=55%>
|
||||
</picture>
|
||||
</p>
|
||||
<h3 align="center">
|
||||
@@ -34,7 +34,8 @@ Easy, fast, and cheap omni-modality model serving for everyone
|
||||
|
||||
<p align="center">
|
||||
<picture>
|
||||
<img alt="vllm-omni" src="./source/architecture/omni-modality model architecture.png" width=55%>
|
||||
<source media="(prefers-color-scheme: dark)" src="./source/architecture/omni-modality-model-architecture.png">
|
||||
<img alt="vllm-omni-arch" src="./source/architecture/omni-modality-model-architecture.png" width=55%>
|
||||
</picture>
|
||||
</p>
|
||||
|
||||
@@ -58,4 +59,6 @@ vLLM-Omni seamlessly supports most popular open-source models on HuggingFace, in
|
||||
- Multi-modality generation models (e.g. Qwen-Image)
|
||||
|
||||
For more information, checkout the following:
|
||||
- [Placeholder for vllm-omni blogpost]()
|
||||
|
||||
- [vllm-omni architecture design and recent roadmaps](https://docs.google.com/presentation/d/1qv4qMW1rKAqDREMXiUDLIgqqHQe7TDPj/edit?usp=sharing&ouid=110473603432222024453&rtpof=true&sd=true)
|
||||
- [vllm-omni announcement blogpost](https://blog.vllm.ai/2025/11/30/vllm-omni.html)
|
||||
+58
-38
@@ -4,27 +4,31 @@
|
||||
|
||||
Main entry points for vLLM-Omni inference and serving.
|
||||
|
||||
- [vllm_omni.entrypoints.async_omni.AsyncOmni][]
|
||||
- [vllm_omni.entrypoints.async_omni_diffusion.AsyncOmniDiffusion][]
|
||||
- [vllm_omni.entrypoints.async_omni_llm.AsyncOmniLLM][]
|
||||
- [vllm_omni.entrypoints.async_omni_llm.AsyncOmniStageLLM][]
|
||||
- [vllm_omni.entrypoints.chat_utils.OmniAsyncMultiModalContentParser][]
|
||||
- [vllm_omni.entrypoints.chat_utils.OmniAsyncMultiModalItemTracker][]
|
||||
- [vllm_omni.entrypoints.chat_utils.parse_chat_messages_futures][]
|
||||
- [vllm_omni.entrypoints.chat_utils.extract_audio_from_video_async][]
|
||||
- [vllm_omni.entrypoints.cli.benchmark.base.OmniBenchmarkSubcommandBase][]
|
||||
- [vllm_omni.entrypoints.cli.benchmark.main.OmniBenchmarkSubcommand][]
|
||||
- [vllm_omni.entrypoints.cli.benchmark.serve.OmniBenchmarkServingSubcommand][]
|
||||
- [vllm_omni.entrypoints.cli.serve.OmniServeCommand][]
|
||||
- [vllm_omni.entrypoints.log_utils.OrchestratorMetrics][]
|
||||
- [vllm_omni.entrypoints.client_request_state.ClientRequestState][]
|
||||
- [vllm_omni.entrypoints.omni.Omni][]
|
||||
- [vllm_omni.entrypoints.omni.OmniBase][]
|
||||
- [vllm_omni.entrypoints.omni_diffusion.OmniDiffusion][]
|
||||
- [vllm_omni.entrypoints.omni_llm.OmniLLM][]
|
||||
- [vllm_omni.entrypoints.omni_llm.OmniStageLLM][]
|
||||
- [vllm_omni.entrypoints.omni_stage.OmniStage][]
|
||||
- [vllm_omni.entrypoints.openai.serving_chat.OmniOpenAIServingChat][]
|
||||
- [vllm_omni.entrypoints.stage_utils.OmniStageTaskType][]
|
||||
|
||||
## Inputs
|
||||
|
||||
Input data structures for multi-modal inputs.
|
||||
|
||||
- [vllm_omni.inputs.data.OmniDiffusionSamplingParams][]
|
||||
- [vllm_omni.inputs.data.OmniEmbedsPrompt][]
|
||||
- [vllm_omni.inputs.data.OmniTextPrompt][]
|
||||
- [vllm_omni.inputs.data.OmniTokenInputs][]
|
||||
- [vllm_omni.inputs.data.OmniTokensPrompt][]
|
||||
- [vllm_omni.inputs.parse.parse_singleton_prompt_omni][]
|
||||
- [vllm_omni.inputs.preprocess.OmniInputPreprocessor][]
|
||||
|
||||
## Engine
|
||||
@@ -34,59 +38,75 @@ Engine classes for offline and online inference.
|
||||
- [vllm_omni.diffusion.diffusion_engine.DiffusionEngine][]
|
||||
- [vllm_omni.engine.AdditionalInformationEntry][]
|
||||
- [vllm_omni.engine.AdditionalInformationPayload][]
|
||||
- [vllm_omni.engine.OmniEngineCoreOutput][]
|
||||
- [vllm_omni.engine.OmniEngineCoreOutputs][]
|
||||
- [vllm_omni.engine.OmniEngineCoreRequest][]
|
||||
- [vllm_omni.engine.PromptEmbedsPayload][]
|
||||
- [vllm_omni.engine.arg_utils.AsyncOmniEngineArgs][]
|
||||
- [vllm_omni.engine.arg_utils.OmniEngineArgs][]
|
||||
- [vllm_omni.engine.input_processor.OmniInputProcessor][]
|
||||
- [vllm_omni.engine.output_processor.MultimodalOutputProcessor][]
|
||||
- [vllm_omni.engine.output_processor.OmniRequestState][]
|
||||
- [vllm_omni.engine.processor.OmniProcessor][]
|
||||
|
||||
## Core
|
||||
|
||||
Core scheduling and caching components.
|
||||
|
||||
- [vllm_omni.core.dit_cache_manager.DiTCacheManager][]
|
||||
- [vllm_omni.core.sched.diffusion_scheduler.DiffusionScheduler][]
|
||||
- [vllm_omni.core.sched.omni_ar_scheduler.KVCacheTransferData][]
|
||||
- [vllm_omni.core.sched.omni_ar_scheduler.OmniARScheduler][]
|
||||
- [vllm_omni.core.sched.omni_generation_scheduler.OmniGenerationScheduler][]
|
||||
- [vllm_omni.core.sched.output.OmniCachedRequestData][]
|
||||
- [vllm_omni.core.sched.output.OmniNewRequestData][]
|
||||
- [vllm_omni.core.sched.scheduler.OmniScheduler][]
|
||||
|
||||
## Model Executor
|
||||
|
||||
Model execution components.
|
||||
|
||||
- [vllm_omni.model_executor.models.qwen2_5_omni.qwen2_5_omni.OmniOutput][]
|
||||
- [vllm_omni.model_executor.models.qwen2_5_omni.qwen2_5_omni.Qwen2_5OmniForConditionalGeneration][]
|
||||
- [vllm_omni.model_executor.models.qwen2_5_omni.qwen2_5_omni_talker.Qwen2_5OmniTalkerForConditionalGeneration][]
|
||||
- [vllm_omni.model_executor.models.qwen2_5_omni.qwen2_5_omni_thinker.Qwen2_5OmniAudioFeatureInputs][]
|
||||
- [vllm_omni.model_executor.models.qwen2_5_omni.qwen2_5_omni_thinker.Qwen2_5OmniConditionalGenerationMixin][]
|
||||
- [vllm_omni.model_executor.models.qwen2_5_omni.qwen2_5_omni_thinker.Qwen2_5OmniThinkerDummyInputsBuilder][]
|
||||
- [vllm_omni.model_executor.models.qwen2_5_omni.qwen2_5_omni_thinker.Qwen2_5OmniThinkerForConditionalGeneration][]
|
||||
- [vllm_omni.model_executor.models.qwen2_5_omni.qwen2_5_omni_thinker.Qwen2_5OmniThinkerMultiModalDataParser][]
|
||||
- [vllm_omni.model_executor.models.qwen2_5_omni.qwen2_5_omni_thinker.Qwen2_5OmniThinkerMultiModalProcessor][]
|
||||
- [vllm_omni.model_executor.models.qwen2_5_omni.qwen2_5_omni_thinker.Qwen2_5OmniThinkerProcessingInfo][]
|
||||
- [vllm_omni.model_executor.models.qwen2_5_omni.qwen2_5_omni_token2wav.Qwen2_5OmniToken2WavBigVGANModel][]
|
||||
- [vllm_omni.model_executor.models.qwen2_5_omni.qwen2_5_omni_token2wav.Qwen2_5OmniToken2WavDiTModel][]
|
||||
- [vllm_omni.model_executor.models.qwen2_5_omni.qwen2_5_omni_token2wav.Qwen2_5OmniToken2WavForConditionalGenerationVLLM][]
|
||||
- [vllm_omni.model_executor.models.qwen2_5_omni.qwen2_5_omni_token2wav.Qwen2_5OmniToken2WavModel][]
|
||||
- [vllm_omni.model_executor.models.qwen2_5_omni.qwen2_old.Qwen2EmbeddingModel][]
|
||||
- [vllm_omni.model_executor.models.qwen2_5_omni.qwen2_old.Qwen2ForCausalLM][]
|
||||
- [vllm_omni.model_executor.models.qwen2_5_omni.qwen2_old.Qwen2Model][]
|
||||
- [vllm_omni.core.sched.output.OmniSchedulerOutput][]
|
||||
- [vllm_omni.model_executor.models.qwen3_tts.tokenizer_25hz.vq.core_vq.DistributedGroupResidualVectorQuantization][]
|
||||
- [vllm_omni.model_executor.models.qwen3_tts.tokenizer_25hz.vq.core_vq.DistributedResidualVectorQuantization][]
|
||||
- [vllm_omni.model_executor.models.qwen3_tts.tokenizer_25hz.vq.core_vq.EuclideanCodebook][]
|
||||
- [vllm_omni.model_executor.models.qwen3_tts.tokenizer_25hz.vq.core_vq.VectorQuantization][]
|
||||
- [vllm_omni.model_executor.models.qwen3_tts.tokenizer_25hz.vq.core_vq.preprocess][]
|
||||
|
||||
## Configuration
|
||||
|
||||
Configuration classes.
|
||||
|
||||
- [vllm_omni.config.model.OmniModelConfig][]
|
||||
- [vllm_omni.diffusion.cache.teacache.config.TeaCacheConfig][]
|
||||
- [vllm_omni.distributed.omni_connectors.utils.config.ConnectorSpec][]
|
||||
- [vllm_omni.distributed.omni_connectors.utils.config.OmniTransferConfig][]
|
||||
- [vllm_omni.model_executor.models.qwen3_tts.configuration_qwen3_tts.Qwen3TTSConfig][]
|
||||
- [vllm_omni.model_executor.models.qwen3_tts.configuration_qwen3_tts.Qwen3TTSSpeakerEncoderConfig][]
|
||||
- [vllm_omni.model_executor.models.qwen3_tts.configuration_qwen3_tts.Qwen3TTSTalkerCodePredictorConfig][]
|
||||
- [vllm_omni.model_executor.models.qwen3_tts.configuration_qwen3_tts.Qwen3TTSTalkerConfig][]
|
||||
- [vllm_omni.model_executor.models.qwen3_tts.tokenizer_12hz.configuration_qwen3_tts_tokenizer_v2.Qwen3TTSTokenizerV2Config][]
|
||||
- [vllm_omni.model_executor.models.qwen3_tts.tokenizer_12hz.configuration_qwen3_tts_tokenizer_v2.Qwen3TTSTokenizerV2DecoderConfig][]
|
||||
- [vllm_omni.model_executor.models.qwen3_tts.tokenizer_25hz.configuration_qwen3_tts_tokenizer_v1.Qwen3TTSTokenizerV1Config][]
|
||||
- [vllm_omni.model_executor.models.qwen3_tts.tokenizer_25hz.configuration_qwen3_tts_tokenizer_v1.Qwen3TTSTokenizerV1DecoderBigVGANConfig][]
|
||||
- [vllm_omni.model_executor.models.qwen3_tts.tokenizer_25hz.configuration_qwen3_tts_tokenizer_v1.Qwen3TTSTokenizerV1DecoderConfig][]
|
||||
- [vllm_omni.model_executor.models.qwen3_tts.tokenizer_25hz.configuration_qwen3_tts_tokenizer_v1.Qwen3TTSTokenizerV1DecoderDiTConfig][]
|
||||
- [vllm_omni.model_executor.models.qwen3_tts.tokenizer_25hz.configuration_qwen3_tts_tokenizer_v1.Qwen3TTSTokenizerV1EncoderConfig][]
|
||||
|
||||
## Workers
|
||||
|
||||
Worker classes and model runners for distributed inference.
|
||||
|
||||
- [vllm_omni.diffusion.worker.gpu_worker.GPUWorker][]
|
||||
- [vllm_omni.diffusion.worker.gpu_worker.WorkerProc][]
|
||||
- [vllm_omni.diffusion.worker.diffusion_model_runner.DiffusionModelRunner][]
|
||||
- [vllm_omni.diffusion.worker.diffusion_worker.DiffusionWorker][]
|
||||
- [vllm_omni.diffusion.worker.diffusion_worker.WorkerProc][]
|
||||
- [vllm_omni.platforms.npu.worker.npu_ar_model_runner.ExecuteModelState][]
|
||||
- [vllm_omni.platforms.npu.worker.npu_ar_model_runner.NPUARModelRunner][]
|
||||
- [vllm_omni.platforms.npu.worker.npu_ar_worker.NPUARWorker][]
|
||||
- [vllm_omni.platforms.npu.worker.npu_generation_model_runner.NPUGenerationModelRunner][]
|
||||
- [vllm_omni.platforms.npu.worker.npu_generation_worker.NPUGenerationWorker][]
|
||||
- [vllm_omni.platforms.npu.worker.npu_model_runner.OmniNPUModelRunner][]
|
||||
- [vllm_omni.platforms.xpu.worker.xpu_ar_model_runner.XPUARModelRunner][]
|
||||
- [vllm_omni.platforms.xpu.worker.xpu_ar_worker.XPUARWorker][]
|
||||
- [vllm_omni.platforms.xpu.worker.xpu_generation_model_runner.XPUGenerationModelRunner][]
|
||||
- [vllm_omni.platforms.xpu.worker.xpu_generation_worker.XPUGenerationWorker][]
|
||||
- [vllm_omni.worker.base.OmniGPUWorkerBase][]
|
||||
- [vllm_omni.worker.gpu_ar_model_runner.ExecuteModelState][]
|
||||
- [vllm_omni.worker.gpu_ar_model_runner.GPUARModelRunner][]
|
||||
- [vllm_omni.worker.gpu_ar_worker.GPUARWorker][]
|
||||
- [vllm_omni.worker.gpu_diffusion_model_runner.GPUDiffusionModelRunner][]
|
||||
- [vllm_omni.worker.gpu_diffusion_worker.GPUDiffusionWorker][]
|
||||
- [vllm_omni.worker.gpu_generation_model_runner.GPUGenerationModelRunner][]
|
||||
- [vllm_omni.worker.gpu_generation_worker.GPUGenerationWorker][]
|
||||
- [vllm_omni.worker.gpu_memory_utils.parse_cuda_visible_devices][]
|
||||
- [vllm_omni.worker.gpu_model_runner.OmniGPUModelRunner][]
|
||||
- [vllm_omni.worker.mixins.OmniWorkerMixin][]
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 127 KiB |
@@ -22,3 +22,21 @@ If you have custom stage configs file, launch the server with command below
|
||||
```bash
|
||||
vllm serve Qwen/Qwen2.5-Omni-7B --omni --stage-configs-path /path/to/stage_configs_file
|
||||
```
|
||||
|
||||
|
||||
## bench
|
||||
|
||||
Run benchmark tests for online serving throughput.
|
||||
Available Commands:
|
||||
|
||||
```bash
|
||||
vllm bench serve --omni \
|
||||
--model Qwen/Qwen2.5-Omni-7B \
|
||||
--host server-host \
|
||||
--port server-port \
|
||||
--random-input-len 32 \
|
||||
--random-output-len 4 \
|
||||
--num-prompts 5
|
||||
```
|
||||
|
||||
See [vllm bench serve](./bench/serve.md) for the full reference of all available arguments.
|
||||
@@ -0,0 +1,359 @@
|
||||
# vLLM-Omni Benchmark CLI Guide
|
||||
The vllm bench command launches the vLLM-Omni benchmark to evaluate the performance of multimodal models.
|
||||
|
||||
## Notes
|
||||
We currently only support using the "openai-chat-omni" backend.
|
||||
|
||||
## Basic Parameter Description
|
||||
You can use `vllm bench serve --omni --help=all` to get descriptions of all parameters. The commonly used parameters are described below:
|
||||
- `--omni`
|
||||
Enable Omni (multimodal) mode, supporting multimodal inputs and outputs such as images, videos, and audio.
|
||||
|
||||
- `--backend`
|
||||
Specify the backend adapter as openai-chat-omni, using OpenAI Chat compatible API behavior as the protocol. Currently only openai-chat-omni is supported.
|
||||
|
||||
- `--model`
|
||||
The model identifier to load, filled according to the models supported by vLLM-Omni.
|
||||
|
||||
- `--endpoint`
|
||||
The API endpoint exposed externally, to which clients send their requests.
|
||||
|
||||
- `--dataset-name`
|
||||
The name of the dataset used; random-mm indicates generating random multimodal inputs (images, videos, audio).
|
||||
|
||||
- `--num-prompts`
|
||||
The total number of requests to send, an integer.
|
||||
|
||||
- `--max-concurrency`
|
||||
"Maximum number of concurrent requests. This can be used "
|
||||
"to help simulate an environment where a higher level component "
|
||||
"is enforcing a maximum number of concurrent requests. While the "
|
||||
"--request-rate argument controls the rate at which requests are "
|
||||
"initiated, this argument will control how many are actually allowed "
|
||||
"to execute at a time. This means that when used in combination, the "
|
||||
"actual request rate may be lower than specified with --request-rate, "
|
||||
"if the server is not processing requests fast enough to keep up."
|
||||
|
||||
- `--request-rate`
|
||||
"Number of requests per second. If this is inf, "
|
||||
"then all the requests are sent at time 0. "
|
||||
"Otherwise, we use Poisson process or gamma distribution "
|
||||
"to synthesize the request arrival times."
|
||||
|
||||
- `--ignore-eos`
|
||||
"Set ignore_eos flag when sending the benchmark request."
|
||||
|
||||
- `--metric-percentiles`
|
||||
Comma-separated list of percentiles for selected metrics. "
|
||||
"To report 25-th, 50-th, and 75-th percentiles, use \"25,50,75\". "
|
||||
"Default value is \"99\"."
|
||||
"Use \"--percentile-metrics\" to select metrics.
|
||||
|
||||
- `--percentile-metrics`
|
||||
"Comma-separated list of selected metrics to report percentiles."
|
||||
"This argument specifies the metrics to report percentiles."
|
||||
'Allowed metric names are "ttft", "tpot", "itl", "e2el", "audio_ttfp", "audio_rtf", "audio_duration". '
|
||||
|
||||
- `--save-result`
|
||||
Specify to save benchmark results to a json file
|
||||
|
||||
- `--save-detailed`
|
||||
"When saving the results, whether to include per request "
|
||||
"information such as response, error, ttfs, tpots, etc."
|
||||
|
||||
- `--result-dir`
|
||||
"Specify directory to save benchmark json results."
|
||||
"If not specified, results are saved in the current directory."
|
||||
|
||||
- `--result-filename`
|
||||
"Specify the filename to save benchmark json results."
|
||||
"If not specified, results will be saved in "
|
||||
"{label}-{args.request_rate}qps-{base_model_id}-{current_dt}.json"
|
||||
|
||||
- `--random-prefix-len`
|
||||
Number of fixed prefix tokens before the random context in a request.
|
||||
The total input length is the sum of random-prefix-len and a random
|
||||
context length sampled from [input_len * (1 - range_ratio),
|
||||
input_len * (1 + range_ratio)].Only the random and random-mm modes
|
||||
support this parameter.
|
||||
|
||||
- `--random-input-len`
|
||||
Number of input tokens per request.Only the random and random-mm modes support this parameter.
|
||||
|
||||
- `--random-output-len`
|
||||
Number of output tokens per request.Only the random and random-mm modes support this parameter.
|
||||
|
||||
- `--random-range-ratio`
|
||||
Range ratio for sampling input/output length,
|
||||
used only for random sampling. Must be in the range [0, 1) to define
|
||||
a symmetric sampling range
|
||||
[length * (1 - range_ratio), length * (1 + range_ratio)].
|
||||
Only the random and random-mm modes support this parameter.
|
||||
|
||||
- `--random-mm-base-items-per-request`
|
||||
Base number of multimodal items per request for random-mm.
|
||||
Actual per-request count is sampled around this base using
|
||||
--random-mm-num-mm-items-range-ratio.
|
||||
Only the random-mm mode supports this parameter.
|
||||
|
||||
- `--random-mm-limit-mm-per-prompt`
|
||||
Per-modality hard caps for items attached per request, e.g.
|
||||
'{"image": 3, "video": 1, "audio": 1}'. The sampled per-request item
|
||||
count is clamped to the sum of these limits. When a modality
|
||||
reaches its cap, its buckets are excluded and probabilities are
|
||||
renormalized.
|
||||
Only the random-mm mode supports this parameter.
|
||||
|
||||
- `--random-mm-num-mm-items-range-ratio`
|
||||
Range ratio r in [0, 1] for sampling items per request.
|
||||
We sample uniformly from the closed integer range
|
||||
[floor(n*(1-r)), ceil(n*(1+r))]
|
||||
where n is the base items per request.
|
||||
r=0 keeps it fixed; r=1 allows 0 items. The maximum is clamped
|
||||
to the sum of per-modality limits from
|
||||
--random-mm-limit-mm-per-prompt.
|
||||
An error is raised if the computed min exceeds the max.
|
||||
Only the random-mm mode supports this parameter.
|
||||
|
||||
- `--random-mm-bucket-config`
|
||||
The bucket config is a dictionary mapping a multimodal item
|
||||
sampling configuration to a probability.
|
||||
Currently allows for 3 modalities: audio, images and videos.
|
||||
A bucket key is a tuple of (height, width, num_frames)
|
||||
The value is the probability of sampling that specific item.
|
||||
Example:
|
||||
--random-mm-bucket-config
|
||||
"{(256, 256, 1): 0.5, (720, 1280, 16): 0.4, (0, 1, 5): 0.10}"
|
||||
First item: images with resolution 256x256 w.p. 0.5
|
||||
Second item: videos with resolution 720x1280 and 16 frames
|
||||
Third item: audios with 1s duration and 5 channels w.p. 0.1
|
||||
OBS.: If the probabilities do not sum to 1, they are normalized.
|
||||
Only the random-mm mode supports this parameter
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Online Benchmark
|
||||
<details class="admonition abstract" markdown="1">
|
||||
<summary>Show more</summary>
|
||||
|
||||
First start serving your model:
|
||||
|
||||
```bash
|
||||
vllm serve Qwen/Qwen2.5-Omni-7B --omni
|
||||
```
|
||||
|
||||
Then run the benchmarking for sharegpt:
|
||||
|
||||
```bash
|
||||
# download dataset
|
||||
# wget https://huggingface.co/datasets/anon8231489123/ShareGPT_Vicuna_unfiltered/resolve/main/ShareGPT_V3_unfiltered_cleaned_split.json
|
||||
vllm bench serve \
|
||||
--omni \
|
||||
--port 43845 \
|
||||
--model /home/models/Qwen/Qwen3-Omni-30B-A3B-Instruct \
|
||||
--endpoint /v1/chat/completions \
|
||||
--backend openai-chat-omni \
|
||||
--num-prompts 2 \
|
||||
--dataset-name sharegpt \
|
||||
--dataset-path ShareGPT_V3_unfiltered_cleaned_split.json \
|
||||
--percentile-metrics ttft,tpot,itl,e2el
|
||||
```
|
||||
If successful, you will see the following output:
|
||||
```text
|
||||
============ Serving Benchmark Result ============
|
||||
Successful requests: 2
|
||||
Failed requests: 0
|
||||
Benchmark duration (s): 81.63
|
||||
Request throughput (req/s): 0.02
|
||||
Peak concurrent requests: 2.00
|
||||
----------------End-to-end Latency----------------
|
||||
Mean E2EL (ms): 56966.13
|
||||
Median E2EL (ms): 56966.13
|
||||
P99 E2EL (ms): 81016.80
|
||||
================== Text Result ===================
|
||||
Total input tokens: 36
|
||||
Total generated tokens: 5926
|
||||
Output token throughput (tok/s): 72.60
|
||||
Peak output token throughput (tok/s): 103.00
|
||||
Peak concurrent requests: 2.00
|
||||
Total Token throughput (tok/s): 73.04
|
||||
---------------Time to First Token----------------
|
||||
Mean TTFT (ms): 124.76
|
||||
Median TTFT (ms): 124.76
|
||||
P99 TTFT (ms): 156.10
|
||||
-----Time per Output Token (excl. 1st token)------
|
||||
Mean TPOT (ms): 481.30
|
||||
Median TPOT (ms): 481.30
|
||||
P99 TPOT (ms): 947.55
|
||||
---------------Inter-token Latency----------------
|
||||
Mean ITL (ms): 25.11
|
||||
Median ITL (ms): 0.33
|
||||
P99 ITL (ms): 25.17
|
||||
================== Audio Result ==================
|
||||
Total audio duration generated(s): 3.95
|
||||
Total audio frames generated: 94890
|
||||
Audio throughput(audio duration/s): 0.05
|
||||
==================================================
|
||||
```
|
||||
|
||||
Or run the benchmarking for random:
|
||||
|
||||
```bash
|
||||
vllm bench serve \
|
||||
--omni \
|
||||
--port 43845 \
|
||||
--endpoint /v1/chat/completions \
|
||||
--backend openai-chat-omni \
|
||||
--model /home/models/Qwen/Qwen3-Omni-30B-A3B-Instruct \
|
||||
--dataset-name random \
|
||||
--num-prompts 2 \
|
||||
--random-prefix-len 5 \
|
||||
--random-input-len 10 \
|
||||
--random-output-len 100 \
|
||||
--percentile-metrics ttft,tpot,itl,e2el,audio_ttfp,audio_rtf \
|
||||
--ignore-eos
|
||||
```
|
||||
|
||||
If successful, you will see the following output:
|
||||
|
||||
```text
|
||||
============ Serving Benchmark Result ============
|
||||
Successful requests: 2
|
||||
Failed requests: 0
|
||||
Benchmark duration (s): 3.89
|
||||
Request throughput (req/s): 0.51
|
||||
Peak concurrent requests: 2.00
|
||||
----------------End-to-end Latency----------------
|
||||
Mean E2EL (ms): 3824.76
|
||||
Median E2EL (ms): 3824.76
|
||||
P99 E2EL (ms): 3888.54
|
||||
================== Text Result ===================
|
||||
Total input tokens: 30
|
||||
Total generated tokens: 10101
|
||||
Output token throughput (tok/s): 2595.57
|
||||
Peak output token throughput (tok/s): 111.00
|
||||
Peak concurrent requests: 2.00
|
||||
Total Token throughput (tok/s): 2603.28
|
||||
---------------Time to First Token----------------
|
||||
Mean TTFT (ms): 117.15
|
||||
Median TTFT (ms): 117.15
|
||||
P99 TTFT (ms): 142.69
|
||||
-----Time per Output Token (excl. 1st token)------
|
||||
Mean TPOT (ms): 0.73
|
||||
Median TPOT (ms): 0.73
|
||||
P99 TPOT (ms): 0.74
|
||||
---------------Inter-token Latency----------------
|
||||
Mean ITL (ms): 16.47
|
||||
Median ITL (ms): 16.19
|
||||
P99 ITL (ms): 52.55
|
||||
================== Audio Result ==================
|
||||
Total audio duration generated(s): 15.79
|
||||
Total audio frames generated: 379050
|
||||
Audio throughput(audio duration/s): 4.06
|
||||
---------------Time to First Packet---------------
|
||||
Mean AUDIO_TTFP (ms): 3701.37
|
||||
Median AUDIO_TTFP (ms): 3701.37
|
||||
P99 AUDIO_TTFP (ms): 3762.25
|
||||
-----------------Real Time Factor-----------------
|
||||
Mean AUDIO_RTF: 0.47
|
||||
Median AUDIO_RTF: 0.47
|
||||
P99 AUDIO_RTF: 0.48
|
||||
==================================================
|
||||
```
|
||||
Notes:
|
||||
We use audio generation time / audio duration to calculate RTF.
|
||||
|
||||
</details>
|
||||
|
||||
### Multi-Modal Benchmark
|
||||
|
||||
<details class="admonition abstract" markdown="1">
|
||||
<summary>Show more</summary>
|
||||
|
||||
Benchmark the performance of multi-modal requests in vLLM-Omni.
|
||||
|
||||
Generate synthetic image、video、audio inputs alongside random text prompts to stress-test vision models without external datasets.
|
||||
|
||||
Notes:
|
||||
|
||||
- Works only with online benchmark via the OpenAI backend (`--backend openai-chat-omni`) and endpoint `/v1/chat/completions`.
|
||||
|
||||
Start the server (example):
|
||||
|
||||
```bash
|
||||
vllm serve Qwen/Qwen2.5-Omni-7B --omni
|
||||
```
|
||||
|
||||
It is recommended to use the flag `--ignore-eos` to simulate real responses. You can set the size of the output via the arg `random-output-len`.
|
||||
|
||||
Then run the benchmarking script:
|
||||
```bash
|
||||
vllm bench serve \
|
||||
--omni \
|
||||
--dataset-name random-mm \
|
||||
--port 40849 \
|
||||
--model /home/models/Qwen/Qwen3-Omni-30B-A3B-Instruct \
|
||||
--endpoint /v1/chat/completions \
|
||||
--backend openai-chat-omni \
|
||||
--request-rate 1 \
|
||||
--num-prompts 1 \
|
||||
--random-input-len 10 \
|
||||
--random-range-ratio 0.0 \
|
||||
--random-mm-base-items-per-request 2 \
|
||||
--random-mm-num-mm-items-range-ratio 0 \
|
||||
--random-mm-limit-mm-per-prompt '{"image":1,"video":1, "audio": 1}' \
|
||||
--random-mm-bucket-config '{"(32, 32, 1)": 0.5, "(0, 1, 1)": 0.1, "(32, 32, 2)":0.4}' \
|
||||
--ignore-eos \
|
||||
--percentile-metrics ttft,tpot,itl \
|
||||
--random-output-len 2 \
|
||||
--extra_body '{"modalities": ["text"]}'
|
||||
```
|
||||
|
||||
If successful, you will see the following output:
|
||||
|
||||
```text
|
||||
============ Serving Benchmark Result ============
|
||||
Successful requests: 1
|
||||
Failed requests: 0
|
||||
Request rate configured (RPS): 1.00
|
||||
Benchmark duration (s): 1.21
|
||||
Request throughput (req/s): 0.83
|
||||
Peak concurrent requests: 1.00
|
||||
================== Text Result ===================
|
||||
Total input tokens: 10
|
||||
Total generated tokens: 3
|
||||
Output token throughput (tok/s): 2.49
|
||||
Peak output token throughput (tok/s): 3.00
|
||||
Peak concurrent requests: 1.00
|
||||
Total Token throughput (tok/s): 10.77
|
||||
---------------Time to First Token----------------
|
||||
Mean TTFT (ms): 179.74
|
||||
Median TTFT (ms): 179.74
|
||||
P99 TTFT (ms): 179.74
|
||||
-----Time per Output Token (excl. 1st token)------
|
||||
Mean TPOT (ms): 12.76
|
||||
Median TPOT (ms): 12.76
|
||||
P99 TPOT (ms): 12.76
|
||||
---------------Inter-token Latency----------------
|
||||
Mean ITL (ms): 12.76
|
||||
Median ITL (ms): 12.76
|
||||
P99 ITL (ms): 25.24
|
||||
================== Audio Result ==================
|
||||
Total audio duration generated(s): 0.00
|
||||
Total audio frames generated: 0
|
||||
Audio throughput(audio duration/s): 0.00
|
||||
==================================================
|
||||
```
|
||||
|
||||
Behavioral notes:
|
||||
|
||||
- If the requested base item count cannot be satisfied under the provided per-prompt limits, the tool raises an error rather than silently clamping.
|
||||
|
||||
How sampling works:
|
||||
|
||||
- Determine per-request item count k by sampling uniformly from the integer range defined by `--random-mm-base-items-per-request` and `--random-mm-num-mm-items-range-ratio`, then clamp k to at most the sum of per-modality limits.
|
||||
- For each of the k items, sample a bucket (H, W, T) according to the normalized probabilities in `--random-mm-bucket-config`, while tracking how many items of each modality have been added.
|
||||
- If a modality (e.g., image) reaches its limit from `--random-mm-limit-mm-per-prompt`, all buckets of that modality are excluded and the remaining bucket probabilities are renormalized before continuing.
|
||||
This should be seen as an edge case, and if this behavior can be avoided by setting `--random-mm-limit-mm-per-prompt` to a large number. Note that this might result in errors due to engine config `--limit-mm-per-prompt`.
|
||||
- The resulting request contains synthetic image data in `multi_modal_data` (OpenAI Chat format). When `random-mm` is used with the OpenAI Chat backend, prompts remain text and MM content is attached via `multi_modal_data`.
|
||||
</details>
|
||||
@@ -0,0 +1,9 @@
|
||||
# vllm-omni serve
|
||||
|
||||
## JSON CLI Arguments
|
||||
|
||||
--8<-- "docs/cli/json_tip.inc.md"
|
||||
|
||||
## Arguments
|
||||
|
||||
--8<-- "docs/generated/argparse_omni/omni_serve.inc.md"
|
||||
@@ -2,4 +2,4 @@
|
||||
|
||||
- For technical questions and feature requests, please use GitHub [Issues](https://github.com/vllm-project/vllm-omni/issues)
|
||||
- For coordinating contributions and development and discussing with other users and developers, please join `sig-omni` channel in our [Slack](https://slack.vllm.ai/) or use the [vLLM Forum](https://discuss.vllm.ai/)
|
||||
- For security disclosures, please use GitHub's [Security Advisories](https://github.com/vllm-project/vllm/security/advisories) feature
|
||||
- For security disclosures, please use GitHub's [Security Advisories](https://github.com/vllm-project/vllm-omni/security/advisories) feature
|
||||
@@ -0,0 +1,12 @@
|
||||
# Volunteers for Bugfix and CI
|
||||
|
||||
We encourage you to check current docs and [issues](https://github.com/vllm-project/vllm-omni/issues) to find possible solutions for your questions. If non of these can solve it, please propose an issue to describe your questions about bug or CI problems for developing.
|
||||
|
||||
If you have urgent need for locating and solving bugfix or CI problems, please find community volunteers below.
|
||||
|
||||
| Dec 4-Dec 12 | Dec 15-Dec 19 | Dec 22-Dec 26 | Dec 29- Jan 2, 2026| Jan 5-Jan 9 | Jan 12-Jan 16 |
|
||||
|----------|----------|----------|----------|----------|----------|
|
||||
| <a href="https://github.com/congw729">Conw729</a> | <a href="https://github.com/yinpeiqi">yinpeiqi</a> | <a href="https://github.com/tzhouam">tzhouam</a> | <a href="https://github.com/SamitHuang">SamitHuang</a> | <a href="https://github.com/gcanlin">gcanlin</a> | <a href="https://github.com/natureofnature">natureofnature</a> |
|
||||
| <a href="https://github.com/david6666666">david6666666</a> | <a href="https://github.com/R2-Y">R2-Y</a> | <a href="https://github.com/hsliuustc0106">hsliuustc0106</a> | <a href="https://github.com/Gaohan123">Gaohan123</a> | <a href="https://github.com/ZJY0516">ZJY0516</a> | <a href="https://github.com/qibaoyuan">qibaoyuan</a> |
|
||||
|
||||
We kindly welcome more contributors to fix bugs and contribute new features!
|
||||
@@ -2,10 +2,20 @@
|
||||
|
||||
This section lists the most common options for running vLLM-Omni.
|
||||
|
||||
For options within a vLLM Engine. Please refer to [vLLM Configuration](https://docs.vllm.ai/en/v0.11.0/configuration/index.html)
|
||||
For options within a vLLM Engine. Please refer to [vLLM Configuration](https://docs.vllm.ai/en/v0.14.0/configuration/index.html)
|
||||
|
||||
Currently, the main options are maintained by stage configs for each model.
|
||||
|
||||
For specific example, please refer to [Qwen2.5-omni stage config](stage_configs/qwen2_5_omni.yaml)
|
||||
|
||||
For introduction, please check [Introduction for stage config](./stage_configs.md)
|
||||
|
||||
## Memory Configuration
|
||||
|
||||
- **[GPU Memory Calculation and Configuration](./gpu_memory_utilization.md)** - Guide on how to calculate memory requirements and set up `gpu_memory_utilization` for optimal performance
|
||||
|
||||
## Optimization Features
|
||||
|
||||
- **[TeaCache Configuration](../user_guide/diffusion/teacache.md)** - Enable TeaCache adaptive caching for DiT models to achieve 1.5x-2.0x speedup with minimal quality loss
|
||||
- **[Cache-DiT Configuration](../user_guide/diffusion/cache_dit_acceleration.md)** - Enable Cache-DiT as cache acceleration backends for DiT models
|
||||
- **[Parallelism Configuration](../user_guide/diffusion/parallelism_acceleration.md)** - Enable parallelism (e.g., sequence parallelism) for for DiT models
|
||||
@@ -0,0 +1,207 @@
|
||||
# GPU Memory Calculation and Configuration
|
||||
|
||||
This guide explains how to calculate GPU memory requirements and properly configure `gpu_memory_utilization` for vLLM-Omni stages.
|
||||
|
||||
## Overview
|
||||
|
||||
`gpu_memory_utilization` is a critical parameter that controls how much GPU memory each stage can use. It's specified as a fraction between 0.0 and 1.0, where:
|
||||
- `0.8` means 80% of the GPU's total memory
|
||||
- `1.0` means 100% of the GPU's total memory (not recommended, leaves no buffer)
|
||||
|
||||
## How Memory is Calculated
|
||||
|
||||
### Memory Allocation Formula
|
||||
|
||||
For each stage, vLLM-Omni calculates the requested memory as:
|
||||
|
||||
```
|
||||
requested_memory = total_gpu_memory × gpu_memory_utilization
|
||||
```
|
||||
|
||||
The system checks that:
|
||||
```
|
||||
free_memory ≥ requested_memory
|
||||
```
|
||||
|
||||
If this condition is not met, the stage will fail to initialize with an error message showing the memory requirements.
|
||||
|
||||
### Memory Components
|
||||
|
||||
The total memory used by a stage includes:
|
||||
|
||||
1. **Model Weights**: The size of the model parameters loaded on the GPU
|
||||
2. **KV Cache**: Memory for storing key-value cache during generation
|
||||
3. **Activation Memory**: Temporary memory for intermediate computations
|
||||
4. **System Overhead**: Memory used by CUDA, PyTorch, and other system components
|
||||
5. **Non-Torch Memory**: Memory allocated outside of PyTorch (e.g., CUDA graphs)
|
||||
|
||||
### Example Calculation
|
||||
|
||||
For a GPU with 80GB total memory:
|
||||
- `gpu_memory_utilization: 0.8` → 64GB available for the stage
|
||||
- `gpu_memory_utilization: 0.6` → 48GB available for the stage
|
||||
- `gpu_memory_utilization: 0.15` → 12GB available for the stage
|
||||
|
||||
## Setting Up `gpu_memory_utilization`
|
||||
|
||||
### Step 1: Determine GPU Memory
|
||||
|
||||
First, check your GPU's total memory:
|
||||
|
||||
```bash
|
||||
# Using nvidia-smi
|
||||
nvidia-smi --query-gpu=memory.total --format=csv
|
||||
|
||||
# Or using Python
|
||||
python -c "import torch; print(f'{torch.cuda.get_device_properties(0).total_memory / 1e9:.1f} GB')"
|
||||
```
|
||||
|
||||
### Step 2: Estimate Model Memory Requirements
|
||||
|
||||
#### For Autoregressive (AR) Stages
|
||||
|
||||
AR stages typically need more memory due to:
|
||||
- Large model weights
|
||||
- KV cache for attention
|
||||
- Activation buffers
|
||||
|
||||
#### For Diffusion/Generation Stages
|
||||
|
||||
Diffusion stages (like code2wav) typically need less memory:
|
||||
- Smaller model components
|
||||
- Different memory access patterns
|
||||
|
||||
**Typical values:**
|
||||
- `0.1 - 0.3` for most diffusion stages
|
||||
|
||||
### Step 3: Consider Multi-Stage Scenarios
|
||||
|
||||
When multiple stages share the same GPU, you must ensure the sum of their `gpu_memory_utilization` values doesn't exceed 1.0.
|
||||
|
||||
**Example: Two stages on GPU 0**
|
||||
```yaml
|
||||
stage_args:
|
||||
- stage_id: 0
|
||||
runtime:
|
||||
devices: "0"
|
||||
engine_args:
|
||||
gpu_memory_utilization: 0.6 # Uses 60% of GPU 0
|
||||
|
||||
- stage_id: 1
|
||||
runtime:
|
||||
devices: "0"
|
||||
engine_args:
|
||||
gpu_memory_utilization: 0.3 # Uses 30% of GPU 0
|
||||
# Total: 90% of GPU 0 (safe, leaves 10% buffer)
|
||||
```
|
||||
|
||||
**Important:** If stages run on different GPUs, each can use up to 1.0 independently.
|
||||
|
||||
### Step 4: Account for Tensor Parallelism
|
||||
|
||||
When using `tensor_parallel_size > 1`, the model is split across multiple GPUs, so each GPU needs less memory.
|
||||
|
||||
**Example: 2-way tensor parallelism**
|
||||
```yaml
|
||||
stage_args:
|
||||
- stage_id: 0
|
||||
runtime:
|
||||
devices: "0,1" # Uses both GPUs
|
||||
engine_args:
|
||||
tensor_parallel_size: 2
|
||||
gpu_memory_utilization: 0.6 # 60% per GPU
|
||||
# Model is split, so each GPU uses ~30% of model memory
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Qwen3-Omni-MoE on 2x H100-80GB
|
||||
|
||||
```yaml
|
||||
stage_args:
|
||||
- stage_id: 0 # Thinker stage with TP=2
|
||||
runtime:
|
||||
devices: "0,1"
|
||||
engine_args:
|
||||
tensor_parallel_size: 2
|
||||
gpu_memory_utilization: 0.6 # 48GB per GPU
|
||||
|
||||
- stage_id: 1 # Talker stage
|
||||
runtime:
|
||||
devices: "1"
|
||||
engine_args:
|
||||
gpu_memory_utilization: 0.3 # 24GB on GPU 1
|
||||
|
||||
- stage_id: 2 # Code2Wav stage
|
||||
runtime:
|
||||
devices: "0"
|
||||
engine_args:
|
||||
gpu_memory_utilization: 0.1 # 8GB on GPU 0
|
||||
```
|
||||
**Note:** In this configuration, stages 0 and 2 share GPU 0, but they run at different times in the pipeline, so their memory usage doesn't overlap.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Error: "Free memory is less than desired GPU memory utilization"
|
||||
|
||||
This means the GPU doesn't have enough free memory when the stage starts.
|
||||
|
||||
**Solutions:**
|
||||
1. Free up memory by closing other processes
|
||||
2. Reduce `gpu_memory_utilization` for this stage
|
||||
3. Use a GPU with more memory
|
||||
4. Move the stage to a different GPU
|
||||
|
||||
### Error: OOM during inference
|
||||
|
||||
The stage initialized but ran out of memory during processing.
|
||||
|
||||
**Solutions:**
|
||||
1. Reduce `max_num_batched_tokens`
|
||||
2. Reduce `max_batch_size` in runtime config
|
||||
3. Lower `gpu_memory_utilization` slightly
|
||||
4. Enable quantization if supported
|
||||
|
||||
### Memory Not Fully Utilized
|
||||
|
||||
If you see low memory usage, you can:
|
||||
1. Increase `gpu_memory_utilization` to allow larger KV cache
|
||||
2. Increase `max_num_batched_tokens` for better batching
|
||||
3. Check if other stages are limiting throughput
|
||||
|
||||
## Useful formula for Memory Calculation
|
||||
|
||||
### KV Cache Memory
|
||||
|
||||
The KV cache size depends on:
|
||||
- Number of sequences in batch
|
||||
- Sequence length (prompt + generation)
|
||||
- Model hidden size
|
||||
- Number of attention heads
|
||||
- Number of layers
|
||||
|
||||
approximate Formula:
|
||||
```
|
||||
kv_cache_memory ≈ batch_size × seq_len × hidden_size × num_layers × 2 × dtype_size
|
||||
```
|
||||
2 for k & v
|
||||
|
||||
### Model Weight Memory
|
||||
|
||||
```
|
||||
model_memory ≈ num_parameters × dtype_size
|
||||
```
|
||||
|
||||
For example:
|
||||
- 7B parameters in FP16: ~14GB
|
||||
- 7B parameters in FP32: ~28GB
|
||||
- 7B parameters in INT8: ~7GB
|
||||
|
||||
### Activation Memory
|
||||
|
||||
Activation memory is typically smaller but varies with:
|
||||
- Batch size
|
||||
- Sequence length
|
||||
- Model architecture
|
||||
|
||||
It's usually 10-30% of model weight memory during inference.
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||
In vLLM-Omni, the target model is separated into multiple stages, which are processed by different LLMEngines, DiffusionEngines or other types of engines. Depending on different types of stages, such as Autoregressive (AR) stage or Diffusion transformer (DiT) stage, each can choose corresponding schedulers, model workers to load with the Engines in a plug-in fashion.
|
||||
|
||||
!!! note
|
||||
Default stage config YAMLs (for example, `vllm_omni/model_executor/stage_configs/qwen2_5_omni.yaml` and `vllm_omni/model_executor/stage_configs/qwen3_omni_moe.yaml`) are bundled and loaded automatically when `stage_configs_path` is not provided. They have been verified to work on 1xH100 for Qwen2.5-Omni and 2xH100 for Qwen3-Omni.
|
||||
|
||||
Therefore, as a core part of vLLM-Omni, the stage configs for a model have several main functions:
|
||||
|
||||
- Claim partition of stages and their corresponding class implementation in `model_executor/models`.
|
||||
@@ -37,14 +40,14 @@ stage_args:
|
||||
engine_args: # Engine arguments for a certain engine
|
||||
model_stage: thinker
|
||||
model_arch: Qwen2_5OmniForConditionalGeneration # The model implementation registered in model_executor/models/registry.py
|
||||
worker_cls: vllm_omni.worker.gpu_ar_worker.GPUARWorker # The specific worker used
|
||||
scheduler_cls: vllm_omni.core.sched.scheduler.OmniScheduler # The specific scehduler used
|
||||
worker_type: ar # The specific worker used
|
||||
scheduler_cls: vllm_omni.core.sched.omni_ar_scheduler.OmniARScheduler # The specific scehduler used
|
||||
gpu_memory_utilization: 0.8 # The gpu memory allocation for the stage within a single chip
|
||||
enforce_eager: true # Now we only support eager mode
|
||||
trust_remote_code: true # Needed by huggingface config parsing
|
||||
engine_output_type: latent # It claims that the stage will input latent hiddenstates besides token ids
|
||||
enable_prefix_caching: false # For request with hiddenstates output, the prefix caching is not supported now
|
||||
is_comprehension: true # If the stage is a text or multimodal comprehension module. If it is, the AsyncOmniLLM will use its tokenizer as default
|
||||
is_comprehension: true # If the stage is a text or multimodal comprehension module. If it is, the AsyncOmni will use its tokenizer as default
|
||||
final_output: true # If the stage has output as part of final outputs. If it is false, which means that the stage only works as a intermediate role.
|
||||
final_output_type: text # What is the final output type. It can be text and audio now.
|
||||
default_sampling_params: # sampling parameters for the stage. Their meaning aligns with vLLM.
|
||||
@@ -63,8 +66,8 @@ stage_args:
|
||||
engine_args:
|
||||
model_stage: talker
|
||||
model_arch: Qwen2_5OmniForConditionalGeneration
|
||||
worker_cls: vllm_omni.worker.gpu_ar_worker.GPUARWorker
|
||||
scheduler_cls: vllm_omni.core.sched.scheduler.OmniScheduler
|
||||
worker_type: ar
|
||||
scheduler_cls: vllm_omni.core.sched.omni_ar_scheduler.OmniARScheduler
|
||||
gpu_memory_utilization: 0.8
|
||||
enforce_eager: true
|
||||
trust_remote_code: true
|
||||
@@ -89,8 +92,8 @@ stage_args:
|
||||
engine_args:
|
||||
model_stage: code2wav
|
||||
model_arch: Qwen2_5OmniForConditionalGeneration
|
||||
worker_cls: vllm_omni.worker.gpu_diffusion_worker.GPUDiffusionWorker
|
||||
scheduler_cls: vllm_omni.core.sched.diffusion_scheduler.DiffusionScheduler
|
||||
worker_type: generation
|
||||
scheduler_cls: vllm_omni.core.sched.omni_generation_scheduler.OmniGenerationScheduler
|
||||
gpu_memory_utilization: 0.15
|
||||
enforce_eager: true
|
||||
trust_remote_code: true
|
||||
@@ -168,11 +171,11 @@ The model architecture class name that is registered in `model_executor/models/r
|
||||
|
||||
#### `engine_args.worker_cls`
|
||||
|
||||
The specific worker class to use for this stage. This determines how the model computations are executed. Examples include `vllm_omni.worker.gpu_ar_worker.GPUARWorker` for autoregressive stages and `vllm_omni.worker.gpu_diffusion_worker.GPUDiffusionWorker` for diffusion-based stages.
|
||||
The specific worker class to use for this stage. This determines how the model computations are executed. Examples include `vllm_omni.worker.gpu_ar_worker.GPUARWorker` for autoregressive stages and `vllm_omni.worker.gpu_generation_worker.GPUGenerationWorker` for diffusion-based stages.
|
||||
|
||||
#### `engine_args.scheduler_cls`
|
||||
|
||||
The scheduler class to use for this stage. The scheduler manages request queuing, batching, and execution order. Examples include `vllm_omni.core.sched.scheduler.OmniScheduler` for standard stages and `vllm_omni.core.sched.diffusion_scheduler.DiffusionScheduler` for diffusion stages.
|
||||
The scheduler class to use for this stage. The scheduler manages request queuing, batching, and execution order. Examples include `vllm_omni.core.sched.omni_ar_scheduler.OmniARScheduler` for standard stages and `vllm_omni.core.sched.omni_generation_scheduler.OmniGenerationScheduler` for diffusion stages.
|
||||
|
||||
#### `engine_args.gpu_memory_utilization`
|
||||
|
||||
@@ -180,6 +183,9 @@ The fraction of GPU memory to allocate for this stage within a single GPU chip.
|
||||
|
||||
Default: `0.8`
|
||||
|
||||
!!! tip "Memory Configuration Guide"
|
||||
For detailed information on how to calculate memory requirements and properly configure `gpu_memory_utilization`, see the [GPU Memory Calculation and Configuration Guide](./gpu_memory_utilization.md).
|
||||
|
||||
#### `engine_args.enforce_eager`
|
||||
|
||||
Whether to enforce eager execution mode. When set to `true`, the engine will run in eager mode without using CUDA graphs or other compilation optimizations. Currently, vLLM-Omni only supports eager mode.
|
||||
@@ -206,7 +212,7 @@ Default: `false`
|
||||
|
||||
### `is_comprehension`
|
||||
|
||||
Whether this stage is a text or multimodal comprehension module. When set to `true`, the stage acts as a comprehension module that processes input text or multimodal content. If this is the first comprehension stage, `AsyncOmniLLM` will use its tokenizer as the default tokenizer for the entire pipeline.
|
||||
Whether this stage is a text or multimodal comprehension module. When set to `true`, the stage acts as a comprehension module that processes input text or multimodal content. If this is the first comprehension stage, `AsyncOmni` will use its tokenizer as the default tokenizer for the entire pipeline.
|
||||
|
||||
Default: `true`
|
||||
|
||||
@@ -267,3 +273,15 @@ Default: `True`
|
||||
Penalty applied to tokens that have already appeared in the generated sequence. Values greater than 1.0 discourage repetition, while values less than 1.0 encourage it. A value of 1.0 applies no penalty.
|
||||
|
||||
Default: `1.1`
|
||||
|
||||
### `tts_args` (TTS stages only)
|
||||
|
||||
Configuration for Text-to-Speech specific parameters. This section is only applicable to TTS model stages (e.g., `qwen3_tts`).
|
||||
|
||||
#### `tts_args.max_instructions_length`
|
||||
|
||||
Maximum character length for voice style/emotion instructions. Instructions exceeding this limit will be rejected with a validation error.
|
||||
|
||||
Default: `500`
|
||||
|
||||
This value can be overridden at runtime using the `--tts-max-instructions-length` CLI parameter when starting the server.
|
||||
@@ -8,8 +8,8 @@ stage_args:
|
||||
engine_args:
|
||||
model_stage: thinker
|
||||
model_arch: Qwen2_5OmniForConditionalGeneration
|
||||
worker_cls: vllm_omni.worker.gpu_ar_worker.GPUARWorker
|
||||
scheduler_cls: vllm_omni.core.sched.scheduler.OmniScheduler
|
||||
worker_type: ar
|
||||
scheduler_cls: vllm_omni.core.sched.omni_ar_scheduler.OmniARScheduler
|
||||
gpu_memory_utilization: 0.8
|
||||
enforce_eager: true # Now we only support eager mode
|
||||
trust_remote_code: true
|
||||
@@ -34,8 +34,8 @@ stage_args:
|
||||
engine_args:
|
||||
model_stage: talker
|
||||
model_arch: Qwen2_5OmniForConditionalGeneration
|
||||
worker_cls: vllm_omni.worker.gpu_ar_worker.GPUARWorker
|
||||
scheduler_cls: vllm_omni.core.sched.scheduler.OmniScheduler
|
||||
worker_type: ar
|
||||
scheduler_cls: vllm_omni.core.sched.omni_ar_scheduler.OmniARScheduler
|
||||
gpu_memory_utilization: 0.8
|
||||
enforce_eager: true
|
||||
trust_remote_code: true
|
||||
@@ -60,8 +60,8 @@ stage_args:
|
||||
engine_args:
|
||||
model_stage: code2wav
|
||||
model_arch: Qwen2_5OmniForConditionalGeneration
|
||||
worker_cls: vllm_omni.worker.gpu_diffusion_worker.GPUDiffusionWorker
|
||||
scheduler_cls: vllm_omni.core.sched.diffusion_scheduler.DiffusionScheduler
|
||||
worker_type: generation
|
||||
scheduler_cls: vllm_omni.core.sched.omni_generation_scheduler.OmniGenerationScheduler
|
||||
gpu_memory_utilization: 0.15
|
||||
enforce_eager: true
|
||||
trust_remote_code: true
|
||||
|
||||
@@ -9,7 +9,7 @@ This directory contains the source files for the vLLM-Omni documentation.
|
||||
Install documentation dependencies:
|
||||
|
||||
```bash
|
||||
pip install -e ".[docs]"
|
||||
uv pip install -e ".[docs]"
|
||||
```
|
||||
|
||||
### Build and Serve Documentation
|
||||
@@ -84,7 +84,7 @@ The documentation is automatically deployed to GitHub Pages using GitHub Actions
|
||||
```
|
||||
|
||||
3. **Documentation will be available at**:
|
||||
- `https://vllm-project.github.io/vllm-omni/`
|
||||
- `https://vllm-omni.readthedocs.io`
|
||||
|
||||
The GitHub Actions workflow (`.github/workflows/docs.yml`) will automatically:
|
||||
- Build the documentation when you push to `main` branch
|
||||
|
||||
+10
-12
@@ -2,6 +2,9 @@
|
||||
|
||||
Thank you for your interest in contributing to vLLM-Omni! This document provides guidelines and instructions for contributing.
|
||||
|
||||
!!! note
|
||||
We host weekly developer-facing online meetings to discuss milestones and updates **every Tuesday at 19:30 PDT**. Meeting link as well as the past meeting notes can be found [here](https://tinyurl.com/vllm-omni-meeting).
|
||||
|
||||
## Getting Started
|
||||
|
||||
vLLM-Omni uses `uv` as the environment manager, to create and manage Python environments. Please follow the documentation to install `uv`. After installing `uv`, you can create a new Python environment using the following commands:
|
||||
@@ -10,23 +13,18 @@ vLLM-Omni uses `uv` as the environment manager, to create and manage Python envi
|
||||
uv venv --python 3.12 --seed
|
||||
source .venv/bin/activate
|
||||
```
|
||||
### Development Environment for vLLM
|
||||
|
||||
vLLM-Omni is built on top of the stable release 0.11.0 of vLLM. You may install `vllm==0.11.0` directly or install the library from source if you need to check, modify or debug with source code of vLLM.
|
||||
### Development Environment for vLLM and vLLM-Omni
|
||||
|
||||
### Development Environment for vLLM-Omni
|
||||
|
||||
Install vLLM-Omni from source with development dependencies:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/vllm-project/vllm-omni.git
|
||||
cd vllm_omni
|
||||
uv pip install -e ".[dev]"
|
||||
```
|
||||
vLLM-Omni is quickly evolving, please see the [installation guide](../getting_started/installation/README.md) for details. It's recommended to build from source to provide the latest development environment.
|
||||
|
||||
!!! tip
|
||||
vLLM-Omni is compatible with Python versions 3.10 to 3.12. However, we recommend developing with Python 3.12 to minimize the chance of your local environment clashing with our CI environment.
|
||||
|
||||
### Adding a new model to vLLM-Omni
|
||||
|
||||
Please check [model implementation](model/README.md) for how to add diffusion and omni-modality models to vLLM-Omni.
|
||||
|
||||
### Linting
|
||||
|
||||
vLLM-Omni uses `pre-commit` to lint and format the codebase. See [pre-commit documentation](https://pre-commit.com/#usage) if `pre-commit` is new to you. Setting up `pre-commit` is as easy as:
|
||||
@@ -118,7 +116,7 @@ Only specific types of PRs will be reviewed. The PR title is prefixed appropriat
|
||||
- `[Model]` for adding a new model or improving an existing model. Model name should appear in the title.
|
||||
- `[Frontend]` For changes on the vLLM-Omni frontend (e.g., OpenAI API server, `OmniLLM` class, etc.)
|
||||
- `[Kernel]` for changes affecting CUDA kernels or other compute kernels.
|
||||
- `[Core]` for changes in the core vLLM-Omni logic (e.g., `OmniProcessor`, `OmniScheduler`, etc.)
|
||||
- `[Core]` for changes in the core vLLM-Omni logic (e.g., `OmniProcessor`, `OmniARScheduler`, etc.)
|
||||
- `[Hardware][Vendor]` for hardware-specific changes. Vendor name should appear in the prefix, such as [Ascend] for Ascend NPUs.
|
||||
- `[Misc]` for PRs that do not fit the above categories. Please use this sparingly.
|
||||
|
||||
|
||||
@@ -0,0 +1,678 @@
|
||||
# Multi-Level Automated Testing System Documentation
|
||||
|
||||
## Document Overview
|
||||
|
||||
This testing system aims to build a complete, efficient, and well-structured quality assurance framework for the development, integration, and release of model services. It draws on the concept of the test pyramid from modern software engineering, progressively expanding testing activities from basic code logic verification to complex end-to-end (E2E) functionality, performance, accuracy, and even long-term stability validation.
|
||||
|
||||
Through five levels (L1-L5) and common (Common) specifications, the system clarifies the testing objectives, scope, execution frequency, and required resources for different development stages (e.g., each commit, PR merge, daily build, pre-release). This ensures that models meet high standards for functionality, performance, and reliability across various deployment scenarios (online serving and offline inference).
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Level</th>
|
||||
<th>Scope & Focus</th>
|
||||
<th>Time Cost</th>
|
||||
<th>Test Dir</th>
|
||||
<th>Doc</th>
|
||||
<th>Frequency</th>
|
||||
<th>Hardware</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td rowspan="2"><strong>Common</strong></td>
|
||||
<td>Contribution Guideline & PR checklist</td>
|
||||
<td>/</td>
|
||||
<td>/</td>
|
||||
<td>.github/PULL_REQUEST_TEMPLATE.md <a href="../tests_style/"> Test Style (PR Checklist)</a></td>
|
||||
<td>/</td>
|
||||
<td>/</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>CI Failure Description</td>
|
||||
<td>/</td>
|
||||
<td>/</td>
|
||||
<td><a href="../failures/"> CI Failures</a></td>
|
||||
<td>/</td>
|
||||
<td>/</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>L1</strong><br>(Unit & Logic)</td>
|
||||
<td>Unit tests for components like entrypoints, models</td>
|
||||
<td rowspan="2"><15min</td>
|
||||
<td>/tests/{component_name}/test_xxx</td>
|
||||
<td>
|
||||
<a href="#chapter-1-l1-l2-level-testing-unit-testing-and-basic-end-to-end-verification">Chapter 1</a><br>
|
||||
Section 1 L1&L2: Purpose, Test Content, Directory Location, Example
|
||||
</td>
|
||||
<td>PR with ready label (also can run locally)</td>
|
||||
<td>CPU</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>L2</strong><br>(E2E across models & GPU-required UT)</td>
|
||||
<td>Online & Offline (basic deployment scenarios):<br>dummy, normal inference function (output format, stream), some instance startup UT</td>
|
||||
<td>
|
||||
/tests/e2e/online_serving/test_{model_name}.py<br>
|
||||
/tests/e2e/offline_inference/test_{model_name}.py
|
||||
</td>
|
||||
<td>
|
||||
<a href="#chapter-1-l1-l2-level-testing-unit-testing-and-basic-end-to-end-verification">Chapter 1</a><br>
|
||||
L1&L2: Purpose, Test Content, Directory Location, Example
|
||||
</td>
|
||||
<td>PR with ready label</td>
|
||||
<td>GPU</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>L3</strong><br>(Important Perf & Integration & Accuracy)</td>
|
||||
<td>Online & Offline (multiple deployment scenarios):<br>real model, normal inference function, normal accuracy</td>
|
||||
<td><30min</td>
|
||||
<td>
|
||||
/tests/e2e/online_serving/test_{model_name}_expansion.py<br>
|
||||
/tests/e2e/offline_inference/test_{model_name}_expansion.py
|
||||
</td>
|
||||
<td>
|
||||
<a href="#chapter-2-l3-level-testing-core-integration-performance-and-accuracy-verification">Chapter 2</a><br>
|
||||
L3: Purpose, Test Content, Directory Location, Example
|
||||
</td>
|
||||
<td>PR Merged (Also run L1&L2 Tests)</td>
|
||||
<td>GPU</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>L4</strong><br>(Perf & Integration & Accuracy)</td>
|
||||
<td>Online & Offline: full functional scenarios + performance test + doc test</td>
|
||||
<td><3 hour</td>
|
||||
<td>
|
||||
<strong>Full Function:</strong><br>
|
||||
/tests/e2e/online_serving/test_{model_name}_expansion.py<br>
|
||||
/tests/e2e/offline_inference/test_{model_name}_expansion.py<br>
|
||||
<strong>Performance:</strong><br>
|
||||
/tests/e2e/perf/nightly.json<br>
|
||||
<strong>Doc Test:</strong><br>
|
||||
tests/example/online_serving/test_{model_name}.py<br>
|
||||
tests/example/offline_inference/test_{model_name}.py
|
||||
</td>
|
||||
<td>
|
||||
<a href="#chapter-3-l4-level-testing-full-functionality-performance-and-documentation-testing">Chapter 3</a><br>
|
||||
L4: Purpose, Test Content, Directory Location, Example
|
||||
</td>
|
||||
<td>Nightly</td>
|
||||
<td>GPU</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>L5</strong><br>(Stability & Reliability)</td>
|
||||
<td>Online & Offline: long-term stability test + reliability test</td>
|
||||
<td> Depends on reality </td>
|
||||
<td>
|
||||
<strong>Stability:</strong><br>
|
||||
tests/e2e/stability/weekly.json<br>
|
||||
<strong>Reliability:</strong><br>
|
||||
tests/e2e/reliability/test_{model_name}.py
|
||||
</td>
|
||||
<td>
|
||||
<a href="#chapter-4-l5-level-testing-stability-and-reliability-testing">Chapter 4</a><br>
|
||||
L5: Purpose, Test Content, Directory Location, Example
|
||||
</td>
|
||||
<td>Weekly / Days before Release</td>
|
||||
<td>GPU</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
||||
---
|
||||
<details>
|
||||
<summary> The folder structure for tests file based on the 5 levels design</summary>
|
||||
Legend: `✅` = test exists, `⬜` = suggested to add.
|
||||
```
|
||||
vllm_omni/ tests/
|
||||
├── config/ → ├── config/
|
||||
│ ├── model.py │ └── test_model.py ⬜
|
||||
│ └── lora.py │ └── test_lora.py ⬜
|
||||
│
|
||||
├── core/ → ├── core/
|
||||
│ └── sched/ │ └── sched/
|
||||
│ ├── omni_ar_scheduler.py │ ├── test_omni_ar_scheduler.py ⬜
|
||||
│ ├── omni_generation_scheduler.py │ ├── test_omni_generation_scheduler.py ⬜
|
||||
│ └── output.py │ └── test_output.py ✅ currently in entrypoints/test_omni_new_request_data.py (tests output.OmniNewRequestData)
|
||||
│
|
||||
├── diffusion/ → ├── diffusion/
|
||||
│ ├── diffusion_engine.py │ ├── test_diffusion_engine.py ⬜
|
||||
│ ├── attention/ │ ├── attention/
|
||||
│ │ ├── layer.py │ │ ├── test_attention_sp.py ✅
|
||||
│ │ └── backends/ │ │ └── test_flash_attn.py ✅
|
||||
│ ├── distributed/ │ ├── distributed/
|
||||
│ │ └── ... │ │ ├── test_comm.py ✅
|
||||
│ │ │ │ ├── test_cfg_parallel.py ✅
|
||||
│ │ │ │ └── test_sp_plan_hooks.py ✅
|
||||
│ ├── lora/ │ ├── lora/
|
||||
│ │ └── ... │ │ ├── test_base_linear.py ✅
|
||||
│ │ │ │ └── test_lora_manager.py ✅
|
||||
│ ├── models/ │ ├── models/
|
||||
│ │ ├── qwen_image/ │ │ ├── qwen_image/ (e2e coverage)
|
||||
│ │ ├── z_image/ │ │ └── z_image/
|
||||
│ │ └── ... │ │ └── test_zimage_tp_constraints.py ✅
|
||||
│ └── worker/ │ └── worker/
|
||||
│ ├── diffusion_worker.py │ └── test_diffusion_worker.py ✅ file at diffusion/test_diffusion_worker.py
|
||||
│ └── diffusion_model_runner.py │
|
||||
│
|
||||
├── distributed/ → ├── distributed/
|
||||
│ └── omni_connectors/ │ └── omni_connectors/
|
||||
│ ├── adapter.py │ ├── test_adapter_and_flow.py ✅
|
||||
│ ├── kv_transfer_manager.py │ ├── test_basic_connectors.py ✅
|
||||
│ ├── connectors/ │ ├── test_kv_flow.py ✅
|
||||
│ └── utils/ │ └── test_omni_connector_configs.py ✅
|
||||
│
|
||||
├── engine/ → ├── engine/
|
||||
│ ├── input_processor.py │ ├── test_input_processor.py ⬜ (no processor.py in source)
|
||||
│ ├── output_processor.py │ └── test_output_processor.py ⬜
|
||||
│ └── arg_utils.py │ └── test_arg_utils.py ⬜
|
||||
│
|
||||
├── entrypoints/ → ├── entrypoints/
|
||||
│ ├── omni.py │ ├── test_omni.py ⬜ (E2E covered by e2e/offline, e2e/online)
|
||||
│ ├── omni_llm.py │ ├── test_omni_llm.py ✅
|
||||
│ ├── omni_stage.py │ ├── test_omni_stage.py ⬜ (partial in test_omni_stage_diffusion_config.py)
|
||||
│ ├── omni_diffusion.py │ ├── test_omni_diffusion.py ✅
|
||||
│ ├── async_omni.py │ ├── test_async_omni.py ✅ actually in e2e/online_serving/test_async_omni.py
|
||||
│ ├── async_omni_diffusion.py │ ├── test_async_omni_diffusion_config.py ✅
|
||||
│ ├── stage_utils.py │ ├── test_stage_utils.py ✅
|
||||
│ ├── cli/ │ ├── cli/ (benchmarks/test_serve_cli.py covers CLI serve)
|
||||
│ │ └── ... │ │ └── test_*.py ⬜
|
||||
│ └── openai/ │ └── openai_api/ # maps to entrypoints/openai/
|
||||
│ ├── api_server.py │ ├── test_api_server.py ⬜ (e2e indirect coverage)
|
||||
│ ├── serving_chat.py │ ├── test_serving_chat_sampling_params.py ✅
|
||||
│ ├── serving_speech.py │ ├── test_serving_speech.py ✅
|
||||
│ └── image_api_utils.py │ └── test_image_server.py ✅
|
||||
│
|
||||
├── inputs/ → ├── inputs/
|
||||
│ ├── data.py │ ├── test_data.py ⬜
|
||||
│ ├── parse.py │ ├── test_parse.py ⬜
|
||||
│ └── preprocess.py │ └── test_preprocess.py ✅ currently in entrypoints/test_omni_input_preprocessor.py
|
||||
│
|
||||
├── model_executor/ → ├── model_executor/
|
||||
│ ├── layers/ │ ├── layers/
|
||||
│ │ └── mrope.py │ │ └── test_mrope.py ⬜
|
||||
│ ├── model_loader/ │ ├── model_loader/
|
||||
│ │ └── weight_utils.py │ │ └── test_weight_utils.py ⬜
|
||||
│ ├── models/ │ ├── models/
|
||||
│ │ ├── qwen2_5_omni/ │ │ ├── qwen2_5_omni/
|
||||
│ │ │ ├── qwen2_5_omni_thinker.py │ │ │ ├── test_audio_length.py ✅
|
||||
│ │ │ ├── qwen2_5_omni_talker.py │ │ │ ├── test_qwen2_5_omni_thinker.py ⬜
|
||||
│ │ │ └── qwen2_5_omni_token2wav.py │ │ │ ├── test_qwen2_5_omni_talker.py ⬜
|
||||
│ │ └── qwen3_omni/ │ │ │ └── test_qwen2_5_omni_token2wav.py ⬜
|
||||
│ │ └── ... │ │ └── qwen3_omni/
|
||||
│ ├── stage_configs/ │ │ └── test_*.py ⬜
|
||||
│ │ └── *.yaml │ └── stage_configs/ (used by e2e, test_*.py can be added) ⬜
|
||||
│ └── stage_input_processors/ │ └── stage_input_processors/
|
||||
│ └── ... │ └── test_*.py ⬜
|
||||
│
|
||||
├── sample/ → ├── sample/
|
||||
│ └── __init__.py │ └── test_*.py ⬜
|
||||
│
|
||||
├── utils/ → ├── utils/
|
||||
│ └── __init__.py │ └── test_*.py ⬜ (no platform_utils.py currently)
|
||||
│
|
||||
├── worker/ → ├── worker/
|
||||
│ ├── gpu_ar_model_runner.py │ ├── test_gpu_ar_model_runner.py ⬜
|
||||
│ ├── gpu_ar_worker.py │ ├── test_gpu_ar_worker.py ⬜
|
||||
│ ├── gpu_generation_model_runner.py │ ├── test_gpu_generation_model_runner.py ✅
|
||||
│ ├── gpu_generation_worker.py │ ├── test_gpu_generation_worker.py ⬜
|
||||
│ ├── gpu_model_runner.py │ ├── test_omni_gpu_model_runner.py ✅
|
||||
│ └── mixins.py │ └── (npu under platforms/npu/worker/) # not worker/npu/
|
||||
│
|
||||
├── platforms/ → (no tests/platforms/, e2e and stage_configs provide indirect coverage)
|
||||
│ ├── cuda/
|
||||
│ ├── npu/worker/ # NPU worker here, not vllm_omni/worker/npu/
|
||||
│ ├── rocm/
|
||||
│ └── xpu/worker/
|
||||
│
|
||||
├── outputs.py → test_outputs.py ✅ (at tests root)
|
||||
├── (logger, patch, request, version) → (no corresponding unit test)
|
||||
│
|
||||
└── e2e (tests side only) → ├── e2e/
|
||||
├── online_serving/ ✅ non-empty
|
||||
│ ├── test_async_omni.py
|
||||
│ ├── test_qwen3_omni.py
|
||||
│ ├── test_qwen3_omni_expansion.py
|
||||
│ ├── test_image_gen_edit.py
|
||||
│ ├── test_images_generations_lora.py
|
||||
│ └── stage_configs/
|
||||
└── offline_inference/ ✅
|
||||
├── test_qwen2_5_omni.py
|
||||
├── test_qwen3_omni.py
|
||||
├── test_bagel_text2img.py
|
||||
├── test_t2i_model.py
|
||||
├── test_t2v_model.py
|
||||
├── test_ovis_image.py
|
||||
├── test_zimage_tensor_parallel.py
|
||||
├── test_cache_dit.py
|
||||
├── test_teacache.py
|
||||
├── test_stable_audio_model.py
|
||||
├── test_diffusion_cpu_offload.py
|
||||
├── test_diffusion_layerwise_offload.py
|
||||
├── test_diffusion_lora.py
|
||||
├── test_sequence_parallel.py
|
||||
└── stage_configs/
|
||||
├── qwen2_5_omni_ci.yaml
|
||||
├── qwen3_omni_ci.yaml
|
||||
├── bagel_*.yaml
|
||||
└── npu/, rocm/, etc.
|
||||
```
|
||||
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
## Common Specifications
|
||||
|
||||
Before entering specific testing levels, the project establishes two common specifications aimed at standardizing the development process and quickly locating issues.
|
||||
|
||||
1. ***PR Checklist ([Tests Style](../ci/tests_style.md))***: This template defines the self-check items that must be completed before submitting a code review (Pull Request). It ensures that each code change meets basic requirements such as code style, dependency updates, and documentation synchronization before entering the automated testing pipeline, serving as the first manual line of defense for quality assurance.
|
||||
2. ***CI Failure Explanation ([CI Failures](../ci/failures.md))***: This document archives and explains common failure patterns in the Continuous Integration (CI) pipeline, error log interpretation, and preliminary troubleshooting steps. It helps developers and testers quickly diagnose the causes of automated test failures, improving problem-solving efficiency.
|
||||
|
||||
## Chapter 1: L1 & L2 Level Testing - Unit Testing and Basic End-to-End Verification
|
||||
|
||||
### 1.1 Testing Purpose
|
||||
|
||||
L1 and L2 level testing form the foundation of the quality assurance system. L1 level testing focuses on verifying the internal logic correctness of code units (e.g., functions, classes), ensuring each independent component behaves as designed.
|
||||
|
||||
L2 level testing builds upon L1 by introducing GPU resources and verifying that the end-to-end (E2E) process of the model in basic deployment scenarios is smooth. For example, it uses dummy models to confirm that core interfaces like the inference pipeline, output format, and streaming response work properly. The common goal of these two levels is to provide developers with rapid feedback, discovering and fixing issues early in the development cycle .
|
||||
|
||||
|
||||
|
||||
### 1.2 Testing Content and Scope
|
||||
|
||||
- ***L1 (Unit & Logic Testing)***:
|
||||
- - ***Scope***: Tests internal functions and methods of core components such as `entrypoints`, `models`.
|
||||
- ***Focus***: Branch coverage, exception handling, algorithm logic correctness. Does not involve external dependencies or the complete service stack.
|
||||
- ***Time Cost***: Execution time is controlled within ***15 minutes*** to ensure fast feedback.
|
||||
- ***L2 (Basic End-to-End Testing)***:
|
||||
- - ***Scope***: Covers two basic deployment scenarios: `online` (serving) and `offline` (inference).
|
||||
- ***Focus***: Uses `dummy` models or lightweight real models to verify that the entire chain from request input to result output works normally, including output data structure, streaming (stream) support, etc. Also includes some unit tests that require launching independent service instances.
|
||||
- ***Characteristic***: Requires ***GPU*** resources to perform model computations.
|
||||
|
||||
### 1.3 Test Directory and Execution Files
|
||||
|
||||
A clear directory structure is key to managing test cases efficiently.
|
||||
|
||||
- ***L1 Test Directory***: `/tests/{component_name}/test_xxx.py`
|
||||
- - Here, `{component_name}` corresponds to modules in the source code, such as `distributed`, `entrypoints`, etc., and `test_xxx.py` is the specific test file.
|
||||
- ***L2 Test Directory***:
|
||||
- - Online Serving: `/tests/e2e/online_serving/test_{model_name}.py`
|
||||
- Offline Inference: `/tests/e2e/offline_inference/test_{model_name}.py`
|
||||
|
||||
### 1.4 Execution Method and Example
|
||||
|
||||
- ***Trigger Timing***: **`PR with ready label`**. That is, when a developer adds a "ready for review" or similar label to a PR on platforms like GitHub, L1 and L2 tests are automatically triggered.
|
||||
- ***Execution Environment***: L1 uses ***CPU*** environment; L2 requires ***GPU*** environment.
|
||||
- ***Script Example***:
|
||||
|
||||
<details>
|
||||
<summary> L1 Test Examples</summary>
|
||||
|
||||
Examples from `tests/model_executor/models/qwen2_5_omni/test_audio_length.py`
|
||||
```python
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
import pytest
|
||||
|
||||
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
|
||||
|
||||
def test_resolve_max_mel_frames_default():
|
||||
from vllm_omni.model_executor.models.qwen2_5_omni.audio_length import resolve_max_mel_frames
|
||||
|
||||
assert resolve_max_mel_frames(None, default=30000) == 30000
|
||||
assert resolve_max_mel_frames(None, default=6000) == 6000
|
||||
|
||||
|
||||
def test_resolve_max_mel_frames_explicit():
|
||||
from vllm_omni.model_executor.models.qwen2_5_omni.audio_length import resolve_max_mel_frames
|
||||
|
||||
# Explicit argument always wins over default
|
||||
assert resolve_max_mel_frames(123, default=30000) == 123
|
||||
assert resolve_max_mel_frames(6000, default=30000) == 6000
|
||||
assert resolve_max_mel_frames(0, default=30000) == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("repeats", [2, 4])
|
||||
@pytest.mark.parametrize("code_len", [0, 1, 32768])
|
||||
@pytest.mark.parametrize("max_mel_frames", [None, -1, 0, 1, 6000, 30000])
|
||||
def test_cap_and_align_mel_length_no_mismatch(repeats, code_len, max_mel_frames):
|
||||
"""Guard that any max_mel_frames yields a mel length aligned to repeats, and
|
||||
consistent with the truncated code length (prevents concat mismatch).
|
||||
"""
|
||||
from vllm_omni.model_executor.models.qwen2_5_omni.audio_length import cap_and_align_mel_length
|
||||
|
||||
target_code_len, target_mel_len = cap_and_align_mel_length(
|
||||
code_len=code_len,
|
||||
repeats=repeats,
|
||||
max_mel_frames=max_mel_frames,
|
||||
)
|
||||
|
||||
assert isinstance(target_code_len, int)
|
||||
assert isinstance(target_mel_len, int)
|
||||
|
||||
if code_len == 0:
|
||||
assert target_code_len == 0
|
||||
assert target_mel_len == 0
|
||||
return
|
||||
|
||||
assert target_code_len >= 1
|
||||
assert target_mel_len >= repeats
|
||||
assert target_mel_len % repeats == 0
|
||||
assert target_mel_len == target_code_len * repeats
|
||||
assert target_code_len <= code_len
|
||||
|
||||
if max_mel_frames is not None and int(max_mel_frames) > 0 and int(max_mel_frames) >= repeats:
|
||||
assert target_mel_len <= int(max_mel_frames)
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary> L2 Test Examples</summary>
|
||||
You can refer to Test Examples in Chapter 2 to see example test cases that incorporate both L2 and L3 testing logic.
|
||||
</details>
|
||||
|
||||
- - ***Run Command***:
|
||||
|
||||
`pytest -s -v /tests/e2e/online_serving/test_{model_name}.py`
|
||||
`pytest -s -v -m 'core_model and cpu' --run-level=core_model`
|
||||
|
||||
## Chapter 2: L3 Level Testing - Core Integration, Performance, and Accuracy Verification
|
||||
|
||||
### 2.1 Testing Purpose
|
||||
|
||||
L3 level testing executes after code is merged into the main branch. Its core purpose is to verify the integration effect, key performance indicators, and output accuracy of ***real models*** in ***multiple deployment scenarios***
|
||||
|
||||
. It acts as the "quality gatekeeper" for the main branch, ensuring that no merge breaks the core capabilities of the model service. Testing needs to provide clear conclusions within a relatively short time (<30min), balancing test depth with feedback speed.
|
||||
|
||||
|
||||
|
||||
### 2.2 Testing Content and Scope
|
||||
|
||||
- ***Deployment Scenarios***: Covers richer `online` and `offline` deployment configurations, which may include different hardware configurations, batch sizes, concurrency levels, etc.
|
||||
- ***Core Verification***:
|
||||
- 1. ***Inference Functionality***: Ensures real models can perform forward computation normally and return results.
|
||||
2. ***Accuracy Compliance***: Verifies that the model's evaluation metrics (e.g., accuracy) meet the expected baseline, preventing code changes from introducing accuracy issues.
|
||||
3. ***Important Performance***: Verifies whether performance (e.g., P99 latency, throughput) in core scenarios meets preset thresholds.
|
||||
|
||||
### 2.3 Test Directory and Execution Files
|
||||
|
||||
- ***Functional Testing***:
|
||||
- - Online Serving: `/tests/e2e/online_serving/test_{model_name}_expansion.py`
|
||||
- Offline Inference: `/tests/e2e/offline_inference/test_{model_name}_expansion.py`
|
||||
- (Note: `_expansion.py` likely means it contains more comprehensive scenario cases compared to L2 tests).
|
||||
|
||||
### 2.4 Execution Method and Example
|
||||
|
||||
- ***Trigger Timing***: **`PR Merged`**. Automatically triggered after code review is approved and merged into the main branch.
|
||||
- ***Execution Environment***: ***GPU*** servers.
|
||||
- ***Script Example***:
|
||||
|
||||
???+ example "Test Examples"
|
||||
|
||||
**2.4.1 Mark Declaration Section**
|
||||
|
||||
```python
|
||||
@pytest.mark.advanced_model
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.parametrize("omni_server", test_params, indirect=True)
|
||||
```
|
||||
|
||||
**Explanation**:
|
||||
|
||||
@pytest.mark.advanced_model: Marks the test as L3 or L4 level, indicating that this test case performs deep validation, using real models for performance, integration, and accuracy testing. This forms a "basic-advanced" correspondence with the core_model mark at the L2 level.
|
||||
|
||||
@pytest.mark.core_model: Marks the test as L1 or L2 level, indicating that this test case validates the basic functionality of the core model. It uses mock weights and only checks if the relevant interface functions correctly.
|
||||
|
||||
@pytest.mark.parametrize: A parameterization decorator that allows abstracting test data into parameters, enabling reuse of the same test logic across different data configurations. indirect=True indicates that parameters will be passed to the fixture for processing.
|
||||
|
||||
**Notes**: If you believe the test case only needs to execute basic run logic at the PR-level CI, you can mark it only with @pytest.mark.core_model. If you believe it only needs to execute deep validation run logic at the merge or nightly level, you can mark it only with @pytest.mark.advanced_model. If you believe the test case needs to accommodate both basic run and deep validation test logic, you should mark it with both @pytest.mark.core_model and @pytest.mark.advanced_model.
|
||||
|
||||
**2.4.2 Test Function Definition and Documentation**
|
||||
|
||||
```python
|
||||
def test_mix_to_text_audio_001(omni_server, openai_client) -> None:
|
||||
"""
|
||||
Test multi-modal input processing and text/audio output generation via OpenAI API.
|
||||
Deploy Setting: default yaml
|
||||
Input Modal: text + audio + video + image
|
||||
Output Modal: text + audio
|
||||
Input Setting: stream=True
|
||||
Datasets: single request
|
||||
"""
|
||||
```
|
||||
|
||||
**Explanation**:
|
||||
|
||||
**Function Naming Convention**: Uses the test_ prefix, describes the test scenario mix_to_text_audio, and the number 001 indicates the first test case for this scenario.
|
||||
|
||||
**Parameter Explanation**:
|
||||
|
||||
omni_server: Omni server instance obtained via fixture, containing model information and configuration.
|
||||
|
||||
openai_client: Unified OpenAI client processing instance, encapsulating request sending and response validation logic.
|
||||
|
||||
Docstring: Describes the test purpose, deployment settings, input/output modalities, streaming settings, and dataset type in detail, providing clear context for test maintenance.
|
||||
|
||||
**2.4.3 Multimodal Data Preparation**
|
||||
|
||||
```python
|
||||
video_data_url = f"data:video/mp4;base64,{generate_synthetic_video(224, 224, 300)['base64']}"
|
||||
image_data_url = f"data:image/jpeg;base64,{generate_synthetic_image(224, 224)['base64']}"
|
||||
audio_data_url = f"data:audio/wav;base64,{generate_synthetic_audio(5, 1)['base64']}"
|
||||
```
|
||||
|
||||
**Explanation**:
|
||||
|
||||
**Data Generation Functions**: Use the generate_synthetic_* series of functions to generate synthetic test data, avoiding reliance on external resources and ensuring test reproducibility and stability.
|
||||
|
||||
**Parameter Explanation**:
|
||||
|
||||
Video: width, height, duration_frames
|
||||
|
||||
Image: width, height
|
||||
|
||||
Audio: duration_seconds, channels
|
||||
|
||||
**2.4.4 Request Configuration and Keyword Validation**
|
||||
|
||||
```python
|
||||
request_config = {
|
||||
"model": omni_server.model,
|
||||
"messages": messages,
|
||||
"stream": True,
|
||||
"key_words": {
|
||||
"audio": ["water", "cricket"],
|
||||
"video": ["sphere", "globe", "circle", "round"],
|
||||
"image": ["square", "quadrate"],
|
||||
"text": ["beijing"]
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
**Explanation**:
|
||||
|
||||
**Model Specification**: Uses omni_server.model to ensure the test aligns with the model configured on the server.
|
||||
|
||||
**Keyword Validation Mechanism**: This is an innovative design of the template to address the specific needs of multimodal testing:
|
||||
|
||||
Audio Keywords: Validate whether the generated text's description of audio content contains expected elements (e.g., "water" for water sounds, "cricket" for cricket sounds). If you provide multiple keywords, the validation is considered successful if at least one keyword is present.
|
||||
|
||||
**Video Keywords**: Validate whether the generated text's description of video content contains expected elements. If you provide multiple keywords, the validation is considered successful if at least one keyword is present.
|
||||
|
||||
Image Keywords: Validate whether the generated text's description of image content contains expected elements. If you provide multiple keywords, the validation is considered successful if at least one keyword is present.
|
||||
|
||||
Text Keywords: Validate whether the generated text contains expected elements. If you provide multiple keywords, the validation is considered successful if at least one keyword is present.
|
||||
|
||||
**2.4.5 Request Execution**
|
||||
|
||||
```python
|
||||
openai_client.send_request(request_config, request_num=1)
|
||||
```
|
||||
|
||||
**Explanation**:
|
||||
|
||||
**Unified Client**: Uses the OpenAIClientHandler instance to send requests. This client encapsulates error handling, retry mechanisms, and response validation logic.
|
||||
|
||||
**Single Request**: The comment clearly states this is a single-request completion test. For concurrent testing, it can be extended to multiple requests using request_num = n.
|
||||
|
||||
**Implicit Validation**: The send_request method internally includes validation logic dynamically selected based on the --run-level parameter: core_model performs basic validation, while advanced_model performs deep validation.
|
||||
|
||||
- ***Run Command***: `pytest -s -v /tests/e2e/online_serving/test_{model_name}.py -m advanced_model --run-level=advanced_model`
|
||||
|
||||
## Chapter 3: L4 Level Testing - Full Functionality, Performance, and Documentation Testing
|
||||
|
||||
### 3.1 Testing Purpose
|
||||
|
||||
L4 level testing is a comprehensive quality audit before a version release. It expands upon L3, executing ***full*** functional scenarios, conducting systematic ***performance stress tests***, and simultaneously verifying the correctness of accompanying ***example documentation***. Its purpose is to perform deep validation of the system during off-peak nighttime hours, providing quality trend reports for daytime development and data support for release decisions.
|
||||
|
||||
|
||||
|
||||
### 3.2 Testing Content and Scope
|
||||
|
||||
- ***Full Functionality Testing***: Executes all test cases defined in `test_{model_name}_expansion.py`, covering all implemented features, positive flows, boundary conditions, and exception handling.
|
||||
- ***Performance Testing***: Uses the `/tests/e2e/perf/nightly.json` configuration file to drive performance testing tools for stress, load, and endurance tests, collecting metrics like throughput, response time, and resource utilization.
|
||||
- ***Documentation Testing***: Verifies whether the example code provided to users is runnable and its results match the description.
|
||||
|
||||
### 3.3 Test Directory and Execution Files
|
||||
|
||||
- ***Functional Testing***: Same directories as L3.
|
||||
- ***Performance Test Configuration***: `/tests/e2e/perf/nightly.json`
|
||||
- ***Documentation Example Tests***:
|
||||
- - `tests/example/online_serving/test_{model_name}.py`
|
||||
- `tests/example/offline_inference/test_{model_name}.py`
|
||||
|
||||
### 3.4 Execution Method and Example
|
||||
|
||||
- ***Trigger Timing***: **`Nightly`**, automatically executed every night.
|
||||
- ***Execution Environment***: ***GPU*** server clusters to meet the resource demands of performance testing.
|
||||
- ***Script Example***:
|
||||
<details>
|
||||
<summary> Test Examples</summary>
|
||||
When you want to add L4-level performance test cases, you can refer to the following format for case addition in tests/perf/tests/test.json:
|
||||
|
||||
```JSON
|
||||
{
|
||||
"test_name": "test_qwen3_omni",
|
||||
"server_params": {
|
||||
"model": "Qwen/Qwen3-Omni-30B-A3B-Instruct",
|
||||
"stage_config_name": "qwen3_omni.yaml"
|
||||
},
|
||||
"benchmark_params": [
|
||||
{
|
||||
"dataset_name": "random",
|
||||
"num_prompts": [10, 20],
|
||||
"request_rate": [0.5, 1],
|
||||
"random_input_len": 2500,
|
||||
"random_output_len": 900,
|
||||
"ignore_eos": true,
|
||||
"percentile-metrics": "ttft,tpot,itl,e2el,audio_rtf,audio_ttfp,audio_duration",
|
||||
"baseline": {
|
||||
"mean_ttft_ms": 100000,
|
||||
"mean_audio_ttfp_ms": 100000,
|
||||
"mean_audio_rtf": 100000
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### Parameter Explanation
|
||||
|
||||
***Overview***
|
||||
|
||||
| Field | Required | Description |
|
||||
| ---------------- | -------- | --------------------------------------------------------------- |
|
||||
| test_name | Yes | Unique identifier for the test case |
|
||||
| server_params | Yes | Server-side configuration parameters |
|
||||
| benchmark_params | Yes | Benchmark running parameters (supports multiple configurations) |
|
||||
|
||||
#### server_params Configuration
|
||||
|
||||
##### Basic Parameters
|
||||
|
||||
| Parameter | Required | Example | Description |
|
||||
| ----------------- | -------- | ---------------------------------- | ----------------------------- |
|
||||
| model | Yes | "Qwen/Qwen3-Omni-30B-A3B-Instruct" | Model name or path |
|
||||
| stage_config_name | Yes | "qwen3_omni.yaml" | Stage configuration file name |
|
||||
|
||||
##### Dynamic Configuration (update/delete)
|
||||
|
||||
Supports incremental modifications based on the basic configuration:
|
||||
|
||||
| Operation | Description |
|
||||
| --------- | ------------------------------------ |
|
||||
| update | Update or add configuration items |
|
||||
| delete | Delete specified configuration items |
|
||||
|
||||
***Example***:
|
||||
```
|
||||
"update": {
|
||||
"async_chunk": true, // Enable asynchronous chunk processing
|
||||
"stage_args": {
|
||||
"0": {
|
||||
"engine_args.custom_process_next_stage_input_func": "vllm_omni.model_executor.stage_input_processors.qwen3_omni.thinker2talker_async_chunk"
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"stage_args": {
|
||||
"2": ["custom_process_input_func"] // Delete this configuration for stage 2
|
||||
}
|
||||
}
|
||||
```
|
||||
#### benchmark_params Configuration
|
||||
|
||||
You can add any benchmark running parameters you need here. For all optional parameters, refer to the [benchmark documentation](https://github.com/vllm-project/vllm-omni/blob/main/docs/cli/bench/serve.md). General modifications are as follows:
|
||||
|
||||
1. Change the ---xxx-xx-xx running parameters to xxx_xx_xx format and fill them as keys in the JSON file.
|
||||
2. For boolean variables in the running parameters, modify them to forms such as ignore_eos: true/false and fill them into the JSON file.
|
||||
3. Add the baseline parameter to specify the required validation values, ensuring the validation metric names match those in the result.json generated by the benchmark.
|
||||
4. The qps and concurrency modes are mutually exclusive. For detailed explanations, see the table below:
|
||||
|
||||
| Parameter | Type | Required | Example/Values | Description |
|
||||
| --------------- | ----------- | -------- | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| num_prompts | int / array | Yes | 10,[10, 20, 30] | Number of requests. Supports single values or arrays. If a single value is used, it will be automatically expanded to match the number of qps or max_concurrency, e.g., [10,10,10]. If an array is used, its length must match the number of qps or max_concurrency. |
|
||||
| request_rate | int / array | No | 1, [1, 2, 3] | Queries per second. Supports single values or arrays. If a single value is used, it will be automatically expanded to match the number of num_prompts, e.g., [1,1,1]. If an array is used, its length must match the number of num_prompts. |
|
||||
| max_concurrency | int / array | No | 1, [1, 2, 3] | Queries per second. Supports single values or arrays. If a single value is used, it will be automatically expanded to match the number of num_prompts, e.g., [1,1,1]. If an array is used, its length must match the number of num_prompts. |
|
||||
</details>
|
||||
|
||||
- - ***Run Command***: (Specific commands would depend on the performance testing tool and configuration defined in `nightly.json`).
|
||||
|
||||
## Chapter 4: L5 Level Testing - Stability and Reliability Testing
|
||||
|
||||
### 4.1 Testing Purpose
|
||||
|
||||
L5 level testing focuses on the performance of model services under ***long-running*** and ***abnormal fault*** scenarios. It aims to uncover deep-seated issues that only manifest under sustained pressure or extreme conditions, such as memory leaks, resource contention, gradual performance degradation, and lack of fault tolerance mechanisms. This is the final, yet crucial, line of defense for ensuring service high availability and production environment robustness.
|
||||
|
||||
|
||||
|
||||
### 4.2 Testing Content and Scope
|
||||
|
||||
- ***Long-term Stability (Stability) Testing***: Uses the `tests/e2e/stability/weekly.json` configuration to run the service under moderate load for an extended period (e.g., over 12 hours), monitoring whether metrics like memory/VRAM usage, response time, and throughput degrade over time, and whether the service process remains stable.
|
||||
- ***Reliability Testing***: Uses `tests/e2e/reliability/test_{model_name}.py` to actively simulate various fault and abnormal scenarios, such as: dependent service interruption, abnormal input data, network flicker, hardware resource preemption, etc., to verify the system's fault tolerance, self-healing, and graceful degradation capabilities.
|
||||
|
||||
### 4.3 Test Directory and Execution Files
|
||||
|
||||
- ***Stability Test Configuration***: `tests/e2e/stability/weekly.json`
|
||||
- ***Reliability Test Suite***: `tests/e2e/reliability/test_{model_name}.py`
|
||||
|
||||
### 4.4 Execution Method and Example
|
||||
|
||||
- ***Trigger Timing***: **`Weekly`** (weekly) or **`Days before Release`** (several days before a major release). Due to long execution times, the frequency is lower.
|
||||
- ***Execution Environment***: ***GPU*** servers, requiring a stable and exclusive testing environment.
|
||||
- ***Script Example***:
|
||||
<details>
|
||||
<summary> Test Examples</summary>
|
||||
```python
|
||||
# WIP
|
||||
```
|
||||
</details>
|
||||
|
||||
- - ***Stability***: (Execution would be driven by the configuration in `weekly.json`).
|
||||
- ***Reliability***: `pytest -s -v tests/e2e/reliability/test_{model_name}.py`
|
||||
|
||||
## Summary
|
||||
|
||||
This multi-level testing system achieves continuous, progressive validation of model service quality by tightly integrating testing activities with the development workflow (commit, review, merge, release). From rapid unit testing to comprehensive end-to-end testing, and further to in-depth performance, stability, and reliability verification, each level has clear objectives, collectively building a robust quality protection net. By following this system, teams can deliver high-quality, highly reliable model services more efficiently.
|
||||
@@ -1,4 +1,10 @@
|
||||
# CI Failures
|
||||
|
||||
What should I do when a CI job fails on my PR, but I don't think my PR caused
|
||||
the failure?
|
||||
What should I do when a CI job fails on my PR, but I don't think my PR caused the failure?
|
||||
|
||||
|
||||
## Common Case 1:
|
||||
|
||||
🚨 Error: The command was interrupted by a signal: signal: terminated
|
||||
|
||||
Reason: The test is terminated due to exceed the time limits. Sometimes rebuild the test will pass.
|
||||
@@ -0,0 +1,157 @@
|
||||
# Markers for Tests
|
||||
|
||||
By adding markers before test functions, tests can later be executed uniformly by simply declaring the corresponding marker type.
|
||||
|
||||
## Current Markers
|
||||
Defined in `pyproject.toml`:
|
||||
|
||||
| Marker | Description |
|
||||
| ------------------ | --------------------------------------------------------- |
|
||||
| `core_model` | L1&L2 tests (run in each PR) |
|
||||
| `advanced_model` | L3&L4 level tests (run in each merge or nightly) |
|
||||
| `diffusion` | Diffusion model tests |
|
||||
| `omni` | Omni model tests |
|
||||
| `cache` | Cache backend tests |
|
||||
| `parallel` | Parallelism/distributed tests |
|
||||
| `cpu` | Tests that run on CPU |
|
||||
| `gpu` | Tests that run on GPU * |
|
||||
| `cuda` | Tests that run on CUDA * |
|
||||
| `rocm` | Tests that run on AMD/ROCm * |
|
||||
| `npu` | Tests that run on NPU/Ascend * |
|
||||
| `H100` | Tests that require H100 GPU * |
|
||||
| `L4` | Tests that require L4 GPU * |
|
||||
| `MI325` | Tests that require MI325 GPU (AMD/ROCm) * |
|
||||
| `A2` | Tests that require A2 NPU * |
|
||||
| `A3` | Tests that require A3 NPU * |
|
||||
| `distributed_cuda` | Tests that require multi cards on CUDA platform * |
|
||||
| `distributed_rocm` | Tests that require multi cards on ROCm platform * |
|
||||
| `distributed_npu` | Tests that require multi cards on NPU platform * |
|
||||
| `skipif_cuda` | Skip if the num of CUDA cards is less than the required * |
|
||||
| `skipif_rocm` | Skip if the num of ROCm cards is less than the required * |
|
||||
| `skipif_npu` | Skip if the num of NPU cards is less than the required * |
|
||||
| `slow` | Slow tests (may skip in quick CI) |
|
||||
| `benchmark` | Benchmark tests |
|
||||
|
||||
\* Means those markers are auto-added, and they will be added by the `@hardware_test` decorator.
|
||||
|
||||
### Example usage for markers
|
||||
|
||||
```python
|
||||
from tests.utils import hardware_test
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.omni
|
||||
@hardware_test(
|
||||
res={"cuda": "L4", "rocm": "MI325", "npu": "A2"},
|
||||
num_cards=2,
|
||||
)
|
||||
@pytest.mark.parametrize("omni_server", test_params, indirect=True)
|
||||
def test_video_to_audio()
|
||||
...
|
||||
```
|
||||
### Decorator: `@hardware_test`
|
||||
|
||||
This decorator is intended to make hardware-aware, cross-platform test authoring easier and more robust for CI/CD environments. The `hardware_test` decorator in `vllm-omni/tests/utils.py` performs the following actions:
|
||||
|
||||
1. **Applies platform and resource markers**
|
||||
Adds the appropriate pytest markers for each specified hardware platform (e.g., `cuda`, `rocm`, `npu`) and resource type (e.g., `L4`, `H100`, `MI325`, `A2`, `A3`).
|
||||
```
|
||||
@pytest.mark.cuda
|
||||
@pytest.mark.L4
|
||||
```
|
||||
2. **Handles multi-card (distributed) scenarios**
|
||||
For tests requiring multiple cards, it automatically adds distributed markers such as `distributed_cuda`, `distributed_rocm`, or `distributed_npu`.
|
||||
```
|
||||
@pytest.mark.distributed_cuda(num_cards=num_cards)
|
||||
```
|
||||
3. **Supports flexible card requirements**
|
||||
Accepts `num_cards` as either a single integer for all platforms or as a dictionary with per-platform values. If not specified, defaults to 1 card per platform.
|
||||
|
||||
4. **Integrates resource validation**
|
||||
On CUDA, adds a skip marker (`skipif_cuda`) if the system does not have the required number of devices.
|
||||
Support for `skipif_rocm` and `skipif_npu` will be implemented later.
|
||||
|
||||
|
||||
5. **Works with pytest filtering**
|
||||
Allows tests to be filtered and selected at runtime using standard pytest marker expressions (e.g., `-m "distributed_cuda and L4"`).
|
||||
|
||||
#### Example usage for decorator
|
||||
- Single call for multiple platforms:
|
||||
```python
|
||||
@hardware_test(
|
||||
res={"cuda": "L4", "rocm": "MI325", "npu": "A2"},
|
||||
num_cards={"cuda": 2, "rocm": 2, "npu": 2},
|
||||
)
|
||||
```
|
||||
or
|
||||
```python
|
||||
@hardware_test(
|
||||
res={"cuda": "L4", "rocm": "MI325", "npu": "A2"},
|
||||
num_cards=2,
|
||||
)
|
||||
```
|
||||
- `res` must be a dict; supported resources: CUDA (L4/H100), ROCm (MI325), NPU (A2/A3)
|
||||
- `num_cards` can be int (all platforms) or dict (per platform); defaults to 1 when missing
|
||||
- Distributed markers (`distributed_cuda`, `distributed_rocm`, `distributed_npu`) are auto-added for multi-card cases
|
||||
- Filtering examples:
|
||||
- CUDA only: `pytest -m "distributed_cuda and L4"`
|
||||
- ROCm only: `pytest -m "distributed_rocm and MI325"`
|
||||
- NPU only: `pytest -m "distributed_npu"`
|
||||
|
||||
## Add Support for a New Platform
|
||||
|
||||
If you want to add support for a new platform (e.g., "tpu" for a new accelerator), follow these steps:
|
||||
|
||||
1. **Extend the marker list in your pytest config** so that platform/resource markers are defined:
|
||||
```toml
|
||||
# In pyproject.toml or pytest.ini
|
||||
[tool.pytest.ini_options]
|
||||
markers = [
|
||||
# ... existing markers ...
|
||||
"tpu: Tests that require TPU device",
|
||||
"TPU_V3: Tests that require TPU v3 hardware",
|
||||
"distributed_tpu: Tests that require multiple TPU devices",
|
||||
]
|
||||
```
|
||||
2. **Implement a marker construction function for your platform** in `vllm-omni/tests/utils.py`:
|
||||
```python
|
||||
# In vllm-omni/tests/utils.py
|
||||
|
||||
def tpu_marks(*, res: str, num_cards: int):
|
||||
test_platform = pytest.mark.tpu
|
||||
if res == "TPU_V3":
|
||||
test_resource = pytest.mark.TPU_V3
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Invalid TPU resource type: {res}. Supported: TPU_V3")
|
||||
|
||||
if num_cards == 1:
|
||||
return [test_platform, test_resource]
|
||||
else:
|
||||
test_distributed = pytest.mark.distributed_tpu(num_cards=num_cards)
|
||||
# Optionally: add skipif_tpu when implemented
|
||||
return [test_platform, test_resource, test_distributed]
|
||||
```
|
||||
3. **Update `hardware_test` to recognize your new platform**:
|
||||
In the relevant place (see the `hardware_test` implementation), add:
|
||||
```python
|
||||
if platform == "tpu":
|
||||
marks = tpu_marks(res=resource, num_cards=cards)
|
||||
```
|
||||
4. **(Recommended) Add a test using your new markers**:
|
||||
```python
|
||||
@hardware_test(
|
||||
res={"tpu": "TPU_V3"},
|
||||
num_cards=2,
|
||||
)
|
||||
def test_my_tpu_feature():
|
||||
...
|
||||
```
|
||||
|
||||
**Summary**:
|
||||
- Add pytest markers for your new platform/resources
|
||||
- Implement a marker function (`xxx_marks`)
|
||||
- Plug into `hardware_test`
|
||||
- You're done: tests decorated with `@hardware_test` using your platform now automatically get the correct markers, distribution, and isolation!
|
||||
|
||||
See code in `vllm-omni/tests/utils.py` for existing examples (`cuda_marks`, `rocm_marks`, `npu_marks`).
|
||||
@@ -0,0 +1,467 @@
|
||||
# Test File Structure and Style Guide
|
||||
|
||||
To ensure project maintainability and sustainable development, we encourage contributors to submit test code (unit tests, system tests, or end-to-end tests) alongside their code changes. This document outlines the guidelines for organizing and naming test files.
|
||||
|
||||
## Checklist before submitting your test files
|
||||
|
||||
1. The file is saved in an appropriate place and the file name is clear.
|
||||
2. The coding style follows the requirements outlined below.
|
||||
3. All test functions have appropriate pytest markers.
|
||||
4. For tests that need run in CI, please ensure it labeled as ``@pytest.mark.core_model` the test is configured under the `./buildkite/` folder.
|
||||
|
||||
|
||||
## Test Types
|
||||
|
||||
For more details about our [Five Levels Tests design](../ci/CI_5levels.md).
|
||||
|
||||
### Unit Tests and System Tests
|
||||
For unit tests and system tests, we strongly recommend placing test files in the same directory structure as the source code being tested, using the naming convention `test_*.py`.
|
||||
|
||||
### End-to-End (E2E) Tests for Models
|
||||
End-to-end tests verify the complete functionality of a system or component. For our project, the E2E tests for different omni models are organized into two subdirectories:
|
||||
|
||||
- **`tests/e2e/offline_inference/`**: Tests for offline inference modes (e.g., Qwen3Omni offline inference)
|
||||
|
||||
- **`tests/e2e/online_serving/`**: Tests for online serving scenarios (e.g., API server tests)
|
||||
|
||||
**Example:** The test file for `vllm_omni/entrypoints/omni_llm.py` should be located at `tests/entrypoints/test_omni_llm.py`.
|
||||
|
||||
## Test Directory Structure
|
||||
|
||||
The ideal directory structure mirrors the source code organization. Legend: `✅` = test exists, `⬜` = suggested to add.
|
||||
|
||||
```
|
||||
vllm_omni/ tests/
|
||||
├── config/ → ├── config/
|
||||
│ ├── model.py │ └── test_model.py ⬜
|
||||
│ └── lora.py │ └── test_lora.py ⬜
|
||||
│
|
||||
├── core/ → ├── core/
|
||||
│ └── sched/ │ └── sched/
|
||||
│ ├── omni_ar_scheduler.py │ ├── test_omni_ar_scheduler.py ⬜
|
||||
│ ├── omni_generation_scheduler.py │ ├── test_omni_generation_scheduler.py ⬜
|
||||
│ └── output.py │ └── test_output.py ✅ currently in entrypoints/test_omni_new_request_data.py (tests output.OmniNewRequestData)
|
||||
│
|
||||
├── diffusion/ → ├── diffusion/
|
||||
│ ├── diffusion_engine.py │ ├── test_diffusion_engine.py ⬜
|
||||
│ ├── attention/ │ ├── attention/
|
||||
│ │ ├── layer.py │ │ ├── test_attention_sp.py ✅
|
||||
│ │ └── backends/ │ │ └── test_flash_attn.py ✅
|
||||
│ ├── distributed/ │ ├── distributed/
|
||||
│ │ └── ... │ │ ├── test_comm.py ✅
|
||||
│ │ │ │ ├── test_cfg_parallel.py ✅
|
||||
│ │ │ │ └── test_sp_plan_hooks.py ✅
|
||||
│ ├── lora/ │ ├── lora/
|
||||
│ │ └── ... │ │ ├── test_base_linear.py ✅
|
||||
│ │ │ │ └── test_lora_manager.py ✅
|
||||
│ ├── models/ │ ├── models/
|
||||
│ │ ├── qwen_image/ │ │ ├── qwen_image/ (e2e coverage)
|
||||
│ │ ├── z_image/ │ │ └── z_image/
|
||||
│ │ └── ... │ │ └── test_zimage_tp_constraints.py ✅
|
||||
│ └── worker/ │ └── worker/
|
||||
│ ├── diffusion_worker.py │ └── test_diffusion_worker.py ✅ file at diffusion/test_diffusion_worker.py
|
||||
│ └── diffusion_model_runner.py │
|
||||
│
|
||||
├── distributed/ → ├── distributed/
|
||||
│ └── omni_connectors/ │ └── omni_connectors/
|
||||
│ ├── adapter.py │ ├── test_adapter_and_flow.py ✅
|
||||
│ ├── kv_transfer_manager.py │ ├── test_basic_connectors.py ✅
|
||||
│ ├── connectors/ │ ├── test_kv_flow.py ✅
|
||||
│ └── utils/ │ └── test_omni_connector_configs.py ✅
|
||||
│
|
||||
├── engine/ → ├── engine/
|
||||
│ ├── input_processor.py │ ├── test_input_processor.py ⬜ (no processor.py in source)
|
||||
│ ├── output_processor.py │ └── test_output_processor.py ⬜
|
||||
│ └── arg_utils.py │ └── test_arg_utils.py ⬜
|
||||
│
|
||||
├── entrypoints/ → ├── entrypoints/
|
||||
│ ├── omni.py │ ├── test_omni.py ⬜ (E2E covered by e2e/offline, e2e/online)
|
||||
│ ├── omni_llm.py │ ├── test_omni_llm.py ✅
|
||||
│ ├── omni_stage.py │ ├── test_omni_stage.py ⬜ (partial in test_omni_stage_diffusion_config.py)
|
||||
│ ├── omni_diffusion.py │ ├── test_omni_diffusion.py ✅
|
||||
│ ├── async_omni.py │ ├── test_async_omni.py ✅ actually in e2e/online_serving/test_async_omni.py
|
||||
│ ├── async_omni_diffusion.py │ ├── test_async_omni_diffusion_config.py ✅
|
||||
│ ├── stage_utils.py │ ├── test_stage_utils.py ✅
|
||||
│ ├── cli/ │ ├── cli/ (benchmarks/test_serve_cli.py covers CLI serve)
|
||||
│ │ └── ... │ │ └── test_*.py ⬜
|
||||
│ └── openai/ │ └── openai_api/ # maps to entrypoints/openai/
|
||||
│ ├── api_server.py │ ├── test_api_server.py ⬜ (e2e indirect coverage)
|
||||
│ ├── serving_chat.py │ ├── test_serving_chat_sampling_params.py ✅
|
||||
│ ├── serving_speech.py │ ├── test_serving_speech.py ✅
|
||||
│ └── image_api_utils.py │ └── test_image_server.py ✅
|
||||
│
|
||||
├── inputs/ → ├── inputs/
|
||||
│ ├── data.py │ ├── test_data.py ⬜
|
||||
│ ├── parse.py │ ├── test_parse.py ⬜
|
||||
│ └── preprocess.py │ └── test_preprocess.py ✅ currently in entrypoints/test_omni_input_preprocessor.py
|
||||
│
|
||||
├── model_executor/ → ├── model_executor/
|
||||
│ ├── layers/ │ ├── layers/
|
||||
│ │ └── mrope.py │ │ └── test_mrope.py ⬜
|
||||
│ ├── model_loader/ │ ├── model_loader/
|
||||
│ │ └── weight_utils.py │ │ └── test_weight_utils.py ⬜
|
||||
│ ├── models/ │ ├── models/
|
||||
│ │ ├── qwen2_5_omni/ │ │ ├── qwen2_5_omni/
|
||||
│ │ │ ├── qwen2_5_omni_thinker.py │ │ │ ├── test_audio_length.py ✅
|
||||
│ │ │ ├── qwen2_5_omni_talker.py │ │ │ ├── test_qwen2_5_omni_thinker.py ⬜
|
||||
│ │ │ └── qwen2_5_omni_token2wav.py │ │ │ ├── test_qwen2_5_omni_talker.py ⬜
|
||||
│ │ └── qwen3_omni/ │ │ │ └── test_qwen2_5_omni_token2wav.py ⬜
|
||||
│ │ └── ... │ │ └── qwen3_omni/
|
||||
│ ├── stage_configs/ │ │ └── test_*.py ⬜
|
||||
│ │ └── *.yaml │ └── stage_configs/ (used by e2e, test_*.py can be added) ⬜
|
||||
│ └── stage_input_processors/ │ └── stage_input_processors/
|
||||
│ └── ... │ └── test_*.py ⬜
|
||||
│
|
||||
├── sample/ → ├── sample/
|
||||
│ └── __init__.py │ └── test_*.py ⬜
|
||||
│
|
||||
├── utils/ → ├── utils/
|
||||
│ └── __init__.py │ └── test_*.py ⬜ (no platform_utils.py currently)
|
||||
│
|
||||
├── worker/ → ├── worker/
|
||||
│ ├── gpu_ar_model_runner.py │ ├── test_gpu_ar_model_runner.py ⬜
|
||||
│ ├── gpu_ar_worker.py │ ├── test_gpu_ar_worker.py ⬜
|
||||
│ ├── gpu_generation_model_runner.py │ ├── test_gpu_generation_model_runner.py ✅
|
||||
│ ├── gpu_generation_worker.py │ ├── test_gpu_generation_worker.py ⬜
|
||||
│ ├── gpu_model_runner.py │ ├── test_omni_gpu_model_runner.py ✅
|
||||
│ └── mixins.py │ └── (npu under platforms/npu/worker/) # not worker/npu/
|
||||
│
|
||||
├── platforms/ → (no tests/platforms/, e2e and stage_configs provide indirect coverage)
|
||||
│ ├── cuda/
|
||||
│ ├── npu/worker/ # NPU worker here, not vllm_omni/worker/npu/
|
||||
│ ├── rocm/
|
||||
│ └── xpu/worker/
|
||||
│
|
||||
├── outputs.py → test_outputs.py ✅ (at tests root)
|
||||
├── (logger, patch, request, version) → (no corresponding unit test)
|
||||
│
|
||||
└── e2e (tests side only) → ├── e2e/
|
||||
├── online_serving/ ✅ non-empty
|
||||
│ ├── test_async_omni.py
|
||||
│ ├── test_qwen3_omni.py
|
||||
│ ├── test_qwen3_omni_expansion.py
|
||||
│ ├── test_image_gen_edit.py
|
||||
│ ├── test_images_generations_lora.py
|
||||
│ └── stage_configs/
|
||||
└── offline_inference/ ✅
|
||||
├── test_qwen2_5_omni.py
|
||||
├── test_qwen3_omni.py
|
||||
├── test_bagel_text2img.py
|
||||
├── test_t2i_model.py
|
||||
├── test_t2v_model.py
|
||||
├── test_ovis_image.py
|
||||
├── test_zimage_tensor_parallel.py
|
||||
├── test_cache_dit.py
|
||||
├── test_teacache.py
|
||||
├── test_stable_audio_model.py
|
||||
├── test_diffusion_cpu_offload.py
|
||||
├── test_diffusion_layerwise_offload.py
|
||||
├── test_diffusion_lora.py
|
||||
├── test_sequence_parallel.py
|
||||
└── stage_configs/
|
||||
├── qwen2_5_omni_ci.yaml
|
||||
├── qwen3_omni_ci.yaml
|
||||
├── bagel_*.yaml
|
||||
└── npu/, rocm/, etc.
|
||||
```
|
||||
|
||||
|
||||
|
||||
### Naming Conventions
|
||||
|
||||
- **Unit Tests**: Use `test_<module_name>.py` format. Example: `omni_llm.py` → `test_omni_llm.py`
|
||||
|
||||
- **E2E Tests**: Place in `tests/e2e/offline_inference/` or `tests/e2e/online_serving/` with descriptive names. Example: `tests/e2e/offline_inference/test_qwen3_omni.py`, `tests/e2e/offline_inference/test_diffusion_model.py`
|
||||
|
||||
- **Expansion Tests**
|
||||
|
||||
### Best Practices
|
||||
|
||||
1. **Mirror Source Structure**: Test directories should mirror the source code structure
|
||||
2. **Test Type Indicators**: Use comments to indicate test types (UT for unit tests, E2E for end-to-end tests)
|
||||
3. **Shared Resources**: Place shared test configurations (e.g., CI configs) in appropriate subdirectories
|
||||
4. **Consistent Naming**: Follow the `test_*.py` naming convention consistently across all test files
|
||||
|
||||
|
||||
## Test codes requirements
|
||||
|
||||
### Coding style
|
||||
|
||||
1. **File header**: Add SPDX license header to all test files
|
||||
2. **Imports**: Pls don't use manual `sys.path` modifications, use standard imports instead.
|
||||
3. **Test type differentiation**:
|
||||
|
||||
- Unit tests: Maintain mock style
|
||||
- E2E tests for models: Consider using OmniRunner uniformly, avoid decorators
|
||||
|
||||
4. **Documentation**: Add docstrings to all test functions
|
||||
5. **Environment variables**: Set uniformly in `conftest.py` or at the top of files
|
||||
6. **Type annotations**: Add type annotations to all test function parameters
|
||||
7. **Pytest Markers**: Add necessary markers like `@pytest.mark.core_model` and use `@hardware_test` to declare hardware requirements (check detailed in [Markers for Tests](../ci/tests_markers.md)).
|
||||
|
||||
### Template
|
||||
#### E2E - Online serving
|
||||
|
||||
E2E Online tests for Qwen3-Omni model with mix input and audio+text output. Based on `tests/e2e/online_serving/test_qwen3_omni.py`.
|
||||
|
||||
```python
|
||||
"""
|
||||
E2E Online tests for Qwen3-Omni model with mix input and audio+text output.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn"
|
||||
os.environ["VLLM_TEST_CLEAN_GPU_MEMORY"] = "0"
|
||||
|
||||
import threading
|
||||
from pathlib import Path
|
||||
|
||||
import openai
|
||||
import pytest
|
||||
|
||||
from tests.conftest import (
|
||||
OmniServer,
|
||||
convert_audio_to_text,
|
||||
cosine_similarity_text,
|
||||
dummy_messages_from_mix_data,
|
||||
generate_synthetic_video,
|
||||
merge_base64_and_convert_to_text,
|
||||
)
|
||||
from vllm_omni.platforms import current_omni_platform
|
||||
|
||||
# Edit: model name and stage config path
|
||||
models = ["Qwen/Qwen3-Omni-30B-A3B-Instruct"]
|
||||
|
||||
#If you use the default configuration file, you can directly use the following address.
|
||||
def get_default_config():
|
||||
return str(Path(__file__).parent.parent / "stage_configs" / "qwen3_omni_ci.yaml")
|
||||
|
||||
#If you need to modify the configuration file, you can use modify_stage_config.
|
||||
def get_chunk_config():
|
||||
path = modify_stage_config(
|
||||
get_default_config(),
|
||||
updates={
|
||||
"async_chunk": True,
|
||||
"stage_args": {
|
||||
0: {
|
||||
"engine_args.custom_process_next_stage_input_func": "vllm_omni.model_executor.stage_input_processors.qwen3_omni.thinker2talker_async_chunk"
|
||||
},
|
||||
1: {
|
||||
"engine_args.custom_process_next_stage_input_func": "vllm_omni.model_executor.stage_input_processors.qwen3_omni.talker2code2wav_async_chunk"
|
||||
},
|
||||
},
|
||||
},
|
||||
deletes={"stage_args": {2: ["custom_process_input_func"]}},
|
||||
)
|
||||
return path
|
||||
|
||||
stage_configs = [get_default_config(), CHUNK_CONFIG_PATH]
|
||||
|
||||
test_params = [(model, stage_config) for model in models for stage_config in stage_configs]
|
||||
|
||||
|
||||
#Please use this method to launch the online instance.
|
||||
_omni_server_lock = threading.Lock()
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def omni_server(request):
|
||||
"""Start vLLM-Omni server as a subprocess. Use module scope for multi-stage init (10-20+ min)."""
|
||||
with _omni_server_lock:
|
||||
model, stage_config_path = request.param
|
||||
with OmniServer(
|
||||
model,
|
||||
["--stage-configs-path", stage_config_path, "--stage-init-timeout", "120"],
|
||||
) as server:
|
||||
yield server
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client(omni_server):
|
||||
"""OpenAI client for the running vLLM-Omni server."""
|
||||
return openai.OpenAI(
|
||||
base_url=f"http://{omni_server.host}:{omni_server.port}/v1",
|
||||
api_key="EMPTY",
|
||||
)
|
||||
|
||||
#Please use function definitions above the test function to define the prompts and other parameters you need.
|
||||
def get_system_prompt():
|
||||
return {
|
||||
"role": "system",
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": (
|
||||
"You are Qwen, a virtual human developed by the Qwen Team, "
|
||||
"Alibaba Group, capable of perceiving auditory and visual inputs, "
|
||||
"as well as generating text and speech."
|
||||
),
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
...
|
||||
|
||||
#Please define test case tags according to the instructions in the marker documentation.
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.omni
|
||||
@pytest.mark.parametrize("omni_server", test_params, indirect=True)
|
||||
def test_mix_to_text_audio_001(client: openai.OpenAI, omni_server, request) -> None:
|
||||
# PLEASE FOLLOW THESE TEMPLATE INSTRUCTIONS:
|
||||
# ============================================================================
|
||||
# TEMPLATE USAGE GUIDE:
|
||||
# 1. Copy this entire function as a starting point for multi-modal tests
|
||||
# 2. Update the test name to reflect your specific test scenario
|
||||
# 3. Modify input/output modalities as needed (see OPTIONS section below)
|
||||
# 4. Adjust assertions based on your expected outcomes
|
||||
# 5. Add custom validation logic for your specific use case
|
||||
# ============================================================================
|
||||
|
||||
#Please list the relevant test points.
|
||||
"""
|
||||
Test multi-modal input processing and text/audio output generation via OpenAI API.
|
||||
Deploy Setting: default yaml
|
||||
Input Modal: text + audio + video + image
|
||||
Output Modal: text + audio
|
||||
Input Setting: stream=True
|
||||
Datasets: single request
|
||||
"""
|
||||
# SECTION 1: TEST SETUP AND INITIALIZATION
|
||||
# =========================================
|
||||
# INSTRUCTIONS: Initialize test variables and prepare test environment
|
||||
# MODIFY: Add any additional test setup required for your scenario
|
||||
e2e_list = list()
|
||||
# SECTION 2: TEST DATA GENERATION
|
||||
# ================================
|
||||
# INSTRUCTIONS: Generate or load test data for each input modality
|
||||
# MODIFY: Replace synthetic generators with your actual data sources
|
||||
# VIDEO DATA - Generate synthetic video for testing
|
||||
# FORMAT: data:video/mp4;base64,{base64_encoded_video}
|
||||
# PARAMETERS: width, height, duration_frames
|
||||
video_data_url = f"data:video/mp4;base64,{generate_synthetic_video(224, 224, 300)['base64']}"
|
||||
# IMAGE DATA - Generate synthetic image for testing
|
||||
# FORMAT: data:image/jpeg;base64,{base64_encoded_image}
|
||||
# PARAMETERS: width, height
|
||||
image_data_url = f"data:image/jpeg;base64,{generate_synthetic_image(224, 224)['base64']}"
|
||||
# AUDIO DATA - Generate synthetic audio for testing
|
||||
# FORMAT: data:audio/wav;base64,{base64_encoded_audio}
|
||||
# PARAMETERS: duration_seconds, channels
|
||||
audio_data_url = f"data:audio/wav;base64,{generate_synthetic_audio(5, 1)['base64']}"
|
||||
|
||||
# SECTION 3: MESSAGE CONSTRUCTION
|
||||
# ================================
|
||||
# INSTRUCTIONS: Assemble the complete message payload for API request
|
||||
# MODIFY: Add/remove modalities or change prompt structure as needed
|
||||
|
||||
# USAGE: Construct a message containing all input modalities
|
||||
# IMPORTANT: Ensure the message structure matches OpenAI API expectations
|
||||
# CUSTOMIZATION POINTS:
|
||||
# - system_prompt: Controls the assistant's behavior
|
||||
# - content_text: The user's text prompt/question
|
||||
# - *_data_url: URLs for media content (video/image/audio)
|
||||
messages = dummy_messages_from_mix_data(
|
||||
system_prompt=get_system_prompt(),
|
||||
video_data_url=video_data_url,
|
||||
image_data_url=image_data_url,
|
||||
audio_data_url=audio_data_url,
|
||||
content_text=get_prompt("mix"),
|
||||
)
|
||||
|
||||
# SECTION 4: API REQUEST EXECUTION
|
||||
# =================================
|
||||
# INSTRUCTIONS: Make the API call and measure performance
|
||||
# MODIFY: Add timeout, retry logic, or additional parameters
|
||||
start_time = time.perf_counter()
|
||||
chat_completion = client.chat.completions.create(model=omni_server.model, messages=messages, stream=True)
|
||||
|
||||
#Call using your preferred method and obtain the final audio and text outputs.
|
||||
...
|
||||
|
||||
# SECTION 5: OUTPUT VALIDATION
|
||||
# =============================
|
||||
# INSTRUCTIONS: Verify that outputs meet expected criteria
|
||||
# MODIFY: Adjust validation logic for your specific requirements
|
||||
|
||||
# ASSERTION 1: E2E Validation
|
||||
# PURPOSE: Verify that the E2E latency is less than the baseline.
|
||||
current_e2e = time.perf_counter() - start_time
|
||||
print(f"the request e2e is: {current_e2e}")
|
||||
e2e_list.append(current_e2e)
|
||||
|
||||
print(f"the avg e2e is: {sum(e2e_list) / len(e2e_list)}")
|
||||
|
||||
|
||||
|
||||
# ASSERTION 2: Text Output Validation
|
||||
# PURPOSE: Verify that text output was generated with keyword content
|
||||
assert text_content is not None and len(text_content) >= 2, "No text output is generated"
|
||||
assert any(
|
||||
keyword in text_content.lower() for keyword in ["square", "quadrate", "sphere", "globe", "circle", "round"]
|
||||
), "The output does not contain any of the keywords."
|
||||
|
||||
|
||||
# ASSERTION 3: Cross-Modal Consistency
|
||||
# PURPOSE: Verify text and audio outputs convey the same information
|
||||
# CUSTOMIZATION: Adjust similarity threshold (0.9) based on accuracy requirements
|
||||
assert audio_data is not None, "No audio output is generated"
|
||||
audio_content = merge_base64_and_convert_to_text(audio_data)
|
||||
print(f"text content is: {text_content}")
|
||||
print(f"audio content is: {audio_content}")
|
||||
similarity = cosine_similarity_text(audio_content.lower(), text_content.lower())
|
||||
print(f"similarity is: {similarity}")
|
||||
assert similarity > 0.9, "The audio content is not same as the text"
|
||||
```
|
||||
|
||||
|
||||
#### E2E - Offline inference
|
||||
```python
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""
|
||||
Offline E2E smoke test for an omni model (video → audio).
|
||||
"""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from vllm.assets.video import VideoAsset
|
||||
|
||||
from tests.utils import hardware_test
|
||||
from ..multi_stages.conftest import OmniRunner
|
||||
|
||||
# Optional: set process start method for workers
|
||||
os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn"
|
||||
|
||||
models = ["{your model name}"] #Edit here to load your model
|
||||
stage_configs = [str(Path(__file__).parent / "stage_configs" / {your model yaml})] #Edit here to load your model yaml
|
||||
|
||||
# Create parameter combinations for model and stage config
|
||||
test_params = [(model, stage_config) for model in models for stage_config in stage_configs]
|
||||
|
||||
# function name: test_{input_modality}_to_{output_modality}
|
||||
# modality candidate: text, image, audio, video, mixed_modalities
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.omni
|
||||
@hardware_test(
|
||||
res={"cuda": "L4", "rocm": "MI325", "npu": "A2"},
|
||||
num_cards=2,
|
||||
)
|
||||
@pytest.mark.parametrize("test_config", test_params)
|
||||
def test_video_to_audio(omni_runner: type[OmniRunner], model: str) -> None:
|
||||
"""Offline inference: video input, audio output."""
|
||||
model, stage_config_path = test_config
|
||||
with omni_runner(model, seed=42, stage_configs_path=stage_config_path) as runner:
|
||||
# Prepare inputs
|
||||
video = VideoAsset(name="sample", num_frames=4).np_ndarrays
|
||||
|
||||
outputs = runner.generate_multimodal(
|
||||
prompts="Describe this video briefly.",
|
||||
videos=video,
|
||||
)
|
||||
|
||||
# Minimal assertions: got outputs and at least one audio result
|
||||
assert outputs
|
||||
has_audio = any(o.final_output_type == "audio" for o in outputs)
|
||||
assert has_audio
|
||||
```
|
||||
@@ -0,0 +1,173 @@
|
||||
|
||||
# Metrics
|
||||
|
||||
You can use these metrics in production to monitor the health and performance of the vLLM-omni system. Typical scenarios include:
|
||||
|
||||
- **Performance Monitoring**: Track throughput (e.g., `e2e_avg_tokens_per_s`), latency (e.g., `e2e_total_ms`), and resource utilization to verify that the system meets expected standards.
|
||||
|
||||
- **Debugging and Troubleshooting**: Use detailed per-request metrics to diagnose issues, such as high transfer times or unexpected token counts.
|
||||
|
||||
## How to Enable and View Metrics
|
||||
|
||||
### Start the Service with Metrics Logging
|
||||
|
||||
```bash
|
||||
vllm serve /workspace/models/Qwen3-Omni-30B-A3B-Instruct --omni --port 8014 --log-stats
|
||||
```
|
||||
|
||||
### Send a Request
|
||||
|
||||
```bash
|
||||
python openai_chat_completion_client_for_multimodal_generation.py --query-type use_image
|
||||
```
|
||||
|
||||
### What You Will See
|
||||
|
||||
With `--log-stats` enabled, the server will output detailed metrics logs after each request. Example output:
|
||||
|
||||
|
||||
#### Overall Summary
|
||||
|
||||
| Field | Value |
|
||||
|-----------------------------|--------------|
|
||||
| e2e_requests | 1 |
|
||||
| e2e_wall_time_ms | 41,299.190 |
|
||||
| e2e_total_tokens | 5,202 |
|
||||
| e2e_avg_time_per_request_ms | 41,299.190 |
|
||||
| e2e_avg_tokens_per_s | 125.959 |
|
||||
| e2e_stage_0_wall_time_ms | 10,192.289 |
|
||||
| e2e_stage_1_wall_time_ms | 30,541.409 |
|
||||
| e2e_stage_2_wall_time_ms | 207.496 |
|
||||
|
||||
#### RequestE2EStats
|
||||
|
||||
| Field | Value |
|
||||
|-------------------------|------------|
|
||||
| e2e_total_ms | 41,299.133 |
|
||||
| e2e_total_tokens | 5,202 |
|
||||
| transfers_total_time_ms | 245.895 |
|
||||
| transfers_total_kbytes | 138,089.939|
|
||||
|
||||
#### StageRequestStats
|
||||
|
||||
| Field | 0 | 1 | 2 |
|
||||
|------------------------|--------|--------|--------|
|
||||
| audio_generated_frames | 0 | 0 | 525,525|
|
||||
| batch_id | 38 | 274 | 0 |
|
||||
| batch_size | 1 | 1 | 1 |
|
||||
| num_tokens_in | 4,860 | 4,826 | 4,384 |
|
||||
| num_tokens_out | 67 | 275 | 0 |
|
||||
| postprocess_time_ms | 256.158| 0.491 | 0.000 |
|
||||
| stage_gen_time_ms | 9,910.007|30,379.198|160.745|
|
||||
|
||||
#### TransferEdgeStats
|
||||
|
||||
| Field | 0->1 | 1->2 |
|
||||
|---------------------|-------------|------------|
|
||||
| size_kbytes | 109,277.349 | 28,812.591 |
|
||||
| tx_time_ms | 78.701 | 18.790 |
|
||||
| rx_decode_time_ms | 111.865 | 31.706 |
|
||||
| in_flight_time_ms | 2.015 | 2.819 |
|
||||
|
||||
|
||||
These logs include:
|
||||
|
||||
- **Overall summary**: total requests, wall time, average tokens/sec, etc.
|
||||
|
||||
- **E2E table**: per-request latency and token counts.
|
||||
|
||||
- **Stage table**: per-stage batch and timing details.
|
||||
|
||||
- **Transfer table**: data transfer and timing for each edge.
|
||||
|
||||
You can use these logs to monitor system health, debug performance, and analyze request-level metrics as described above.
|
||||
|
||||
|
||||
## Metrics Scope: Offline vs Online Inference
|
||||
|
||||
For **offline inference** (batch mode), the summary includes both system-level metrics (aggregated across all requests) and per-request metrics. In this case, `e2e_requests` can be greater than 1, reflecting multiple completed requests in a batch.
|
||||
|
||||
For **online inference** (serving mode), the summary is always per-request. `e2e_requests` is always 1, and only request-level metrics are reported for each completion.
|
||||
|
||||
---
|
||||
|
||||
## Parameter Details
|
||||
|
||||
### Summary Metrics
|
||||
|
||||
| Field | Meaning |
|
||||
|---------------------------|----------------------------------------------------------------------------------------------|
|
||||
| `e2e_requests` | Number of completed requests. |
|
||||
| `e2e_wall_time_ms` | Wall-clock time span from run start to last completion, in ms. |
|
||||
| `e2e_total_tokens` | Total tokens counted across all completed requests (stage0 input + all stage outputs). |
|
||||
| `e2e_avg_time_per_request_ms` | Average wall time per request: `e2e_wall_time_ms / e2e_requests`. |
|
||||
| `e2e_avg_tokens_per_s` | Average token throughput over wall time: `e2e_total_tokens * 1000 / e2e_wall_time_ms`. |
|
||||
| `e2e_stage_{i}_wall_time_ms` | Wall-clock time span for stage i, in ms. Each stage's wall time is reported as a separate field, e.g., `e2e_stage_0_wall_time_ms`, `e2e_stage_1_wall_time_ms`, etc. |
|
||||
|
||||
---
|
||||
|
||||
### E2E Table (per request)
|
||||
|
||||
| Field | Meaning |
|
||||
|---------------------------|-----------------------------------------------------------------------|
|
||||
| `e2e_total_ms` | End-to-end latency in ms. |
|
||||
| `e2e_total_tokens` | Total tokens for the request (stage0 input + all stage outputs). |
|
||||
| `transfers_total_time_ms` | Sum of transfer edge `total_time_ms` for this request. |
|
||||
| `transfers_total_kbytes` | Sum of transfer kbytes for this request. |
|
||||
|
||||
|
||||
---
|
||||
|
||||
### Stage Table (per stage event / request)
|
||||
|
||||
| Field | Meaning |
|
||||
|---------------------------|-------------------------------------------------------------------------------------------------|
|
||||
| `batch_id` | Batch index. |
|
||||
| `batch_size` | Batch size. |
|
||||
| `num_tokens_in` | Input tokens to the stage. |
|
||||
| `num_tokens_out` | Output tokens from the stage. |
|
||||
| `stage_gen_time_ms` | Stage compute time in ms, excluding postprocessing time (reported separately as `postprocess_time_ms`). |
|
||||
| `image_num` | Number of images generated (for diffusion/image stages). |
|
||||
| `resolution` | Image resolution (for diffusion/image stages). |
|
||||
| `postprocess_time_ms` | Diffusion/image: post-processing time in ms. |
|
||||
|
||||
---
|
||||
|
||||
### Transfer Table (per edge / request)
|
||||
|
||||
| Field | Meaning |
|
||||
|----------------------|---------------------------------------------------------------------------|
|
||||
| `size_kbytes` | Total kbytes transferred. |
|
||||
| `tx_time_ms` | Sender transfer time in ms. |
|
||||
| `rx_decode_time_ms` | Receiver decode time in ms. |
|
||||
| `in_flight_time_ms` | In-flight time in ms. |
|
||||
|
||||
|
||||
### Expectation of the Numbers (Verification)
|
||||
|
||||
**Formulas:**
|
||||
|
||||
- `e2e_total_tokens = Stage0's num_tokens_in + sum(all stages' num_tokens_out)`
|
||||
|
||||
- `transfers_total_time_ms = sum(tx_time_ms + rx_decode_time_ms + in_flight_time_ms)` for every edge
|
||||
|
||||
**Using the example above:**
|
||||
|
||||
**e2e_total_tokens**
|
||||
|
||||
- Stage0's `num_tokens_in`: **4,860**
|
||||
- Stage0's `num_tokens_out`: **67**
|
||||
- Stage1's `num_tokens_out`: **275**
|
||||
- Stage2's `num_tokens_out`: **0**
|
||||
|
||||
so `e2e_total_tokens = 4,860 + 67 + 275 + 0 = 5,202`, which matches the table value `e2e_total_tokens`.
|
||||
|
||||
**transfers_total_time_ms**
|
||||
|
||||
For each edge:
|
||||
|
||||
- 0->1: tx_time_ms (**78.701**) + rx_decode_time_ms (**111.865**) + in_flight_time_ms (**2.015**) = **192.581**
|
||||
|
||||
- 1->2: tx_time_ms (**18.790**) + rx_decode_time_ms (**31.706**) + in_flight_time_ms (**2.819**) = **53.315**
|
||||
|
||||
192.581 + 53.315 = **245.896** = transfers_total_time_ms, which matches the calculation (difference is due to rounding)
|
||||
@@ -1,2 +1,15 @@
|
||||
# Adding a New Model
|
||||
Stay tuned for guidance on how to add a new model on vLLM-Omni!
|
||||
|
||||
This section provides comprehensive guidance on how to add a new model to vLLM-Omni.
|
||||
|
||||
## Documentation
|
||||
|
||||
- **[Adding an Omni-Modality Model](adding_omni_model.md)**: Complete step-by-step guide using Qwen3-Omni as an example.
|
||||
|
||||
- **[Adding a Diffusion Model](adding_diffusion_model.md)**: Complete step-by-step guide using Qwen/Qwen-Image-Edit as an example.
|
||||
|
||||
|
||||
|
||||
## Quick Start
|
||||
|
||||
For a quick reference, see the [Adding a New Multi-Stage Model Guide](adding_omni_model.md) and [Adding a New Diffusion Model Guide](adding_diffusion_model.md).
|
||||
@@ -0,0 +1,951 @@
|
||||
# Adding a Diffusion Model
|
||||
|
||||
This guide walks you through adding a new diffusion model to vLLM-Omni. We use **Qwen-Image** as the primary example, with references to other models (LongCat, Flux, Wan2.2) to illustrate different patterns.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Overview](#overview)
|
||||
2. [Directory Structure](#directory-structure)
|
||||
3. [Basic Implementation](#basic-implementation)
|
||||
4. [Advanced Features](#advanced-features)
|
||||
5. [Troubleshooting](#troubleshooting)
|
||||
6. [Pull Request Checklist](#pull-request-checklist)
|
||||
7. [Reference Implementations](#reference-implementations)
|
||||
8. [Summary](#summary)
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
vLLM-Omni's diffusion inference follows this architecture:
|
||||
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" src="https://raw.githubusercontent.com/vllm-project/vllm-omni/refs/heads/main/docs/source/architecture/vllm-omni-diffusion-flow.png">
|
||||
<img alt="Diffusion Flow" src="https://raw.githubusercontent.com/vllm-project/vllm-omni/refs/heads/main/docs/source/architecture/vllm-omni-diffusion-flow.png" width=55%>
|
||||
</picture>
|
||||
</p>
|
||||
|
||||
**Key Components:**
|
||||
|
||||
1. **Request Handling:** User prompts → `OmniDiffusionRequest`
|
||||
2. **Diffusion Engine:** Request → Preprocessing (Optional) → Pipeline execution -> Post-processing
|
||||
3. **Pipeline Execution:** Request → Encode prompt → Diffusion steps → Vae decode
|
||||
|
||||
|
||||
## Directory Structure
|
||||
|
||||
Organize your model files following this structure:
|
||||
|
||||
```
|
||||
vllm_omni/
|
||||
└── diffusion/
|
||||
├── registry.py # ← Register your model here
|
||||
├── request.py # Request data structures
|
||||
└── models/
|
||||
└── your_model_name/ # ← Create this directory
|
||||
├── __init__.py # Export pipeline and transformer
|
||||
├── pipeline_xxx.py # Pipeline implementation
|
||||
└── xxx_transformer.py # Transformer implementation
|
||||
```
|
||||
|
||||
**Naming Conventions:**
|
||||
|
||||
- **Model directory:** `your_model_name` (lowercase, underscores), e.g., `qwen_image`, `flux`, `longcat_image`, `wan2_2`
|
||||
- **Pipeline file:** `pipeline_xxx.py` where `xxx` describes the task, e.g., `pipeline_qwen_image.py`, `pipeline_qwen_image_edit.py`
|
||||
- **Transformer file:** `xxx_transformer.py` matching transformer class name, e.g., `qwen_image_transformer.py`, `flux_transformer.py`
|
||||
|
||||
---
|
||||
|
||||
## Basic Implementation
|
||||
|
||||
This section covers the minimal steps to get a model working in vLLM-Omni with basic features (online/offline serving, batch requests).
|
||||
|
||||
### Step 1: Adapt Transformer Model
|
||||
|
||||
The transformer is the core denoising network. Start by copying the transformer implementation from Diffusers and making these adaptations.
|
||||
|
||||
|
||||
#### 1.1: Remove Diffusers Mixins
|
||||
|
||||
Diffusers' `Mixin` classes are not needed in vLLM-Omni. Remove them:
|
||||
|
||||
```diff
|
||||
# Before (Diffusers)
|
||||
- from diffusers.models.modeling_utils import ModelMixin
|
||||
- from diffusers.models.attention_processor import AttentionModuleMixin
|
||||
|
||||
- class YourModelTransformer2DModel(ModelMixin, AttentionModuleMixin):
|
||||
+ class YourModelTransformer2DModel(nn.Module):
|
||||
"""Your transformer model."""
|
||||
```
|
||||
|
||||
**Example mixins to remove:**
|
||||
|
||||
- `ModelMixin` - Weight loading utilities (vLLM-Omni has its own weight loader)
|
||||
- `AttentionModuleMixin` - Attention processors (using vLLM-Omni's Attention layer instead)
|
||||
- `ConfigMixin` - Config management (not needed)
|
||||
- `PeftAdapterMixin` - Parameter efficient finetune utilities (not needed)
|
||||
|
||||
#### 1.2: Replace Attention Implementation
|
||||
|
||||
**The most important adaptation:** Replace Diffusers' attention with vLLM-Omni's optimized `Attention` layer.
|
||||
|
||||
**Before (Diffusers):**
|
||||
```python
|
||||
from diffusers.models.attention_processor import dispatch_attention_fn
|
||||
|
||||
class YourAttentionBlock(nn.Module):
|
||||
def forward(self, hidden_states, encoder_hidden_states=None, ...):
|
||||
...
|
||||
hidden_states = dispatch_attention_fn(
|
||||
query, key, value,
|
||||
attn_mask=attention_mask,
|
||||
dropout_p=0.0,
|
||||
is_causal=False,
|
||||
backend=self._attention_backend,
|
||||
)
|
||||
```
|
||||
|
||||
**After (vLLM-Omni):**
|
||||
```python
|
||||
from vllm_omni.diffusion.attention.layer import Attention
|
||||
from vllm_omni.diffusion.attention.backends.abstract import AttentionMetadata
|
||||
|
||||
class YourAttentionBlock(nn.Module):
|
||||
def __init__(self, ...):
|
||||
super().__init__()
|
||||
|
||||
# Initialize vLLM-Omni's Attention layer
|
||||
self.attn = Attention(
|
||||
num_heads=self.num_heads,
|
||||
head_size=self.head_dim,
|
||||
softmax_scale=1.0 / (self.head_dim ** 0.5),
|
||||
causal=False, # Diffusion models typically use bidirectional attention
|
||||
num_kv_heads=self.num_kv_heads,
|
||||
)
|
||||
|
||||
def forward(self, hidden_states, encoder_hidden_states=None, attention_mask=None, ...):
|
||||
...
|
||||
# Create attention metadata
|
||||
attn_metadata = AttentionMetadata(attn_mask=attention_mask)
|
||||
hidden_states = self.attn(query, key, value, attn_metadata=attn_metadata)
|
||||
|
||||
```
|
||||
|
||||
**Key Points:**
|
||||
|
||||
- **Attention layer initialization:** Done in `__init__`, not per-forward
|
||||
- **Tensor shapes:** vLLM-Omni `Attention` expects QKV to have `[B, seq, num_heads, head_dim]` shape
|
||||
- **AttentionMetadata:** Wraps attention mask and other metadata
|
||||
|
||||
**Attention backends:** vLLM-Omni automatically selects the attention backend given the environmental variable `DIFFUSION_ATTENTION_BACKEND`. The default attention backend is `FLASH_ATTN` for diffusion models.
|
||||
|
||||
#### 1.3: Replace Imports and Utilities
|
||||
|
||||
**Logger:**
|
||||
```diff
|
||||
- from diffusers.utils import logging
|
||||
- logger = logging.get_logger(__name__)
|
||||
|
||||
+ from vllm.logger import init_logger
|
||||
+ logger = init_logger(__name__)
|
||||
```
|
||||
|
||||
**Custom layers from vLLM and vLLM-Omni (if needed):**
|
||||
|
||||
```python
|
||||
from vllm.model_executor.layers.layernorm import RMSNorm
|
||||
from vllm_omni.diffusion.layers.rope import RotaryEmbedding
|
||||
from vllm_omni.diffusion.layers.adalayernorm import AdaLayerNorm
|
||||
```
|
||||
|
||||
#### 1.4: Remove Training-Only Code
|
||||
|
||||
Remove code that's only needed for training:
|
||||
|
||||
```diff
|
||||
# Remove gradient checkpointing
|
||||
- if torch.is_grad_enabled() and self.gradient_checkpointing:
|
||||
- hidden_states = torch.utils.checkpoint.checkpoint(
|
||||
- self._forward_block, hidden_states, ...
|
||||
- )
|
||||
- else:
|
||||
- hidden_states = self._forward_block(hidden_states, ...)
|
||||
+ hidden_states = self._forward_block(hidden_states, ...)
|
||||
|
||||
# Remove training-specific attributes
|
||||
- self.gradient_checkpointing = False
|
||||
|
||||
# Remove dropout (set to 0 or remove)
|
||||
- self.dropout = nn.Dropout(dropout_prob)
|
||||
+ # Removed dropout for inference
|
||||
```
|
||||
|
||||
#### 1.5: Add Configuration Support
|
||||
|
||||
Add support for vLLM-Omni's `OmniDiffusionConfig`:
|
||||
|
||||
```python
|
||||
from vllm_omni.diffusion.data import OmniDiffusionConfig
|
||||
|
||||
class YourModelTransformer2DModel(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
od_config: OmniDiffusionConfig | None = None, # ← Add vLLM-Omni config
|
||||
# ... other model-specific parameters
|
||||
num_layers: int = 28,
|
||||
hidden_size: int = 3072,
|
||||
num_heads: int = 24,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__()
|
||||
|
||||
# Store config
|
||||
self.od_config = od_config
|
||||
self.parallel_config = od_config.parallel_config if od_config else None
|
||||
|
||||
# Model architecture
|
||||
self.num_layers = num_layers
|
||||
self.hidden_size = hidden_size
|
||||
# ... initialize layers
|
||||
```
|
||||
|
||||
### Step 2: Adapt Pipeline
|
||||
|
||||
The pipeline orchestrates the full generation process (text encoding, denoising loop, VAE decoding). Adapt it from Diffusers format to vLLM-Omni's interface.
|
||||
|
||||
#### 2.1: Remove Diffusers Inheritance
|
||||
|
||||
**Remove Diffusers base classes:**
|
||||
```diff
|
||||
- from diffusers import DiffusionPipeline
|
||||
- from diffusers.loaders import LoraLoaderMixin
|
||||
|
||||
- class YourModelPipeline(DiffusionPipeline, LoraLoaderMixin):
|
||||
+ class YourModelPipeline(nn.Module):
|
||||
"""Your model pipeline for vLLM-Omni."""
|
||||
```
|
||||
|
||||
#### 2.2: Adapt `__init__` Method
|
||||
|
||||
**Before (Diffusers):**
|
||||
```python
|
||||
class YourModelPipeline(DiffusionPipeline):
|
||||
def __init__(
|
||||
self,
|
||||
vae: AutoencoderKL,
|
||||
text_encoder: CLIPTextModel,
|
||||
tokenizer: CLIPTokenizer,
|
||||
transformer: YourTransformer,
|
||||
scheduler: FlowMatchScheduler,
|
||||
):
|
||||
super().__init__()
|
||||
self.register_modules(
|
||||
vae=vae,
|
||||
text_encoder=text_encoder,
|
||||
tokenizer=tokenizer,
|
||||
transformer=transformer,
|
||||
scheduler=scheduler,
|
||||
)
|
||||
```
|
||||
|
||||
**After (vLLM-Omni):**
|
||||
```python
|
||||
import os
|
||||
from diffusers import AutoencoderKL
|
||||
from diffusers.schedulers import FlowMatchEulerDiscreteScheduler
|
||||
from transformers import CLIPTextModel, CLIPTokenizer
|
||||
|
||||
from vllm_omni.diffusion.data import OmniDiffusionConfig
|
||||
from vllm_omni.diffusion.distributed.utils import get_local_device
|
||||
from vllm_omni.diffusion.utils.tf_utils import get_transformer_config_kwargs
|
||||
from vllm_omni.diffusion.models.your_model_name.your_model_transformer import (
|
||||
YourModelTransformer2DModel,
|
||||
)
|
||||
|
||||
|
||||
class YourModelPipeline(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
od_config: OmniDiffusionConfig,
|
||||
prefix: str = "",
|
||||
):
|
||||
super().__init__()
|
||||
self.od_config = od_config
|
||||
self.parallel_config = od_config.parallel_config
|
||||
self.device = get_local_device()
|
||||
model = od_config.model
|
||||
local_files_only = os.path.exists(model)
|
||||
|
||||
# Load components from checkpoint
|
||||
self.scheduler = FlowMatchEulerDiscreteScheduler.from_pretrained(
|
||||
model, subfolder="scheduler", local_files_only=local_files_only)
|
||||
self.text_encoder = CLIPTextModel.from_pretrained(
|
||||
model, subfolder="text_encoder", local_files_only=local_files_only).to(self.device)
|
||||
self.tokenizer = CLIPTokenizer.from_pretrained(
|
||||
model, subfolder="tokenizer", local_files_only=local_files_only)
|
||||
self.vae = AutoencoderKL.from_pretrained(
|
||||
model, subfolder="vae", local_files_only=local_files_only).to(self.device)
|
||||
|
||||
# Initialize transformer with vLLM-Omni config
|
||||
transformer_kwargs = get_transformer_config_kwargs(
|
||||
od_config.tf_model_config, YourModelTransformer2DModel)
|
||||
self.transformer = YourModelTransformer2DModel(
|
||||
od_config=od_config, **transformer_kwargs)
|
||||
|
||||
# Store VAE scale factor for latent space conversions
|
||||
self.vae_scale_factor = 2 ** (len(self.vae.config.block_out_channels) - 1)
|
||||
self.default_sample_size = 128 # Default latent size
|
||||
```
|
||||
|
||||
**Key Changes:**
|
||||
|
||||
1. **`od_config` parameter:** All configuration through `OmniDiffusionConfig`
|
||||
2. **Manual component loading:** No `register_modules()`, load each component explicitly
|
||||
3. **Local files support:** Check `os.path.exists(model)` for local checkpoints
|
||||
4. **Transformer with config:** Pass `od_config` to transformer constructor
|
||||
|
||||
#### 2.3: Adapt `__call__` → `forward` Method
|
||||
|
||||
**Change signature:**
|
||||
|
||||
```diff
|
||||
- @torch.no_grad()
|
||||
- def __call__(
|
||||
+ def forward(
|
||||
self,
|
||||
+ req: OmniDiffusionRequest, # ← Add request parameter here
|
||||
- ):
|
||||
+ ) -> DiffusionOutput: # ← Add return type
|
||||
```
|
||||
|
||||
[`OmniDiffusionRequest`](https://docs.vllm.ai/projects/vllm-omni/en/latest/api/vllm_omni/diffusion/request/#vllm_omni.diffusion.request.OmniDiffusionRequest) is a dataclass that contains the **prompts** and **sampling parameters** [`OmniDiffusionSamplingParams`](https://docs.vllm.ai/projects/vllm-omni/en/latest/api/vllm_omni/inputs/data/#vllm_omni.inputs.data.OmniDiffusionSamplingParams) for the diffusion pipeline execution. It also contains a request_id for other components to trace this request and its outputs.
|
||||
|
||||
See some parameters in `OmniDiffusionSamplingParams` as follows:
|
||||
|
||||
| parameters | type |value | function |
|
||||
|:---:|:---:|:---:|:---:|
|
||||
| `num_inference_steps` | `int` | 50 | The number of diffusion steps during inference|
|
||||
| `guidance_scale` | `float` | 0.0 | The classifier free guidance scale |
|
||||
| `width` and `height` | `int` | None | The width and height of the generated image |
|
||||
|
||||
**Extract parameters from request:**
|
||||
|
||||
```python
|
||||
from vllm_omni.diffusion.request import OmniDiffusionRequest
|
||||
from vllm_omni.diffusion.data import DiffusionOutput
|
||||
|
||||
def forward(
|
||||
self,
|
||||
req: OmniDiffusionRequest,
|
||||
) -> DiffusionOutput:
|
||||
# Extract prompts from request
|
||||
if req.prompts is not None:
|
||||
prompt = [
|
||||
p if isinstance(p, str) else (p.get("prompt") or "")
|
||||
for p in req.prompts
|
||||
]
|
||||
|
||||
# Extract sampling parameters
|
||||
sampling_params = req.sampling_params
|
||||
num_inference_steps = sampling_params.num_inference_steps or 50
|
||||
guidance_scale = sampling_params.guidance_scale or 7.5
|
||||
height = sampling_params.height or (self.default_sample_size * self.vae_scale_factor)
|
||||
width = sampling_params.width or (self.default_sample_size * self.vae_scale_factor)
|
||||
|
||||
# For image editing pipelines, extract images from multi_modal_data
|
||||
if hasattr(req, 'multi_modal_data') and req.multi_modal_data:
|
||||
input_images = req.multi_modal_data.get('image', [])
|
||||
|
||||
# ... rest of generation logic
|
||||
```
|
||||
|
||||
For an image editing model, an example `OmniDiffusionRequest` is like:
|
||||
```python
|
||||
{
|
||||
"prompt": "turn this cat to a dog",
|
||||
"multi_modal_data": {"image": input_image}
|
||||
},
|
||||
```
|
||||
|
||||
**Wrap output:**
|
||||
|
||||
```diff
|
||||
# Generate images
|
||||
images = self.vae.decode(latents)[0]
|
||||
|
||||
- return {"images": images}
|
||||
+ return DiffusionOutput(output=images)
|
||||
```
|
||||
|
||||
#### 2.4: Extract Pre/Post-Processing Functions
|
||||
|
||||
vLLM-Omni separates image processing from the main pipeline for better modularity.
|
||||
|
||||
**Post-processing function (required):**
|
||||
```python
|
||||
def get_your_model_post_process_func(
|
||||
od_config: OmniDiffusionConfig,
|
||||
):
|
||||
"""
|
||||
Create post-processing function for your model.
|
||||
|
||||
Returns a function that converts latents to images.
|
||||
"""
|
||||
from diffusers.image_processor import VaeImageProcessor
|
||||
import json
|
||||
|
||||
# Load VAE config to get scale factor
|
||||
model_path = od_config.model
|
||||
if not os.path.exists(model_path):
|
||||
from vllm_omni.diffusion.model_loader.utils import download_weights_from_hf_specific
|
||||
model_path = download_weights_from_hf_specific(model_path, None, ["*"])
|
||||
|
||||
vae_config_path = os.path.join(model_path, "vae/config.json")
|
||||
with open(vae_config_path) as f:
|
||||
vae_config = json.load(f)
|
||||
vae_scale_factor = 2 ** (len(vae_config["block_out_channels"]) - 1)
|
||||
|
||||
# Create image processor
|
||||
image_processor = VaeImageProcessor(vae_scale_factor=vae_scale_factor)
|
||||
|
||||
def post_process_func(images: torch.Tensor):
|
||||
return image_processor.postprocess(images, output_type="pil")
|
||||
|
||||
return post_process_func
|
||||
```
|
||||
|
||||
**Pre-processing function (for image editing pipelines):**
|
||||
|
||||
```python
|
||||
def get_your_model_pre_process_func(
|
||||
od_config: OmniDiffusionConfig,
|
||||
):
|
||||
"""
|
||||
Create pre-processing function for image editing.
|
||||
|
||||
Returns a function that prepares input images.
|
||||
"""
|
||||
from PIL import Image
|
||||
from diffusers.image_processor import VaeImageProcessor
|
||||
|
||||
# Load VAE config
|
||||
# ... (similar to post_process_func)
|
||||
|
||||
image_processor = VaeImageProcessor(vae_scale_factor=vae_scale_factor)
|
||||
|
||||
def pre_process_func(
|
||||
request: OmniDiffusionRequest,
|
||||
):
|
||||
for i, prompt in enumerate(request.prompts):
|
||||
multi_modal_data = prompt.get("multi_modal_data", {}) if not isinstance(prompt, str) else None
|
||||
raw_image = multi_modal_data.get("image", None) if multi_modal_data is not None else None
|
||||
# image pre-processing
|
||||
# after pre-processing, update the request attributes
|
||||
...
|
||||
return request
|
||||
|
||||
return pre_process_func
|
||||
```
|
||||
|
||||
#### 2.5: Add Weight Loading Support
|
||||
|
||||
Add methods for automatic weight downloading and loading:
|
||||
|
||||
```python
|
||||
from vllm_omni.diffusion.model_loader.diffusers_loader import DiffusersPipelineLoader
|
||||
from vllm.model_executor.models.utils import AutoWeightsLoader
|
||||
|
||||
class YourModelPipeline(nn.Module):
|
||||
def __init__(self, *, od_config: OmniDiffusionConfig, prefix: str = ""):
|
||||
super().__init__()
|
||||
# ... initialization code
|
||||
|
||||
# Define weight sources for automatic loading
|
||||
self.weights_sources = [
|
||||
DiffusersPipelineLoader.ComponentSource(
|
||||
model_or_path=od_config.model,
|
||||
subfolder="transformer",
|
||||
revision=None,
|
||||
prefix="transformer.",
|
||||
fall_back_to_pt=True,
|
||||
)
|
||||
]
|
||||
|
||||
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
|
||||
"""
|
||||
Customize the weight loading behavior, such as filter weights name.
|
||||
|
||||
Args:
|
||||
weights: Iterable of (param_name, param_tensor) tuples
|
||||
|
||||
Returns:
|
||||
Set of loaded parameter names
|
||||
"""
|
||||
loader = AutoWeightsLoader(self)
|
||||
return loader.load_weights(weights)
|
||||
```
|
||||
|
||||
### Step 3: Register Model
|
||||
|
||||
Register your model in `vllm_omni/diffusion/registry.py` so vLLM-Omni can discover and load it.
|
||||
|
||||
#### 3.1: Register Pipeline Class
|
||||
|
||||
```python
|
||||
# vllm_omni/diffusion/registry.py
|
||||
|
||||
_DIFFUSION_MODELS = {
|
||||
# Format: "PipelineClassName": (module_folder, module_file, class_name)
|
||||
|
||||
# Existing models
|
||||
"QwenImagePipeline": ("qwen_image", "pipeline_qwen_image", "QwenImagePipeline"),
|
||||
"FluxPipeline": ("flux", "pipeline_flux", "FluxPipeline"),
|
||||
|
||||
# Add your model
|
||||
"YourModelPipeline": (
|
||||
"your_model_name", # Module folder name
|
||||
"pipeline_your_model", # Python file name (without .py)
|
||||
"YourModelPipeline", # Pipeline class name
|
||||
),
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.2: Register Pre/Post-Processing Function
|
||||
|
||||
```python
|
||||
# vllm_omni/diffusion/registry.py
|
||||
_DIFFUSION_PRE_PROCESS_FUNCS = {
|
||||
# arch: pre_process_func
|
||||
# `pre_process_func` function must be placed in {mod_folder}/{mod_relname}.py,
|
||||
# where mod_folder and mod_relname are defined and mapped using `_DIFFUSION_MODELS` via the `arch` key
|
||||
"GlmImagePipeline": "get_glm_image_pre_process_func",
|
||||
"QwenImageEditPipeline": "get_qwen_image_edit_pre_process_func",
|
||||
|
||||
# Add your model
|
||||
"YourModelPipeline": "get_your_model_pre_process_func", # Optional
|
||||
}
|
||||
_DIFFUSION_POST_PROCESS_FUNCS = {
|
||||
# Format: "PipelineClassName": "function_name"
|
||||
|
||||
# Existing models
|
||||
"QwenImagePipeline": "get_qwen_image_post_process_func",
|
||||
"FluxPipeline": "get_flux_post_process_func",
|
||||
|
||||
# Add your model
|
||||
"YourModelPipeline": "get_your_model_post_process_func",
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
#### 3.3: Export from Module
|
||||
|
||||
Create/update `__init__.py` to export your classes:
|
||||
|
||||
```python
|
||||
# vllm_omni/diffusion/models/your_model_name/__init__.py
|
||||
|
||||
from .pipeline_your_model import (
|
||||
YourModelPipeline,
|
||||
get_your_model_post_process_func,
|
||||
)
|
||||
from .your_model_transformer import YourModelTransformer2DModel
|
||||
|
||||
__all__ = [
|
||||
"YourModelPipeline",
|
||||
"YourModelTransformer2DModel",
|
||||
"get_your_model_post_process_func",
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 4: Add Example Script
|
||||
|
||||
|
||||
If your model is one of Text-to-Image, Text-to-Audio, Text-to-Video, Image-to-Image, Image-to-Video models, you can simply try one of the following offline inference scripts to run your model:
|
||||
|
||||
| Model Category | Offline Inference Script |
|
||||
|---|---|
|
||||
| Image-to-Image | `examples/offline_inference/image_to_image/image_edit.py` |
|
||||
| Image-to-Video | `examples/offline_inference/image_to_video/image_to_video.py` |
|
||||
| Text-to-Image | `examples/offline_inference/text_to_image/text_to_image.py` |
|
||||
| Text-to-Audio | `examples/offline_inference/text_to_audio/text_to_audio.py` |
|
||||
| Text-to-Video | `examples/offline_inference/text_to_video/text_to_video.py` |
|
||||
|
||||
|
||||
If new CLI arguments need to be added, please edit the offline inference script corresponding to your model category from the table above, and update the example inference script in its corresponding document file (e.g., `examples/offline_inference/text_to_video/text_to_video.md`).
|
||||
|
||||
For online inference, all the supported tasks are listed in `docs/user_guide/examples/online_serving/`. If your model falls into these categories, please check the corresponding documentation in this folder and the example at `examples/online_serving/TASK_NAME`. Update them accordingly if needed.
|
||||
|
||||
---
|
||||
|
||||
If your model is an Omni (understanding and generation) model, please follow the steps below.
|
||||
|
||||
#### 4.1: Create Example File
|
||||
|
||||
Taking **BAGEL** model as examples for both offline and online:
|
||||
|
||||
- Offline: `examples/offline_inference/bagel/`
|
||||
- Online: `examples/online_serving/bagel/`
|
||||
|
||||
Add **two example folders** for your model:
|
||||
|
||||
```bash
|
||||
mkdir -p examples/offline_inference/your_model_name
|
||||
mkdir -p examples/online_serving/your_model_name
|
||||
```
|
||||
|
||||
**Offline (recommended minimum):** create `examples/offline_inference/your_model_name/end2end.py` and a README.
|
||||
|
||||
- Script: `examples/offline_inference/your_model_name/end2end.py`
|
||||
- Parse args like BAGEL (`--model`, `--modality`, optional `--image-path`, `--steps`, etc.)
|
||||
- Use `from vllm_omni.entrypoints.omni import Omni` (or `OmniDiffusion` if your model is diffusion-only)
|
||||
- Save outputs (images/audio/video/text) with deterministic filenames (e.g., `output_0_0.png`)
|
||||
- Doc: `examples/offline_inference/your_model_name/README.md`
|
||||
- Include at least one runnable command, e.g.:
|
||||
|
||||
```bash
|
||||
cd examples/offline_inference/your_model_name
|
||||
python end2end.py --model your-org/your-model-name --modality text2img --prompts "A cute cat"
|
||||
```
|
||||
|
||||
#### 4.2: Add Online Serving Example (OpenAI-Compatible)
|
||||
|
||||
Mirror BAGEL’s online serving setup:
|
||||
|
||||
- Server launcher: `examples/online_serving/your_model_name/run_server.sh`
|
||||
- Wrap `vllm serve ... --omni --port ...` (and `--stage-configs-path ...` if needed)
|
||||
- Client: `examples/online_serving/your_model_name/openai_chat_client.py`
|
||||
- Send requests to `POST /v1/chat/completions`
|
||||
- Support multimodal inputs (e.g., base64 image) if your model needs it
|
||||
- Doc: `examples/online_serving/your_model_name/README.md`
|
||||
- Include both “launch server” and “send request”:
|
||||
|
||||
```bash
|
||||
# Terminal 1: launch server
|
||||
cd examples/online_serving/your_model_name
|
||||
bash run_server.sh
|
||||
|
||||
# Terminal 2: send request
|
||||
python openai_chat_client.py --prompt "A cute cat" --modality text2img
|
||||
```
|
||||
|
||||
|
||||
### Step 5: Test Your Implementation
|
||||
|
||||
Before submitting, thoroughly test your implementation.
|
||||
|
||||
#### 5.1: Performance/Speed Check
|
||||
|
||||
Manually compare **latency/throughput** and **output quality** against a Diffusers baseline.
|
||||
|
||||
For a fair comparison, keep the same **prompt**, **seed**, **resolution**, **num_inference_steps**, and **guidance settings**, and run multiple trials to reduce randomness. Record the results (and your hardware / driver / CUDA versions) in your PR description.
|
||||
|
||||
|
||||
#### 5.2 Functionality Check in CI
|
||||
|
||||
To ensure project maintainability and sustainable development, we encourage contributors to submit test code (unit tests, system tests, or end-to-end tests) alongside their code changes.
|
||||
For comprehensive testing guidelines, please refer to the [Test File Structure and Style Guide](../ci/tests_style.md).
|
||||
|
||||
---
|
||||
|
||||
## Advanced Features
|
||||
|
||||
Once basic implementation works, add advanced features for better performance.
|
||||
|
||||
### torch.compile Support
|
||||
|
||||
Enable automatic compilation for repeated blocks:
|
||||
|
||||
```python
|
||||
# In your_model_transformer.py
|
||||
|
||||
class YourModelTransformer2DModel(nn.Module):
|
||||
# Specify which blocks can be compiled
|
||||
_repeated_blocks = ["YourTransformerBlock"] # List of block class names
|
||||
|
||||
def __init__(self, ...):
|
||||
super().__init__()
|
||||
# ... initialization
|
||||
```
|
||||
|
||||
vLLM-Omni automatically compiles blocks in `_repeated_blocks` when `torch.compile` is available.
|
||||
|
||||
### Tensor Parallelism
|
||||
|
||||
See detailed guide: [How to add Tensor Parallel support](../../design/feature/tensor_parallel.md)
|
||||
|
||||
**Quick setup:**
|
||||
|
||||
1. Replace Linear layers by various parallel linear layers (e.g., `ColumnParallelLinear`) in vLLM
|
||||
2. Check `tp_size` validity: `hidden_dim`, `num_heads`, and `num_kv_heads` must be divisible by `tp_size`
|
||||
|
||||
**Usage:** Set `tensor_parallel_size` when initializing:
|
||||
```python
|
||||
omni = Omni(model="your-model", tensor_parallel_size=2)
|
||||
```
|
||||
|
||||
### CFG Parallelism
|
||||
|
||||
See detailed guide: [How to add CFG-Parallel support](../../design/feature/cfg_parallel.md)
|
||||
|
||||
**Quick setup:**
|
||||
|
||||
1. Implement `diffuse()` method
|
||||
2. Inherit `CFGParallelMixin` in your pipeline class
|
||||
|
||||
**Usage:** Set `cfg_parallel_size` when initializing:
|
||||
```python
|
||||
omni = Omni(model="your-model", cfg_parallel_size=2)
|
||||
```
|
||||
|
||||
### Sequence Parallelism
|
||||
|
||||
See detailed guide: [How to add Sequence Parallel support](../../design/feature/sequence_parallel.md)
|
||||
|
||||
**Quick setup:**
|
||||
|
||||
1. Add `_sp_plan` class attribute to transformer
|
||||
2. Specify where to shard/gather tensors
|
||||
|
||||
**Usage:** Set `ulysses_degree` and `ring_degree` when initializing:
|
||||
```python
|
||||
omni = Omni(model="your-model", ulysses_degree=2, ring_degree=2)
|
||||
```
|
||||
|
||||
### Cache Acceleration
|
||||
|
||||
#### TeaCache
|
||||
|
||||
See detailed guide: [How to add TeaCache support](../../design/feature/teacache.md)
|
||||
|
||||
**Quick setup:**
|
||||
|
||||
1. Write extractor function
|
||||
2. Register in `EXTRACTOR_REGISTRY`
|
||||
3. Add polynomial coefficients
|
||||
|
||||
**Usage:** Set `cache_backend` and `cache_config` when initializing:
|
||||
```python
|
||||
omni = Omni(model="your-model",
|
||||
cache_backend="tea_cache",
|
||||
cache_config={"rel_l1_thresh": 0.2}
|
||||
)
|
||||
|
||||
```
|
||||
|
||||
|
||||
#### Cache-DiT
|
||||
|
||||
See detailed guide: [How to add Cache-DiT support](../../design/feature/cache_dit.md)
|
||||
|
||||
**Quick setup:**
|
||||
|
||||
- For standard models: Works automatically
|
||||
- For complex architectures: Write custom cache config
|
||||
|
||||
**Usage:** Set `cache_backend` and `cache_config` when initializing:
|
||||
```python
|
||||
omni = Omni(model="your-model",
|
||||
cache_backend="cache_dit",
|
||||
cache_config={
|
||||
"Fn_compute_blocks": 1,
|
||||
"Bn_compute_blocks": 0,
|
||||
"max_warmup_steps": 4,
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
### CPU Offload
|
||||
|
||||
See detailed guide: [CPU Offloading for Diffusion Models](../../user_guide/diffusion/cpu_offload_diffusion.md)
|
||||
|
||||
vLLM-Omni provides two offloading strategies to reduce GPU memory usage:
|
||||
|
||||
1. **Model-level offload**: Mutual exclusion between DiT and encoders (only one on GPU at a time)
|
||||
2. **Layerwise (Blockwise) offload**: Keeps only a single transformer block on GPU at a time with compute-memory overlap
|
||||
|
||||
**Usage:** Enable offload when initializing:
|
||||
```python
|
||||
# Model-level offload
|
||||
omni = Omni(model="your-model", enable_cpu_offload=True)
|
||||
|
||||
# Layerwise offload
|
||||
omni = Omni(model="your-model", enable_layerwise_offload=True)
|
||||
```
|
||||
|
||||
**To support layerwise offloading:** Define the blocks attribute name in your transformer:
|
||||
|
||||
```python
|
||||
class WanTransformer3DModel(nn.Module):
|
||||
_layerwise_offload_blocks_attr = "blocks" # Attribute name containing transformer blocks
|
||||
|
||||
def __init__(self):
|
||||
self.blocks = nn.ModuleList([...]) # Transformer blocks
|
||||
```
|
||||
|
||||
**Note:** Layerwise offloading is primarily recommended for large **video generation models** where the compute cost per block is high enough to effectively overlap with memory prefetch operations.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
|
||||
**Issue: ImportError when loading model**
|
||||
|
||||
**Symptoms:** `ModuleNotFoundError` or `ImportError` when calling `Omni(model="your-model")`
|
||||
|
||||
**Causes:**
|
||||
|
||||
1. Model not registered in `registry.py`
|
||||
2. Wrong class name in registry
|
||||
3. Missing `__init__.py` exports
|
||||
|
||||
|
||||
**Issue: Shape mismatch in attention**
|
||||
|
||||
**Symptoms:** `RuntimeError: shape mismatch` in attention forward
|
||||
|
||||
**Cause:** Incorrect tensor reshaping for vLLM-Omni's attention interface
|
||||
|
||||
**Solution:** Ensure correct shapes:
|
||||
|
||||
```python
|
||||
# vLLM-Omni expects: [batch, seq_len, num_heads, head_dim]
|
||||
query = query.view(batch_size, seq_len, self.num_heads, self.head_dim)
|
||||
key = key.view(batch_size, kv_seq_len, self.num_kv_heads, self.head_dim)
|
||||
value = value.view(batch_size, kv_seq_len, self.num_kv_heads, self.head_dim)
|
||||
|
||||
hidden_states = self.attn(query, key, value, attn_metadata=attn_metadata)
|
||||
|
||||
# Reshape back: [batch, seq_len, num_heads, head_dim] → [batch, seq_len, hidden_size]
|
||||
hidden_states = hidden_states.reshape(batch_size, seq_len, -1)
|
||||
```
|
||||
|
||||
**Issue: Different outputs compared to Diffusers**
|
||||
|
||||
**Symptoms:** Generated images look different from Diffusers
|
||||
|
||||
**Causes:**
|
||||
|
||||
1. Attention backend differences (FlashAttention vs PyTorch SDPA)
|
||||
2. Missing normalization or scaling
|
||||
|
||||
**4. Issue: Out of memory (OOM)**
|
||||
|
||||
**Symptoms:** CUDA out of memory errors
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. **Reduce batch size:**
|
||||
```python
|
||||
omni.generate(prompts=[...], max_batch_size=2)
|
||||
```
|
||||
|
||||
2. **Use smaller image size:**
|
||||
```python
|
||||
sampling_params = OmniDiffusionSamplingParams(height=512, width=512)
|
||||
```
|
||||
|
||||
3. **Enable model offloading:**
|
||||
```python
|
||||
omni = Omni(model="...", enable_cpu_offload=True)
|
||||
```
|
||||
|
||||
4. **Apply vae tiling and slicing**
|
||||
```python
|
||||
omni = Omni(model="...", vae_use_slicing=True, vae_use_tiling=True,)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pull Request Checklist
|
||||
|
||||
When submitting a PR to add your model, include:
|
||||
|
||||
**1. Implementation Files**
|
||||
|
||||
- ✅ Transformer model (`xxx_transformer.py`)
|
||||
- ✅ Pipeline (`pipeline_xxx.py`)
|
||||
- ✅ Registry entries in `registry.py`
|
||||
- ✅ `__init__.py` with proper exports
|
||||
|
||||
**2. Example and Tests**
|
||||
|
||||
- ✅ Example script in `examples/`
|
||||
- ✅ Test file in `tests/e2e/`
|
||||
- ✅ Documentation (`docs/`) creation or updates
|
||||
|
||||
_Note: End-to-end test files in `tests/e2e/` are optional but strongly recommended. README updates are required for all new models._
|
||||
|
||||
**3. Documentation Updates**
|
||||
|
||||
- ✅ Add model to supported models table in `docs/models/supported_models.md`
|
||||
- ✅ If supporting acceleration features (e.g., sequence parallelism, CFG parallel), update acceleration feature tables in:
|
||||
- `docs/user_guide/diffusion_acceleration.md`
|
||||
- `docs/user_guide/diffusion/parallelism_acceleration.md`
|
||||
|
||||
---
|
||||
|
||||
## Model Recipe
|
||||
|
||||
After implementing and testing your model, please add a model recipe to the [vllm-project/recipes](https://github.com/vllm-project/recipes) repository. This helps other users understand how to use your model with vLLM-Omni.
|
||||
|
||||
**What to Include**
|
||||
|
||||
Your recipe should include:
|
||||
|
||||
1. **Model Overview**: Brief description of the model and its capabilities
|
||||
2. **Installation Instructions**: Step-by-step setup instructions including:
|
||||
- Installing vllm-omni and dependencies
|
||||
- Installing any additional required packages (e.g., xformers, diffusers)
|
||||
- Any version requirements
|
||||
3. **Usage Examples**: Command-line examples demonstrating how to run the model
|
||||
4. **Configuration Details**: Important configuration parameters and their meanings
|
||||
|
||||
**Example**
|
||||
|
||||
For reference, see the [LongCat recipe example](https://github.com/vllm-project/recipes/pull/179) which demonstrates the expected format and structure.
|
||||
|
||||
**Recipe Location**
|
||||
|
||||
Create your recipe file in the appropriate directory structure:
|
||||
- For organization-specific models: `OrganizationName/ModelName.md`
|
||||
- For general models: `ModelName.md`
|
||||
|
||||
The recipe should be a Markdown file that provides clear, reproducible instructions for users to get started with your model.
|
||||
|
||||
---
|
||||
|
||||
## Reference Implementations
|
||||
|
||||
Study these complete examples:
|
||||
|
||||
| Model | Architecture | Key Features | Files |
|
||||
|-------|--------------|--------------|-------|
|
||||
| **Qwen-Image** | Dual-stream transformer | CFG-Parallel, SP, TP, Cache | `vllm_omni/diffusion/models/qwen_image/` |
|
||||
| **Wan2.2** | Video transformer | Dual transformers, SP, CFG-Parallel | `vllm_omni/diffusion/models/wan2_2/` |
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
Adding a diffusion model to vLLM-Omni involves:
|
||||
|
||||
1. ✅ **Adapt transformer** - Replace attention, remove mixins, add config support
|
||||
2. ✅ **Adapt pipeline** - Change interface, add request handling, extract processing
|
||||
3. ✅ **Register model** - Add entries to `registry.py`
|
||||
4. ✅ **Add examples** - Provide runnable scripts
|
||||
5. ✅ **Test thoroughly** - Verify correctness and performance
|
||||
6. ✅ **Add advanced features** - Enable parallelism and acceleration (optional)
|
||||
7. ✅ **Submit PR** - Include verification results and documentation
|
||||
|
||||
**Need help?** Check reference implementations or ask in [slack.vllm.ai](https://slack.vllm.ai) or vLLM user forum at [discuss.vllm.ai](https://discuss.vllm.ai).
|
||||
@@ -0,0 +1,633 @@
|
||||
# Adding an Omni-Modality Model
|
||||
|
||||
This guide walks through the process of adding a new multi-stage model to vLLM-Omni, using **Qwen3-Omni** as a comprehensive example. Qwen3-Omni is a multi-stage omni-modality model that demonstrates the full capabilities of vLLM-Omni's architecture.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Overview](#overview)
|
||||
2. [Directory Structure](#directory-structure)
|
||||
3. [Step-by-Step Implementation](#step-by-step-implementation)
|
||||
4. [Key Components](#key-components)
|
||||
5. [Model Registration](#model-registration)
|
||||
6. [Stage Configuration](#stage-configuration)
|
||||
7. [Stage Input Processors](#stage-input-processors)
|
||||
8. [Testing](#testing)
|
||||
9. [Adding a Model Recipe](#adding-a-model-recipe)
|
||||
10. [Summary](#summary)
|
||||
|
||||
## Overview
|
||||
|
||||
vLLM-Omni supports multi-stage model architectures where different stages can run on different devices and process different modalities. The Qwen3-Omni model exemplifies this with three stages:
|
||||
|
||||
1. **Thinker Stage**: Multimodal understanding (text + audio + video) → text generation
|
||||
2. **Talker Stage**: Text embeddings → RVQ codec codes
|
||||
3. **Code2Wav Stage**: RVQ codes → audio waveform
|
||||
|
||||
Each stage is implemented as a separate model class that can be configured independently.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
When adding a new model, you'll need to create the following structure:
|
||||
|
||||
```
|
||||
vllm_omni/model_executor/models/
|
||||
└── your_model_name/ # Model directory (e.g., qwen3_omni)
|
||||
├── __init__.py # Exports main model class
|
||||
├── your_model.py # Main unified model class
|
||||
├── your_model_stage1_implementation.py # Stage 1 implementation (e.g., thinker)
|
||||
├── your_model_stage2_implementation.py # Stage 2 implementation (e.g., talker)
|
||||
└── your_model_stage3_implementation.py # Stage 3 implementation (e.g., code2wav)
|
||||
└── ... maybe other stage implementations
|
||||
|
||||
vllm_omni/model_executor/stage_input_processors/
|
||||
└── your_model_name.py # Stage transition processors
|
||||
|
||||
vllm_omni/model_executor/stage_configs/
|
||||
└── your_model_name.yaml # Stage configuration file
|
||||
```
|
||||
|
||||
## Step-by-Step Implementation
|
||||
|
||||
### Step 1: Create the Model Directory
|
||||
|
||||
Create a new directory under `vllm_omni/model_executor/models/`
|
||||
|
||||
### Step 2: Implement Stage Components
|
||||
|
||||
For Qwen3-Omni, we have three stage components:
|
||||
|
||||
#### 2.1 Thinker Stage (`qwen3_omni_moe_thinker.py`)
|
||||
|
||||
The thinker stage handles multimodal understanding. Key features:
|
||||
|
||||
- Inherits from base Qwen3 MoE model in vLLM, using vLLM fused ops & page attn to accelerate
|
||||
- Implements multimodal processing interfaces
|
||||
- Handles audio, video, and image inputs
|
||||
- Generates text outputs
|
||||
|
||||
```python
|
||||
from vllm.model_executor.models.interfaces import SupportsMultiModal, SupportsPP
|
||||
from vllm.model_executor.models.qwen3_moe import Qwen3MoeForCausalLM
|
||||
|
||||
class Qwen3OmniMoeThinkerForConditionalGeneration(
|
||||
Qwen3MoeForCausalLM,
|
||||
SupportsMultiModal,
|
||||
SupportsPP
|
||||
):
|
||||
"""Thinker stage: multimodal understanding → text generation."""
|
||||
|
||||
def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
|
||||
# Initialize base model
|
||||
# Set up multimodal processors
|
||||
# Configure audio/video/image encoders
|
||||
pass
|
||||
```
|
||||
|
||||
#### 2.2 Talker Stage (`qwen3_omni_moe_talker.py`)
|
||||
|
||||
The talker stage converts text embeddings to codec codes:
|
||||
|
||||
```python
|
||||
class Qwen3OmniMoeTalkerForConditionalGeneration(
|
||||
Qwen3MoeForCausalLM,
|
||||
SupportsPP
|
||||
):
|
||||
"""Talker stage: text embeddings → RVQ codec codes."""
|
||||
|
||||
def __init__(self, vllm_config, talker_config, prefix):
|
||||
# Initialize base model
|
||||
# Replace LM head with codec head
|
||||
# Set up text projection from thinker
|
||||
pass
|
||||
```
|
||||
|
||||
#### 2.3 Code2Wav Stage (`qwen3_omni_code2wav.py`)
|
||||
|
||||
The code2wav stage generates audio waveforms:
|
||||
|
||||
```python
|
||||
class Qwen3OmniMoeCode2Wav(nn.Module):
|
||||
"""Code2Wav stage: RVQ codes → audio waveform."""
|
||||
|
||||
def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
|
||||
# Initialize audio decoder
|
||||
# Set up codec processing
|
||||
pass
|
||||
```
|
||||
|
||||
### Step 3: Implement the Unified Model Class
|
||||
|
||||
The main model class (`qwen3_omni.py`) orchestrates all stages:
|
||||
|
||||
```python
|
||||
@MULTIMODAL_REGISTRY.register_processor(
|
||||
Qwen3OmniMoeThinkerMultiModalProcessor,
|
||||
info=Qwen3OmniMoeThinkerProcessingInfo,
|
||||
dummy_inputs=Qwen3OmniMoeThinkerDummyInputsBuilder,
|
||||
)
|
||||
class Qwen3OmniMoeForConditionalGeneration(
|
||||
nn.Module, SupportsMultiModal, SupportsPP, Qwen3OmniMoeConditionalGenerationMixin
|
||||
):
|
||||
"""
|
||||
Unified Qwen3 Omni MoE model combining thinker, talker, and code2wav.
|
||||
|
||||
Architecture:
|
||||
- Thinker: Multimodal understanding (text + audio + video) → text generation
|
||||
- Talker: Text embeddings → RVQ codec codes
|
||||
- Code2Wav: RVQ codes → audio waveform
|
||||
|
||||
Usage:
|
||||
Set `model_stage` in vllm_config to one of: "thinker", "talker", "code2wav"
|
||||
"""
|
||||
|
||||
def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
|
||||
super().__init__()
|
||||
self.have_multimodal_outputs = True
|
||||
config: Qwen3OmniMoeConfig = vllm_config.model_config.hf_config
|
||||
|
||||
# Determine which stage to initialize
|
||||
self.model_stage = vllm_config.model_config.model_stage
|
||||
|
||||
if self.model_stage == "thinker":
|
||||
# Initialize thinker model
|
||||
thinker_vllm_config = vllm_config.with_hf_config(
|
||||
config.thinker_config,
|
||||
architectures=["Qwen3OmniMoeThinkerForConditionalGeneration"]
|
||||
)
|
||||
self.thinker = init_vllm_registered_model(
|
||||
vllm_config=thinker_vllm_config,
|
||||
prefix=maybe_prefix(prefix, "thinker"),
|
||||
hf_config=config.thinker_config,
|
||||
architectures=["Qwen3OmniMoeThinkerForConditionalGeneration"],
|
||||
)
|
||||
self.model = self.thinker
|
||||
|
||||
elif self.model_stage == "talker":
|
||||
# Initialize talker model
|
||||
talker_vllm_config = vllm_config.with_hf_config(
|
||||
config.talker_config,
|
||||
architectures=["Qwen3OmniMoeTalkerForConditionalGeneration"]
|
||||
)
|
||||
self.talker = init_vllm_registered_model(
|
||||
vllm_config=talker_vllm_config,
|
||||
prefix=maybe_prefix(prefix, "talker"),
|
||||
hf_config=config.talker_config,
|
||||
architectures=["Qwen3OmniMoeTalkerForConditionalGeneration"],
|
||||
)
|
||||
self.model = self.talker
|
||||
|
||||
elif self.model_stage == "code2wav":
|
||||
# Initialize code2wav model
|
||||
code2wav_vllm_config = vllm_config.with_hf_config(
|
||||
config.code2wav_config,
|
||||
architectures=["Qwen3OmniMoeCode2Wav"]
|
||||
)
|
||||
self.code2wav = init_vllm_registered_model(
|
||||
vllm_config=code2wav_vllm_config,
|
||||
prefix=maybe_prefix(prefix, "code2wav"),
|
||||
hf_config=config.code2wav_config,
|
||||
architectures=["Qwen3OmniMoeCode2Wav"],
|
||||
)
|
||||
self.model = self.code2wav
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Invalid model_stage: {self.model_stage}. "
|
||||
f"Must be one of: 'thinker', 'talker', 'code2wav'"
|
||||
)
|
||||
```
|
||||
|
||||
#### Key Methods to Implement
|
||||
|
||||
1. **`forward()`**: Handles the forward pass for each stage
|
||||
2. **`embed_input_ids()`**: Embeds input token IDs
|
||||
3. **`embed_multimodal()`**: Processes multimodal inputs (if applicable)
|
||||
4. **`compute_logits()`**: Computes logits from hidden states
|
||||
5. **`load_weights()`**: Loads model weights with proper prefixing of different stages
|
||||
|
||||
### Step 4: Create `__init__.py`
|
||||
|
||||
Export the main model class:
|
||||
|
||||
```python
|
||||
# vllm_omni/model_executor/models/qwen3_omni/__init__.py
|
||||
from .qwen3_omni import Qwen3OmniMoeForConditionalGeneration
|
||||
|
||||
__all__ = ["Qwen3OmniMoeForConditionalGeneration"]
|
||||
```
|
||||
|
||||
## Key Components
|
||||
|
||||
### 1. Model Interfaces
|
||||
|
||||
Your model should implement the appropriate interfaces:
|
||||
|
||||
- **`SupportsMultiModal`**: For models that process multimodal inputs
|
||||
- **`SupportsPP`**: For models that support pipeline parallelism
|
||||
- **`SupportsMRoPE`**: For models using multi-dimensional RoPE (if applicable)
|
||||
|
||||
### 2. Multimodal Registration
|
||||
|
||||
If your model processes multimodal inputs, register it with the multimodal registry:
|
||||
|
||||
```python
|
||||
@MULTIMODAL_REGISTRY.register_processor(
|
||||
YourMultiModalProcessor,
|
||||
info=YourProcessingInfo,
|
||||
dummy_inputs=YourDummyInputsBuilder,
|
||||
)
|
||||
class YourModel(nn.Module, SupportsMultiModal):
|
||||
pass
|
||||
```
|
||||
|
||||
### 3. Weight Loading
|
||||
|
||||
Implement `load_weights()` to handle weight loading with proper prefixing:
|
||||
|
||||
```python
|
||||
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
|
||||
"""Load weights for all components of the omni model."""
|
||||
loaded_weights = set()
|
||||
thinker_weights = []
|
||||
talker_weights = []
|
||||
code2wav_weights = []
|
||||
|
||||
# Separate weights by component
|
||||
for k, v in weights:
|
||||
if k.startswith("thinker."):
|
||||
thinker_weights.append((k, v))
|
||||
elif k.startswith("talker."):
|
||||
talker_weights.append((k, v))
|
||||
elif k.startswith("code2wav."):
|
||||
code2wav_weights.append((k, v))
|
||||
|
||||
# Load each component's weights
|
||||
if self.thinker and thinker_weights:
|
||||
thinker_loaded = self.thinker.load_weights(thinker_weights)
|
||||
thinker_loaded = add_prefix_to_loaded_weights(thinker_loaded, "thinker")
|
||||
loaded_weights.update(thinker_loaded)
|
||||
|
||||
# Similar for talker and code2wav...
|
||||
|
||||
return loaded_weights
|
||||
```
|
||||
|
||||
### 4. Output Format
|
||||
|
||||
Use `OmniOutput` for stage outputs:
|
||||
|
||||
```python
|
||||
from vllm_omni.model_executor.models.output_templates import OmniOutput
|
||||
|
||||
# In forward method
|
||||
return OmniOutput(
|
||||
text_hidden_states=hidden_states,
|
||||
multimodal_outputs={"additional_data": data},
|
||||
next_token_id=next_token_id,
|
||||
)
|
||||
```
|
||||
|
||||
## Model Registration
|
||||
|
||||
Register your model in `vllm_omni/model_executor/models/registry.py`:
|
||||
|
||||
```python
|
||||
_OMNI_MODELS = {
|
||||
# ... existing models ...
|
||||
|
||||
# Your new model
|
||||
"YourModelForConditionalGeneration": (
|
||||
"your_model_name", # Module folder name
|
||||
"your_model", # Module file name (without .py)
|
||||
"YourModelForConditionalGeneration", # Class name
|
||||
),
|
||||
"YourModelThinkerForConditionalGeneration": (
|
||||
"your_model_name",
|
||||
"your_model_thinker",
|
||||
"YourModelThinkerForConditionalGeneration",
|
||||
),
|
||||
# ... other stages ...
|
||||
}
|
||||
```
|
||||
|
||||
The registry uses lazy loading, so the model class is imported only when needed.
|
||||
|
||||
## Stage Configuration
|
||||
|
||||
Create a YAML configuration file in `vllm_omni/model_executor/stage_configs/`. For a complete example, see the [Qwen3-Omni configuration file](gh-file:vllm_omni/model_executor/stage_configs/qwen3_omni_moe.yaml).
|
||||
|
||||
### Key Configuration Fields
|
||||
|
||||
- **`model_stage`**: Which stage to run ("thinker", "talker", "code2wav", etc.)
|
||||
- **`model_arch`**: The model architecture name (must match registry)
|
||||
- **`engine_input_source`**: List of stage IDs that provide input to this stage
|
||||
- **`custom_process_input_func`**: Function to process inputs from previous stages
|
||||
- **`final_output`**: Whether this stage produces the final output (True/False)
|
||||
- **`final_output_type`**: Type of final output ("text", "audio", "image", etc.)
|
||||
|
||||
## Stage Input Processors
|
||||
|
||||
Stage transitions are the mechanism by which outputs from one stage are converted into inputs for the next stage. This section explains where and how stage transitions occur.
|
||||
|
||||
### Where Stage Transitions Are Called
|
||||
|
||||
Stage transitions happen automatically in the orchestrator (`OmniLLM` class) during the generation loop. Here's the detailed flow:
|
||||
|
||||
1. **Location**: `vllm_omni/entrypoints/omni_llm.py` in the `_run_generation()` method
|
||||
2. **Trigger**: When a stage completes processing and produces outputs
|
||||
3. **Execution Flow**:
|
||||
```python
|
||||
# In omni_llm.py, _run_generation() method (around line 345-460)
|
||||
|
||||
# Main orchestrator loop polls each stage for completed requests
|
||||
for stage_id, stage in enumerate(self.stage_list):
|
||||
result = stage.try_collect() # Get completed request
|
||||
if result is None:
|
||||
continue
|
||||
|
||||
# Store outputs from this stage
|
||||
engine_outputs = _load(result, obj_key="engine_outputs", shm_key="engine_outputs_shm")
|
||||
stage.set_engine_outputs(engine_outputs)
|
||||
|
||||
# Check if there's a next stage to forward to
|
||||
next_stage_id = stage_id + 1
|
||||
if next_stage_id < len(self.stage_list):
|
||||
next_stage: OmniStage = self.stage_list[next_stage_id]
|
||||
|
||||
# THIS IS WHERE STAGE TRANSITION HAPPENS
|
||||
next_inputs = next_stage.process_engine_inputs(
|
||||
self.stage_list,
|
||||
[request_id_to_prompt[req_id]]
|
||||
)
|
||||
|
||||
# Submit to next stage
|
||||
task = {
|
||||
"type": OmniStageTaskType.GENERATE,
|
||||
"request_id": req_id,
|
||||
"engine_inputs": next_inputs[0],
|
||||
"sampling_params": sampling_params_list[next_stage_id],
|
||||
}
|
||||
next_stage.submit(task)
|
||||
```
|
||||
|
||||
### How Stage Transitions Work
|
||||
|
||||
The stage transition process follows these steps:
|
||||
|
||||
1. **Stage Completion**: When a stage finishes processing a request, it stores outputs via `stage.set_engine_outputs(engine_outputs)`
|
||||
|
||||
2. **Transition Detection**: The orchestrator checks if there's a next stage and calls `process_engine_inputs()` on it
|
||||
|
||||
3. **Input Processing**: The `process_engine_inputs()` method in `OmniStage` (`omni_stage.py`) handles the transition:
|
||||
```python
|
||||
def process_engine_inputs(
|
||||
self, stage_list: list[Any], prompt: OmniTokensPrompt | TextPrompt = None
|
||||
) -> list[OmniTokensPrompt | TextPrompt]:
|
||||
"""Process engine inputs for this stage from upstream stage outputs."""
|
||||
|
||||
if self.custom_process_input_func is None:
|
||||
# Default behavior: pass token IDs directly
|
||||
# Extract outputs from source stage
|
||||
source_stage_id = self.engine_input_source[0]
|
||||
source_outputs = stage_list[source_stage_id].engine_outputs
|
||||
# ... create OmniTokensPrompt from token_ids ...
|
||||
else:
|
||||
# Custom transition function (YOUR CODE HERE)
|
||||
return self.custom_process_input_func(
|
||||
stage_list,
|
||||
self.engine_input_source,
|
||||
prompt,
|
||||
self.requires_multimodal_data
|
||||
)
|
||||
```
|
||||
- If `custom_process_input_func` is configured, it calls that function
|
||||
- Otherwise, it uses default behavior (passing token IDs directly)
|
||||
|
||||
4. **Custom Function Execution**: Your custom function receives:
|
||||
- `stage_list`: List of all stage objects (to access upstream stage outputs)
|
||||
- `engine_input_source`: List of source stage IDs (e.g., `[0]` for stage 0)
|
||||
- `prompt`: Original prompt data (for preserving multimodal data)
|
||||
- `requires_multimodal_data`: Whether multimodal data is required
|
||||
|
||||
5. **Output Format**: The function must return a list of `OmniTokensPrompt` objects ready for the next stage
|
||||
|
||||
### Data Structures in Stage Transitions
|
||||
|
||||
Understanding the data structures is crucial for implementing stage transitions:
|
||||
|
||||
**Input to your function:**
|
||||
- `stage_list[source_stage_id].engine_outputs`: List of `EngineCoreOutput` objects
|
||||
- Each contains `outputs`: List of `RequestOutput` objects
|
||||
- Each `RequestOutput` has:
|
||||
- `token_ids`: Generated token IDs
|
||||
- `multimodal_output`: Dict with keys like `"code_predictor_codes"`, etc.
|
||||
- These are the hidden states or intermediate outputs from the model's forward pass
|
||||
- `prompt_token_ids`: Original prompt token IDs
|
||||
|
||||
**Output from your function:**
|
||||
- Must return `list[OmniTokensPrompt]` where each `OmniTokensPrompt` contains:
|
||||
- `prompt_token_ids`: List[int] - Token IDs for the next stage
|
||||
- `additional_information`: Dict[str, Any] - Optional metadata (e.g., embeddings, hidden states)
|
||||
- `multi_modal_data`: Optional multimodal data if needed
|
||||
|
||||
### How Model Outputs Are Stored
|
||||
|
||||
The model's `forward()` method returns an `OmniOutput` object that contains:
|
||||
- `text_hidden_states`: Final hidden states for text generation
|
||||
- `multimodal_outputs`: Dict containing intermediate outputs
|
||||
|
||||
These outputs are captured during the forward pass and stored in `multimodal_output` with specific keys:
|
||||
|
||||
```python
|
||||
# In your model's forward() method (e.g., qwen3_omni.py)
|
||||
def forward(self, ...):
|
||||
# ... processing ...
|
||||
|
||||
# For thinker stage: capture embeddings and hidden states
|
||||
multimodal_outputs = {
|
||||
"0": captured_embeddings, # Layer 0 embeddings
|
||||
"24": captured_hidden_states, # Layer 24 hidden states
|
||||
"tts_bos_embed": tts_bos_embed,
|
||||
"tts_eos_embed": tts_eos_embed,
|
||||
# ... other intermediate outputs ...
|
||||
}
|
||||
|
||||
return OmniOutput(
|
||||
text_hidden_states=hidden_states,
|
||||
multimodal_outputs=multimodal_outputs,
|
||||
)
|
||||
```
|
||||
|
||||
These keys are then accessible in your stage transition function:
|
||||
```python
|
||||
# In stage_input_processors/qwen3_omni.py
|
||||
thinker_embeddings = output.multimodal_output["0"] # Access by key
|
||||
thinker_hidden_states = output.multimodal_output["24"]
|
||||
```
|
||||
|
||||
### Key Points
|
||||
|
||||
1. **Accessing Upstream Outputs**: Use `stage_list[source_stage_id].engine_outputs` to get outputs from the source stage
|
||||
2. **Extracting Data**: Access `output.multimodal_output[key]` to get specific hidden states or intermediate results
|
||||
- Keys are defined by your model's `forward()` method when it creates `multimodal_outputs`
|
||||
3. **Device Management**: Move tensors to appropriate devices (CPU for serialization, GPU for processing)
|
||||
4. **Shape Transformations**: Reshape tensors as needed for the next stage (e.g., flattening codec codes)
|
||||
5. **Batch Handling**: Process each request in the batch separately and return a list
|
||||
|
||||
### Complete Flow Diagram
|
||||
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" src="https://raw.githubusercontent.com/vllm-project/vllm-omni/refs/heads/main/docs/source/architecture/vllm-omni-dataflow-between-stages.png">
|
||||
<img alt="Data Flow between stages" src="https://raw.githubusercontent.com/vllm-project/vllm-omni/refs/heads/main/docs/source/architecture/vllm-omni-dataflow-between-stages.png" width=55%>
|
||||
</picture>
|
||||
</p>
|
||||
|
||||
### Implementation Example
|
||||
|
||||
Create stage transition processors in `vllm_omni/model_executor/stage_input_processors/your_model_name.py`:
|
||||
|
||||
```python
|
||||
# qwen3_omni.py
|
||||
|
||||
def thinker2talker(
|
||||
stage_list: list[Any],
|
||||
engine_input_source: list[int],
|
||||
prompt: OmniTokensPrompt | TextPrompt | None = None,
|
||||
requires_multimodal_data: bool = False,
|
||||
) -> list[OmniTokensPrompt]:
|
||||
"""
|
||||
Process thinker outputs to create talker inputs.
|
||||
|
||||
Args:
|
||||
stage_list: List of stage objects
|
||||
engine_input_source: Source stage IDs (typically [0] for thinker)
|
||||
prompt: Original prompt data
|
||||
|
||||
Returns:
|
||||
List of OmniTokensPrompt for talker stage
|
||||
"""
|
||||
source_stage_id = engine_input_source[0]
|
||||
thinker_outputs = stage_list[source_stage_id].engine_outputs
|
||||
talker_inputs = []
|
||||
|
||||
for thinker_output in thinker_outputs:
|
||||
output = thinker_output.outputs[0]
|
||||
# Extract thinker embeddings and hidden states
|
||||
thinker_embeddings = output.multimodal_output["0"].float().clone().detach().cuda()
|
||||
thinker_hidden_states = output.multimodal_output["24"].float().clone().detach().cuda()
|
||||
|
||||
info = {
|
||||
"thinker_embeddings": thinker_embeddings,
|
||||
"thinker_hidden_states": thinker_hidden_states,
|
||||
"thinker_sequences": thinker_output.prompt_token_ids + output.token_ids,
|
||||
"thinker_input_ids": thinker_output.prompt_token_ids,
|
||||
}
|
||||
|
||||
talker_inputs.append(
|
||||
OmniTokensPrompt(
|
||||
prompt_token_ids=[0] * computed_length,
|
||||
additional_information=info,
|
||||
multi_modal_data=None,
|
||||
)
|
||||
)
|
||||
|
||||
return talker_inputs
|
||||
|
||||
|
||||
def talker2code2wav(
|
||||
stage_list: list[Any],
|
||||
engine_input_source: list[int],
|
||||
prompt: OmniTokensPrompt | TextPrompt | None = None,
|
||||
requires_multimodal_data: bool = False,
|
||||
) -> list[OmniTokensPrompt]:
|
||||
"""
|
||||
Process talker outputs to create code2wav inputs.
|
||||
"""
|
||||
source_stage_id = engine_input_source[0]
|
||||
talker_outputs = stage_list[source_stage_id].engine_outputs
|
||||
code2wav_inputs = []
|
||||
|
||||
for talker_output in talker_outputs:
|
||||
output = talker_output.outputs[0]
|
||||
# Extract codec codes
|
||||
codec_codes = (
|
||||
output.multimodal_output["code_predictor_codes"]
|
||||
.to(torch.long)
|
||||
.transpose(0, 1)
|
||||
.cpu()
|
||||
.to(torch.long)
|
||||
.reshape(-1)
|
||||
.tolist()
|
||||
)
|
||||
|
||||
code2wav_inputs.append(
|
||||
OmniTokensPrompt(
|
||||
prompt_token_ids=codec_codes,
|
||||
multi_modal_data=None,
|
||||
)
|
||||
)
|
||||
|
||||
return code2wav_inputs
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
For comprehensive testing guidelines, please refer to the [Test File Structure and Style Guide](../ci/tests_style.md).
|
||||
|
||||
## Adding a Model Recipe
|
||||
|
||||
After implementing and testing your model, please add a model recipe to the [vllm-project/recipes](https://github.com/vllm-project/recipes) repository. This helps other users understand how to use your model with vLLM-Omni.
|
||||
|
||||
### What to Include
|
||||
|
||||
Your recipe should include:
|
||||
|
||||
1. **Model Overview**: Brief description of the model and its capabilities
|
||||
2. **Installation Instructions**: Step-by-step setup instructions including:
|
||||
- Installing vllm-omni and dependencies
|
||||
- Installing any additional required packages (e.g., xformers, diffusers)
|
||||
- Any version requirements
|
||||
3. **Usage Examples**: Command-line examples demonstrating how to run the model
|
||||
4. **Configuration Details**: Important configuration parameters and their meanings
|
||||
|
||||
### Example
|
||||
|
||||
For reference, see the [LongCat recipe example](https://github.com/vllm-project/recipes/pull/179) which demonstrates the expected format and structure.
|
||||
|
||||
### Recipe Location
|
||||
|
||||
Create your recipe file in the appropriate directory structure:
|
||||
- For organization-specific models: `OrganizationName/ModelName.md`
|
||||
- For general models: `ModelName.md`
|
||||
|
||||
The recipe should be a Markdown file that provides clear, reproducible instructions for users to get started with your model.
|
||||
|
||||
## Summary
|
||||
|
||||
Adding a new model to vLLM-Omni involves:
|
||||
|
||||
1. **Create model directory structure** with stage implementations
|
||||
2. **Implement unified model class** that orchestrates stages
|
||||
3. **Register model** in `registry.py`
|
||||
4. **Create stage configuration** YAML file
|
||||
5. **Implement stage input processors** for stage transitions
|
||||
6. **Write tests** to verify functionality
|
||||
7. **Add model recipe** to the [vllm-project/recipes](https://github.com/vllm-project/recipes) repository (see [Adding a Model Recipe](#adding-a-model-recipe) section)
|
||||
|
||||
### Qwen3-Omni Reference Files
|
||||
|
||||
For a complete reference implementation, see:
|
||||
|
||||
- **Main model**: `vllm_omni/model_executor/models/qwen3_omni/qwen3_omni.py`
|
||||
- **Thinker**: `vllm_omni/model_executor/models/qwen3_omni/qwen3_omni_moe_thinker.py`
|
||||
- **Talker**: `vllm_omni/model_executor/models/qwen3_omni/qwen3_omni_moe_talker.py`
|
||||
- **Code2Wav**: `vllm_omni/model_executor/models/qwen3_omni/qwen3_omni_code2wav.py`
|
||||
- **Stage config**: `vllm_omni/model_executor/stage_configs/qwen3_omni_moe.yaml`
|
||||
- **Input processors**: `vllm_omni/model_executor/stage_input_processors/qwen3_omni.py`
|
||||
- **Registry**: `vllm_omni/model_executor/models/registry.py`
|
||||
- **Testing**: `vllm_omni/tests/e2e/offline_inference/test_qwen3_omni.py`
|
||||
|
||||
For more information, see:
|
||||
- [Architecture Overview](../../design/architecture_overview.md)
|
||||
- [Supported Models](../../models/supported_models.md)
|
||||
- [Stage Configuration Guide](../../configuration/stage_configs.md)
|
||||
@@ -0,0 +1,149 @@
|
||||
# Profiling vLLM-Omni
|
||||
|
||||
> **Warning:** Profiling incurs significant overhead. Use only for development and debugging, never in production.
|
||||
|
||||
vLLM-Omni uses the PyTorch Profiler to analyze performance across both **multi-stage omni-modality models** and **diffusion models**.
|
||||
|
||||
### 1. Set the Output Directory
|
||||
Before running any script, set this environment variable. The system detects this and automatically saves traces here.
|
||||
|
||||
```bash
|
||||
export VLLM_TORCH_PROFILER_DIR=./profiles
|
||||
```
|
||||
|
||||
### 2. Profiling Omni-Modality Models
|
||||
|
||||
It is best to limit profiling to one iteration to keep trace files manageable.
|
||||
|
||||
```bash
|
||||
export VLLM_PROFILER_MAX_ITERS=1
|
||||
```
|
||||
|
||||
**Selective Stage Profiling**
|
||||
The profiler is default to function across all stages. But It is highly recommended to profile specific stages by passing the stages list, preventing from producing too large trace files:
|
||||
```python
|
||||
# Profile all stages
|
||||
omni_llm.start_profile()
|
||||
|
||||
# Only profile Stage 1
|
||||
omni_llm.start_profile(stages=[1])
|
||||
```
|
||||
|
||||
```python
|
||||
# Stage 0 (Thinker) and Stage 2 (Audio Decoder) for qwen omni
|
||||
omni_llm.start_profile(stages=[0, 2])
|
||||
```
|
||||
|
||||
**Python Usage**: Wrap your generation logic with `start_profile()` and `stop_profile()`.
|
||||
|
||||
```python
|
||||
from vllm_omni import omni_llm
|
||||
|
||||
profiler_enabled = bool(os.getenv("VLLM_TORCH_PROFILER_DIR"))
|
||||
|
||||
# 1. Start profiling if enabled
|
||||
if profiler_enabled:
|
||||
omni_llm.start_profile(stages=[0])
|
||||
|
||||
# Initialize generator
|
||||
omni_generator = omni_llm.generate(prompts, sampling_params_list, py_generator=args.py_generator)
|
||||
|
||||
total_requests = len(prompts)
|
||||
processed_count = 0
|
||||
|
||||
# Main Processing Loop
|
||||
for stage_outputs in omni_generator:
|
||||
|
||||
# ... [Output processing logic for text/audio would go here] ...
|
||||
|
||||
# Update count to track when to stop profiling
|
||||
processed_count += len(stage_outputs.request_output)
|
||||
|
||||
# 2. Check if all requests are done to stop the profiler safely
|
||||
if profiler_enabled and processed_count >= total_requests:
|
||||
print(f"[Info] Processed {processed_count}/{total_requests}. Stopping profiler inside active loop...")
|
||||
|
||||
# Stop the profiler while workers are still active
|
||||
omni_llm.stop_profile()
|
||||
|
||||
# Wait for traces to flush to disk
|
||||
print("[Info] Waiting 30s for workers to write trace files to disk...")
|
||||
time.sleep(30)
|
||||
print("[Info] Trace export wait time finished.")
|
||||
|
||||
omni_llm.close()
|
||||
```
|
||||
|
||||
|
||||
**Examples**:
|
||||
|
||||
1. **Qwen2.5-Omni**: [https://github.com/vllm-project/vllm-omni/blob/main/examples/offline_inference/qwen2_5_omni/end2end.py](https://github.com/vllm-project/vllm-omni/blob/main/examples/offline_inference/qwen2_5_omni/end2end.py)
|
||||
|
||||
2. **Qwen3-Omni**: [https://github.com/vllm-project/vllm-omni/blob/main/examples/offline_inference/qwen3_omni/end2end.py](https://github.com/vllm-project/vllm-omni/blob/main/examples/offline_inference/qwen3_omni/end2end.py)
|
||||
|
||||
|
||||
### 3. Profiling diffusion models
|
||||
|
||||
Diffusion profiling is End-to-End, capturing encoding, denoising loops, and decoding.
|
||||
|
||||
**CLI Usage:**
|
||||
```python
|
||||
|
||||
python image_to_video.py \
|
||||
--model Wan-AI/Wan2.2-I2V-A14B-Diffusers \
|
||||
--image qwen-bear.png \
|
||||
--prompt "A cat playing with yarn, smooth motion" \
|
||||
\
|
||||
# Minimize Spatial Dimensions (Optional but helpful):
|
||||
# Drastically reduces memory usage so the profiler doesn't
|
||||
# crash due to overhead, though for accurate performance
|
||||
# tuning you often want target resolutions.
|
||||
--height 48 \
|
||||
--width 64 \
|
||||
\
|
||||
# Minimize Temporal Dimension (Frames):
|
||||
# Video models process 3D tensors (Time, Height, Width).
|
||||
# Reducing frames to the absolute minimum (2) keeps the
|
||||
# tensor size small, ensuring the trace file doesn't become
|
||||
# multi-gigabytes in size.
|
||||
--num-frames 2 \
|
||||
\
|
||||
# Minimize Iteration Loop (Steps):
|
||||
# This is the most critical setting for profiling.
|
||||
# Diffusion models run the same loop X times.
|
||||
# Profiling 2 steps gives you the exact same performance
|
||||
# data as 50 steps, but saves minutes of runtime and
|
||||
# prevents the trace viewer from freezing.
|
||||
--num-inference-steps 2 \
|
||||
\
|
||||
--guidance-scale 5.0 \
|
||||
--guidance-scale-high 6.0 \
|
||||
--boundary-ratio 0.875 \
|
||||
--flow-shift 12.0 \
|
||||
--fps 16 \
|
||||
--output i2v_output.mp4
|
||||
|
||||
```
|
||||
|
||||
**Examples**:
|
||||
|
||||
1. **Qwen image edit**: [https://github.com/vllm-project/vllm-omni/blob/main/examples/offline_inference/image_to_image/image_edit.py](https://github.com/vllm-project/vllm-omni/blob/main/examples/offline_inference/image_to_image/image_edit.py)
|
||||
|
||||
2. **Wan-AI/Wan2.2-I2V-A14B-Diffusers**: [https://github.com/vllm-project/vllm-omni/tree/main/examples/offline_inference/image_to_video](https://github.com/vllm-project/vllm-omni/tree/main/examples/offline_inference/image_to_video)
|
||||
|
||||
> **Note:**
|
||||
As of now, asynchronous (online) profiling is not fully supported in vLLM-Omni. While start_profile() and stop_profile() methods exist, they are only reliable in offline inference scripts (e.g., the provided end2end.py examples). Do not use them in server-mode or streaming scenarios—traces may be incomplete or fail to flush.
|
||||
|
||||
### 4. Analyzing Omni Traces
|
||||
|
||||
Output files are saved to your configured ```VLLM_TORCH_PROFILER_DIR```.
|
||||
|
||||
**Output**
|
||||
**Chrome Trace** (```.json.gz```): Visual timeline of kernels and stages. Open in Perfetto UI.
|
||||
|
||||
**Viewing Tools:**
|
||||
|
||||
- [Perfetto](https://ui.perfetto.dev/)(recommended)
|
||||
- ```chrome://tracing```(Chrome only)
|
||||
|
||||
**Note**: vLLM-Omni reuses the PyTorch Profiler infrastructure from vLLM. See the official vLLM profiler documentation: [vLLM Profiling Guide](https://docs.vllm.ai/en/stable/contributing/profiling/)
|
||||
@@ -1,143 +0,0 @@
|
||||
# API Design Documentation
|
||||
|
||||
This document contains comprehensive API design documentation for all core modules in vLLM-Omni. These templates provide a standardized structure for designing and implementing the core, engine, executor, and worker modules.
|
||||
|
||||
## 📋 Module API
|
||||
|
||||
### Core Module API
|
||||
**Core module** provides fundamental scheduling, caching, and resource management functionality.
|
||||
|
||||
**Key Components:**
|
||||
- Request scheduling and prioritization
|
||||
- DiT cache management for diffusion models
|
||||
- Resource allocation and coordination
|
||||
- Inter-module communication
|
||||
|
||||
### Engine Module API
|
||||
**Engine module** handles model loading, inference execution, and output processing.
|
||||
|
||||
**Key Components:**
|
||||
- Model loading and initialization
|
||||
- Inference execution for AR and diffusion models
|
||||
- Input preprocessing for various modalities
|
||||
- Output postprocessing and formatting
|
||||
|
||||
### Executor Module API
|
||||
**Executor module** coordinates and manages request execution across different engines and workers.
|
||||
|
||||
**Key Components:**
|
||||
- Request routing and load balancing
|
||||
- Execution pipeline coordination
|
||||
- Worker management and task distribution
|
||||
- Error handling and recovery
|
||||
|
||||
### Worker Module API
|
||||
**Worker module** provides the actual execution environment for model inference.
|
||||
|
||||
**Key Components:**
|
||||
- Model execution and inference
|
||||
- GPU resource management
|
||||
- Request batching and processing
|
||||
- Performance optimization
|
||||
|
||||
|
||||
## 🔧 Implementation Guidelines
|
||||
|
||||
### Code Standards
|
||||
- Use type hints for all methods
|
||||
- Include comprehensive docstrings
|
||||
- Follow PEP 8 style guidelines
|
||||
- Use async/await for I/O operations
|
||||
|
||||
### Error Handling
|
||||
- Define specific exception types
|
||||
- Provide meaningful error messages
|
||||
- Include error recovery strategies
|
||||
- Log errors appropriately
|
||||
|
||||
### Configuration
|
||||
- Use dataclasses for configuration
|
||||
- Provide sensible defaults
|
||||
- Include validation methods
|
||||
- Support environment variables
|
||||
|
||||
### Testing
|
||||
- Write unit tests for all public methods
|
||||
- Include integration tests
|
||||
- Test error conditions
|
||||
- Validate configuration options
|
||||
|
||||
## 🚀 Getting Started
|
||||
|
||||
### For Developers
|
||||
|
||||
1. **Choose a Module**
|
||||
- Pick the module you want to work on
|
||||
- Read the corresponding API documentation
|
||||
- Understand the responsibilities and interfaces
|
||||
|
||||
2. **Set Up Development Environment**
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://github.com/vllm-project/vllm-omni.git
|
||||
cd vllm-omni
|
||||
|
||||
# Install dependencies
|
||||
pip install -e ".[dev]"
|
||||
|
||||
# Set up pre-commit hooks
|
||||
pre-commit install
|
||||
```
|
||||
|
||||
3. **Start Implementation**
|
||||
- Create the module directory
|
||||
- Implement base classes first
|
||||
- Add concrete implementations
|
||||
- Write tests as you go
|
||||
|
||||
### For Contributors
|
||||
|
||||
1. **Review API Documentation**
|
||||
- Read through all module APIs
|
||||
- Understand the overall architecture
|
||||
- Identify areas for improvement
|
||||
|
||||
2. **Propose Changes**
|
||||
- Create issues for API changes
|
||||
- Discuss in pull requests
|
||||
- Update documentation accordingly
|
||||
|
||||
3. **Submit Contributions**
|
||||
- Follow the coding standards
|
||||
- Include tests for new features
|
||||
- Update documentation
|
||||
- Submit pull requests
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
We welcome contributions to improve the API documentation and implementation. Please:
|
||||
|
||||
1. **Follow the Template Structure**
|
||||
- Use the [api template](https://github.com/vllm-project/vllm-omni/docs/contributing/design_documents/api_design_template.md) as a guide
|
||||
- Maintain consistency across modules
|
||||
- Include all required sections
|
||||
|
||||
2. **Provide Working Examples**
|
||||
- Test all code examples
|
||||
- Include both basic and advanced usage
|
||||
- Show integration patterns
|
||||
|
||||
3. **Keep Documentation Updated**
|
||||
- Update docs when APIs change
|
||||
- Version control documentation changes
|
||||
- Maintain backward compatibility notes
|
||||
|
||||
## 📝 Notes
|
||||
|
||||
- All API documentation should be kept up-to-date with implementation
|
||||
- Code examples should be tested and working
|
||||
- Configuration options should be validated
|
||||
- Error handling should be comprehensive
|
||||
- Performance considerations should be documented
|
||||
|
||||
For questions or suggestions about the API documentation, please open an issue or start a discussion in the repository.
|
||||
@@ -1,336 +0,0 @@
|
||||
# API Design Template for vLLM-Omni Modules
|
||||
|
||||
This template provides a standardized structure for designing APIs for core, engine, executor, and worker modules in vLLM-Omni. Use this template to ensure consistency and completeness across all modules.
|
||||
|
||||
## Template Structure
|
||||
|
||||
### 1. Module Overview
|
||||
- **Purpose**: What this module does
|
||||
- **Responsibilities**: Key responsibilities of the module
|
||||
- **Dependencies**: What other modules this depends on
|
||||
- **Integration Points**: How it integrates with other modules
|
||||
|
||||
### 2. Core Classes/Interfaces
|
||||
- **Base Classes**: Abstract base classes and interfaces
|
||||
- **Implementation Classes**: Concrete implementations
|
||||
- **Data Structures**: Key data structures and models
|
||||
|
||||
### 3. Public API Methods
|
||||
- **Initialization**: Constructor and setup methods
|
||||
- **Core Operations**: Main functionality methods
|
||||
- **Configuration**: Configuration and parameter methods
|
||||
- **Lifecycle Management**: Start, stop, cleanup methods
|
||||
- **Monitoring**: Status, metrics, and debugging methods
|
||||
|
||||
### 4. Configuration
|
||||
- **Configuration Classes**: Dataclasses or config objects
|
||||
- **Required Parameters**: Must-have configuration
|
||||
- **Optional Parameters**: Optional configuration with defaults
|
||||
- **Validation**: Parameter validation rules
|
||||
|
||||
### 5. Error Handling
|
||||
- **Custom Exceptions**: Module-specific exceptions
|
||||
- **Error Codes**: Standardized error codes
|
||||
- **Recovery Strategies**: How to handle and recover from errors
|
||||
|
||||
### 6. Examples
|
||||
- **Basic Usage**: Simple usage examples
|
||||
- **Advanced Usage**: Complex scenarios
|
||||
- **Integration Examples**: How to use with other modules
|
||||
|
||||
---
|
||||
|
||||
## Example: Core Module Template
|
||||
|
||||
### 1. Module Overview
|
||||
|
||||
**Purpose**: The core module provides fundamental scheduling and caching functionality for vLLM-Omni.
|
||||
|
||||
**Responsibilities**:
|
||||
- Request scheduling and prioritization
|
||||
- DiT cache management
|
||||
- Resource allocation and coordination
|
||||
- Inter-module communication
|
||||
|
||||
**Dependencies**:
|
||||
- `vllm_omni.request` - Request handling
|
||||
- `vllm_omni.config` - Configuration management
|
||||
- `vllm_omni.utils` - Utility functions
|
||||
|
||||
**Integration Points**:
|
||||
- Receives requests from entrypoints
|
||||
- Coordinates with engine modules
|
||||
- Manages worker allocation
|
||||
- Provides status to monitoring systems
|
||||
|
||||
### 2. Core Classes/Interfaces
|
||||
|
||||
```python
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import List, Optional, Dict, Any
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
|
||||
class DiTCacheManager:
|
||||
"""Manages DiT cache for diffusion models."""
|
||||
|
||||
def __init__(self, config: DiTCacheConfig):
|
||||
self.config = config
|
||||
self.cache = {}
|
||||
self.cache_stats = {}
|
||||
|
||||
async def get_cache(self, cache_key: str) -> Optional[Any]:
|
||||
"""Retrieve cached data."""
|
||||
pass
|
||||
|
||||
async def set_cache(self, cache_key: str, data: Any) -> None:
|
||||
"""Store data in cache."""
|
||||
pass
|
||||
|
||||
async def invalidate_cache(self, cache_key: str) -> None:
|
||||
"""Invalidate cached data."""
|
||||
pass
|
||||
```
|
||||
|
||||
### 3. Public API Methods
|
||||
|
||||
#### Initialization
|
||||
```python
|
||||
class OmniScheduler():
|
||||
def __init__(self, config: SchedulerConfig):
|
||||
"""Initialize the core scheduler."""
|
||||
self.config = config
|
||||
self.scheduler = self._create_scheduler()
|
||||
self.cache_manager = DiTCacheManager(config.dit_cache_config)
|
||||
self._running = False
|
||||
|
||||
def _create_scheduler(self) -> BaseScheduler:
|
||||
"""Factory method to create appropriate scheduler."""
|
||||
if self.config.scheduler_type == SchedulerType.FIFO:
|
||||
return FIFOScheduler(self.config)
|
||||
elif self.config.scheduler_type == SchedulerType.PRIORITY:
|
||||
return PriorityScheduler(self.config)
|
||||
# ... other scheduler types
|
||||
```
|
||||
|
||||
#### Core Operations
|
||||
```python
|
||||
async def schedule(self, request: OmniRequest) -> bool:
|
||||
"""
|
||||
Schedule a request for processing.
|
||||
|
||||
Args:
|
||||
request: The request to schedule
|
||||
|
||||
Returns:
|
||||
bool: True if successfully scheduled, False otherwise
|
||||
|
||||
Raises:
|
||||
SchedulerError: If scheduling fails
|
||||
QueueFullError: If queue is at capacity
|
||||
"""
|
||||
pass
|
||||
```
|
||||
|
||||
#### Configuration
|
||||
```python
|
||||
def update_config(self, new_config: SchedulerConfig) -> None:
|
||||
"""Update scheduler configuration."""
|
||||
pass
|
||||
|
||||
def get_config(self) -> SchedulerConfig:
|
||||
"""Get current configuration."""
|
||||
pass
|
||||
```
|
||||
|
||||
#### Lifecycle Management
|
||||
```python
|
||||
async def start(self) -> None:
|
||||
"""Start the scheduler."""
|
||||
self._running = True
|
||||
# Start background tasks
|
||||
|
||||
async def stop(self) -> None:
|
||||
"""Stop the scheduler gracefully."""
|
||||
self._running = False
|
||||
# Cleanup resources
|
||||
|
||||
async def shutdown(self) -> None:
|
||||
"""Force shutdown the scheduler."""
|
||||
# Immediate cleanup
|
||||
```
|
||||
|
||||
#### Monitoring
|
||||
```python
|
||||
def get_status(self) -> Dict[str, Any]:
|
||||
"""Get current scheduler status."""
|
||||
return {
|
||||
"running": self._running,
|
||||
"queue_size": self.scheduler.queue_size(),
|
||||
"processed_requests": self.scheduler.processed_count(),
|
||||
"cache_hit_rate": self.cache_manager.get_hit_rate()
|
||||
}
|
||||
|
||||
def get_metrics(self) -> Dict[str, float]:
|
||||
"""Get performance metrics."""
|
||||
pass
|
||||
```
|
||||
|
||||
### 4. Configuration
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class OmniConfig:
|
||||
"""Configuration for the core module."""
|
||||
|
||||
# Scheduler configuration
|
||||
scheduler: SchedulerConfig = field(default_factory=SchedulerConfig)
|
||||
|
||||
# Cache configuration
|
||||
dit_cache: DiTCacheConfig = field(default_factory=DiTCacheConfig)
|
||||
|
||||
# Resource limits
|
||||
max_memory_gb: float = 16.0
|
||||
max_gpu_utilization: float = 0.8
|
||||
|
||||
# Timeouts
|
||||
request_timeout: int = 300
|
||||
worker_timeout: int = 60
|
||||
|
||||
def validate(self) -> None:
|
||||
"""Validate configuration parameters."""
|
||||
if self.max_memory_gb <= 0:
|
||||
raise ValueError("max_memory_gb must be positive")
|
||||
if not 0 < self.max_gpu_utilization <= 1:
|
||||
raise ValueError("max_gpu_utilization must be between 0 and 1")
|
||||
```
|
||||
|
||||
### 5. Error Handling
|
||||
|
||||
```python
|
||||
class CoreModuleError(Exception):
|
||||
"""Base exception for core module errors."""
|
||||
pass
|
||||
|
||||
class SchedulerError(CoreModuleError):
|
||||
"""Scheduler-related errors."""
|
||||
pass
|
||||
|
||||
class QueueFullError(SchedulerError):
|
||||
"""Queue is at capacity."""
|
||||
pass
|
||||
|
||||
class CacheError(CoreModuleError):
|
||||
"""Cache-related errors."""
|
||||
pass
|
||||
|
||||
class ResourceError(CoreModuleError):
|
||||
"""Resource allocation errors."""
|
||||
pass
|
||||
```
|
||||
|
||||
### 6. Examples
|
||||
|
||||
#### Basic Usage
|
||||
```python
|
||||
from vllm_omni.core import CoreScheduler, CoreModuleConfig
|
||||
from vllm_omni.request import create_text_request
|
||||
|
||||
# Create configuration
|
||||
config = CoreModuleConfig(
|
||||
scheduler=SchedulerConfig(scheduler_type=SchedulerType.FIFO),
|
||||
max_memory_gb=8.0
|
||||
)
|
||||
|
||||
# Initialize scheduler
|
||||
scheduler = CoreScheduler(config)
|
||||
await scheduler.start()
|
||||
|
||||
# Create and schedule a request
|
||||
request = create_text_request(
|
||||
request_id="req_001",
|
||||
prompt="Hello, world!",
|
||||
sampling_params=sampling_params
|
||||
)
|
||||
|
||||
success = await scheduler.schedule_request(request)
|
||||
if success:
|
||||
result = await scheduler.process_request(request)
|
||||
print(f"Result: {result}")
|
||||
```
|
||||
|
||||
#### Advanced Usage
|
||||
```python
|
||||
# Custom scheduler with priority handling
|
||||
config = CoreModuleConfig(
|
||||
scheduler=SchedulerConfig(
|
||||
scheduler_type=SchedulerType.PRIORITY,
|
||||
priority_weights={"high": 2.0, "normal": 1.0, "low": 0.5}
|
||||
)
|
||||
)
|
||||
|
||||
scheduler = CoreScheduler(config)
|
||||
await scheduler.start()
|
||||
|
||||
# Monitor scheduler status
|
||||
status = scheduler.get_status()
|
||||
print(f"Queue size: {status['queue_size']}")
|
||||
print(f"Cache hit rate: {status['cache_hit_rate']:.2%}")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Module-Specific Guidelines
|
||||
|
||||
### Core Module
|
||||
- Focus on scheduling, caching, and resource management
|
||||
- Provide clear interfaces for other modules
|
||||
- Handle concurrency and thread safety
|
||||
- Implement comprehensive monitoring
|
||||
|
||||
### Engine Module
|
||||
- Handle model loading and inference
|
||||
- Support both AR and diffusion models
|
||||
- Provide unified interface for different model types
|
||||
- Implement efficient memory management
|
||||
|
||||
### Executor Module
|
||||
- Coordinate between different engines
|
||||
- Handle request routing and load balancing
|
||||
- Manage execution pipelines
|
||||
- Provide error recovery mechanisms
|
||||
|
||||
### Worker Module
|
||||
- Handle actual model execution
|
||||
- Manage GPU resources
|
||||
- Implement batching strategies
|
||||
- Provide performance optimization
|
||||
|
||||
---
|
||||
|
||||
## Checklist for API Design
|
||||
|
||||
- [ ] **Clear Purpose**: Module purpose is well-defined
|
||||
- [ ] **Complete Interface**: All public methods are documented
|
||||
- [ ] **Error Handling**: Comprehensive error handling strategy
|
||||
- [ ] **Configuration**: Flexible configuration system
|
||||
- [ ] **Examples**: Basic and advanced usage examples
|
||||
- [ ] **Type Hints**: All methods have proper type hints
|
||||
- [ ] **Documentation**: Comprehensive docstrings
|
||||
- [ ] **Testing**: Unit tests for all public methods
|
||||
- [ ] **Integration**: Clear integration points with other modules
|
||||
- [ ] **Performance**: Performance considerations documented
|
||||
|
||||
---
|
||||
|
||||
## Submission Guidelines
|
||||
|
||||
1. Create a new file: `docs/api/[module_name]_api.md`
|
||||
2. Follow the template structure exactly
|
||||
3. Include all required sections
|
||||
4. Provide working code examples
|
||||
5. Ensure all methods have proper docstrings
|
||||
6. Include error handling strategies
|
||||
7. Submit for review before implementation
|
||||
|
||||
This template ensures consistency and completeness across all vLLM-Omni modules.
|
||||
@@ -1,113 +1,195 @@
|
||||
# Architecture Overview
|
||||
|
||||
# Introduction
|
||||
This document outlines the architectural design for vLLM-Omni.
|
||||
|
||||
This document outlines the architectural design for vLLM-Omni, a new repository developed under the vLLM project. vLLM-Omni supports multi-modality models with non-autoregressive structures and non-textual outputs, extending vLLM beyond traditional text-based, autoregressive generation.
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" src="https://raw.githubusercontent.com/vllm-project/vllm-omni/refs/heads/main/docs/source/architecture/omni-modality-model-architecture.png">
|
||||
<img alt="Omni-Modality Model Architecture" src="https://raw.githubusercontent.com/vllm-project/vllm-omni/refs/heads/main/docs/source/architecture/omni-modality-model-architecture.png" width=55%>
|
||||
</picture>
|
||||
</p>
|
||||
|
||||
# Goals
|
||||
|
||||
The primary goals for vLLM-Omni are:
|
||||
The primary goal of the vLLM-Omni project is to build the fastest and easiest-to-use open-source Omni-Modality model inference & serving engine. vLLM-Omni extends the original vLLM, which was created to support large language models for text-based autoregressive (AR) generation tasks. vLLM-Omni is designed to support:
|
||||
|
||||
* **Support for Non-textual Output:** Enables the integration, efficient processing and output of various data types, including but not limited to, images, audio, and video, alongside text.
|
||||
* **Non-Autoregressive Structure:** Support model structure beyond autoregressive, especially Diffusion Transformer (DiT), which is widely used in visual and audio generation.
|
||||
* **Integration with vLLM Core:** Maintain compatibility and leverage existing vLLM key modules and optimizations where applicable.
|
||||
* **Non-textual Output:** Enables the integration, efficient processing and output of various data types, including but not limited to, images, audio, and video, alongside text.
|
||||
* **Non-Autoregressive Structure:** Support model structure beyond autoregressive, especially Diffusion Transformer (DiT), which is widely used in visual and audio generation.
|
||||
* **Integration with vLLM Core:** Maintain compatibility and leverage existing vLLM key modules and optimizations where applicable.
|
||||
* **Extensibility:** Design a modular and flexible architecture that can easily accommodate new modalities, model architectures, and output formats.
|
||||
|
||||
# Expected Gains
|
||||
|
||||
* It breaks through the limitation of vLLM that only supports autoregressive text generation models, and provides architectural support for popular model structures, especially multimodal generation.
|
||||
* By supporting new output formats and new model architectures, developers can easily connect to mature ecosystems such as Higress Gateway and Ray, improving the ease of deployment and efficiency of corresponding models.
|
||||
* Integrating efficient cache management, multiple parallel strategies, disaggregation from vLLM, to improve inference efficiency of corresponding models.
|
||||
|
||||
# Non-Autoregressive Structure
|
||||
|
||||
* **Diffusion Transformer:** initialized with a random latent noise, the transformer model iteratively predicts latent output for multiple steps, under control of sampling timestamps. Finally, it is decoded by the decoder of Variational AutoEncoder (VAE). It is not supported by vLLM now.
|
||||
* **Autoregressive:** dominant generation paradigm for text, now supported by vLLM. It generates tokens conditioned on previous tokens one by one. The efficient KV Cache management provided by vLLM can accelerate inference effectively.
|
||||
![][image1] ![][image2]
|
||||
|
||||
|
||||
| | Autoregressive | DiT |
|
||||
| :---- | ----- | ----- |
|
||||
| Scenarios | Text generation | Multimodal generation |
|
||||
| Generation | Token-by-token, left to right | Continuous vector, iteratively refine |
|
||||
| Key advantages | Smooth, good local consistency | Global consistency, diversity and controlability |
|
||||
| Efficiency | More efficient with KV cache | Need generation repeatedly, slow |
|
||||
| Error accumulation | Easy to accumulate | Can fix possibly |
|
||||
| Key challenges | Limited global planning, long context generation | Large global computational cost |
|
||||
| Sequence length | Varied length | Fixed length |
|
||||
| Attention mask | Lower triangular | Full bi-directional matrix |
|
||||
| Parallel strategy | TP, DP, PP, .etc | DP, CP, .etc |
|
||||
| vLLM support | Supported | Unsupported |
|
||||
|
||||
# Targeted models to support
|
||||
# Representative omni-modality models
|
||||
|
||||
According to analysis for current popular open-source models, most of them have the combination of AR+DiT. Specifically, they can be further categorized into 3 types below:
|
||||
|
||||
* **DiT as a main structure, with AR as text encoder (e.g.: Qwen-Image)**
|
||||
A popular model for image generation and editing. Main visual generation models are similar to the structure, such as Flux.
|
||||
![][image3]
|
||||
* **AR as a main structure, with DiT as multi-modal generator (e.g. BAGEL)**
|
||||
A unified multimodal comprehension and generation model. Visual generation can utilize CoT text generation output. Main unified multimodal models are similar to the structure.
|
||||
![][image4]
|
||||
* **Extension, multiple AR+DiT (e.g. Qwen-omni)**
|
||||
A new model for multimodal input and output. It designs the Thinker-Talker-Codec structure, which is a double-AR+DiT format.
|
||||
![][image5]
|
||||
**DiT as a main structure, with AR as text encoder (e.g.: Qwen-Image)**
|
||||
A powerful image generation foundation model capable of complex text rendering and precise image editing.
|
||||
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" src="https://raw.githubusercontent.com/vllm-project/vllm-omni/refs/heads/main/docs/source/architecture/dit-main-architecture.png">
|
||||
<img alt="Qwen-Image" src="https://raw.githubusercontent.com/vllm-project/vllm-omni/refs/heads/main/docs/source/architecture/dit-main-architecture.png" width=30%>
|
||||
</picture>
|
||||
</p>
|
||||
|
||||
**AR as a main structure, with DiT as multi-modal generator (e.g. BAGEL)**
|
||||
A unified multimodal comprehension and generation model, with cot text output and visual generation.
|
||||
|
||||
# Architectural Overview
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" src="https://raw.githubusercontent.com/vllm-project/vllm-omni/refs/heads/main/docs/source/architecture/ar-main-architecture.png">
|
||||
<img alt="Bagel" src="https://raw.githubusercontent.com/vllm-project/vllm-omni/refs/heads/main/docs/source/architecture/ar-main-architecture.png" width=30%>
|
||||
</picture>
|
||||
</p>
|
||||
|
||||
vLLM-Omni will import core modules from existing vLLM framework and introduce several new components and modifications to achieve its goals. The core architecture will be based on a modular design, allowing for independent development and integration of different modality handlers and output generators.
|
||||
![][image6]
|
||||
**AR+DiT (e.g. Qwen-Omni)**
|
||||
A natively end-to-end omni-modal LLM for multimodal inputs (text/image/audio/video...) and outputs (text/audio...).
|
||||
|
||||
## General ideas
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" src="https://raw.githubusercontent.com/vllm-project/vllm-omni/refs/heads/main/docs/source/architecture/ar-dit-main-architecture.png">
|
||||
<img alt="Qwen-Omni" src="https://raw.githubusercontent.com/vllm-project/vllm-omni/refs/heads/main/docs/source/architecture/ar-dit-main-architecture.png" width=30%>
|
||||
</picture>
|
||||
</p>
|
||||
|
||||
* It divides models in vLLM-Omni into multiple stages, which are processed by different Engine Cores. Each stage can choose AR Engine Core for AR model part, and Diffusion Engine Core for diffusion model part.
|
||||
* Designed a specialized diffusion engine core for DiT or dLLM.
|
||||
* Plug-in with external DiT acceleration modules such as xDiT, Cache-DiT.
|
||||
* Output linked with Higress API, Gradio for online demo, ComfyUI for less-code process.
|
||||
# vLLM-Omni main architecture
|
||||
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" src="https://raw.githubusercontent.com/vllm-project/vllm-omni/refs/heads/main/docs/source/architecture/vllm-omni-main-architecture.png">
|
||||
<img alt="vLLM-Omni Main Architecture" src="https://raw.githubusercontent.com/vllm-project/vllm-omni/refs/heads/main/docs/source/architecture/vllm-omni-main-architecture.png" width=55%>
|
||||
</picture>
|
||||
</p>
|
||||
|
||||
## Key Components
|
||||
|
||||
* **Processor:** Input processing, tokenization or encoding
|
||||
* **Scheduler:** for order and queue of incoming requests
|
||||
* **Executor:** Prepare model input and run
|
||||
* **Multimodal output processor:** support multimodal outputs
|
||||
* **Hidden State Output processor:** support intermediate hidden vectors as request output
|
||||
* **Engine Core:** accelerate AR modules, imported from vLLM main repo
|
||||
* **Diffusion Engine Core:** Wrap and accelerate DiT modules.
|
||||
* **Diffusion model:** Wrap up diffusion models with vLLM format
|
||||
| Component | Description |
|
||||
| ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **OmniRouter** | provide an intelligent router for Omni-modality requests dispatch |
|
||||
| **EntryPoints** | define the APIs for offline/online serving (APIServer, Omni/AsyncOmni) and provide the OmniStage abstraction for different AR/DiT stages |
|
||||
| **AR** | adapted for omni-modality models while inheriting efficient features from vLLM, such as cache management |
|
||||
| **Diffusion** | natively implemented and optimized using acceleration components |
|
||||
| **OmniConnector** | supports fully disaggregation based on E/P/D/G (Encoding/Processing/Decoding/Generation) disaggregation across stages |
|
||||
|
||||
# Examples data flow of specific models
|
||||
Disaggregated stages are managed through configuration, such as in the Qwen3-Omni example, where stages like Thinker, Talker, and Code2wav are defined as separate OmniStage instances with specific resources and input/output type.
|
||||
|
||||
1. Qwen-omni
|
||||
![][image7]![][image8]
|
||||
2. Qwen-Image
|
||||
![][image9] ![][image10]
|
||||
## Main features
|
||||
|
||||
# Overview of function call dependency
|
||||
vLLM-Omni aims to be fast, flexible, and easy to use with the following features:
|
||||
|
||||
![][image11]
|
||||
### Performance and Acceleration
|
||||
|
||||
The framework achieves high performance through several optimization techniques:
|
||||
|
||||
[image1]: <data:image/png;base64,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 truncated
|
||||
* **Efficient AR Support:** Leverages efficient KV cache management inherited from vLLM.
|
||||
* **Pipelined Execution:** Uses pipelined stage execution overlapping to ensure high throughput.
|
||||
* **Full Disaggregation:** Relies on the OmniConnector and dynamic resource allocation across stages.
|
||||
* **Diffusion Acceleration:** Includes integrated support for diffusion acceleration. This is managed by the acceleration layer, which handles:
|
||||
* **Cache:** Includes DBCache, TeaCache and third-party integration(e.g., [cache-dit](https://github.com/vipshop/cache-dit)).
|
||||
* **Parallelism:** Supports TP, CP, USP, and CFG.
|
||||
* **Attention:** Provides an interface for third-party integration (e.g., FA3, SAGE, MindIE-SD).
|
||||
* **Quantization:** Supports various quantization implementations including FP8 and AWQ.
|
||||
* **FusedOps:** Allows for custom and third-party integration.
|
||||
|
||||
[image2]: <data:image/png;base64,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 truncated
|
||||
### Flexibility and Usability
|
||||
|
||||
[image3]: <data:image/png;base64,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 truncated
|
||||
vLLM-Omni is designed to be flexible and straightforward for users:
|
||||
|
||||
[image4]: <data:image/png;base64,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 truncated
|
||||
* **Heterogeneous Pipeline Abstraction:** Manages complex model workflows effectively.
|
||||
* **Hugging Face Integration:** Offers seamless integration with popular Hugging Face models.
|
||||
* **Distributed Inference:** Supports tensor, pipeline, data, and expert parallelism.
|
||||
* **Streaming Outputs:** Supports streaming outputs.
|
||||
* **Unified API:** Provides a consistent and unified API interface compatible with vLLM.
|
||||
* **OpenAI-compatible API Server:** Includes a FastAPI-based server for online serving that is compatible with the OpenAI API.
|
||||
|
||||
[image5]: <data:image/png;base64,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 truncated
|
||||
# Interface design
|
||||
|
||||
[image6]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAdAAAAHUCAYAAACKzHkWAACAAElEQVR4XuydB1wUW5q3Z77dmdndibuzOzM36DXca85iwAiIZDEAKqJiwpxzzllERUUlGlCM16yIIkGSmHPOCkoGQQWhn6+qoBXLcFFE6dvn7+//6+pTp9Ir3U+/p8459ZvfCL0hhIRKWNnZ2bx8+VJZzs3N5cWLF8p7uTwvL08pf/78ORqNRlmfnp5OZmam8l5ISOhtyZ8X+fMjS/4Mye+1r4XryGXy5ygjI0Ox9vP2qVLzQ++lDpCQkJCQUOmWDEctLGVAZmVlKa8yVOUfqNrXzy01P/Re6gAJCRVX8fHxDBgw4JX79++Pi4uLstyvXz/69OmjlMmW3/ft2/eN+sLCuugRI0aQmpqq/jiUiLTZpjYTlQEqt97Ikl+1y59ban7ovdQB0kXJf0zyH5Hc7Kdrfvr06aummF+LLl++zG9/+1v5w8Zv/+23b/n/vaNMWFhXLf89y3/rf/jDH7h//77641BikjNO+btP+/2nvU1SklLzQ++lDpAuKjQ0lI4dO9K5c2ecnJx0yl27dlXO+9ekK1euKAB13biAiznnOJ15Qlj4V+urXGLEvGH8+c9/5sGDB+qPQ4lJBqicgebk5CjNtV+iz4CaH3ovdYB0UcHBwdjY2CjNJ+oMr7T71q1bdOjQQX1JOi0tQGd5Ticq4RhhD48KC/9qHZsazcAp/b84QGXJ2eeXyDy1UvND76UOkC5KC9CSavcvSd25cwc7Ozt1sU5LAFRYn/w1AfqlpeaH3ksdIF2UFqBf8pfY59Lt27cFQIWFddgCoHosdYB0UQKgpUsCoML6ZAFQPZY6QLooAdDSpXcC9NFRjidHEpMkXBxHJ0YQ+iD4rS9x4a9nAVA9ljpAuigB0NKldwH0yK1DjJg9nDELRgt/oscuHCPFdIYCUfWXuPDXswCoHksdIF2UAGjp0rsAeujGQarWrEblimWoWa2C8Ee6Xq2f+Nc//k8BaYyUyau/xIW/ngVA9VjqAOmiPidAk0KXMNRtL68mwdLksX1qJ2ytLTA3N8fcypmDd9LIS7nF9c1jWRKRUXjzj5Y+AbRilSrMneLA7TPzuXl6nnARffvCQk4cnUK92j8xZr4AaGmzAKgeSx0gXdTnA2gq8xpWpYbLWM4nFky6nJfNINNvWHjgLklJSSRHe1Cr6zye3TvBqWnVaLs+4c1dfKT0CaAVqlTGdZajFGYvSPYULqqz/Lh9bj61qlcUAC2FFgDVY6kDpIv6bAC9tgCjgQcInt+Pqfsv5pdpshlsVRH/ywV1EnZT3nAgcfdPcGJ2Xez8E19t/inSN4AumumIRoJCbuIa4SKaDB8lExUALZ0WANVjqQOki/pcAA0d1pZF16SFuI00GhVQUKphYvPfU7ZKHerXr0/tCoZM33mWvIcniJlcUwD0HRIA/bwWAC3dFgDVY6kDpIv6PAB9zhKL+rTuP46xg2z4t6qjySxYM87oTxhaOdGjRw96uszlRhq8fBBLxIQaAqDvkADo57UAaOm2AKgeSx0gXdTnAOiz85uoZupIWFg44cei8OpanaGhGuKfahhsXYG1Z9+cqDn73nEiJgqAvkslDVBSvCR7okl6XaYpKMt7R33ZecleSn3tq1KW9P76Rbenck/3ffspfLxPtQBo6bYAqB5LHSBdVPEBmse+6daYj96NFpPJO50oY7qMVZEPGWD8f7gfS39ji+w7UYSNKoel35M3yj9WAqAfYQl4pMxniFFFatp0ZWP4YglOEjhfrGShWQVqm9lwN2WD9GvIR4KafAypfoavtLyGwJkmTN68jCPjqjLxgC+axJVsXuTIweilkLUWnkr1nkuvadL+Mvykfa5VgJybIME51Sd/XZZUJ2l1wfnIx10PcZMwr96UMKkeyd7528p1n8n78GL/gDqM3bYcTdqaTwapAGjptgCoHksdIF1U8QH6krP7fybibsrrouzb+PsFsCM2gaj967kYn/N6naTcjMfERQdw8Fr+U+E/Vb9GgN64caMEATqXYa1McRnUmJ6+EyQw+cGpfpj3bUU/q3Y8SliE18whHApfhCZhOfu8B3D+6gqOLLRiyvTuOLUsi2FvZ+Zvm8/pXSM4d8WNkOWd8VjsTL/e1uw+PJsL3m1x7NSW4FMSXJ96kxY9mvE9jRg+xZm4RAnQqTIoXVk6xJypY81pVNOUiBQJsremcCOgA10djRkyrBtxEmAPD63PxB3LJTALgP5aLQCqx1IHSBdVfIB+PX1tgEZFRSnPU/1cDg8PZ926dfIHjakrJ5cAQOcw2NgKD3czWjo5kpjlzfHZ7Vm03YWhZm15Ej8R2zo2zPGfDgluTO7Zit0xywieZ8Y0vzlsGlSVIf5LeXB9Ca4DWuMfPouxBj8wZMUM7qyz4O/1m2M9YxSn17THamBvsm4toK9pTdbtm8XOsY3oMGUEEVJmu2tQQ2ymjOXBvu40LGvAqcduuHczwqKNA9ceeRLQryoDNi4ickwDAdBfuQVA9VjqAOmiBEA/XXXq1GH48OGMGjXqs3n06NFYmFkwfM4wohM/P0AHtTDj5+hheHRrRMAJN6YOsef4yeEMMLWVADoJu4a2LNg0AxLdmNHPnL3HJYDObc30ADcOjq3BhL1evHjkjttgczZJAJ1pXZNNZzagOdeXzrY92H9zBZlH+uIwSso4Tw6jdydbHjxZT3rsCOyc7Ri3fhoTLVsRdN8D0ucysLmZlIFKWentedzbNwjf1QOZ37MOvX3nETVWAPTXbgFQPZY6QLooAdBPVyuTVpTEk+zX+a1jwOR+JQTQ1kTfXEBqYDsshndj9BAH7t4YT/9XAG2H65aZEOfKGMfm7It1/yBAF7Srzf5rW8g97YKdTTd+vrycp4ddsB/pTLwE5j5agB4fTofuEkA3TGOCRSsO3csH6KDm5sQ8dGV+t6aYmVmxedtoVgyo+8UBGh4X8laZcMlbAFSPpQ6QLuqjAZp8Etf5c5k7dy4+O6O58mrevqIr4WwQ2zcGka5JZe+2fdzPLOKxVfraADVtbcqDU0GsmJsfD9lu7h4cPH2fW+H++PwcTHrBpEyy0q8E4rk2kPTn+dDNOn+QhfPyt1vitQ/tneI1q9YoXyolBdATcW5w2YUKteszakI/Eh9NUACaEDcB0xotGL5aAmjCfBwb1eHAqeWvALpvdHVmBq+XYLaCxYPyATq/bW32Xt38ToCmnJ9Cd6umXH2yibjtDhh1cWRp5BJ8OzfHLXIVxE+mfa1mRF+fSddWjWk0aJJ09Ts5NKgevdbNJ/oLAVRuKs9/Yot4WsuXtgCoHksdIF3URwH0xgbqGdRk0ro97NmziwltTajWcgZx6nq/oFOzzKnwp9Yc1zzj8rnLpGYXosxH6EMAffbsmbros6uVqSlPbpzh8B45HnvYe+AQQ1r+D108b7LH5U/8ppw1gTde/8LY6vT/+M333bmWlMuj42uoXMmGTT/nb+vZrxF1Bi4jVarnWWIAnY1LQ2MiUj3JfuGK44/fM2WzG5rHY+jZxIK7GX5sG1gF40aVsbNqhpmUKe4/sYygmcaM3+DKmTm1+d7WgZFe01k5xJT1oTOZZVGdXVekDPRUb2xad2HbJQmgh6TlIV3Jlo57YEZrjBpXp0XzOvhHrObp07VknexLk5o1adOzGVY/mnA8dTmbhjbHosyPNG5hyqLRTbGfMYHDo+oxZmsJ9cJ9dJSI+DBOpMbgHehJ7ca1mb92LtEJ4mktX9ICoHosdYB0UUUHaDaetpXovOX1/LWZ57exc0hbdibB4cGGlKlUixZ9l3D16laa/LMyVSpXpUyF+niEPCYvL5WVg834oVJVrJsZ8be/dORa3i16tOnJsYQXnPWfSJ3vK/FjxXK4n0gmowhMVQM0JSVF6cXq6OjI//7v/5ZI82phmZiY8DRTO2WElJxv6kkV65XIfYsPjCxPjTZjcPeP5lyuVJAdSpfydjQd3p/Lj16wpHsdZuwr9IXxIo5B5ob4nkzEa3UJALTAeYW2l/eVpx3XWVAul2lhpayX18ljPuVxoIXGfmqXC5dpVMvafSjHS5L3+3qbwsfQHl8pS8hfVs6r4Ljqa/gYqwEqf2HLEN17cRdjFo6ikXFDpdPWv77/lxJr0ZT7ZS0AqsdSB0gXVXSAnse2ohVHlHbGNDZPGMGAYWMZM3sZUfEaDvc3oM7c/ElvT65zJzI7f6u4vRPpNHobT66tpm7t4TyVyoLGNaPh/1pyJe+2BNBehN26Ri+Diux7ApqTrlRq5MzAQ2naA79XMkC7dOmiLK9atYoGDRrw7//+78oX4o4dO1S1P79MpQz0aUY+QNNPbKBpo04cLxjyumd4JZwmzMdqiBsjj+WRsc0O28WH6e/izPn7DxloWpvttwrtjBy2jnJgxIYzeHt6lhhA9c2FATrRbQKhD47Qb6ILZcp/r/ydaL1400IuZJ/lVMZxybHCX8iX8y4wfM5Q/vKXv5Ce/uZ48V+b3qSHkJ4B9CoOlRsRoMx9kMOjK5eICZiLm93/MjRSQ8iAegzcW5CNnVtJjaqVqVypEuW++z86uYVyP6A/9R39kI9ybdcM2lfqLgFUzkD7cVjKWG2qWXH2mQTQ1Bt0tbfEbPWd14d+j+7evav0hJXn2ZX/O7SWP4z29vZ0796dbt26vdfy+o+1s7Oz4l69elG9enWyNXmk7B7EQGcpEz//OhvdPeRHxu85RR/HmfRfuJtFFsasO3mZrt2duPAgjfF2NdlyvaBynpwpZ+PV14IRARcEQD+jtQCtXeNHuvRz5H/+8T9v/K3I/v1//B4zu9ZYOJhJNhf+grZ2tKJK7cr87ne/w8HBQZnyU/sZ+1RrP6elLaMthA4hWeoA6aKKDlCIHGtIpYFb0d5dTAycxhSzPzMkIh+gQw/kSKUJ9P/ub7jdz69zcmVXnKbsI+GCH63Npynbxiy0o9m37QoA2oLine truncated
|
||||
If you use vLLM, then you know how to use vLLM-Omni from Day 0:
|
||||
|
||||
[image7]: <data:image/png;base64,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 truncated
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" src="https://raw.githubusercontent.com/vllm-project/vllm-omni/refs/heads/main/docs/source/architecture/vllm-omni-user-interface.png">
|
||||
<img alt="vLLM-Omni interface design" src="https://raw.githubusercontent.com/vllm-project/vllm-omni/refs/heads/main/docs/source/architecture/vllm-omni-user-interface.png" width=55%>
|
||||
</picture>
|
||||
</p>
|
||||
|
||||
[image8]: <data:image/png;base64,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 truncated
|
||||
Taking **Qwen3-Omni** as an example:
|
||||
|
||||
[image9]: <data:image/png;base64,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 truncated
|
||||
## Offline Inference
|
||||
The **Omni** class provides a Python interface for offline batched inference. Users initialize the Omni class with a Hugging Face model name and use the generate method, passing inputs that include both text prompts and multi-modal data:
|
||||
|
||||
[image10]: <data:image/png;base64,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 truncated
|
||||
```
|
||||
# Create an omni_lm with HF model name.
|
||||
from vllm_omni.entrypoints.omni import Omni
|
||||
|
||||
[image11]: <data:image/png;base64,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 truncated
|
||||
omni_lm = Omni(model="Qwen/Qwen3-Omni-30B-A3B-Instruct")
|
||||
|
||||
# Example prompts.
|
||||
om_inputs = {"prompt": prompt,
|
||||
"multi_modal_data": {
|
||||
"video": video_frames,
|
||||
"audio": audio_signal,
|
||||
}}
|
||||
|
||||
# Generate texts and audio from the multi-modality inputs.
|
||||
outputs = omni_lm.generate(om_inputs, sampling_params_list)
|
||||
```
|
||||
|
||||
## Online Serving
|
||||
Similar to vLLM, vLLM-Omni also provides a FastAPI-based server for online serving. Users can launch the server using the vllm serve command with the `--omni` flag:
|
||||
|
||||
```
|
||||
vllm serve Qwen/Qwen3-Omni-30B-A3B-Instruct --omni --port 8091
|
||||
```
|
||||
|
||||
Users can send requests to the server using curl:
|
||||
|
||||
```
|
||||
# prepare user content
|
||||
user_content='[
|
||||
{
|
||||
"type": "video_url",
|
||||
"video_url": {
|
||||
"url": "'"$SAMPLE_VIDEO_URL"'"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Why is this video funny?"
|
||||
}
|
||||
]'
|
||||
sampling_params_list='[
|
||||
'"$thinker_sampling_params"',
|
||||
'"$talker_sampling_params"',
|
||||
'"$code2wav_sampling_params"'
|
||||
]'
|
||||
mm_processor_kwargs="{}"
|
||||
|
||||
# send the request
|
||||
curl -sS -X POST http://localhost:8091/v1/chat/completions \
|
||||
-H "Content-Type: application/json" \
|
||||
-d @- <<EOF
|
||||
{
|
||||
"model": "Qwen/Qwen3-Omni-30B-A3B-Instruct",
|
||||
"sampling_params_list": $sampling_params_list,
|
||||
"mm_processor_kwargs": $mm_processor_kwargs,
|
||||
"messages": [
|
||||
{
|
||||
"role": "system",
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "You are Qwen, a virtual human developed by the Qwen Team, Alibaba Group, capable of perceiving auditory and visual inputs, as well as generating text and speech."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": $user_content
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
For more usages, please refer to [examples](https://github.com/vllm-project/vllm-omni/tree/main/examples).
|
||||
@@ -0,0 +1,286 @@
|
||||
# Cache-DiT
|
||||
|
||||
This section describes how to add cache-dit acceleration to a new diffusion pipeline. We use the Qwen-Image pipeline and LongCat-Image pipeline as reference implementations.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Standard Models: Automatic Support](#standard-models-automatic-support)
|
||||
- [Custom Architectures: Writing Custom Implementation](#custom-architectures-writing-custom-implementation)
|
||||
- [Testing](#testing)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
- [Reference Implementations](#reference-implementations)
|
||||
- [Summary](#summary)
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
### What is Cache-DiT?
|
||||
|
||||
Cache-DiT is an acceleration library for Diffusion Transformers (DiT) that caches intermediate computation results across denoising steps. The core insight is that adjacent denoising steps often produce similar intermediate features, so we can skip redundant computations by reusing cached results.
|
||||
|
||||
The library supports three main caching strategies:
|
||||
|
||||
- **DBCache:** Dynamic block-level caching that selectively computes or caches transformer blocks based on residual differences
|
||||
- **TaylorSeer:** Calibration-based prediction that estimates block outputs using Taylor expansion
|
||||
- **SCM (Step Computation Masking):** Dynamic step skipping based on configurable policies
|
||||
|
||||
### Architecture
|
||||
|
||||
vLLM-omni integrates cache-dit through the `CacheDiTBackend` class, which provides a unified interface for managing cache-dit acceleration on diffusion models.
|
||||
|
||||
| Method/Class | Purpose | Behavior |
|
||||
|--------------|---------|----------|
|
||||
| [`CacheDiTBackend`](https://docs.vllm.ai/projects/vllm-omni/en/latest/api/vllm_omni/diffusion/cache/#vllm_omni.diffusion.cache.CacheBackend) | Unified backend interface | Automatically handles enabler selection and cache refresh |
|
||||
| [`enable_cache_for_dit()`](https://docs.vllm.ai/projects/vllm-omni/en/latest/api/vllm_omni/diffusion/cache/cache_dit_backend/#vllm_omni.diffusion.cache.cache_dit_backend.enable_cache_for_dit) | Apply caching to transformer | Configures DBCache on transformer blocks |
|
||||
|
||||
**Key APIs from Cache-DiT:**
|
||||
|
||||
[Cache-DiT API Reference](https://cache-dit.readthedocs.io/en/latest/user_guide/CACHE_API/)
|
||||
|
||||
| API | Description |
|
||||
|-----|-------------|
|
||||
| `BlockAdapter` | Core abstraction for applying cache-dit to transformers. Specifies transformer module(s), block list(s), and forward signature pattern(s). |
|
||||
| `ForwardPattern` | Defines block forward signature patterns: `Pattern_0`, `Pattern_1`, `Pattern_2` |
|
||||
| `ParamsModifier` | Per-transformer or per-block-list cache configuration customization |
|
||||
| `DBCacheConfig` | Configuration for DBCache parameters (warmup steps, cached steps, thresholds) |
|
||||
| `refresh_context()` | Update cache context | Called when `num_inference_steps` changes |
|
||||
|
||||
---
|
||||
|
||||
## Standard Models: Automatic Support
|
||||
|
||||
Most DiT models follow this pattern:
|
||||
- Single transformer with one `ModuleList` of blocks
|
||||
- Standard forward signature
|
||||
- Compatible with cache-dit's automatic detection
|
||||
|
||||
**Examples:** Qwen-Image, Z-Image
|
||||
|
||||
For standard single-transformer models, **no code changes are needed**. The `CacheDiTBackend` automatically uses `enable_cache_for_dit()`:
|
||||
|
||||
```python
|
||||
from vllm_omni import Omni
|
||||
|
||||
# Works automatically for standard models
|
||||
omni = Omni(
|
||||
model="Qwen/Qwen-Image", # Standard single-transformer model
|
||||
cache_backend="cache_dit",
|
||||
cache_config={
|
||||
"Fn_compute_blocks": 1,
|
||||
"Bn_compute_blocks": 0,
|
||||
"max_warmup_steps": 4,
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
**What happens automatically:**
|
||||
|
||||
```python
|
||||
def enable_cache_for_dit(pipeline: Any, cache_config: Any) -> Callable[[int], None]:
|
||||
"""Default enabler for standard single-transformer DiT models."""
|
||||
|
||||
# Build cache configuration
|
||||
db_cache_config = DBCacheConfig(
|
||||
num_inference_steps=None, # Will be set during first inference
|
||||
Fn_compute_blocks=cache_config.Fn_compute_blocks,
|
||||
Bn_compute_blocks=cache_config.Bn_compute_blocks,
|
||||
max_warmup_steps=cache_config.max_warmup_steps,
|
||||
max_cached_steps=cache_config.max_cached_steps,
|
||||
max_continuous_cached_steps=cache_config.max_continuous_cached_steps,
|
||||
residual_diff_threshold=cache_config.residual_diff_threshold,
|
||||
)
|
||||
|
||||
# Enable cache-dit on transformer
|
||||
cache_dit.enable_cache(
|
||||
pipeline.transformer,
|
||||
cache_config=db_cache_config,
|
||||
)
|
||||
|
||||
# Return refresh function for dynamic num_inference_steps updates
|
||||
def refresh_cache_context(pipeline: Any, num_inference_steps: int, verbose: bool = True):
|
||||
cache_dit.refresh_context(pipeline.transformer, num_inference_steps=num_inference_steps, verbose=verbose)
|
||||
|
||||
return refresh_cache_context
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Custom Architectures: Writing Custom Implementation
|
||||
|
||||
Some models require custom handling:
|
||||
|
||||
- **Dual-transformer:** Models with separate high-noise and low-noise transformers (e.g., Wan2.2)
|
||||
- **Multi-block-list:** Models with multiple block lists in one transformer (e.g., LongCatImage with `transformer_blocks` + `single_transformer_blocks`)
|
||||
- **Special forward patterns:** Models with non-standard block execution patterns
|
||||
|
||||
### Example 1: Dual-Transformer Model (Wan2.2)
|
||||
|
||||
Wan2.2 uses two transformers: one for high-noise steps and one for low-noise steps.
|
||||
|
||||
**Key difference:** Use `BlockAdapter` to wrap multiple transformers with separate configurations.
|
||||
|
||||
```python
|
||||
# Standard: cache_dit.enable_cache(pipeline.transformer, ...)
|
||||
# Custom: Use BlockAdapter to handle multiple transformers
|
||||
cache_dit.enable_cache(
|
||||
BlockAdapter(
|
||||
transformer=[pipeline.transformer, pipeline.transformer_2], # Multiple transformers
|
||||
blocks=[pipeline.transformer.blocks, pipeline.transformer_2.blocks],
|
||||
forward_pattern=[ForwardPattern.Pattern_2, ForwardPattern.Pattern_2],
|
||||
params_modifiers=[
|
||||
ParamsModifier(...), # Config for high-noise transformer
|
||||
ParamsModifier(...), # Config for low-noise transformer (different params)
|
||||
],
|
||||
),
|
||||
cache_config=db_cache_config,
|
||||
)
|
||||
```
|
||||
|
||||
**Key difference:** `refresh_context` must be called on each transformer separately.
|
||||
|
||||
```python
|
||||
# Standard: cache_dit.refresh_context(pipeline.transformer, num_inference_steps=N)
|
||||
# Custom: Refresh each transformer with its own step count
|
||||
def refresh_cache_context(pipeline, num_inference_steps, verbose=True):
|
||||
high_steps, low_steps = _split_inference_steps(num_inference_steps)
|
||||
cache_dit.refresh_context(pipeline.transformer, num_inference_steps=high_steps, ...)
|
||||
cache_dit.refresh_context(pipeline.transformer_2, num_inference_steps=low_steps, ...)
|
||||
```
|
||||
|
||||
### Example 2: Multi-Block-List Model (LongCatImage)
|
||||
|
||||
LongCatImage has a single transformer with two block lists: `transformer_blocks` and `single_transformer_blocks`.
|
||||
|
||||
**Key difference:** Use `BlockAdapter` to specify multiple block lists within one transformer.
|
||||
|
||||
```python
|
||||
# Standard: cache_dit.enable_cache(pipeline.transformer, ...)
|
||||
# - Automatically detects single block list
|
||||
# Custom: Use BlockAdapter to specify multiple block lists
|
||||
cache_dit.enable_cache(
|
||||
BlockAdapter(
|
||||
transformer=pipeline.transformer, # Single transformer
|
||||
blocks=[
|
||||
pipeline.transformer.transformer_blocks, # Block list 1
|
||||
pipeline.transformer.single_transformer_blocks, # Block list 2
|
||||
],
|
||||
forward_pattern=[ForwardPattern.Pattern_1, ForwardPattern.Pattern_1],
|
||||
params_modifiers=[modifier],
|
||||
),
|
||||
cache_config=db_cache_config,
|
||||
)
|
||||
```
|
||||
|
||||
> **Note:** For single transformer with multiple block lists, `refresh_context` works the same as standard models.
|
||||
|
||||
### Registering Custom Implementations
|
||||
|
||||
After writing your custom enabler, register it in `CUSTOM_DIT_ENABLERS` in `vllm_omni/diffusion/cache/cache_dit_backend.py`:
|
||||
|
||||
```python
|
||||
CUSTOM_DIT_ENABLERS = {
|
||||
"Wan22Pipeline": enable_cache_for_wan22,
|
||||
"LongCatImagePipeline": enable_cache_for_longcat_image,
|
||||
"YourCustomPipeline": enable_cache_for_your_model, # Add here
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
After adding cache-dit support, test with:
|
||||
|
||||
```python
|
||||
from vllm_omni import Omni
|
||||
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
|
||||
|
||||
# Test your custom model
|
||||
omni = Omni(
|
||||
model="your-model-name",
|
||||
cache_backend="cache_dit",
|
||||
cache_config={
|
||||
"Fn_compute_blocks": 1,
|
||||
"Bn_compute_blocks": 0,
|
||||
"max_warmup_steps": 4,
|
||||
"residual_diff_threshold": 0.24,
|
||||
}
|
||||
)
|
||||
|
||||
images = omni.generate(
|
||||
"a beautiful landscape",
|
||||
OmniDiffusionSamplingParams(num_inference_steps=50),
|
||||
)
|
||||
```
|
||||
|
||||
**Verify:**
|
||||
|
||||
1. Cache is applied (check logs for "Cache-dit enabled successfully on xxx")
|
||||
2. Performance improvement (should be around 1.5x-2x faster)
|
||||
3. Image quality (compare with `cache_backend=None`)
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Issue: Cache not applied
|
||||
|
||||
**Symptoms:** No speedup observed, no cache-related log messages.
|
||||
|
||||
**Causes & Solutions:**
|
||||
|
||||
- **Enabler not registered:**
|
||||
|
||||
**Problem:** Pipeline name not in `CUSTOM_DIT_ENABLERS` registry.
|
||||
|
||||
**Solution:** Verify `pipeline.__class__.__name__` matches the registry key and add your enabler to `CUSTOM_DIT_ENABLERS`.
|
||||
|
||||
### Issue: Quality degradation
|
||||
|
||||
**Symptoms:** Generated images have artifacts or lower quality compared to non-cached inference.
|
||||
|
||||
**Causes & Solutions:**
|
||||
|
||||
- **Cache parameters too aggressive:**
|
||||
|
||||
**Solution:**
|
||||
```python
|
||||
cache_config={
|
||||
"residual_diff_threshold": 0.12, # Lower from 0.24 (try 0.12-0.18)
|
||||
"max_warmup_steps": 6, # Increase from 4 (try 6-8)
|
||||
"max_continuous_cached_steps": 2, # Reduce if higher
|
||||
}
|
||||
```
|
||||
|
||||
Check the [user guide for cache_dit](../../user_guide/diffusion/cache_dit_acceleration.md) for more adjustable parameters.
|
||||
|
||||
---
|
||||
|
||||
## Reference Implementations
|
||||
|
||||
Complete examples in the codebase:
|
||||
|
||||
| Model | Path | Pattern | Notes |
|
||||
|-------|------|---------|-------|
|
||||
| **Standard DiT** | [`cache_dit_backend.py::enable_cache_for_dit`](https://docs.vllm.ai/projects/vllm-omni/en/latest/api/vllm_omni/diffusion/cache/cache_dit_backend/#vllm_omni.diffusion.cache.cache_dit_backend.enable_cache_for_dit) | Default enabler | Single transformer, automatic |
|
||||
| **Wan2.2** | [`cache_dit_backend.py::enable_cache_for_wan22`](https://docs.vllm.ai/projects/vllm-omni/en/latest/api/vllm_omni/diffusion/cache/cache_dit_backend/#vllm_omni.diffusion.cache.cache_dit_backend.enable_cache_for_wan22) | Dual-transformer | Separate high/low noise transformers |
|
||||
| **LongCat** | [`cache_dit_backend.py::enable_cache_for_longcat_image`](https://docs.vllm.ai/projects/vllm-omni/en/latest/api/vllm_omni/diffusion/cache/cache_dit_backend/#vllm_omni.diffusion.cache.cache_dit_backend.enable_cache_for_longcat_image) | Multi-block-list | Two block lists in one transformer |
|
||||
| **BAGEL** | [`cache_dit_backend.py::enable_cache_for_bagel`](https://docs.vllm.ai/projects/vllm-omni/en/latest/api/vllm_omni/diffusion/cache/cache_dit_backend/#vllm_omni.diffusion.cache.cache_dit_backend.enable_cache_for_bagel) | Omni model | Complex architecture |
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
Adding cache-dit support:
|
||||
|
||||
1. ✅ **Check model type** - Standard models work automatically, custom architectures need enablers
|
||||
2. ✅ **Write enabler** (if needed) - Use `BlockAdapter` for complex architectures
|
||||
3. ✅ **Register enabler** (if needed) - Add to `CUSTOM_DIT_ENABLERS` dictionary
|
||||
4. ✅ **Return refresh function** (if needed) - Handle `num_inference_steps` changes
|
||||
5. ✅ **Test** - Verify with `cache_backend="cache_dit"`
|
||||
|
||||
For most models, the default enabler is sufficient. Only write custom enablers for complex architectures!
|
||||
@@ -0,0 +1,249 @@
|
||||
# CFG-Parallel
|
||||
|
||||
This section describes how to add CFG-Parallel (Classifier-Free Guidance Parallel) to a diffusion pipeline. We use the Qwen-Image pipeline as the reference implementation.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Step-by-Step Implementation](#step-by-step-implementation)
|
||||
- [Customization](#customization)
|
||||
- [Testing](#testing)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
- [Reference Implementations](#reference-implementations)
|
||||
- [Summary](#summary)
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
### What is CFG-Parallel?
|
||||
|
||||
In standard Classifier-Free Guidance, each diffusion step requires two forward passes through the transformer:
|
||||
|
||||
1. **Positive/Conditional**: Guided by the text prompt
|
||||
2. **Negative/Unconditional**: Typically using empty or negative prompt
|
||||
|
||||
CFG-Parallel eliminates this bottleneck by distributing the two forward passes across different GPU ranks, allowing them to execute simultaneously rather than sequentially.
|
||||
|
||||
### Architecture
|
||||
|
||||
vLLM-omni provides `CFGParallelMixin` that encapsulates all CFG parallel logic. Pipelines inherit from this mixin and implement a `diffuse()` method that orchestrates the denoising loop.
|
||||
|
||||
| Method | Purpose | Automatic Behavior |
|
||||
|--------|---------|-------------------|
|
||||
| [`predict_noise_maybe_with_cfg()`](https://docs.vllm.ai/projects/vllm-omni/en/latest/api/vllm_omni/diffusion/distributed/cfg_parallel/) | Predict noise with CFG | Detects parallel mode, distributes computation, gathers results |
|
||||
| [`scheduler_step_maybe_with_cfg()`](https://docs.vllm.ai/projects/vllm-omni/en/latest/api/vllm_omni/diffusion/distributed/cfg_parallel/) | Step scheduler with sync | Rank 0 steps, broadcasts latents to all ranks |
|
||||
| [`combine_cfg_noise()`](https://docs.vllm.ai/projects/vllm-omni/en/latest/api/vllm_omni/diffusion/distributed/cfg_parallel/) | Combine positive/negative | Applies CFG formula with optional normalization |
|
||||
| [`predict_noise()`](https://docs.vllm.ai/projects/vllm-omni/en/latest/api/vllm_omni/diffusion/distributed/cfg_parallel/) | Forward pass wrapper | Override for custom transformer calls |
|
||||
| [`cfg_normalize_function()`](https://docs.vllm.ai/projects/vllm-omni/en/latest/api/vllm_omni/diffusion/distributed/cfg_parallel/) | Normalize CFG output | Override for custom normalization |
|
||||
|
||||
### How It Works
|
||||
|
||||
`predict_noise_maybe_with_cfg()` automatically detects and switches between two execution modes:
|
||||
|
||||
- **CFG-Parallel mode** (when `cfg_world_size > 1`):
|
||||
- Rank 0 computes positive prompt prediction
|
||||
- Rank 1 computes negative prompt prediction
|
||||
- Results are gathered via `all_gather()`
|
||||
- Combined on rank 0 using CFG formula
|
||||
|
||||
- **Sequential mode** (when `cfg_world_size == 1`):
|
||||
- Single rank computes both positive and negative predictions
|
||||
- Directly combines them with CFG formula
|
||||
|
||||
`scheduler_step_maybe_with_cfg()` ensures consistent latent states across all ranks:
|
||||
|
||||
- **CFG-Parallel mode**:
|
||||
- Only rank 0 performs the scheduler step (applies noise prediction to update latents)
|
||||
- Updated latents are broadcast to all other ranks via `broadcast()`
|
||||
- All ranks maintain synchronized latent states for the next iteration
|
||||
|
||||
- **Sequential mode**:
|
||||
- Single rank directly performs the scheduler step
|
||||
- No synchronization needed
|
||||
|
||||
---
|
||||
|
||||
## Step-by-Step Implementation
|
||||
|
||||
### Step 1: Inherit `CFGParallelMixin`
|
||||
|
||||
Allow your pipeline to inherit from `CFGParallelMixin` and implements the `diffuse()` method for your specific model.
|
||||
|
||||
**Example (Qwen-Image):**
|
||||
|
||||
```python
|
||||
from vllm_omni.diffusion.distributed.cfg_parallel import CFGParallelMixin
|
||||
import torch.nn as nn
|
||||
class YourModelPipeline(nn.Module, CFGParallelMixin):
|
||||
def diffuse(self, ...) -> torch.Tensor:
|
||||
for i, t in enumerate(timesteps):
|
||||
# Prepare positive_kwargs (conditional) and negative_kwargs (unconditional)
|
||||
positive_kwargs = {...} # hidden_states, encoder_hidden_states, etc.
|
||||
negative_kwargs = {...} if do_true_cfg else None
|
||||
|
||||
# Key method 1: Predict noise with automatic CFG parallel handling
|
||||
noise_pred = self.predict_noise_maybe_with_cfg(
|
||||
do_true_cfg=do_true_cfg,
|
||||
true_cfg_scale=true_cfg_scale,
|
||||
positive_kwargs=positive_kwargs,
|
||||
negative_kwargs=negative_kwargs,
|
||||
)
|
||||
|
||||
# Key method 2: Step scheduler with automatic CFG synchronization
|
||||
latents = self.scheduler_step_maybe_with_cfg(
|
||||
noise_pred, t, latents, do_true_cfg
|
||||
)
|
||||
|
||||
return latents
|
||||
```
|
||||
|
||||
**Key Points:**
|
||||
|
||||
- `positive_kwargs`: transformer arguments for conditional (text-guided) prediction
|
||||
- `negative_kwargs`: transformer arguments for unconditional prediction (set to `None` if CFG disabled)
|
||||
- For image editing pipelines, add `output_slice=image_seq_len` to extract the generative image portion
|
||||
|
||||
### Step 2: Call `diffuse`
|
||||
|
||||
Call `self.diffuse` in your pipeline's forward function:
|
||||
|
||||
```python
|
||||
import torch.nn as nn
|
||||
class YourModelPipeline(nn.Module, CFGParallelMixin):
|
||||
def forward(
|
||||
self,
|
||||
prompt: str,
|
||||
negative_prompt: str | None = None,
|
||||
guidance_scale: float = 3.5,
|
||||
num_inference_steps: int = 50,
|
||||
**kwargs,
|
||||
):
|
||||
# Encode prompts, Initialize latents, Get timesteps
|
||||
...
|
||||
# Run diffusion loop (calls the mixin's diffuse method)
|
||||
latents = self.diffuse(
|
||||
prompt_embeds=prompt_embeds,
|
||||
prompt_embeds_mask=prompt_embeds_mask,
|
||||
negative_prompt_embeds=negative_embeds,
|
||||
negative_prompt_embeds_mask=negative_mask,
|
||||
latents=latents,
|
||||
timesteps=timesteps,
|
||||
do_true_cfg=do_true_cfg,
|
||||
true_cfg_scale=guidance_scale,
|
||||
...
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Customization
|
||||
|
||||
### Override `predict_noise()` for Custom Transformer Calls
|
||||
|
||||
If your transformer requires custom prediction function, you can rewrite `predict_noise` function. Taking Wan2.2 as an example, which has two transformer models. The actual transformer to be called is determined by `self.transformer`.
|
||||
|
||||
```python
|
||||
class Wan22Pipeline(nn.Module, CFGParallelMixin):
|
||||
def predict_noise(self, current_model: nn.Module | None = None, **kwargs: Any) -> torch.Tensor:
|
||||
if current_model is None:
|
||||
current_model = self.transformer
|
||||
return current_model(**kwargs)[0]
|
||||
```
|
||||
|
||||
|
||||
### Override `cfg_normalize_function()` for Custom Normalization
|
||||
|
||||
Some models have their own normalization function. Taking LongCat Image model as an example:
|
||||
|
||||
```python
|
||||
class LongCatImagePipeline(nn.Module, CFGParallelMixin):
|
||||
def cfg_normalize_function(self, noise_pred, comb_pred, cfg_renorm_min=0.0):
|
||||
"""
|
||||
Normalize the combined noise prediction.
|
||||
"""
|
||||
cond_norm = torch.norm(noise_pred, dim=-1, keepdim=True)
|
||||
noise_norm = torch.norm(comb_pred, dim=-1, keepdim=True)
|
||||
scale = (cond_norm / (noise_norm + 1e-8)).clamp(min=cfg_renorm_min, max=1.0)
|
||||
noise_pred = comb_pred * scale
|
||||
return noise_pred
|
||||
|
||||
# The original cfg_normalize_function function in CFGParallelMixin
|
||||
# cond_norm = torch.norm(noise_pred, dim=-1, keepdim=True)
|
||||
# noise_norm = torch.norm(comb_pred, dim=-1, keepdim=True)
|
||||
# noise_pred = comb_pred * (cond_norm / noise_norm)
|
||||
# return noise_pred
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
After adding CFG-Parallel support, test with:
|
||||
|
||||
```bash
|
||||
cd examples/offline_inference/text_to_image
|
||||
python text_to_image.py \
|
||||
--model Your-org/your-model \
|
||||
--prompt "a cup of coffee on the table" \
|
||||
--negative-prompt "ugly, unclear" \
|
||||
--cfg-scale 4.0 \
|
||||
--num-inference-steps 50 \
|
||||
--output "cfg_enabled.png" \
|
||||
--cfg-parallel-size 2
|
||||
```
|
||||
|
||||
**Verify:**
|
||||
|
||||
1. Check logs for CFG parallel being activated
|
||||
2. Record the `e2e_time_ms` in the log and compare with CFG-Parallel disabled
|
||||
3. Compare the generated result quality with baseline
|
||||
4. Record comparison results in your PR
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Issue: CFG parallel not activating
|
||||
|
||||
**Symptoms:** Generation still slow, logs don't show CFG parallel being used.
|
||||
|
||||
**Causes & Solutions:**
|
||||
|
||||
- **CFG is not enabled:**
|
||||
|
||||
**Problem:** Guidance scale too low or negative prompt not provided.
|
||||
|
||||
**Solution:** Ensure `guidance_scale > 1.0` and negative prompt is provided:
|
||||
```python
|
||||
images = pipeline(
|
||||
prompt="a cat",
|
||||
negative_prompt="", # Must provide (even if empty)
|
||||
guidance_scale=3.5, # Must be > 1.0
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Reference Implementations
|
||||
|
||||
Complete examples in the codebase:
|
||||
|
||||
| Model | Path | Pattern | Notes |
|
||||
|-------|------|---------|-------|
|
||||
| **Qwen-Image** | `vllm_omni/diffusion/models/qwen_image/cfg_parallel.py` | Mixin | Dual-stream transformer |
|
||||
| **Qwen-Image-Edit** | `vllm_omni/diffusion/models/qwen_image/pipeline_qwen_image_edit.py` | Mixin | Image editing with `output_slice` |
|
||||
| **Wan2.2** | `vllm_omni/diffusion/models/wan2_2/pipeline_wan2_2.py` | Mixin | Dual-transformer architecture |
|
||||
| **CFGParallelMixin** | `vllm_omni/diffusion/distributed/cfg_parallel.py` | Base implementation | Core mixin class |
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
Adding CFG-Parallel support:
|
||||
|
||||
1. ✅ **Create mixin** - Inherit from `CFGParallelMixin` and implement `diffuse()` method
|
||||
2. ✅ **(Optional) Customize** - Override `predict_noise()` or `cfg_normalize_function()` for custom behavior
|
||||
3. ✅ **Test** - Verify with `--cfg-parallel-size 2` and compare performance
|
||||
@@ -0,0 +1,108 @@
|
||||
# Disaggregated Inference for Omni-Modality Models
|
||||
|
||||
This guide explains how to configure and use distributed connectors
|
||||
(`vllm_omni/distributed/omni_connectors`) in vllm-omni for multi-stage pipelines.
|
||||
|
||||
Backend-specific setup lives in separate docs:
|
||||
|
||||
- [SharedMemoryConnector](omni_connectors/shared_memory_connector.md)
|
||||
- [MooncakeStoreConnector](omni_connectors/mooncake_store_connector.md)
|
||||
- [MooncakeTransferEngineConnector](omni_connectors/mooncake_transfer_engine_connector.md)
|
||||
- [YuanrongConnector](omni_connectors/yuanrong_connector.md)
|
||||
|
||||
## Overview
|
||||
|
||||
Connectors enable data transfer between pipeline stages (e.g., Thinker -> Talker).
|
||||
Current connectors operate in D2H2D (device to host to device) mode.
|
||||
|
||||
## Connector Choices
|
||||
|
||||
| Use Case | Recommended Connector | Notes |
|
||||
| :--- | :--- | :--- |
|
||||
| Single node | SharedMemoryConnector | Auto-configured if no connector is specified. |
|
||||
| Multi node (Mooncake Store) | MooncakeStoreConnector | TCP-based, requires Mooncake Master + metadata server. |
|
||||
| Multi node (Mooncake RDMA) | MooncakeTransferEngineConnector | RDMA/TCP direct transfer with managed memory pool. Fastest. |
|
||||
| Multi node (Yuanrong) | YuanrongConnector | Requires Yuanrong Datasystem + etcd. |
|
||||
|
||||
## Core API
|
||||
|
||||
The connector system is built around `OmniConnectorBase`.
|
||||
|
||||
```python
|
||||
class OmniConnectorBase(ABC):
|
||||
@abstractmethod
|
||||
def put(self, from_stage: str, to_stage: str, put_key: str, data: Any) -> tuple[bool, int, Optional[dict]]:
|
||||
"""
|
||||
Store data.
|
||||
Returns: (success, serialized_size, metadata)
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get(self, from_stage: str, to_stage: str, get_key: str, metadata: Optional[dict] = None) -> Optional[tuple[Any, int]]:
|
||||
"""
|
||||
Retrieve data.
|
||||
Args: metadata - transport-specific handles returned by put() (e.g., SHM name).
|
||||
Returns: (object, serialized_size)
|
||||
"""
|
||||
pass
|
||||
```
|
||||
|
||||
### Metadata Passing
|
||||
|
||||
Some connectors (e.g., SharedMemoryConnector) generate transient resources during `put()`.
|
||||
This `metadata` must be passed through the control plane so `get()` can locate the data.
|
||||
|
||||
## Configuration Model
|
||||
|
||||
Define connectors in runtime:
|
||||
|
||||
```yaml
|
||||
runtime:
|
||||
connectors:
|
||||
connector_of_shared_memory:
|
||||
name: SharedMemoryConnector
|
||||
extra:
|
||||
shm_threshold_bytes: 65536
|
||||
```
|
||||
|
||||
Wire stages to connectors:
|
||||
|
||||
```yaml
|
||||
stage_args:
|
||||
- stage_id: 0
|
||||
output_connectors:
|
||||
to_stage_1: connector_of_shared_memory
|
||||
|
||||
- stage_id: 1
|
||||
input_connectors:
|
||||
from_stage_0: connector_of_shared_memory
|
||||
```
|
||||
|
||||
If a pipeline edge has no explicit connector, the system auto-creates a
|
||||
SharedMemoryConnector for that edge.
|
||||
|
||||
## Relationship with vLLM
|
||||
|
||||
vLLM provides specialized distributed mechanisms for specific artifacts:
|
||||
|
||||
- KV Transfer (`vllm.distributed.kv_transfer`): optimized for KV caches.
|
||||
- EC Transfer (`vllm.distributed.ec_transfer`): optimized for encoder embeddings.
|
||||
- Device Communicators (`vllm.distributed.device_communicators`): low-level primitives (NCCL, SHM).
|
||||
|
||||
vllm-omni complements this with a generalized connector abstraction:
|
||||
|
||||
1. Unifies transport via a single `put`/`get` API for any stage artifact.
|
||||
2. Enables DAG-style pipelines across processes or nodes with per-edge transports.
|
||||
3. Can wrap vLLM-specific transfers for KV paths while keeping a consistent interface.
|
||||
|
||||
## Operational Notes
|
||||
|
||||
- Fail-fast config validation: missing expected edges cause startup failures.
|
||||
- Missing payloads halt stages: verify connector wiring and metadata propagation.
|
||||
|
||||
## Future Roadmap: D2D Transport
|
||||
|
||||
Current connectors use D2H2D paths. Future versions will introduce direct
|
||||
device-to-device connectors (NCCL, UCX, IPC) to reduce latency for large
|
||||
tensor payloads.
|
||||
@@ -0,0 +1,74 @@
|
||||
# MooncakeStoreConnector
|
||||
|
||||
## When to Use
|
||||
|
||||
Best for multi-node distributed inference using Mooncake.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
# For CUDA-enabled systems (recommended)
|
||||
pip install mooncake-transfer-engine
|
||||
|
||||
# For non-CUDA systems
|
||||
pip install mooncake-transfer-engine-non-cuda
|
||||
```
|
||||
|
||||
## Start Mooncake Master
|
||||
|
||||
```bash
|
||||
# If you use Mooncake SSD storage
|
||||
mkdir -p ./mc_storage
|
||||
|
||||
mooncake_master \
|
||||
--rpc_port=50051 \
|
||||
--enable_http_metadata_server=true \
|
||||
--http_metadata_server_host=0.0.0.0 \
|
||||
--http_metadata_server_port=8080 \
|
||||
--metrics_port=9003 \
|
||||
--root_fs_dir=./mc_storage/ \
|
||||
--cluster_id=mc-local-1 &
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Define the connector in runtime:
|
||||
|
||||
```yaml
|
||||
runtime:
|
||||
connectors:
|
||||
connector_of_mooncake:
|
||||
name: MooncakeStoreConnector
|
||||
extra:
|
||||
host: "127.0.0.1"
|
||||
metadata_server: "http://<MASTER_IP>:8080/metadata"
|
||||
master: "<MASTER_IP>:50051"
|
||||
segment: 512000000
|
||||
localbuf: 64000000
|
||||
proto: "tcp"
|
||||
```
|
||||
|
||||
Wire stages to the connector:
|
||||
|
||||
```yaml
|
||||
stage_args:
|
||||
- stage_id: 0
|
||||
output_connectors:
|
||||
to_stage_1: connector_of_mooncake
|
||||
|
||||
- stage_id: 1
|
||||
input_connectors:
|
||||
from_stage_0: connector_of_mooncake
|
||||
```
|
||||
|
||||
Parameters:
|
||||
|
||||
- host: local worker IP registered in the metadata server.
|
||||
- metadata_server: metadata server URL for discovery and setup.
|
||||
- master: Mooncake Master address.
|
||||
- segment: global memory segment size in bytes.
|
||||
- localbuf: local buffer size in bytes.
|
||||
- proto: transport protocol ("tcp" or "rdma").
|
||||
|
||||
For more details, refer to the
|
||||
[Mooncake repository](https://github.com/kvcache-ai/Mooncake).
|
||||
@@ -0,0 +1,207 @@
|
||||
# MooncakeTransferEngineConnector
|
||||
|
||||
## When to Use
|
||||
|
||||
Best for high-performance multi-node data transfer between stages using Mooncake
|
||||
Transfer Engine. Supports both RDMA and TCP protocols with a managed memory pool,
|
||||
zero-copy deserialization, and optional GPUDirect RDMA. Applicable to any
|
||||
inter-stage data (KV caches, request payloads, etc.), not limited to KV cache transfer.
|
||||
|
||||
Compared to `MooncakeStoreConnector` (TCP key-value store), this connector
|
||||
provides **~60x faster** data transfer via RDMA direct memory access.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pip install mooncake-transfer-engine
|
||||
```
|
||||
|
||||
Ensure RDMA drivers are installed on all nodes (e.g., Mellanox OFED for
|
||||
InfiniBand/RoCE NICs).
|
||||
|
||||
## Configuration
|
||||
|
||||
Define the connector in runtime:
|
||||
|
||||
```yaml
|
||||
runtime:
|
||||
connectors:
|
||||
rdma_connector:
|
||||
name: MooncakeTransferEngineConnector
|
||||
extra:
|
||||
host: "auto" # Auto-detect local RDMA IP
|
||||
zmq_port: 50051 # ZMQ base port (see "Port Offset Scheme" below)
|
||||
protocol: "rdma" # "rdma" or "tcp"
|
||||
device_name: "" # RDMA device (e.g., "mlx5_0"), empty for auto-detect
|
||||
memory_pool_size: 2147483648 # 2GB memory pool
|
||||
memory_pool_device: "cpu" # "cpu" for pinned memory, "cuda" for GPUDirect RDMA
|
||||
```
|
||||
|
||||
Wire stages to the connector:
|
||||
|
||||
```yaml
|
||||
stage_args:
|
||||
- stage_id: 0
|
||||
output_connectors:
|
||||
to_stage_1: rdma_connector
|
||||
|
||||
- stage_id: 1
|
||||
input_connectors:
|
||||
from_stage_0: rdma_connector
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
### Required
|
||||
|
||||
| Parameter | Description |
|
||||
|---|---|
|
||||
| `role` | **Internal, do not set manually.** Auto-injected by the orchestration layer (`"sender"` for `output_connectors`, `"receiver"` for `input_connectors`). Defaults to `"sender"` if omitted. |
|
||||
| `host` | Local IP address for RDMA. `"auto"` detects from network interfaces. |
|
||||
| `protocol` | Transport protocol: `"rdma"` (InfiniBand/RoCE) or `"tcp"`. |
|
||||
|
||||
### Memory Pool
|
||||
|
||||
| Parameter | Default | Description |
|
||||
|---|---|---|
|
||||
| `memory_pool_size` | 1 GB | Total size of the RDMA-registered memory pool in bytes. |
|
||||
| `memory_pool_device` | `"cpu"` | `"cpu"`: pinned host memory (recommended). `"cuda"`: GPU VRAM for GPUDirect RDMA (requires NIC-GPU direct PCIe connectivity). |
|
||||
|
||||
### Networking
|
||||
|
||||
| Parameter | Default | Description |
|
||||
|---|---|---|
|
||||
| `zmq_port` | 50051 | ZMQ **base** port. The orchestration layer computes the actual port as `base + purpose_offset + stage_offset` (see table below). Users only set this base value. |
|
||||
| `sender_host` | `None` | **Internal.** Receiver-side only — dynamically resolved via `update_sender_info()`. Not needed in YAML. |
|
||||
| `sender_zmq_port` | `None` | **Internal.** Receiver-side only — defaults to the sender's adjusted port. Not needed in YAML. |
|
||||
| `device_name` | `""` | RDMA device name (e.g., `"mlx5_0"`). Empty for auto-detect. Can also be set via `RDMA_DEVICE_NAME` env var. |
|
||||
|
||||
#### ZMQ Port Offset Scheme
|
||||
|
||||
To avoid port conflicts when multiple edges, purposes, DP replicas, or TP ranks share the same node, the actual ZMQ port is computed as:
|
||||
|
||||
```
|
||||
side_channel_port = zmq_port + purpose_offset + stage_offset + dp_index * tp_size
|
||||
sender_listen = side_channel_port + tp_rank
|
||||
receiver_connect = remote_side_channel_port + tp_rank
|
||||
```
|
||||
|
||||
| Component | Value | Description |
|
||||
|---|---|---|
|
||||
| `zmq_port` | 50051 (default) | Base port from YAML config |
|
||||
| `purpose_offset` | `request_forwarding` = 0, `kv_transfer` = 100 | Separates control-plane vs KV-cache connections |
|
||||
| `stage_offset` | `int(from_stage)` (0, 1, 2...) | Separates edges from different source stages |
|
||||
| `dp_index * tp_size` | e.g., DP1 × TP2 = 2 | Each DP replica reserves a port range of size `tp_size` (following vLLM convention: `VLLM_MOONCAKE_BOOTSTRAP_PORT + dp_index * tp_size`) |
|
||||
| `tp_rank` | 0, 1, 2... | Each TP rank within a DP replica uses its own port |
|
||||
| orchestrator | +200 | Extra offset when caller is the orchestrator (avoids collision with stage workers on the same node) |
|
||||
|
||||
**Example** (base=50051, stage 0→1, DP=2, TP=2, kv_transfer):
|
||||
|
||||
| Caller | DP | TP rank | Port |
|
||||
|---|---|---|---|
|
||||
| Stage worker | DP0 | rank 0 | `50051 + 100 + 0 + 0×2 + 0 = 50151` |
|
||||
| Stage worker | DP0 | rank 1 | `50051 + 100 + 0 + 0×2 + 1 = 50152` |
|
||||
| Stage worker | DP1 | rank 0 | `50051 + 100 + 0 + 1×2 + 0 = 50153` |
|
||||
| Stage worker | DP1 | rank 1 | `50051 + 100 + 0 + 1×2 + 1 = 50154` |
|
||||
| Orchestrator | — | — | `50051 + 200 + 0 = 50251` |
|
||||
|
||||
## Memory Pool Modes
|
||||
|
||||
| Mode | Config | Data Flow | Best For |
|
||||
|---|---|---|---|
|
||||
| CPU Pinned | `memory_pool_device: "cpu"` | GPU → CPU pool → RDMA → CPU pool → GPU | Most hardware topologies (recommended) |
|
||||
| GPUDirect | `memory_pool_device: "cuda"` | GPU → GPU pool → RDMA (NIC reads GPU BAR1) → GPU pool | NIC-GPU direct PCIe (PIX topology) |
|
||||
|
||||
> **Note**: GPUDirect RDMA requires the NIC and GPU to share a direct PCIe
|
||||
> switch (PIX topology). On systems where they are connected via PXB or NODE,
|
||||
> CPU pinned memory is faster due to GPU BAR1 bandwidth limitations.
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Description |
|
||||
|---|---|
|
||||
| `RDMA_DEVICE_NAME` | Override RDMA device name (e.g., `mlx5_0`). |
|
||||
| `MC_IB_PCI_RELAXED_ORDERING` | Set to `1` to enable PCIe relaxed ordering for GPUDirect. |
|
||||
|
||||
## Docker / Container Setup
|
||||
|
||||
RDMA requires host-level device access:
|
||||
|
||||
```bash
|
||||
docker run -it \
|
||||
--cap-add=SYS_PTRACE \
|
||||
--cap-add=IPC_LOCK \
|
||||
--security-opt seccomp=unconfined \
|
||||
--network=host \
|
||||
--device=/dev/infiniband \
|
||||
-v /sys/class/infiniband:/sys/class/infiniband:ro \
|
||||
your-image:tag
|
||||
```
|
||||
|
||||
## Performance
|
||||
|
||||
Benchmark results on H800 GPUs with mlx5_0 RDMA NIC (~186 MB KV cache):
|
||||
|
||||
| Metric | MooncakeStoreConnector | MooncakeTransferEngineConnector (CPU) |
|
||||
|---|---|---|
|
||||
| KV transfer wall time | ~810 ms | **~14 ms** |
|
||||
| RDMA throughput | N/A (TCP) | ~22 GB/s |
|
||||
| Speedup | 1x | **58x** |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Quick Diagnostics
|
||||
|
||||
```bash
|
||||
# 1. Check RDMA devices and link status
|
||||
ibdev2netdev
|
||||
# Expected: "mlx5_X port 1 ==> <iface> (Up)"
|
||||
# RoCE devices map to Ethernet interfaces (e.g., enp75s0f0)
|
||||
# IB devices map to ib0, ib1, etc.
|
||||
|
||||
# 2. Check InfiniBand device details
|
||||
ibstat
|
||||
|
||||
# 3. Verify /dev/infiniband is accessible (required in containers)
|
||||
ls /dev/infiniband/
|
||||
|
||||
# 4. Check Mooncake installation
|
||||
python -c "from mooncake.engine import TransferEngine; print('OK')"
|
||||
|
||||
# 5. Check environment variables
|
||||
echo "RDMA_DEVICE_NAME=${RDMA_DEVICE_NAME:-<not set>}"
|
||||
echo "MC_IB_PCI_RELAXED_ORDERING=${MC_IB_PCI_RELAXED_ORDERING:-<not set>}"
|
||||
```
|
||||
|
||||
### Common Issues
|
||||
|
||||
| Symptom | Cause | Fix |
|
||||
|---|---|---|
|
||||
| `Failed to modify QP to RTR` | Cross-NIC QP handshake failure (multi-NIC DGX) | Set `device_name` to a single RoCE NIC (e.g., `mlx5_2`) or set `RDMA_DEVICE_NAME` env var |
|
||||
| `transport retry counter exceeded` | RDMA path between incompatible NICs | Same as above — restrict to one NIC |
|
||||
| `zmq.error.Again: Resource temporarily unavailable` | ZMQ recv timeout (transfer took too long) | Check NIC selection; increase data may need longer timeout |
|
||||
| `Mooncake Engine initialization failed` | Missing RDMA drivers or `/dev/infiniband` | Install Mellanox OFED; in Docker add `--device=/dev/infiniband` |
|
||||
| `MemoryError` in allocator | Memory pool too small for payload | Increase `memory_pool_size` |
|
||||
| GPU transfer slower than CPU | GPU BAR1 bandwidth limitation (PXB/NODE topology) | Use `memory_pool_device: "cpu"` instead of `"cuda"` |
|
||||
|
||||
### Multi-NIC Environments (DGX)
|
||||
|
||||
On DGX machines with 12+ RDMA NICs, only RoCE NICs (with a bound network
|
||||
interface) reliably support loopback. IB-only NICs may fail cross-NIC QP
|
||||
handshakes. To identify RoCE NICs:
|
||||
|
||||
```bash
|
||||
ibdev2netdev | grep -v "ib[0-9]"
|
||||
# RoCE devices show Ethernet interface names like enp75s0f0
|
||||
```
|
||||
|
||||
Then configure the connector:
|
||||
```yaml
|
||||
device_name: "mlx5_2" # or set RDMA_DEVICE_NAME=mlx5_2
|
||||
```
|
||||
|
||||
See the RDMA Test README in tests/distributed/omni_connectors/README.md
|
||||
for test-specific setup instructions.
|
||||
|
||||
For more details on the underlying engine, refer to the
|
||||
[Mooncake repository](https://github.com/kvcache-ai/Mooncake).
|
||||
@@ -0,0 +1,27 @@
|
||||
# SharedMemoryConnector
|
||||
|
||||
## When to Use
|
||||
|
||||
Best for single-node deployments where stages run on the same host. It is
|
||||
auto-configured when no explicit connector is specified for an edge.
|
||||
|
||||
## How It Works
|
||||
|
||||
- Small payloads (< threshold): serialized and passed inline in metadata.
|
||||
- Large payloads (>= threshold): stored in shared memory; the SHM name is
|
||||
returned in metadata.
|
||||
|
||||
## Configuration
|
||||
|
||||
```yaml
|
||||
runtime:
|
||||
connectors:
|
||||
connector_of_shared_memory:
|
||||
name: SharedMemoryConnector
|
||||
extra:
|
||||
shm_threshold_bytes: 65536
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- Auto-mode uses SharedMemoryConnector if no connector is declared for an edge.
|
||||
@@ -0,0 +1,101 @@
|
||||
# YuanrongConnector
|
||||
|
||||
## When to Use
|
||||
|
||||
Best for multi-node distributed inference using Yuanrong Datasystem.
|
||||
|
||||
## Mechanism
|
||||
|
||||
Uses Yuanrong Datasystem's distributed KV store (`datasystem.kv_client`).
|
||||
|
||||
- Data Plane: TCP or RDMA for high-bandwidth transfer.
|
||||
- Control Plane: Yuanrong Datasystem workers and etcd.
|
||||
- Keying: deterministic keys based on `put_key` (often composed as `request_id:fromStage_toStage`).
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pip install openyuanrong-datasystem
|
||||
```
|
||||
|
||||
## Start etcd
|
||||
|
||||
```bash
|
||||
# Download and install etcd (v3.5.12 or higher)
|
||||
ETCD_VERSION="v3.5.12"
|
||||
ETCD_ARCH="linux-arm64"
|
||||
wget https://github.com/etcd-io/etcd/releases/download/${ETCD_VERSION}/etcd-${ETCD_VERSION}-${ETCD_ARCH}.tar.gz
|
||||
tar -xvf etcd-${ETCD_VERSION}-${ETCD_ARCH}.tar.gz
|
||||
cd etcd-${ETCD_VERSION}-${ETCD_ARCH}
|
||||
sudo cp etcd etcdctl /usr/local/bin/
|
||||
|
||||
# Start etcd
|
||||
etcd \
|
||||
--name etcd-single \
|
||||
--data-dir /tmp/etcd-data \
|
||||
--listen-client-urls http://0.0.0.0:2379 \
|
||||
--advertise-client-urls http://0.0.0.0:2379 \
|
||||
--listen-peer-urls http://0.0.0.0:2380 \
|
||||
--initial-advertise-peer-urls http://0.0.0.0:2380 \
|
||||
--initial-cluster etcd-single=http://0.0.0.0:2380 &
|
||||
|
||||
# Verify etcd is running
|
||||
etcdctl --endpoints "127.0.0.1:2379" put key "value"
|
||||
etcdctl --endpoints "127.0.0.1:2379" get key
|
||||
```
|
||||
|
||||
For production environments, refer to the
|
||||
[official etcd clustering documentation](https://etcd.io/docs/current/op-guide/clustering/).
|
||||
|
||||
## Start Datasystem Worker
|
||||
|
||||
```bash
|
||||
# Replace ${ETCD_IP} with etcd node IP, ${WORKER_IP} with local node IP
|
||||
dscli start -w \
|
||||
--worker_address "${WORKER_IP}:31501" \
|
||||
--etcd_address "${ETCD_IP}:2379" \
|
||||
--shared_memory_size_mb 20480
|
||||
```
|
||||
|
||||
To stop the worker:
|
||||
|
||||
```bash
|
||||
dscli stop --worker_address "${WORKER_IP}:31501"
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Define the connector in runtime:
|
||||
|
||||
```yaml
|
||||
runtime:
|
||||
connectors:
|
||||
connector_of_yuanrong:
|
||||
name: YuanrongConnector
|
||||
extra:
|
||||
host: "127.0.0.1"
|
||||
port: 31501
|
||||
get_sub_timeout_ms: 1000
|
||||
```
|
||||
|
||||
Wire stages to the connector:
|
||||
|
||||
```yaml
|
||||
stage_args:
|
||||
- stage_id: 0
|
||||
output_connectors:
|
||||
to_stage_1: connector_of_yuanrong
|
||||
|
||||
- stage_id: 1
|
||||
input_connectors:
|
||||
from_stage_0: connector_of_yuanrong
|
||||
```
|
||||
|
||||
Parameters:
|
||||
|
||||
- host: datasystem worker host.
|
||||
- port: datasystem worker port.
|
||||
- get_sub_timeout_ms: get timeout in milliseconds (0 for no timeout).
|
||||
|
||||
For more details, refer to the
|
||||
[Yuanrong Datasystem repository](https://atomgit.com/openeuler/yuanrong-datasystem).
|
||||
@@ -0,0 +1,59 @@
|
||||
# Distributed utils
|
||||
|
||||
This directory (vllm_omni/distributed/ray_utils) contains utilities for distributed execution in vllm-omni, supporting both **Ray** and **Multiprocessing** backends.
|
||||
## 1. Installation
|
||||
```bash
|
||||
pip install "ray[default]"
|
||||
```
|
||||
## 2. Ray Utils
|
||||
|
||||
The `ray_utils` module provides helper functions for managing Ray clusters and actors, which is essential for:
|
||||
* **Multi-node deployment**: Running pipeline stages across different physical machines.
|
||||
* **Resource management**: Efficient GPU/CPU allocation.
|
||||
|
||||
### 2.1 Basic Usage
|
||||
|
||||
To use the Ray backend, specify `worker_backend="ray"` when initializing the engine.
|
||||
|
||||
**Command Line Example:**
|
||||
```bash
|
||||
vllm serve Qwen/Qwen2.5-Omni-7B \
|
||||
--omni \
|
||||
--port 8091 \
|
||||
--worker-backend ray \
|
||||
--ray-address auto
|
||||
```
|
||||
|
||||
### 2.2 Cluster Setup
|
||||
|
||||
**Step 1: Start Head Node**
|
||||
Run this on your primary machine:
|
||||
```bash
|
||||
ray start --head --port=6399
|
||||
```
|
||||
|
||||
**Step 2: Connect Worker Nodes**
|
||||
Run this on each worker machine:
|
||||
```bash
|
||||
ray start --address=<HEAD_NODE_IP>:6399
|
||||
```
|
||||
|
||||
> **Tip**: For a complete cluster setup script, refer to the vLLM example:
|
||||
> [run_cluster.sh](https://github.com/vllm-project/vllm/blob/main/examples/online_serving/run_cluster.sh)
|
||||
|
||||
### 2.3 Distributed Connector Support
|
||||
|
||||
When running on Ray, the system automatically adapts its communication strategy:
|
||||
|
||||
* **Cross-Node**: Recommended to use `MooncakeTransferEngineConnector` (RDMA, fastest) or `MooncakeStoreConnector` (TCP fallback).
|
||||
* **Same-Node**: Can still use `SharedMemoryConnector` for efficiency, or Ray's native object store (plasma).
|
||||
* **SHM threshold default differs**: when `worker_backend="ray"`, the SharedMemoryConnector default threshold is set to `sys.maxsize`, which forces payloads to go inline (no SHM). Override `shm_threshold_bytes` in the connector config if you want SHM for Ray runs.
|
||||
|
||||
### 2.4 Internal Helpers
|
||||
|
||||
* **`initialize_ray_cluster`**: Connects to an existing Ray cluster or starts a local one.
|
||||
|
||||
## 3. Troubleshooting
|
||||
|
||||
* **Connection Issues**: Ensure the Ray head node is accessible and ports (default 6399 in this example) are open.
|
||||
* **Version Mismatch**: Ensure all nodes run the same version of Ray and Python.
|
||||
@@ -0,0 +1,456 @@
|
||||
# Sequence Parallel
|
||||
|
||||
This section describes how to add Sequence Parallel (SP) to a diffusion transformer model. We use the Qwen-Image transformer and Wan2.2 transformer as reference implementations.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Approach 1: Non-Intrusive `_sp_plan` (Recommended)](#approach-1-non-intrusive-_sp_plan-recommended)
|
||||
- [Approach 2: Intrusive Modification (For Complex Cases)](#approach-2-intrusive-modification-for-complex-cases)
|
||||
- [Testing](#testing)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
- [Reference Implementations](#reference-implementations)
|
||||
- [Summary](#summary)
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
|
||||
### What is Sequence Parallel?
|
||||
|
||||
**Terminology Note:** Our "Sequence Parallelism" (SP) corresponds to "Context Parallelism" (CP) in the [diffusers library](https://github.com/huggingface/diffusers/blob/main/src/diffusers/models/_modeling_parallel.py). We use "Sequence Parallelism" to align with vLLM-Omni's terminology.
|
||||
|
||||
Diffusion transformers process long sequences of image patches or video frames. For high-resolution generation, these sequences can become very large. Enabling SP allows each GPU to process only a portion of the sequence, with attention mechanisms (Ulysses/Ring) handling cross-GPU communication transparently.
|
||||
|
||||
### Architecture
|
||||
|
||||
The major APIs for Sequence Parallel:
|
||||
|
||||
```python
|
||||
from vllm_omni.diffusion.distributed.sp_plan import (
|
||||
SequenceParallelInput, # For sharding (splitting) tensors
|
||||
SequenceParallelOutput, # For gathering tensors
|
||||
)
|
||||
from vllm_omni.diffusion.distributed.sp_sharding import sp_shard, sp_gather
|
||||
```
|
||||
|
||||
| Method/Class | Purpose | Behavior |
|
||||
|--------------|---------|----------|
|
||||
| `SequenceParallelInput` | Declare input sharding in `_sp_plan` | Auto-shards tensors at module input |
|
||||
| `SequenceParallelOutput` | Declare output gathering in `_sp_plan` | Auto-gathers tensors at module output |
|
||||
| `sp_shard()` | Manual tensor sharding | Splits tensor across SP workers |
|
||||
| `sp_gather()` | Manual tensor gathering | Gathers sharded tensors from all workers |
|
||||
|
||||
---
|
||||
|
||||
## Approach 1: Non-Intrusive `_sp_plan` (Recommended)
|
||||
|
||||
The `_sp_plan` mechanism allows SP **without modifying `forward()` logic**. The framework automatically registers hooks to shard inputs and gather outputs at module boundaries.
|
||||
|
||||
**When to use:**
|
||||
- Standard transformer architectures
|
||||
- Tensor operations happen at `nn.Module` boundaries
|
||||
- Predictable sharding/gathering patterns
|
||||
|
||||
**How it works:**
|
||||
1. Declare `_sp_plan` dict in your transformer class
|
||||
2. Framework automatically applies hooks when `sequence_parallel_size > 1`
|
||||
3. Hooks shard/gather tensors at specified module boundaries
|
||||
4. Attention layers handle cross-GPU communication internally
|
||||
|
||||
```python
|
||||
class StandardTransformer(nn.Module):
|
||||
_sp_plan = {
|
||||
# Shard hidden_states at first transformer block input
|
||||
"blocks.0": {
|
||||
"hidden_states": SequenceParallelInput(split_dim=1, expected_dims=3),
|
||||
},
|
||||
# Gather at final output projection
|
||||
"proj_out": SequenceParallelOutput(gather_dim=1, expected_dims=3),
|
||||
}
|
||||
```
|
||||
|
||||
`StandardTransformer` has a transformer blocks list `self.blocks = nn.ModuleList([...])`, and a projection output layer `self.proj_out`. The `_sp_plan` above defines that when SP is enabled, sharding the input tensor to the first transformer block, and gathering the sharded tensor at the final output projection layer.
|
||||
|
||||
**Requirements:**
|
||||
- Tensor operations that need sharding/gathering must happen at **`nn.Module` boundaries**
|
||||
- Inline Python operations (e.g., `torch.cat`, `pad_sequence`) **cannot be hooked**
|
||||
|
||||
**Solution for inline operations:** Extract into a submodule (see Step 2 below).
|
||||
|
||||
### Step 1: Understand Module Boundaries
|
||||
|
||||
Identify where tensors need to be sharded or gathered in your model's forward pass:
|
||||
|
||||
```python
|
||||
class MyTransformer(nn.Module):
|
||||
def __init__(self):
|
||||
self.patch_embed = PatchEmbed() # ← Boundary 1
|
||||
self.pos_embed = RoPE() # ← Boundary 2
|
||||
self.blocks = nn.ModuleList([...]) # ← Boundary 3
|
||||
self.norm_out = LayerNorm()
|
||||
self.proj_out = Linear() # ← Boundary 4
|
||||
|
||||
def forward(self, x):
|
||||
x = self.patch_embed(x) # ← Shard before this?
|
||||
pos = self.pos_embed(x) # ← Shard RoPE outputs?
|
||||
for block in self.blocks:
|
||||
x = block(x, pos) # ← Blocks process sharded x
|
||||
x = self.norm_out(x)
|
||||
output = self.proj_out(x) # ← Gather after this?
|
||||
return output
|
||||
```
|
||||
|
||||
### Step 2: Handle Inline Operations
|
||||
|
||||
If your `forward()` contains inline tensor operations, **extract them into submodules**.
|
||||
|
||||
**Example: Z-Image concatenates image + text features inline**
|
||||
|
||||
```python
|
||||
# ❌ BAD: Inline operation - hooks cannot intercept
|
||||
class ZImageTransformer(nn.Module):
|
||||
def forward(self, x, cap_feats):
|
||||
# This concatenation happens inline - _sp_plan can't shard it!
|
||||
unified = torch.cat([x, cap_feats], dim=1)
|
||||
|
||||
for layer in self.layers:
|
||||
unified = layer(unified)
|
||||
|
||||
return unified
|
||||
|
||||
# ✅ GOOD: Extract into submodule
|
||||
class UnifiedPrepare(nn.Module):
|
||||
"""Submodule to concatenate image and text features."""
|
||||
def forward(self, x, cap_feats):
|
||||
return torch.cat([x, cap_feats], dim=1)
|
||||
|
||||
class ZImageTransformer(nn.Module):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.unified_prepare = UnifiedPrepare() # Now a module!
|
||||
self.layers = nn.ModuleList([...])
|
||||
|
||||
def forward(self, x, cap_feats):
|
||||
# Now _sp_plan can shard the output of unified_prepare!
|
||||
unified = self.unified_prepare(x, cap_feats)
|
||||
|
||||
for layer in self.layers:
|
||||
unified = layer(unified)
|
||||
|
||||
return unified
|
||||
```
|
||||
|
||||
**Other common cases:**
|
||||
- `pad_sequence()` → `PadSequenceModule`
|
||||
- `torch.cat()` → `ConcatModule`
|
||||
- `tensor.reshape()` → `ReshapeModule`
|
||||
- Complex preprocessing → `PreprocessModule`
|
||||
|
||||
### Step 3: Write `_sp_plan` for Your Model
|
||||
|
||||
Create a class-level `_sp_plan` dictionary specifying where to shard/gather tensors.
|
||||
|
||||
Typically, there are two patterns for diffusion models:
|
||||
|
||||
**Pattern 1: Shard at first block, gather at output projection**
|
||||
|
||||
Most common pattern for standard transformers:
|
||||
|
||||
```python
|
||||
from vllm_omni.diffusion.distributed.sp_plan import (
|
||||
SequenceParallelInput, # For sharding (splitting) tensors
|
||||
SequenceParallelOutput, # For gathering tensors
|
||||
)
|
||||
class StandardTransformer(nn.Module):
|
||||
_sp_plan = {
|
||||
# Shard hidden_states at first transformer block input
|
||||
"blocks.0": {
|
||||
"hidden_states": SequenceParallelInput(split_dim=1, expected_dims=3),
|
||||
},
|
||||
# Gather at final output projection
|
||||
"proj_out": SequenceParallelOutput(gather_dim=1, expected_dims=3),
|
||||
}
|
||||
```
|
||||
|
||||
**Pattern 2: Shard RoPE embeddings separately**
|
||||
|
||||
When RoPE is computed in a separate module:
|
||||
|
||||
```python
|
||||
from vllm_omni.diffusion.distributed.sp_plan import (
|
||||
SequenceParallelInput, # For sharding (splitting) tensors
|
||||
SequenceParallelOutput, # For gathering tensors
|
||||
)
|
||||
class TransformerWithRoPE(nn.Module):
|
||||
_sp_plan = {
|
||||
# Shard RoPE module OUTPUTS (returns tuple of cos, sin)
|
||||
"rope": {
|
||||
0: SequenceParallelInput(split_dim=1, expected_dims=4, split_output=True), # cos
|
||||
1: SequenceParallelInput(split_dim=1, expected_dims=4, split_output=True), # sin
|
||||
},
|
||||
# Shard transformer block INPUT
|
||||
"blocks.0": {
|
||||
"hidden_states": SequenceParallelInput(split_dim=1, expected_dims=3),
|
||||
},
|
||||
# Gather at output
|
||||
"proj_out": SequenceParallelOutput(gather_dim=1, expected_dims=3),
|
||||
}
|
||||
```
|
||||
|
||||
### API Reference
|
||||
|
||||
**SequenceParallelInput Parameters:**
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `split_dim` | int | Dimension to split (usually `1` for sequence) |
|
||||
| `expected_dims` | int \| None | Expected tensor rank for validation (optional) |
|
||||
| `split_output` | bool | `False`: shard **input** params; `True`: shard **output** tensors |
|
||||
| `auto_pad` | bool | Auto-pad if sequence not divisible by world_size (default: `False`) |
|
||||
|
||||
**SequenceParallelOutput Parameters:**
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `gather_dim` | int | Dimension to gather (usually `1` for sequence) |
|
||||
| `expected_dims` | int \| None | Expected tensor rank for validation (optional) |
|
||||
|
||||
**Module Naming Conventions:**
|
||||
|
||||
| Key | Meaning | Python equivalent |
|
||||
|-----|---------|-------------------|
|
||||
| `""` | Root model | `model` |
|
||||
| `"blocks.0"` | First element of ModuleList | `model.blocks[0]` |
|
||||
| `"blocks.*"` | All elements of ModuleList | `for b in model.blocks` |
|
||||
| `"rope"` | Named submodule | `model.rope` |
|
||||
| `"outputs.main"` | ModuleDict entry | `model.outputs["main"]` |
|
||||
|
||||
**Dictionary Value Types:**
|
||||
|
||||
| Key type | `split_output` | Description |
|
||||
|----------|----------------|-------------|
|
||||
| `"param_name"` (str) | `False` | Shard **input parameter** by name |
|
||||
| `0`, `1`, ... (int) | `True` | Shard **output tuple** by index |
|
||||
|
||||
---
|
||||
|
||||
## Approach 2: Intrusive Modification (For Complex Cases)
|
||||
|
||||
For models with dynamic sharding logic that cannot be expressed via `_sp_plan`, manually insert shard/gather calls.
|
||||
|
||||
**When to use:**
|
||||
- Dynamic/conditional sharding logic
|
||||
- Complex tensor manipulations that can't be encapsulated
|
||||
- Temporary workaround during development
|
||||
|
||||
```python
|
||||
from vllm_omni.diffusion.distributed.sp_sharding import sp_shard, sp_gather
|
||||
|
||||
def forward(self, hidden_states, ...):
|
||||
if self.parallel_config.sequence_parallel_size > 1:
|
||||
hidden_states = sp_shard(hidden_states, dim=1)
|
||||
|
||||
# ... computation ...
|
||||
|
||||
if self.parallel_config.sequence_parallel_size > 1:
|
||||
output = sp_gather(output, dim=1)
|
||||
|
||||
return output
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
After implementing Sequence Parallel support, thoroughly test your implementation to ensure correctness and performance across different configurations.
|
||||
|
||||
**Test Different `sp_size`:**
|
||||
|
||||
Test your model with various sequence parallel world sizes to verify correctness and identify optimal configurations:
|
||||
|
||||
```bash
|
||||
cd examples/offline_inference/text_to_image
|
||||
python text_to_image.py \
|
||||
--model Your-org/your-model \
|
||||
--prompt "a cup of coffee on the table" \
|
||||
--num-inference-steps 50 \
|
||||
--ulysses-degree 2 \
|
||||
--ring-degree 2 \
|
||||
--output sp_test_image_ulysses=2_ring=2.png
|
||||
```
|
||||
|
||||
**Verify:**
|
||||
|
||||
1. **Correctness:** Output should be identical across all `sp_size` values
|
||||
2. **Speed:** Throughput should remain stable or improve (especially for large sequences)
|
||||
3. **Logs:** Check for any shape mismatch or communication errors
|
||||
|
||||
**Test with Tensor Parallel:**
|
||||
|
||||
Sequence Parallel can be combined with other parallelism strategies:
|
||||
|
||||
```bash
|
||||
cd examples/offline_inference/text_to_image
|
||||
python text_to_image.py \
|
||||
--model Your-org/your-model \
|
||||
--prompt "a cup of coffee on the table" \
|
||||
--num-inference-steps 50 \
|
||||
--ulysses-degree 2 \
|
||||
--tensor-parallel-size 2 \
|
||||
--output sp_test_image_ulysses=2_tp=2.png
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Issue: Shape mismatch errors
|
||||
|
||||
**Symptoms:** `RuntimeError: shape mismatch` during forward pass.
|
||||
|
||||
**Causes & Solutions:**
|
||||
|
||||
- **RoPE dimension mismatch:**
|
||||
|
||||
**Problem:** RoPE embeddings not sharded, but hidden_states is sharded.
|
||||
|
||||
**Solution:** Shard RoPE outputs in `_sp_plan`:
|
||||
```python
|
||||
_sp_plan = {
|
||||
"rope": {
|
||||
0: SequenceParallelInput(split_dim=1, expected_dims=4, split_output=True),
|
||||
1: SequenceParallelInput(split_dim=1, expected_dims=4, split_output=True),
|
||||
},
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
- **Sequence Length not divisible by sp_size:**
|
||||
|
||||
**Problem:** `SequenceParallelInput(auto_pad=False)` - auto_pad should be True to enable automatic sequence padding.
|
||||
|
||||
**Solution:** In `SequenceParallelInput`, set `auto_pad=True` and add attention mask support.
|
||||
|
||||
> **Experimental Feature:** `auto_pad=True` is an experimental feature and may be changed in the future. We plan to improve this solution to involve minimal changes to model files. More details are [here](https://github.com/vllm-project/vllm-omni/issues/1324).
|
||||
|
||||
**Constraints of auto_pad:**
|
||||
|
||||
| Constraint | Description |
|
||||
|------------|-------------|
|
||||
| **Attention Backend Compatibility** | The attention backends must support `attention_mask`. Currently only `TORCH_SDPA` and `FLASH_ATTN` (default for diffusion models) are supported. |
|
||||
| **Ring Attention Limitation** | Ring attention does not support `attention_mask`. Therefore, when using `auto_pad=True`, the combination of Ulysses + Ring attention is not feasible. |
|
||||
|
||||
1. Enable `auto_pad=True` for all sequence-dimension inputs in `_sp_plan`:
|
||||
```python
|
||||
_sp_plan = {
|
||||
"rope": {
|
||||
0: SequenceParallelInput(split_dim=1, expected_dims=4, split_output=True, auto_pad=True),
|
||||
1: SequenceParallelInput(split_dim=1, expected_dims=4, split_output=True, auto_pad=True),
|
||||
},
|
||||
"blocks.0": {
|
||||
"hidden_states": SequenceParallelInput(split_dim=1, expected_dims=3, auto_pad=True)
|
||||
},
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
2. Create attention mask dynamically when padding is applied:
|
||||
```python
|
||||
from vllm_omni.diffusion.forward_context import get_forward_context
|
||||
from vllm_omni.diffusion.attention.backends.abstract import AttentionMetadata
|
||||
|
||||
# In model forward(), before transformer blocks:
|
||||
hidden_states_mask = None
|
||||
ctx = get_forward_context()
|
||||
if ctx.sp_original_seq_len is not None and ctx.sp_padding_size > 0:
|
||||
batch_size = hidden_states.shape[0]
|
||||
padded_seq_len = ctx.sp_original_seq_len + ctx.sp_padding_size
|
||||
hidden_states_mask = torch.ones(batch_size, padded_seq_len, dtype=torch.bool, device=hidden_states.device)
|
||||
hidden_states_mask[:, ctx.sp_original_seq_len:] = False
|
||||
|
||||
# Pass mask to attention layers
|
||||
attn_metadata = AttentionMetadata(attn_mask=hidden_states_mask) if hidden_states_mask is not None else None
|
||||
output = self.attn(query, key, value, attn_metadata)
|
||||
```
|
||||
|
||||
**Important Quality Considerations:**
|
||||
|
||||
While `auto_pad` enables generation for irregular resolutions, be aware of potential quality impacts:
|
||||
|
||||
| Aspect | Impact |
|
||||
|--------|--------|
|
||||
| **Training Distribution** | Models perform best on aspect ratios seen during training (e.g., 1:1, 16:9, 4:3). Unusual ratios like 700x400 (1.75:1) may produce lower quality results. |
|
||||
| **Padding Overhead** | Padded positions consume compute even when masked. For best efficiency, prefer resolutions divisible by `sp_size`. |
|
||||
|
||||
**Recommendations for users:**
|
||||
- Use standard aspect ratios when possible (e.g., 768x432 for 16:9 instead of 700x400)
|
||||
- Ensure post-patch dimensions are divisible by `sp_size` for optimal quality
|
||||
- Test generation quality when using unusual resolutions
|
||||
|
||||
### Issue: Inline operations not sharded
|
||||
|
||||
**Symptoms:** Some tensors remain full-sized, not sharded.
|
||||
|
||||
**Causes & Solutions:**
|
||||
|
||||
- **Operations happen inline in `forward()`, not at module boundaries:**
|
||||
|
||||
**Problem:**
|
||||
```python
|
||||
def forward(self, x, cap):
|
||||
unified = torch.cat([x, cap], dim=1) # ← Inline operation!
|
||||
# _sp_plan can't hook this
|
||||
```
|
||||
|
||||
**Solution:** Extract into submodule:
|
||||
```python
|
||||
class ConcatModule(nn.Module):
|
||||
def forward(self, x, cap):
|
||||
return torch.cat([x, cap], dim=1)
|
||||
|
||||
class MyModel(nn.Module):
|
||||
_sp_plan = {
|
||||
"concat": {
|
||||
0: SequenceParallelInput(split_dim=1, expected_dims=4, split_output=True),
|
||||
1: SequenceParallelInput(split_dim=1, expected_dims=4, split_output=True),
|
||||
},
|
||||
...
|
||||
}
|
||||
def __init__(self):
|
||||
self.concat = ConcatModule() # Now hookable!
|
||||
|
||||
def forward(self, x, cap):
|
||||
unified = self.concat(x, cap) # ← Can be sharded via _sp_plan
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Reference Implementations
|
||||
|
||||
Complete examples in the codebase:
|
||||
|
||||
| Model | Path | Pattern | Notes |
|
||||
|-------|------|---------|-------|
|
||||
| **Qwen-Image** | `vllm_omni/diffusion/models/qwen_image/qwen_image_transformer.py` | Dual-stream + preprocessing | auto_pad, separate RoPE |
|
||||
| **Wan2.2** | `vllm_omni/diffusion/models/wan2_2/wan2_2_transformer.py` | Dual-Transformer + RoPE | Video transformer |
|
||||
| **Z-Image** | `vllm_omni/diffusion/models/z_image/z_image_transformer.py` | Unified sequence | Concatenated input |
|
||||
| **SP Plan Types** | `vllm_omni/diffusion/distributed/sp_plan.py` | Type definitions | SequenceParallelInput/Output |
|
||||
| **Hook Implementation** | `vllm_omni/diffusion/hooks/sequence_parallel.py` | Hook mechanics | How hooks work |
|
||||
| **Tests** | `tests/diffusion/distributed/test_sp_plan_hooks.py` | Test examples | Validation patterns |
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
Adding Sequence Parallel support to a transformer:
|
||||
|
||||
1. ✅ **Choose approach** - Use `_sp_plan` for standard cases, intrusive modification for complex cases
|
||||
2. ✅ **Identify sharding boundaries** - Where should tensors be split/gathered?
|
||||
3. ✅ **Extract inline operations** - Move `torch.cat`, `pad_sequence`, etc. to submodules
|
||||
4. ✅ **Define `_sp_plan`** - Declare shard/gather points as class attribute
|
||||
5. ✅ **Use `auto_pad` for variable lengths** - Support non-uniform sequences
|
||||
6. ✅ **Test** - Verify with different `ulysses_degree` and `ring_degree` combinations
|
||||
Loaded 100 of 663 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user