Add files using upload-large-folder tool
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- bin/activate +76 -0
- bin/deepspeed +6 -0
- bin/ds_report +6 -0
- bin/fastapi +8 -0
- bin/huggingface-cli +8 -0
- bin/ipython3 +8 -0
- bin/isympy +8 -0
- bin/markdown_py +8 -0
- bin/pip3 +8 -0
- bin/pip3.12 +8 -0
- bin/proton +8 -0
- bin/pyftmerge +8 -0
- bin/ray +8 -0
- bin/serve +8 -0
- bin/wandb +8 -0
- bin/wb +8 -0
- lib/python3.12/site-packages/decorator.py +459 -0
- lib/python3.12/site-packages/ipython_pygments_lexers.py +582 -0
- lib/python3.12/site-packages/isympy.py +342 -0
- lib/python3.12/site-packages/package_readme.md +97 -0
- lib/python3.12/site-packages/pylab.py +3 -0
- lib/python3.12/site-packages/sglang/__init__.py +76 -0
- lib/python3.12/site-packages/sglang/api.py +279 -0
- lib/python3.12/site-packages/sglang/bench_offline_throughput.py +424 -0
- lib/python3.12/site-packages/sglang/bench_one_batch.py +568 -0
- lib/python3.12/site-packages/sglang/bench_one_batch_server.py +192 -0
- lib/python3.12/site-packages/sglang/bench_serving.py +1623 -0
- lib/python3.12/site-packages/sglang/check_env.py +305 -0
- lib/python3.12/site-packages/sglang/compile_deep_gemm.py +177 -0
- lib/python3.12/site-packages/sglang/global_config.py +49 -0
- lib/python3.12/site-packages/sglang/launch_server.py +16 -0
- lib/python3.12/site-packages/sglang/llama3_eval.py +316 -0
- lib/python3.12/site-packages/sglang/srt/_custom_ops.py +115 -0
- lib/python3.12/site-packages/sglang/srt/aio_rwlock.py +100 -0
- lib/python3.12/site-packages/sglang/srt/code_completion_parser.py +174 -0
- lib/python3.12/site-packages/sglang/srt/conversation.py +794 -0
- lib/python3.12/site-packages/sglang/srt/custom_op.py +44 -0
- lib/python3.12/site-packages/sglang/srt/disaggregation/decode.py +661 -0
- lib/python3.12/site-packages/sglang/srt/disaggregation/fake/__init__.py +1 -0
- lib/python3.12/site-packages/sglang/srt/disaggregation/fake/__pycache__/__init__.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/sglang/srt/disaggregation/fake/__pycache__/conn.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/sglang/srt/disaggregation/fake/conn.py +88 -0
- lib/python3.12/site-packages/sglang/srt/disaggregation/mini_lb.py +318 -0
- lib/python3.12/site-packages/sglang/srt/disaggregation/mooncake/__init__.py +6 -0
- lib/python3.12/site-packages/sglang/srt/disaggregation/mooncake/__pycache__/transfer_engine.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/sglang/srt/disaggregation/mooncake/conn.py +752 -0
- lib/python3.12/site-packages/sglang/srt/disaggregation/mooncake/transfer_engine.py +77 -0
- lib/python3.12/site-packages/sglang/srt/disaggregation/prefill.py +417 -0
- lib/python3.12/site-packages/sglang/srt/disaggregation/utils.py +121 -0
- lib/python3.12/site-packages/sglang/srt/function_call_parser.py +761 -0
bin/activate
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# This file must be used with "source bin/activate" *from bash*
|
| 2 |
+
# You cannot run it directly
|
| 3 |
+
|
| 4 |
+
deactivate () {
|
| 5 |
+
# reset old environment variables
|
| 6 |
+
if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then
|
| 7 |
+
PATH="${_OLD_VIRTUAL_PATH:-}"
|
| 8 |
+
export PATH
|
| 9 |
+
unset _OLD_VIRTUAL_PATH
|
| 10 |
+
fi
|
| 11 |
+
if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then
|
| 12 |
+
PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}"
|
| 13 |
+
export PYTHONHOME
|
| 14 |
+
unset _OLD_VIRTUAL_PYTHONHOME
|
| 15 |
+
fi
|
| 16 |
+
|
| 17 |
+
# Call hash to forget past locations. Without forgetting
|
| 18 |
+
# past locations the $PATH changes we made may not be respected.
|
| 19 |
+
# See "man bash" for more details. hash is usually a builtin of your shell
|
| 20 |
+
hash -r 2> /dev/null
|
| 21 |
+
|
| 22 |
+
if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then
|
| 23 |
+
PS1="${_OLD_VIRTUAL_PS1:-}"
|
| 24 |
+
export PS1
|
| 25 |
+
unset _OLD_VIRTUAL_PS1
|
| 26 |
+
fi
|
| 27 |
+
|
| 28 |
+
unset VIRTUAL_ENV
|
| 29 |
+
unset VIRTUAL_ENV_PROMPT
|
| 30 |
+
if [ ! "${1:-}" = "nondestructive" ] ; then
|
| 31 |
+
# Self destruct!
|
| 32 |
+
unset -f deactivate
|
| 33 |
+
fi
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
# unset irrelevant variables
|
| 37 |
+
deactivate nondestructive
|
| 38 |
+
|
| 39 |
+
# on Windows, a path can contain colons and backslashes and has to be converted:
|
| 40 |
+
case "$(uname)" in
|
| 41 |
+
CYGWIN*|MSYS*|MINGW*)
|
| 42 |
+
# transform D:\path\to\venv to /d/path/to/venv on MSYS and MINGW
|
| 43 |
+
# and to /cygdrive/d/path/to/venv on Cygwin
|
| 44 |
+
VIRTUAL_ENV=$(cygpath /storage/openpsi/users/zzy/.threadweaver)
|
| 45 |
+
export VIRTUAL_ENV
|
| 46 |
+
;;
|
| 47 |
+
*)
|
| 48 |
+
# use the path as-is
|
| 49 |
+
export VIRTUAL_ENV=/storage/openpsi/users/zzy/.threadweaver
|
| 50 |
+
;;
|
| 51 |
+
esac
|
| 52 |
+
|
| 53 |
+
_OLD_VIRTUAL_PATH="$PATH"
|
| 54 |
+
PATH="$VIRTUAL_ENV/"bin":$PATH"
|
| 55 |
+
export PATH
|
| 56 |
+
|
| 57 |
+
VIRTUAL_ENV_PROMPT='(.threadweaver) '
|
| 58 |
+
export VIRTUAL_ENV_PROMPT
|
| 59 |
+
|
| 60 |
+
# unset PYTHONHOME if set
|
| 61 |
+
# this will fail if PYTHONHOME is set to the empty string (which is bad anyway)
|
| 62 |
+
# could use `if (set -u; : $PYTHONHOME) ;` in bash
|
| 63 |
+
if [ -n "${PYTHONHOME:-}" ] ; then
|
| 64 |
+
_OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}"
|
| 65 |
+
unset PYTHONHOME
|
| 66 |
+
fi
|
| 67 |
+
|
| 68 |
+
if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then
|
| 69 |
+
_OLD_VIRTUAL_PS1="${PS1:-}"
|
| 70 |
+
PS1="("'(.threadweaver) '") ${PS1:-}"
|
| 71 |
+
export PS1
|
| 72 |
+
fi
|
| 73 |
+
|
| 74 |
+
# Call hash to forget past commands. Without forgetting
|
| 75 |
+
# past commands the $PATH changes we made may not be respected
|
| 76 |
+
hash -r 2> /dev/null
|
bin/deepspeed
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/storage/openpsi/users/zzy/.threadweaver/bin/python3
|
| 2 |
+
|
| 3 |
+
from deepspeed.launcher.runner import main
|
| 4 |
+
|
| 5 |
+
if __name__ == '__main__':
|
| 6 |
+
main()
|
bin/ds_report
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/storage/openpsi/users/zzy/.threadweaver/bin/python3
|
| 2 |
+
|
| 3 |
+
from deepspeed.env_report import cli_main
|
| 4 |
+
|
| 5 |
+
if __name__ == '__main__':
|
| 6 |
+
cli_main()
|
bin/fastapi
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/storage/openpsi/users/zzy/.threadweaver/bin/python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
import re
|
| 4 |
+
import sys
|
| 5 |
+
from fastapi.cli import main
|
| 6 |
+
if __name__ == '__main__':
|
| 7 |
+
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
| 8 |
+
sys.exit(main())
|
bin/huggingface-cli
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/storage/openpsi/users/zzy/.threadweaver/bin/python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
import re
|
| 4 |
+
import sys
|
| 5 |
+
from huggingface_hub.commands.huggingface_cli import main
|
| 6 |
+
if __name__ == '__main__':
|
| 7 |
+
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
| 8 |
+
sys.exit(main())
|
bin/ipython3
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/storage/openpsi/users/zzy/.threadweaver/bin/python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
import re
|
| 4 |
+
import sys
|
| 5 |
+
from IPython import start_ipython
|
| 6 |
+
if __name__ == '__main__':
|
| 7 |
+
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
| 8 |
+
sys.exit(start_ipython())
|
bin/isympy
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/storage/openpsi/users/zzy/.threadweaver/bin/python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
import re
|
| 4 |
+
import sys
|
| 5 |
+
from isympy import main
|
| 6 |
+
if __name__ == '__main__':
|
| 7 |
+
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
| 8 |
+
sys.exit(main())
|
bin/markdown_py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/storage/openpsi/users/zzy/.threadweaver/bin/python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
import re
|
| 4 |
+
import sys
|
| 5 |
+
from markdown.__main__ import run
|
| 6 |
+
if __name__ == '__main__':
|
| 7 |
+
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
| 8 |
+
sys.exit(run())
|
bin/pip3
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/storage/openpsi/users/zzy/.threadweaver/bin/python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
import re
|
| 4 |
+
import sys
|
| 5 |
+
from pip._internal.cli.main import main
|
| 6 |
+
if __name__ == '__main__':
|
| 7 |
+
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
| 8 |
+
sys.exit(main())
|
bin/pip3.12
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/storage/openpsi/users/zzy/.threadweaver/bin/python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
import re
|
| 4 |
+
import sys
|
| 5 |
+
from pip._internal.cli.main import main
|
| 6 |
+
if __name__ == '__main__':
|
| 7 |
+
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
| 8 |
+
sys.exit(main())
|
bin/proton
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/storage/openpsi/users/zzy/.threadweaver/bin/python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
import re
|
| 4 |
+
import sys
|
| 5 |
+
from triton.profiler.proton import main
|
| 6 |
+
if __name__ == '__main__':
|
| 7 |
+
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
| 8 |
+
sys.exit(main())
|
bin/pyftmerge
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/storage/openpsi/users/zzy/.threadweaver/bin/python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
import re
|
| 4 |
+
import sys
|
| 5 |
+
from fontTools.merge import main
|
| 6 |
+
if __name__ == '__main__':
|
| 7 |
+
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
| 8 |
+
sys.exit(main())
|
bin/ray
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/storage/openpsi/users/zzy/.threadweaver/bin/python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
import re
|
| 4 |
+
import sys
|
| 5 |
+
from ray.scripts.scripts import main
|
| 6 |
+
if __name__ == '__main__':
|
| 7 |
+
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
| 8 |
+
sys.exit(main())
|
bin/serve
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/storage/openpsi/users/zzy/.threadweaver/bin/python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
import re
|
| 4 |
+
import sys
|
| 5 |
+
from ray.serve.scripts import cli
|
| 6 |
+
if __name__ == '__main__':
|
| 7 |
+
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
| 8 |
+
sys.exit(cli())
|
bin/wandb
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/storage/openpsi/users/zzy/.threadweaver/bin/python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
import re
|
| 4 |
+
import sys
|
| 5 |
+
from wandb.cli.cli import cli
|
| 6 |
+
if __name__ == '__main__':
|
| 7 |
+
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
| 8 |
+
sys.exit(cli())
|
bin/wb
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/storage/openpsi/users/zzy/.threadweaver/bin/python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
import re
|
| 4 |
+
import sys
|
| 5 |
+
from wandb.cli.cli import cli
|
| 6 |
+
if __name__ == '__main__':
|
| 7 |
+
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
| 8 |
+
sys.exit(cli())
|
lib/python3.12/site-packages/decorator.py
ADDED
|
@@ -0,0 +1,459 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ######################### LICENSE ############################ #
|
| 2 |
+
|
| 3 |
+
# Copyright (c) 2005-2025, Michele Simionato
|
| 4 |
+
# All rights reserved.
|
| 5 |
+
|
| 6 |
+
# Redistribution and use in source and binary forms, with or without
|
| 7 |
+
# modification, are permitted provided that the following conditions are
|
| 8 |
+
# met:
|
| 9 |
+
|
| 10 |
+
# Redistributions of source code must retain the above copyright
|
| 11 |
+
# notice, this list of conditions and the following disclaimer.
|
| 12 |
+
# Redistributions in bytecode form must reproduce the above copyright
|
| 13 |
+
# notice, this list of conditions and the following disclaimer in
|
| 14 |
+
# the documentation and/or other materials provided with the
|
| 15 |
+
# distribution.
|
| 16 |
+
|
| 17 |
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
| 18 |
+
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
| 19 |
+
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
| 20 |
+
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
| 21 |
+
# HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
| 22 |
+
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
| 23 |
+
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
|
| 24 |
+
# OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
| 25 |
+
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
|
| 26 |
+
# TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
|
| 27 |
+
# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
|
| 28 |
+
# DAMAGE.
|
| 29 |
+
|
| 30 |
+
"""
|
| 31 |
+
Decorator module, see
|
| 32 |
+
https://github.com/micheles/decorator/blob/master/docs/documentation.md
|
| 33 |
+
for the documentation.
|
| 34 |
+
"""
|
| 35 |
+
import re
|
| 36 |
+
import sys
|
| 37 |
+
import inspect
|
| 38 |
+
import operator
|
| 39 |
+
import itertools
|
| 40 |
+
import functools
|
| 41 |
+
from contextlib import _GeneratorContextManager
|
| 42 |
+
from inspect import getfullargspec, iscoroutinefunction, isgeneratorfunction
|
| 43 |
+
|
| 44 |
+
__version__ = '5.2.1'
|
| 45 |
+
|
| 46 |
+
DEF = re.compile(r'\s*def\s*([_\w][_\w\d]*)\s*\(')
|
| 47 |
+
POS = inspect.Parameter.POSITIONAL_OR_KEYWORD
|
| 48 |
+
EMPTY = inspect.Parameter.empty
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
# this is not used anymore in the core, but kept for backward compatibility
|
| 52 |
+
class FunctionMaker(object):
|
| 53 |
+
"""
|
| 54 |
+
An object with the ability to create functions with a given signature.
|
| 55 |
+
It has attributes name, doc, module, signature, defaults, dict and
|
| 56 |
+
methods update and make.
|
| 57 |
+
"""
|
| 58 |
+
|
| 59 |
+
# Atomic get-and-increment provided by the GIL
|
| 60 |
+
_compile_count = itertools.count()
|
| 61 |
+
|
| 62 |
+
# make pylint happy
|
| 63 |
+
args = varargs = varkw = defaults = kwonlyargs = kwonlydefaults = ()
|
| 64 |
+
|
| 65 |
+
def __init__(self, func=None, name=None, signature=None,
|
| 66 |
+
defaults=None, doc=None, module=None, funcdict=None):
|
| 67 |
+
self.shortsignature = signature
|
| 68 |
+
if func:
|
| 69 |
+
# func can be a class or a callable, but not an instance method
|
| 70 |
+
self.name = func.__name__
|
| 71 |
+
if self.name == '<lambda>': # small hack for lambda functions
|
| 72 |
+
self.name = '_lambda_'
|
| 73 |
+
self.doc = func.__doc__
|
| 74 |
+
self.module = func.__module__
|
| 75 |
+
if inspect.isroutine(func) or isinstance(func, functools.partial):
|
| 76 |
+
argspec = getfullargspec(func)
|
| 77 |
+
self.annotations = getattr(func, '__annotations__', {})
|
| 78 |
+
for a in ('args', 'varargs', 'varkw', 'defaults', 'kwonlyargs',
|
| 79 |
+
'kwonlydefaults'):
|
| 80 |
+
setattr(self, a, getattr(argspec, a))
|
| 81 |
+
for i, arg in enumerate(self.args):
|
| 82 |
+
setattr(self, 'arg%d' % i, arg)
|
| 83 |
+
allargs = list(self.args)
|
| 84 |
+
allshortargs = list(self.args)
|
| 85 |
+
if self.varargs:
|
| 86 |
+
allargs.append('*' + self.varargs)
|
| 87 |
+
allshortargs.append('*' + self.varargs)
|
| 88 |
+
elif self.kwonlyargs:
|
| 89 |
+
allargs.append('*') # single star syntax
|
| 90 |
+
for a in self.kwonlyargs:
|
| 91 |
+
allargs.append('%s=None' % a)
|
| 92 |
+
allshortargs.append('%s=%s' % (a, a))
|
| 93 |
+
if self.varkw:
|
| 94 |
+
allargs.append('**' + self.varkw)
|
| 95 |
+
allshortargs.append('**' + self.varkw)
|
| 96 |
+
self.signature = ', '.join(allargs)
|
| 97 |
+
self.shortsignature = ', '.join(allshortargs)
|
| 98 |
+
self.dict = func.__dict__.copy()
|
| 99 |
+
# func=None happens when decorating a caller
|
| 100 |
+
if name:
|
| 101 |
+
self.name = name
|
| 102 |
+
if signature is not None:
|
| 103 |
+
self.signature = signature
|
| 104 |
+
if defaults:
|
| 105 |
+
self.defaults = defaults
|
| 106 |
+
if doc:
|
| 107 |
+
self.doc = doc
|
| 108 |
+
if module:
|
| 109 |
+
self.module = module
|
| 110 |
+
if funcdict:
|
| 111 |
+
self.dict = funcdict
|
| 112 |
+
# check existence required attributes
|
| 113 |
+
assert hasattr(self, 'name')
|
| 114 |
+
if not hasattr(self, 'signature'):
|
| 115 |
+
raise TypeError('You are decorating a non function: %s' % func)
|
| 116 |
+
|
| 117 |
+
def update(self, func, **kw):
|
| 118 |
+
"""
|
| 119 |
+
Update the signature of func with the data in self
|
| 120 |
+
"""
|
| 121 |
+
func.__name__ = self.name
|
| 122 |
+
func.__doc__ = getattr(self, 'doc', None)
|
| 123 |
+
func.__dict__ = getattr(self, 'dict', {})
|
| 124 |
+
func.__defaults__ = self.defaults
|
| 125 |
+
func.__kwdefaults__ = self.kwonlydefaults or None
|
| 126 |
+
func.__annotations__ = getattr(self, 'annotations', None)
|
| 127 |
+
try:
|
| 128 |
+
frame = sys._getframe(3)
|
| 129 |
+
except AttributeError: # for IronPython and similar implementations
|
| 130 |
+
callermodule = '?'
|
| 131 |
+
else:
|
| 132 |
+
callermodule = frame.f_globals.get('__name__', '?')
|
| 133 |
+
func.__module__ = getattr(self, 'module', callermodule)
|
| 134 |
+
func.__dict__.update(kw)
|
| 135 |
+
|
| 136 |
+
def make(self, src_templ, evaldict=None, addsource=False, **attrs):
|
| 137 |
+
"""
|
| 138 |
+
Make a new function from a given template and update the signature
|
| 139 |
+
"""
|
| 140 |
+
src = src_templ % vars(self) # expand name and signature
|
| 141 |
+
evaldict = evaldict or {}
|
| 142 |
+
mo = DEF.search(src)
|
| 143 |
+
if mo is None:
|
| 144 |
+
raise SyntaxError('not a valid function template\n%s' % src)
|
| 145 |
+
name = mo.group(1) # extract the function name
|
| 146 |
+
names = set([name] + [arg.strip(' *') for arg in
|
| 147 |
+
self.shortsignature.split(',')])
|
| 148 |
+
for n in names:
|
| 149 |
+
if n in ('_func_', '_call_'):
|
| 150 |
+
raise NameError('%s is overridden in\n%s' % (n, src))
|
| 151 |
+
|
| 152 |
+
if not src.endswith('\n'): # add a newline for old Pythons
|
| 153 |
+
src += '\n'
|
| 154 |
+
|
| 155 |
+
# Ensure each generated function has a unique filename for profilers
|
| 156 |
+
# (such as cProfile) that depend on the tuple of (<filename>,
|
| 157 |
+
# <definition line>, <function name>) being unique.
|
| 158 |
+
filename = '<decorator-gen-%d>' % next(self._compile_count)
|
| 159 |
+
try:
|
| 160 |
+
code = compile(src, filename, 'single')
|
| 161 |
+
exec(code, evaldict)
|
| 162 |
+
except Exception:
|
| 163 |
+
print('Error in generated code:', file=sys.stderr)
|
| 164 |
+
print(src, file=sys.stderr)
|
| 165 |
+
raise
|
| 166 |
+
func = evaldict[name]
|
| 167 |
+
if addsource:
|
| 168 |
+
attrs['__source__'] = src
|
| 169 |
+
self.update(func, **attrs)
|
| 170 |
+
return func
|
| 171 |
+
|
| 172 |
+
@classmethod
|
| 173 |
+
def create(cls, obj, body, evaldict, defaults=None,
|
| 174 |
+
doc=None, module=None, addsource=True, **attrs):
|
| 175 |
+
"""
|
| 176 |
+
Create a function from the strings name, signature and body.
|
| 177 |
+
evaldict is the evaluation dictionary. If addsource is true an
|
| 178 |
+
attribute __source__ is added to the result. The attributes attrs
|
| 179 |
+
are added, if any.
|
| 180 |
+
"""
|
| 181 |
+
if isinstance(obj, str): # "name(signature)"
|
| 182 |
+
name, rest = obj.strip().split('(', 1)
|
| 183 |
+
signature = rest[:-1] # strip a right parens
|
| 184 |
+
func = None
|
| 185 |
+
else: # a function
|
| 186 |
+
name = None
|
| 187 |
+
signature = None
|
| 188 |
+
func = obj
|
| 189 |
+
self = cls(func, name, signature, defaults, doc, module)
|
| 190 |
+
ibody = '\n'.join(' ' + line for line in body.splitlines())
|
| 191 |
+
caller = evaldict.get('_call_') # when called from `decorate`
|
| 192 |
+
if caller and iscoroutinefunction(caller):
|
| 193 |
+
body = ('async def %(name)s(%(signature)s):\n' + ibody).replace(
|
| 194 |
+
'return', 'return await')
|
| 195 |
+
else:
|
| 196 |
+
body = 'def %(name)s(%(signature)s):\n' + ibody
|
| 197 |
+
return self.make(body, evaldict, addsource, **attrs)
|
| 198 |
+
|
| 199 |
+
|
| 200 |
+
def fix(args, kwargs, sig):
|
| 201 |
+
"""
|
| 202 |
+
Fix args and kwargs to be consistent with the signature
|
| 203 |
+
"""
|
| 204 |
+
ba = sig.bind(*args, **kwargs)
|
| 205 |
+
ba.apply_defaults() # needed for test_dan_schult
|
| 206 |
+
return ba.args, ba.kwargs
|
| 207 |
+
|
| 208 |
+
|
| 209 |
+
def decorate(func, caller, extras=(), kwsyntax=False):
|
| 210 |
+
"""
|
| 211 |
+
Decorates a function/generator/coroutine using a caller.
|
| 212 |
+
If kwsyntax is True calling the decorated functions with keyword
|
| 213 |
+
syntax will pass the named arguments inside the ``kw`` dictionary,
|
| 214 |
+
even if such argument are positional, similarly to what functools.wraps
|
| 215 |
+
does. By default kwsyntax is False and the the arguments are untouched.
|
| 216 |
+
"""
|
| 217 |
+
sig = inspect.signature(func)
|
| 218 |
+
if isinstance(func, functools.partial):
|
| 219 |
+
func = functools.update_wrapper(func, func.func)
|
| 220 |
+
if iscoroutinefunction(caller):
|
| 221 |
+
async def fun(*args, **kw):
|
| 222 |
+
if not kwsyntax:
|
| 223 |
+
args, kw = fix(args, kw, sig)
|
| 224 |
+
return await caller(func, *(extras + args), **kw)
|
| 225 |
+
elif isgeneratorfunction(caller):
|
| 226 |
+
def fun(*args, **kw):
|
| 227 |
+
if not kwsyntax:
|
| 228 |
+
args, kw = fix(args, kw, sig)
|
| 229 |
+
for res in caller(func, *(extras + args), **kw):
|
| 230 |
+
yield res
|
| 231 |
+
else:
|
| 232 |
+
def fun(*args, **kw):
|
| 233 |
+
if not kwsyntax:
|
| 234 |
+
args, kw = fix(args, kw, sig)
|
| 235 |
+
return caller(func, *(extras + args), **kw)
|
| 236 |
+
|
| 237 |
+
fun.__name__ = func.__name__
|
| 238 |
+
fun.__doc__ = func.__doc__
|
| 239 |
+
fun.__wrapped__ = func
|
| 240 |
+
fun.__signature__ = sig
|
| 241 |
+
fun.__qualname__ = func.__qualname__
|
| 242 |
+
# builtin functions like defaultdict.__setitem__ lack many attributes
|
| 243 |
+
try:
|
| 244 |
+
fun.__defaults__ = func.__defaults__
|
| 245 |
+
except AttributeError:
|
| 246 |
+
pass
|
| 247 |
+
try:
|
| 248 |
+
fun.__kwdefaults__ = func.__kwdefaults__
|
| 249 |
+
except AttributeError:
|
| 250 |
+
pass
|
| 251 |
+
try:
|
| 252 |
+
fun.__annotations__ = func.__annotations__
|
| 253 |
+
except AttributeError:
|
| 254 |
+
pass
|
| 255 |
+
try:
|
| 256 |
+
fun.__module__ = func.__module__
|
| 257 |
+
except AttributeError:
|
| 258 |
+
pass
|
| 259 |
+
try:
|
| 260 |
+
fun.__name__ = func.__name__
|
| 261 |
+
except AttributeError: # happens with old versions of numpy.vectorize
|
| 262 |
+
func.__name__ == 'noname'
|
| 263 |
+
try:
|
| 264 |
+
fun.__dict__.update(func.__dict__)
|
| 265 |
+
except AttributeError:
|
| 266 |
+
pass
|
| 267 |
+
return fun
|
| 268 |
+
|
| 269 |
+
|
| 270 |
+
def decoratorx(caller):
|
| 271 |
+
"""
|
| 272 |
+
A version of "decorator" implemented via "exec" and not via the
|
| 273 |
+
Signature object. Use this if you are want to preserve the `.__code__`
|
| 274 |
+
object properties (https://github.com/micheles/decorator/issues/129).
|
| 275 |
+
"""
|
| 276 |
+
def dec(func):
|
| 277 |
+
return FunctionMaker.create(
|
| 278 |
+
func,
|
| 279 |
+
"return _call_(_func_, %(shortsignature)s)",
|
| 280 |
+
dict(_call_=caller, _func_=func),
|
| 281 |
+
__wrapped__=func, __qualname__=func.__qualname__)
|
| 282 |
+
return dec
|
| 283 |
+
|
| 284 |
+
|
| 285 |
+
def decorator(caller, _func=None, kwsyntax=False):
|
| 286 |
+
"""
|
| 287 |
+
decorator(caller) converts a caller function into a decorator
|
| 288 |
+
"""
|
| 289 |
+
if _func is not None: # return a decorated function
|
| 290 |
+
# this is obsolete behavior; you should use decorate instead
|
| 291 |
+
return decorate(_func, caller, (), kwsyntax)
|
| 292 |
+
# else return a decorator function
|
| 293 |
+
sig = inspect.signature(caller)
|
| 294 |
+
dec_params = [p for p in sig.parameters.values() if p.kind is POS]
|
| 295 |
+
|
| 296 |
+
def dec(func=None, *args, **kw):
|
| 297 |
+
na = len(args) + 1
|
| 298 |
+
extras = args + tuple(kw.get(p.name, p.default)
|
| 299 |
+
for p in dec_params[na:]
|
| 300 |
+
if p.default is not EMPTY)
|
| 301 |
+
if func is None:
|
| 302 |
+
return lambda func: decorate(func, caller, extras, kwsyntax)
|
| 303 |
+
else:
|
| 304 |
+
return decorate(func, caller, extras, kwsyntax)
|
| 305 |
+
dec.__signature__ = sig.replace(parameters=dec_params)
|
| 306 |
+
dec.__name__ = caller.__name__
|
| 307 |
+
dec.__doc__ = caller.__doc__
|
| 308 |
+
dec.__wrapped__ = caller
|
| 309 |
+
dec.__qualname__ = caller.__qualname__
|
| 310 |
+
dec.__kwdefaults__ = getattr(caller, '__kwdefaults__', None)
|
| 311 |
+
dec.__dict__.update(caller.__dict__)
|
| 312 |
+
return dec
|
| 313 |
+
|
| 314 |
+
|
| 315 |
+
# ####################### contextmanager ####################### #
|
| 316 |
+
|
| 317 |
+
|
| 318 |
+
class ContextManager(_GeneratorContextManager):
|
| 319 |
+
def __init__(self, g, *a, **k):
|
| 320 |
+
_GeneratorContextManager.__init__(self, g, a, k)
|
| 321 |
+
|
| 322 |
+
def __call__(self, func):
|
| 323 |
+
def caller(f, *a, **k):
|
| 324 |
+
with self.__class__(self.func, *self.args, **self.kwds):
|
| 325 |
+
return f(*a, **k)
|
| 326 |
+
return decorate(func, caller)
|
| 327 |
+
|
| 328 |
+
|
| 329 |
+
_contextmanager = decorator(ContextManager)
|
| 330 |
+
|
| 331 |
+
|
| 332 |
+
def contextmanager(func):
|
| 333 |
+
# Enable Pylint config: contextmanager-decorators=decorator.contextmanager
|
| 334 |
+
return _contextmanager(func)
|
| 335 |
+
|
| 336 |
+
|
| 337 |
+
# ############################ dispatch_on ############################ #
|
| 338 |
+
|
| 339 |
+
def append(a, vancestors):
|
| 340 |
+
"""
|
| 341 |
+
Append ``a`` to the list of the virtual ancestors, unless it is already
|
| 342 |
+
included.
|
| 343 |
+
"""
|
| 344 |
+
add = True
|
| 345 |
+
for j, va in enumerate(vancestors):
|
| 346 |
+
if issubclass(va, a):
|
| 347 |
+
add = False
|
| 348 |
+
break
|
| 349 |
+
if issubclass(a, va):
|
| 350 |
+
vancestors[j] = a
|
| 351 |
+
add = False
|
| 352 |
+
if add:
|
| 353 |
+
vancestors.append(a)
|
| 354 |
+
|
| 355 |
+
|
| 356 |
+
# inspired from simplegeneric by P.J. Eby and functools.singledispatch
|
| 357 |
+
def dispatch_on(*dispatch_args):
|
| 358 |
+
"""
|
| 359 |
+
Factory of decorators turning a function into a generic function
|
| 360 |
+
dispatching on the given arguments.
|
| 361 |
+
"""
|
| 362 |
+
assert dispatch_args, 'No dispatch args passed'
|
| 363 |
+
dispatch_str = '(%s,)' % ', '.join(dispatch_args)
|
| 364 |
+
|
| 365 |
+
def check(arguments, wrong=operator.ne, msg=''):
|
| 366 |
+
"""Make sure one passes the expected number of arguments"""
|
| 367 |
+
if wrong(len(arguments), len(dispatch_args)):
|
| 368 |
+
raise TypeError('Expected %d arguments, got %d%s' %
|
| 369 |
+
(len(dispatch_args), len(arguments), msg))
|
| 370 |
+
|
| 371 |
+
def gen_func_dec(func):
|
| 372 |
+
"""Decorator turning a function into a generic function"""
|
| 373 |
+
|
| 374 |
+
# first check the dispatch arguments
|
| 375 |
+
argset = set(getfullargspec(func).args)
|
| 376 |
+
if not set(dispatch_args) <= argset:
|
| 377 |
+
raise NameError('Unknown dispatch arguments %s' % dispatch_str)
|
| 378 |
+
|
| 379 |
+
typemap = {}
|
| 380 |
+
|
| 381 |
+
def vancestors(*types):
|
| 382 |
+
"""
|
| 383 |
+
Get a list of sets of virtual ancestors for the given types
|
| 384 |
+
"""
|
| 385 |
+
check(types)
|
| 386 |
+
ras = [[] for _ in range(len(dispatch_args))]
|
| 387 |
+
for types_ in typemap:
|
| 388 |
+
for t, type_, ra in zip(types, types_, ras):
|
| 389 |
+
if issubclass(t, type_) and type_ not in t.mro():
|
| 390 |
+
append(type_, ra)
|
| 391 |
+
return [set(ra) for ra in ras]
|
| 392 |
+
|
| 393 |
+
def ancestors(*types):
|
| 394 |
+
"""
|
| 395 |
+
Get a list of virtual MROs, one for each type
|
| 396 |
+
"""
|
| 397 |
+
check(types)
|
| 398 |
+
lists = []
|
| 399 |
+
for t, vas in zip(types, vancestors(*types)):
|
| 400 |
+
n_vas = len(vas)
|
| 401 |
+
if n_vas > 1:
|
| 402 |
+
raise RuntimeError(
|
| 403 |
+
'Ambiguous dispatch for %s: %s' % (t, vas))
|
| 404 |
+
elif n_vas == 1:
|
| 405 |
+
va, = vas
|
| 406 |
+
mro = type('t', (t, va), {}).mro()[1:]
|
| 407 |
+
else:
|
| 408 |
+
mro = t.mro()
|
| 409 |
+
lists.append(mro[:-1]) # discard t and object
|
| 410 |
+
return lists
|
| 411 |
+
|
| 412 |
+
def register(*types):
|
| 413 |
+
"""
|
| 414 |
+
Decorator to register an implementation for the given types
|
| 415 |
+
"""
|
| 416 |
+
check(types)
|
| 417 |
+
|
| 418 |
+
def dec(f):
|
| 419 |
+
check(getfullargspec(f).args, operator.lt, ' in ' + f.__name__)
|
| 420 |
+
typemap[types] = f
|
| 421 |
+
return f
|
| 422 |
+
return dec
|
| 423 |
+
|
| 424 |
+
def dispatch_info(*types):
|
| 425 |
+
"""
|
| 426 |
+
An utility to introspect the dispatch algorithm
|
| 427 |
+
"""
|
| 428 |
+
check(types)
|
| 429 |
+
lst = []
|
| 430 |
+
for ancs in itertools.product(*ancestors(*types)):
|
| 431 |
+
lst.append(tuple(a.__name__ for a in ancs))
|
| 432 |
+
return lst
|
| 433 |
+
|
| 434 |
+
def _dispatch(dispatch_args, *args, **kw):
|
| 435 |
+
types = tuple(type(arg) for arg in dispatch_args)
|
| 436 |
+
try: # fast path
|
| 437 |
+
f = typemap[types]
|
| 438 |
+
except KeyError:
|
| 439 |
+
pass
|
| 440 |
+
else:
|
| 441 |
+
return f(*args, **kw)
|
| 442 |
+
combinations = itertools.product(*ancestors(*types))
|
| 443 |
+
next(combinations) # the first one has been already tried
|
| 444 |
+
for types_ in combinations:
|
| 445 |
+
f = typemap.get(types_)
|
| 446 |
+
if f is not None:
|
| 447 |
+
return f(*args, **kw)
|
| 448 |
+
|
| 449 |
+
# else call the default implementation
|
| 450 |
+
return func(*args, **kw)
|
| 451 |
+
|
| 452 |
+
return FunctionMaker.create(
|
| 453 |
+
func, 'return _f_(%s, %%(shortsignature)s)' % dispatch_str,
|
| 454 |
+
dict(_f_=_dispatch), register=register, default=func,
|
| 455 |
+
typemap=typemap, vancestors=vancestors, ancestors=ancestors,
|
| 456 |
+
dispatch_info=dispatch_info, __wrapped__=func)
|
| 457 |
+
|
| 458 |
+
gen_func_dec.__name__ = 'dispatch_on' + dispatch_str
|
| 459 |
+
return gen_func_dec
|
lib/python3.12/site-packages/ipython_pygments_lexers.py
ADDED
|
@@ -0,0 +1,582 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""
|
| 3 |
+
Defines a variety of Pygments lexers for highlighting IPython code.
|
| 4 |
+
|
| 5 |
+
This includes:
|
| 6 |
+
|
| 7 |
+
IPythonLexer, IPython3Lexer
|
| 8 |
+
Lexers for pure IPython (python + magic/shell commands)
|
| 9 |
+
|
| 10 |
+
IPythonPartialTracebackLexer, IPythonTracebackLexer
|
| 11 |
+
Supports 2.x and 3.x via keyword `python3`. The partial traceback
|
| 12 |
+
lexer reads everything but the Python code appearing in a traceback.
|
| 13 |
+
The full lexer combines the partial lexer with an IPython lexer.
|
| 14 |
+
|
| 15 |
+
IPythonConsoleLexer
|
| 16 |
+
A lexer for IPython console sessions, with support for tracebacks.
|
| 17 |
+
|
| 18 |
+
IPyLexer
|
| 19 |
+
A friendly lexer which examines the first line of text and from it,
|
| 20 |
+
decides whether to use an IPython lexer or an IPython console lexer.
|
| 21 |
+
This is probably the only lexer that needs to be explicitly added
|
| 22 |
+
to Pygments.
|
| 23 |
+
|
| 24 |
+
"""
|
| 25 |
+
# -----------------------------------------------------------------------------
|
| 26 |
+
# Copyright (c) 2013, the IPython Development Team.
|
| 27 |
+
#
|
| 28 |
+
# Distributed under the terms of the Modified BSD License.
|
| 29 |
+
#
|
| 30 |
+
# The full license is in the file COPYING.txt, distributed with this software.
|
| 31 |
+
# -----------------------------------------------------------------------------
|
| 32 |
+
|
| 33 |
+
__version__ = "1.1.1"
|
| 34 |
+
|
| 35 |
+
# Standard library
|
| 36 |
+
import re
|
| 37 |
+
|
| 38 |
+
# Third party
|
| 39 |
+
from pygments.lexers import (
|
| 40 |
+
BashLexer,
|
| 41 |
+
HtmlLexer,
|
| 42 |
+
JavascriptLexer,
|
| 43 |
+
RubyLexer,
|
| 44 |
+
PerlLexer,
|
| 45 |
+
Python2Lexer,
|
| 46 |
+
Python3Lexer,
|
| 47 |
+
TexLexer,
|
| 48 |
+
)
|
| 49 |
+
from pygments.lexer import (
|
| 50 |
+
Lexer,
|
| 51 |
+
DelegatingLexer,
|
| 52 |
+
RegexLexer,
|
| 53 |
+
do_insertions,
|
| 54 |
+
bygroups,
|
| 55 |
+
using,
|
| 56 |
+
)
|
| 57 |
+
from pygments.token import (
|
| 58 |
+
Generic,
|
| 59 |
+
Keyword,
|
| 60 |
+
Literal,
|
| 61 |
+
Name,
|
| 62 |
+
Operator,
|
| 63 |
+
Other,
|
| 64 |
+
Text,
|
| 65 |
+
Error,
|
| 66 |
+
)
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
line_re = re.compile(".*?\n")
|
| 70 |
+
|
| 71 |
+
__all__ = [
|
| 72 |
+
"IPython3Lexer",
|
| 73 |
+
"IPythonLexer",
|
| 74 |
+
"IPythonPartialTracebackLexer",
|
| 75 |
+
"IPythonTracebackLexer",
|
| 76 |
+
"IPythonConsoleLexer",
|
| 77 |
+
"IPyLexer",
|
| 78 |
+
]
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
ipython_tokens = [
|
| 82 |
+
(
|
| 83 |
+
r"(?s)(\s*)(%%capture)([^\n]*\n)(.*)",
|
| 84 |
+
bygroups(Text, Operator, Text, using(Python3Lexer)),
|
| 85 |
+
),
|
| 86 |
+
(
|
| 87 |
+
r"(?s)(\s*)(%%debug)([^\n]*\n)(.*)",
|
| 88 |
+
bygroups(Text, Operator, Text, using(Python3Lexer)),
|
| 89 |
+
),
|
| 90 |
+
(
|
| 91 |
+
r"(?is)(\s*)(%%html)([^\n]*\n)(.*)",
|
| 92 |
+
bygroups(Text, Operator, Text, using(HtmlLexer)),
|
| 93 |
+
),
|
| 94 |
+
(
|
| 95 |
+
r"(?s)(\s*)(%%javascript)([^\n]*\n)(.*)",
|
| 96 |
+
bygroups(Text, Operator, Text, using(JavascriptLexer)),
|
| 97 |
+
),
|
| 98 |
+
(
|
| 99 |
+
r"(?s)(\s*)(%%js)([^\n]*\n)(.*)",
|
| 100 |
+
bygroups(Text, Operator, Text, using(JavascriptLexer)),
|
| 101 |
+
),
|
| 102 |
+
(
|
| 103 |
+
r"(?s)(\s*)(%%latex)([^\n]*\n)(.*)",
|
| 104 |
+
bygroups(Text, Operator, Text, using(TexLexer)),
|
| 105 |
+
),
|
| 106 |
+
(
|
| 107 |
+
r"(?s)(\s*)(%%perl)([^\n]*\n)(.*)",
|
| 108 |
+
bygroups(Text, Operator, Text, using(PerlLexer)),
|
| 109 |
+
),
|
| 110 |
+
(
|
| 111 |
+
r"(?s)(\s*)(%%prun)([^\n]*\n)(.*)",
|
| 112 |
+
bygroups(Text, Operator, Text, using(Python3Lexer)),
|
| 113 |
+
),
|
| 114 |
+
(
|
| 115 |
+
r"(?s)(\s*)(%%pypy)([^\n]*\n)(.*)",
|
| 116 |
+
bygroups(Text, Operator, Text, using(Python3Lexer)),
|
| 117 |
+
),
|
| 118 |
+
(
|
| 119 |
+
r"(?s)(\s*)(%%python2)([^\n]*\n)(.*)",
|
| 120 |
+
bygroups(Text, Operator, Text, using(Python2Lexer)),
|
| 121 |
+
),
|
| 122 |
+
(
|
| 123 |
+
r"(?s)(\s*)(%%python3)([^\n]*\n)(.*)",
|
| 124 |
+
bygroups(Text, Operator, Text, using(Python3Lexer)),
|
| 125 |
+
),
|
| 126 |
+
(
|
| 127 |
+
r"(?s)(\s*)(%%python)([^\n]*\n)(.*)",
|
| 128 |
+
bygroups(Text, Operator, Text, using(Python3Lexer)),
|
| 129 |
+
),
|
| 130 |
+
(
|
| 131 |
+
r"(?s)(\s*)(%%ruby)([^\n]*\n)(.*)",
|
| 132 |
+
bygroups(Text, Operator, Text, using(RubyLexer)),
|
| 133 |
+
),
|
| 134 |
+
(
|
| 135 |
+
r"(?s)(\s*)(%%timeit)([^\n]*\n)(.*)",
|
| 136 |
+
bygroups(Text, Operator, Text, using(Python3Lexer)),
|
| 137 |
+
),
|
| 138 |
+
(
|
| 139 |
+
r"(?s)(\s*)(%%time)([^\n]*\n)(.*)",
|
| 140 |
+
bygroups(Text, Operator, Text, using(Python3Lexer)),
|
| 141 |
+
),
|
| 142 |
+
(
|
| 143 |
+
r"(?s)(\s*)(%%writefile)([^\n]*\n)(.*)",
|
| 144 |
+
bygroups(Text, Operator, Text, using(Python3Lexer)),
|
| 145 |
+
),
|
| 146 |
+
(
|
| 147 |
+
r"(?s)(\s*)(%%file)([^\n]*\n)(.*)",
|
| 148 |
+
bygroups(Text, Operator, Text, using(Python3Lexer)),
|
| 149 |
+
),
|
| 150 |
+
(r"(?s)(\s*)(%%)(\w+)(.*)", bygroups(Text, Operator, Keyword, Text)),
|
| 151 |
+
(
|
| 152 |
+
r"(?s)(^\s*)(%%!)([^\n]*\n)(.*)",
|
| 153 |
+
bygroups(Text, Operator, Text, using(BashLexer)),
|
| 154 |
+
),
|
| 155 |
+
(r"(%%?)(\w+)(\?\??)$", bygroups(Operator, Keyword, Operator)),
|
| 156 |
+
(r"\b(\?\??)(\s*)$", bygroups(Operator, Text)),
|
| 157 |
+
(r"(%)(sx|sc|system)(.*)(\n)", bygroups(Operator, Keyword, using(BashLexer), Text)),
|
| 158 |
+
(r"(%)(\w+)(.*\n)", bygroups(Operator, Keyword, Text)),
|
| 159 |
+
(r"^(!!)(.+)(\n)", bygroups(Operator, using(BashLexer), Text)),
|
| 160 |
+
(r"(!)(?!=)(.+)(\n)", bygroups(Operator, using(BashLexer), Text)),
|
| 161 |
+
(r"^(\s*)(\?\??)(\s*%{0,2}[\w\.\*]*)", bygroups(Text, Operator, Text)),
|
| 162 |
+
(r"(\s*%{0,2}[\w\.\*]*)(\?\??)(\s*)$", bygroups(Text, Operator, Text)),
|
| 163 |
+
]
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
class IPython3Lexer(Python3Lexer):
|
| 167 |
+
"""IPython code lexer (based on Python 3)"""
|
| 168 |
+
|
| 169 |
+
name = "IPython"
|
| 170 |
+
aliases = ["ipython", "ipython3"]
|
| 171 |
+
|
| 172 |
+
tokens = Python3Lexer.tokens.copy()
|
| 173 |
+
tokens["root"] = ipython_tokens + tokens["root"]
|
| 174 |
+
|
| 175 |
+
|
| 176 |
+
IPythonLexer = IPython3Lexer
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
class IPythonPartialTracebackLexer(RegexLexer):
|
| 180 |
+
"""
|
| 181 |
+
Partial lexer for IPython tracebacks.
|
| 182 |
+
|
| 183 |
+
Handles all the non-python output.
|
| 184 |
+
|
| 185 |
+
"""
|
| 186 |
+
|
| 187 |
+
name = "IPython Partial Traceback"
|
| 188 |
+
|
| 189 |
+
tokens = {
|
| 190 |
+
"root": [
|
| 191 |
+
# Tracebacks for syntax errors have a different style.
|
| 192 |
+
# For both types of tracebacks, we mark the first line with
|
| 193 |
+
# Generic.Traceback. For syntax errors, we mark the filename
|
| 194 |
+
# as we mark the filenames for non-syntax tracebacks.
|
| 195 |
+
#
|
| 196 |
+
# These two regexps define how IPythonConsoleLexer finds a
|
| 197 |
+
# traceback.
|
| 198 |
+
#
|
| 199 |
+
## Non-syntax traceback
|
| 200 |
+
(r"^(\^C)?(-+\n)", bygroups(Error, Generic.Traceback)),
|
| 201 |
+
## Syntax traceback
|
| 202 |
+
(
|
| 203 |
+
r"^( File)(.*)(, line )(\d+\n)",
|
| 204 |
+
bygroups(
|
| 205 |
+
Generic.Traceback,
|
| 206 |
+
Name.Namespace,
|
| 207 |
+
Generic.Traceback,
|
| 208 |
+
Literal.Number.Integer,
|
| 209 |
+
),
|
| 210 |
+
),
|
| 211 |
+
# (Exception Identifier)(Whitespace)(Traceback Message)
|
| 212 |
+
(
|
| 213 |
+
r"(?u)(^[^\d\W]\w*)(\s*)(Traceback.*?\n)",
|
| 214 |
+
bygroups(Name.Exception, Generic.Whitespace, Text),
|
| 215 |
+
),
|
| 216 |
+
# (Module/Filename)(Text)(Callee)(Function Signature)
|
| 217 |
+
# Better options for callee and function signature?
|
| 218 |
+
(
|
| 219 |
+
r"(.*)( in )(.*)(\(.*\)\n)",
|
| 220 |
+
bygroups(Name.Namespace, Text, Name.Entity, Name.Tag),
|
| 221 |
+
),
|
| 222 |
+
# Regular line: (Whitespace)(Line Number)(Python Code)
|
| 223 |
+
(
|
| 224 |
+
r"(\s*?)(\d+)(.*?\n)",
|
| 225 |
+
bygroups(Generic.Whitespace, Literal.Number.Integer, Other),
|
| 226 |
+
),
|
| 227 |
+
# Emphasized line: (Arrow)(Line Number)(Python Code)
|
| 228 |
+
# Using Exception token so arrow color matches the Exception.
|
| 229 |
+
(
|
| 230 |
+
r"(-*>?\s?)(\d+)(.*?\n)",
|
| 231 |
+
bygroups(Name.Exception, Literal.Number.Integer, Other),
|
| 232 |
+
),
|
| 233 |
+
# (Exception Identifier)(Message)
|
| 234 |
+
(r"(?u)(^[^\d\W]\w*)(:.*?\n)", bygroups(Name.Exception, Text)),
|
| 235 |
+
# Tag everything else as Other, will be handled later.
|
| 236 |
+
(r".*\n", Other),
|
| 237 |
+
],
|
| 238 |
+
}
|
| 239 |
+
|
| 240 |
+
|
| 241 |
+
class IPythonTracebackLexer(DelegatingLexer):
|
| 242 |
+
"""
|
| 243 |
+
IPython traceback lexer.
|
| 244 |
+
|
| 245 |
+
For doctests, the tracebacks can be snipped as much as desired with the
|
| 246 |
+
exception to the lines that designate a traceback. For non-syntax error
|
| 247 |
+
tracebacks, this is the line of hyphens. For syntax error tracebacks,
|
| 248 |
+
this is the line which lists the File and line number.
|
| 249 |
+
|
| 250 |
+
"""
|
| 251 |
+
|
| 252 |
+
# The lexer inherits from DelegatingLexer. The "root" lexer is an
|
| 253 |
+
# appropriate IPython lexer, which depends on the value of the boolean
|
| 254 |
+
# `python3`. First, we parse with the partial IPython traceback lexer.
|
| 255 |
+
# Then, any code marked with the "Other" token is delegated to the root
|
| 256 |
+
# lexer.
|
| 257 |
+
#
|
| 258 |
+
name = "IPython Traceback"
|
| 259 |
+
aliases = ["ipythontb", "ipython3tb"]
|
| 260 |
+
|
| 261 |
+
def __init__(self, **options):
|
| 262 |
+
"""
|
| 263 |
+
A subclass of `DelegatingLexer` which delegates to the appropriate to either IPyLexer,
|
| 264 |
+
IPythonPartialTracebackLexer.
|
| 265 |
+
"""
|
| 266 |
+
# note we need a __init__ doc, as otherwise it inherits the doc from the super class
|
| 267 |
+
# which will fail the documentation build as it references section of the pygments docs that
|
| 268 |
+
# do not exists when building IPython's docs.
|
| 269 |
+
DelegatingLexer.__init__(
|
| 270 |
+
self, IPython3Lexer, IPythonPartialTracebackLexer, **options
|
| 271 |
+
)
|
| 272 |
+
|
| 273 |
+
|
| 274 |
+
class IPythonConsoleLexer(Lexer):
|
| 275 |
+
"""
|
| 276 |
+
An IPython console lexer for IPython code-blocks and doctests, such as:
|
| 277 |
+
|
| 278 |
+
.. code-block:: rst
|
| 279 |
+
|
| 280 |
+
.. code-block:: ipythonconsole
|
| 281 |
+
|
| 282 |
+
In [1]: a = 'foo'
|
| 283 |
+
|
| 284 |
+
In [2]: a
|
| 285 |
+
Out[2]: 'foo'
|
| 286 |
+
|
| 287 |
+
In [3]: print(a)
|
| 288 |
+
foo
|
| 289 |
+
|
| 290 |
+
|
| 291 |
+
Support is also provided for IPython exceptions:
|
| 292 |
+
|
| 293 |
+
.. code-block:: rst
|
| 294 |
+
|
| 295 |
+
.. code-block:: ipythonconsole
|
| 296 |
+
|
| 297 |
+
In [1]: raise Exception
|
| 298 |
+
Traceback (most recent call last):
|
| 299 |
+
...
|
| 300 |
+
Exception
|
| 301 |
+
|
| 302 |
+
"""
|
| 303 |
+
|
| 304 |
+
name = "IPython console session"
|
| 305 |
+
aliases = ["ipythonconsole", "ipython3console"]
|
| 306 |
+
mimetypes = ["text/x-ipython-console"]
|
| 307 |
+
|
| 308 |
+
# The regexps used to determine what is input and what is output.
|
| 309 |
+
# The default prompts for IPython are:
|
| 310 |
+
#
|
| 311 |
+
# in = 'In [#]: '
|
| 312 |
+
# continuation = ' .D.: '
|
| 313 |
+
# template = 'Out[#]: '
|
| 314 |
+
#
|
| 315 |
+
# Where '#' is the 'prompt number' or 'execution count' and 'D'
|
| 316 |
+
# D is a number of dots matching the width of the execution count
|
| 317 |
+
#
|
| 318 |
+
in1_regex = r"In \[[0-9]+\]: "
|
| 319 |
+
in2_regex = r" \.\.+\.: "
|
| 320 |
+
out_regex = r"Out\[[0-9]+\]: "
|
| 321 |
+
|
| 322 |
+
#: The regex to determine when a traceback starts.
|
| 323 |
+
ipytb_start = re.compile(r"^(\^C)?(-+\n)|^( File)(.*)(, line )(\d+\n)")
|
| 324 |
+
|
| 325 |
+
def __init__(self, **options):
|
| 326 |
+
"""Initialize the IPython console lexer.
|
| 327 |
+
|
| 328 |
+
Parameters
|
| 329 |
+
----------
|
| 330 |
+
in1_regex : RegexObject
|
| 331 |
+
The compiled regular expression used to detect the start
|
| 332 |
+
of inputs. Although the IPython configuration setting may have a
|
| 333 |
+
trailing whitespace, do not include it in the regex. If `None`,
|
| 334 |
+
then the default input prompt is assumed.
|
| 335 |
+
in2_regex : RegexObject
|
| 336 |
+
The compiled regular expression used to detect the continuation
|
| 337 |
+
of inputs. Although the IPython configuration setting may have a
|
| 338 |
+
trailing whitespace, do not include it in the regex. If `None`,
|
| 339 |
+
then the default input prompt is assumed.
|
| 340 |
+
out_regex : RegexObject
|
| 341 |
+
The compiled regular expression used to detect outputs. If `None`,
|
| 342 |
+
then the default output prompt is assumed.
|
| 343 |
+
|
| 344 |
+
"""
|
| 345 |
+
in1_regex = options.get("in1_regex", self.in1_regex)
|
| 346 |
+
in2_regex = options.get("in2_regex", self.in2_regex)
|
| 347 |
+
out_regex = options.get("out_regex", self.out_regex)
|
| 348 |
+
|
| 349 |
+
# So that we can work with input and output prompts which have been
|
| 350 |
+
# rstrip'd (possibly by editors) we also need rstrip'd variants. If
|
| 351 |
+
# we do not do this, then such prompts will be tagged as 'output'.
|
| 352 |
+
# The reason can't just use the rstrip'd variants instead is because
|
| 353 |
+
# we want any whitespace associated with the prompt to be inserted
|
| 354 |
+
# with the token. This allows formatted code to be modified so as hide
|
| 355 |
+
# the appearance of prompts, with the whitespace included. One example
|
| 356 |
+
# use of this is in copybutton.js from the standard lib Python docs.
|
| 357 |
+
in1_regex_rstrip = in1_regex.rstrip() + "\n"
|
| 358 |
+
in2_regex_rstrip = in2_regex.rstrip() + "\n"
|
| 359 |
+
out_regex_rstrip = out_regex.rstrip() + "\n"
|
| 360 |
+
|
| 361 |
+
# Compile and save them all.
|
| 362 |
+
attrs = [
|
| 363 |
+
"in1_regex",
|
| 364 |
+
"in2_regex",
|
| 365 |
+
"out_regex",
|
| 366 |
+
"in1_regex_rstrip",
|
| 367 |
+
"in2_regex_rstrip",
|
| 368 |
+
"out_regex_rstrip",
|
| 369 |
+
]
|
| 370 |
+
for attr in attrs:
|
| 371 |
+
self.__setattr__(attr, re.compile(locals()[attr]))
|
| 372 |
+
|
| 373 |
+
Lexer.__init__(self, **options)
|
| 374 |
+
|
| 375 |
+
self.pylexer = IPython3Lexer(**options)
|
| 376 |
+
self.tblexer = IPythonTracebackLexer(**options)
|
| 377 |
+
|
| 378 |
+
self.reset()
|
| 379 |
+
|
| 380 |
+
def reset(self):
|
| 381 |
+
self.mode = "output"
|
| 382 |
+
self.index = 0
|
| 383 |
+
self.buffer = ""
|
| 384 |
+
self.insertions = []
|
| 385 |
+
|
| 386 |
+
def buffered_tokens(self):
|
| 387 |
+
"""
|
| 388 |
+
Generator of unprocessed tokens after doing insertions and before
|
| 389 |
+
changing to a new state.
|
| 390 |
+
|
| 391 |
+
"""
|
| 392 |
+
if self.mode == "output":
|
| 393 |
+
tokens = [(0, Generic.Output, self.buffer)]
|
| 394 |
+
elif self.mode == "input":
|
| 395 |
+
tokens = self.pylexer.get_tokens_unprocessed(self.buffer)
|
| 396 |
+
else: # traceback
|
| 397 |
+
tokens = self.tblexer.get_tokens_unprocessed(self.buffer)
|
| 398 |
+
|
| 399 |
+
for i, t, v in do_insertions(self.insertions, tokens):
|
| 400 |
+
# All token indexes are relative to the buffer.
|
| 401 |
+
yield self.index + i, t, v
|
| 402 |
+
|
| 403 |
+
# Clear it all
|
| 404 |
+
self.index += len(self.buffer)
|
| 405 |
+
self.buffer = ""
|
| 406 |
+
self.insertions = []
|
| 407 |
+
|
| 408 |
+
def get_mci(self, line):
|
| 409 |
+
"""
|
| 410 |
+
Parses the line and returns a 3-tuple: (mode, code, insertion).
|
| 411 |
+
|
| 412 |
+
`mode` is the next mode (or state) of the lexer, and is always equal
|
| 413 |
+
to 'input', 'output', or 'tb'.
|
| 414 |
+
|
| 415 |
+
`code` is a portion of the line that should be added to the buffer
|
| 416 |
+
corresponding to the next mode and eventually lexed by another lexer.
|
| 417 |
+
For example, `code` could be Python code if `mode` were 'input'.
|
| 418 |
+
|
| 419 |
+
`insertion` is a 3-tuple (index, token, text) representing an
|
| 420 |
+
unprocessed "token" that will be inserted into the stream of tokens
|
| 421 |
+
that are created from the buffer once we change modes. This is usually
|
| 422 |
+
the input or output prompt.
|
| 423 |
+
|
| 424 |
+
In general, the next mode depends on current mode and on the contents
|
| 425 |
+
of `line`.
|
| 426 |
+
|
| 427 |
+
"""
|
| 428 |
+
# To reduce the number of regex match checks, we have multiple
|
| 429 |
+
# 'if' blocks instead of 'if-elif' blocks.
|
| 430 |
+
|
| 431 |
+
# Check for possible end of input
|
| 432 |
+
in2_match = self.in2_regex.match(line)
|
| 433 |
+
in2_match_rstrip = self.in2_regex_rstrip.match(line)
|
| 434 |
+
if (
|
| 435 |
+
in2_match and in2_match.group().rstrip() == line.rstrip()
|
| 436 |
+
) or in2_match_rstrip:
|
| 437 |
+
end_input = True
|
| 438 |
+
else:
|
| 439 |
+
end_input = False
|
| 440 |
+
if end_input and self.mode != "tb":
|
| 441 |
+
# Only look for an end of input when not in tb mode.
|
| 442 |
+
# An ellipsis could appear within the traceback.
|
| 443 |
+
mode = "output"
|
| 444 |
+
code = ""
|
| 445 |
+
insertion = (0, Generic.Prompt, line)
|
| 446 |
+
return mode, code, insertion
|
| 447 |
+
|
| 448 |
+
# Check for output prompt
|
| 449 |
+
out_match = self.out_regex.match(line)
|
| 450 |
+
out_match_rstrip = self.out_regex_rstrip.match(line)
|
| 451 |
+
if out_match or out_match_rstrip:
|
| 452 |
+
mode = "output"
|
| 453 |
+
if out_match:
|
| 454 |
+
idx = out_match.end()
|
| 455 |
+
else:
|
| 456 |
+
idx = out_match_rstrip.end()
|
| 457 |
+
code = line[idx:]
|
| 458 |
+
# Use the 'heading' token for output. We cannot use Generic.Error
|
| 459 |
+
# since it would conflict with exceptions.
|
| 460 |
+
insertion = (0, Generic.Heading, line[:idx])
|
| 461 |
+
return mode, code, insertion
|
| 462 |
+
|
| 463 |
+
# Check for input or continuation prompt (non stripped version)
|
| 464 |
+
in1_match = self.in1_regex.match(line)
|
| 465 |
+
if in1_match or (in2_match and self.mode != "tb"):
|
| 466 |
+
# New input or when not in tb, continued input.
|
| 467 |
+
# We do not check for continued input when in tb since it is
|
| 468 |
+
# allowable to replace a long stack with an ellipsis.
|
| 469 |
+
mode = "input"
|
| 470 |
+
if in1_match:
|
| 471 |
+
idx = in1_match.end()
|
| 472 |
+
else: # in2_match
|
| 473 |
+
idx = in2_match.end()
|
| 474 |
+
code = line[idx:]
|
| 475 |
+
insertion = (0, Generic.Prompt, line[:idx])
|
| 476 |
+
return mode, code, insertion
|
| 477 |
+
|
| 478 |
+
# Check for input or continuation prompt (stripped version)
|
| 479 |
+
in1_match_rstrip = self.in1_regex_rstrip.match(line)
|
| 480 |
+
if in1_match_rstrip or (in2_match_rstrip and self.mode != "tb"):
|
| 481 |
+
# New input or when not in tb, continued input.
|
| 482 |
+
# We do not check for continued input when in tb since it is
|
| 483 |
+
# allowable to replace a long stack with an ellipsis.
|
| 484 |
+
mode = "input"
|
| 485 |
+
if in1_match_rstrip:
|
| 486 |
+
idx = in1_match_rstrip.end()
|
| 487 |
+
else: # in2_match
|
| 488 |
+
idx = in2_match_rstrip.end()
|
| 489 |
+
code = line[idx:]
|
| 490 |
+
insertion = (0, Generic.Prompt, line[:idx])
|
| 491 |
+
return mode, code, insertion
|
| 492 |
+
|
| 493 |
+
# Check for traceback
|
| 494 |
+
if self.ipytb_start.match(line):
|
| 495 |
+
mode = "tb"
|
| 496 |
+
code = line
|
| 497 |
+
insertion = None
|
| 498 |
+
return mode, code, insertion
|
| 499 |
+
|
| 500 |
+
# All other stuff...
|
| 501 |
+
if self.mode in ("input", "output"):
|
| 502 |
+
# We assume all other text is output. Multiline input that
|
| 503 |
+
# does not use the continuation marker cannot be detected.
|
| 504 |
+
# For example, the 3 in the following is clearly output:
|
| 505 |
+
#
|
| 506 |
+
# In [1]: print(3)
|
| 507 |
+
# 3
|
| 508 |
+
#
|
| 509 |
+
# But the following second line is part of the input:
|
| 510 |
+
#
|
| 511 |
+
# In [2]: while True:
|
| 512 |
+
# print(True)
|
| 513 |
+
#
|
| 514 |
+
# In both cases, the 2nd line will be 'output'.
|
| 515 |
+
#
|
| 516 |
+
mode = "output"
|
| 517 |
+
else:
|
| 518 |
+
mode = "tb"
|
| 519 |
+
|
| 520 |
+
code = line
|
| 521 |
+
insertion = None
|
| 522 |
+
|
| 523 |
+
return mode, code, insertion
|
| 524 |
+
|
| 525 |
+
def get_tokens_unprocessed(self, text):
|
| 526 |
+
self.reset()
|
| 527 |
+
for match in line_re.finditer(text):
|
| 528 |
+
line = match.group()
|
| 529 |
+
mode, code, insertion = self.get_mci(line)
|
| 530 |
+
|
| 531 |
+
if mode != self.mode:
|
| 532 |
+
# Yield buffered tokens before transitioning to new mode.
|
| 533 |
+
for token in self.buffered_tokens():
|
| 534 |
+
yield token
|
| 535 |
+
self.mode = mode
|
| 536 |
+
|
| 537 |
+
if insertion:
|
| 538 |
+
self.insertions.append((len(self.buffer), [insertion]))
|
| 539 |
+
self.buffer += code
|
| 540 |
+
|
| 541 |
+
for token in self.buffered_tokens():
|
| 542 |
+
yield token
|
| 543 |
+
|
| 544 |
+
|
| 545 |
+
class IPyLexer(Lexer):
|
| 546 |
+
r"""
|
| 547 |
+
Primary lexer for all IPython-like code.
|
| 548 |
+
|
| 549 |
+
This is a simple helper lexer. If the first line of the text begins with
|
| 550 |
+
"In \[[0-9]+\]:", then the entire text is parsed with an IPython console
|
| 551 |
+
lexer. If not, then the entire text is parsed with an IPython lexer.
|
| 552 |
+
|
| 553 |
+
The goal is to reduce the number of lexers that are registered
|
| 554 |
+
with Pygments.
|
| 555 |
+
|
| 556 |
+
"""
|
| 557 |
+
|
| 558 |
+
name = "IPy session"
|
| 559 |
+
aliases = ["ipy", "ipy3"]
|
| 560 |
+
|
| 561 |
+
def __init__(self, **options):
|
| 562 |
+
"""
|
| 563 |
+
Create a new IPyLexer instance which dispatch to either an
|
| 564 |
+
IPythonCOnsoleLexer (if In prompts are present) or and IPythonLexer (if
|
| 565 |
+
In prompts are not present).
|
| 566 |
+
"""
|
| 567 |
+
# init docstring is necessary for docs not to fail to build do to parent
|
| 568 |
+
# docs referenceing a section in pygments docs.
|
| 569 |
+
Lexer.__init__(self, **options)
|
| 570 |
+
|
| 571 |
+
self.IPythonLexer = IPythonLexer(**options)
|
| 572 |
+
self.IPythonConsoleLexer = IPythonConsoleLexer(**options)
|
| 573 |
+
|
| 574 |
+
def get_tokens_unprocessed(self, text):
|
| 575 |
+
# Search for the input prompt anywhere...this allows code blocks to
|
| 576 |
+
# begin with comments as well.
|
| 577 |
+
if re.match(r".*(In \[[0-9]+\]:)", text.strip(), re.DOTALL):
|
| 578 |
+
lex = self.IPythonConsoleLexer
|
| 579 |
+
else:
|
| 580 |
+
lex = self.IPythonLexer
|
| 581 |
+
for token in lex.get_tokens_unprocessed(text):
|
| 582 |
+
yield token
|
lib/python3.12/site-packages/isympy.py
ADDED
|
@@ -0,0 +1,342 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Python shell for SymPy.
|
| 3 |
+
|
| 4 |
+
This is just a normal Python shell (IPython shell if you have the
|
| 5 |
+
IPython package installed), that executes the following commands for
|
| 6 |
+
the user:
|
| 7 |
+
|
| 8 |
+
>>> from __future__ import division
|
| 9 |
+
>>> from sympy import *
|
| 10 |
+
>>> x, y, z, t = symbols('x y z t')
|
| 11 |
+
>>> k, m, n = symbols('k m n', integer=True)
|
| 12 |
+
>>> f, g, h = symbols('f g h', cls=Function)
|
| 13 |
+
>>> init_printing()
|
| 14 |
+
|
| 15 |
+
So starting 'isympy' is equivalent to starting Python (or IPython) and
|
| 16 |
+
executing the above commands by hand. It is intended for easy and quick
|
| 17 |
+
experimentation with SymPy. isympy is a good way to use SymPy as an
|
| 18 |
+
interactive calculator. If you have IPython and Matplotlib installed, then
|
| 19 |
+
interactive plotting is enabled by default.
|
| 20 |
+
|
| 21 |
+
COMMAND LINE OPTIONS
|
| 22 |
+
--------------------
|
| 23 |
+
|
| 24 |
+
-c CONSOLE, --console=CONSOLE
|
| 25 |
+
|
| 26 |
+
Use the specified shell (Python or IPython) shell as the console
|
| 27 |
+
backend instead of the default one (IPython if present, Python
|
| 28 |
+
otherwise), e.g.:
|
| 29 |
+
|
| 30 |
+
$isympy -c python
|
| 31 |
+
|
| 32 |
+
CONSOLE must be one of 'ipython' or 'python'
|
| 33 |
+
|
| 34 |
+
-p PRETTY, --pretty PRETTY
|
| 35 |
+
|
| 36 |
+
Setup pretty-printing in SymPy. When pretty-printing is enabled,
|
| 37 |
+
expressions can be printed with Unicode or ASCII. The default is
|
| 38 |
+
to use pretty-printing (with Unicode if the terminal supports it).
|
| 39 |
+
When this option is 'no', expressions will not be pretty-printed
|
| 40 |
+
and ASCII will be used:
|
| 41 |
+
|
| 42 |
+
$isympy -p no
|
| 43 |
+
|
| 44 |
+
PRETTY must be one of 'unicode', 'ascii', or 'no'
|
| 45 |
+
|
| 46 |
+
-t TYPES, --types=TYPES
|
| 47 |
+
|
| 48 |
+
Setup the ground types for the polys. By default, gmpy ground types
|
| 49 |
+
are used if gmpy2 or gmpy is installed, otherwise it falls back to python
|
| 50 |
+
ground types, which are a little bit slower. You can manually
|
| 51 |
+
choose python ground types even if gmpy is installed (e.g., for
|
| 52 |
+
testing purposes):
|
| 53 |
+
|
| 54 |
+
$isympy -t python
|
| 55 |
+
|
| 56 |
+
TYPES must be one of 'gmpy', 'gmpy1' or 'python'
|
| 57 |
+
|
| 58 |
+
Note that the ground type gmpy1 is primarily intended for testing; it
|
| 59 |
+
forces the use of gmpy version 1 even if gmpy2 is available.
|
| 60 |
+
|
| 61 |
+
This is the same as setting the environment variable
|
| 62 |
+
SYMPY_GROUND_TYPES to the given ground type (e.g.,
|
| 63 |
+
SYMPY_GROUND_TYPES='gmpy')
|
| 64 |
+
|
| 65 |
+
The ground types can be determined interactively from the variable
|
| 66 |
+
sympy.polys.domains.GROUND_TYPES.
|
| 67 |
+
|
| 68 |
+
-o ORDER, --order ORDER
|
| 69 |
+
|
| 70 |
+
Setup the ordering of terms for printing. The default is lex, which
|
| 71 |
+
orders terms lexicographically (e.g., x**2 + x + 1). You can choose
|
| 72 |
+
other orderings, such as rev-lex, which will use reverse
|
| 73 |
+
lexicographic ordering (e.g., 1 + x + x**2):
|
| 74 |
+
|
| 75 |
+
$isympy -o rev-lex
|
| 76 |
+
|
| 77 |
+
ORDER must be one of 'lex', 'rev-lex', 'grlex', 'rev-grlex',
|
| 78 |
+
'grevlex', 'rev-grevlex', 'old', or 'none'.
|
| 79 |
+
|
| 80 |
+
Note that for very large expressions, ORDER='none' may speed up
|
| 81 |
+
printing considerably but the terms will have no canonical order.
|
| 82 |
+
|
| 83 |
+
-q, --quiet
|
| 84 |
+
|
| 85 |
+
Print only Python's and SymPy's versions to stdout at startup.
|
| 86 |
+
|
| 87 |
+
-d, --doctest
|
| 88 |
+
|
| 89 |
+
Use the same format that should be used for doctests. This is
|
| 90 |
+
equivalent to -c python -p no.
|
| 91 |
+
|
| 92 |
+
-C, --no-cache
|
| 93 |
+
|
| 94 |
+
Disable the caching mechanism. Disabling the cache may slow certain
|
| 95 |
+
operations down considerably. This is useful for testing the cache,
|
| 96 |
+
or for benchmarking, as the cache can result in deceptive timings.
|
| 97 |
+
|
| 98 |
+
This is equivalent to setting the environment variable
|
| 99 |
+
SYMPY_USE_CACHE to 'no'.
|
| 100 |
+
|
| 101 |
+
-a, --auto-symbols (requires at least IPython 0.11)
|
| 102 |
+
|
| 103 |
+
Automatically create missing symbols. Normally, typing a name of a
|
| 104 |
+
Symbol that has not been instantiated first would raise NameError,
|
| 105 |
+
but with this option enabled, any undefined name will be
|
| 106 |
+
automatically created as a Symbol.
|
| 107 |
+
|
| 108 |
+
Note that this is intended only for interactive, calculator style
|
| 109 |
+
usage. In a script that uses SymPy, Symbols should be instantiated
|
| 110 |
+
at the top, so that it's clear what they are.
|
| 111 |
+
|
| 112 |
+
This will not override any names that are already defined, which
|
| 113 |
+
includes the single character letters represented by the mnemonic
|
| 114 |
+
QCOSINE (see the "Gotchas and Pitfalls" document in the
|
| 115 |
+
documentation). You can delete existing names by executing "del
|
| 116 |
+
name". If a name is defined, typing "'name' in dir()" will return True.
|
| 117 |
+
|
| 118 |
+
The Symbols that are created using this have default assumptions.
|
| 119 |
+
If you want to place assumptions on symbols, you should create them
|
| 120 |
+
using symbols() or var().
|
| 121 |
+
|
| 122 |
+
Finally, this only works in the top level namespace. So, for
|
| 123 |
+
example, if you define a function in isympy with an undefined
|
| 124 |
+
Symbol, it will not work.
|
| 125 |
+
|
| 126 |
+
See also the -i and -I options.
|
| 127 |
+
|
| 128 |
+
-i, --int-to-Integer (requires at least IPython 0.11)
|
| 129 |
+
|
| 130 |
+
Automatically wrap int literals with Integer. This makes it so that
|
| 131 |
+
things like 1/2 will come out as Rational(1, 2), rather than 0.5. This
|
| 132 |
+
works by preprocessing the source and wrapping all int literals with
|
| 133 |
+
Integer. Note that this will not change the behavior of int literals
|
| 134 |
+
assigned to variables, and it also won't change the behavior of functions
|
| 135 |
+
that return int literals.
|
| 136 |
+
|
| 137 |
+
If you want an int, you can wrap the literal in int(), e.g. int(3)/int(2)
|
| 138 |
+
gives 1.5 (with division imported from __future__).
|
| 139 |
+
|
| 140 |
+
-I, --interactive (requires at least IPython 0.11)
|
| 141 |
+
|
| 142 |
+
This is equivalent to --auto-symbols --int-to-Integer. Future options
|
| 143 |
+
designed for ease of interactive use may be added to this.
|
| 144 |
+
|
| 145 |
+
-D, --debug
|
| 146 |
+
|
| 147 |
+
Enable debugging output. This is the same as setting the
|
| 148 |
+
environment variable SYMPY_DEBUG to 'True'. The debug status is set
|
| 149 |
+
in the variable SYMPY_DEBUG within isympy.
|
| 150 |
+
|
| 151 |
+
-- IPython options
|
| 152 |
+
|
| 153 |
+
Additionally you can pass command line options directly to the IPython
|
| 154 |
+
interpreter (the standard Python shell is not supported). However you
|
| 155 |
+
need to add the '--' separator between two types of options, e.g the
|
| 156 |
+
startup banner option and the colors option. You need to enter the
|
| 157 |
+
options as required by the version of IPython that you are using, too:
|
| 158 |
+
|
| 159 |
+
in IPython 0.11,
|
| 160 |
+
|
| 161 |
+
$isympy -q -- --colors=NoColor
|
| 162 |
+
|
| 163 |
+
or older versions of IPython,
|
| 164 |
+
|
| 165 |
+
$isympy -q -- -colors NoColor
|
| 166 |
+
|
| 167 |
+
See also isympy --help.
|
| 168 |
+
"""
|
| 169 |
+
|
| 170 |
+
import os
|
| 171 |
+
import sys
|
| 172 |
+
|
| 173 |
+
# DO NOT IMPORT SYMPY HERE! Or the setting of the sympy environment variables
|
| 174 |
+
# by the command line will break.
|
| 175 |
+
|
| 176 |
+
def main() -> None:
|
| 177 |
+
from argparse import ArgumentParser, RawDescriptionHelpFormatter
|
| 178 |
+
|
| 179 |
+
VERSION = None
|
| 180 |
+
if '--version' in sys.argv:
|
| 181 |
+
# We cannot import sympy before this is run, because flags like -C and
|
| 182 |
+
# -t set environment variables that must be set before SymPy is
|
| 183 |
+
# imported. The only thing we need to import it for is to get the
|
| 184 |
+
# version, which only matters with the --version flag.
|
| 185 |
+
import sympy
|
| 186 |
+
VERSION = sympy.__version__
|
| 187 |
+
|
| 188 |
+
usage = 'isympy [options] -- [ipython options]'
|
| 189 |
+
parser = ArgumentParser(
|
| 190 |
+
usage=usage,
|
| 191 |
+
description=__doc__,
|
| 192 |
+
formatter_class=RawDescriptionHelpFormatter,
|
| 193 |
+
)
|
| 194 |
+
|
| 195 |
+
parser.add_argument('--version', action='version', version=VERSION)
|
| 196 |
+
|
| 197 |
+
parser.add_argument(
|
| 198 |
+
'-c', '--console',
|
| 199 |
+
dest='console',
|
| 200 |
+
action='store',
|
| 201 |
+
default=None,
|
| 202 |
+
choices=['ipython', 'python'],
|
| 203 |
+
metavar='CONSOLE',
|
| 204 |
+
help='select type of interactive session: ipython | python; defaults '
|
| 205 |
+
'to ipython if IPython is installed, otherwise python')
|
| 206 |
+
|
| 207 |
+
parser.add_argument(
|
| 208 |
+
'-p', '--pretty',
|
| 209 |
+
dest='pretty',
|
| 210 |
+
action='store',
|
| 211 |
+
default=None,
|
| 212 |
+
metavar='PRETTY',
|
| 213 |
+
choices=['unicode', 'ascii', 'no'],
|
| 214 |
+
help='setup pretty printing: unicode | ascii | no; defaults to '
|
| 215 |
+
'unicode printing if the terminal supports it, otherwise ascii')
|
| 216 |
+
|
| 217 |
+
parser.add_argument(
|
| 218 |
+
'-t', '--types',
|
| 219 |
+
dest='types',
|
| 220 |
+
action='store',
|
| 221 |
+
default=None,
|
| 222 |
+
metavar='TYPES',
|
| 223 |
+
choices=['gmpy', 'gmpy1', 'python'],
|
| 224 |
+
help='setup ground types: gmpy | gmpy1 | python; defaults to gmpy if gmpy2 '
|
| 225 |
+
'or gmpy is installed, otherwise python')
|
| 226 |
+
|
| 227 |
+
parser.add_argument(
|
| 228 |
+
'-o', '--order',
|
| 229 |
+
dest='order',
|
| 230 |
+
action='store',
|
| 231 |
+
default=None,
|
| 232 |
+
metavar='ORDER',
|
| 233 |
+
choices=['lex', 'grlex', 'grevlex', 'rev-lex', 'rev-grlex', 'rev-grevlex', 'old', 'none'],
|
| 234 |
+
help='setup ordering of terms: [rev-]lex | [rev-]grlex | [rev-]grevlex | old | none; defaults to lex')
|
| 235 |
+
|
| 236 |
+
parser.add_argument(
|
| 237 |
+
'-q', '--quiet',
|
| 238 |
+
dest='quiet',
|
| 239 |
+
action='store_true',
|
| 240 |
+
default=False,
|
| 241 |
+
help='print only version information at startup')
|
| 242 |
+
|
| 243 |
+
parser.add_argument(
|
| 244 |
+
'-d', '--doctest',
|
| 245 |
+
dest='doctest',
|
| 246 |
+
action='store_true',
|
| 247 |
+
default=False,
|
| 248 |
+
help='use the doctest format for output (you can just copy and paste it)')
|
| 249 |
+
|
| 250 |
+
parser.add_argument(
|
| 251 |
+
'-C', '--no-cache',
|
| 252 |
+
dest='cache',
|
| 253 |
+
action='store_false',
|
| 254 |
+
default=True,
|
| 255 |
+
help='disable caching mechanism')
|
| 256 |
+
|
| 257 |
+
parser.add_argument(
|
| 258 |
+
'-a', '--auto-symbols',
|
| 259 |
+
dest='auto_symbols',
|
| 260 |
+
action='store_true',
|
| 261 |
+
default=False,
|
| 262 |
+
help='automatically construct missing symbols')
|
| 263 |
+
|
| 264 |
+
parser.add_argument(
|
| 265 |
+
'-i', '--int-to-Integer',
|
| 266 |
+
dest='auto_int_to_Integer',
|
| 267 |
+
action='store_true',
|
| 268 |
+
default=False,
|
| 269 |
+
help="automatically wrap int literals with Integer")
|
| 270 |
+
|
| 271 |
+
parser.add_argument(
|
| 272 |
+
'-I', '--interactive',
|
| 273 |
+
dest='interactive',
|
| 274 |
+
action='store_true',
|
| 275 |
+
default=False,
|
| 276 |
+
help="equivalent to -a -i")
|
| 277 |
+
|
| 278 |
+
parser.add_argument(
|
| 279 |
+
'-D', '--debug',
|
| 280 |
+
dest='debug',
|
| 281 |
+
action='store_true',
|
| 282 |
+
default=False,
|
| 283 |
+
help='enable debugging output')
|
| 284 |
+
|
| 285 |
+
(options, ipy_args) = parser.parse_known_args()
|
| 286 |
+
if '--' in ipy_args:
|
| 287 |
+
ipy_args.remove('--')
|
| 288 |
+
|
| 289 |
+
if not options.cache:
|
| 290 |
+
os.environ['SYMPY_USE_CACHE'] = 'no'
|
| 291 |
+
|
| 292 |
+
if options.types:
|
| 293 |
+
os.environ['SYMPY_GROUND_TYPES'] = options.types
|
| 294 |
+
|
| 295 |
+
if options.debug:
|
| 296 |
+
os.environ['SYMPY_DEBUG'] = str(options.debug)
|
| 297 |
+
|
| 298 |
+
if options.doctest:
|
| 299 |
+
options.pretty = 'no'
|
| 300 |
+
options.console = 'python'
|
| 301 |
+
|
| 302 |
+
session = options.console
|
| 303 |
+
|
| 304 |
+
if session is not None:
|
| 305 |
+
ipython = session == 'ipython'
|
| 306 |
+
else:
|
| 307 |
+
try:
|
| 308 |
+
import IPython
|
| 309 |
+
ipython = True
|
| 310 |
+
except ImportError:
|
| 311 |
+
if not options.quiet:
|
| 312 |
+
from sympy.interactive.session import no_ipython
|
| 313 |
+
print(no_ipython)
|
| 314 |
+
ipython = False
|
| 315 |
+
|
| 316 |
+
args = {
|
| 317 |
+
'pretty_print': True,
|
| 318 |
+
'use_unicode': None,
|
| 319 |
+
'use_latex': None,
|
| 320 |
+
'order': None,
|
| 321 |
+
'argv': ipy_args,
|
| 322 |
+
}
|
| 323 |
+
|
| 324 |
+
if options.pretty == 'unicode':
|
| 325 |
+
args['use_unicode'] = True
|
| 326 |
+
elif options.pretty == 'ascii':
|
| 327 |
+
args['use_unicode'] = False
|
| 328 |
+
elif options.pretty == 'no':
|
| 329 |
+
args['pretty_print'] = False
|
| 330 |
+
|
| 331 |
+
if options.order is not None:
|
| 332 |
+
args['order'] = options.order
|
| 333 |
+
|
| 334 |
+
args['quiet'] = options.quiet
|
| 335 |
+
args['auto_symbols'] = options.auto_symbols or options.interactive
|
| 336 |
+
args['auto_int_to_Integer'] = options.auto_int_to_Integer or options.interactive
|
| 337 |
+
|
| 338 |
+
from sympy.interactive import init_session
|
| 339 |
+
init_session(ipython, **args)
|
| 340 |
+
|
| 341 |
+
if __name__ == "__main__":
|
| 342 |
+
main()
|
lib/python3.12/site-packages/package_readme.md
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<div align="center">
|
| 2 |
+
<img src="https://i.imgur.com/RUtiVzH.png" width="600" /><br><br>
|
| 3 |
+
</div>
|
| 4 |
+
|
| 5 |
+
# Weights and Biases [](https://pypi.python.org/pypi/wandb) [](https://anaconda.org/conda-forge/wandb) [](https://circleci.com/gh/wandb/wandb) [](https://codecov.io/gh/wandb/wandb)
|
| 6 |
+
|
| 7 |
+
Use W&B to build better models faster. Track and visualize all the pieces of your machine learning pipeline, from datasets to production machine learning models. Get started with W&B today, [sign up for an account!](https://wandb.com?utm_source=github&utm_medium=code&utm_campaign=wandb&utm_content=readme)
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
See the [W&B Developer Guide](https://docs.wandb.ai/?utm_source=github&utm_medium=code&utm_campaign=wandb&utm_content=documentation) and [API Reference Guide](https://docs.wandb.ai/ref?utm_source=github&utm_medium=code&utm_campaign=wandb&utm_content=documentation) for a full technical description of the W&B platform.
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
# Quickstart
|
| 16 |
+
|
| 17 |
+
Get started with W&B in four steps:
|
| 18 |
+
|
| 19 |
+
1. First, sign up for a [W&B account](https://wandb.ai/login?utm_source=github&utm_medium=code&utm_campaign=wandb&utm_content=quickstart).
|
| 20 |
+
|
| 21 |
+
2. Second, install the W&B SDK with [pip](https://pip.pypa.io/en/stable/). Navigate to your terminal and type the following command:
|
| 22 |
+
|
| 23 |
+
```shell
|
| 24 |
+
pip install wandb
|
| 25 |
+
```
|
| 26 |
+
|
| 27 |
+
3. Third, log into W&B:
|
| 28 |
+
|
| 29 |
+
```python
|
| 30 |
+
wandb.login()
|
| 31 |
+
```
|
| 32 |
+
|
| 33 |
+
4. Use the example code snippet below as a template to integrate W&B to your Python script:
|
| 34 |
+
|
| 35 |
+
```python
|
| 36 |
+
import wandb
|
| 37 |
+
|
| 38 |
+
# Start a W&B Run with wandb.init
|
| 39 |
+
run = wandb.init(project="my_first_project")
|
| 40 |
+
|
| 41 |
+
# Save model inputs and hyperparameters in a wandb.config object
|
| 42 |
+
config = run.config
|
| 43 |
+
config.learning_rate = 0.01
|
| 44 |
+
|
| 45 |
+
# Model training code here ...
|
| 46 |
+
|
| 47 |
+
# Log metrics over time to visualize performance with wandb.log
|
| 48 |
+
for i in range(10):
|
| 49 |
+
run.log({"loss": ...})
|
| 50 |
+
|
| 51 |
+
# Mark the run as finished, and finish uploading all data
|
| 52 |
+
run.finish()
|
| 53 |
+
```
|
| 54 |
+
|
| 55 |
+
For example, if the preceding code was stored in a script called train.py:
|
| 56 |
+
|
| 57 |
+
```shell
|
| 58 |
+
python train.py
|
| 59 |
+
```
|
| 60 |
+
|
| 61 |
+
You will see a URL in your terminal logs when your script starts and finishes. Data is staged locally in a directory named _wandb_ relative to your script. Navigate to the W&B App to view a dashboard of your first W&B Experiment. Use the W&B App to compare multiple experiments in a unified place, dive into the results of a single run, and much more!
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
# Integrations
|
| 66 |
+
|
| 67 |
+
Use your favorite framework with W&B. W&B integrations make it fast and easy to set up experiment tracking and data versioning inside existing projects. For more information on how to integrate W&B with the framework of your choice, see [W&B Integrations](https://docs.wandb.ai/guides/integrations) in the W&B Developer Guide.
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
# Python Version Support
|
| 72 |
+
|
| 73 |
+
We are committed to supporting our minimum required Python version for *at least* six months after its official end-of-life (EOL) date, as defined by the Python Software Foundation. You can find a list of Python EOL dates [here](https://devguide.python.org/versions/).
|
| 74 |
+
|
| 75 |
+
When we discontinue support for a Python version, we will increment the library’s minor version number to reflect this change.
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
# Contribution guidelines
|
| 80 |
+
Weights & Biases ❤️ open source, and we welcome contributions from the community! See the [Contribution guide](https://github.com/wandb/wandb/blob/main/CONTRIBUTING.md) for more information on the development workflow and the internals of the wandb library. For wandb bugs and feature requests, visit [GitHub Issues](https://github.com/wandb/wandb/issues) or contact support@wandb.com.
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
# Academic Researchers
|
| 85 |
+
Reach out to W&B Support at support@wandb.com to get a [free academic license](https://www.wandb.com/academic) for you and your research group.
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
# W&B Community
|
| 90 |
+
|
| 91 |
+
Be a part of the growing W&B Community and interact with the W&B team in our [Discord](https://wandb.me/discord). Stay connected with the latest ML updates and tutorials with [W&B Fully Connected](https://wandb.ai/fully-connected).
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
# License
|
| 96 |
+
|
| 97 |
+
[MIT License](https://github.com/wandb/wandb/blob/main/LICENSE)
|
lib/python3.12/site-packages/pylab.py
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from matplotlib.pylab import * # noqa: F401, F403
|
| 2 |
+
import matplotlib.pylab
|
| 3 |
+
__doc__ = matplotlib.pylab.__doc__
|
lib/python3.12/site-packages/sglang/__init__.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SGLang public APIs
|
| 2 |
+
|
| 3 |
+
# Frontend Language APIs
|
| 4 |
+
from sglang.api import (
|
| 5 |
+
Engine,
|
| 6 |
+
Runtime,
|
| 7 |
+
assistant,
|
| 8 |
+
assistant_begin,
|
| 9 |
+
assistant_end,
|
| 10 |
+
flush_cache,
|
| 11 |
+
function,
|
| 12 |
+
gen,
|
| 13 |
+
gen_int,
|
| 14 |
+
gen_string,
|
| 15 |
+
get_server_info,
|
| 16 |
+
image,
|
| 17 |
+
select,
|
| 18 |
+
set_default_backend,
|
| 19 |
+
system,
|
| 20 |
+
system_begin,
|
| 21 |
+
system_end,
|
| 22 |
+
user,
|
| 23 |
+
user_begin,
|
| 24 |
+
user_end,
|
| 25 |
+
video,
|
| 26 |
+
)
|
| 27 |
+
from sglang.global_config import global_config
|
| 28 |
+
from sglang.lang.backend.runtime_endpoint import RuntimeEndpoint
|
| 29 |
+
from sglang.lang.choices import (
|
| 30 |
+
greedy_token_selection,
|
| 31 |
+
token_length_normalized,
|
| 32 |
+
unconditional_likelihood_normalized,
|
| 33 |
+
)
|
| 34 |
+
from sglang.utils import LazyImport
|
| 35 |
+
from sglang.version import __version__
|
| 36 |
+
|
| 37 |
+
ServerArgs = LazyImport("sglang.srt.server_args", "ServerArgs")
|
| 38 |
+
Anthropic = LazyImport("sglang.lang.backend.anthropic", "Anthropic")
|
| 39 |
+
LiteLLM = LazyImport("sglang.lang.backend.litellm", "LiteLLM")
|
| 40 |
+
OpenAI = LazyImport("sglang.lang.backend.openai", "OpenAI")
|
| 41 |
+
VertexAI = LazyImport("sglang.lang.backend.vertexai", "VertexAI")
|
| 42 |
+
|
| 43 |
+
__all__ = [
|
| 44 |
+
"Engine",
|
| 45 |
+
"Runtime",
|
| 46 |
+
"assistant",
|
| 47 |
+
"assistant_begin",
|
| 48 |
+
"assistant_end",
|
| 49 |
+
"flush_cache",
|
| 50 |
+
"function",
|
| 51 |
+
"gen",
|
| 52 |
+
"gen_int",
|
| 53 |
+
"gen_string",
|
| 54 |
+
"get_server_info",
|
| 55 |
+
"image",
|
| 56 |
+
"select",
|
| 57 |
+
"set_default_backend",
|
| 58 |
+
"system",
|
| 59 |
+
"system_begin",
|
| 60 |
+
"system_end",
|
| 61 |
+
"user",
|
| 62 |
+
"user_begin",
|
| 63 |
+
"user_end",
|
| 64 |
+
"video",
|
| 65 |
+
"RuntimeEndpoint",
|
| 66 |
+
"greedy_token_selection",
|
| 67 |
+
"token_length_normalized",
|
| 68 |
+
"unconditional_likelihood_normalized",
|
| 69 |
+
"ServerArgs",
|
| 70 |
+
"Anthropic",
|
| 71 |
+
"LiteLLM",
|
| 72 |
+
"OpenAI",
|
| 73 |
+
"VertexAI",
|
| 74 |
+
"global_config",
|
| 75 |
+
"__version__",
|
| 76 |
+
]
|
lib/python3.12/site-packages/sglang/api.py
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Public APIs of the language."""
|
| 2 |
+
|
| 3 |
+
import re
|
| 4 |
+
from typing import Callable, List, Optional, Union
|
| 5 |
+
|
| 6 |
+
from sglang.global_config import global_config
|
| 7 |
+
from sglang.lang.backend.base_backend import BaseBackend
|
| 8 |
+
from sglang.lang.choices import ChoicesSamplingMethod, token_length_normalized
|
| 9 |
+
from sglang.lang.ir import (
|
| 10 |
+
SglExpr,
|
| 11 |
+
SglExprList,
|
| 12 |
+
SglFunction,
|
| 13 |
+
SglGen,
|
| 14 |
+
SglImage,
|
| 15 |
+
SglRoleBegin,
|
| 16 |
+
SglRoleEnd,
|
| 17 |
+
SglSelect,
|
| 18 |
+
SglVideo,
|
| 19 |
+
)
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def function(
|
| 23 |
+
func: Optional[Callable] = None, num_api_spec_tokens: Optional[int] = None
|
| 24 |
+
):
|
| 25 |
+
if func:
|
| 26 |
+
return SglFunction(func, num_api_spec_tokens=num_api_spec_tokens)
|
| 27 |
+
|
| 28 |
+
def decorator(func):
|
| 29 |
+
return SglFunction(func, num_api_spec_tokens=num_api_spec_tokens)
|
| 30 |
+
|
| 31 |
+
return decorator
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def Runtime(*args, **kwargs):
|
| 35 |
+
# Avoid importing unnecessary dependency
|
| 36 |
+
from sglang.lang.backend.runtime_endpoint import Runtime
|
| 37 |
+
|
| 38 |
+
return Runtime(*args, **kwargs)
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def Engine(*args, **kwargs):
|
| 42 |
+
# Avoid importing unnecessary dependency
|
| 43 |
+
from sglang.srt.entrypoints.engine import Engine
|
| 44 |
+
|
| 45 |
+
return Engine(*args, **kwargs)
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def set_default_backend(backend: BaseBackend):
|
| 49 |
+
global_config.default_backend = backend
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def flush_cache(backend: Optional[BaseBackend] = None):
|
| 53 |
+
backend = backend or global_config.default_backend
|
| 54 |
+
if backend is None:
|
| 55 |
+
return False
|
| 56 |
+
|
| 57 |
+
# If backend is Runtime
|
| 58 |
+
if hasattr(backend, "endpoint"):
|
| 59 |
+
backend = backend.endpoint
|
| 60 |
+
return backend.flush_cache()
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def get_server_info(backend: Optional[BaseBackend] = None):
|
| 64 |
+
backend = backend or global_config.default_backend
|
| 65 |
+
if backend is None:
|
| 66 |
+
return None
|
| 67 |
+
|
| 68 |
+
# If backend is Runtime
|
| 69 |
+
if hasattr(backend, "endpoint"):
|
| 70 |
+
backend = backend.endpoint
|
| 71 |
+
return backend.get_server_info()
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def gen(
|
| 75 |
+
name: Optional[str] = None,
|
| 76 |
+
max_tokens: Optional[int] = None,
|
| 77 |
+
min_tokens: Optional[int] = None,
|
| 78 |
+
n: Optional[int] = None,
|
| 79 |
+
stop: Optional[Union[str, List[str]]] = None,
|
| 80 |
+
stop_token_ids: Optional[List[int]] = None,
|
| 81 |
+
temperature: Optional[float] = None,
|
| 82 |
+
top_p: Optional[float] = None,
|
| 83 |
+
top_k: Optional[int] = None,
|
| 84 |
+
min_p: Optional[float] = None,
|
| 85 |
+
frequency_penalty: Optional[float] = None,
|
| 86 |
+
presence_penalty: Optional[float] = None,
|
| 87 |
+
ignore_eos: Optional[bool] = None,
|
| 88 |
+
return_logprob: Optional[bool] = None,
|
| 89 |
+
logprob_start_len: Optional[int] = None,
|
| 90 |
+
top_logprobs_num: Optional[int] = None,
|
| 91 |
+
return_text_in_logprobs: Optional[bool] = None,
|
| 92 |
+
dtype: Optional[Union[type, str]] = None,
|
| 93 |
+
choices: Optional[List[str]] = None,
|
| 94 |
+
choices_method: Optional[ChoicesSamplingMethod] = None,
|
| 95 |
+
regex: Optional[str] = None,
|
| 96 |
+
json_schema: Optional[str] = None,
|
| 97 |
+
):
|
| 98 |
+
"""Call the model to generate. See the meaning of the arguments in docs/backend/sampling_params.md"""
|
| 99 |
+
|
| 100 |
+
if choices:
|
| 101 |
+
return SglSelect(
|
| 102 |
+
name,
|
| 103 |
+
choices,
|
| 104 |
+
0.0 if temperature is None else temperature,
|
| 105 |
+
token_length_normalized if choices_method is None else choices_method,
|
| 106 |
+
)
|
| 107 |
+
|
| 108 |
+
# check regex is valid
|
| 109 |
+
if regex is not None:
|
| 110 |
+
try:
|
| 111 |
+
re.compile(regex)
|
| 112 |
+
except re.error as e:
|
| 113 |
+
raise e
|
| 114 |
+
|
| 115 |
+
return SglGen(
|
| 116 |
+
name,
|
| 117 |
+
max_tokens,
|
| 118 |
+
min_tokens,
|
| 119 |
+
n,
|
| 120 |
+
stop,
|
| 121 |
+
stop_token_ids,
|
| 122 |
+
temperature,
|
| 123 |
+
top_p,
|
| 124 |
+
top_k,
|
| 125 |
+
min_p,
|
| 126 |
+
frequency_penalty,
|
| 127 |
+
presence_penalty,
|
| 128 |
+
ignore_eos,
|
| 129 |
+
return_logprob,
|
| 130 |
+
logprob_start_len,
|
| 131 |
+
top_logprobs_num,
|
| 132 |
+
return_text_in_logprobs,
|
| 133 |
+
dtype,
|
| 134 |
+
regex,
|
| 135 |
+
json_schema,
|
| 136 |
+
)
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
def gen_int(
|
| 140 |
+
name: Optional[str] = None,
|
| 141 |
+
max_tokens: Optional[int] = None,
|
| 142 |
+
n: Optional[int] = None,
|
| 143 |
+
stop: Optional[Union[str, List[str]]] = None,
|
| 144 |
+
stop_token_ids: Optional[List[int]] = None,
|
| 145 |
+
temperature: Optional[float] = None,
|
| 146 |
+
top_p: Optional[float] = None,
|
| 147 |
+
top_k: Optional[int] = None,
|
| 148 |
+
min_p: Optional[float] = None,
|
| 149 |
+
frequency_penalty: Optional[float] = None,
|
| 150 |
+
presence_penalty: Optional[float] = None,
|
| 151 |
+
ignore_eos: Optional[bool] = None,
|
| 152 |
+
return_logprob: Optional[bool] = None,
|
| 153 |
+
logprob_start_len: Optional[int] = None,
|
| 154 |
+
top_logprobs_num: Optional[int] = None,
|
| 155 |
+
return_text_in_logprobs: Optional[bool] = None,
|
| 156 |
+
):
|
| 157 |
+
return SglGen(
|
| 158 |
+
name,
|
| 159 |
+
max_tokens,
|
| 160 |
+
None,
|
| 161 |
+
n,
|
| 162 |
+
stop,
|
| 163 |
+
stop_token_ids,
|
| 164 |
+
temperature,
|
| 165 |
+
top_p,
|
| 166 |
+
top_k,
|
| 167 |
+
min_p,
|
| 168 |
+
frequency_penalty,
|
| 169 |
+
presence_penalty,
|
| 170 |
+
ignore_eos,
|
| 171 |
+
return_logprob,
|
| 172 |
+
logprob_start_len,
|
| 173 |
+
top_logprobs_num,
|
| 174 |
+
return_text_in_logprobs,
|
| 175 |
+
int,
|
| 176 |
+
None,
|
| 177 |
+
)
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
def gen_string(
|
| 181 |
+
name: Optional[str] = None,
|
| 182 |
+
max_tokens: Optional[int] = None,
|
| 183 |
+
n: Optional[int] = None,
|
| 184 |
+
stop: Optional[Union[str, List[str]]] = None,
|
| 185 |
+
stop_token_ids: Optional[List[int]] = None,
|
| 186 |
+
temperature: Optional[float] = None,
|
| 187 |
+
top_p: Optional[float] = None,
|
| 188 |
+
top_k: Optional[int] = None,
|
| 189 |
+
min_p: Optional[float] = None,
|
| 190 |
+
frequency_penalty: Optional[float] = None,
|
| 191 |
+
presence_penalty: Optional[float] = None,
|
| 192 |
+
ignore_eos: Optional[bool] = None,
|
| 193 |
+
return_logprob: Optional[bool] = None,
|
| 194 |
+
logprob_start_len: Optional[int] = None,
|
| 195 |
+
top_logprobs_num: Optional[int] = None,
|
| 196 |
+
return_text_in_logprobs: Optional[bool] = None,
|
| 197 |
+
):
|
| 198 |
+
return SglGen(
|
| 199 |
+
name,
|
| 200 |
+
max_tokens,
|
| 201 |
+
None,
|
| 202 |
+
n,
|
| 203 |
+
stop,
|
| 204 |
+
stop_token_ids,
|
| 205 |
+
temperature,
|
| 206 |
+
top_p,
|
| 207 |
+
top_k,
|
| 208 |
+
min_p,
|
| 209 |
+
frequency_penalty,
|
| 210 |
+
presence_penalty,
|
| 211 |
+
ignore_eos,
|
| 212 |
+
return_logprob,
|
| 213 |
+
logprob_start_len,
|
| 214 |
+
top_logprobs_num,
|
| 215 |
+
return_text_in_logprobs,
|
| 216 |
+
str,
|
| 217 |
+
None,
|
| 218 |
+
)
|
| 219 |
+
|
| 220 |
+
|
| 221 |
+
def image(expr: SglExpr):
|
| 222 |
+
return SglImage(expr)
|
| 223 |
+
|
| 224 |
+
|
| 225 |
+
def video(path: str, num_frames: int):
|
| 226 |
+
return SglVideo(path, num_frames)
|
| 227 |
+
|
| 228 |
+
|
| 229 |
+
def select(
|
| 230 |
+
name: Optional[str] = None,
|
| 231 |
+
choices: Optional[List[str]] = None,
|
| 232 |
+
temperature: float = 0.0,
|
| 233 |
+
choices_method: ChoicesSamplingMethod = token_length_normalized,
|
| 234 |
+
):
|
| 235 |
+
assert choices is not None
|
| 236 |
+
return SglSelect(name, choices, temperature, choices_method)
|
| 237 |
+
|
| 238 |
+
|
| 239 |
+
def _role_common(name: str, expr: Optional[SglExpr] = None):
|
| 240 |
+
if expr is None:
|
| 241 |
+
return SglExprList([SglRoleBegin(name), SglRoleEnd(name)])
|
| 242 |
+
else:
|
| 243 |
+
return SglExprList([SglRoleBegin(name), expr, SglRoleEnd(name)])
|
| 244 |
+
|
| 245 |
+
|
| 246 |
+
def system(expr: Optional[SglExpr] = None):
|
| 247 |
+
return _role_common("system", expr)
|
| 248 |
+
|
| 249 |
+
|
| 250 |
+
def user(expr: Optional[SglExpr] = None):
|
| 251 |
+
return _role_common("user", expr)
|
| 252 |
+
|
| 253 |
+
|
| 254 |
+
def assistant(expr: Optional[SglExpr] = None):
|
| 255 |
+
return _role_common("assistant", expr)
|
| 256 |
+
|
| 257 |
+
|
| 258 |
+
def system_begin():
|
| 259 |
+
return SglRoleBegin("system")
|
| 260 |
+
|
| 261 |
+
|
| 262 |
+
def system_end():
|
| 263 |
+
return SglRoleEnd("system")
|
| 264 |
+
|
| 265 |
+
|
| 266 |
+
def user_begin():
|
| 267 |
+
return SglRoleBegin("user")
|
| 268 |
+
|
| 269 |
+
|
| 270 |
+
def user_end():
|
| 271 |
+
return SglRoleEnd("user")
|
| 272 |
+
|
| 273 |
+
|
| 274 |
+
def assistant_begin():
|
| 275 |
+
return SglRoleBegin("assistant")
|
| 276 |
+
|
| 277 |
+
|
| 278 |
+
def assistant_end():
|
| 279 |
+
return SglRoleEnd("assistant")
|
lib/python3.12/site-packages/sglang/bench_offline_throughput.py
ADDED
|
@@ -0,0 +1,424 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Benchmark the throughput in the offline mode.
|
| 3 |
+
It accepts server arguments (the same as launch_server.py) and benchmark arguments (the same as bench_serving.py).
|
| 4 |
+
|
| 5 |
+
# Usage
|
| 6 |
+
## Sharegpt dataset with default args
|
| 7 |
+
python -m sglang.bench_offline_throughput --model-path meta-llama/Meta-Llama-3.1-8B-Instruct --num-prompts 10
|
| 8 |
+
|
| 9 |
+
## Random dataset with default args
|
| 10 |
+
python -m sglang.bench_offline_throughput --model-path meta-llama/Meta-Llama-3.1-8B-Instruct --dataset-name random --random-input 1024 --random-output 1024
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
import argparse
|
| 14 |
+
import dataclasses
|
| 15 |
+
import json
|
| 16 |
+
import logging
|
| 17 |
+
import os
|
| 18 |
+
import random
|
| 19 |
+
import time
|
| 20 |
+
from typing import Dict, List, Optional, Tuple
|
| 21 |
+
|
| 22 |
+
import numpy as np
|
| 23 |
+
|
| 24 |
+
from sglang.bench_serving import (
|
| 25 |
+
get_dataset,
|
| 26 |
+
get_tokenizer,
|
| 27 |
+
sample_random_requests,
|
| 28 |
+
set_ulimit,
|
| 29 |
+
)
|
| 30 |
+
from sglang.lang.backend.runtime_endpoint import Runtime
|
| 31 |
+
from sglang.srt.entrypoints.engine import Engine
|
| 32 |
+
from sglang.srt.server_args import ServerArgs
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
@dataclasses.dataclass
|
| 36 |
+
class BenchArgs:
|
| 37 |
+
backend: str = "engine"
|
| 38 |
+
result_filename: str = ""
|
| 39 |
+
dataset_name: str = "sharegpt"
|
| 40 |
+
dataset_path: str = ""
|
| 41 |
+
num_prompts: int = 1000
|
| 42 |
+
sharegpt_output_len: Optional[int] = None
|
| 43 |
+
sharegpt_context_len: Optional[int] = None
|
| 44 |
+
random_input_len: int = 1024
|
| 45 |
+
random_output_len: int = 1024
|
| 46 |
+
random_range_ratio: float = 0.0
|
| 47 |
+
gsp_num_groups: int = 64
|
| 48 |
+
gsp_prompts_per_group: int = 16
|
| 49 |
+
gsp_system_prompt_len: int = 2048
|
| 50 |
+
gsp_question_len: int = 128
|
| 51 |
+
gsp_output_len: int = 256
|
| 52 |
+
seed: int = 1
|
| 53 |
+
disable_ignore_eos: bool = False
|
| 54 |
+
extra_request_body: Optional[str] = None
|
| 55 |
+
apply_chat_template: bool = False
|
| 56 |
+
profile: bool = False
|
| 57 |
+
skip_warmup: bool = False
|
| 58 |
+
do_not_exit: bool = False
|
| 59 |
+
prompt_suffix: str = ""
|
| 60 |
+
|
| 61 |
+
@staticmethod
|
| 62 |
+
def add_cli_args(parser: argparse.ArgumentParser):
|
| 63 |
+
parser.add_argument("--backend", type=str, default=BenchArgs.backend)
|
| 64 |
+
parser.add_argument(
|
| 65 |
+
"--result-filename", type=str, default=BenchArgs.result_filename
|
| 66 |
+
)
|
| 67 |
+
parser.add_argument(
|
| 68 |
+
"--dataset-name",
|
| 69 |
+
type=str,
|
| 70 |
+
default="sharegpt",
|
| 71 |
+
choices=["sharegpt", "random", "generated-shared-prefix"],
|
| 72 |
+
help="Name of the dataset to benchmark on.",
|
| 73 |
+
)
|
| 74 |
+
parser.add_argument(
|
| 75 |
+
"--dataset-path", type=str, default="", help="Path to the dataset."
|
| 76 |
+
)
|
| 77 |
+
parser.add_argument(
|
| 78 |
+
"--num-prompts",
|
| 79 |
+
type=int,
|
| 80 |
+
default=BenchArgs.num_prompts,
|
| 81 |
+
help="Number of prompts to process. Default is 1000.",
|
| 82 |
+
)
|
| 83 |
+
parser.add_argument(
|
| 84 |
+
"--sharegpt-output-len",
|
| 85 |
+
type=int,
|
| 86 |
+
default=BenchArgs.sharegpt_output_len,
|
| 87 |
+
help="Output length for each request. Overrides the output length from the ShareGPT dataset.",
|
| 88 |
+
)
|
| 89 |
+
parser.add_argument(
|
| 90 |
+
"--sharegpt-context-len",
|
| 91 |
+
type=int,
|
| 92 |
+
default=BenchArgs.sharegpt_context_len,
|
| 93 |
+
help="The context length of the model for the ShareGPT dataset. Requests longer than the context length will be dropped.",
|
| 94 |
+
)
|
| 95 |
+
parser.add_argument(
|
| 96 |
+
"--random-input-len",
|
| 97 |
+
type=int,
|
| 98 |
+
default=BenchArgs.random_input_len,
|
| 99 |
+
help="Number of input tokens per request, used only for random dataset.",
|
| 100 |
+
)
|
| 101 |
+
parser.add_argument(
|
| 102 |
+
"--random-output-len",
|
| 103 |
+
type=int,
|
| 104 |
+
default=BenchArgs.random_output_len,
|
| 105 |
+
help="Number of output tokens per request, used only for random dataset.",
|
| 106 |
+
)
|
| 107 |
+
parser.add_argument(
|
| 108 |
+
"--random-range-ratio",
|
| 109 |
+
type=float,
|
| 110 |
+
default=BenchArgs.random_range_ratio,
|
| 111 |
+
help="Range of sampled ratio of input/output length, "
|
| 112 |
+
"used only for random dataset.",
|
| 113 |
+
)
|
| 114 |
+
parser.add_argument(
|
| 115 |
+
"--gsp-num-groups",
|
| 116 |
+
type=int,
|
| 117 |
+
default=BenchArgs.gsp_num_groups,
|
| 118 |
+
help="Number of groups with shared prefix, used"
|
| 119 |
+
"only for generate-shared-prefix",
|
| 120 |
+
)
|
| 121 |
+
parser.add_argument(
|
| 122 |
+
"--gsp-prompts-per-group",
|
| 123 |
+
type=int,
|
| 124 |
+
default=BenchArgs.gsp_prompts_per_group,
|
| 125 |
+
help="Number of prompts per group of shared prefix, used"
|
| 126 |
+
"only for generate-shared-prefix",
|
| 127 |
+
)
|
| 128 |
+
parser.add_argument(
|
| 129 |
+
"--gsp-system-prompt-len",
|
| 130 |
+
type=int,
|
| 131 |
+
default=BenchArgs.gsp_system_prompt_len,
|
| 132 |
+
help="System prompt length, used" "only for generate-shared-prefix",
|
| 133 |
+
)
|
| 134 |
+
parser.add_argument(
|
| 135 |
+
"--gsp-question-len",
|
| 136 |
+
type=int,
|
| 137 |
+
default=BenchArgs.gsp_question_len,
|
| 138 |
+
help="Question length, used" "only for generate-shared-prefix",
|
| 139 |
+
)
|
| 140 |
+
parser.add_argument(
|
| 141 |
+
"--gsp-output-len",
|
| 142 |
+
type=int,
|
| 143 |
+
default=BenchArgs.gsp_output_len,
|
| 144 |
+
help="Target length in tokens for outputs in generated-shared-prefix dataset",
|
| 145 |
+
)
|
| 146 |
+
parser.add_argument("--seed", type=int, default=1, help="The random seed.")
|
| 147 |
+
parser.add_argument(
|
| 148 |
+
"--disable-ignore-eos",
|
| 149 |
+
action="store_true",
|
| 150 |
+
help="Disable ignore EOS token",
|
| 151 |
+
)
|
| 152 |
+
parser.add_argument(
|
| 153 |
+
"--extra-request-body",
|
| 154 |
+
metavar='{"key1": "value1", "key2": "value2"}',
|
| 155 |
+
type=str,
|
| 156 |
+
default=BenchArgs.extra_request_body,
|
| 157 |
+
help="Append given JSON object to the request payload. You can use this to specify"
|
| 158 |
+
"additional generate params like sampling params.",
|
| 159 |
+
)
|
| 160 |
+
parser.add_argument(
|
| 161 |
+
"--apply-chat-template",
|
| 162 |
+
action="store_true",
|
| 163 |
+
help="Apply chat template",
|
| 164 |
+
)
|
| 165 |
+
parser.add_argument(
|
| 166 |
+
"--profile",
|
| 167 |
+
action="store_true",
|
| 168 |
+
help="Use Torch Profiler. The endpoint must be launched with "
|
| 169 |
+
"SGLANG_TORCH_PROFILER_DIR to enable profiler.",
|
| 170 |
+
)
|
| 171 |
+
parser.add_argument(
|
| 172 |
+
"--skip-warmup",
|
| 173 |
+
action="store_true",
|
| 174 |
+
help="Skip the warmup batches.",
|
| 175 |
+
)
|
| 176 |
+
parser.add_argument(
|
| 177 |
+
"--do-not-exit",
|
| 178 |
+
action="store_true",
|
| 179 |
+
help="Do not exit the program. This is useful for nsys profile with --duration and --delay.",
|
| 180 |
+
)
|
| 181 |
+
parser.add_argument(
|
| 182 |
+
"--prompt-suffix",
|
| 183 |
+
type=str,
|
| 184 |
+
default="",
|
| 185 |
+
help="Suffix applied to the end of all user prompts, followed by assistant prompt suffix.",
|
| 186 |
+
)
|
| 187 |
+
|
| 188 |
+
@classmethod
|
| 189 |
+
def from_cli_args(cls, args: argparse.Namespace):
|
| 190 |
+
attrs = [attr.name for attr in dataclasses.fields(cls)]
|
| 191 |
+
return cls(**{attr: getattr(args, attr) for attr in attrs})
|
| 192 |
+
|
| 193 |
+
|
| 194 |
+
def throughput_test_once(
|
| 195 |
+
backend_name: str,
|
| 196 |
+
backend,
|
| 197 |
+
reqs: List[Tuple[str, int, int]],
|
| 198 |
+
ignore_eos: bool,
|
| 199 |
+
extra_request_body: Dict,
|
| 200 |
+
profile: bool,
|
| 201 |
+
):
|
| 202 |
+
measurement_results = {
|
| 203 |
+
"backend": backend_name,
|
| 204 |
+
"successful_requests": len(reqs),
|
| 205 |
+
"total_latency": -1,
|
| 206 |
+
"total_input_tokens": sum(r[1] for r in reqs),
|
| 207 |
+
"total_output_tokens": -1,
|
| 208 |
+
"request_throughput": -1,
|
| 209 |
+
"input_throughput": -1,
|
| 210 |
+
"output_throughput": -1,
|
| 211 |
+
"total_throughput": -1,
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
prompt = [r[0] for r in reqs]
|
| 215 |
+
sampling_params = [
|
| 216 |
+
{
|
| 217 |
+
"temperature": 0,
|
| 218 |
+
"max_new_tokens": r[2],
|
| 219 |
+
"ignore_eos": ignore_eos,
|
| 220 |
+
**extra_request_body,
|
| 221 |
+
}
|
| 222 |
+
for r in reqs
|
| 223 |
+
]
|
| 224 |
+
|
| 225 |
+
if profile:
|
| 226 |
+
assert (
|
| 227 |
+
"SGLANG_TORCH_PROFILER_DIR" in os.environ
|
| 228 |
+
), "Please set SGLANG_TORCH_PROFILER_DIR."
|
| 229 |
+
os.makedirs(os.environ["SGLANG_TORCH_PROFILER_DIR"], exist_ok=True)
|
| 230 |
+
backend.start_profile()
|
| 231 |
+
|
| 232 |
+
st = time.perf_counter()
|
| 233 |
+
gen_out = backend.generate(prompt=prompt, sampling_params=sampling_params)
|
| 234 |
+
latency = time.perf_counter() - st
|
| 235 |
+
|
| 236 |
+
if profile:
|
| 237 |
+
backend.stop_profile()
|
| 238 |
+
monitor_trace_file(os.getenv("SGLANG_TORCH_PROFILER_DIR"))
|
| 239 |
+
|
| 240 |
+
if backend_name == "runtime":
|
| 241 |
+
gen_out = json.loads(gen_out)
|
| 242 |
+
|
| 243 |
+
server_info = backend.get_server_info()
|
| 244 |
+
|
| 245 |
+
measurement_results["total_latency"] = latency
|
| 246 |
+
measurement_results["total_output_tokens"] = sum(
|
| 247 |
+
o["meta_info"]["completion_tokens"] for o in gen_out
|
| 248 |
+
)
|
| 249 |
+
measurement_results["request_throughput"] = (
|
| 250 |
+
measurement_results["successful_requests"] / latency
|
| 251 |
+
)
|
| 252 |
+
measurement_results["input_throughput"] = (
|
| 253 |
+
measurement_results["total_input_tokens"] / latency
|
| 254 |
+
)
|
| 255 |
+
measurement_results["output_throughput"] = (
|
| 256 |
+
measurement_results["total_output_tokens"] / latency
|
| 257 |
+
)
|
| 258 |
+
measurement_results["total_throughput"] = (
|
| 259 |
+
measurement_results["total_input_tokens"]
|
| 260 |
+
+ measurement_results["total_output_tokens"]
|
| 261 |
+
) / latency
|
| 262 |
+
measurement_results["last_gen_throughput"] = server_info["last_gen_throughput"]
|
| 263 |
+
|
| 264 |
+
return measurement_results
|
| 265 |
+
|
| 266 |
+
|
| 267 |
+
def monitor_trace_file(directory, interval=1):
|
| 268 |
+
|
| 269 |
+
print(f"Monitoring {directory} for new trace files...")
|
| 270 |
+
|
| 271 |
+
known_files = set(os.listdir(directory))
|
| 272 |
+
|
| 273 |
+
while True:
|
| 274 |
+
flag = False
|
| 275 |
+
time.sleep(interval)
|
| 276 |
+
current_files = set(os.listdir(directory))
|
| 277 |
+
|
| 278 |
+
new_files = current_files - known_files
|
| 279 |
+
for new_file in new_files:
|
| 280 |
+
new_file_path = os.path.join(directory, new_file)
|
| 281 |
+
print(f"New file detected: {new_file}")
|
| 282 |
+
|
| 283 |
+
previous_size = 0
|
| 284 |
+
while True:
|
| 285 |
+
try:
|
| 286 |
+
current_size = os.path.getsize(new_file_path)
|
| 287 |
+
except FileNotFoundError:
|
| 288 |
+
print(f"File {new_file} is no longer accessible.")
|
| 289 |
+
break
|
| 290 |
+
|
| 291 |
+
if current_size > previous_size:
|
| 292 |
+
previous_size = current_size
|
| 293 |
+
else:
|
| 294 |
+
flag = True
|
| 295 |
+
break
|
| 296 |
+
|
| 297 |
+
time.sleep(interval)
|
| 298 |
+
if flag:
|
| 299 |
+
break
|
| 300 |
+
|
| 301 |
+
|
| 302 |
+
def throughput_test(
|
| 303 |
+
server_args: ServerArgs,
|
| 304 |
+
bench_args: BenchArgs,
|
| 305 |
+
):
|
| 306 |
+
if bench_args.backend == "engine":
|
| 307 |
+
backend = Engine(**dataclasses.asdict(server_args))
|
| 308 |
+
if not backend:
|
| 309 |
+
raise ValueError("Please provide valid engine arguments")
|
| 310 |
+
elif bench_args.backend == "runtime":
|
| 311 |
+
backend = Runtime(**dataclasses.asdict(server_args))
|
| 312 |
+
else:
|
| 313 |
+
raise ValueError('Please set backend to either "engine" or "runtime"')
|
| 314 |
+
|
| 315 |
+
tokenizer_id = server_args.tokenizer_path or server_args.model_path
|
| 316 |
+
tokenizer = get_tokenizer(tokenizer_id)
|
| 317 |
+
|
| 318 |
+
# Set global environmnets
|
| 319 |
+
set_ulimit()
|
| 320 |
+
random.seed(bench_args.seed)
|
| 321 |
+
np.random.seed(bench_args.seed)
|
| 322 |
+
|
| 323 |
+
# Parse args
|
| 324 |
+
extra_request_body = {}
|
| 325 |
+
if bench_args.extra_request_body:
|
| 326 |
+
extra_request_body = json.loads(args.extra_request_body)
|
| 327 |
+
|
| 328 |
+
# Read dataset
|
| 329 |
+
input_requests = get_dataset(bench_args, tokenizer)
|
| 330 |
+
|
| 331 |
+
warmup_requests = sample_random_requests(
|
| 332 |
+
input_len=256,
|
| 333 |
+
output_len=16,
|
| 334 |
+
num_prompts=min(bench_args.num_prompts, 16),
|
| 335 |
+
range_ratio=1.0,
|
| 336 |
+
tokenizer=tokenizer,
|
| 337 |
+
dataset_path=bench_args.dataset_path,
|
| 338 |
+
)
|
| 339 |
+
|
| 340 |
+
# Warm up
|
| 341 |
+
if not bench_args.skip_warmup:
|
| 342 |
+
logging.info("\nWarmup...")
|
| 343 |
+
throughput_test_once(
|
| 344 |
+
backend_name=bench_args.backend,
|
| 345 |
+
backend=backend,
|
| 346 |
+
reqs=warmup_requests,
|
| 347 |
+
ignore_eos=not bench_args.disable_ignore_eos,
|
| 348 |
+
extra_request_body=extra_request_body,
|
| 349 |
+
profile=False,
|
| 350 |
+
)
|
| 351 |
+
time.sleep(0.5)
|
| 352 |
+
|
| 353 |
+
logging.info("\nBenchmark...")
|
| 354 |
+
result = throughput_test_once(
|
| 355 |
+
backend_name=bench_args.backend,
|
| 356 |
+
backend=backend,
|
| 357 |
+
reqs=input_requests,
|
| 358 |
+
ignore_eos=not bench_args.disable_ignore_eos,
|
| 359 |
+
extra_request_body=extra_request_body,
|
| 360 |
+
profile=bench_args.profile,
|
| 361 |
+
)
|
| 362 |
+
backend.shutdown()
|
| 363 |
+
|
| 364 |
+
if bench_args.result_filename:
|
| 365 |
+
with open(bench_args.result_filename, "a") as fout:
|
| 366 |
+
fout.write(json.dumps(result) + "\n")
|
| 367 |
+
|
| 368 |
+
print(
|
| 369 |
+
"\n{s:{c}^{n}}".format(s=" Offline Throughput Benchmark Result ", n=50, c="=")
|
| 370 |
+
)
|
| 371 |
+
print("{:<40} {:<10}".format("Backend:", result["backend"]))
|
| 372 |
+
print("{:<40} {:<10}".format("Successful requests:", result["successful_requests"]))
|
| 373 |
+
print("{:<40} {:<10.2f}".format("Benchmark duration (s):", result["total_latency"]))
|
| 374 |
+
print("{:<40} {:<10}".format("Total input tokens:", result["total_input_tokens"]))
|
| 375 |
+
print(
|
| 376 |
+
"{:<40} {:<10}".format("Total generated tokens:", result["total_output_tokens"])
|
| 377 |
+
)
|
| 378 |
+
print(
|
| 379 |
+
"{:<40} {:<10.2f}".format(
|
| 380 |
+
"Last generation throughput (tok/s):", result["last_gen_throughput"]
|
| 381 |
+
)
|
| 382 |
+
)
|
| 383 |
+
print(
|
| 384 |
+
"{:<40} {:<10.2f}".format(
|
| 385 |
+
"Request throughput (req/s):", result["request_throughput"]
|
| 386 |
+
)
|
| 387 |
+
)
|
| 388 |
+
print(
|
| 389 |
+
"{:<40} {:<10.2f}".format(
|
| 390 |
+
"Input token throughput (tok/s):", result["input_throughput"]
|
| 391 |
+
)
|
| 392 |
+
)
|
| 393 |
+
print(
|
| 394 |
+
"{:<40} {:<10.2f}".format(
|
| 395 |
+
"Output token throughput (tok/s):", result["output_throughput"]
|
| 396 |
+
)
|
| 397 |
+
)
|
| 398 |
+
print(
|
| 399 |
+
"{:<40} {:<10.2f}".format(
|
| 400 |
+
"Total token throughput (tok/s):", result["total_throughput"]
|
| 401 |
+
)
|
| 402 |
+
)
|
| 403 |
+
print("=" * 50)
|
| 404 |
+
|
| 405 |
+
return result
|
| 406 |
+
|
| 407 |
+
|
| 408 |
+
if __name__ == "__main__":
|
| 409 |
+
parser = argparse.ArgumentParser()
|
| 410 |
+
ServerArgs.add_cli_args(parser)
|
| 411 |
+
BenchArgs.add_cli_args(parser)
|
| 412 |
+
args = parser.parse_args()
|
| 413 |
+
server_args = ServerArgs.from_cli_args(args)
|
| 414 |
+
bench_args = BenchArgs.from_cli_args(args)
|
| 415 |
+
|
| 416 |
+
logging.basicConfig(
|
| 417 |
+
level=getattr(logging, server_args.log_level.upper()),
|
| 418 |
+
format="%(message)s",
|
| 419 |
+
)
|
| 420 |
+
|
| 421 |
+
throughput_test(server_args, bench_args)
|
| 422 |
+
|
| 423 |
+
while bench_args.do_not_exit:
|
| 424 |
+
pass
|
lib/python3.12/site-packages/sglang/bench_one_batch.py
ADDED
|
@@ -0,0 +1,568 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Benchmark the latency of running a single static batch without a server.
|
| 3 |
+
|
| 4 |
+
This script does not launch a server and uses the low-level APIs.
|
| 5 |
+
It accepts server arguments (the same as launch_server.py) and benchmark arguments (e.g., batch size, input lengths).
|
| 6 |
+
|
| 7 |
+
# Usage (latency test)
|
| 8 |
+
## with dummy weights:
|
| 9 |
+
python -m sglang.bench_one_batch --model-path meta-llama/Meta-Llama-3-8B-Instruct --load-format dummy
|
| 10 |
+
## sweep through multiple data points and store (append) the results in a jsonl file:
|
| 11 |
+
python -m sglang.bench_one_batch --model-path meta-llama/Meta-Llama-3-8B-Instruct --batch 1 12 14 --input-len 256 512 --output-len 32 256 --run-name test_run
|
| 12 |
+
## run with profiling:
|
| 13 |
+
python -m sglang.bench_one_batch --model-path meta-llama/Meta-Llama-3-8B-Instruct --batch 1 12 14 --input-len 256 512 --profile
|
| 14 |
+
# Usage (correctness test):
|
| 15 |
+
python -m sglang.bench_one_batch --model-path TinyLlama/TinyLlama-1.1B-Chat-v0.4 --correct
|
| 16 |
+
|
| 17 |
+
## Reference output (of the correctness test above, can be gpu dependent):
|
| 18 |
+
input_ids=[[1, 450, 7483, 310, 3444, 338], [1, 450, 7483, 310, 278, 3303, 13187, 290, 338], [1, 20628, 338, 263, 6575, 1460, 2462, 322, 306, 763]]
|
| 19 |
+
|
| 20 |
+
prefill logits (first half): tensor([[-10.0312, -9.5000, 0.8931, ..., -4.9414, -3.2422, -3.3633],
|
| 21 |
+
[-10.0312, -9.5000, 0.8931, ..., -4.9414, -3.2422, -3.3633],
|
| 22 |
+
[ -9.1875, -10.2500, 2.7129, ..., -4.3359, -4.0664, -4.1328]],
|
| 23 |
+
device='cuda:0')
|
| 24 |
+
|
| 25 |
+
prefill logits (final): tensor([[-8.3125, -7.1172, 3.3457, ..., -4.9570, -4.1328, -3.4141],
|
| 26 |
+
[-8.9141, -9.0156, 4.1445, ..., -4.9922, -4.4961, -4.0781],
|
| 27 |
+
[-9.6328, -9.0547, 4.0195, ..., -5.3047, -4.7148, -4.4570]],
|
| 28 |
+
device='cuda:0')
|
| 29 |
+
|
| 30 |
+
========== Prompt 0 ==========
|
| 31 |
+
<s> The capital of France is Paris.
|
| 32 |
+
The capital of the United States is Washington, D.C.
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
========== Prompt 1 ==========
|
| 36 |
+
<s> The capital of the United Kindom is London.
|
| 37 |
+
The capital of the United Kingdom is London.
|
| 38 |
+
The capital of the
|
| 39 |
+
|
| 40 |
+
========== Prompt 2 ==========
|
| 41 |
+
<s> Today is a sunny day and I like to go for a walk in the park.
|
| 42 |
+
I'm going to the park
|
| 43 |
+
"""
|
| 44 |
+
|
| 45 |
+
import argparse
|
| 46 |
+
import dataclasses
|
| 47 |
+
import itertools
|
| 48 |
+
import json
|
| 49 |
+
import logging
|
| 50 |
+
import multiprocessing
|
| 51 |
+
import os
|
| 52 |
+
import time
|
| 53 |
+
from typing import Tuple
|
| 54 |
+
|
| 55 |
+
import numpy as np
|
| 56 |
+
import torch
|
| 57 |
+
import torch.distributed as dist
|
| 58 |
+
|
| 59 |
+
from sglang.srt.configs.model_config import ModelConfig
|
| 60 |
+
from sglang.srt.distributed.parallel_state import destroy_distributed_environment
|
| 61 |
+
from sglang.srt.entrypoints.engine import _set_envs_and_config
|
| 62 |
+
from sglang.srt.hf_transformers_utils import get_tokenizer
|
| 63 |
+
from sglang.srt.managers.schedule_batch import Req, ScheduleBatch
|
| 64 |
+
from sglang.srt.managers.scheduler import Scheduler
|
| 65 |
+
from sglang.srt.model_executor.forward_batch_info import ForwardBatch
|
| 66 |
+
from sglang.srt.model_executor.model_runner import ModelRunner
|
| 67 |
+
from sglang.srt.sampling.sampling_params import SamplingParams
|
| 68 |
+
from sglang.srt.server_args import PortArgs, ServerArgs
|
| 69 |
+
from sglang.srt.speculative.spec_info import SpeculativeAlgorithm
|
| 70 |
+
from sglang.srt.utils import (
|
| 71 |
+
configure_logger,
|
| 72 |
+
get_bool_env_var,
|
| 73 |
+
kill_process_tree,
|
| 74 |
+
set_gpu_proc_affinity,
|
| 75 |
+
suppress_other_loggers,
|
| 76 |
+
)
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
@dataclasses.dataclass
|
| 80 |
+
class BenchArgs:
|
| 81 |
+
run_name: str = "default"
|
| 82 |
+
batch_size: Tuple[int] = (1,)
|
| 83 |
+
input_len: Tuple[int] = (1024,)
|
| 84 |
+
output_len: Tuple[int] = (16,)
|
| 85 |
+
result_filename: str = "result.jsonl"
|
| 86 |
+
correctness_test: bool = False
|
| 87 |
+
# This is only used for correctness test
|
| 88 |
+
cut_len: int = 4
|
| 89 |
+
log_decode_step: int = 0
|
| 90 |
+
profile: bool = False
|
| 91 |
+
profile_filename_prefix: str = "profile"
|
| 92 |
+
|
| 93 |
+
@staticmethod
|
| 94 |
+
def add_cli_args(parser: argparse.ArgumentParser):
|
| 95 |
+
parser.add_argument("--run-name", type=str, default=BenchArgs.run_name)
|
| 96 |
+
parser.add_argument(
|
| 97 |
+
"--batch-size", type=int, nargs="+", default=BenchArgs.batch_size
|
| 98 |
+
)
|
| 99 |
+
parser.add_argument(
|
| 100 |
+
"--input-len", type=int, nargs="+", default=BenchArgs.input_len
|
| 101 |
+
)
|
| 102 |
+
parser.add_argument(
|
| 103 |
+
"--output-len", type=int, nargs="+", default=BenchArgs.output_len
|
| 104 |
+
)
|
| 105 |
+
parser.add_argument(
|
| 106 |
+
"--result-filename", type=str, default=BenchArgs.result_filename
|
| 107 |
+
)
|
| 108 |
+
parser.add_argument("--correctness-test", action="store_true")
|
| 109 |
+
parser.add_argument("--cut-len", type=int, default=BenchArgs.cut_len)
|
| 110 |
+
parser.add_argument(
|
| 111 |
+
"--log-decode-step",
|
| 112 |
+
type=int,
|
| 113 |
+
default=BenchArgs.log_decode_step,
|
| 114 |
+
help="Log decode latency by step, default is set to zero to disable.",
|
| 115 |
+
)
|
| 116 |
+
parser.add_argument(
|
| 117 |
+
"--profile", action="store_true", help="Use Torch Profiler."
|
| 118 |
+
)
|
| 119 |
+
parser.add_argument(
|
| 120 |
+
"--profile-filename-prefix",
|
| 121 |
+
type=str,
|
| 122 |
+
default=BenchArgs.profile_filename_prefix,
|
| 123 |
+
help="Prefix of the profiling file names. The full profiling result file(s) be "
|
| 124 |
+
'"[profile_filename_prefix]_batch[batch_size]_input[input_len]_output[output_len].trace.json.gz"',
|
| 125 |
+
)
|
| 126 |
+
|
| 127 |
+
@classmethod
|
| 128 |
+
def from_cli_args(cls, args: argparse.Namespace):
|
| 129 |
+
# use the default value's type to cast the args into correct types.
|
| 130 |
+
attrs = [(attr.name, type(attr.default)) for attr in dataclasses.fields(cls)]
|
| 131 |
+
return cls(
|
| 132 |
+
**{attr: attr_type(getattr(args, attr)) for attr, attr_type in attrs}
|
| 133 |
+
)
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
def load_model(server_args, port_args, tp_rank):
|
| 137 |
+
suppress_other_loggers()
|
| 138 |
+
rank_print = print if tp_rank == 0 else lambda *args, **kwargs: None
|
| 139 |
+
|
| 140 |
+
model_config = ModelConfig(
|
| 141 |
+
server_args.model_path,
|
| 142 |
+
trust_remote_code=server_args.trust_remote_code,
|
| 143 |
+
revision=server_args.revision,
|
| 144 |
+
context_length=server_args.context_length,
|
| 145 |
+
model_override_args=server_args.json_model_override_args,
|
| 146 |
+
is_embedding=server_args.is_embedding,
|
| 147 |
+
enable_multimodal=server_args.enable_multimodal,
|
| 148 |
+
dtype=server_args.dtype,
|
| 149 |
+
quantization=server_args.quantization,
|
| 150 |
+
)
|
| 151 |
+
model_runner = ModelRunner(
|
| 152 |
+
model_config=model_config,
|
| 153 |
+
mem_fraction_static=server_args.mem_fraction_static,
|
| 154 |
+
gpu_id=tp_rank,
|
| 155 |
+
tp_rank=tp_rank,
|
| 156 |
+
tp_size=server_args.tp_size,
|
| 157 |
+
nccl_port=port_args.nccl_port,
|
| 158 |
+
server_args=server_args,
|
| 159 |
+
)
|
| 160 |
+
rank_print(f"max_total_num_tokens={model_runner.max_total_num_tokens}")
|
| 161 |
+
tokenizer = get_tokenizer(
|
| 162 |
+
server_args.tokenizer_path,
|
| 163 |
+
tokenizer_mode=server_args.tokenizer_mode,
|
| 164 |
+
trust_remote_code=server_args.trust_remote_code,
|
| 165 |
+
)
|
| 166 |
+
if server_args.tp_size > 1:
|
| 167 |
+
dist.barrier()
|
| 168 |
+
return model_runner, tokenizer
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
def prepare_inputs_for_correctness_test(bench_args, tokenizer):
|
| 172 |
+
prompts = [
|
| 173 |
+
"The capital of France is",
|
| 174 |
+
"The capital of the United Kindom is",
|
| 175 |
+
"Today is a sunny day and I like",
|
| 176 |
+
]
|
| 177 |
+
input_ids = [tokenizer.encode(p) for p in prompts]
|
| 178 |
+
sampling_params = SamplingParams(
|
| 179 |
+
temperature=0,
|
| 180 |
+
max_new_tokens=BenchArgs.output_len,
|
| 181 |
+
)
|
| 182 |
+
|
| 183 |
+
reqs = []
|
| 184 |
+
for i in range(len(prompts)):
|
| 185 |
+
assert len(input_ids[i]) > bench_args.cut_len
|
| 186 |
+
|
| 187 |
+
tmp_input_ids = input_ids[i][: bench_args.cut_len]
|
| 188 |
+
req = Req(
|
| 189 |
+
rid=i,
|
| 190 |
+
origin_input_text=prompts[i],
|
| 191 |
+
origin_input_ids=tmp_input_ids,
|
| 192 |
+
sampling_params=sampling_params,
|
| 193 |
+
)
|
| 194 |
+
req.prefix_indices = []
|
| 195 |
+
req.fill_ids = req.origin_input_ids
|
| 196 |
+
req.extend_input_len = len(req.fill_ids) - len(req.prefix_indices)
|
| 197 |
+
req.logprob_start_len = len(req.origin_input_ids) - 1
|
| 198 |
+
reqs.append(req)
|
| 199 |
+
|
| 200 |
+
return input_ids, reqs
|
| 201 |
+
|
| 202 |
+
|
| 203 |
+
def prepare_extend_inputs_for_correctness_test(
|
| 204 |
+
bench_args, input_ids, reqs, model_runner
|
| 205 |
+
):
|
| 206 |
+
for i in range(len(reqs)):
|
| 207 |
+
req = reqs[i]
|
| 208 |
+
req.fill_ids += input_ids[i][bench_args.cut_len :]
|
| 209 |
+
req.prefix_indices = model_runner.req_to_token_pool.req_to_token[
|
| 210 |
+
i, : bench_args.cut_len
|
| 211 |
+
]
|
| 212 |
+
req.extend_input_len = len(req.fill_ids) - len(req.prefix_indices)
|
| 213 |
+
req.logprob_start_len = len(req.origin_input_ids) - 1
|
| 214 |
+
return reqs
|
| 215 |
+
|
| 216 |
+
|
| 217 |
+
def prepare_synthetic_inputs_for_latency_test(batch_size, input_len):
|
| 218 |
+
input_ids = np.random.randint(0, 10000, (batch_size, input_len), dtype=np.int32)
|
| 219 |
+
sampling_params = SamplingParams(
|
| 220 |
+
temperature=0,
|
| 221 |
+
max_new_tokens=BenchArgs.output_len,
|
| 222 |
+
)
|
| 223 |
+
|
| 224 |
+
reqs = []
|
| 225 |
+
for i in range(len(input_ids)):
|
| 226 |
+
req = Req(
|
| 227 |
+
rid=i,
|
| 228 |
+
origin_input_text="",
|
| 229 |
+
origin_input_ids=list(input_ids[i]),
|
| 230 |
+
sampling_params=sampling_params,
|
| 231 |
+
)
|
| 232 |
+
req.prefix_indices = []
|
| 233 |
+
req.fill_ids = req.origin_input_ids
|
| 234 |
+
req.extend_input_len = len(req.fill_ids) - len(req.prefix_indices)
|
| 235 |
+
req.logprob_start_len = len(req.origin_input_ids) - 1
|
| 236 |
+
reqs.append(req)
|
| 237 |
+
|
| 238 |
+
return reqs
|
| 239 |
+
|
| 240 |
+
|
| 241 |
+
@torch.no_grad
|
| 242 |
+
def extend(reqs, model_runner):
|
| 243 |
+
batch = ScheduleBatch.init_new(
|
| 244 |
+
reqs=reqs,
|
| 245 |
+
req_to_token_pool=model_runner.req_to_token_pool,
|
| 246 |
+
token_to_kv_pool_allocator=model_runner.token_to_kv_pool_allocator,
|
| 247 |
+
tree_cache=None,
|
| 248 |
+
model_config=model_runner.model_config,
|
| 249 |
+
enable_overlap=False,
|
| 250 |
+
spec_algorithm=SpeculativeAlgorithm.NONE,
|
| 251 |
+
enable_custom_logit_processor=False,
|
| 252 |
+
)
|
| 253 |
+
batch.prepare_for_extend()
|
| 254 |
+
_maybe_prepare_dp_attn_batch(batch, model_runner)
|
| 255 |
+
model_worker_batch = batch.get_model_worker_batch()
|
| 256 |
+
forward_batch = ForwardBatch.init_new(model_worker_batch, model_runner)
|
| 257 |
+
logits_output = model_runner.forward(forward_batch)
|
| 258 |
+
next_token_ids = model_runner.sample(logits_output, forward_batch)
|
| 259 |
+
return next_token_ids, logits_output.next_token_logits, batch
|
| 260 |
+
|
| 261 |
+
|
| 262 |
+
@torch.no_grad
|
| 263 |
+
def decode(input_token_ids, batch, model_runner):
|
| 264 |
+
batch.output_ids = input_token_ids
|
| 265 |
+
batch.prepare_for_decode()
|
| 266 |
+
_maybe_prepare_dp_attn_batch(batch, model_runner)
|
| 267 |
+
model_worker_batch = batch.get_model_worker_batch()
|
| 268 |
+
forward_batch = ForwardBatch.init_new(model_worker_batch, model_runner)
|
| 269 |
+
logits_output = model_runner.forward(forward_batch)
|
| 270 |
+
next_token_ids = model_runner.sample(logits_output, forward_batch)
|
| 271 |
+
return next_token_ids, logits_output.next_token_logits
|
| 272 |
+
|
| 273 |
+
|
| 274 |
+
def _maybe_prepare_dp_attn_batch(batch: ScheduleBatch, model_runner):
|
| 275 |
+
if model_runner.server_args.enable_dp_attention:
|
| 276 |
+
Scheduler.prepare_dp_attn_batch_raw(
|
| 277 |
+
batch,
|
| 278 |
+
dp_size=model_runner.server_args.dp_size,
|
| 279 |
+
attn_tp_size=1,
|
| 280 |
+
tp_cpu_group=model_runner.tp_group.cpu_group,
|
| 281 |
+
get_idle_batch=None,
|
| 282 |
+
disable_cuda_graph=model_runner.server_args.disable_cuda_graph,
|
| 283 |
+
spec_algorithm=SpeculativeAlgorithm.NONE,
|
| 284 |
+
speculative_num_draft_tokens=None,
|
| 285 |
+
)
|
| 286 |
+
|
| 287 |
+
|
| 288 |
+
def correctness_test(
|
| 289 |
+
server_args,
|
| 290 |
+
port_args,
|
| 291 |
+
bench_args,
|
| 292 |
+
tp_rank,
|
| 293 |
+
):
|
| 294 |
+
# Configure the logger
|
| 295 |
+
configure_logger(server_args, prefix=f" TP{tp_rank}")
|
| 296 |
+
rank_print = print if tp_rank == 0 else lambda *args, **kwargs: None
|
| 297 |
+
|
| 298 |
+
# Load the model
|
| 299 |
+
model_runner, tokenizer = load_model(server_args, port_args, tp_rank)
|
| 300 |
+
|
| 301 |
+
# Prepare inputs
|
| 302 |
+
input_ids, reqs = prepare_inputs_for_correctness_test(bench_args, tokenizer)
|
| 303 |
+
rank_print(f"\n{input_ids=}\n")
|
| 304 |
+
|
| 305 |
+
if bench_args.cut_len > 0:
|
| 306 |
+
# Prefill
|
| 307 |
+
next_token_ids, next_token_logits, batch = extend(reqs, model_runner)
|
| 308 |
+
rank_print(f"prefill logits (first half): {next_token_logits} \n")
|
| 309 |
+
|
| 310 |
+
# Prepare extend inputs
|
| 311 |
+
reqs = prepare_extend_inputs_for_correctness_test(
|
| 312 |
+
bench_args, input_ids, reqs, model_runner
|
| 313 |
+
)
|
| 314 |
+
|
| 315 |
+
# Extend (prefill w/ KV cache)
|
| 316 |
+
next_token_ids, next_token_logits, batch = extend(reqs, model_runner)
|
| 317 |
+
rank_print(f"prefill logits (final): {next_token_logits} \n")
|
| 318 |
+
|
| 319 |
+
# Decode
|
| 320 |
+
output_ids = [input_ids[i] + [next_token_ids[i]] for i in range(len(input_ids))]
|
| 321 |
+
for _ in range(bench_args.output_len[0] - 1):
|
| 322 |
+
next_token_ids, _ = decode(next_token_ids, batch, model_runner)
|
| 323 |
+
next_token_ids_list = next_token_ids.tolist()
|
| 324 |
+
for i in range(len(reqs)):
|
| 325 |
+
output_ids[i].append(next_token_ids_list[i])
|
| 326 |
+
|
| 327 |
+
# Print output texts
|
| 328 |
+
for i in range(len(reqs)):
|
| 329 |
+
rank_print(f"========== Prompt {i} ==========")
|
| 330 |
+
rank_print(tokenizer.decode(output_ids[i]), "\n")
|
| 331 |
+
|
| 332 |
+
|
| 333 |
+
def synchronize(device):
|
| 334 |
+
torch.get_device_module(device).synchronize()
|
| 335 |
+
|
| 336 |
+
|
| 337 |
+
def latency_test_run_once(
|
| 338 |
+
run_name,
|
| 339 |
+
model_runner,
|
| 340 |
+
rank_print,
|
| 341 |
+
reqs,
|
| 342 |
+
batch_size,
|
| 343 |
+
input_len,
|
| 344 |
+
output_len,
|
| 345 |
+
device,
|
| 346 |
+
log_decode_step,
|
| 347 |
+
profile,
|
| 348 |
+
profile_filename_prefix,
|
| 349 |
+
):
|
| 350 |
+
max_batch_size = model_runner.max_total_num_tokens // (input_len + output_len)
|
| 351 |
+
if batch_size > max_batch_size:
|
| 352 |
+
rank_print(
|
| 353 |
+
f"skipping ({batch_size}, {input_len}, {output_len}) due to max batch size limit"
|
| 354 |
+
)
|
| 355 |
+
return
|
| 356 |
+
|
| 357 |
+
# Clear the pools.
|
| 358 |
+
model_runner.req_to_token_pool.clear()
|
| 359 |
+
model_runner.token_to_kv_pool_allocator.clear()
|
| 360 |
+
|
| 361 |
+
measurement_results = {
|
| 362 |
+
"run_name": run_name,
|
| 363 |
+
"batch_size": batch_size,
|
| 364 |
+
"input_len": input_len,
|
| 365 |
+
"output_len": output_len,
|
| 366 |
+
}
|
| 367 |
+
|
| 368 |
+
tot_latency = 0
|
| 369 |
+
|
| 370 |
+
profiler = None
|
| 371 |
+
if profile:
|
| 372 |
+
profiler = torch.profiler.profile(
|
| 373 |
+
activities=[
|
| 374 |
+
torch.profiler.ProfilerActivity.CPU,
|
| 375 |
+
torch.profiler.ProfilerActivity.CUDA,
|
| 376 |
+
],
|
| 377 |
+
with_stack=True,
|
| 378 |
+
)
|
| 379 |
+
profiler.start()
|
| 380 |
+
|
| 381 |
+
# Prefill
|
| 382 |
+
synchronize(device)
|
| 383 |
+
tic = time.time()
|
| 384 |
+
next_token_ids, _, batch = extend(reqs, model_runner)
|
| 385 |
+
synchronize(device)
|
| 386 |
+
prefill_latency = time.time() - tic
|
| 387 |
+
tot_latency += prefill_latency
|
| 388 |
+
throughput = input_len * batch_size / prefill_latency
|
| 389 |
+
rank_print(
|
| 390 |
+
f"Prefill. latency: {prefill_latency:6.5f} s, throughput: {throughput:9.2f} token/s"
|
| 391 |
+
)
|
| 392 |
+
measurement_results["prefill_latency"] = prefill_latency
|
| 393 |
+
measurement_results["prefill_throughput"] = throughput
|
| 394 |
+
|
| 395 |
+
# Decode
|
| 396 |
+
decode_latencies = []
|
| 397 |
+
for i in range(output_len - 1):
|
| 398 |
+
synchronize(device)
|
| 399 |
+
tic = time.time()
|
| 400 |
+
next_token_ids, _ = decode(next_token_ids, batch, model_runner)
|
| 401 |
+
synchronize(device)
|
| 402 |
+
latency = time.time() - tic
|
| 403 |
+
tot_latency += latency
|
| 404 |
+
throughput = batch_size / latency
|
| 405 |
+
decode_latencies.append(latency)
|
| 406 |
+
if i < 5 or (log_decode_step > 0 and i % log_decode_step == 0):
|
| 407 |
+
rank_print(
|
| 408 |
+
f"Decode {i}. Batch size: {batch_size}, latency: {latency:6.5f} s, throughput: {throughput:9.2f} token/s"
|
| 409 |
+
)
|
| 410 |
+
|
| 411 |
+
if profile:
|
| 412 |
+
profiler.stop()
|
| 413 |
+
profile_filename = f"{profile_filename_prefix}_batch{batch_size}_input{input_len}_output{output_len}.trace.json.gz"
|
| 414 |
+
parent_dir = os.path.dirname(os.path.abspath(profile_filename))
|
| 415 |
+
os.makedirs(parent_dir, exist_ok=True)
|
| 416 |
+
profiler.export_chrome_trace(profile_filename)
|
| 417 |
+
rank_print(f"torch profiler chrome trace saved to {profile_filename}")
|
| 418 |
+
|
| 419 |
+
# Record decode timing from 2nd output
|
| 420 |
+
if output_len > 1:
|
| 421 |
+
med_decode_latency = np.median(decode_latencies)
|
| 422 |
+
med_decode_throughput = batch_size / med_decode_latency
|
| 423 |
+
rank_print(
|
| 424 |
+
f"Decode. median latency: {med_decode_latency:6.5f} s, median throughput: {med_decode_throughput:9.2f} token/s"
|
| 425 |
+
)
|
| 426 |
+
measurement_results["median_decode_latency"] = med_decode_latency
|
| 427 |
+
measurement_results["median_decode_throughput"] = med_decode_throughput
|
| 428 |
+
|
| 429 |
+
throughput = (input_len + output_len) * batch_size / tot_latency
|
| 430 |
+
rank_print(
|
| 431 |
+
f"Total. latency: {tot_latency:6.3f} s, throughput: {throughput:9.2f} token/s"
|
| 432 |
+
)
|
| 433 |
+
measurement_results["total_latency"] = tot_latency
|
| 434 |
+
measurement_results["overall_throughput"] = throughput
|
| 435 |
+
return measurement_results
|
| 436 |
+
|
| 437 |
+
|
| 438 |
+
def latency_test(
|
| 439 |
+
server_args,
|
| 440 |
+
port_args,
|
| 441 |
+
bench_args,
|
| 442 |
+
tp_rank,
|
| 443 |
+
):
|
| 444 |
+
# Set CPU affinity
|
| 445 |
+
if get_bool_env_var("SGLANG_SET_CPU_AFFINITY"):
|
| 446 |
+
set_gpu_proc_affinity(server_args.tp_size, server_args.nnodes, tp_rank)
|
| 447 |
+
|
| 448 |
+
# Configure the logger
|
| 449 |
+
configure_logger(server_args, prefix=f" TP{tp_rank}")
|
| 450 |
+
rank_print = print if tp_rank == 0 else lambda *args, **kwargs: None
|
| 451 |
+
|
| 452 |
+
# Load the model
|
| 453 |
+
model_runner, tokenizer = load_model(server_args, port_args, tp_rank)
|
| 454 |
+
|
| 455 |
+
# Prepare inputs for warm up
|
| 456 |
+
reqs = prepare_synthetic_inputs_for_latency_test(
|
| 457 |
+
bench_args.batch_size[0], bench_args.input_len[0]
|
| 458 |
+
)
|
| 459 |
+
|
| 460 |
+
# Warm up
|
| 461 |
+
rank_print("Warmup ...")
|
| 462 |
+
latency_test_run_once(
|
| 463 |
+
bench_args.run_name,
|
| 464 |
+
model_runner,
|
| 465 |
+
rank_print,
|
| 466 |
+
reqs,
|
| 467 |
+
bench_args.batch_size[0],
|
| 468 |
+
bench_args.input_len[0],
|
| 469 |
+
min(32, bench_args.output_len[0]), # shorter decoding to speed up the warmup
|
| 470 |
+
server_args.device,
|
| 471 |
+
log_decode_step=0,
|
| 472 |
+
profile=False,
|
| 473 |
+
profile_filename_prefix="", # not used
|
| 474 |
+
)
|
| 475 |
+
|
| 476 |
+
rank_print("Benchmark ...")
|
| 477 |
+
|
| 478 |
+
# Run the sweep
|
| 479 |
+
result_list = []
|
| 480 |
+
for bs, il, ol in itertools.product(
|
| 481 |
+
bench_args.batch_size, bench_args.input_len, bench_args.output_len
|
| 482 |
+
):
|
| 483 |
+
reqs = prepare_synthetic_inputs_for_latency_test(bs, il)
|
| 484 |
+
ret = latency_test_run_once(
|
| 485 |
+
bench_args.run_name,
|
| 486 |
+
model_runner,
|
| 487 |
+
rank_print,
|
| 488 |
+
reqs,
|
| 489 |
+
bs,
|
| 490 |
+
il,
|
| 491 |
+
ol,
|
| 492 |
+
server_args.device,
|
| 493 |
+
bench_args.log_decode_step,
|
| 494 |
+
bench_args.profile if tp_rank == 0 else None,
|
| 495 |
+
bench_args.profile_filename_prefix,
|
| 496 |
+
)
|
| 497 |
+
if ret is not None:
|
| 498 |
+
result_list.append(ret)
|
| 499 |
+
|
| 500 |
+
# Write results in jsonlines format on rank 0.
|
| 501 |
+
if tp_rank == 0 and bench_args.result_filename:
|
| 502 |
+
with open(bench_args.result_filename, "a") as fout:
|
| 503 |
+
for result in result_list:
|
| 504 |
+
fout.write(json.dumps(result) + "\n")
|
| 505 |
+
|
| 506 |
+
if server_args.tp_size > 1:
|
| 507 |
+
destroy_distributed_environment()
|
| 508 |
+
|
| 509 |
+
|
| 510 |
+
def main(server_args, bench_args):
|
| 511 |
+
server_args.cuda_graph_max_bs = max(bench_args.batch_size)
|
| 512 |
+
|
| 513 |
+
_set_envs_and_config(server_args)
|
| 514 |
+
|
| 515 |
+
if server_args.model_path:
|
| 516 |
+
if bench_args.correctness_test:
|
| 517 |
+
work_func = correctness_test
|
| 518 |
+
else:
|
| 519 |
+
work_func = latency_test
|
| 520 |
+
else:
|
| 521 |
+
raise ValueError(
|
| 522 |
+
"Provide --model-path for running the tests or "
|
| 523 |
+
"provide --result-filename for plotting the results"
|
| 524 |
+
)
|
| 525 |
+
|
| 526 |
+
port_args = PortArgs.init_new(server_args)
|
| 527 |
+
|
| 528 |
+
if server_args.tp_size == 1:
|
| 529 |
+
work_func(server_args, port_args, bench_args, 0)
|
| 530 |
+
else:
|
| 531 |
+
workers = []
|
| 532 |
+
for tp_rank in range(server_args.tp_size):
|
| 533 |
+
proc = multiprocessing.Process(
|
| 534 |
+
target=work_func,
|
| 535 |
+
args=(
|
| 536 |
+
server_args,
|
| 537 |
+
port_args,
|
| 538 |
+
bench_args,
|
| 539 |
+
tp_rank,
|
| 540 |
+
),
|
| 541 |
+
)
|
| 542 |
+
proc.start()
|
| 543 |
+
workers.append(proc)
|
| 544 |
+
|
| 545 |
+
for proc in workers:
|
| 546 |
+
proc.join()
|
| 547 |
+
|
| 548 |
+
proc.terminate()
|
| 549 |
+
|
| 550 |
+
|
| 551 |
+
if __name__ == "__main__":
|
| 552 |
+
parser = argparse.ArgumentParser()
|
| 553 |
+
ServerArgs.add_cli_args(parser)
|
| 554 |
+
BenchArgs.add_cli_args(parser)
|
| 555 |
+
args = parser.parse_args()
|
| 556 |
+
server_args = ServerArgs.from_cli_args(args)
|
| 557 |
+
bench_args = BenchArgs.from_cli_args(args)
|
| 558 |
+
|
| 559 |
+
logging.basicConfig(
|
| 560 |
+
level=getattr(logging, server_args.log_level.upper()),
|
| 561 |
+
format="%(message)s",
|
| 562 |
+
)
|
| 563 |
+
|
| 564 |
+
try:
|
| 565 |
+
main(server_args, bench_args)
|
| 566 |
+
finally:
|
| 567 |
+
if server_args.tp_size != 1:
|
| 568 |
+
kill_process_tree(os.getpid(), include_parent=False)
|
lib/python3.12/site-packages/sglang/bench_one_batch_server.py
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Benchmark the latency of running a single batch with a server.
|
| 3 |
+
|
| 4 |
+
This script launches a server and uses the HTTP interface.
|
| 5 |
+
It accepts server arguments (the same as launch_server.py) and benchmark arguments (e.g., batch size, input lengths).
|
| 6 |
+
|
| 7 |
+
Usage:
|
| 8 |
+
python3 -m sglang.bench_one_batch_server --model meta-llama/Meta-Llama-3.1-8B --batch-size 1 16 64 --input-len 1024 --output-len 8
|
| 9 |
+
|
| 10 |
+
python3 -m sglang.bench_one_batch_server --model None --base-url http://localhost:30000 --batch-size 16 --input-len 1024 --output-len 8
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
import argparse
|
| 14 |
+
import dataclasses
|
| 15 |
+
import itertools
|
| 16 |
+
import json
|
| 17 |
+
import multiprocessing
|
| 18 |
+
import os
|
| 19 |
+
import time
|
| 20 |
+
from typing import Tuple
|
| 21 |
+
|
| 22 |
+
import numpy as np
|
| 23 |
+
import requests
|
| 24 |
+
|
| 25 |
+
from sglang.srt.entrypoints.http_server import launch_server
|
| 26 |
+
from sglang.srt.server_args import ServerArgs
|
| 27 |
+
from sglang.srt.utils import kill_process_tree
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
@dataclasses.dataclass
|
| 31 |
+
class BenchArgs:
|
| 32 |
+
run_name: str = "default"
|
| 33 |
+
batch_size: Tuple[int] = (1,)
|
| 34 |
+
input_len: Tuple[int] = (1024,)
|
| 35 |
+
output_len: Tuple[int] = (16,)
|
| 36 |
+
result_filename: str = "result.jsonl"
|
| 37 |
+
base_url: str = ""
|
| 38 |
+
skip_warmup: bool = False
|
| 39 |
+
|
| 40 |
+
@staticmethod
|
| 41 |
+
def add_cli_args(parser: argparse.ArgumentParser):
|
| 42 |
+
parser.add_argument("--run-name", type=str, default=BenchArgs.run_name)
|
| 43 |
+
parser.add_argument(
|
| 44 |
+
"--batch-size", type=int, nargs="+", default=BenchArgs.batch_size
|
| 45 |
+
)
|
| 46 |
+
parser.add_argument(
|
| 47 |
+
"--input-len", type=int, nargs="+", default=BenchArgs.input_len
|
| 48 |
+
)
|
| 49 |
+
parser.add_argument(
|
| 50 |
+
"--output-len", type=int, nargs="+", default=BenchArgs.output_len
|
| 51 |
+
)
|
| 52 |
+
parser.add_argument(
|
| 53 |
+
"--result-filename", type=str, default=BenchArgs.result_filename
|
| 54 |
+
)
|
| 55 |
+
parser.add_argument("--base-url", type=str, default=BenchArgs.base_url)
|
| 56 |
+
parser.add_argument("--skip-warmup", action="store_true")
|
| 57 |
+
|
| 58 |
+
@classmethod
|
| 59 |
+
def from_cli_args(cls, args: argparse.Namespace):
|
| 60 |
+
# use the default value's type to cast the args into correct types.
|
| 61 |
+
attrs = [(attr.name, type(attr.default)) for attr in dataclasses.fields(cls)]
|
| 62 |
+
return cls(
|
| 63 |
+
**{attr: attr_type(getattr(args, attr)) for attr, attr_type in attrs}
|
| 64 |
+
)
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def launch_server_internal(server_args):
|
| 68 |
+
try:
|
| 69 |
+
launch_server(server_args)
|
| 70 |
+
except Exception as e:
|
| 71 |
+
raise e
|
| 72 |
+
finally:
|
| 73 |
+
kill_process_tree(os.getpid(), include_parent=False)
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def launch_server_process(server_args: ServerArgs):
|
| 77 |
+
proc = multiprocessing.Process(target=launch_server_internal, args=(server_args,))
|
| 78 |
+
proc.start()
|
| 79 |
+
base_url = f"http://{server_args.host}:{server_args.port}"
|
| 80 |
+
timeout = 600
|
| 81 |
+
|
| 82 |
+
start_time = time.time()
|
| 83 |
+
while time.time() - start_time < timeout:
|
| 84 |
+
try:
|
| 85 |
+
headers = {
|
| 86 |
+
"Content-Type": "application/json; charset=utf-8",
|
| 87 |
+
}
|
| 88 |
+
response = requests.get(f"{base_url}/v1/models", headers=headers)
|
| 89 |
+
if response.status_code == 200:
|
| 90 |
+
return proc, base_url
|
| 91 |
+
except requests.RequestException:
|
| 92 |
+
pass
|
| 93 |
+
time.sleep(10)
|
| 94 |
+
raise TimeoutError("Server failed to start within the timeout period.")
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def run_one_case(
|
| 98 |
+
url: str,
|
| 99 |
+
batch_size: int,
|
| 100 |
+
input_len: int,
|
| 101 |
+
output_len: int,
|
| 102 |
+
run_name: str,
|
| 103 |
+
result_filename: str,
|
| 104 |
+
):
|
| 105 |
+
input_ids = [
|
| 106 |
+
[int(x) for x in np.random.randint(0, high=16384, size=(input_len,))]
|
| 107 |
+
for _ in range(batch_size)
|
| 108 |
+
]
|
| 109 |
+
|
| 110 |
+
tic = time.time()
|
| 111 |
+
response = requests.post(
|
| 112 |
+
url + "/generate",
|
| 113 |
+
json={
|
| 114 |
+
"input_ids": input_ids,
|
| 115 |
+
"sampling_params": {
|
| 116 |
+
"temperature": 0,
|
| 117 |
+
"max_new_tokens": output_len,
|
| 118 |
+
"ignore_eos": True,
|
| 119 |
+
},
|
| 120 |
+
},
|
| 121 |
+
)
|
| 122 |
+
latency = time.time() - tic
|
| 123 |
+
|
| 124 |
+
_ = response.json()
|
| 125 |
+
output_throughput = batch_size * output_len / latency
|
| 126 |
+
overall_throughput = batch_size * (input_len + output_len) / latency
|
| 127 |
+
|
| 128 |
+
print(f"batch size: {batch_size}")
|
| 129 |
+
print(f"latency: {latency:.2f} s")
|
| 130 |
+
print(f"output throughput: {output_throughput:.2f} token/s")
|
| 131 |
+
print(f"(input + output) throughput: {overall_throughput:.2f} token/s")
|
| 132 |
+
|
| 133 |
+
if result_filename:
|
| 134 |
+
with open(result_filename, "a") as fout:
|
| 135 |
+
res = {
|
| 136 |
+
"run_name": run_name,
|
| 137 |
+
"batch_size": batch_size,
|
| 138 |
+
"input_len": input_len,
|
| 139 |
+
"output_len": output_len,
|
| 140 |
+
"latency": round(latency, 4),
|
| 141 |
+
"output_throughput": round(output_throughput, 2),
|
| 142 |
+
"overall_throughput": round(overall_throughput, 2),
|
| 143 |
+
}
|
| 144 |
+
fout.write(json.dumps(res) + "\n")
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
def run_benchmark(server_args: ServerArgs, bench_args: BenchArgs):
|
| 148 |
+
if bench_args.base_url:
|
| 149 |
+
proc, base_url = None, bench_args.base_url
|
| 150 |
+
else:
|
| 151 |
+
proc, base_url = launch_server_process(server_args)
|
| 152 |
+
|
| 153 |
+
# warmup
|
| 154 |
+
if not bench_args.skip_warmup:
|
| 155 |
+
run_one_case(
|
| 156 |
+
base_url,
|
| 157 |
+
batch_size=16,
|
| 158 |
+
input_len=1024,
|
| 159 |
+
output_len=16,
|
| 160 |
+
run_name="",
|
| 161 |
+
result_filename="",
|
| 162 |
+
)
|
| 163 |
+
|
| 164 |
+
# benchmark
|
| 165 |
+
try:
|
| 166 |
+
for bs, il, ol in itertools.product(
|
| 167 |
+
bench_args.batch_size, bench_args.input_len, bench_args.output_len
|
| 168 |
+
):
|
| 169 |
+
run_one_case(
|
| 170 |
+
base_url,
|
| 171 |
+
bs,
|
| 172 |
+
il,
|
| 173 |
+
ol,
|
| 174 |
+
bench_args.run_name,
|
| 175 |
+
bench_args.result_filename,
|
| 176 |
+
)
|
| 177 |
+
finally:
|
| 178 |
+
if proc:
|
| 179 |
+
kill_process_tree(proc.pid)
|
| 180 |
+
|
| 181 |
+
print(f"\nResults are saved to {bench_args.result_filename}")
|
| 182 |
+
|
| 183 |
+
|
| 184 |
+
if __name__ == "__main__":
|
| 185 |
+
parser = argparse.ArgumentParser()
|
| 186 |
+
ServerArgs.add_cli_args(parser)
|
| 187 |
+
BenchArgs.add_cli_args(parser)
|
| 188 |
+
args = parser.parse_args()
|
| 189 |
+
server_args = ServerArgs.from_cli_args(args)
|
| 190 |
+
bench_args = BenchArgs.from_cli_args(args)
|
| 191 |
+
|
| 192 |
+
run_benchmark(server_args, bench_args)
|
lib/python3.12/site-packages/sglang/bench_serving.py
ADDED
|
@@ -0,0 +1,1623 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Adapted from https://github.com/vllm-project/vllm/blob/6366efc67b0aedd2c1721c14385370e50b297fb3/benchmarks/backend_request_func.py
|
| 2 |
+
# Adapted from https://github.com/vllm-project/vllm/blob/6366efc67b0aedd2c1721c14385370e50b297fb3/benchmarks/benchmark_serving.py
|
| 3 |
+
|
| 4 |
+
"""
|
| 5 |
+
Benchmark online serving with dynamic requests.
|
| 6 |
+
|
| 7 |
+
Usage:
|
| 8 |
+
python3 -m sglang.bench_serving --backend sglang --num-prompt 10
|
| 9 |
+
|
| 10 |
+
python3 -m sglang.bench_serving --backend sglang --dataset-name random --num-prompts 3000 --random-input 1024 --random-output 1024 --random-range-ratio 0.5
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
import argparse
|
| 14 |
+
import asyncio
|
| 15 |
+
import json
|
| 16 |
+
import os
|
| 17 |
+
import pickle
|
| 18 |
+
import random
|
| 19 |
+
import resource
|
| 20 |
+
import sys
|
| 21 |
+
import time
|
| 22 |
+
import traceback
|
| 23 |
+
import warnings
|
| 24 |
+
from argparse import ArgumentParser
|
| 25 |
+
from dataclasses import dataclass, field
|
| 26 |
+
from datetime import datetime
|
| 27 |
+
from pathlib import Path
|
| 28 |
+
from typing import Any, AsyncGenerator, Dict, List, Optional, Tuple, Union
|
| 29 |
+
|
| 30 |
+
import aiohttp
|
| 31 |
+
import numpy as np
|
| 32 |
+
import requests
|
| 33 |
+
from tqdm.asyncio import tqdm
|
| 34 |
+
from transformers import (
|
| 35 |
+
AutoTokenizer,
|
| 36 |
+
PreTrainedTokenizer,
|
| 37 |
+
PreTrainedTokenizerBase,
|
| 38 |
+
PreTrainedTokenizerFast,
|
| 39 |
+
)
|
| 40 |
+
|
| 41 |
+
AIOHTTP_TIMEOUT = aiohttp.ClientTimeout(total=6 * 60 * 60)
|
| 42 |
+
ASSISTANT_SUFFIX = "Assistant:"
|
| 43 |
+
|
| 44 |
+
global args
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
# don't want to import sglang package here
|
| 48 |
+
def _get_bool_env_var(name: str, default: str = "false") -> bool:
|
| 49 |
+
value = os.getenv(name, default)
|
| 50 |
+
return value.lower() in ("true", "1")
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
@dataclass
|
| 54 |
+
class RequestFuncInput:
|
| 55 |
+
prompt: str
|
| 56 |
+
api_url: str
|
| 57 |
+
prompt_len: int
|
| 58 |
+
output_len: int
|
| 59 |
+
model: str
|
| 60 |
+
lora_name: str
|
| 61 |
+
extra_request_body: Dict[str, Any]
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
@dataclass
|
| 65 |
+
class RequestFuncOutput:
|
| 66 |
+
generated_text: str = ""
|
| 67 |
+
success: bool = False
|
| 68 |
+
latency: float = 0.0
|
| 69 |
+
ttft: float = 0.0 # Time to first token
|
| 70 |
+
itl: List[float] = field(default_factory=list) # List of inter-token latencies
|
| 71 |
+
prompt_len: int = 0
|
| 72 |
+
error: str = ""
|
| 73 |
+
output_len: int = 0
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def remove_prefix(text: str, prefix: str) -> str:
|
| 77 |
+
return text[len(prefix) :] if text.startswith(prefix) else text
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def remove_suffix(text: str, suffix: str) -> str:
|
| 81 |
+
return text[: -len(suffix)] if text.endswith(suffix) else text
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def get_auth_headers() -> Dict[str, str]:
|
| 85 |
+
api_key = os.environ.get("OPENAI_API_KEY")
|
| 86 |
+
if api_key:
|
| 87 |
+
return {"Authorization": f"Bearer {api_key}"}
|
| 88 |
+
else:
|
| 89 |
+
return {}
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
# trt llm does not support ignore_eos
|
| 93 |
+
# https://github.com/triton-inference-server/tensorrtllm_backend/issues/505
|
| 94 |
+
async def async_request_trt_llm(
|
| 95 |
+
request_func_input: RequestFuncInput,
|
| 96 |
+
pbar: Optional[tqdm] = None,
|
| 97 |
+
) -> RequestFuncOutput:
|
| 98 |
+
api_url = request_func_input.api_url
|
| 99 |
+
assert api_url.endswith("generate_stream")
|
| 100 |
+
|
| 101 |
+
async with aiohttp.ClientSession(timeout=AIOHTTP_TIMEOUT) as session:
|
| 102 |
+
payload = {
|
| 103 |
+
"accumulate_tokens": True,
|
| 104 |
+
"text_input": request_func_input.prompt,
|
| 105 |
+
"temperature": 0.000001,
|
| 106 |
+
"top_p": 1.0,
|
| 107 |
+
"max_tokens": request_func_input.output_len,
|
| 108 |
+
"stream": True,
|
| 109 |
+
"min_length": request_func_input.output_len,
|
| 110 |
+
"end_id": 1048576,
|
| 111 |
+
**request_func_input.extra_request_body,
|
| 112 |
+
}
|
| 113 |
+
if args.disable_ignore_eos:
|
| 114 |
+
del payload["min_length"]
|
| 115 |
+
del payload["end_id"]
|
| 116 |
+
output = RequestFuncOutput()
|
| 117 |
+
output.prompt_len = request_func_input.prompt_len
|
| 118 |
+
|
| 119 |
+
ttft = 0.0
|
| 120 |
+
st = time.perf_counter()
|
| 121 |
+
most_recent_timestamp = st
|
| 122 |
+
try:
|
| 123 |
+
async with session.post(url=api_url, json=payload) as response:
|
| 124 |
+
if response.status == 200:
|
| 125 |
+
async for chunk_bytes in response.content:
|
| 126 |
+
chunk_bytes = chunk_bytes.strip()
|
| 127 |
+
if not chunk_bytes:
|
| 128 |
+
continue
|
| 129 |
+
|
| 130 |
+
chunk = remove_prefix(chunk_bytes.decode("utf-8"), "data:")
|
| 131 |
+
|
| 132 |
+
data = json.loads(chunk)
|
| 133 |
+
output.generated_text += data["text_output"]
|
| 134 |
+
timestamp = time.perf_counter()
|
| 135 |
+
# First token
|
| 136 |
+
if ttft == 0.0:
|
| 137 |
+
ttft = timestamp - st
|
| 138 |
+
output.ttft = ttft
|
| 139 |
+
|
| 140 |
+
# Decoding phase
|
| 141 |
+
else:
|
| 142 |
+
output.itl.append(timestamp - most_recent_timestamp)
|
| 143 |
+
|
| 144 |
+
most_recent_timestamp = timestamp
|
| 145 |
+
|
| 146 |
+
output.latency = most_recent_timestamp - st
|
| 147 |
+
output.success = True
|
| 148 |
+
output.output_len = request_func_input.output_len
|
| 149 |
+
|
| 150 |
+
else:
|
| 151 |
+
output.error = response.reason or ""
|
| 152 |
+
output.success = False
|
| 153 |
+
except Exception:
|
| 154 |
+
output.success = False
|
| 155 |
+
exc_info = sys.exc_info()
|
| 156 |
+
output.error = "".join(traceback.format_exception(*exc_info))
|
| 157 |
+
|
| 158 |
+
if pbar:
|
| 159 |
+
pbar.update(1)
|
| 160 |
+
return output
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
# set ignore_eos True by default
|
| 164 |
+
async def async_request_openai_completions(
|
| 165 |
+
request_func_input: RequestFuncInput,
|
| 166 |
+
pbar: Optional[tqdm] = None,
|
| 167 |
+
) -> RequestFuncOutput:
|
| 168 |
+
api_url = request_func_input.api_url
|
| 169 |
+
assert api_url.endswith(
|
| 170 |
+
"completions"
|
| 171 |
+
), "OpenAI Completions API URL must end with 'completions'."
|
| 172 |
+
|
| 173 |
+
prompt = request_func_input.prompt
|
| 174 |
+
|
| 175 |
+
async with aiohttp.ClientSession(timeout=AIOHTTP_TIMEOUT) as session:
|
| 176 |
+
payload = {
|
| 177 |
+
"model": request_func_input.model,
|
| 178 |
+
"prompt": prompt,
|
| 179 |
+
"temperature": 0.0,
|
| 180 |
+
"best_of": 1,
|
| 181 |
+
"max_tokens": request_func_input.output_len,
|
| 182 |
+
"stream": not args.disable_stream,
|
| 183 |
+
"ignore_eos": not args.disable_ignore_eos,
|
| 184 |
+
**request_func_input.extra_request_body,
|
| 185 |
+
}
|
| 186 |
+
headers = get_auth_headers()
|
| 187 |
+
|
| 188 |
+
output = RequestFuncOutput()
|
| 189 |
+
output.prompt_len = request_func_input.prompt_len
|
| 190 |
+
|
| 191 |
+
generated_text = ""
|
| 192 |
+
output_len = request_func_input.output_len
|
| 193 |
+
ttft = 0.0
|
| 194 |
+
st = time.perf_counter()
|
| 195 |
+
most_recent_timestamp = st
|
| 196 |
+
try:
|
| 197 |
+
async with session.post(
|
| 198 |
+
url=api_url, json=payload, headers=headers
|
| 199 |
+
) as response:
|
| 200 |
+
if response.status == 200:
|
| 201 |
+
async for chunk_bytes in response.content:
|
| 202 |
+
chunk_bytes = chunk_bytes.strip()
|
| 203 |
+
if not chunk_bytes:
|
| 204 |
+
continue
|
| 205 |
+
|
| 206 |
+
chunk = remove_prefix(chunk_bytes.decode("utf-8"), "data: ")
|
| 207 |
+
latency = time.perf_counter() - st
|
| 208 |
+
if chunk == "[DONE]":
|
| 209 |
+
pass
|
| 210 |
+
else:
|
| 211 |
+
data = json.loads(chunk)
|
| 212 |
+
|
| 213 |
+
# NOTE: Some completion API might have a last
|
| 214 |
+
# usage summary response without a token so we
|
| 215 |
+
# want to check a token was generated
|
| 216 |
+
if data["choices"][0]["text"]:
|
| 217 |
+
timestamp = time.perf_counter()
|
| 218 |
+
# First token
|
| 219 |
+
if ttft == 0.0:
|
| 220 |
+
ttft = time.perf_counter() - st
|
| 221 |
+
output.ttft = ttft
|
| 222 |
+
|
| 223 |
+
# Decoding phase
|
| 224 |
+
else:
|
| 225 |
+
output.itl.append(timestamp - most_recent_timestamp)
|
| 226 |
+
|
| 227 |
+
most_recent_timestamp = timestamp
|
| 228 |
+
generated_text += data["choices"][0]["text"]
|
| 229 |
+
output_len = (data.get("usage") or {}).get(
|
| 230 |
+
"completion_tokens", output_len
|
| 231 |
+
)
|
| 232 |
+
|
| 233 |
+
output.generated_text = generated_text
|
| 234 |
+
output.success = True
|
| 235 |
+
output.latency = latency
|
| 236 |
+
output.output_len = output_len
|
| 237 |
+
else:
|
| 238 |
+
output.error = response.reason or ""
|
| 239 |
+
output.success = False
|
| 240 |
+
except Exception:
|
| 241 |
+
output.success = False
|
| 242 |
+
exc_info = sys.exc_info()
|
| 243 |
+
output.error = "".join(traceback.format_exception(*exc_info))
|
| 244 |
+
|
| 245 |
+
if pbar:
|
| 246 |
+
pbar.update(1)
|
| 247 |
+
return output
|
| 248 |
+
|
| 249 |
+
|
| 250 |
+
async def async_request_truss(
|
| 251 |
+
request_func_input: RequestFuncInput,
|
| 252 |
+
pbar: Optional[tqdm] = None,
|
| 253 |
+
) -> RequestFuncOutput:
|
| 254 |
+
api_url = request_func_input.api_url
|
| 255 |
+
|
| 256 |
+
prompt = request_func_input.prompt
|
| 257 |
+
|
| 258 |
+
async with aiohttp.ClientSession(timeout=AIOHTTP_TIMEOUT) as session:
|
| 259 |
+
payload = {
|
| 260 |
+
"model": request_func_input.model,
|
| 261 |
+
"prompt": prompt,
|
| 262 |
+
"temperature": 0.0,
|
| 263 |
+
"best_of": 1,
|
| 264 |
+
"max_tokens": request_func_input.output_len,
|
| 265 |
+
"stream": not args.disable_stream,
|
| 266 |
+
"ignore_eos": not args.disable_ignore_eos,
|
| 267 |
+
**request_func_input.extra_request_body,
|
| 268 |
+
}
|
| 269 |
+
headers = get_auth_headers()
|
| 270 |
+
|
| 271 |
+
output = RequestFuncOutput()
|
| 272 |
+
output.prompt_len = request_func_input.prompt_len
|
| 273 |
+
|
| 274 |
+
generated_text = ""
|
| 275 |
+
ttft = 0.0
|
| 276 |
+
st = time.perf_counter()
|
| 277 |
+
most_recent_timestamp = st
|
| 278 |
+
try:
|
| 279 |
+
async with session.post(
|
| 280 |
+
url=api_url, json=payload, headers=headers
|
| 281 |
+
) as response:
|
| 282 |
+
if response.status == 200:
|
| 283 |
+
async for chunk_bytes in response.content:
|
| 284 |
+
chunk_bytes = chunk_bytes.strip()
|
| 285 |
+
if not chunk_bytes:
|
| 286 |
+
continue
|
| 287 |
+
|
| 288 |
+
chunk = remove_prefix(chunk_bytes.decode("utf-8"), "data: ")
|
| 289 |
+
latency = time.perf_counter() - st
|
| 290 |
+
if chunk == "[DONE]":
|
| 291 |
+
pass
|
| 292 |
+
else:
|
| 293 |
+
data = json.loads(chunk)
|
| 294 |
+
|
| 295 |
+
# NOTE: Some completion API might have a last
|
| 296 |
+
# usage summary response without a token so we
|
| 297 |
+
# want to check a token was generated
|
| 298 |
+
if data["choices"][0]["text"]:
|
| 299 |
+
timestamp = time.perf_counter()
|
| 300 |
+
# First token
|
| 301 |
+
if ttft == 0.0:
|
| 302 |
+
ttft = time.perf_counter() - st
|
| 303 |
+
output.ttft = ttft
|
| 304 |
+
|
| 305 |
+
# Decoding phase
|
| 306 |
+
else:
|
| 307 |
+
output.itl.append(timestamp - most_recent_timestamp)
|
| 308 |
+
|
| 309 |
+
most_recent_timestamp = timestamp
|
| 310 |
+
generated_text += data["choices"][0]["text"]
|
| 311 |
+
|
| 312 |
+
output.generated_text = generated_text
|
| 313 |
+
output.success = True
|
| 314 |
+
output.latency = latency
|
| 315 |
+
output.output_len = request_func_input.output_len
|
| 316 |
+
else:
|
| 317 |
+
output.error = response.reason or ""
|
| 318 |
+
output.success = False
|
| 319 |
+
except Exception:
|
| 320 |
+
output.success = False
|
| 321 |
+
exc_info = sys.exc_info()
|
| 322 |
+
output.error = "".join(traceback.format_exception(*exc_info))
|
| 323 |
+
|
| 324 |
+
if pbar:
|
| 325 |
+
pbar.update(1)
|
| 326 |
+
return output
|
| 327 |
+
|
| 328 |
+
|
| 329 |
+
async def async_request_sglang_generate(
|
| 330 |
+
request_func_input: RequestFuncInput,
|
| 331 |
+
pbar: Optional[tqdm] = None,
|
| 332 |
+
) -> RequestFuncOutput:
|
| 333 |
+
api_url = request_func_input.api_url
|
| 334 |
+
prompt = request_func_input.prompt
|
| 335 |
+
|
| 336 |
+
async with aiohttp.ClientSession(timeout=AIOHTTP_TIMEOUT) as session:
|
| 337 |
+
payload = {
|
| 338 |
+
"text": prompt,
|
| 339 |
+
"sampling_params": {
|
| 340 |
+
"temperature": 0.0,
|
| 341 |
+
"max_new_tokens": request_func_input.output_len,
|
| 342 |
+
"ignore_eos": not args.disable_ignore_eos,
|
| 343 |
+
},
|
| 344 |
+
"stream": not args.disable_stream,
|
| 345 |
+
"lora_path": request_func_input.lora_name,
|
| 346 |
+
"return_logprob": args.return_logprob,
|
| 347 |
+
"logprob_start_len": -1,
|
| 348 |
+
**request_func_input.extra_request_body,
|
| 349 |
+
}
|
| 350 |
+
headers = get_auth_headers()
|
| 351 |
+
|
| 352 |
+
output = RequestFuncOutput()
|
| 353 |
+
output.prompt_len = request_func_input.prompt_len
|
| 354 |
+
|
| 355 |
+
generated_text = ""
|
| 356 |
+
output_len = request_func_input.output_len
|
| 357 |
+
ttft = 0.0
|
| 358 |
+
st = time.perf_counter()
|
| 359 |
+
most_recent_timestamp = st
|
| 360 |
+
last_output_len = 0
|
| 361 |
+
try:
|
| 362 |
+
async with session.post(
|
| 363 |
+
url=api_url, json=payload, headers=headers
|
| 364 |
+
) as response:
|
| 365 |
+
if response.status == 200:
|
| 366 |
+
async for chunk_bytes in response.content:
|
| 367 |
+
chunk_bytes = chunk_bytes.strip()
|
| 368 |
+
if not chunk_bytes:
|
| 369 |
+
continue
|
| 370 |
+
# print(chunk_bytes)
|
| 371 |
+
|
| 372 |
+
chunk = remove_prefix(chunk_bytes.decode("utf-8"), "data: ")
|
| 373 |
+
latency = time.perf_counter() - st
|
| 374 |
+
if chunk == "[DONE]":
|
| 375 |
+
pass
|
| 376 |
+
else:
|
| 377 |
+
data = json.loads(chunk)
|
| 378 |
+
|
| 379 |
+
# NOTE: Some completion API might have a last
|
| 380 |
+
# usage summary response without a token so we
|
| 381 |
+
# want to check a token was generated
|
| 382 |
+
if data["text"]:
|
| 383 |
+
timestamp = time.perf_counter()
|
| 384 |
+
generated_text = data["text"]
|
| 385 |
+
output_len = data["meta_info"]["completion_tokens"]
|
| 386 |
+
|
| 387 |
+
# First token
|
| 388 |
+
if ttft == 0.0:
|
| 389 |
+
ttft = time.perf_counter() - st
|
| 390 |
+
output.ttft = ttft
|
| 391 |
+
|
| 392 |
+
# Decoding phase
|
| 393 |
+
else:
|
| 394 |
+
num_new_tokens = output_len - last_output_len
|
| 395 |
+
if num_new_tokens == 0:
|
| 396 |
+
continue
|
| 397 |
+
adjust_itl = (
|
| 398 |
+
timestamp - most_recent_timestamp
|
| 399 |
+
) / num_new_tokens
|
| 400 |
+
output.itl.extend([adjust_itl] * num_new_tokens)
|
| 401 |
+
|
| 402 |
+
most_recent_timestamp = timestamp
|
| 403 |
+
last_output_len = output_len
|
| 404 |
+
|
| 405 |
+
output.generated_text = generated_text
|
| 406 |
+
output.success = True
|
| 407 |
+
output.latency = latency
|
| 408 |
+
output.output_len = output_len
|
| 409 |
+
else:
|
| 410 |
+
output.error = response.reason or ""
|
| 411 |
+
output.success = False
|
| 412 |
+
except Exception:
|
| 413 |
+
output.success = False
|
| 414 |
+
exc_info = sys.exc_info()
|
| 415 |
+
output.error = "".join(traceback.format_exception(*exc_info))
|
| 416 |
+
print(f"{output.error=}")
|
| 417 |
+
|
| 418 |
+
if pbar:
|
| 419 |
+
pbar.update(1)
|
| 420 |
+
return output
|
| 421 |
+
|
| 422 |
+
|
| 423 |
+
async def async_request_gserver(
|
| 424 |
+
request_func_input: RequestFuncInput,
|
| 425 |
+
pbar: Optional[tqdm] = None,
|
| 426 |
+
) -> RequestFuncOutput:
|
| 427 |
+
raise NotImplementedError()
|
| 428 |
+
|
| 429 |
+
|
| 430 |
+
async def async_request_profile(api_url: str) -> RequestFuncOutput:
|
| 431 |
+
async with aiohttp.ClientSession(timeout=AIOHTTP_TIMEOUT) as session:
|
| 432 |
+
output = RequestFuncOutput()
|
| 433 |
+
try:
|
| 434 |
+
async with session.post(url=api_url) as response:
|
| 435 |
+
if response.status == 200:
|
| 436 |
+
output.success = True
|
| 437 |
+
else:
|
| 438 |
+
output.error = response.reason or ""
|
| 439 |
+
output.success = False
|
| 440 |
+
except Exception:
|
| 441 |
+
output.success = False
|
| 442 |
+
exc_info = sys.exc_info()
|
| 443 |
+
output.error = "".join(traceback.format_exception(*exc_info))
|
| 444 |
+
|
| 445 |
+
return output
|
| 446 |
+
|
| 447 |
+
|
| 448 |
+
def get_model(pretrained_model_name_or_path: str) -> str:
|
| 449 |
+
if os.getenv("SGLANG_USE_MODELSCOPE", "false").lower() == "true":
|
| 450 |
+
import huggingface_hub.constants
|
| 451 |
+
from modelscope import snapshot_download
|
| 452 |
+
|
| 453 |
+
model_path = snapshot_download(
|
| 454 |
+
model_id=pretrained_model_name_or_path,
|
| 455 |
+
local_files_only=huggingface_hub.constants.HF_HUB_OFFLINE,
|
| 456 |
+
ignore_file_pattern=[".*.pt", ".*.safetensors", ".*.bin"],
|
| 457 |
+
)
|
| 458 |
+
|
| 459 |
+
return model_path
|
| 460 |
+
return pretrained_model_name_or_path
|
| 461 |
+
|
| 462 |
+
|
| 463 |
+
def get_tokenizer(
|
| 464 |
+
pretrained_model_name_or_path: str,
|
| 465 |
+
) -> Union[PreTrainedTokenizer, PreTrainedTokenizerFast]:
|
| 466 |
+
if pretrained_model_name_or_path.endswith(
|
| 467 |
+
".json"
|
| 468 |
+
) or pretrained_model_name_or_path.endswith(".model"):
|
| 469 |
+
from sglang.srt.hf_transformers_utils import get_tokenizer
|
| 470 |
+
|
| 471 |
+
return get_tokenizer(pretrained_model_name_or_path)
|
| 472 |
+
|
| 473 |
+
if pretrained_model_name_or_path is not None and not os.path.exists(
|
| 474 |
+
pretrained_model_name_or_path
|
| 475 |
+
):
|
| 476 |
+
pretrained_model_name_or_path = get_model(pretrained_model_name_or_path)
|
| 477 |
+
return AutoTokenizer.from_pretrained(
|
| 478 |
+
pretrained_model_name_or_path, trust_remote_code=True
|
| 479 |
+
)
|
| 480 |
+
|
| 481 |
+
|
| 482 |
+
def get_dataset(args, tokenizer):
|
| 483 |
+
if args.dataset_name == "sharegpt":
|
| 484 |
+
input_requests = sample_sharegpt_requests(
|
| 485 |
+
dataset_path=args.dataset_path,
|
| 486 |
+
num_requests=args.num_prompts,
|
| 487 |
+
tokenizer=tokenizer,
|
| 488 |
+
fixed_output_len=args.sharegpt_output_len,
|
| 489 |
+
context_len=args.sharegpt_context_len,
|
| 490 |
+
prompt_suffix=args.prompt_suffix,
|
| 491 |
+
apply_chat_template=args.apply_chat_template,
|
| 492 |
+
)
|
| 493 |
+
elif args.dataset_name.startswith("random"):
|
| 494 |
+
input_requests = sample_random_requests(
|
| 495 |
+
input_len=args.random_input_len,
|
| 496 |
+
output_len=args.random_output_len,
|
| 497 |
+
num_prompts=args.num_prompts,
|
| 498 |
+
range_ratio=args.random_range_ratio,
|
| 499 |
+
tokenizer=tokenizer,
|
| 500 |
+
dataset_path=args.dataset_path,
|
| 501 |
+
random_sample=args.dataset_name == "random",
|
| 502 |
+
)
|
| 503 |
+
elif args.dataset_name == "generated-shared-prefix":
|
| 504 |
+
input_requests = sample_generated_shared_prefix_requests(
|
| 505 |
+
num_groups=args.gsp_num_groups,
|
| 506 |
+
prompts_per_group=args.gsp_prompts_per_group,
|
| 507 |
+
system_prompt_len=args.gsp_system_prompt_len,
|
| 508 |
+
question_len=args.gsp_question_len,
|
| 509 |
+
output_len=args.gsp_output_len,
|
| 510 |
+
tokenizer=tokenizer,
|
| 511 |
+
args=args,
|
| 512 |
+
)
|
| 513 |
+
else:
|
| 514 |
+
raise ValueError(f"Unknown dataset: {args.dataset_name}")
|
| 515 |
+
return input_requests
|
| 516 |
+
|
| 517 |
+
|
| 518 |
+
ASYNC_REQUEST_FUNCS = {
|
| 519 |
+
"sglang": async_request_sglang_generate,
|
| 520 |
+
"sglang-native": async_request_sglang_generate,
|
| 521 |
+
"sglang-oai": async_request_openai_completions,
|
| 522 |
+
"vllm": async_request_openai_completions,
|
| 523 |
+
"lmdeploy": async_request_openai_completions,
|
| 524 |
+
"trt": async_request_trt_llm,
|
| 525 |
+
"gserver": async_request_gserver,
|
| 526 |
+
"truss": async_request_truss,
|
| 527 |
+
}
|
| 528 |
+
|
| 529 |
+
|
| 530 |
+
@dataclass
|
| 531 |
+
class BenchmarkMetrics:
|
| 532 |
+
completed: int
|
| 533 |
+
total_input: int
|
| 534 |
+
total_output: int
|
| 535 |
+
total_output_retokenized: int
|
| 536 |
+
request_throughput: float
|
| 537 |
+
input_throughput: float
|
| 538 |
+
output_throughput: float
|
| 539 |
+
output_throughput_retokenized: float
|
| 540 |
+
total_throughput: float
|
| 541 |
+
total_throughput_retokenized: float
|
| 542 |
+
mean_ttft_ms: float
|
| 543 |
+
median_ttft_ms: float
|
| 544 |
+
std_ttft_ms: float
|
| 545 |
+
p99_ttft_ms: float
|
| 546 |
+
mean_tpot_ms: float
|
| 547 |
+
median_tpot_ms: float
|
| 548 |
+
std_tpot_ms: float
|
| 549 |
+
p99_tpot_ms: float
|
| 550 |
+
mean_itl_ms: float
|
| 551 |
+
median_itl_ms: float
|
| 552 |
+
std_itl_ms: float
|
| 553 |
+
p95_itl_ms: float
|
| 554 |
+
p99_itl_ms: float
|
| 555 |
+
max_itl_ms: float
|
| 556 |
+
mean_e2e_latency_ms: float
|
| 557 |
+
median_e2e_latency_ms: float
|
| 558 |
+
std_e2e_latency_ms: float
|
| 559 |
+
p99_e2e_latency_ms: float
|
| 560 |
+
concurrency: float
|
| 561 |
+
|
| 562 |
+
|
| 563 |
+
SHAREGPT_URL = "https://huggingface.co/datasets/anon8231489123/ShareGPT_Vicuna_unfiltered/resolve/main/ShareGPT_V3_unfiltered_cleaned_split.json"
|
| 564 |
+
|
| 565 |
+
|
| 566 |
+
def download_and_cache_file(url: str, filename: Optional[str] = None):
|
| 567 |
+
"""Read and cache a file from a url."""
|
| 568 |
+
if filename is None:
|
| 569 |
+
filename = os.path.join("/tmp", url.split("/")[-1])
|
| 570 |
+
|
| 571 |
+
# Check if the cache file already exists
|
| 572 |
+
if os.path.exists(filename):
|
| 573 |
+
return filename
|
| 574 |
+
|
| 575 |
+
print(f"Downloading from {url} to {filename}")
|
| 576 |
+
|
| 577 |
+
# Stream the response to show the progress bar
|
| 578 |
+
response = requests.get(url, stream=True)
|
| 579 |
+
response.raise_for_status() # Check for request errors
|
| 580 |
+
|
| 581 |
+
# Total size of the file in bytes
|
| 582 |
+
total_size = int(response.headers.get("content-length", 0))
|
| 583 |
+
chunk_size = 1024 # Download in chunks of 1KB
|
| 584 |
+
|
| 585 |
+
# Use tqdm to display the progress bar
|
| 586 |
+
with open(filename, "wb") as f, tqdm(
|
| 587 |
+
desc=filename,
|
| 588 |
+
total=total_size,
|
| 589 |
+
unit="B",
|
| 590 |
+
unit_scale=True,
|
| 591 |
+
unit_divisor=1024,
|
| 592 |
+
) as bar:
|
| 593 |
+
for chunk in response.iter_content(chunk_size=chunk_size):
|
| 594 |
+
f.write(chunk)
|
| 595 |
+
bar.update(len(chunk))
|
| 596 |
+
|
| 597 |
+
return filename
|
| 598 |
+
|
| 599 |
+
|
| 600 |
+
def sample_sharegpt_requests(
|
| 601 |
+
dataset_path: str,
|
| 602 |
+
num_requests: int,
|
| 603 |
+
tokenizer: PreTrainedTokenizerBase,
|
| 604 |
+
fixed_output_len: Optional[int] = None,
|
| 605 |
+
context_len: Optional[int] = None,
|
| 606 |
+
prompt_suffix: Optional[str] = "",
|
| 607 |
+
apply_chat_template=False,
|
| 608 |
+
) -> List[Tuple[str, int, int]]:
|
| 609 |
+
if fixed_output_len is not None and fixed_output_len < 4:
|
| 610 |
+
raise ValueError("output_len too small")
|
| 611 |
+
|
| 612 |
+
# Download sharegpt if necessary
|
| 613 |
+
if not os.path.isfile(dataset_path) and dataset_path == "":
|
| 614 |
+
dataset_path = download_and_cache_file(SHAREGPT_URL)
|
| 615 |
+
|
| 616 |
+
# Load the dataset.
|
| 617 |
+
with open(dataset_path) as f:
|
| 618 |
+
dataset = json.load(f)
|
| 619 |
+
|
| 620 |
+
# Filter out the conversations with less than 2 turns.
|
| 621 |
+
dataset = [
|
| 622 |
+
data
|
| 623 |
+
for data in dataset
|
| 624 |
+
if len(data.get("conversations", data.get("conversation", []))) >= 2
|
| 625 |
+
]
|
| 626 |
+
# Only keep the first two turns of each conversation.
|
| 627 |
+
dataset = [
|
| 628 |
+
(
|
| 629 |
+
data.get("conversations", data.get("conversation", []))[0]["value"],
|
| 630 |
+
data.get("conversations", data.get("conversation", []))[1]["value"],
|
| 631 |
+
)
|
| 632 |
+
for data in dataset
|
| 633 |
+
]
|
| 634 |
+
|
| 635 |
+
# Shuffle the dataset.
|
| 636 |
+
random.shuffle(dataset)
|
| 637 |
+
|
| 638 |
+
# Filter out sequences that are too long or too short
|
| 639 |
+
filtered_dataset: List[Tuple[str, int, int]] = []
|
| 640 |
+
for i in range(len(dataset)):
|
| 641 |
+
if len(filtered_dataset) == num_requests:
|
| 642 |
+
break
|
| 643 |
+
|
| 644 |
+
# Tokenize the prompts and completions.
|
| 645 |
+
prompt = dataset[i][0]
|
| 646 |
+
if prompt_suffix:
|
| 647 |
+
prompt = (
|
| 648 |
+
remove_suffix(prompt, ASSISTANT_SUFFIX)
|
| 649 |
+
+ prompt_suffix
|
| 650 |
+
+ ASSISTANT_SUFFIX
|
| 651 |
+
)
|
| 652 |
+
|
| 653 |
+
if apply_chat_template:
|
| 654 |
+
prompt = tokenizer.apply_chat_template(
|
| 655 |
+
[{"role": "user", "content": prompt}],
|
| 656 |
+
add_generation_prompt=True,
|
| 657 |
+
tokenize=False,
|
| 658 |
+
)
|
| 659 |
+
prompt = prompt.replace(tokenizer.bos_token, "")
|
| 660 |
+
|
| 661 |
+
prompt_token_ids = tokenizer.encode(prompt)
|
| 662 |
+
completion = dataset[i][1]
|
| 663 |
+
completion_token_ids = tokenizer.encode(completion)
|
| 664 |
+
prompt_len = len(prompt_token_ids)
|
| 665 |
+
output_len = (
|
| 666 |
+
len(completion_token_ids) if fixed_output_len is None else fixed_output_len
|
| 667 |
+
)
|
| 668 |
+
|
| 669 |
+
if prompt_len < 2 or output_len < 2:
|
| 670 |
+
# Prune too short sequences.
|
| 671 |
+
continue
|
| 672 |
+
|
| 673 |
+
if context_len and prompt_len + output_len > context_len:
|
| 674 |
+
# Prune too long sequences.
|
| 675 |
+
continue
|
| 676 |
+
|
| 677 |
+
filtered_dataset.append((prompt, prompt_len, output_len))
|
| 678 |
+
|
| 679 |
+
print(f"#Input tokens: {np.sum([x[1] for x in filtered_dataset])}")
|
| 680 |
+
print(f"#Output tokens: {np.sum([x[2] for x in filtered_dataset])}")
|
| 681 |
+
return filtered_dataset
|
| 682 |
+
|
| 683 |
+
|
| 684 |
+
def sample_random_requests(
|
| 685 |
+
input_len: int,
|
| 686 |
+
output_len: int,
|
| 687 |
+
num_prompts: int,
|
| 688 |
+
range_ratio: float,
|
| 689 |
+
tokenizer: PreTrainedTokenizerBase,
|
| 690 |
+
dataset_path: str,
|
| 691 |
+
random_sample: bool = True,
|
| 692 |
+
) -> List[Tuple[str, int, int]]:
|
| 693 |
+
input_lens = np.random.randint(
|
| 694 |
+
max(int(input_len * range_ratio), 1),
|
| 695 |
+
input_len + 1,
|
| 696 |
+
size=num_prompts,
|
| 697 |
+
)
|
| 698 |
+
output_lens = np.random.randint(
|
| 699 |
+
int(output_len * range_ratio),
|
| 700 |
+
output_len + 1,
|
| 701 |
+
size=num_prompts,
|
| 702 |
+
)
|
| 703 |
+
|
| 704 |
+
if random_sample:
|
| 705 |
+
# Sample token ids from ShareGPT and repeat/truncate them to satisfy the input_lens
|
| 706 |
+
|
| 707 |
+
# Download sharegpt if necessary
|
| 708 |
+
if not os.path.isfile(dataset_path):
|
| 709 |
+
dataset_path = download_and_cache_file(SHAREGPT_URL)
|
| 710 |
+
|
| 711 |
+
# Load the dataset.
|
| 712 |
+
with open(dataset_path) as f:
|
| 713 |
+
dataset = json.load(f)
|
| 714 |
+
# Filter out the conversations with less than 2 turns.
|
| 715 |
+
dataset = [
|
| 716 |
+
data
|
| 717 |
+
for data in dataset
|
| 718 |
+
if len(data.get("conversations", data.get("conversation", []))) >= 2
|
| 719 |
+
]
|
| 720 |
+
# Only keep the first two turns of each conversation.
|
| 721 |
+
dataset = [
|
| 722 |
+
(
|
| 723 |
+
data.get("conversations", data.get("conversation", []))[0]["value"],
|
| 724 |
+
data.get("conversations", data.get("conversation", []))[1]["value"],
|
| 725 |
+
)
|
| 726 |
+
for data in dataset
|
| 727 |
+
]
|
| 728 |
+
# Shuffle the dataset.
|
| 729 |
+
random.shuffle(dataset)
|
| 730 |
+
|
| 731 |
+
# Filter out sequences that are too long or too short
|
| 732 |
+
input_requests: List[Tuple[str, int, int]] = []
|
| 733 |
+
for data in dataset:
|
| 734 |
+
i = len(input_requests)
|
| 735 |
+
if i == num_prompts:
|
| 736 |
+
break
|
| 737 |
+
|
| 738 |
+
# Tokenize the prompts and completions.
|
| 739 |
+
prompt = data[0]
|
| 740 |
+
prompt_token_ids = tokenizer.encode(prompt)
|
| 741 |
+
prompt_len = len(prompt_token_ids)
|
| 742 |
+
|
| 743 |
+
# Skip empty prompt
|
| 744 |
+
if prompt_len == 0:
|
| 745 |
+
continue
|
| 746 |
+
|
| 747 |
+
if prompt_len > input_lens[i]:
|
| 748 |
+
input_ids = prompt_token_ids[: input_lens[i]]
|
| 749 |
+
else:
|
| 750 |
+
ratio = (input_lens[i] + prompt_len - 1) // prompt_len
|
| 751 |
+
input_ids = (prompt_token_ids * ratio)[: input_lens[i]]
|
| 752 |
+
prompt = tokenizer.decode(input_ids)
|
| 753 |
+
input_requests.append((prompt, int(input_lens[i]), int(output_lens[i])))
|
| 754 |
+
else:
|
| 755 |
+
# Sample token ids from random integers. This can cause some NaN issues.
|
| 756 |
+
offsets = np.random.randint(0, tokenizer.vocab_size, size=num_prompts)
|
| 757 |
+
input_requests = []
|
| 758 |
+
for i in range(num_prompts):
|
| 759 |
+
prompt = tokenizer.decode(
|
| 760 |
+
[
|
| 761 |
+
(offsets[i] + i + j) % tokenizer.vocab_size
|
| 762 |
+
for j in range(input_lens[i])
|
| 763 |
+
]
|
| 764 |
+
)
|
| 765 |
+
input_requests.append((prompt, int(input_lens[i]), int(output_lens[i])))
|
| 766 |
+
|
| 767 |
+
print(f"#Input tokens: {np.sum(input_lens)}")
|
| 768 |
+
print(f"#Output tokens: {np.sum(output_lens)}")
|
| 769 |
+
return input_requests
|
| 770 |
+
|
| 771 |
+
|
| 772 |
+
def gen_prompt(tokenizer, token_num):
|
| 773 |
+
"""Generate a random prompt of specified token length using tokenizer vocabulary."""
|
| 774 |
+
all_available_tokens = list(tokenizer.get_vocab().values())
|
| 775 |
+
selected_tokens = random.choices(all_available_tokens, k=token_num)
|
| 776 |
+
return tokenizer.decode(selected_tokens)
|
| 777 |
+
|
| 778 |
+
|
| 779 |
+
def get_gen_prefix_cache_path(args, tokenizer):
|
| 780 |
+
"""Create cache directory under ~/.cache/sglang/benchmark"""
|
| 781 |
+
cache_dir = Path.home() / ".cache" / "sglang" / "benchmark"
|
| 782 |
+
|
| 783 |
+
# Create a unique cache filename based on the generation parameters
|
| 784 |
+
cache_key = (
|
| 785 |
+
f"gen_shared_prefix_{args.gsp_num_groups}_{args.gsp_prompts_per_group}_"
|
| 786 |
+
f"{args.gsp_system_prompt_len}_{args.gsp_question_len}_{args.gsp_output_len}_"
|
| 787 |
+
f"{tokenizer.__class__.__name__}.pkl"
|
| 788 |
+
)
|
| 789 |
+
return cache_dir / cache_key
|
| 790 |
+
|
| 791 |
+
|
| 792 |
+
def sample_generated_shared_prefix_requests(
|
| 793 |
+
num_groups: int,
|
| 794 |
+
prompts_per_group: int,
|
| 795 |
+
system_prompt_len: int,
|
| 796 |
+
question_len: int,
|
| 797 |
+
output_len: int,
|
| 798 |
+
tokenizer: PreTrainedTokenizerBase,
|
| 799 |
+
args: argparse.Namespace,
|
| 800 |
+
) -> List[Tuple[str, int, int]]:
|
| 801 |
+
"""Generate benchmark requests with shared system prompts using random tokens and caching."""
|
| 802 |
+
cache_path = get_gen_prefix_cache_path(args, tokenizer)
|
| 803 |
+
|
| 804 |
+
# Try to load from cache first
|
| 805 |
+
if cache_path.exists():
|
| 806 |
+
print(f"\nLoading cached generated input data from {cache_path}")
|
| 807 |
+
with open(cache_path, "rb") as f:
|
| 808 |
+
return pickle.load(f)
|
| 809 |
+
|
| 810 |
+
print("\nGenerating new input data...")
|
| 811 |
+
|
| 812 |
+
# Generate system prompts for each group
|
| 813 |
+
system_prompts = []
|
| 814 |
+
for _ in range(num_groups):
|
| 815 |
+
system_prompt = gen_prompt(tokenizer, system_prompt_len)
|
| 816 |
+
system_prompts.append(system_prompt)
|
| 817 |
+
|
| 818 |
+
# Generate questions
|
| 819 |
+
questions = []
|
| 820 |
+
for _ in range(num_groups * prompts_per_group):
|
| 821 |
+
question = gen_prompt(tokenizer, question_len)
|
| 822 |
+
questions.append(question)
|
| 823 |
+
|
| 824 |
+
# Combine system prompts with questions
|
| 825 |
+
input_requests = []
|
| 826 |
+
total_input_tokens = 0
|
| 827 |
+
total_output_tokens = 0
|
| 828 |
+
|
| 829 |
+
for group_idx in tqdm(range(num_groups), desc="Generating system prompt"):
|
| 830 |
+
system_prompt = system_prompts[group_idx]
|
| 831 |
+
for prompt_idx in tqdm(
|
| 832 |
+
range(prompts_per_group), desc="Generating questions", leave=False
|
| 833 |
+
):
|
| 834 |
+
question = questions[group_idx * prompts_per_group + prompt_idx]
|
| 835 |
+
full_prompt = f"{system_prompt}\n\n{question}"
|
| 836 |
+
prompt_len = len(tokenizer.encode(full_prompt))
|
| 837 |
+
|
| 838 |
+
input_requests.append((full_prompt, prompt_len, output_len))
|
| 839 |
+
total_input_tokens += prompt_len
|
| 840 |
+
total_output_tokens += output_len
|
| 841 |
+
|
| 842 |
+
# Shuffle questions
|
| 843 |
+
random.shuffle(input_requests)
|
| 844 |
+
|
| 845 |
+
# Print statistics
|
| 846 |
+
print(f"\nGenerated shared prefix dataset statistics:")
|
| 847 |
+
print(f"Number of groups: {num_groups}")
|
| 848 |
+
print(f"Prompts per group: {prompts_per_group}")
|
| 849 |
+
print(f"Total prompts: {len(input_requests)}")
|
| 850 |
+
print(f"Total input tokens: {total_input_tokens}")
|
| 851 |
+
print(f"Total output tokens: {total_output_tokens}")
|
| 852 |
+
print(
|
| 853 |
+
f"Average system prompt length: {sum(len(tokenizer.encode(sp)) for sp in system_prompts) / len(system_prompts):.1f} tokens"
|
| 854 |
+
)
|
| 855 |
+
print(
|
| 856 |
+
f"Average question length: {sum(len(tokenizer.encode(q)) for q in questions) / len(questions):.1f} tokens\n"
|
| 857 |
+
)
|
| 858 |
+
|
| 859 |
+
# Save to cache
|
| 860 |
+
cache_path.parent.mkdir(parents=True, exist_ok=True)
|
| 861 |
+
print(f"Caching generated input data to {cache_path}")
|
| 862 |
+
with open(cache_path, "wb") as f:
|
| 863 |
+
pickle.dump(input_requests, f)
|
| 864 |
+
|
| 865 |
+
return input_requests
|
| 866 |
+
|
| 867 |
+
|
| 868 |
+
async def get_request(
|
| 869 |
+
input_requests: List[Tuple[str, int, int]],
|
| 870 |
+
request_rate: float,
|
| 871 |
+
) -> AsyncGenerator[Tuple[str, int, int], None]:
|
| 872 |
+
input_requests = iter(input_requests)
|
| 873 |
+
for request in input_requests:
|
| 874 |
+
yield request
|
| 875 |
+
|
| 876 |
+
if request_rate == float("inf"):
|
| 877 |
+
# If the request rate is infinity, then we don't need to wait.
|
| 878 |
+
continue
|
| 879 |
+
|
| 880 |
+
# Sample the request interval from the exponential distribution.
|
| 881 |
+
interval = np.random.exponential(1.0 / request_rate)
|
| 882 |
+
# The next request will be sent after the interval.
|
| 883 |
+
await asyncio.sleep(interval)
|
| 884 |
+
|
| 885 |
+
|
| 886 |
+
def calculate_metrics(
|
| 887 |
+
input_requests: List[Tuple[str, int, int]],
|
| 888 |
+
outputs: List[RequestFuncOutput],
|
| 889 |
+
dur_s: float,
|
| 890 |
+
tokenizer: PreTrainedTokenizerBase,
|
| 891 |
+
backend: str,
|
| 892 |
+
) -> Tuple[BenchmarkMetrics, List[int]]:
|
| 893 |
+
output_lens: List[int] = []
|
| 894 |
+
retokenized_output_lens: List[int] = []
|
| 895 |
+
total_input = 0
|
| 896 |
+
completed = 0
|
| 897 |
+
itls: List[float] = []
|
| 898 |
+
tpots: List[float] = []
|
| 899 |
+
ttfts: List[float] = []
|
| 900 |
+
e2e_latencies: List[float] = []
|
| 901 |
+
for i in range(len(outputs)):
|
| 902 |
+
if outputs[i].success:
|
| 903 |
+
output_len = outputs[i].output_len
|
| 904 |
+
output_lens.append(output_len)
|
| 905 |
+
retokenized_output_len = len(
|
| 906 |
+
tokenizer.encode(outputs[i].generated_text, add_special_tokens=False)
|
| 907 |
+
)
|
| 908 |
+
retokenized_output_lens.append(retokenized_output_len)
|
| 909 |
+
total_input += input_requests[i][1]
|
| 910 |
+
if output_len > 1:
|
| 911 |
+
tpots.append((outputs[i].latency - outputs[i].ttft) / (output_len - 1))
|
| 912 |
+
itls += outputs[i].itl
|
| 913 |
+
ttfts.append(outputs[i].ttft)
|
| 914 |
+
|
| 915 |
+
e2e_latencies.append(outputs[i].latency)
|
| 916 |
+
|
| 917 |
+
completed += 1
|
| 918 |
+
else:
|
| 919 |
+
output_lens.append(0)
|
| 920 |
+
retokenized_output_lens.append(0)
|
| 921 |
+
|
| 922 |
+
if completed == 0:
|
| 923 |
+
warnings.warn(
|
| 924 |
+
"All requests failed. This is likely due to a misconfiguration "
|
| 925 |
+
"on the benchmark arguments.",
|
| 926 |
+
stacklevel=2,
|
| 927 |
+
)
|
| 928 |
+
metrics = BenchmarkMetrics(
|
| 929 |
+
completed=completed,
|
| 930 |
+
total_input=total_input,
|
| 931 |
+
total_output=sum(output_lens),
|
| 932 |
+
total_output_retokenized=sum(retokenized_output_lens),
|
| 933 |
+
request_throughput=completed / dur_s,
|
| 934 |
+
input_throughput=total_input / dur_s,
|
| 935 |
+
output_throughput=sum(output_lens) / dur_s,
|
| 936 |
+
output_throughput_retokenized=sum(retokenized_output_lens) / dur_s,
|
| 937 |
+
total_throughput=(total_input + sum(output_lens)) / dur_s,
|
| 938 |
+
total_throughput_retokenized=(total_input + sum(retokenized_output_lens))
|
| 939 |
+
/ dur_s,
|
| 940 |
+
mean_ttft_ms=np.mean(ttfts or 0)
|
| 941 |
+
* 1000, # ttfts is empty if streaming is not supported by backend
|
| 942 |
+
median_ttft_ms=np.median(ttfts or 0) * 1000,
|
| 943 |
+
std_ttft_ms=np.std(ttfts or 0) * 1000,
|
| 944 |
+
p99_ttft_ms=np.percentile(ttfts or 0, 99) * 1000,
|
| 945 |
+
mean_tpot_ms=np.mean(tpots or 0) * 1000,
|
| 946 |
+
median_tpot_ms=np.median(tpots or 0) * 1000,
|
| 947 |
+
std_tpot_ms=np.std(tpots or 0) * 1000,
|
| 948 |
+
p99_tpot_ms=np.percentile(tpots or 0, 99) * 1000,
|
| 949 |
+
mean_itl_ms=np.mean(itls or 0) * 1000,
|
| 950 |
+
median_itl_ms=np.median(itls or 0) * 1000,
|
| 951 |
+
std_itl_ms=np.std(itls or 0) * 1000,
|
| 952 |
+
p95_itl_ms=np.percentile(itls or 0, 95) * 1000,
|
| 953 |
+
p99_itl_ms=np.percentile(itls or 0, 99) * 1000,
|
| 954 |
+
max_itl_ms=np.max(itls or 0) * 1000,
|
| 955 |
+
mean_e2e_latency_ms=np.mean(e2e_latencies) * 1000,
|
| 956 |
+
median_e2e_latency_ms=np.median(e2e_latencies) * 1000,
|
| 957 |
+
std_e2e_latency_ms=np.std(e2e_latencies) * 1000,
|
| 958 |
+
p99_e2e_latency_ms=np.percentile(e2e_latencies, 99) * 1000,
|
| 959 |
+
concurrency=np.sum(e2e_latencies) / dur_s,
|
| 960 |
+
)
|
| 961 |
+
|
| 962 |
+
return metrics, output_lens
|
| 963 |
+
|
| 964 |
+
|
| 965 |
+
async def benchmark(
|
| 966 |
+
backend: str,
|
| 967 |
+
api_url: str,
|
| 968 |
+
base_url: str,
|
| 969 |
+
model_id: str,
|
| 970 |
+
tokenizer: PreTrainedTokenizerBase,
|
| 971 |
+
input_requests: List[Tuple[str, int, int]],
|
| 972 |
+
request_rate: float,
|
| 973 |
+
max_concurrency: Optional[int],
|
| 974 |
+
disable_tqdm: bool,
|
| 975 |
+
lora_names: List[str],
|
| 976 |
+
extra_request_body: Dict[str, Any],
|
| 977 |
+
profile: bool,
|
| 978 |
+
pd_seperated: bool = False,
|
| 979 |
+
flush_cache: bool = False,
|
| 980 |
+
warmup_requests: int = 1,
|
| 981 |
+
):
|
| 982 |
+
if backend in ASYNC_REQUEST_FUNCS:
|
| 983 |
+
request_func = ASYNC_REQUEST_FUNCS[backend]
|
| 984 |
+
else:
|
| 985 |
+
raise ValueError(f"Unknown backend: {backend}")
|
| 986 |
+
|
| 987 |
+
# Limit concurrency
|
| 988 |
+
# From https://github.com/vllm-project/vllm/pull/9390
|
| 989 |
+
semaphore = asyncio.Semaphore(max_concurrency) if max_concurrency else None
|
| 990 |
+
|
| 991 |
+
async def limited_request_func(request_func_input, pbar):
|
| 992 |
+
if semaphore is None:
|
| 993 |
+
return await request_func(request_func_input=request_func_input, pbar=pbar)
|
| 994 |
+
async with semaphore:
|
| 995 |
+
return await request_func(request_func_input=request_func_input, pbar=pbar)
|
| 996 |
+
|
| 997 |
+
# Warmup
|
| 998 |
+
print(f"Starting warmup with {warmup_requests} sequences...")
|
| 999 |
+
|
| 1000 |
+
# Use the first request for all warmup iterations
|
| 1001 |
+
test_prompt, test_prompt_len, test_output_len = input_requests[0]
|
| 1002 |
+
if lora_names is not None and len(lora_names) != 0:
|
| 1003 |
+
lora_name = lora_names[0]
|
| 1004 |
+
else:
|
| 1005 |
+
lora_name = None
|
| 1006 |
+
|
| 1007 |
+
# Create the test input once
|
| 1008 |
+
test_input = RequestFuncInput(
|
| 1009 |
+
model=model_id,
|
| 1010 |
+
prompt=test_prompt,
|
| 1011 |
+
api_url=api_url,
|
| 1012 |
+
prompt_len=test_prompt_len,
|
| 1013 |
+
output_len=min(test_output_len, 32),
|
| 1014 |
+
lora_name=lora_name,
|
| 1015 |
+
extra_request_body=extra_request_body,
|
| 1016 |
+
)
|
| 1017 |
+
|
| 1018 |
+
# Run warmup requests
|
| 1019 |
+
warmup_tasks = []
|
| 1020 |
+
for _ in range(warmup_requests):
|
| 1021 |
+
warmup_tasks.append(
|
| 1022 |
+
asyncio.create_task(request_func(request_func_input=test_input))
|
| 1023 |
+
)
|
| 1024 |
+
|
| 1025 |
+
warmup_outputs = await asyncio.gather(*warmup_tasks)
|
| 1026 |
+
|
| 1027 |
+
# Check if at least one warmup request succeeded
|
| 1028 |
+
if warmup_requests > 0 and not any(output.success for output in warmup_outputs):
|
| 1029 |
+
raise ValueError(
|
| 1030 |
+
"Warmup failed - Please make sure benchmark arguments "
|
| 1031 |
+
f"are correctly specified. Error: {warmup_outputs[0].error}"
|
| 1032 |
+
)
|
| 1033 |
+
else:
|
| 1034 |
+
print(
|
| 1035 |
+
f"Warmup completed with {args.warmup_requests} sequences. Starting main benchmark run..."
|
| 1036 |
+
)
|
| 1037 |
+
|
| 1038 |
+
# Flush cache
|
| 1039 |
+
if ("sglang" in backend and _get_bool_env_var("SGLANG_IS_IN_CI")) or flush_cache:
|
| 1040 |
+
requests.post(base_url + "/flush_cache", headers=get_auth_headers())
|
| 1041 |
+
|
| 1042 |
+
time.sleep(1.0)
|
| 1043 |
+
|
| 1044 |
+
# Start profiler
|
| 1045 |
+
if profile:
|
| 1046 |
+
print("Starting profiler...")
|
| 1047 |
+
profile_output = await async_request_profile(
|
| 1048 |
+
api_url=base_url + "/start_profile"
|
| 1049 |
+
)
|
| 1050 |
+
if profile_output.success:
|
| 1051 |
+
print("Profiler started")
|
| 1052 |
+
|
| 1053 |
+
pbar = None if disable_tqdm else tqdm(total=len(input_requests))
|
| 1054 |
+
|
| 1055 |
+
# Run all requests
|
| 1056 |
+
benchmark_start_time = time.perf_counter()
|
| 1057 |
+
tasks: List[asyncio.Task] = []
|
| 1058 |
+
async for request in get_request(input_requests, request_rate):
|
| 1059 |
+
prompt, prompt_len, output_len = request
|
| 1060 |
+
if lora_names is not None and len(lora_names) != 0:
|
| 1061 |
+
idx = random.randint(0, len(lora_names) - 1)
|
| 1062 |
+
lora_name = lora_names[idx]
|
| 1063 |
+
else:
|
| 1064 |
+
lora_name = None
|
| 1065 |
+
|
| 1066 |
+
request_func_input = RequestFuncInput(
|
| 1067 |
+
model=model_id,
|
| 1068 |
+
prompt=prompt,
|
| 1069 |
+
api_url=api_url,
|
| 1070 |
+
prompt_len=prompt_len,
|
| 1071 |
+
output_len=output_len,
|
| 1072 |
+
lora_name=lora_name,
|
| 1073 |
+
extra_request_body=extra_request_body,
|
| 1074 |
+
)
|
| 1075 |
+
tasks.append(
|
| 1076 |
+
asyncio.create_task(
|
| 1077 |
+
limited_request_func(request_func_input=request_func_input, pbar=pbar)
|
| 1078 |
+
)
|
| 1079 |
+
)
|
| 1080 |
+
outputs: List[RequestFuncOutput] = await asyncio.gather(*tasks)
|
| 1081 |
+
|
| 1082 |
+
# Stop profiler
|
| 1083 |
+
if profile:
|
| 1084 |
+
print("Stopping profiler...")
|
| 1085 |
+
profile_output = await async_request_profile(api_url=base_url + "/stop_profile")
|
| 1086 |
+
if profile_output.success:
|
| 1087 |
+
print("Profiler stopped")
|
| 1088 |
+
|
| 1089 |
+
if pbar is not None:
|
| 1090 |
+
pbar.close()
|
| 1091 |
+
|
| 1092 |
+
if "sglang" in backend:
|
| 1093 |
+
server_info = requests.get(base_url + "/get_server_info")
|
| 1094 |
+
if pd_seperated:
|
| 1095 |
+
accept_length = server_info.json()["decode"][0].get(
|
| 1096 |
+
"avg_spec_accept_length", None
|
| 1097 |
+
)
|
| 1098 |
+
else:
|
| 1099 |
+
accept_length = server_info.json().get("avg_spec_accept_length", None)
|
| 1100 |
+
else:
|
| 1101 |
+
accept_length = None
|
| 1102 |
+
|
| 1103 |
+
# Compute metrics and print results
|
| 1104 |
+
benchmark_duration = time.perf_counter() - benchmark_start_time
|
| 1105 |
+
metrics, output_lens = calculate_metrics(
|
| 1106 |
+
input_requests=input_requests,
|
| 1107 |
+
outputs=outputs,
|
| 1108 |
+
dur_s=benchmark_duration,
|
| 1109 |
+
tokenizer=tokenizer,
|
| 1110 |
+
backend=backend,
|
| 1111 |
+
)
|
| 1112 |
+
|
| 1113 |
+
print("\n{s:{c}^{n}}".format(s=" Serving Benchmark Result ", n=50, c="="))
|
| 1114 |
+
print("{:<40} {:<10}".format("Backend:", backend))
|
| 1115 |
+
print("{:<40} {:<10}".format("Traffic request rate:", request_rate))
|
| 1116 |
+
print(
|
| 1117 |
+
"{:<40} {:<10}".format(
|
| 1118 |
+
"Max reqeuest concurrency:",
|
| 1119 |
+
max_concurrency if max_concurrency else "not set",
|
| 1120 |
+
)
|
| 1121 |
+
)
|
| 1122 |
+
print("{:<40} {:<10}".format("Successful requests:", metrics.completed))
|
| 1123 |
+
print("{:<40} {:<10.2f}".format("Benchmark duration (s):", benchmark_duration))
|
| 1124 |
+
print("{:<40} {:<10}".format("Total input tokens:", metrics.total_input))
|
| 1125 |
+
print("{:<40} {:<10}".format("Total generated tokens:", metrics.total_output))
|
| 1126 |
+
print(
|
| 1127 |
+
"{:<40} {:<10}".format(
|
| 1128 |
+
"Total generated tokens (retokenized):", metrics.total_output_retokenized
|
| 1129 |
+
)
|
| 1130 |
+
)
|
| 1131 |
+
print(
|
| 1132 |
+
"{:<40} {:<10.2f}".format(
|
| 1133 |
+
"Request throughput (req/s):", metrics.request_throughput
|
| 1134 |
+
)
|
| 1135 |
+
)
|
| 1136 |
+
print(
|
| 1137 |
+
"{:<40} {:<10.2f}".format(
|
| 1138 |
+
"Input token throughput (tok/s):", metrics.input_throughput
|
| 1139 |
+
)
|
| 1140 |
+
)
|
| 1141 |
+
print(
|
| 1142 |
+
"{:<40} {:<10.2f}".format(
|
| 1143 |
+
"Output token throughput (tok/s):", metrics.output_throughput
|
| 1144 |
+
)
|
| 1145 |
+
)
|
| 1146 |
+
print(
|
| 1147 |
+
"{:<40} {:<10.2f}".format(
|
| 1148 |
+
"Total token throughput (tok/s):", metrics.total_throughput
|
| 1149 |
+
)
|
| 1150 |
+
)
|
| 1151 |
+
print("{:<40} {:<10.2f}".format("Concurrency:", metrics.concurrency))
|
| 1152 |
+
if accept_length:
|
| 1153 |
+
print("{:<40} {:<10.2f}".format("Accept length:", accept_length))
|
| 1154 |
+
print("{s:{c}^{n}}".format(s="End-to-End Latency", n=50, c="-"))
|
| 1155 |
+
print(
|
| 1156 |
+
"{:<40} {:<10.2f}".format("Mean E2E Latency (ms):", metrics.mean_e2e_latency_ms)
|
| 1157 |
+
)
|
| 1158 |
+
print(
|
| 1159 |
+
"{:<40} {:<10.2f}".format(
|
| 1160 |
+
"Median E2E Latency (ms):", metrics.median_e2e_latency_ms
|
| 1161 |
+
)
|
| 1162 |
+
)
|
| 1163 |
+
print("{s:{c}^{n}}".format(s="Time to First Token", n=50, c="-"))
|
| 1164 |
+
print("{:<40} {:<10.2f}".format("Mean TTFT (ms):", metrics.mean_ttft_ms))
|
| 1165 |
+
print("{:<40} {:<10.2f}".format("Median TTFT (ms):", metrics.median_ttft_ms))
|
| 1166 |
+
print("{:<40} {:<10.2f}".format("P99 TTFT (ms):", metrics.p99_ttft_ms))
|
| 1167 |
+
print("{s:{c}^{n}}".format(s="Inter-Token Latency", n=50, c="-"))
|
| 1168 |
+
print("{:<40} {:<10.2f}".format("Mean ITL (ms):", metrics.mean_itl_ms))
|
| 1169 |
+
print("{:<40} {:<10.2f}".format("Median ITL (ms):", metrics.median_itl_ms))
|
| 1170 |
+
print("{:<40} {:<10.2f}".format("P95 ITL (ms):", metrics.p95_itl_ms))
|
| 1171 |
+
print("{:<40} {:<10.2f}".format("P99 ITL (ms):", metrics.p99_itl_ms))
|
| 1172 |
+
print("{:<40} {:<10.2f}".format("Max ITL (ms):", metrics.max_itl_ms))
|
| 1173 |
+
print("=" * 50)
|
| 1174 |
+
|
| 1175 |
+
if (
|
| 1176 |
+
metrics.median_ttft_ms is not None
|
| 1177 |
+
and metrics.mean_itl_ms is not None
|
| 1178 |
+
and metrics.output_throughput is not None
|
| 1179 |
+
):
|
| 1180 |
+
result = {
|
| 1181 |
+
# Arguments
|
| 1182 |
+
"backend": args.backend,
|
| 1183 |
+
"dataset_name": args.dataset_name,
|
| 1184 |
+
"request_rate": request_rate,
|
| 1185 |
+
"max_concurrency": max_concurrency,
|
| 1186 |
+
"sharegpt_output_len": args.sharegpt_output_len,
|
| 1187 |
+
"random_input_len": args.random_input_len,
|
| 1188 |
+
"random_output_len": args.random_output_len,
|
| 1189 |
+
"random_range_ratio": args.random_range_ratio,
|
| 1190 |
+
# Results
|
| 1191 |
+
"duration": benchmark_duration,
|
| 1192 |
+
"completed": metrics.completed,
|
| 1193 |
+
"total_input_tokens": metrics.total_input,
|
| 1194 |
+
"total_output_tokens": metrics.total_output,
|
| 1195 |
+
"total_output_tokens_retokenized": metrics.total_output_retokenized,
|
| 1196 |
+
"request_throughput": metrics.request_throughput,
|
| 1197 |
+
"input_throughput": metrics.input_throughput,
|
| 1198 |
+
"output_throughput": metrics.output_throughput,
|
| 1199 |
+
"mean_e2e_latency_ms": metrics.mean_e2e_latency_ms,
|
| 1200 |
+
"median_e2e_latency_ms": metrics.median_e2e_latency_ms,
|
| 1201 |
+
"std_e2e_latency_ms": metrics.std_e2e_latency_ms,
|
| 1202 |
+
"p99_e2e_latency_ms": metrics.p99_e2e_latency_ms,
|
| 1203 |
+
"mean_ttft_ms": metrics.mean_ttft_ms,
|
| 1204 |
+
"median_ttft_ms": metrics.median_ttft_ms,
|
| 1205 |
+
"std_ttft_ms": metrics.std_ttft_ms,
|
| 1206 |
+
"p99_ttft_ms": metrics.p99_ttft_ms,
|
| 1207 |
+
"mean_tpot_ms": metrics.mean_tpot_ms,
|
| 1208 |
+
"median_tpot_ms": metrics.median_tpot_ms,
|
| 1209 |
+
"std_tpot_ms": metrics.std_tpot_ms,
|
| 1210 |
+
"p99_tpot_ms": metrics.p99_tpot_ms,
|
| 1211 |
+
"mean_itl_ms": metrics.mean_itl_ms,
|
| 1212 |
+
"median_itl_ms": metrics.median_itl_ms,
|
| 1213 |
+
"std_itl_ms": metrics.std_itl_ms,
|
| 1214 |
+
"p95_itl_ms": metrics.p95_itl_ms,
|
| 1215 |
+
"p99_itl_ms": metrics.p99_itl_ms,
|
| 1216 |
+
"concurrency": metrics.concurrency,
|
| 1217 |
+
"accept_length": accept_length,
|
| 1218 |
+
}
|
| 1219 |
+
else:
|
| 1220 |
+
print(f"Error running benchmark for request rate: {request_rate}")
|
| 1221 |
+
print("-" * 30)
|
| 1222 |
+
|
| 1223 |
+
# Determine output file name
|
| 1224 |
+
if args.output_file:
|
| 1225 |
+
output_file_name = args.output_file
|
| 1226 |
+
else:
|
| 1227 |
+
now = datetime.now().strftime("%m%d")
|
| 1228 |
+
if args.dataset_name.startswith("random"):
|
| 1229 |
+
output_file_name = f"{args.backend}_{now}_{args.num_prompts}_{args.random_input_len}_{args.random_output_len}.jsonl"
|
| 1230 |
+
else:
|
| 1231 |
+
output_file_name = f"{args.backend}_{now}_{args.num_prompts}_sharegpt.jsonl"
|
| 1232 |
+
|
| 1233 |
+
# Append results to a JSONL file
|
| 1234 |
+
with open(output_file_name, "a") as file:
|
| 1235 |
+
file.write(json.dumps(result) + "\n")
|
| 1236 |
+
|
| 1237 |
+
result.update(
|
| 1238 |
+
{
|
| 1239 |
+
"input_lens": [output.prompt_len for output in outputs],
|
| 1240 |
+
"output_lens": output_lens,
|
| 1241 |
+
"ttfts": [output.ttft for output in outputs],
|
| 1242 |
+
"itls": [output.itl for output in outputs],
|
| 1243 |
+
"generated_texts": [output.generated_text for output in outputs],
|
| 1244 |
+
"errors": [output.error for output in outputs],
|
| 1245 |
+
}
|
| 1246 |
+
)
|
| 1247 |
+
return result
|
| 1248 |
+
|
| 1249 |
+
|
| 1250 |
+
def check_chat_template(model_path):
|
| 1251 |
+
try:
|
| 1252 |
+
tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)
|
| 1253 |
+
return "chat_template" in tokenizer.init_kwargs
|
| 1254 |
+
except Exception as e:
|
| 1255 |
+
print(f"Fail to load tokenizer config with error={e}")
|
| 1256 |
+
return False
|
| 1257 |
+
|
| 1258 |
+
|
| 1259 |
+
def set_global_args(args_: argparse.Namespace):
|
| 1260 |
+
"""Set the global args."""
|
| 1261 |
+
global args
|
| 1262 |
+
args = args_
|
| 1263 |
+
|
| 1264 |
+
|
| 1265 |
+
def run_benchmark(args_: argparse.Namespace):
|
| 1266 |
+
global args
|
| 1267 |
+
args = args_
|
| 1268 |
+
|
| 1269 |
+
# Set default value for max_concurrency if not present
|
| 1270 |
+
if not hasattr(args, "max_concurrency"):
|
| 1271 |
+
args.max_concurrency = None
|
| 1272 |
+
|
| 1273 |
+
# Set default value for warmup_requests if not present
|
| 1274 |
+
if not hasattr(args, "warmup_requests"):
|
| 1275 |
+
args.warmup_requests = 1
|
| 1276 |
+
|
| 1277 |
+
print(f"benchmark_args={args}")
|
| 1278 |
+
|
| 1279 |
+
# Set global environments
|
| 1280 |
+
set_ulimit()
|
| 1281 |
+
random.seed(args.seed)
|
| 1282 |
+
np.random.seed(args.seed)
|
| 1283 |
+
|
| 1284 |
+
extra_request_body = {}
|
| 1285 |
+
if args.extra_request_body:
|
| 1286 |
+
extra_request_body = json.loads(args.extra_request_body)
|
| 1287 |
+
|
| 1288 |
+
# Set url
|
| 1289 |
+
if args.port is None:
|
| 1290 |
+
args.port = {
|
| 1291 |
+
"sglang": 30000,
|
| 1292 |
+
"sglang-native": 30000,
|
| 1293 |
+
"sglang-oai": 30000,
|
| 1294 |
+
"lmdeploy": 23333,
|
| 1295 |
+
"vllm": 8000,
|
| 1296 |
+
"trt": 8000,
|
| 1297 |
+
"gserver": 9988,
|
| 1298 |
+
"truss": 8080,
|
| 1299 |
+
}.get(args.backend, 30000)
|
| 1300 |
+
|
| 1301 |
+
model_url = (
|
| 1302 |
+
f"{args.base_url}/v1/models"
|
| 1303 |
+
if args.base_url
|
| 1304 |
+
else f"http://{args.host}:{args.port}/v1/models"
|
| 1305 |
+
)
|
| 1306 |
+
|
| 1307 |
+
if args.backend in ["sglang", "sglang-native"]:
|
| 1308 |
+
api_url = (
|
| 1309 |
+
f"{args.base_url}/generate"
|
| 1310 |
+
if args.base_url
|
| 1311 |
+
else f"http://{args.host}:{args.port}/generate"
|
| 1312 |
+
)
|
| 1313 |
+
elif args.backend in ["sglang-oai", "vllm", "lmdeploy"]:
|
| 1314 |
+
api_url = (
|
| 1315 |
+
f"{args.base_url}/v1/completions"
|
| 1316 |
+
if args.base_url
|
| 1317 |
+
else f"http://{args.host}:{args.port}/v1/completions"
|
| 1318 |
+
)
|
| 1319 |
+
elif args.backend == "trt":
|
| 1320 |
+
api_url = (
|
| 1321 |
+
f"{args.base_url}/v2/models/ensemble/generate_stream"
|
| 1322 |
+
if args.base_url
|
| 1323 |
+
else f"http://{args.host}:{args.port}/v2/models/ensemble/generate_stream"
|
| 1324 |
+
)
|
| 1325 |
+
if args.model is None:
|
| 1326 |
+
print("Please provide a model using `--model` when using `trt` backend.")
|
| 1327 |
+
sys.exit(1)
|
| 1328 |
+
elif args.backend == "gserver":
|
| 1329 |
+
api_url = args.base_url if args.base_url else f"{args.host}:{args.port}"
|
| 1330 |
+
args.model = args.model or "default"
|
| 1331 |
+
elif args.backend == "truss":
|
| 1332 |
+
api_url = (
|
| 1333 |
+
f"{args.base_url}/v1/models/model:predict"
|
| 1334 |
+
if args.base_url
|
| 1335 |
+
else f"http://{args.host}:{args.port}/v1/models/model:predict"
|
| 1336 |
+
)
|
| 1337 |
+
base_url = (
|
| 1338 |
+
f"http://{args.host}:{args.port}" if args.base_url is None else args.base_url
|
| 1339 |
+
)
|
| 1340 |
+
|
| 1341 |
+
# Get model name
|
| 1342 |
+
if args.model is None:
|
| 1343 |
+
if args.backend == "truss":
|
| 1344 |
+
print(
|
| 1345 |
+
"Please provide a model with `--model` when using truss backend. e.g. --model meta-llama/Llama-3.1-8B-Instruct"
|
| 1346 |
+
)
|
| 1347 |
+
sys.exit(1)
|
| 1348 |
+
try:
|
| 1349 |
+
response = requests.get(model_url, headers=get_auth_headers())
|
| 1350 |
+
model_list = response.json().get("data", [])
|
| 1351 |
+
args.model = model_list[0]["id"] if model_list else None
|
| 1352 |
+
except Exception as e:
|
| 1353 |
+
print(f"Failed to fetch model from {model_url}. Error: {e}")
|
| 1354 |
+
print(
|
| 1355 |
+
"Please specify the correct host and port using `--host` and `--port`."
|
| 1356 |
+
)
|
| 1357 |
+
sys.exit(1)
|
| 1358 |
+
|
| 1359 |
+
if args.model is None:
|
| 1360 |
+
print("No model specified or found. Please provide a model using `--model`.")
|
| 1361 |
+
sys.exit(1)
|
| 1362 |
+
|
| 1363 |
+
if not check_chat_template(args.model):
|
| 1364 |
+
print(
|
| 1365 |
+
"\nWARNING It is recommended to use the `Chat` or `Instruct` model for benchmarking.\n"
|
| 1366 |
+
"Because when the tokenizer counts the output tokens, if there is gibberish, it might count incorrectly.\n"
|
| 1367 |
+
)
|
| 1368 |
+
|
| 1369 |
+
print(f"{args}\n")
|
| 1370 |
+
|
| 1371 |
+
# Read dataset
|
| 1372 |
+
backend = args.backend
|
| 1373 |
+
model_id = args.model
|
| 1374 |
+
tokenizer_id = args.tokenizer if args.tokenizer is not None else args.model
|
| 1375 |
+
tokenizer = get_tokenizer(tokenizer_id)
|
| 1376 |
+
input_requests = get_dataset(args, tokenizer)
|
| 1377 |
+
|
| 1378 |
+
# compatible with SimpleNamespace
|
| 1379 |
+
if not hasattr(args, "flush_cache"):
|
| 1380 |
+
args.flush_cache = False
|
| 1381 |
+
|
| 1382 |
+
return asyncio.run(
|
| 1383 |
+
benchmark(
|
| 1384 |
+
backend=backend,
|
| 1385 |
+
api_url=api_url,
|
| 1386 |
+
base_url=base_url,
|
| 1387 |
+
model_id=model_id,
|
| 1388 |
+
tokenizer=tokenizer,
|
| 1389 |
+
input_requests=input_requests,
|
| 1390 |
+
request_rate=args.request_rate,
|
| 1391 |
+
max_concurrency=args.max_concurrency,
|
| 1392 |
+
disable_tqdm=args.disable_tqdm,
|
| 1393 |
+
lora_names=args.lora_name,
|
| 1394 |
+
extra_request_body=extra_request_body,
|
| 1395 |
+
profile=args.profile,
|
| 1396 |
+
pd_seperated=args.pd_seperated,
|
| 1397 |
+
flush_cache=args.flush_cache,
|
| 1398 |
+
)
|
| 1399 |
+
)
|
| 1400 |
+
|
| 1401 |
+
|
| 1402 |
+
def set_ulimit(target_soft_limit=65535):
|
| 1403 |
+
resource_type = resource.RLIMIT_NOFILE
|
| 1404 |
+
current_soft, current_hard = resource.getrlimit(resource_type)
|
| 1405 |
+
|
| 1406 |
+
if current_soft < target_soft_limit:
|
| 1407 |
+
try:
|
| 1408 |
+
resource.setrlimit(resource_type, (target_soft_limit, current_hard))
|
| 1409 |
+
except ValueError as e:
|
| 1410 |
+
print(f"Fail to set RLIMIT_NOFILE: {e}")
|
| 1411 |
+
|
| 1412 |
+
|
| 1413 |
+
class LoRAPathAction(argparse.Action):
|
| 1414 |
+
def __call__(self, parser, namespace, values, option_string=None):
|
| 1415 |
+
setattr(namespace, self.dest, [])
|
| 1416 |
+
for lora_name in values:
|
| 1417 |
+
getattr(namespace, self.dest).append(lora_name)
|
| 1418 |
+
|
| 1419 |
+
|
| 1420 |
+
if __name__ == "__main__":
|
| 1421 |
+
parser = ArgumentParser(description="Benchmark the online serving throughput.")
|
| 1422 |
+
parser.add_argument(
|
| 1423 |
+
"--backend",
|
| 1424 |
+
type=str,
|
| 1425 |
+
choices=list(ASYNC_REQUEST_FUNCS.keys()),
|
| 1426 |
+
default="sglang",
|
| 1427 |
+
help="Must specify a backend, depending on the LLM Inference Engine.",
|
| 1428 |
+
)
|
| 1429 |
+
parser.add_argument(
|
| 1430 |
+
"--base-url",
|
| 1431 |
+
type=str,
|
| 1432 |
+
default=None,
|
| 1433 |
+
help="Server or API base url if not using http host and port.",
|
| 1434 |
+
)
|
| 1435 |
+
parser.add_argument(
|
| 1436 |
+
"--host", type=str, default="0.0.0.0", help="Default host is 0.0.0.0."
|
| 1437 |
+
)
|
| 1438 |
+
parser.add_argument(
|
| 1439 |
+
"--port",
|
| 1440 |
+
type=int,
|
| 1441 |
+
help="If not set, the default port is configured according to its default value for different LLM Inference Engines.",
|
| 1442 |
+
)
|
| 1443 |
+
parser.add_argument(
|
| 1444 |
+
"--dataset-name",
|
| 1445 |
+
type=str,
|
| 1446 |
+
default="sharegpt",
|
| 1447 |
+
choices=["sharegpt", "random", "random-ids", "generated-shared-prefix"],
|
| 1448 |
+
help="Name of the dataset to benchmark on.",
|
| 1449 |
+
)
|
| 1450 |
+
parser.add_argument(
|
| 1451 |
+
"--dataset-path", type=str, default="", help="Path to the dataset."
|
| 1452 |
+
)
|
| 1453 |
+
parser.add_argument(
|
| 1454 |
+
"--model",
|
| 1455 |
+
type=str,
|
| 1456 |
+
help="Name or path of the model. If not set, the default model will request /v1/models for conf.",
|
| 1457 |
+
)
|
| 1458 |
+
parser.add_argument(
|
| 1459 |
+
"--tokenizer",
|
| 1460 |
+
type=str,
|
| 1461 |
+
help="Name or path of the tokenizer. If not set, using the model conf.",
|
| 1462 |
+
)
|
| 1463 |
+
parser.add_argument(
|
| 1464 |
+
"--num-prompts",
|
| 1465 |
+
type=int,
|
| 1466 |
+
default=1000,
|
| 1467 |
+
help="Number of prompts to process. Default is 1000.",
|
| 1468 |
+
)
|
| 1469 |
+
parser.add_argument(
|
| 1470 |
+
"--sharegpt-output-len",
|
| 1471 |
+
type=int,
|
| 1472 |
+
default=None,
|
| 1473 |
+
help="Output length for each request. Overrides the output length from the ShareGPT dataset.",
|
| 1474 |
+
)
|
| 1475 |
+
parser.add_argument(
|
| 1476 |
+
"--sharegpt-context-len",
|
| 1477 |
+
type=int,
|
| 1478 |
+
default=None,
|
| 1479 |
+
help="The context length of the model for the ShareGPT dataset. Requests longer than the context length will be dropped.",
|
| 1480 |
+
)
|
| 1481 |
+
parser.add_argument(
|
| 1482 |
+
"--random-input-len",
|
| 1483 |
+
type=int,
|
| 1484 |
+
default=1024,
|
| 1485 |
+
help="Number of input tokens per request, used only for random dataset.",
|
| 1486 |
+
)
|
| 1487 |
+
parser.add_argument(
|
| 1488 |
+
"--random-output-len",
|
| 1489 |
+
default=1024,
|
| 1490 |
+
type=int,
|
| 1491 |
+
help="Number of output tokens per request, used only for random dataset.",
|
| 1492 |
+
)
|
| 1493 |
+
parser.add_argument(
|
| 1494 |
+
"--random-range-ratio",
|
| 1495 |
+
type=float,
|
| 1496 |
+
default=0.0,
|
| 1497 |
+
help="Range of sampled ratio of input/output length, "
|
| 1498 |
+
"used only for random dataset.",
|
| 1499 |
+
)
|
| 1500 |
+
parser.add_argument(
|
| 1501 |
+
"--request-rate",
|
| 1502 |
+
type=float,
|
| 1503 |
+
default=float("inf"),
|
| 1504 |
+
help="Number of requests per second. If this is inf, then all the requests are sent at time 0. "
|
| 1505 |
+
"Otherwise, we use Poisson process to synthesize the request arrival times. Default is inf.",
|
| 1506 |
+
)
|
| 1507 |
+
parser.add_argument(
|
| 1508 |
+
"--max-concurrency",
|
| 1509 |
+
type=int,
|
| 1510 |
+
default=None,
|
| 1511 |
+
help="Maximum number of concurrent requests. This can be used "
|
| 1512 |
+
"to help simulate an environment where a higher level component "
|
| 1513 |
+
"is enforcing a maximum number of concurrent requests. While the "
|
| 1514 |
+
"--request-rate argument controls the rate at which requests are "
|
| 1515 |
+
"initiated, this argument will control how many are actually allowed "
|
| 1516 |
+
"to execute at a time. This means that when used in combination, the "
|
| 1517 |
+
"actual request rate may be lower than specified with --request-rate, "
|
| 1518 |
+
"if the server is not processing requests fast enough to keep up.",
|
| 1519 |
+
)
|
| 1520 |
+
parser.add_argument("--output-file", type=str, help="Output JSONL file name.")
|
| 1521 |
+
parser.add_argument(
|
| 1522 |
+
"--disable-tqdm",
|
| 1523 |
+
action="store_true",
|
| 1524 |
+
help="Specify to disable tqdm progress bar.",
|
| 1525 |
+
)
|
| 1526 |
+
parser.add_argument(
|
| 1527 |
+
"--disable-stream",
|
| 1528 |
+
action="store_true",
|
| 1529 |
+
help="Disable streaming mode.",
|
| 1530 |
+
)
|
| 1531 |
+
parser.add_argument(
|
| 1532 |
+
"--return-logprob",
|
| 1533 |
+
action="store_true",
|
| 1534 |
+
help="Return logprob.",
|
| 1535 |
+
)
|
| 1536 |
+
parser.add_argument("--seed", type=int, default=1, help="The random seed.")
|
| 1537 |
+
parser.add_argument(
|
| 1538 |
+
"--disable-ignore-eos",
|
| 1539 |
+
action="store_true",
|
| 1540 |
+
help="Disable ignoring EOS.",
|
| 1541 |
+
)
|
| 1542 |
+
parser.add_argument(
|
| 1543 |
+
"--extra-request-body",
|
| 1544 |
+
metavar='{"key1": "value1", "key2": "value2"}',
|
| 1545 |
+
type=str,
|
| 1546 |
+
help="Append given JSON object to the request payload. You can use this to specify"
|
| 1547 |
+
"additional generate params like sampling params.",
|
| 1548 |
+
)
|
| 1549 |
+
parser.add_argument(
|
| 1550 |
+
"--apply-chat-template",
|
| 1551 |
+
action="store_true",
|
| 1552 |
+
help="Apply chat template",
|
| 1553 |
+
)
|
| 1554 |
+
parser.add_argument(
|
| 1555 |
+
"--profile",
|
| 1556 |
+
action="store_true",
|
| 1557 |
+
help="Use Torch Profiler. The endpoint must be launched with "
|
| 1558 |
+
"SGLANG_TORCH_PROFILER_DIR to enable profiler.",
|
| 1559 |
+
)
|
| 1560 |
+
parser.add_argument(
|
| 1561 |
+
"--lora-name",
|
| 1562 |
+
type=str,
|
| 1563 |
+
nargs="*",
|
| 1564 |
+
default=None,
|
| 1565 |
+
action=LoRAPathAction,
|
| 1566 |
+
help="The names of LoRA adapters. You can provide a list of names in the format {name} {name} {name}...",
|
| 1567 |
+
)
|
| 1568 |
+
parser.add_argument(
|
| 1569 |
+
"--prompt-suffix",
|
| 1570 |
+
type=str,
|
| 1571 |
+
default="",
|
| 1572 |
+
help="Suffix applied to the end of all user prompts, followed by assistant prompt suffix.",
|
| 1573 |
+
)
|
| 1574 |
+
parser.add_argument(
|
| 1575 |
+
"--pd-seperated",
|
| 1576 |
+
action="store_true",
|
| 1577 |
+
help="Benchmark PD disaggregation server",
|
| 1578 |
+
)
|
| 1579 |
+
parser.add_argument(
|
| 1580 |
+
"--flush-cache",
|
| 1581 |
+
action="store_true",
|
| 1582 |
+
help="Flush the cache before running the benchmark",
|
| 1583 |
+
)
|
| 1584 |
+
parser.add_argument(
|
| 1585 |
+
"--warmup-requests",
|
| 1586 |
+
type=int,
|
| 1587 |
+
default=1,
|
| 1588 |
+
help="Number of warmup requests to run before the benchmark",
|
| 1589 |
+
)
|
| 1590 |
+
|
| 1591 |
+
group = parser.add_argument_group("generated-shared-prefix dataset arguments")
|
| 1592 |
+
group.add_argument(
|
| 1593 |
+
"--gsp-num-groups",
|
| 1594 |
+
type=int,
|
| 1595 |
+
default=64,
|
| 1596 |
+
help="Number of system prompt groups for generated-shared-prefix dataset",
|
| 1597 |
+
)
|
| 1598 |
+
group.add_argument(
|
| 1599 |
+
"--gsp-prompts-per-group",
|
| 1600 |
+
type=int,
|
| 1601 |
+
default=16,
|
| 1602 |
+
help="Number of prompts per system prompt group for generated-shared-prefix dataset",
|
| 1603 |
+
)
|
| 1604 |
+
group.add_argument(
|
| 1605 |
+
"--gsp-system-prompt-len",
|
| 1606 |
+
type=int,
|
| 1607 |
+
default=2048,
|
| 1608 |
+
help="Target length in tokens for system prompts in generated-shared-prefix dataset",
|
| 1609 |
+
)
|
| 1610 |
+
group.add_argument(
|
| 1611 |
+
"--gsp-question-len",
|
| 1612 |
+
type=int,
|
| 1613 |
+
default=128,
|
| 1614 |
+
help="Target length in tokens for questions in generated-shared-prefix dataset",
|
| 1615 |
+
)
|
| 1616 |
+
group.add_argument(
|
| 1617 |
+
"--gsp-output-len",
|
| 1618 |
+
type=int,
|
| 1619 |
+
default=256,
|
| 1620 |
+
help="Target length in tokens for outputs in generated-shared-prefix dataset",
|
| 1621 |
+
)
|
| 1622 |
+
args = parser.parse_args()
|
| 1623 |
+
run_benchmark(args)
|
lib/python3.12/site-packages/sglang/check_env.py
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Check environment configurations and dependency versions."""
|
| 2 |
+
|
| 3 |
+
import importlib.metadata
|
| 4 |
+
import os
|
| 5 |
+
import resource
|
| 6 |
+
import subprocess
|
| 7 |
+
import sys
|
| 8 |
+
from collections import OrderedDict, defaultdict
|
| 9 |
+
|
| 10 |
+
import torch
|
| 11 |
+
|
| 12 |
+
from sglang.srt.utils import is_hip
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def is_cuda_v2():
|
| 16 |
+
return torch.version.cuda is not None
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
# List of packages to check versions
|
| 20 |
+
PACKAGE_LIST = [
|
| 21 |
+
"sglang",
|
| 22 |
+
"sgl_kernel",
|
| 23 |
+
"flashinfer",
|
| 24 |
+
"triton",
|
| 25 |
+
"transformers",
|
| 26 |
+
"torchao",
|
| 27 |
+
"numpy",
|
| 28 |
+
"aiohttp",
|
| 29 |
+
"fastapi",
|
| 30 |
+
"hf_transfer",
|
| 31 |
+
"huggingface_hub",
|
| 32 |
+
"interegular",
|
| 33 |
+
"modelscope",
|
| 34 |
+
"orjson",
|
| 35 |
+
"outlines",
|
| 36 |
+
"packaging",
|
| 37 |
+
"psutil",
|
| 38 |
+
"pydantic",
|
| 39 |
+
"multipart",
|
| 40 |
+
"zmq",
|
| 41 |
+
"torchao",
|
| 42 |
+
"uvicorn",
|
| 43 |
+
"uvloop",
|
| 44 |
+
"vllm",
|
| 45 |
+
"xgrammar",
|
| 46 |
+
"openai",
|
| 47 |
+
"tiktoken",
|
| 48 |
+
"anthropic",
|
| 49 |
+
"litellm",
|
| 50 |
+
"decord",
|
| 51 |
+
]
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def get_package_versions(packages):
|
| 55 |
+
"""
|
| 56 |
+
Get versions of specified packages.
|
| 57 |
+
"""
|
| 58 |
+
versions = {}
|
| 59 |
+
for package in packages:
|
| 60 |
+
package_name = package.split("==")[0].split(">=")[0].split("<=")[0]
|
| 61 |
+
try:
|
| 62 |
+
version = importlib.metadata.version(package_name)
|
| 63 |
+
versions[package_name] = version
|
| 64 |
+
except ModuleNotFoundError:
|
| 65 |
+
versions[package_name] = "Module Not Found"
|
| 66 |
+
return versions
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def get_cuda_info():
|
| 70 |
+
"""
|
| 71 |
+
Get CUDA-related information if available.
|
| 72 |
+
"""
|
| 73 |
+
if is_cuda_v2():
|
| 74 |
+
cuda_info = {"CUDA available": torch.cuda.is_available()}
|
| 75 |
+
|
| 76 |
+
if cuda_info["CUDA available"]:
|
| 77 |
+
cuda_info.update(_get_gpu_info())
|
| 78 |
+
cuda_info.update(_get_cuda_version_info())
|
| 79 |
+
|
| 80 |
+
return cuda_info
|
| 81 |
+
elif is_hip():
|
| 82 |
+
cuda_info = {"ROCM available": torch.cuda.is_available()}
|
| 83 |
+
|
| 84 |
+
if cuda_info["ROCM available"]:
|
| 85 |
+
cuda_info.update(_get_gpu_info())
|
| 86 |
+
cuda_info.update(_get_cuda_version_info())
|
| 87 |
+
|
| 88 |
+
return cuda_info
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def _get_gpu_info():
|
| 92 |
+
"""
|
| 93 |
+
Get information about available GPUs.
|
| 94 |
+
"""
|
| 95 |
+
devices = defaultdict(list)
|
| 96 |
+
capabilities = defaultdict(list)
|
| 97 |
+
for k in range(torch.cuda.device_count()):
|
| 98 |
+
devices[torch.cuda.get_device_name(k)].append(str(k))
|
| 99 |
+
capability = torch.cuda.get_device_capability(k)
|
| 100 |
+
capabilities[f"{capability[0]}.{capability[1]}"].append(str(k))
|
| 101 |
+
|
| 102 |
+
gpu_info = {}
|
| 103 |
+
for name, device_ids in devices.items():
|
| 104 |
+
gpu_info[f"GPU {','.join(device_ids)}"] = name
|
| 105 |
+
|
| 106 |
+
if len(capabilities) == 1:
|
| 107 |
+
# All GPUs have the same compute capability
|
| 108 |
+
cap, gpu_ids = list(capabilities.items())[0]
|
| 109 |
+
gpu_info[f"GPU {','.join(gpu_ids)} Compute Capability"] = cap
|
| 110 |
+
else:
|
| 111 |
+
# GPUs have different compute capabilities
|
| 112 |
+
for cap, gpu_ids in capabilities.items():
|
| 113 |
+
gpu_info[f"GPU {','.join(gpu_ids)} Compute Capability"] = cap
|
| 114 |
+
|
| 115 |
+
return gpu_info
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
def _get_cuda_version_info():
|
| 119 |
+
"""
|
| 120 |
+
Get CUDA version information.
|
| 121 |
+
"""
|
| 122 |
+
if is_cuda_v2():
|
| 123 |
+
from torch.utils.cpp_extension import CUDA_HOME
|
| 124 |
+
|
| 125 |
+
cuda_info = {"CUDA_HOME": CUDA_HOME}
|
| 126 |
+
|
| 127 |
+
if CUDA_HOME and os.path.isdir(CUDA_HOME):
|
| 128 |
+
cuda_info.update(_get_nvcc_info())
|
| 129 |
+
cuda_info.update(_get_cuda_driver_version())
|
| 130 |
+
|
| 131 |
+
return cuda_info
|
| 132 |
+
elif is_hip():
|
| 133 |
+
from torch.utils.cpp_extension import ROCM_HOME as ROCM_HOME
|
| 134 |
+
|
| 135 |
+
cuda_info = {"ROCM_HOME": ROCM_HOME}
|
| 136 |
+
|
| 137 |
+
if ROCM_HOME and os.path.isdir(ROCM_HOME):
|
| 138 |
+
cuda_info.update(_get_nvcc_info())
|
| 139 |
+
cuda_info.update(_get_cuda_driver_version())
|
| 140 |
+
|
| 141 |
+
return cuda_info
|
| 142 |
+
else:
|
| 143 |
+
cuda_info = {"CUDA_HOME": ""}
|
| 144 |
+
return cuda_info
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
def _get_nvcc_info():
|
| 148 |
+
"""
|
| 149 |
+
Get NVCC version information.
|
| 150 |
+
"""
|
| 151 |
+
if is_cuda_v2():
|
| 152 |
+
from torch.utils.cpp_extension import CUDA_HOME
|
| 153 |
+
|
| 154 |
+
try:
|
| 155 |
+
nvcc = os.path.join(CUDA_HOME, "bin/nvcc")
|
| 156 |
+
nvcc_output = (
|
| 157 |
+
subprocess.check_output(f'"{nvcc}" -V', shell=True)
|
| 158 |
+
.decode("utf-8")
|
| 159 |
+
.strip()
|
| 160 |
+
)
|
| 161 |
+
return {
|
| 162 |
+
"NVCC": nvcc_output[
|
| 163 |
+
nvcc_output.rfind("Cuda compilation tools") : nvcc_output.rfind(
|
| 164 |
+
"Build"
|
| 165 |
+
)
|
| 166 |
+
].strip()
|
| 167 |
+
}
|
| 168 |
+
except subprocess.SubprocessError:
|
| 169 |
+
return {"NVCC": "Not Available"}
|
| 170 |
+
elif is_hip():
|
| 171 |
+
from torch.utils.cpp_extension import ROCM_HOME
|
| 172 |
+
|
| 173 |
+
try:
|
| 174 |
+
hipcc = os.path.join(ROCM_HOME, "bin/hipcc")
|
| 175 |
+
hipcc_output = (
|
| 176 |
+
subprocess.check_output(f'"{hipcc}" --version', shell=True)
|
| 177 |
+
.decode("utf-8")
|
| 178 |
+
.strip()
|
| 179 |
+
)
|
| 180 |
+
return {
|
| 181 |
+
"HIPCC": hipcc_output[
|
| 182 |
+
hipcc_output.rfind("HIP version") : hipcc_output.rfind("AMD clang")
|
| 183 |
+
].strip()
|
| 184 |
+
}
|
| 185 |
+
except subprocess.SubprocessError:
|
| 186 |
+
return {"HIPCC": "Not Available"}
|
| 187 |
+
else:
|
| 188 |
+
return {"NVCC": "Not Available"}
|
| 189 |
+
|
| 190 |
+
|
| 191 |
+
def _get_cuda_driver_version():
|
| 192 |
+
"""
|
| 193 |
+
Get CUDA driver version.
|
| 194 |
+
"""
|
| 195 |
+
versions = set()
|
| 196 |
+
if is_cuda_v2():
|
| 197 |
+
try:
|
| 198 |
+
output = subprocess.check_output(
|
| 199 |
+
[
|
| 200 |
+
"nvidia-smi",
|
| 201 |
+
"--query-gpu=driver_version",
|
| 202 |
+
"--format=csv,noheader,nounits",
|
| 203 |
+
]
|
| 204 |
+
)
|
| 205 |
+
versions = set(output.decode().strip().split("\n"))
|
| 206 |
+
if len(versions) == 1:
|
| 207 |
+
return {"CUDA Driver Version": versions.pop()}
|
| 208 |
+
else:
|
| 209 |
+
return {"CUDA Driver Versions": ", ".join(sorted(versions))}
|
| 210 |
+
except subprocess.SubprocessError:
|
| 211 |
+
return {"CUDA Driver Version": "Not Available"}
|
| 212 |
+
elif is_hip():
|
| 213 |
+
try:
|
| 214 |
+
output = subprocess.check_output(
|
| 215 |
+
[
|
| 216 |
+
"rocm-smi",
|
| 217 |
+
"--showdriverversion",
|
| 218 |
+
"--csv",
|
| 219 |
+
]
|
| 220 |
+
)
|
| 221 |
+
versions = set(output.decode().strip().split("\n"))
|
| 222 |
+
versions.discard("name, value")
|
| 223 |
+
ver = versions.pop()
|
| 224 |
+
ver = ver.replace('"Driver version", ', "").replace('"', "")
|
| 225 |
+
|
| 226 |
+
return {"ROCM Driver Version": ver}
|
| 227 |
+
except subprocess.SubprocessError:
|
| 228 |
+
return {"ROCM Driver Version": "Not Available"}
|
| 229 |
+
else:
|
| 230 |
+
return {"CUDA Driver Version": "Not Available"}
|
| 231 |
+
|
| 232 |
+
|
| 233 |
+
def get_gpu_topology():
|
| 234 |
+
"""
|
| 235 |
+
Get GPU topology information.
|
| 236 |
+
"""
|
| 237 |
+
if is_cuda_v2():
|
| 238 |
+
try:
|
| 239 |
+
result = subprocess.run(
|
| 240 |
+
["nvidia-smi", "topo", "-m"],
|
| 241 |
+
stdout=subprocess.PIPE,
|
| 242 |
+
stderr=subprocess.PIPE,
|
| 243 |
+
text=True,
|
| 244 |
+
check=True,
|
| 245 |
+
)
|
| 246 |
+
return "\n" + result.stdout if result.returncode == 0 else None
|
| 247 |
+
except subprocess.SubprocessError:
|
| 248 |
+
return None
|
| 249 |
+
elif is_hip():
|
| 250 |
+
try:
|
| 251 |
+
result = subprocess.run(
|
| 252 |
+
["rocm-smi", "--showtopotype"],
|
| 253 |
+
stdout=subprocess.PIPE,
|
| 254 |
+
stderr=subprocess.PIPE,
|
| 255 |
+
text=True,
|
| 256 |
+
check=True,
|
| 257 |
+
)
|
| 258 |
+
return "\n" + result.stdout if result.returncode == 0 else None
|
| 259 |
+
except subprocess.SubprocessError:
|
| 260 |
+
return None
|
| 261 |
+
else:
|
| 262 |
+
return None
|
| 263 |
+
|
| 264 |
+
|
| 265 |
+
def get_hypervisor_vendor():
|
| 266 |
+
try:
|
| 267 |
+
output = subprocess.check_output(["lscpu"], text=True)
|
| 268 |
+
for line in output.split("\n"):
|
| 269 |
+
if "Hypervisor vendor:" in line:
|
| 270 |
+
return line.split(":")[1].strip()
|
| 271 |
+
return None
|
| 272 |
+
except:
|
| 273 |
+
return None
|
| 274 |
+
|
| 275 |
+
|
| 276 |
+
def check_env():
|
| 277 |
+
"""
|
| 278 |
+
Check and print environment information.
|
| 279 |
+
"""
|
| 280 |
+
env_info = OrderedDict()
|
| 281 |
+
env_info["Python"] = sys.version.replace("\n", "")
|
| 282 |
+
env_info.update(get_cuda_info())
|
| 283 |
+
env_info["PyTorch"] = torch.__version__
|
| 284 |
+
env_info.update(get_package_versions(PACKAGE_LIST))
|
| 285 |
+
|
| 286 |
+
gpu_topo = get_gpu_topology()
|
| 287 |
+
if gpu_topo:
|
| 288 |
+
if is_cuda_v2():
|
| 289 |
+
env_info["NVIDIA Topology"] = gpu_topo
|
| 290 |
+
elif is_hip():
|
| 291 |
+
env_info["AMD Topology"] = gpu_topo
|
| 292 |
+
|
| 293 |
+
hypervisor_vendor = get_hypervisor_vendor()
|
| 294 |
+
if hypervisor_vendor:
|
| 295 |
+
env_info["Hypervisor vendor"] = hypervisor_vendor
|
| 296 |
+
|
| 297 |
+
ulimit_soft, _ = resource.getrlimit(resource.RLIMIT_NOFILE)
|
| 298 |
+
env_info["ulimit soft"] = ulimit_soft
|
| 299 |
+
|
| 300 |
+
for k, v in env_info.items():
|
| 301 |
+
print(f"{k}: {v}")
|
| 302 |
+
|
| 303 |
+
|
| 304 |
+
if __name__ == "__main__":
|
| 305 |
+
check_env()
|
lib/python3.12/site-packages/sglang/compile_deep_gemm.py
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Compile DeepGEMM Kernels for a model with specify server arguments
|
| 3 |
+
|
| 4 |
+
This script launches a server for capturing DeepGEMM calls and then compiles the kernels.
|
| 5 |
+
It accepts server arguments (the same as launch_server.py).
|
| 6 |
+
|
| 7 |
+
Usage:
|
| 8 |
+
python3 -m sglang.compile_deep_gemm --model deepseek-ai/DeepSeek-V3 --tp 8 --trust-remote-code
|
| 9 |
+
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
import argparse
|
| 13 |
+
import dataclasses
|
| 14 |
+
import multiprocessing
|
| 15 |
+
import os
|
| 16 |
+
import time
|
| 17 |
+
|
| 18 |
+
import requests
|
| 19 |
+
|
| 20 |
+
from sglang.srt.entrypoints.http_server import launch_server
|
| 21 |
+
from sglang.srt.managers.io_struct import GenerateReqInput
|
| 22 |
+
from sglang.srt.managers.tokenizer_manager import TokenizerManager
|
| 23 |
+
from sglang.srt.server_args import ServerArgs
|
| 24 |
+
from sglang.srt.utils import kill_process_tree
|
| 25 |
+
from sglang.srt.warmup import warmup
|
| 26 |
+
|
| 27 |
+
multiprocessing.set_start_method("spawn", force=True)
|
| 28 |
+
|
| 29 |
+
# Reduce warning
|
| 30 |
+
os.environ["SGL_IN_DEEPGEMM_PRECOMPILE_STAGE"] = "1"
|
| 31 |
+
# Force enable deep gemm
|
| 32 |
+
os.environ["SGL_ENABLE_JIT_DEEPGEMM"] = "1"
|
| 33 |
+
# Force enable mha chunked kv for DeepSeek V3 to avoid missing kv_b_proj DeepGEMM case
|
| 34 |
+
os.environ["SGL_CHUNKED_PREFIX_CACHE_THRESHOLD"] = "0"
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
@dataclasses.dataclass
|
| 38 |
+
class CompileArgs:
|
| 39 |
+
timeout: int = 3600
|
| 40 |
+
|
| 41 |
+
@staticmethod
|
| 42 |
+
def add_cli_args(parser: argparse.ArgumentParser):
|
| 43 |
+
parser.add_argument("--timeout", type=int, default=CompileArgs.timeout)
|
| 44 |
+
|
| 45 |
+
@classmethod
|
| 46 |
+
def from_cli_args(cls, args: argparse.Namespace):
|
| 47 |
+
# use the default value's type to cast the args into correct types.
|
| 48 |
+
attrs = [(attr.name, type(attr.default)) for attr in dataclasses.fields(cls)]
|
| 49 |
+
return cls(
|
| 50 |
+
**{attr: attr_type(getattr(args, attr)) for attr, attr_type in attrs}
|
| 51 |
+
)
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
@warmup("compile-deep-gemm")
|
| 55 |
+
async def warm_up_compile(tokenizer_manager: TokenizerManager):
|
| 56 |
+
print("\nGenerate warm up request for compiling DeepGEMM...\n")
|
| 57 |
+
generate_req_input = GenerateReqInput(
|
| 58 |
+
input_ids=[0, 1, 2, 3],
|
| 59 |
+
sampling_params={
|
| 60 |
+
"temperature": 0.0,
|
| 61 |
+
"max_new_tokens": 8,
|
| 62 |
+
"ignore_eos": True,
|
| 63 |
+
},
|
| 64 |
+
)
|
| 65 |
+
await tokenizer_manager.generate_request(generate_req_input, None).__anext__()
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def launch_server_internal(server_args):
|
| 69 |
+
try:
|
| 70 |
+
launch_server(server_args)
|
| 71 |
+
except Exception as e:
|
| 72 |
+
raise e
|
| 73 |
+
finally:
|
| 74 |
+
kill_process_tree(os.getpid(), include_parent=False)
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def launch_server_process_and_send_one_request(
|
| 78 |
+
server_args: ServerArgs, compile_args: CompileArgs
|
| 79 |
+
):
|
| 80 |
+
proc = multiprocessing.Process(target=launch_server_internal, args=(server_args,))
|
| 81 |
+
proc.start()
|
| 82 |
+
base_url = f"http://{server_args.host}:{server_args.port}"
|
| 83 |
+
timeout = compile_args.timeout
|
| 84 |
+
|
| 85 |
+
start_time = time.time()
|
| 86 |
+
while time.time() - start_time < timeout:
|
| 87 |
+
try:
|
| 88 |
+
headers = {
|
| 89 |
+
"Content-Type": "application/json; charset=utf-8",
|
| 90 |
+
}
|
| 91 |
+
if server_args.node_rank == 0:
|
| 92 |
+
response = requests.get(f"{base_url}/v1/models", headers=headers)
|
| 93 |
+
else:
|
| 94 |
+
# This http api is created by launch_dummy_health_check_server for none-rank0 node.
|
| 95 |
+
response = requests.get(f"{base_url}/health", headers=headers)
|
| 96 |
+
if response.status_code == 200:
|
| 97 |
+
# Rank-0 node send a request to sync with other node and then return.
|
| 98 |
+
if server_args.node_rank == 0:
|
| 99 |
+
response = requests.post(
|
| 100 |
+
f"{base_url}/generate",
|
| 101 |
+
json={
|
| 102 |
+
"input_ids": [0, 1, 2, 3],
|
| 103 |
+
"sampling_params": {
|
| 104 |
+
"max_new_tokens": 8,
|
| 105 |
+
"temperature": 0,
|
| 106 |
+
},
|
| 107 |
+
},
|
| 108 |
+
timeout=600,
|
| 109 |
+
)
|
| 110 |
+
if response.status_code != 200:
|
| 111 |
+
error = response.json()
|
| 112 |
+
raise RuntimeError(f"Sync request failed: {error}")
|
| 113 |
+
# Other nodes should wait for the exit signal from Rank-0 node.
|
| 114 |
+
else:
|
| 115 |
+
start_time_waiting = time.time()
|
| 116 |
+
while proc.is_alive():
|
| 117 |
+
if time.time() - start_time_waiting < timeout:
|
| 118 |
+
time.sleep(10)
|
| 119 |
+
else:
|
| 120 |
+
raise TimeoutError("Waiting for main node timeout!")
|
| 121 |
+
return proc
|
| 122 |
+
except requests.RequestException:
|
| 123 |
+
pass
|
| 124 |
+
time.sleep(10)
|
| 125 |
+
raise TimeoutError(
|
| 126 |
+
"DeepGEMM Kernels compilation timeout."
|
| 127 |
+
"\n\nFeel free and please restart the command."
|
| 128 |
+
)
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
def refine_server_args(server_args: ServerArgs, compile_args: CompileArgs):
|
| 132 |
+
# Disbale cuda graph and torch compile to save time
|
| 133 |
+
server_args.disable_cuda_graph = True
|
| 134 |
+
server_args.enable_torch_compile = False
|
| 135 |
+
print(f"Disable CUDA Graph and Torch Compile to save time...")
|
| 136 |
+
|
| 137 |
+
# Set watchdog timeout to compile_args.timeout because compilation will take a long time
|
| 138 |
+
server_args.watchdog_timeout = compile_args.timeout
|
| 139 |
+
server_args.warmups = "compile-deep-gemm"
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
def run_compile(server_args: ServerArgs, compile_args: CompileArgs):
|
| 143 |
+
print(
|
| 144 |
+
"Begin DeepGEMM Kernels compilation...\n"
|
| 145 |
+
"It may take a long time and timeout maybe raised "
|
| 146 |
+
"while the compilation is still in progress.\n"
|
| 147 |
+
"Just feel free to restart the command "
|
| 148 |
+
"until the compilation is fully finished.\n"
|
| 149 |
+
)
|
| 150 |
+
|
| 151 |
+
proc = launch_server_process_and_send_one_request(server_args, compile_args)
|
| 152 |
+
|
| 153 |
+
print("\nDeepGEMM Kernels compilation finished successfully.")
|
| 154 |
+
|
| 155 |
+
# Sleep for safety
|
| 156 |
+
time.sleep(10)
|
| 157 |
+
if proc.is_alive():
|
| 158 |
+
# This is the rank0 node.
|
| 159 |
+
kill_process_tree(proc.pid)
|
| 160 |
+
else:
|
| 161 |
+
try:
|
| 162 |
+
kill_process_tree(proc.pid)
|
| 163 |
+
except Exception:
|
| 164 |
+
pass
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
if __name__ == "__main__":
|
| 168 |
+
parser = argparse.ArgumentParser()
|
| 169 |
+
ServerArgs.add_cli_args(parser)
|
| 170 |
+
CompileArgs.add_cli_args(parser)
|
| 171 |
+
args = parser.parse_args()
|
| 172 |
+
server_args = ServerArgs.from_cli_args(args)
|
| 173 |
+
compile_args = CompileArgs.from_cli_args(args)
|
| 174 |
+
|
| 175 |
+
refine_server_args(server_args, compile_args)
|
| 176 |
+
|
| 177 |
+
run_compile(server_args, compile_args)
|
lib/python3.12/site-packages/sglang/global_config.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Global configurations"""
|
| 2 |
+
|
| 3 |
+
import os
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class GlobalConfig:
|
| 7 |
+
"""
|
| 8 |
+
Store some global constants.
|
| 9 |
+
|
| 10 |
+
See also python/sglang/srt/managers/schedule_batch.py::global_server_args_dict, which stores
|
| 11 |
+
many global runtime arguments as well.
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
def __init__(self):
|
| 15 |
+
# Verbosity level
|
| 16 |
+
# 0: do not output anything
|
| 17 |
+
# 2: output final text after every run
|
| 18 |
+
self.verbosity = 0
|
| 19 |
+
|
| 20 |
+
# Default backend of the language
|
| 21 |
+
self.default_backend = None
|
| 22 |
+
|
| 23 |
+
# Runtime constants: New generation token ratio estimation
|
| 24 |
+
self.default_init_new_token_ratio = float(
|
| 25 |
+
os.environ.get("SGLANG_INIT_NEW_TOKEN_RATIO", 0.7)
|
| 26 |
+
)
|
| 27 |
+
self.default_min_new_token_ratio_factor = float(
|
| 28 |
+
os.environ.get("SGLANG_MIN_NEW_TOKEN_RATIO_FACTOR", 0.14)
|
| 29 |
+
)
|
| 30 |
+
self.default_new_token_ratio_decay_steps = float(
|
| 31 |
+
os.environ.get("SGLANG_NEW_TOKEN_RATIO_DECAY_STEPS", 600)
|
| 32 |
+
)
|
| 33 |
+
|
| 34 |
+
# Runtime constants: others
|
| 35 |
+
self.retract_decode_steps = 20
|
| 36 |
+
self.flashinfer_workspace_size = os.environ.get(
|
| 37 |
+
"FLASHINFER_WORKSPACE_SIZE", 384 * 1024 * 1024
|
| 38 |
+
)
|
| 39 |
+
|
| 40 |
+
# Output tokenization configs
|
| 41 |
+
self.skip_special_tokens_in_output = True
|
| 42 |
+
self.spaces_between_special_tokens_in_out = True
|
| 43 |
+
|
| 44 |
+
# Language frontend interpreter optimization configs
|
| 45 |
+
self.enable_precache_with_tracing = True
|
| 46 |
+
self.enable_parallel_encoding = True
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
global_config = GlobalConfig()
|
lib/python3.12/site-packages/sglang/launch_server.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Launch the inference server."""
|
| 2 |
+
|
| 3 |
+
import os
|
| 4 |
+
import sys
|
| 5 |
+
|
| 6 |
+
from sglang.srt.entrypoints.http_server import launch_server
|
| 7 |
+
from sglang.srt.server_args import prepare_server_args
|
| 8 |
+
from sglang.srt.utils import kill_process_tree
|
| 9 |
+
|
| 10 |
+
if __name__ == "__main__":
|
| 11 |
+
server_args = prepare_server_args(sys.argv[1:])
|
| 12 |
+
|
| 13 |
+
try:
|
| 14 |
+
launch_server(server_args)
|
| 15 |
+
finally:
|
| 16 |
+
kill_process_tree(os.getpid(), include_parent=False)
|
lib/python3.12/site-packages/sglang/llama3_eval.py
ADDED
|
@@ -0,0 +1,316 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Adapt from https://github.com/fw-ai/llm_eval_meta
|
| 2 |
+
|
| 3 |
+
import argparse
|
| 4 |
+
import asyncio
|
| 5 |
+
import os
|
| 6 |
+
import pickle
|
| 7 |
+
import re
|
| 8 |
+
import shutil
|
| 9 |
+
from collections import defaultdict
|
| 10 |
+
from dataclasses import dataclass
|
| 11 |
+
|
| 12 |
+
import httpx
|
| 13 |
+
import numpy as np
|
| 14 |
+
import openai
|
| 15 |
+
import transformers
|
| 16 |
+
from datasets import load_dataset
|
| 17 |
+
from openai import AsyncOpenAI
|
| 18 |
+
from tqdm import tqdm
|
| 19 |
+
|
| 20 |
+
# Mapping providers to their clients and models
|
| 21 |
+
provider_to_models = {
|
| 22 |
+
"b10": {
|
| 23 |
+
"8b": "meta-llama/Llama-3.1-8B-Instruct",
|
| 24 |
+
"70b": "meta-llama/Llama-3.1-70B-Instruct",
|
| 25 |
+
"405b": "meta-llama/Llama-3.1-405B-Instruct",
|
| 26 |
+
},
|
| 27 |
+
"oai": {
|
| 28 |
+
"8b": "meta-llama/Llama-3.1-8B-Instruct",
|
| 29 |
+
"70b": "meta-llama/Llama-3.1-70B-Instruct",
|
| 30 |
+
"405b": "meta-llama/Llama-3.1-405B-Instruct",
|
| 31 |
+
},
|
| 32 |
+
"sgl": {
|
| 33 |
+
"8b": "meta-llama/Llama-3.1-8B-Instruct",
|
| 34 |
+
"70b": "meta-llama/Llama-3.1-70B-Instruct",
|
| 35 |
+
"405b": "meta-llama/Llama-3.1-405B-Instruct",
|
| 36 |
+
},
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
async def fetch_responses(
|
| 41 |
+
client, prompt, semaphore, index, provider, model_size, output_dir, max_tokens
|
| 42 |
+
):
|
| 43 |
+
output_file = os.path.join(output_dir, f"response_{index}.pkl")
|
| 44 |
+
if os.path.exists(output_file):
|
| 45 |
+
print(f"File {output_file} already exists, skipping.")
|
| 46 |
+
return
|
| 47 |
+
|
| 48 |
+
async with semaphore:
|
| 49 |
+
response = await client.completions.create(
|
| 50 |
+
model=provider_to_models[provider][model_size],
|
| 51 |
+
prompt=prompt,
|
| 52 |
+
temperature=0.0,
|
| 53 |
+
max_tokens=max_tokens,
|
| 54 |
+
)
|
| 55 |
+
if isinstance(response, openai.BadRequestError):
|
| 56 |
+
with open(output_file, "wb") as f:
|
| 57 |
+
pickle.dump("bad_response", f)
|
| 58 |
+
assert isinstance(response, openai.types.completion.Completion)
|
| 59 |
+
# Save response to a file
|
| 60 |
+
with open(output_file, "wb") as f:
|
| 61 |
+
pickle.dump(response, f)
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
TASK_TO_MAX_TOKENS = {
|
| 65 |
+
"evals__mmlu__details": 1,
|
| 66 |
+
"evals__mmlu__0_shot__cot__details": 1024,
|
| 67 |
+
# Official meta uses 1024, but a small % (.05) of questions are answered correctly after relaxing
|
| 68 |
+
"evals__mmlu_pro__details": 2048,
|
| 69 |
+
"evals__gsm8k__details": 1024,
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
TASK_TO_EVAL_SET = {
|
| 73 |
+
"mmlu": "evals__mmlu__details",
|
| 74 |
+
"mmlu_cot": "evals__mmlu__0_shot__cot__details",
|
| 75 |
+
"mmlu_pro": "evals__mmlu_pro__details",
|
| 76 |
+
"gsm8k": "evals__gsm8k__details",
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
class CustomAsyncHTTPXClient(httpx.AsyncClient):
|
| 81 |
+
async def send(self, request: httpx.Request, *args, **kwargs) -> httpx.Response:
|
| 82 |
+
request.url = httpx.URL(
|
| 83 |
+
f"https://model-{os.getenv('MODEL_ID')}.api.baseten.co/development/predict"
|
| 84 |
+
)
|
| 85 |
+
return await super().send(request, *args, **kwargs)
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
def get_client(provider):
|
| 89 |
+
if provider not in "b10":
|
| 90 |
+
if os.getenv("OPENAI_API_KEY") == None:
|
| 91 |
+
os.environ["OPENAI_API_KEY"] = "EMPTY"
|
| 92 |
+
return {
|
| 93 |
+
"oai": AsyncOpenAI(base_url="http://127.0.0.1:8000/v1/"),
|
| 94 |
+
"b10": AsyncOpenAI(
|
| 95 |
+
api_key=f"Api-Key {os.getenv('OPENAI_API_KEY')}",
|
| 96 |
+
base_url=f"https://model-{os.getenv('MODEL_ID')}.api.baseten.co/development/predict",
|
| 97 |
+
http_client=CustomAsyncHTTPXClient(),
|
| 98 |
+
),
|
| 99 |
+
"sgl": AsyncOpenAI(base_url="http://127.0.0.1:30000/v1/"),
|
| 100 |
+
}[provider]
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
# Define the benchmark function
|
| 104 |
+
async def benchmark(args):
|
| 105 |
+
ds = load_dataset(
|
| 106 |
+
"meta-llama/Llama-3.1-405B-Instruct-evals",
|
| 107 |
+
f"Llama-3.1-405B-Instruct-{TASK_TO_EVAL_SET[args.task]}",
|
| 108 |
+
)
|
| 109 |
+
semaphore = asyncio.Semaphore(args.concurrency) # Limit to 16 concurrent tasks
|
| 110 |
+
|
| 111 |
+
if args.num_examples is None:
|
| 112 |
+
args.num_examples = len(ds["latest"]["input_final_prompts"])
|
| 113 |
+
prompts = ds["latest"]["input_final_prompts"][: args.num_examples]
|
| 114 |
+
|
| 115 |
+
# Create the output directory if it does not exist
|
| 116 |
+
os.makedirs(args.output_dir, exist_ok=True)
|
| 117 |
+
|
| 118 |
+
tasks = []
|
| 119 |
+
# Create the tasks with tqdm progress bar
|
| 120 |
+
max_tokens = TASK_TO_MAX_TOKENS[TASK_TO_EVAL_SET[args.task]]
|
| 121 |
+
client = get_client(args.provider)
|
| 122 |
+
for idx, prompt in enumerate(tqdm(prompts, desc="Creating tasks")):
|
| 123 |
+
tasks.append(
|
| 124 |
+
asyncio.create_task(
|
| 125 |
+
fetch_responses(
|
| 126 |
+
client,
|
| 127 |
+
f"<|begin_of_text|>{prompt[0]}",
|
| 128 |
+
semaphore,
|
| 129 |
+
idx,
|
| 130 |
+
args.provider,
|
| 131 |
+
args.model_size,
|
| 132 |
+
args.output_dir,
|
| 133 |
+
max_tokens=max_tokens,
|
| 134 |
+
)
|
| 135 |
+
)
|
| 136 |
+
)
|
| 137 |
+
|
| 138 |
+
# Run the tasks with tqdm progress bar
|
| 139 |
+
for future in tqdm(
|
| 140 |
+
asyncio.as_completed(tasks), total=len(tasks), desc="Processing tasks"
|
| 141 |
+
):
|
| 142 |
+
await future
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
def get_mmlu_answer(response):
|
| 146 |
+
if response is not None:
|
| 147 |
+
return response.choices[0].text.lstrip().rstrip().upper().replace(".", "")
|
| 148 |
+
return None
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
def get_mmlu_cot_answer(response):
|
| 152 |
+
pattern = r"The best answer is (.+)\.?"
|
| 153 |
+
match = re.search(pattern, response.choices[0].text)
|
| 154 |
+
if match:
|
| 155 |
+
return match.group(1).replace(".", "").replace("*", "")
|
| 156 |
+
|
| 157 |
+
pattern = r"the best answer is (.+)\.?"
|
| 158 |
+
match = re.search(pattern, response.choices[0].text)
|
| 159 |
+
if match:
|
| 160 |
+
return match.group(1).replace(".", "")
|
| 161 |
+
|
| 162 |
+
pattern = r"The correct answer is (.+)\.?"
|
| 163 |
+
match = re.search(pattern, response.choices[0].text)
|
| 164 |
+
if match:
|
| 165 |
+
return match.group(1).replace(".", "")
|
| 166 |
+
|
| 167 |
+
pattern = r"the correct answer is (.+)\.?"
|
| 168 |
+
match = re.search(pattern, response.choices[0].text)
|
| 169 |
+
if match:
|
| 170 |
+
return match.group(1).replace(".", "")
|
| 171 |
+
|
| 172 |
+
|
| 173 |
+
def get_answer_gsm8k(response):
|
| 174 |
+
pattern = r"The final answer is (.+)\.?"
|
| 175 |
+
match = re.search(pattern, response.choices[0].text)
|
| 176 |
+
if match:
|
| 177 |
+
s = match.group(1)
|
| 178 |
+
for ok_symbol in ["%", "$"]:
|
| 179 |
+
s = s.replace(ok_symbol, "")
|
| 180 |
+
return s
|
| 181 |
+
|
| 182 |
+
|
| 183 |
+
TASK_TO_ANSWER_EXTRACTOR = {
|
| 184 |
+
"evals__mmlu__details": get_mmlu_answer,
|
| 185 |
+
"evals__mmlu__0_shot__cot__details": get_mmlu_cot_answer,
|
| 186 |
+
"evals__gsm8k__details": get_answer_gsm8k,
|
| 187 |
+
"evals__mmlu_pro__details": get_mmlu_cot_answer,
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
|
| 191 |
+
def get_dataset_from_task(task, response_path, model_size):
|
| 192 |
+
ds_405b = load_dataset(
|
| 193 |
+
f"meta-llama/Llama-3.1-405B-Instruct-evals",
|
| 194 |
+
f"Llama-3.1-405B-Instruct-{task}",
|
| 195 |
+
)
|
| 196 |
+
ds_405b_hash_order = [x[0] for x in ds_405b["latest"]["input_final_prompts_hash"]]
|
| 197 |
+
|
| 198 |
+
if "70b" in model_size or "8b" in model_size:
|
| 199 |
+
if "70" in model_size:
|
| 200 |
+
ref_model_ds = load_dataset(
|
| 201 |
+
f"meta-llama/Llama-3.1-70B-Instruct-evals",
|
| 202 |
+
f"Llama-3.1-70B-Instruct-{task}",
|
| 203 |
+
)
|
| 204 |
+
else:
|
| 205 |
+
ref_model_ds = load_dataset(
|
| 206 |
+
f"meta-llama/Llama-3.1-8B-Instruct-evals",
|
| 207 |
+
f"Llama-3.1-8B-Instruct-{task}",
|
| 208 |
+
)
|
| 209 |
+
|
| 210 |
+
hash_to_row = {}
|
| 211 |
+
for row in ref_model_ds["latest"]:
|
| 212 |
+
hash_to_row[row["input_final_prompts_hash"][0]] = row
|
| 213 |
+
reordered_rows = []
|
| 214 |
+
for prompt_hash in ds_405b_hash_order:
|
| 215 |
+
reordered_rows.append(hash_to_row[prompt_hash])
|
| 216 |
+
ref_model_ds["latest"] = reordered_rows
|
| 217 |
+
return ref_model_ds
|
| 218 |
+
|
| 219 |
+
return ds_405b
|
| 220 |
+
|
| 221 |
+
|
| 222 |
+
def analyze(task, response_path, model_size):
|
| 223 |
+
ds = get_dataset_from_task(task, response_path, model_size)
|
| 224 |
+
|
| 225 |
+
responses = []
|
| 226 |
+
total = len(ds["latest"])
|
| 227 |
+
|
| 228 |
+
for i in range(0, total):
|
| 229 |
+
response = pickle.load(
|
| 230 |
+
open(os.path.join(response_path, f"response_{i}.pkl"), "rb")
|
| 231 |
+
)
|
| 232 |
+
responses.append(response)
|
| 233 |
+
|
| 234 |
+
@dataclass
|
| 235 |
+
class Stats:
|
| 236 |
+
correct: int = 0
|
| 237 |
+
total: int = 0
|
| 238 |
+
meta_correct: int = 0
|
| 239 |
+
|
| 240 |
+
average: float = None
|
| 241 |
+
|
| 242 |
+
subtask_name_to_stats = defaultdict(lambda: Stats())
|
| 243 |
+
|
| 244 |
+
for response, ds_row in zip(responses, ds["latest"]):
|
| 245 |
+
model_answer = TASK_TO_ANSWER_EXTRACTOR[task](response)
|
| 246 |
+
|
| 247 |
+
subtask = ds_row["subtask_name"]
|
| 248 |
+
|
| 249 |
+
is_eval_correct = model_answer in ds_row["input_correct_responses"]
|
| 250 |
+
if is_eval_correct:
|
| 251 |
+
subtask_name_to_stats[subtask].correct += 1
|
| 252 |
+
|
| 253 |
+
if ds_row["is_correct"]:
|
| 254 |
+
subtask_name_to_stats[subtask].meta_correct += 1
|
| 255 |
+
|
| 256 |
+
subtask_name_to_stats[subtask].total += 1
|
| 257 |
+
|
| 258 |
+
micro_stats = Stats()
|
| 259 |
+
for subtask, stats in subtask_name_to_stats.items():
|
| 260 |
+
stats.average = stats.correct / stats.total
|
| 261 |
+
stats.meta_average = stats.meta_correct / stats.total
|
| 262 |
+
|
| 263 |
+
micro_stats.correct += stats.correct
|
| 264 |
+
micro_stats.total += stats.total
|
| 265 |
+
micro_stats.meta_correct += stats.meta_correct
|
| 266 |
+
|
| 267 |
+
micro_stats.average = micro_stats.correct / micro_stats.total
|
| 268 |
+
micro_stats.meta_average = micro_stats.meta_correct / micro_stats.total
|
| 269 |
+
|
| 270 |
+
print("Macro average", np.mean([x.average for x in subtask_name_to_stats.values()]))
|
| 271 |
+
print(
|
| 272 |
+
"Meta Macro average",
|
| 273 |
+
np.mean([x.meta_average for x in subtask_name_to_stats.values()]),
|
| 274 |
+
)
|
| 275 |
+
print("Micro average", micro_stats.average)
|
| 276 |
+
print("Meta Micro average", micro_stats.meta_average)
|
| 277 |
+
|
| 278 |
+
|
| 279 |
+
# Entry point for the script
|
| 280 |
+
if __name__ == "__main__":
|
| 281 |
+
parser = argparse.ArgumentParser(
|
| 282 |
+
description="Script to run model with specified parameters."
|
| 283 |
+
)
|
| 284 |
+
parser.add_argument(
|
| 285 |
+
"--model-size",
|
| 286 |
+
type=str,
|
| 287 |
+
default="8b",
|
| 288 |
+
help="Size of the model (e.g., 8b or 70b)",
|
| 289 |
+
)
|
| 290 |
+
parser.add_argument(
|
| 291 |
+
"--provider",
|
| 292 |
+
type=str,
|
| 293 |
+
default="sgl",
|
| 294 |
+
help="Provider name (e.g., sgl, oai, b10)",
|
| 295 |
+
)
|
| 296 |
+
parser.add_argument(
|
| 297 |
+
"--task",
|
| 298 |
+
type=str,
|
| 299 |
+
required=True,
|
| 300 |
+
help="Task (e.g., mmlu, mmlu_cot, mmlu_pro, gsm8k)",
|
| 301 |
+
)
|
| 302 |
+
parser.add_argument(
|
| 303 |
+
"--num-examples", type=int, default=None, help="Number of examples to process"
|
| 304 |
+
)
|
| 305 |
+
parser.add_argument("--concurrency", type=int, default=16)
|
| 306 |
+
parser.add_argument(
|
| 307 |
+
"--output-dir",
|
| 308 |
+
type=str,
|
| 309 |
+
default="tmp-output-dir",
|
| 310 |
+
help="Directory to save responses",
|
| 311 |
+
)
|
| 312 |
+
|
| 313 |
+
args = parser.parse_args()
|
| 314 |
+
asyncio.run(benchmark(args))
|
| 315 |
+
analyze(TASK_TO_EVAL_SET[args.task], args.output_dir, args.model_size)
|
| 316 |
+
shutil.rmtree("tmp-output-dir", ignore_errors=True)
|
lib/python3.12/site-packages/sglang/srt/_custom_ops.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Adapted from https://github.com/vllm-project/vllm/blob/v0.6.4.post1/vllm/_custom_ops.py
|
| 2 |
+
import logging
|
| 3 |
+
from typing import List, Tuple
|
| 4 |
+
|
| 5 |
+
import torch
|
| 6 |
+
|
| 7 |
+
from sglang.srt.utils import get_bool_env_var, is_hip, is_hpu
|
| 8 |
+
|
| 9 |
+
logger = logging.getLogger(__name__)
|
| 10 |
+
use_vllm_custom_allreduce = get_bool_env_var(
|
| 11 |
+
"USE_VLLM_CUSTOM_ALLREDUCE", default="false"
|
| 12 |
+
)
|
| 13 |
+
|
| 14 |
+
if not is_hpu():
|
| 15 |
+
# ROCm does not use vllm custom allreduce
|
| 16 |
+
if use_vllm_custom_allreduce and not is_hip():
|
| 17 |
+
try:
|
| 18 |
+
import vllm._C
|
| 19 |
+
except ImportError as e:
|
| 20 |
+
logger.warning("Failed to import from vllm._C with %r", e)
|
| 21 |
+
else:
|
| 22 |
+
try:
|
| 23 |
+
import sgl_kernel
|
| 24 |
+
except ImportError as e:
|
| 25 |
+
logger.warning("Failed to import from custom_ar with %r", e)
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
if not is_hip():
|
| 29 |
+
if use_vllm_custom_allreduce:
|
| 30 |
+
custom_op = torch.ops._C_custom_ar
|
| 31 |
+
else:
|
| 32 |
+
custom_op = sgl_kernel.allreduce
|
| 33 |
+
|
| 34 |
+
# custom allreduce
|
| 35 |
+
def init_custom_ar(
|
| 36 |
+
ipc_tensors: List[torch.Tensor],
|
| 37 |
+
rank_data: torch.Tensor,
|
| 38 |
+
rank: int,
|
| 39 |
+
full_nvlink: bool,
|
| 40 |
+
) -> int:
|
| 41 |
+
return custom_op.init_custom_ar(ipc_tensors, rank_data, rank, full_nvlink)
|
| 42 |
+
|
| 43 |
+
def all_reduce(
|
| 44 |
+
fa: int,
|
| 45 |
+
inp: torch.Tensor,
|
| 46 |
+
out: torch.Tensor,
|
| 47 |
+
reg_buffer: int,
|
| 48 |
+
reg_buffer_sz_bytes: int,
|
| 49 |
+
) -> None:
|
| 50 |
+
custom_op.all_reduce(fa, inp, out, reg_buffer, reg_buffer_sz_bytes)
|
| 51 |
+
|
| 52 |
+
def dispose(fa: int) -> None:
|
| 53 |
+
custom_op.dispose(fa)
|
| 54 |
+
|
| 55 |
+
def meta_size() -> int:
|
| 56 |
+
return custom_op.meta_size()
|
| 57 |
+
|
| 58 |
+
def register_buffer(fa: int, ipc_tensors: List[int]) -> None:
|
| 59 |
+
return custom_op.register_buffer(fa, ipc_tensors)
|
| 60 |
+
|
| 61 |
+
def get_graph_buffer_ipc_meta(fa: int) -> Tuple[List[int], List[int]]:
|
| 62 |
+
return custom_op.get_graph_buffer_ipc_meta(fa)
|
| 63 |
+
|
| 64 |
+
def register_graph_buffers(
|
| 65 |
+
fa: int, handles: List[List[int]], offsets: List[List[int]]
|
| 66 |
+
) -> None:
|
| 67 |
+
custom_op.register_graph_buffers(fa, handles, offsets)
|
| 68 |
+
|
| 69 |
+
else:
|
| 70 |
+
# ROCM custom allreduce
|
| 71 |
+
|
| 72 |
+
def init_custom_ar(
|
| 73 |
+
meta: torch.Tensor,
|
| 74 |
+
rank_data: torch.Tensor,
|
| 75 |
+
handles: List[str],
|
| 76 |
+
offsets: List[int],
|
| 77 |
+
rank: int,
|
| 78 |
+
full_nvlink: bool,
|
| 79 |
+
) -> int:
|
| 80 |
+
return sgl_kernel.allreduce.init_custom_ar(
|
| 81 |
+
meta, rank_data, handles, offsets, rank, full_nvlink
|
| 82 |
+
)
|
| 83 |
+
|
| 84 |
+
def all_reduce_reg(fa: int, inp: torch.Tensor, out: torch.Tensor) -> None:
|
| 85 |
+
sgl_kernel.allreduce.all_reduce_reg(fa, inp, out)
|
| 86 |
+
|
| 87 |
+
def all_reduce_unreg(
|
| 88 |
+
fa: int, inp: torch.Tensor, reg_buffer: torch.Tensor, out: torch.Tensor
|
| 89 |
+
) -> None:
|
| 90 |
+
sgl_kernel.allreduce.all_reduce_unreg(fa, inp, reg_buffer, out)
|
| 91 |
+
|
| 92 |
+
def dispose(fa: int) -> None:
|
| 93 |
+
sgl_kernel.allreduce.dispose(fa)
|
| 94 |
+
|
| 95 |
+
def meta_size() -> int:
|
| 96 |
+
return sgl_kernel.allreduce.meta_size()
|
| 97 |
+
|
| 98 |
+
def register_buffer(
|
| 99 |
+
fa: int, t: torch.Tensor, handles: List[str], offsets: List[int]
|
| 100 |
+
) -> None:
|
| 101 |
+
return sgl_kernel.allreduce.register_buffer(fa, t, handles, offsets)
|
| 102 |
+
|
| 103 |
+
def get_graph_buffer_ipc_meta(fa: int) -> Tuple[torch.Tensor, List[int]]:
|
| 104 |
+
return sgl_kernel.allreduce.get_graph_buffer_ipc_meta(fa)
|
| 105 |
+
|
| 106 |
+
def register_graph_buffers(
|
| 107 |
+
fa: int, handles: List[str], offsets: List[List[int]]
|
| 108 |
+
) -> None:
|
| 109 |
+
sgl_kernel.allreduce.register_graph_buffers(fa, handles, offsets)
|
| 110 |
+
|
| 111 |
+
def allocate_meta_buffer(size: int) -> torch.Tensor:
|
| 112 |
+
return sgl_kernel.allreduce.allocate_meta_buffer(size)
|
| 113 |
+
|
| 114 |
+
def get_meta_buffer_ipc_handle(inp: torch.Tensor) -> torch.Tensor:
|
| 115 |
+
return sgl_kernel.allreduce.get_meta_buffer_ipc_handle(inp)
|
lib/python3.12/site-packages/sglang/srt/aio_rwlock.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import asyncio
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
class RWLock:
|
| 5 |
+
def __init__(self):
|
| 6 |
+
# Protects internal state
|
| 7 |
+
self._lock = asyncio.Lock()
|
| 8 |
+
|
| 9 |
+
# Condition variable used to wait for state changes
|
| 10 |
+
self._cond = asyncio.Condition(self._lock)
|
| 11 |
+
|
| 12 |
+
# Number of readers currently holding the lock
|
| 13 |
+
self._readers = 0
|
| 14 |
+
|
| 15 |
+
# Whether a writer is currently holding the lock
|
| 16 |
+
self._writer_active = False
|
| 17 |
+
|
| 18 |
+
# How many writers are queued waiting for a turn
|
| 19 |
+
self._waiting_writers = 0
|
| 20 |
+
|
| 21 |
+
@property
|
| 22 |
+
def reader_lock(self):
|
| 23 |
+
"""
|
| 24 |
+
A context manager for acquiring a shared (reader) lock.
|
| 25 |
+
|
| 26 |
+
Example:
|
| 27 |
+
async with rwlock.reader_lock:
|
| 28 |
+
# read-only access
|
| 29 |
+
"""
|
| 30 |
+
return _ReaderLock(self)
|
| 31 |
+
|
| 32 |
+
@property
|
| 33 |
+
def writer_lock(self):
|
| 34 |
+
"""
|
| 35 |
+
A context manager for acquiring an exclusive (writer) lock.
|
| 36 |
+
|
| 37 |
+
Example:
|
| 38 |
+
async with rwlock.writer_lock:
|
| 39 |
+
# exclusive access
|
| 40 |
+
"""
|
| 41 |
+
return _WriterLock(self)
|
| 42 |
+
|
| 43 |
+
async def acquire_reader(self):
|
| 44 |
+
async with self._lock:
|
| 45 |
+
# Wait until there is no active writer or waiting writer
|
| 46 |
+
# to ensure fairness.
|
| 47 |
+
while self._writer_active or self._waiting_writers > 0:
|
| 48 |
+
await self._cond.wait()
|
| 49 |
+
self._readers += 1
|
| 50 |
+
|
| 51 |
+
async def release_reader(self):
|
| 52 |
+
async with self._lock:
|
| 53 |
+
self._readers -= 1
|
| 54 |
+
# If this was the last reader, wake up anyone waiting
|
| 55 |
+
# (potentially a writer or new readers).
|
| 56 |
+
if self._readers == 0:
|
| 57 |
+
self._cond.notify_all()
|
| 58 |
+
|
| 59 |
+
async def acquire_writer(self):
|
| 60 |
+
async with self._lock:
|
| 61 |
+
# Increment the count of writers waiting
|
| 62 |
+
self._waiting_writers += 1
|
| 63 |
+
try:
|
| 64 |
+
# Wait while either a writer is active or readers are present
|
| 65 |
+
while self._writer_active or self._readers > 0:
|
| 66 |
+
await self._cond.wait()
|
| 67 |
+
self._writer_active = True
|
| 68 |
+
finally:
|
| 69 |
+
# Decrement waiting writers only after we've acquired the writer lock
|
| 70 |
+
self._waiting_writers -= 1
|
| 71 |
+
|
| 72 |
+
async def release_writer(self):
|
| 73 |
+
async with self._lock:
|
| 74 |
+
self._writer_active = False
|
| 75 |
+
# Wake up anyone waiting (readers or writers)
|
| 76 |
+
self._cond.notify_all()
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
class _ReaderLock:
|
| 80 |
+
def __init__(self, rwlock: RWLock):
|
| 81 |
+
self._rwlock = rwlock
|
| 82 |
+
|
| 83 |
+
async def __aenter__(self):
|
| 84 |
+
await self._rwlock.acquire_reader()
|
| 85 |
+
return self
|
| 86 |
+
|
| 87 |
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
| 88 |
+
await self._rwlock.release_reader()
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
class _WriterLock:
|
| 92 |
+
def __init__(self, rwlock: RWLock):
|
| 93 |
+
self._rwlock = rwlock
|
| 94 |
+
|
| 95 |
+
async def __aenter__(self):
|
| 96 |
+
await self._rwlock.acquire_writer()
|
| 97 |
+
return self
|
| 98 |
+
|
| 99 |
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
| 100 |
+
await self._rwlock.release_writer()
|
lib/python3.12/site-packages/sglang/srt/code_completion_parser.py
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2023-2024 SGLang Team
|
| 2 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 3 |
+
# you may not use this file except in compliance with the License.
|
| 4 |
+
# You may obtain a copy of the License at
|
| 5 |
+
#
|
| 6 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 7 |
+
#
|
| 8 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 9 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 10 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 11 |
+
# See the License for the specific language governing permissions and
|
| 12 |
+
# limitations under the License.
|
| 13 |
+
# ==============================================================================
|
| 14 |
+
"""Completion templates."""
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
import dataclasses
|
| 18 |
+
import json
|
| 19 |
+
import logging
|
| 20 |
+
import os
|
| 21 |
+
from enum import auto
|
| 22 |
+
|
| 23 |
+
from sglang.srt.openai_api.protocol import ChatCompletionRequest
|
| 24 |
+
|
| 25 |
+
logger = logging.getLogger(__name__)
|
| 26 |
+
completion_template_name = None
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
class FimPosition:
|
| 30 |
+
"""Postion of fim middle token."""
|
| 31 |
+
|
| 32 |
+
MIDDLE = auto()
|
| 33 |
+
END = auto()
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
@dataclasses.dataclass
|
| 37 |
+
class CompletionTemplate:
|
| 38 |
+
"""A class that manages completion prompt templates. only for code completion currently."""
|
| 39 |
+
|
| 40 |
+
# The name of this template
|
| 41 |
+
name: str
|
| 42 |
+
|
| 43 |
+
# the fim begin token
|
| 44 |
+
fim_begin_token: str
|
| 45 |
+
|
| 46 |
+
# The fim middle token
|
| 47 |
+
fim_middle_token: str
|
| 48 |
+
|
| 49 |
+
# The fim end token
|
| 50 |
+
fim_end_token: str
|
| 51 |
+
|
| 52 |
+
# The position of the fim middle token
|
| 53 |
+
fim_position: FimPosition
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
# A global registry for all completion templates
|
| 57 |
+
completion_templates: dict[str, CompletionTemplate] = {}
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def load_completion_template_for_openai_api(completion_template_arg):
|
| 61 |
+
global completion_template_name
|
| 62 |
+
|
| 63 |
+
logger.info(
|
| 64 |
+
f"Use completion template for the OpenAI-compatible API server: {completion_template_arg}"
|
| 65 |
+
)
|
| 66 |
+
|
| 67 |
+
if not completion_template_exists(completion_template_arg):
|
| 68 |
+
if not os.path.exists(completion_template_arg):
|
| 69 |
+
raise RuntimeError(
|
| 70 |
+
f"Completion template {completion_template_arg} is not a built-in template name "
|
| 71 |
+
"or a valid completion template file path."
|
| 72 |
+
)
|
| 73 |
+
|
| 74 |
+
assert completion_template_arg.endswith(
|
| 75 |
+
".json"
|
| 76 |
+
), "unrecognized format of completion template file"
|
| 77 |
+
with open(completion_template_arg, "r") as filep:
|
| 78 |
+
template = json.load(filep)
|
| 79 |
+
try:
|
| 80 |
+
fim_position = FimPosition[template["fim_position"]]
|
| 81 |
+
except KeyError:
|
| 82 |
+
raise ValueError(
|
| 83 |
+
f"Unknown fim position: {template['fim_position']}"
|
| 84 |
+
) from None
|
| 85 |
+
register_completion_template(
|
| 86 |
+
CompletionTemplate(
|
| 87 |
+
name=template["name"],
|
| 88 |
+
fim_begin_token=template["fim_begin_token"],
|
| 89 |
+
fim_middle_token=template["fim_middle_token"],
|
| 90 |
+
fim_end_token=template["fim_end_token"],
|
| 91 |
+
fim_position=fim_position,
|
| 92 |
+
),
|
| 93 |
+
override=True,
|
| 94 |
+
)
|
| 95 |
+
completion_template_name = template["name"]
|
| 96 |
+
else:
|
| 97 |
+
completion_template_name = completion_template_arg
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
def register_completion_template(template: CompletionTemplate, override: bool = False):
|
| 101 |
+
"""Register a new completion template."""
|
| 102 |
+
if not override:
|
| 103 |
+
assert (
|
| 104 |
+
template.name not in completion_templates
|
| 105 |
+
), f"{template.name} has been registered."
|
| 106 |
+
|
| 107 |
+
completion_templates[template.name] = template
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
def completion_template_exists(template_name: str) -> bool:
|
| 111 |
+
return template_name in completion_templates
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
def is_completion_template_defined() -> bool:
|
| 115 |
+
global completion_template_name
|
| 116 |
+
return completion_template_name is not None
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
def generate_completion_prompt_from_request(request: ChatCompletionRequest) -> str:
|
| 120 |
+
global completion_template_name
|
| 121 |
+
if request.suffix == "":
|
| 122 |
+
return request.prompt
|
| 123 |
+
|
| 124 |
+
return generate_completion_prompt(
|
| 125 |
+
request.prompt, request.suffix, completion_template_name
|
| 126 |
+
)
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
def generate_completion_prompt(prompt: str, suffix: str, template_name: str) -> str:
|
| 130 |
+
|
| 131 |
+
completion_template = completion_templates[template_name]
|
| 132 |
+
fim_begin_token = completion_template.fim_begin_token
|
| 133 |
+
fim_middle_token = completion_template.fim_middle_token
|
| 134 |
+
fim_end_token = completion_template.fim_end_token
|
| 135 |
+
fim_position = completion_template.fim_position
|
| 136 |
+
|
| 137 |
+
if fim_position == FimPosition.MIDDLE:
|
| 138 |
+
prompt = f"{fim_begin_token}{prompt}{fim_middle_token}{suffix}{fim_end_token}"
|
| 139 |
+
elif fim_position == FimPosition.END:
|
| 140 |
+
prompt = f"{fim_begin_token}{prompt}{fim_end_token}{suffix}{fim_middle_token}"
|
| 141 |
+
|
| 142 |
+
return prompt
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
register_completion_template(
|
| 146 |
+
CompletionTemplate(
|
| 147 |
+
name="deepseek_coder",
|
| 148 |
+
fim_begin_token="<|fim▁begin|>",
|
| 149 |
+
fim_middle_token="<|fim▁hole|>",
|
| 150 |
+
fim_end_token="<|fim▁end|>",
|
| 151 |
+
fim_position=FimPosition.MIDDLE,
|
| 152 |
+
)
|
| 153 |
+
)
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
register_completion_template(
|
| 157 |
+
CompletionTemplate(
|
| 158 |
+
name="star_coder",
|
| 159 |
+
fim_begin_token="<fim_prefix>",
|
| 160 |
+
fim_middle_token="<fim_middle>",
|
| 161 |
+
fim_end_token="<fim_suffix>",
|
| 162 |
+
fim_position=FimPosition.END,
|
| 163 |
+
)
|
| 164 |
+
)
|
| 165 |
+
|
| 166 |
+
register_completion_template(
|
| 167 |
+
CompletionTemplate(
|
| 168 |
+
name="qwen_coder",
|
| 169 |
+
fim_begin_token="<|fim_prefix|>",
|
| 170 |
+
fim_middle_token="<|fim_middle|>",
|
| 171 |
+
fim_end_token="<|fim_suffix|>",
|
| 172 |
+
fim_position=FimPosition.END,
|
| 173 |
+
)
|
| 174 |
+
)
|
lib/python3.12/site-packages/sglang/srt/conversation.py
ADDED
|
@@ -0,0 +1,794 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2023-2024 SGLang Team
|
| 2 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 3 |
+
# you may not use this file except in compliance with the License.
|
| 4 |
+
# You may obtain a copy of the License at
|
| 5 |
+
#
|
| 6 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 7 |
+
#
|
| 8 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 9 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 10 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 11 |
+
# See the License for the specific language governing permissions and
|
| 12 |
+
# limitations under the License.
|
| 13 |
+
# ==============================================================================
|
| 14 |
+
"""Conversation chat templates."""
|
| 15 |
+
|
| 16 |
+
# Adapted from
|
| 17 |
+
# https://github.com/lm-sys/FastChat/blob/main/fastchat/conversation.py
|
| 18 |
+
import dataclasses
|
| 19 |
+
from enum import IntEnum, auto
|
| 20 |
+
from typing import Dict, List, Optional, Tuple, Union
|
| 21 |
+
|
| 22 |
+
from sglang.srt.openai_api.protocol import ChatCompletionRequest
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
class SeparatorStyle(IntEnum):
|
| 26 |
+
"""Separator styles."""
|
| 27 |
+
|
| 28 |
+
ADD_COLON_SINGLE = auto()
|
| 29 |
+
ADD_COLON_TWO = auto()
|
| 30 |
+
ADD_COLON_SPACE_SINGLE = auto()
|
| 31 |
+
NO_COLON_SINGLE = auto()
|
| 32 |
+
NO_COLON_TWO = auto()
|
| 33 |
+
ADD_NEW_LINE_SINGLE = auto()
|
| 34 |
+
LLAMA2 = auto()
|
| 35 |
+
LLAMA3 = auto()
|
| 36 |
+
LLAMA4 = auto()
|
| 37 |
+
CHATGLM = auto()
|
| 38 |
+
CHATML = auto()
|
| 39 |
+
CHATINTERN = auto()
|
| 40 |
+
DOLLY = auto()
|
| 41 |
+
RWKV = auto()
|
| 42 |
+
PHOENIX = auto()
|
| 43 |
+
ROBIN = auto()
|
| 44 |
+
FALCON_CHAT = auto()
|
| 45 |
+
CHATGLM3 = auto()
|
| 46 |
+
DEEPSEEK_CHAT = auto()
|
| 47 |
+
METAMATH = auto()
|
| 48 |
+
DeepSeekVL2 = auto()
|
| 49 |
+
QWEN2_VL_EMBED = auto()
|
| 50 |
+
GEMMA3 = auto()
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
@dataclasses.dataclass
|
| 54 |
+
class Conversation:
|
| 55 |
+
"""A class that manages prompt templates and keeps all conversation history."""
|
| 56 |
+
|
| 57 |
+
# The name of this template
|
| 58 |
+
name: str
|
| 59 |
+
# The template of the system prompt
|
| 60 |
+
system_template: str = "{system_message}"
|
| 61 |
+
# The system message
|
| 62 |
+
system_message: str = ""
|
| 63 |
+
# The names of two roles
|
| 64 |
+
roles: Tuple[str] = ("USER", "ASSISTANT")
|
| 65 |
+
# All messages. Each item is (role, message).
|
| 66 |
+
messages: List[List[str]] = ()
|
| 67 |
+
# The number of few shot examples
|
| 68 |
+
offset: int = 0
|
| 69 |
+
# The separator style and configurations
|
| 70 |
+
sep_style: SeparatorStyle = SeparatorStyle.ADD_COLON_SINGLE
|
| 71 |
+
sep: str = "\n"
|
| 72 |
+
sep2: str = None
|
| 73 |
+
# Stop criteria (the default one is EOS token)
|
| 74 |
+
stop_str: Union[str, List[str]] = None
|
| 75 |
+
# The string that represents an image token in the prompt
|
| 76 |
+
image_token: str = "<image>"
|
| 77 |
+
audio_token: str = "<audio>"
|
| 78 |
+
|
| 79 |
+
image_data: Optional[List[str]] = None
|
| 80 |
+
modalities: Optional[List[str]] = None
|
| 81 |
+
stop_token_ids: Optional[int] = None
|
| 82 |
+
|
| 83 |
+
audio_data: Optional[List[str]] = None
|
| 84 |
+
|
| 85 |
+
def get_prompt(self) -> str:
|
| 86 |
+
"""Get the prompt for generation."""
|
| 87 |
+
system_prompt = self.system_template.format(system_message=self.system_message)
|
| 88 |
+
if self.sep_style == SeparatorStyle.ADD_COLON_SINGLE:
|
| 89 |
+
ret = system_prompt + self.sep
|
| 90 |
+
for role, message in self.messages:
|
| 91 |
+
if message:
|
| 92 |
+
ret += role + ": " + message + self.sep
|
| 93 |
+
else:
|
| 94 |
+
ret += role + ":"
|
| 95 |
+
return ret
|
| 96 |
+
elif self.sep_style == SeparatorStyle.ADD_COLON_TWO:
|
| 97 |
+
seps = [self.sep, self.sep2]
|
| 98 |
+
ret = system_prompt + seps[0]
|
| 99 |
+
for i, (role, message) in enumerate(self.messages):
|
| 100 |
+
if message:
|
| 101 |
+
ret += role + ": " + message + seps[i % 2]
|
| 102 |
+
else:
|
| 103 |
+
ret += role + ":"
|
| 104 |
+
return ret
|
| 105 |
+
elif self.sep_style == SeparatorStyle.ADD_COLON_SPACE_SINGLE:
|
| 106 |
+
ret = system_prompt + self.sep
|
| 107 |
+
for role, message in self.messages:
|
| 108 |
+
if message:
|
| 109 |
+
ret += role + ": " + message + self.sep
|
| 110 |
+
else:
|
| 111 |
+
ret += role + ": " # must be end with a space
|
| 112 |
+
return ret
|
| 113 |
+
elif self.sep_style == SeparatorStyle.ADD_NEW_LINE_SINGLE:
|
| 114 |
+
ret = "" if system_prompt == "" else system_prompt + self.sep
|
| 115 |
+
for role, message in self.messages:
|
| 116 |
+
if message:
|
| 117 |
+
ret += role + "\n" + message + self.sep
|
| 118 |
+
else:
|
| 119 |
+
ret += role + "\n"
|
| 120 |
+
return ret
|
| 121 |
+
elif self.sep_style == SeparatorStyle.QWEN2_VL_EMBED:
|
| 122 |
+
ret = "" if system_prompt == "" else system_prompt + self.sep
|
| 123 |
+
for role, message in self.messages:
|
| 124 |
+
if message:
|
| 125 |
+
ret += role + "\n" + message + self.sep
|
| 126 |
+
else:
|
| 127 |
+
ret += role + "\n"
|
| 128 |
+
ret += self.stop_str
|
| 129 |
+
return ret
|
| 130 |
+
elif self.sep_style == SeparatorStyle.NO_COLON_SINGLE:
|
| 131 |
+
ret = system_prompt
|
| 132 |
+
for role, message in self.messages:
|
| 133 |
+
if message:
|
| 134 |
+
ret += role + message + self.sep
|
| 135 |
+
else:
|
| 136 |
+
ret += role
|
| 137 |
+
return ret
|
| 138 |
+
elif self.sep_style == SeparatorStyle.NO_COLON_TWO:
|
| 139 |
+
seps = [self.sep, self.sep2]
|
| 140 |
+
ret = system_prompt
|
| 141 |
+
for i, (role, message) in enumerate(self.messages):
|
| 142 |
+
if message:
|
| 143 |
+
ret += role + message + seps[i % 2]
|
| 144 |
+
else:
|
| 145 |
+
ret += role
|
| 146 |
+
return ret
|
| 147 |
+
elif self.sep_style == SeparatorStyle.RWKV:
|
| 148 |
+
ret = system_prompt
|
| 149 |
+
for i, (role, message) in enumerate(self.messages):
|
| 150 |
+
if message:
|
| 151 |
+
ret += (
|
| 152 |
+
role
|
| 153 |
+
+ ": "
|
| 154 |
+
+ message.replace("\r\n", "\n").replace("\n\n", "\n")
|
| 155 |
+
)
|
| 156 |
+
ret += "\n\n"
|
| 157 |
+
else:
|
| 158 |
+
ret += role + ":"
|
| 159 |
+
return ret
|
| 160 |
+
elif self.sep_style == SeparatorStyle.LLAMA4:
|
| 161 |
+
# begin_of_text is added by default
|
| 162 |
+
if self.system_message:
|
| 163 |
+
ret = system_prompt
|
| 164 |
+
else:
|
| 165 |
+
ret = ""
|
| 166 |
+
for i, (role, message) in enumerate(self.messages):
|
| 167 |
+
if message:
|
| 168 |
+
ret += f"<|header_start|>{role}<|header_end|>\n\n"
|
| 169 |
+
ret += f"{message.strip()}<|eot|>"
|
| 170 |
+
else:
|
| 171 |
+
ret += f"<|header_start|>{role}<|header_end|>\n\n"
|
| 172 |
+
return ret
|
| 173 |
+
elif self.sep_style == SeparatorStyle.LLAMA3:
|
| 174 |
+
if self.system_message:
|
| 175 |
+
ret = system_prompt
|
| 176 |
+
else:
|
| 177 |
+
ret = ""
|
| 178 |
+
for i, (role, message) in enumerate(self.messages):
|
| 179 |
+
if message:
|
| 180 |
+
ret += f"<|start_header_id|>{role}<|end_header_id|>\n\n"
|
| 181 |
+
ret += f"{message.strip()}<|eot_id|>"
|
| 182 |
+
else:
|
| 183 |
+
ret += f"<|start_header_id|>{role}<|end_header_id|>\n\n"
|
| 184 |
+
return ret
|
| 185 |
+
elif self.sep_style == SeparatorStyle.LLAMA2:
|
| 186 |
+
seps = [self.sep, self.sep2]
|
| 187 |
+
if self.system_message:
|
| 188 |
+
ret = system_prompt
|
| 189 |
+
else:
|
| 190 |
+
ret = "[INST] "
|
| 191 |
+
for i, (role, message) in enumerate(self.messages):
|
| 192 |
+
tag = self.roles[i % 2]
|
| 193 |
+
if message:
|
| 194 |
+
if i == 0:
|
| 195 |
+
ret += message + " "
|
| 196 |
+
else:
|
| 197 |
+
ret += tag + " " + message + seps[i % 2]
|
| 198 |
+
else:
|
| 199 |
+
ret += tag
|
| 200 |
+
return ret
|
| 201 |
+
elif self.sep_style == SeparatorStyle.CHATGLM:
|
| 202 |
+
# source: https://huggingface.co/THUDM/chatglm-6b/blob/1d240ba371910e9282298d4592532d7f0f3e9f3e/modeling_chatglm.py#L1302-L1308
|
| 203 |
+
# source2: https://huggingface.co/THUDM/chatglm2-6b/blob/e186c891cf64310ac66ef10a87e6635fa6c2a579/modeling_chatglm.py#L926
|
| 204 |
+
round_add_n = 1 if self.name == "chatglm2" else 0
|
| 205 |
+
if system_prompt:
|
| 206 |
+
ret = system_prompt + self.sep
|
| 207 |
+
else:
|
| 208 |
+
ret = ""
|
| 209 |
+
|
| 210 |
+
for i, (role, message) in enumerate(self.messages):
|
| 211 |
+
if i % 2 == 0:
|
| 212 |
+
ret += f"[Round {i // 2 + round_add_n}]{self.sep}"
|
| 213 |
+
|
| 214 |
+
if message:
|
| 215 |
+
ret += f"{role}:{message}{self.sep}"
|
| 216 |
+
else:
|
| 217 |
+
ret += f"{role}:"
|
| 218 |
+
return ret
|
| 219 |
+
elif self.sep_style == SeparatorStyle.CHATML:
|
| 220 |
+
ret = "" if system_prompt == "" else system_prompt + self.sep + "\n"
|
| 221 |
+
for role, message in self.messages:
|
| 222 |
+
if message:
|
| 223 |
+
ret += role + "\n" + message + self.sep + "\n"
|
| 224 |
+
else:
|
| 225 |
+
ret += role + "\n"
|
| 226 |
+
return ret
|
| 227 |
+
elif self.sep_style == SeparatorStyle.CHATGLM3:
|
| 228 |
+
ret = ""
|
| 229 |
+
if self.system_message:
|
| 230 |
+
ret += system_prompt
|
| 231 |
+
for role, message in self.messages:
|
| 232 |
+
if message:
|
| 233 |
+
ret += role + "\n" + message
|
| 234 |
+
else:
|
| 235 |
+
ret += role
|
| 236 |
+
return ret
|
| 237 |
+
elif self.sep_style == SeparatorStyle.CHATINTERN:
|
| 238 |
+
# source: https://huggingface.co/internlm/internlm-chat-7b-8k/blob/bd546fa984b4b0b86958f56bf37f94aa75ab8831/modeling_internlm.py#L771
|
| 239 |
+
seps = [self.sep, self.sep2]
|
| 240 |
+
ret = system_prompt
|
| 241 |
+
for i, (role, message) in enumerate(self.messages):
|
| 242 |
+
if i % 2 == 0:
|
| 243 |
+
ret += "<s>"
|
| 244 |
+
if message:
|
| 245 |
+
ret += role + ":" + message + seps[i % 2] + "\n"
|
| 246 |
+
else:
|
| 247 |
+
ret += role + ":"
|
| 248 |
+
return ret
|
| 249 |
+
elif self.sep_style == SeparatorStyle.DOLLY:
|
| 250 |
+
seps = [self.sep, self.sep2]
|
| 251 |
+
ret = system_prompt
|
| 252 |
+
for i, (role, message) in enumerate(self.messages):
|
| 253 |
+
if message:
|
| 254 |
+
ret += role + ":\n" + message + seps[i % 2]
|
| 255 |
+
if i % 2 == 1:
|
| 256 |
+
ret += "\n\n"
|
| 257 |
+
else:
|
| 258 |
+
ret += role + ":\n"
|
| 259 |
+
return ret
|
| 260 |
+
elif self.sep_style == SeparatorStyle.PHOENIX:
|
| 261 |
+
ret = system_prompt
|
| 262 |
+
for role, message in self.messages:
|
| 263 |
+
if message:
|
| 264 |
+
ret += role + ": " + "<s>" + message + "</s>"
|
| 265 |
+
else:
|
| 266 |
+
ret += role + ": " + "<s>"
|
| 267 |
+
return ret
|
| 268 |
+
elif self.sep_style == SeparatorStyle.ROBIN:
|
| 269 |
+
ret = system_prompt + self.sep
|
| 270 |
+
for role, message in self.messages:
|
| 271 |
+
if message:
|
| 272 |
+
ret += role + ":\n" + message + self.sep
|
| 273 |
+
else:
|
| 274 |
+
ret += role + ":\n"
|
| 275 |
+
return ret
|
| 276 |
+
elif self.sep_style == SeparatorStyle.FALCON_CHAT:
|
| 277 |
+
ret = ""
|
| 278 |
+
if self.system_message:
|
| 279 |
+
ret += system_prompt + self.sep
|
| 280 |
+
for role, message in self.messages:
|
| 281 |
+
if message:
|
| 282 |
+
ret += role + ": " + message + self.sep
|
| 283 |
+
else:
|
| 284 |
+
ret += role + ":"
|
| 285 |
+
return ret
|
| 286 |
+
elif self.sep_style == SeparatorStyle.METAMATH:
|
| 287 |
+
ret = "" if system_prompt == "" else system_prompt + self.sep
|
| 288 |
+
for i, (role, message) in enumerate(self.messages):
|
| 289 |
+
# For MetaMath, sep2 is used to prefix the message.
|
| 290 |
+
starting_sep = ":\n" if i % 2 == 0 else ": " + self.sep2
|
| 291 |
+
ending_sep = self.sep if i % 2 == 0 else ""
|
| 292 |
+
if message:
|
| 293 |
+
ret += role + starting_sep + message + ending_sep
|
| 294 |
+
else:
|
| 295 |
+
ret += role + starting_sep
|
| 296 |
+
return ret
|
| 297 |
+
elif self.sep_style == SeparatorStyle.DEEPSEEK_CHAT:
|
| 298 |
+
seps = [self.sep, self.sep2]
|
| 299 |
+
ret = system_prompt
|
| 300 |
+
for i, (role, message) in enumerate(self.messages):
|
| 301 |
+
if message:
|
| 302 |
+
ret += role + ": " + message + seps[i % 2]
|
| 303 |
+
else:
|
| 304 |
+
ret += role + ":"
|
| 305 |
+
return ret
|
| 306 |
+
elif self.sep_style == SeparatorStyle.DeepSeekVL2:
|
| 307 |
+
seps = [self.sep, self.sep2]
|
| 308 |
+
if system_prompt == "" or system_prompt is None:
|
| 309 |
+
ret = ""
|
| 310 |
+
else:
|
| 311 |
+
ret = system_prompt + seps[0]
|
| 312 |
+
for i, (role, message) in enumerate(self.messages):
|
| 313 |
+
if message:
|
| 314 |
+
ret += role + ": " + message + seps[i % 2]
|
| 315 |
+
else:
|
| 316 |
+
ret += role + ":"
|
| 317 |
+
return ret
|
| 318 |
+
elif self.sep_style == SeparatorStyle.GEMMA3:
|
| 319 |
+
ret = system_prompt
|
| 320 |
+
for i, (role, message) in enumerate(self.messages):
|
| 321 |
+
if message:
|
| 322 |
+
if i == 0:
|
| 323 |
+
ret += message + self.sep
|
| 324 |
+
else:
|
| 325 |
+
ret += role + message + self.sep
|
| 326 |
+
else:
|
| 327 |
+
ret += role
|
| 328 |
+
return ret
|
| 329 |
+
|
| 330 |
+
else:
|
| 331 |
+
raise ValueError(f"Invalid style: {self.sep_style}")
|
| 332 |
+
|
| 333 |
+
def set_system_message(self, system_message: str):
|
| 334 |
+
"""Set the system message."""
|
| 335 |
+
self.system_message = system_message
|
| 336 |
+
|
| 337 |
+
def append_message(self, role: str, message: str):
|
| 338 |
+
"""Append a new message."""
|
| 339 |
+
self.messages.append([role, message])
|
| 340 |
+
|
| 341 |
+
def append_image(self, image: str):
|
| 342 |
+
"""Append a new message."""
|
| 343 |
+
self.image_data.append(image)
|
| 344 |
+
|
| 345 |
+
def append_audio(self, audio: str):
|
| 346 |
+
"""Append a new message."""
|
| 347 |
+
self.audio_data.append(audio)
|
| 348 |
+
|
| 349 |
+
def update_last_message(self, message: str):
|
| 350 |
+
"""Update the last output.
|
| 351 |
+
|
| 352 |
+
The last message is typically set to be None when constructing the prompt,
|
| 353 |
+
so we need to update it in-place after getting the response from a model.
|
| 354 |
+
"""
|
| 355 |
+
self.messages[-1][1] = message
|
| 356 |
+
|
| 357 |
+
def to_gradio_chatbot(self):
|
| 358 |
+
"""Convert the conversation to gradio chatbot format."""
|
| 359 |
+
ret = []
|
| 360 |
+
for i, (role, msg) in enumerate(self.messages[self.offset :]):
|
| 361 |
+
if i % 2 == 0:
|
| 362 |
+
ret.append([msg, None])
|
| 363 |
+
else:
|
| 364 |
+
ret[-1][-1] = msg
|
| 365 |
+
return ret
|
| 366 |
+
|
| 367 |
+
def to_openai_api_messages(self):
|
| 368 |
+
"""Convert the conversation to OpenAI chat completion format."""
|
| 369 |
+
if self.system_message == "":
|
| 370 |
+
ret = []
|
| 371 |
+
else:
|
| 372 |
+
ret = [{"role": "system", "content": self.system_message}]
|
| 373 |
+
|
| 374 |
+
for i, (_, msg) in enumerate(self.messages[self.offset :]):
|
| 375 |
+
if i % 2 == 0:
|
| 376 |
+
ret.append({"role": "user", "content": msg})
|
| 377 |
+
else:
|
| 378 |
+
if msg is not None:
|
| 379 |
+
ret.append({"role": "assistant", "content": msg})
|
| 380 |
+
return ret
|
| 381 |
+
|
| 382 |
+
def copy(self):
|
| 383 |
+
return Conversation(
|
| 384 |
+
name=self.name,
|
| 385 |
+
system_template=self.system_template,
|
| 386 |
+
system_message=self.system_message,
|
| 387 |
+
roles=self.roles,
|
| 388 |
+
messages=[[x, y] for x, y in self.messages],
|
| 389 |
+
offset=self.offset,
|
| 390 |
+
sep_style=self.sep_style,
|
| 391 |
+
sep=self.sep,
|
| 392 |
+
sep2=self.sep2,
|
| 393 |
+
stop_str=self.stop_str,
|
| 394 |
+
image_token=self.image_token,
|
| 395 |
+
audio_token=self.audio_token,
|
| 396 |
+
)
|
| 397 |
+
|
| 398 |
+
def dict(self):
|
| 399 |
+
return {
|
| 400 |
+
"template_name": self.name,
|
| 401 |
+
"system_message": self.system_message,
|
| 402 |
+
"roles": self.roles,
|
| 403 |
+
"messages": self.messages,
|
| 404 |
+
"offset": self.offset,
|
| 405 |
+
}
|
| 406 |
+
|
| 407 |
+
|
| 408 |
+
# A global registry for all conversation templates
|
| 409 |
+
chat_templates: Dict[str, Conversation] = {}
|
| 410 |
+
|
| 411 |
+
|
| 412 |
+
def register_conv_template(template: Conversation, override: bool = False):
|
| 413 |
+
"""Register a new conversation template."""
|
| 414 |
+
if not override:
|
| 415 |
+
assert (
|
| 416 |
+
template.name not in chat_templates
|
| 417 |
+
), f"{template.name} has been registered."
|
| 418 |
+
|
| 419 |
+
chat_templates[template.name] = template
|
| 420 |
+
|
| 421 |
+
|
| 422 |
+
def chat_template_exists(template_name: str) -> bool:
|
| 423 |
+
return template_name in chat_templates
|
| 424 |
+
|
| 425 |
+
|
| 426 |
+
def generate_embedding_convs(
|
| 427 |
+
texts: List[str], images: List[str], template_name: str
|
| 428 |
+
) -> List[Conversation]:
|
| 429 |
+
conv_template = chat_templates[template_name].copy()
|
| 430 |
+
convs = []
|
| 431 |
+
for text, image in zip(texts, images):
|
| 432 |
+
conv = Conversation(
|
| 433 |
+
name=conv_template.name,
|
| 434 |
+
system_template=conv_template.system_template,
|
| 435 |
+
system_message=conv_template.system_message,
|
| 436 |
+
roles=conv_template.roles,
|
| 437 |
+
messages=list(conv_template.messages), # prevent in-place modification
|
| 438 |
+
offset=conv_template.offset,
|
| 439 |
+
sep_style=SeparatorStyle(conv_template.sep_style),
|
| 440 |
+
sep=conv_template.sep,
|
| 441 |
+
sep2=conv_template.sep2,
|
| 442 |
+
stop_str=conv_template.stop_str,
|
| 443 |
+
image_data=[],
|
| 444 |
+
modalities=[],
|
| 445 |
+
image_token=conv_template.image_token,
|
| 446 |
+
)
|
| 447 |
+
real_content = ""
|
| 448 |
+
|
| 449 |
+
if image is not None:
|
| 450 |
+
image_token = (
|
| 451 |
+
conv.image_token + "\n"
|
| 452 |
+
if conv.name != "gme-qwen2-vl"
|
| 453 |
+
else conv.image_token
|
| 454 |
+
)
|
| 455 |
+
real_content += image_token
|
| 456 |
+
if text is not None:
|
| 457 |
+
real_content += text
|
| 458 |
+
conv.append_message(conv.roles[0], real_content)
|
| 459 |
+
# Add a blank message for the assistant.
|
| 460 |
+
conv.append_message(conv.roles[1], None)
|
| 461 |
+
convs.append(conv)
|
| 462 |
+
|
| 463 |
+
return convs
|
| 464 |
+
|
| 465 |
+
|
| 466 |
+
# Models in which system adds modality tokens at prompt start automatically
|
| 467 |
+
# when media inputs exceed modality tokens in prompt (e.g. 3 images but 2 <image> tokens)
|
| 468 |
+
_MODELS_REQUIRING_MODALITY_SUPPLEMENT = {"deepseek-vl2"}
|
| 469 |
+
|
| 470 |
+
|
| 471 |
+
# adapted from https://github.com/vllm-project/vllm/blob/5124f5bf51b83e6f344c1bc6652e8c4d81313b34/vllm/entrypoints/chat_utils.py#L856
|
| 472 |
+
def _get_full_multimodal_text_prompt(
|
| 473 |
+
modality_token: str, modality_count: int, text_prompt: str
|
| 474 |
+
) -> str:
|
| 475 |
+
"""Combine multimodal prompts for a multimodal language model."""
|
| 476 |
+
|
| 477 |
+
# For any existing placeholder in the text prompt, we leave it as is
|
| 478 |
+
left: int = modality_count - text_prompt.count(modality_token)
|
| 479 |
+
if left < 0:
|
| 480 |
+
raise ValueError(
|
| 481 |
+
f"Found more '{modality_token}' placeholders in input prompt than "
|
| 482 |
+
"actual multimodal data items."
|
| 483 |
+
)
|
| 484 |
+
|
| 485 |
+
# NOTE: For now we always add missing modality_token at the front of
|
| 486 |
+
# the prompt. This may change to be customizable in the future.
|
| 487 |
+
return "\n".join([modality_token] * left + [text_prompt])
|
| 488 |
+
|
| 489 |
+
|
| 490 |
+
def generate_chat_conv(
|
| 491 |
+
request: ChatCompletionRequest, template_name: str
|
| 492 |
+
) -> Conversation:
|
| 493 |
+
conv = chat_templates[template_name].copy()
|
| 494 |
+
conv = Conversation(
|
| 495 |
+
name=conv.name,
|
| 496 |
+
system_template=conv.system_template,
|
| 497 |
+
system_message=conv.system_message,
|
| 498 |
+
roles=conv.roles,
|
| 499 |
+
messages=list(conv.messages), # prevent in-place modification
|
| 500 |
+
offset=conv.offset,
|
| 501 |
+
sep_style=SeparatorStyle(conv.sep_style),
|
| 502 |
+
sep=conv.sep,
|
| 503 |
+
sep2=conv.sep2,
|
| 504 |
+
stop_str=conv.stop_str,
|
| 505 |
+
image_data=[],
|
| 506 |
+
audio_data=[],
|
| 507 |
+
modalities=[],
|
| 508 |
+
image_token=conv.image_token,
|
| 509 |
+
audio_token=conv.audio_token,
|
| 510 |
+
)
|
| 511 |
+
|
| 512 |
+
if isinstance(request.messages, str):
|
| 513 |
+
raise ValueError("The messages should be a list of dict.")
|
| 514 |
+
for message in request.messages:
|
| 515 |
+
msg_role = message.role
|
| 516 |
+
if msg_role == "system":
|
| 517 |
+
if isinstance(message.content, str):
|
| 518 |
+
conv.system_message = message.content
|
| 519 |
+
elif isinstance(message.content, list):
|
| 520 |
+
if (
|
| 521 |
+
len(message.content) != 1
|
| 522 |
+
or getattr(message.content[0], "type", None) != "text"
|
| 523 |
+
):
|
| 524 |
+
raise ValueError("The system message should be a single text.")
|
| 525 |
+
else:
|
| 526 |
+
conv.system_message = getattr(message.content[0], "text", "")
|
| 527 |
+
elif msg_role == "user":
|
| 528 |
+
# Handle the various types of Chat Request content types here.
|
| 529 |
+
if isinstance(message.content, str):
|
| 530 |
+
conv.append_message(conv.roles[0], message.content)
|
| 531 |
+
else:
|
| 532 |
+
real_content = ""
|
| 533 |
+
# calculate number of image_url
|
| 534 |
+
num_image_url = 0
|
| 535 |
+
for content in message.content:
|
| 536 |
+
if content.type == "image_url":
|
| 537 |
+
num_image_url += 1
|
| 538 |
+
conv.modalities.append(content.modalities)
|
| 539 |
+
if num_image_url > 1:
|
| 540 |
+
image_token = conv.image_token
|
| 541 |
+
else:
|
| 542 |
+
image_token = (
|
| 543 |
+
conv.image_token + "\n"
|
| 544 |
+
if conv.name != "qwen2-vl"
|
| 545 |
+
else conv.image_token
|
| 546 |
+
)
|
| 547 |
+
add_token_as_needed: bool = (
|
| 548 |
+
conv.name in _MODELS_REQUIRING_MODALITY_SUPPLEMENT
|
| 549 |
+
)
|
| 550 |
+
if add_token_as_needed:
|
| 551 |
+
image_token = ""
|
| 552 |
+
|
| 553 |
+
audio_token = conv.audio_token
|
| 554 |
+
for content in message.content:
|
| 555 |
+
if content.type == "text":
|
| 556 |
+
if num_image_url > 16:
|
| 557 |
+
real_content += "\n" # for video
|
| 558 |
+
real_content += content.text
|
| 559 |
+
elif content.type == "image_url":
|
| 560 |
+
# NOTE: Only works for llava
|
| 561 |
+
real_content += image_token
|
| 562 |
+
conv.append_image(content.image_url.url)
|
| 563 |
+
elif content.type == "audio_url":
|
| 564 |
+
real_content += audio_token
|
| 565 |
+
conv.append_audio(content.audio_url.url)
|
| 566 |
+
if add_token_as_needed:
|
| 567 |
+
real_content = _get_full_multimodal_text_prompt(
|
| 568 |
+
conv.image_token, num_image_url, real_content
|
| 569 |
+
)
|
| 570 |
+
conv.append_message(conv.roles[0], real_content)
|
| 571 |
+
elif msg_role == "assistant":
|
| 572 |
+
parsed_content = ""
|
| 573 |
+
if isinstance(message.content, str):
|
| 574 |
+
parsed_content = message.content
|
| 575 |
+
elif isinstance(message.content, list):
|
| 576 |
+
if (
|
| 577 |
+
len(message.content) != 1
|
| 578 |
+
or getattr(message.content[0], "type", None) != "text"
|
| 579 |
+
):
|
| 580 |
+
raise ValueError(
|
| 581 |
+
"The assistant's response should be a single text."
|
| 582 |
+
)
|
| 583 |
+
else:
|
| 584 |
+
parsed_content = getattr(message.content[0], "text", "")
|
| 585 |
+
conv.append_message(conv.roles[1], parsed_content)
|
| 586 |
+
else:
|
| 587 |
+
raise ValueError(f"Unknown role: {msg_role}")
|
| 588 |
+
|
| 589 |
+
# Add a blank message for the assistant.
|
| 590 |
+
conv.append_message(conv.roles[1], None)
|
| 591 |
+
return conv
|
| 592 |
+
|
| 593 |
+
|
| 594 |
+
# llama2 template
|
| 595 |
+
# reference: https://huggingface.co/blog/codellama#conversational-instructions
|
| 596 |
+
# reference: https://github.com/facebookresearch/llama/blob/1a240688810f8036049e8da36b073f63d2ac552c/llama/generation.py#L212
|
| 597 |
+
register_conv_template(
|
| 598 |
+
Conversation(
|
| 599 |
+
name="llama-2",
|
| 600 |
+
system_template="[INST] <<SYS>>\n{system_message}\n<</SYS>>\n\n",
|
| 601 |
+
roles=("[INST]", "[/INST]"),
|
| 602 |
+
sep_style=SeparatorStyle.LLAMA2,
|
| 603 |
+
sep=" ",
|
| 604 |
+
sep2=" </s><s>",
|
| 605 |
+
stop_str=["[INST]", "[/INST]", "<<SYS>>", "<</SYS>>"],
|
| 606 |
+
)
|
| 607 |
+
)
|
| 608 |
+
|
| 609 |
+
# reference: https://huggingface.co/meta-llama/Llama-4-Scout-17B-16E-Instruct/blob/main/chat_template.json
|
| 610 |
+
register_conv_template(
|
| 611 |
+
Conversation(
|
| 612 |
+
name="llama-4",
|
| 613 |
+
system_template="<|header_start|>system<|header_end|>\n\n{system_message}<|eot|>",
|
| 614 |
+
roles=("user", "assistant"),
|
| 615 |
+
sep_style=SeparatorStyle.LLAMA4,
|
| 616 |
+
sep="",
|
| 617 |
+
stop_str=["<|end_of_text|>", "<|eot|>", "<|eom|>"],
|
| 618 |
+
image_token="<|image|>",
|
| 619 |
+
)
|
| 620 |
+
)
|
| 621 |
+
|
| 622 |
+
register_conv_template(
|
| 623 |
+
Conversation(
|
| 624 |
+
name="chatml",
|
| 625 |
+
system_template="<|im_start|>system\n{system_message}",
|
| 626 |
+
system_message="You are a helpful assistant.",
|
| 627 |
+
roles=("<|im_start|>user", "<|im_start|>assistant"),
|
| 628 |
+
sep_style=SeparatorStyle.CHATML,
|
| 629 |
+
sep="<|im_end|>",
|
| 630 |
+
stop_str=["<|endoftext|>", "<|im_end|>"],
|
| 631 |
+
)
|
| 632 |
+
)
|
| 633 |
+
|
| 634 |
+
register_conv_template(
|
| 635 |
+
Conversation(
|
| 636 |
+
name="chatml-llava",
|
| 637 |
+
system_template="<|im_start|>system\n{system_message}",
|
| 638 |
+
system_message="You are a helpful assistant.",
|
| 639 |
+
roles=("<|im_start|>user", "<|im_start|>assistant"),
|
| 640 |
+
sep_style=SeparatorStyle.CHATML,
|
| 641 |
+
sep="<|im_end|>",
|
| 642 |
+
stop_str=["<|endoftext|>", "<|im_end|>"],
|
| 643 |
+
)
|
| 644 |
+
)
|
| 645 |
+
|
| 646 |
+
register_conv_template(
|
| 647 |
+
Conversation(
|
| 648 |
+
name="vicuna_v1.1",
|
| 649 |
+
system_message="A chat between a curious user and an artificial intelligence assistant. "
|
| 650 |
+
"The assistant gives helpful, detailed, and polite answers to the user's questions.",
|
| 651 |
+
roles=("USER", "ASSISTANT"),
|
| 652 |
+
sep_style=SeparatorStyle.ADD_COLON_TWO,
|
| 653 |
+
sep=" ",
|
| 654 |
+
sep2="</s>",
|
| 655 |
+
)
|
| 656 |
+
)
|
| 657 |
+
|
| 658 |
+
register_conv_template(
|
| 659 |
+
Conversation(
|
| 660 |
+
name="llama_3_vision",
|
| 661 |
+
system_message="You are a helpful language and vision assistant. You are able to understand the visual content that the user provides, and assist the user with a variety of tasks using natural language.",
|
| 662 |
+
system_template="<|start_header_id|>system<|end_header_id|>\n\n{system_message}<|eot_id|>",
|
| 663 |
+
roles=("user", "assistant"),
|
| 664 |
+
sep_style=SeparatorStyle.LLAMA3,
|
| 665 |
+
sep="",
|
| 666 |
+
stop_str=["<|end_of_text|>", "<|eot_id|>"],
|
| 667 |
+
image_token="<|image|>",
|
| 668 |
+
)
|
| 669 |
+
)
|
| 670 |
+
|
| 671 |
+
register_conv_template(
|
| 672 |
+
Conversation(
|
| 673 |
+
name="llava_llama_3",
|
| 674 |
+
system_message="You are a helpful language and vision assistant. You are able to understand the visual content that the user provides, and assist the user with a variety of tasks using natural language.",
|
| 675 |
+
system_template="<|start_header_id|>system<|end_header_id|>\n\n{system_message}<|eot_id|>",
|
| 676 |
+
roles=("user", "assistant"),
|
| 677 |
+
sep_style=SeparatorStyle.LLAMA3,
|
| 678 |
+
sep="",
|
| 679 |
+
stop_str=["<|end_of_text|>", "<|eot_id|>"],
|
| 680 |
+
)
|
| 681 |
+
)
|
| 682 |
+
# Reference: https://github.com/InternLM/lmdeploy/blob/387bf54b4f124e72aab30ae9755f562e435d3d01/lmdeploy/model.py#L425-L442
|
| 683 |
+
register_conv_template(
|
| 684 |
+
Conversation(
|
| 685 |
+
name="internlm2-chat",
|
| 686 |
+
system_template="<|im_start|>system\n{system_message}",
|
| 687 |
+
roles=("<|im_start|>user", "<|im_start|>assistant"),
|
| 688 |
+
sep="\n",
|
| 689 |
+
stop_str=["<|im_end|>", "<|action_end|>"],
|
| 690 |
+
)
|
| 691 |
+
)
|
| 692 |
+
|
| 693 |
+
# Reference: https://huggingface.co/docs/transformers/main/model_doc/qwen2_vl#usage-example
|
| 694 |
+
register_conv_template(
|
| 695 |
+
Conversation(
|
| 696 |
+
name="qwen2-vl",
|
| 697 |
+
system_message="You are a helpful assistant.",
|
| 698 |
+
system_template="<|im_start|>system\n{system_message}",
|
| 699 |
+
roles=("<|im_start|>user", "<|im_start|>assistant"),
|
| 700 |
+
sep="<|im_end|>\n",
|
| 701 |
+
sep_style=SeparatorStyle.ADD_NEW_LINE_SINGLE,
|
| 702 |
+
stop_str=["<|im_end|>"],
|
| 703 |
+
image_token="<|vision_start|><|image_pad|><|vision_end|>",
|
| 704 |
+
)
|
| 705 |
+
)
|
| 706 |
+
|
| 707 |
+
register_conv_template(
|
| 708 |
+
Conversation(
|
| 709 |
+
name="deepseek-vl2",
|
| 710 |
+
system_template="{system_message}",
|
| 711 |
+
# system_message="You are a helpful assistant. Please answer truthfully and write out your "
|
| 712 |
+
# "thinking step by step to be sure you get the right answer.",
|
| 713 |
+
system_message="",
|
| 714 |
+
roles=("<|User|>", "<|Assistant|>"),
|
| 715 |
+
messages=(),
|
| 716 |
+
offset=0,
|
| 717 |
+
sep_style=SeparatorStyle.DeepSeekVL2,
|
| 718 |
+
sep="\n\n",
|
| 719 |
+
sep2="<|end▁of▁sentence|>",
|
| 720 |
+
stop_str=["User:", "<|end▁of▁sentence|>"],
|
| 721 |
+
)
|
| 722 |
+
)
|
| 723 |
+
|
| 724 |
+
# Reference: https://huggingface.co/google/gemma-3-4b-it/blob/main/config.json
|
| 725 |
+
register_conv_template(
|
| 726 |
+
Conversation(
|
| 727 |
+
name="gemma-it",
|
| 728 |
+
system_message="You are a helpful assistant.",
|
| 729 |
+
system_template="<start_of_turn>user{system_message}\n\n",
|
| 730 |
+
roles=("<start_of_turn>user\n", "<start_of_turn>model\n"),
|
| 731 |
+
sep="<end_of_turn>\n",
|
| 732 |
+
sep_style=SeparatorStyle.GEMMA3,
|
| 733 |
+
stop_str=["<end_of_turn>"],
|
| 734 |
+
image_token="<start_of_image>",
|
| 735 |
+
)
|
| 736 |
+
)
|
| 737 |
+
|
| 738 |
+
# Reference: https://huggingface.co/Alibaba-NLP/gme-Qwen2-VL-2B-Instruct#usage
|
| 739 |
+
register_conv_template(
|
| 740 |
+
Conversation(
|
| 741 |
+
name="gme-qwen2-vl",
|
| 742 |
+
system_message="You are a helpful assistant.",
|
| 743 |
+
system_template="<|im_start|>system\n{system_message}",
|
| 744 |
+
roles=("<|im_start|>user", "<|im_start|>assistant"),
|
| 745 |
+
sep="<|im_end|>\n",
|
| 746 |
+
sep_style=SeparatorStyle.QWEN2_VL_EMBED,
|
| 747 |
+
stop_str="<|endoftext|>",
|
| 748 |
+
image_token="<|vision_start|><|image_pad|><|vision_end|>",
|
| 749 |
+
)
|
| 750 |
+
)
|
| 751 |
+
|
| 752 |
+
# Reference: https://huggingface.co/openbmb/MiniCPM-V-2_6#usage
|
| 753 |
+
register_conv_template(
|
| 754 |
+
Conversation(
|
| 755 |
+
name="minicpmv",
|
| 756 |
+
system_message="You are a helpful assistant",
|
| 757 |
+
system_template="<|im_start|>system\n{system_message}.",
|
| 758 |
+
roles=("<|im_start|>user", "<|im_start|>assistant"),
|
| 759 |
+
sep="<|im_end|>\n",
|
| 760 |
+
sep_style=SeparatorStyle.ADD_NEW_LINE_SINGLE,
|
| 761 |
+
stop_str=("<|im_end|>", "<|endoftext|>"),
|
| 762 |
+
image_token="(<image>./</image>)",
|
| 763 |
+
)
|
| 764 |
+
)
|
| 765 |
+
|
| 766 |
+
# Reference: https://github.com/deepseek-ai/Janus?tab=readme-ov-file#janus-pro
|
| 767 |
+
register_conv_template(
|
| 768 |
+
Conversation(
|
| 769 |
+
name="janus-pro",
|
| 770 |
+
system_message="You are a helpful language and vision assistant. You are able to understand the visual content that the user provides, and assist the user with a variety of tasks using natural language",
|
| 771 |
+
system_template="{system_message}.",
|
| 772 |
+
roles=("User", "Assistant"),
|
| 773 |
+
sep="\n\n",
|
| 774 |
+
sep2="<|end▁of▁sentence|>",
|
| 775 |
+
sep_style=SeparatorStyle.ADD_COLON_TWO,
|
| 776 |
+
stop_str=["<|User|>", "<|end▁of▁sentence|>"],
|
| 777 |
+
image_token="<image_placeholder>",
|
| 778 |
+
)
|
| 779 |
+
)
|
| 780 |
+
|
| 781 |
+
# Reference: https://huggingface.co/openbmb/MiniCPM-o-2_6#usage
|
| 782 |
+
register_conv_template(
|
| 783 |
+
Conversation(
|
| 784 |
+
name="minicpmo",
|
| 785 |
+
system_message="You are Qwen, created by Alibaba Cloud. You are a helpful assistant.",
|
| 786 |
+
system_template="<|im_start|>system\n{system_message}",
|
| 787 |
+
roles=("<|im_start|>user", "<|im_start|>assistant"),
|
| 788 |
+
sep="<|im_end|>\n",
|
| 789 |
+
sep_style=SeparatorStyle.ADD_NEW_LINE_SINGLE,
|
| 790 |
+
stop_str=("<|im_end|>", "<|endoftext|>"),
|
| 791 |
+
image_token="(<image>./</image>)",
|
| 792 |
+
audio_token="(<audio>./</audio>)",
|
| 793 |
+
)
|
| 794 |
+
)
|
lib/python3.12/site-packages/sglang/srt/custom_op.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Optional
|
| 2 |
+
|
| 3 |
+
import torch
|
| 4 |
+
from torch import nn
|
| 5 |
+
|
| 6 |
+
from sglang.srt.utils import is_cuda, is_hip
|
| 7 |
+
|
| 8 |
+
_is_cuda = is_cuda()
|
| 9 |
+
_is_hip = is_hip()
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class CustomOp(nn.Module):
|
| 13 |
+
def __init__(self):
|
| 14 |
+
super().__init__()
|
| 15 |
+
self._forward_method = self.dispatch_forward()
|
| 16 |
+
|
| 17 |
+
def forward(self, *args, **kwargs):
|
| 18 |
+
return self._forward_method(*args, **kwargs)
|
| 19 |
+
|
| 20 |
+
def forward_native(self, *args, **kwargs):
|
| 21 |
+
raise NotImplementedError
|
| 22 |
+
|
| 23 |
+
def forward_cuda(self, *args, **kwargs):
|
| 24 |
+
raise NotImplementedError
|
| 25 |
+
|
| 26 |
+
def forward_hip(self, *args, **kwargs):
|
| 27 |
+
return self.forward_cuda(*args, **kwargs)
|
| 28 |
+
|
| 29 |
+
def forward_xpu(self, *args, **kwargs):
|
| 30 |
+
return self.forward_native(*args, **kwargs)
|
| 31 |
+
|
| 32 |
+
def forward_hpu(self, *args, **kwargs):
|
| 33 |
+
return self.forward_native(*args, **kwargs)
|
| 34 |
+
|
| 35 |
+
def forward_cpu(self, *args, **kwargs):
|
| 36 |
+
return self.forward_native(*args, **kwargs)
|
| 37 |
+
|
| 38 |
+
def dispatch_forward(self):
|
| 39 |
+
if _is_cuda:
|
| 40 |
+
return self.forward_cuda
|
| 41 |
+
elif _is_hip:
|
| 42 |
+
return self.forward_hip
|
| 43 |
+
else:
|
| 44 |
+
return self.forward_native
|
lib/python3.12/site-packages/sglang/srt/disaggregation/decode.py
ADDED
|
@@ -0,0 +1,661 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Life cycle of a request in the decode server
|
| 3 |
+
|
| 4 |
+
1. PreallocQueue:
|
| 5 |
+
a. Initialize a receiver for each request
|
| 6 |
+
b. The request handshakes first, and pre-allocate kv once there is available kv.
|
| 7 |
+
c. Move the request to TransferQueue.
|
| 8 |
+
|
| 9 |
+
2. TransferQueue:
|
| 10 |
+
a. Poll the receiver to check the transfer state
|
| 11 |
+
b. If the transfer has finished, move the request to waiting queue
|
| 12 |
+
|
| 13 |
+
3. WaitingQueue:
|
| 14 |
+
a. Use the requests in the queue to construct a PrebuiltExtendBatch
|
| 15 |
+
b. Skip the prefill forward but only populate metadata
|
| 16 |
+
|
| 17 |
+
4. RunningBatch:
|
| 18 |
+
a. Merge the resolved PrebuiltExtendBatch into running batch to run decoding
|
| 19 |
+
"""
|
| 20 |
+
|
| 21 |
+
from __future__ import annotations
|
| 22 |
+
|
| 23 |
+
import logging
|
| 24 |
+
from collections import deque
|
| 25 |
+
from dataclasses import dataclass
|
| 26 |
+
from typing import TYPE_CHECKING, List, Optional, Tuple
|
| 27 |
+
|
| 28 |
+
import numpy as np
|
| 29 |
+
import torch
|
| 30 |
+
from torch.distributed import ProcessGroup
|
| 31 |
+
|
| 32 |
+
from sglang.srt.disaggregation.base import BaseKVManager, BaseKVReceiver, KVArgs, KVPoll
|
| 33 |
+
from sglang.srt.disaggregation.utils import (
|
| 34 |
+
DisaggregationMode,
|
| 35 |
+
FakeBootstrapHost,
|
| 36 |
+
KVClassType,
|
| 37 |
+
ReqToMetadataIdxAllocator,
|
| 38 |
+
TransferBackend,
|
| 39 |
+
get_kv_class,
|
| 40 |
+
kv_to_page_indices,
|
| 41 |
+
poll_and_all_reduce,
|
| 42 |
+
)
|
| 43 |
+
from sglang.srt.mem_cache.base_prefix_cache import BasePrefixCache
|
| 44 |
+
from sglang.srt.mem_cache.memory_pool import ReqToTokenPool, TokenToKVPoolAllocator
|
| 45 |
+
from sglang.srt.model_executor.forward_batch_info import ForwardMode
|
| 46 |
+
from sglang.srt.sampling.sampling_batch_info import SamplingBatchInfo
|
| 47 |
+
|
| 48 |
+
logger = logging.getLogger(__name__)
|
| 49 |
+
|
| 50 |
+
if TYPE_CHECKING:
|
| 51 |
+
from sglang.srt.configs.model_config import ModelConfig
|
| 52 |
+
from sglang.srt.managers.schedule_batch import Req, ScheduleBatch
|
| 53 |
+
from sglang.srt.managers.scheduler import Scheduler
|
| 54 |
+
from sglang.srt.server_args import ServerArgs
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
@dataclass
|
| 58 |
+
class DecodeRequest:
|
| 59 |
+
req: Req
|
| 60 |
+
kv_receiver: BaseKVReceiver
|
| 61 |
+
waiting_for_input: bool = False
|
| 62 |
+
metadata_buffer_index: int = -1
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
class DecodePreallocQueue:
|
| 66 |
+
"""
|
| 67 |
+
Store the requests that are preallocating.
|
| 68 |
+
"""
|
| 69 |
+
|
| 70 |
+
def __init__(
|
| 71 |
+
self,
|
| 72 |
+
req_to_token_pool: ReqToTokenPool,
|
| 73 |
+
token_to_kv_pool_allocator: TokenToKVPoolAllocator,
|
| 74 |
+
req_to_metadata_buffer_idx_allocator: ReqToMetadataIdxAllocator,
|
| 75 |
+
metadata_buffers: List[torch.Tensor],
|
| 76 |
+
aux_dtype: torch.dtype,
|
| 77 |
+
scheduler: Scheduler,
|
| 78 |
+
transfer_queue: DecodeTransferQueue,
|
| 79 |
+
tree_cache: BasePrefixCache,
|
| 80 |
+
gloo_group: ProcessGroup,
|
| 81 |
+
tp_rank: int,
|
| 82 |
+
tp_size: int,
|
| 83 |
+
bootstrap_port: int,
|
| 84 |
+
transfer_backend: TransferBackend,
|
| 85 |
+
):
|
| 86 |
+
self.req_to_token_pool = req_to_token_pool
|
| 87 |
+
self.token_to_kv_pool_allocator = token_to_kv_pool_allocator
|
| 88 |
+
self.token_to_kv_pool = token_to_kv_pool_allocator.get_kvcache()
|
| 89 |
+
self.aux_dtype = aux_dtype
|
| 90 |
+
self.metadata_buffers = metadata_buffers
|
| 91 |
+
self.req_to_metadata_buffer_idx_allocator = req_to_metadata_buffer_idx_allocator
|
| 92 |
+
self.scheduler = scheduler
|
| 93 |
+
self.transfer_queue = transfer_queue
|
| 94 |
+
self.tree_cache = tree_cache # this is always a chunk cache
|
| 95 |
+
self.gloo_group = gloo_group
|
| 96 |
+
self.tp_rank = tp_rank
|
| 97 |
+
self.tp_size = tp_size
|
| 98 |
+
self.bootstrap_port = bootstrap_port
|
| 99 |
+
|
| 100 |
+
self.num_reserved_decode_tokens = 512
|
| 101 |
+
|
| 102 |
+
# Queue for requests pending pre-allocation
|
| 103 |
+
self.queue: List[DecodeRequest] = []
|
| 104 |
+
self.transfer_backend = transfer_backend
|
| 105 |
+
self.kv_manager = self._init_kv_manager()
|
| 106 |
+
|
| 107 |
+
def _init_kv_manager(self) -> BaseKVManager:
|
| 108 |
+
kv_args = KVArgs()
|
| 109 |
+
kv_args.engine_rank = self.tp_rank
|
| 110 |
+
kv_data_ptrs, kv_data_lens, kv_item_lens = (
|
| 111 |
+
self.token_to_kv_pool.get_contiguous_buf_infos()
|
| 112 |
+
)
|
| 113 |
+
|
| 114 |
+
kv_args.kv_data_ptrs = kv_data_ptrs
|
| 115 |
+
kv_args.kv_data_lens = kv_data_lens
|
| 116 |
+
kv_args.kv_item_lens = kv_item_lens
|
| 117 |
+
|
| 118 |
+
kv_args.aux_data_ptrs = [
|
| 119 |
+
output_id_tensor.data_ptr() for output_id_tensor in self.metadata_buffers
|
| 120 |
+
]
|
| 121 |
+
kv_args.aux_data_lens = [
|
| 122 |
+
metadata_buffer.nbytes for metadata_buffer in self.metadata_buffers
|
| 123 |
+
]
|
| 124 |
+
kv_args.aux_item_lens = [
|
| 125 |
+
metadata_buffer[0].nbytes for metadata_buffer in self.metadata_buffers
|
| 126 |
+
]
|
| 127 |
+
kv_args.ib_device = self.scheduler.server_args.disaggregation_ib_device
|
| 128 |
+
kv_args.gpu_id = self.scheduler.gpu_id
|
| 129 |
+
kv_manager_class = get_kv_class(self.transfer_backend, KVClassType.MANAGER)
|
| 130 |
+
kv_manager = kv_manager_class(
|
| 131 |
+
kv_args, DisaggregationMode.DECODE, self.scheduler.server_args
|
| 132 |
+
)
|
| 133 |
+
return kv_manager
|
| 134 |
+
|
| 135 |
+
def add(self, req: Req) -> None:
|
| 136 |
+
"""Add a request to the pending queue."""
|
| 137 |
+
if req.bootstrap_host == FakeBootstrapHost:
|
| 138 |
+
# Fake transfer for warmup reqs
|
| 139 |
+
kv_receiver_class = get_kv_class(TransferBackend.FAKE, KVClassType.RECEIVER)
|
| 140 |
+
else:
|
| 141 |
+
kv_receiver_class = get_kv_class(
|
| 142 |
+
self.transfer_backend, KVClassType.RECEIVER
|
| 143 |
+
)
|
| 144 |
+
kv_receiver = kv_receiver_class(
|
| 145 |
+
mgr=self.kv_manager,
|
| 146 |
+
bootstrap_addr=f"{req.bootstrap_host}:{req.bootstrap_port}",
|
| 147 |
+
bootstrap_room=req.bootstrap_room,
|
| 148 |
+
)
|
| 149 |
+
self.queue.append(DecodeRequest(req=req, kv_receiver=kv_receiver))
|
| 150 |
+
|
| 151 |
+
def extend(self, reqs: List[Req]) -> None:
|
| 152 |
+
"""Add a request to the pending queue."""
|
| 153 |
+
for req in reqs:
|
| 154 |
+
self.add(req)
|
| 155 |
+
|
| 156 |
+
def _update_handshake_waiters(self) -> None:
|
| 157 |
+
if not self.queue:
|
| 158 |
+
return
|
| 159 |
+
|
| 160 |
+
if all(decode_req.waiting_for_input for decode_req in self.queue):
|
| 161 |
+
return
|
| 162 |
+
|
| 163 |
+
polls = poll_and_all_reduce(
|
| 164 |
+
[decode_req.kv_receiver for decode_req in self.queue], self.gloo_group
|
| 165 |
+
)
|
| 166 |
+
|
| 167 |
+
for i, (decode_req, poll) in enumerate(zip(self.queue, polls)):
|
| 168 |
+
if poll == KVPoll.Bootstrapping:
|
| 169 |
+
pass
|
| 170 |
+
elif poll == KVPoll.WaitingForInput:
|
| 171 |
+
decode_req.waiting_for_input = True
|
| 172 |
+
elif poll == KVPoll.Failed:
|
| 173 |
+
raise Exception("Handshake failed")
|
| 174 |
+
|
| 175 |
+
def pop_preallocated(self) -> List[DecodeRequest]:
|
| 176 |
+
"""Pop the preallocated requests from the pending queue (FIFO)."""
|
| 177 |
+
self._update_handshake_waiters()
|
| 178 |
+
|
| 179 |
+
preallocated_reqs = []
|
| 180 |
+
indices_to_remove = set()
|
| 181 |
+
allocatable_tokens = self._allocatable_tokens()
|
| 182 |
+
|
| 183 |
+
for i, decode_req in enumerate(self.queue):
|
| 184 |
+
if not decode_req.waiting_for_input:
|
| 185 |
+
continue
|
| 186 |
+
|
| 187 |
+
if self.req_to_token_pool.available_size() <= 0:
|
| 188 |
+
break
|
| 189 |
+
|
| 190 |
+
if self.req_to_metadata_buffer_idx_allocator.available_size() <= 0:
|
| 191 |
+
break
|
| 192 |
+
|
| 193 |
+
required_tokens_for_request = (
|
| 194 |
+
len(decode_req.req.origin_input_ids) + self.num_reserved_decode_tokens
|
| 195 |
+
)
|
| 196 |
+
|
| 197 |
+
if required_tokens_for_request > allocatable_tokens:
|
| 198 |
+
break
|
| 199 |
+
|
| 200 |
+
allocatable_tokens -= required_tokens_for_request
|
| 201 |
+
self._pre_alloc(decode_req.req)
|
| 202 |
+
|
| 203 |
+
kv_indices = (
|
| 204 |
+
self.req_to_token_pool.req_to_token[decode_req.req.req_pool_idx][
|
| 205 |
+
: len(decode_req.req.origin_input_ids)
|
| 206 |
+
]
|
| 207 |
+
.cpu()
|
| 208 |
+
.numpy()
|
| 209 |
+
.astype(np.int64)
|
| 210 |
+
)
|
| 211 |
+
|
| 212 |
+
decode_req.metadata_buffer_index = (
|
| 213 |
+
self.req_to_metadata_buffer_idx_allocator.alloc()
|
| 214 |
+
)
|
| 215 |
+
assert decode_req.metadata_buffer_index is not None
|
| 216 |
+
page_indices = kv_to_page_indices(
|
| 217 |
+
kv_indices, self.token_to_kv_pool_allocator.page_size
|
| 218 |
+
)
|
| 219 |
+
decode_req.kv_receiver.init(page_indices, decode_req.metadata_buffer_index)
|
| 220 |
+
preallocated_reqs.append(decode_req)
|
| 221 |
+
indices_to_remove.add(i)
|
| 222 |
+
|
| 223 |
+
self.queue = [
|
| 224 |
+
entry for i, entry in enumerate(self.queue) if i not in indices_to_remove
|
| 225 |
+
]
|
| 226 |
+
|
| 227 |
+
return preallocated_reqs
|
| 228 |
+
|
| 229 |
+
def _allocatable_tokens(self) -> int:
|
| 230 |
+
allocatable_tokens = (
|
| 231 |
+
self.token_to_kv_pool_allocator.available_size()
|
| 232 |
+
- self.num_reserved_decode_tokens
|
| 233 |
+
* (
|
| 234 |
+
len(self.scheduler.running_batch.reqs)
|
| 235 |
+
+ len(self.transfer_queue.queue)
|
| 236 |
+
+ len(self.scheduler.waiting_queue)
|
| 237 |
+
)
|
| 238 |
+
)
|
| 239 |
+
|
| 240 |
+
# Note: if the last fake extend just finishes, and we enter `pop_preallocated` immediately in the next iteration
|
| 241 |
+
# the extend batch is not in any queue, so we need to explicitly add the tokens slots here
|
| 242 |
+
if (
|
| 243 |
+
self.scheduler.last_batch
|
| 244 |
+
and self.scheduler.last_batch.forward_mode.is_extend()
|
| 245 |
+
):
|
| 246 |
+
allocatable_tokens -= self.num_reserved_decode_tokens * len(
|
| 247 |
+
self.scheduler.last_batch.reqs
|
| 248 |
+
)
|
| 249 |
+
|
| 250 |
+
return allocatable_tokens
|
| 251 |
+
|
| 252 |
+
def _pre_alloc(self, req: Req) -> torch.Tensor:
|
| 253 |
+
"""Pre-allocate the memory for req_to_token and token_kv_pool"""
|
| 254 |
+
req_pool_indices = self.req_to_token_pool.alloc(1)
|
| 255 |
+
|
| 256 |
+
assert req_pool_indices is not None
|
| 257 |
+
|
| 258 |
+
req.req_pool_idx = req_pool_indices[0]
|
| 259 |
+
if self.token_to_kv_pool_allocator.page_size == 1:
|
| 260 |
+
kv_loc = self.token_to_kv_pool_allocator.alloc(
|
| 261 |
+
len(req.origin_input_ids) + max(len(req.output_ids) - 1, 0)
|
| 262 |
+
)
|
| 263 |
+
else:
|
| 264 |
+
num_tokens = len(req.origin_input_ids) + max(len(req.output_ids) - 1, 0)
|
| 265 |
+
kv_loc = self.token_to_kv_pool_allocator.alloc_extend(
|
| 266 |
+
prefix_lens=torch.tensor(
|
| 267 |
+
[0],
|
| 268 |
+
dtype=torch.int64,
|
| 269 |
+
device=self.token_to_kv_pool_allocator.device,
|
| 270 |
+
),
|
| 271 |
+
seq_lens=torch.tensor(
|
| 272 |
+
[num_tokens],
|
| 273 |
+
dtype=torch.int64,
|
| 274 |
+
device=self.token_to_kv_pool_allocator.device,
|
| 275 |
+
),
|
| 276 |
+
last_loc=torch.tensor(
|
| 277 |
+
[-1],
|
| 278 |
+
dtype=torch.int64,
|
| 279 |
+
device=self.token_to_kv_pool_allocator.device,
|
| 280 |
+
),
|
| 281 |
+
extend_num_tokens=num_tokens,
|
| 282 |
+
)
|
| 283 |
+
assert kv_loc is not None
|
| 284 |
+
|
| 285 |
+
self.req_to_token_pool.write((req.req_pool_idx, slice(0, len(kv_loc))), kv_loc)
|
| 286 |
+
|
| 287 |
+
# populate metadata
|
| 288 |
+
req.fill_ids = req.origin_input_ids + req.output_ids
|
| 289 |
+
req.extend_input_len = len(req.origin_input_ids)
|
| 290 |
+
|
| 291 |
+
return kv_loc
|
| 292 |
+
|
| 293 |
+
|
| 294 |
+
class DecodeTransferQueue:
|
| 295 |
+
"""
|
| 296 |
+
Store the requests that is polling kv
|
| 297 |
+
"""
|
| 298 |
+
|
| 299 |
+
def __init__(
|
| 300 |
+
self,
|
| 301 |
+
gloo_group: ProcessGroup,
|
| 302 |
+
req_to_metadata_buffer_idx_allocator: ReqToMetadataIdxAllocator,
|
| 303 |
+
metadata_buffers: torch.Tensor,
|
| 304 |
+
):
|
| 305 |
+
self.queue: List[DecodeRequest] = []
|
| 306 |
+
self.gloo_group = gloo_group
|
| 307 |
+
self.req_to_metadata_buffer_idx_allocator = req_to_metadata_buffer_idx_allocator
|
| 308 |
+
self.metadata_buffers = metadata_buffers
|
| 309 |
+
|
| 310 |
+
def add(self, req_conn: DecodeRequest) -> None:
|
| 311 |
+
self.queue.append(req_conn)
|
| 312 |
+
|
| 313 |
+
def extend(self, req_conns) -> None:
|
| 314 |
+
self.queue.extend(req_conns)
|
| 315 |
+
|
| 316 |
+
def pop_transferred(self) -> List[DecodeRequest]:
|
| 317 |
+
if not self.queue:
|
| 318 |
+
return []
|
| 319 |
+
|
| 320 |
+
polls = poll_and_all_reduce(
|
| 321 |
+
[decode_req.kv_receiver for decode_req in self.queue], self.gloo_group
|
| 322 |
+
)
|
| 323 |
+
|
| 324 |
+
transferred_reqs = []
|
| 325 |
+
indices_to_remove = set()
|
| 326 |
+
for i, (decode_req, poll) in enumerate(zip(self.queue, polls)):
|
| 327 |
+
if poll == KVPoll.Failed:
|
| 328 |
+
raise Exception("Transfer failed")
|
| 329 |
+
elif poll == KVPoll.Success:
|
| 330 |
+
# pop and push it to waiting queue
|
| 331 |
+
idx = decode_req.metadata_buffer_index
|
| 332 |
+
assert len(decode_req.req.output_ids) == 0
|
| 333 |
+
output_id_buffer = self.metadata_buffers[0]
|
| 334 |
+
# the last dimension is padded by the same values.
|
| 335 |
+
output_id = output_id_buffer[idx][0].item()
|
| 336 |
+
assert len(decode_req.req.output_ids) == 0
|
| 337 |
+
assert decode_req.req.transferred_output_id is None
|
| 338 |
+
decode_req.req.transferred_output_id = output_id
|
| 339 |
+
transferred_reqs.append(decode_req)
|
| 340 |
+
indices_to_remove.add(i)
|
| 341 |
+
elif poll in [
|
| 342 |
+
KVPoll.Bootstrapping,
|
| 343 |
+
KVPoll.WaitingForInput,
|
| 344 |
+
KVPoll.Transferring,
|
| 345 |
+
]:
|
| 346 |
+
pass
|
| 347 |
+
else:
|
| 348 |
+
raise ValueError(f"Unexpected poll case: {poll}")
|
| 349 |
+
|
| 350 |
+
for i in indices_to_remove:
|
| 351 |
+
idx = self.queue[i].metadata_buffer_index
|
| 352 |
+
assert idx != -1
|
| 353 |
+
self.req_to_metadata_buffer_idx_allocator.free(idx)
|
| 354 |
+
|
| 355 |
+
self.queue = [
|
| 356 |
+
entry for i, entry in enumerate(self.queue) if i not in indices_to_remove
|
| 357 |
+
]
|
| 358 |
+
|
| 359 |
+
return transferred_reqs
|
| 360 |
+
|
| 361 |
+
|
| 362 |
+
class ScheduleBatchDisaggregationDecodeMixin:
|
| 363 |
+
|
| 364 |
+
def prepare_for_prebuilt_extend(self: ScheduleBatch):
|
| 365 |
+
"""
|
| 366 |
+
Prepare a prebuilt extend by populate metadata
|
| 367 |
+
Adapted from .prepare_for_extend().
|
| 368 |
+
"""
|
| 369 |
+
|
| 370 |
+
self.forward_mode = ForwardMode.EXTEND
|
| 371 |
+
reqs = self.reqs
|
| 372 |
+
input_ids = [r.fill_ids[len(r.prefix_indices) :] for r in reqs]
|
| 373 |
+
extend_num_tokens = sum(len(ids) for ids in input_ids)
|
| 374 |
+
seq_lens = []
|
| 375 |
+
pre_lens = []
|
| 376 |
+
req_pool_indices = []
|
| 377 |
+
|
| 378 |
+
# Pre-calculate total size
|
| 379 |
+
total_size = sum(req.extend_input_len for req in reqs)
|
| 380 |
+
out_cache_loc = torch.empty(total_size, dtype=torch.int64, device=self.device)
|
| 381 |
+
|
| 382 |
+
# Fill the tensor in one pass
|
| 383 |
+
offset = 0
|
| 384 |
+
for i, req in enumerate(reqs):
|
| 385 |
+
req_pool_indices.append(req.req_pool_idx)
|
| 386 |
+
|
| 387 |
+
chunk = self.req_to_token_pool.req_to_token[req.req_pool_idx][
|
| 388 |
+
: req.extend_input_len
|
| 389 |
+
]
|
| 390 |
+
assert (
|
| 391 |
+
offset + req.extend_input_len <= total_size
|
| 392 |
+
), f"Exceeds total size: offset={offset}, req.extend_input_len={req.extend_input_len}, total_size={total_size}"
|
| 393 |
+
out_cache_loc[offset : offset + req.extend_input_len] = chunk
|
| 394 |
+
offset += req.extend_input_len
|
| 395 |
+
|
| 396 |
+
pre_len = len(req.prefix_indices)
|
| 397 |
+
seq_len = len(req.origin_input_ids) + max(0, len(req.output_ids) - 1)
|
| 398 |
+
seq_lens.append(seq_len)
|
| 399 |
+
if len(req.output_ids) == 0:
|
| 400 |
+
assert (
|
| 401 |
+
seq_len - pre_len == req.extend_input_len
|
| 402 |
+
), f"seq_len={seq_len}, pre_len={pre_len}, req.extend_input_len={req.extend_input_len}"
|
| 403 |
+
|
| 404 |
+
req.cached_tokens += pre_len - req.already_computed
|
| 405 |
+
req.already_computed = seq_len
|
| 406 |
+
req.is_retracted = False
|
| 407 |
+
pre_lens.append(pre_len)
|
| 408 |
+
req.extend_logprob_start_len = 0
|
| 409 |
+
|
| 410 |
+
extend_input_logprob_token_ids = None
|
| 411 |
+
|
| 412 |
+
# Set fields
|
| 413 |
+
self.input_ids = torch.tensor(
|
| 414 |
+
sum(input_ids, []), dtype=torch.int32, device=self.device
|
| 415 |
+
)
|
| 416 |
+
self.req_pool_indices = torch.tensor(
|
| 417 |
+
req_pool_indices, dtype=torch.int64, device=self.device
|
| 418 |
+
)
|
| 419 |
+
self.seq_lens = torch.tensor(seq_lens, dtype=torch.int64, device=self.device)
|
| 420 |
+
self.out_cache_loc = out_cache_loc
|
| 421 |
+
self.seq_lens_sum = sum(seq_lens)
|
| 422 |
+
self.extend_num_tokens = extend_num_tokens
|
| 423 |
+
self.prefix_lens = [len(r.prefix_indices) for r in reqs]
|
| 424 |
+
self.extend_lens = [r.extend_input_len for r in reqs]
|
| 425 |
+
self.extend_logprob_start_lens = [r.extend_logprob_start_len for r in reqs]
|
| 426 |
+
self.extend_input_logprob_token_ids = extend_input_logprob_token_ids
|
| 427 |
+
|
| 428 |
+
# Build sampling info
|
| 429 |
+
self.sampling_info = SamplingBatchInfo.from_schedule_batch(
|
| 430 |
+
self,
|
| 431 |
+
self.model_config.vocab_size,
|
| 432 |
+
)
|
| 433 |
+
|
| 434 |
+
def process_prebuilt_extend(
|
| 435 |
+
self: ScheduleBatch, server_args: ServerArgs, model_config: ModelConfig
|
| 436 |
+
):
|
| 437 |
+
"""Assign the buffered last input id to schedule batch"""
|
| 438 |
+
self.output_ids = []
|
| 439 |
+
for req in self.reqs:
|
| 440 |
+
if req.output_ids and len(req.output_ids) > 0:
|
| 441 |
+
# resumed retracted req
|
| 442 |
+
self.output_ids.append(req.output_ids[-1])
|
| 443 |
+
else:
|
| 444 |
+
assert req.transferred_output_id is not None
|
| 445 |
+
req.output_ids.append(req.transferred_output_id)
|
| 446 |
+
self.output_ids.append(req.transferred_output_id)
|
| 447 |
+
self.tree_cache.cache_unfinished_req(req)
|
| 448 |
+
self.output_ids = torch.tensor(self.output_ids, device=self.device)
|
| 449 |
+
|
| 450 |
+
|
| 451 |
+
class SchedulerDisaggregationDecodeMixin:
|
| 452 |
+
|
| 453 |
+
def _prepare_idle_batch_and_run(self, batch, delay_process=False):
|
| 454 |
+
batch, _ = self.prepare_dp_attn_batch(batch)
|
| 455 |
+
result = None
|
| 456 |
+
if batch:
|
| 457 |
+
result = self.run_batch(batch)
|
| 458 |
+
if not delay_process:
|
| 459 |
+
self.process_batch_result(batch, result)
|
| 460 |
+
return batch, result
|
| 461 |
+
|
| 462 |
+
@torch.no_grad()
|
| 463 |
+
def event_loop_normal_disagg_decode(self: Scheduler):
|
| 464 |
+
"""A normal scheduler loop for decode worker in disaggregation mode."""
|
| 465 |
+
|
| 466 |
+
while True:
|
| 467 |
+
recv_reqs = self.recv_requests()
|
| 468 |
+
self.process_input_requests(recv_reqs)
|
| 469 |
+
# polling and allocating kv cache
|
| 470 |
+
self.process_decode_queue()
|
| 471 |
+
batch = self.get_next_disagg_decode_batch_to_run()
|
| 472 |
+
self.cur_batch = batch
|
| 473 |
+
|
| 474 |
+
prepare_dp_attn_flag = (
|
| 475 |
+
self.server_args.enable_dp_attention
|
| 476 |
+
or self.server_args.enable_sp_layernorm
|
| 477 |
+
)
|
| 478 |
+
|
| 479 |
+
if batch:
|
| 480 |
+
# Generate fake extend output.
|
| 481 |
+
if batch.forward_mode.is_extend():
|
| 482 |
+
# Note: Logprobs should be handled on the prefill engine.
|
| 483 |
+
self.stream_output(batch.reqs, False)
|
| 484 |
+
if prepare_dp_attn_flag:
|
| 485 |
+
self._prepare_idle_batch_and_run(None)
|
| 486 |
+
else:
|
| 487 |
+
if prepare_dp_attn_flag:
|
| 488 |
+
self.prepare_dp_attn_batch(batch)
|
| 489 |
+
result = self.run_batch(batch)
|
| 490 |
+
self.process_batch_result(batch, result)
|
| 491 |
+
elif prepare_dp_attn_flag:
|
| 492 |
+
batch, _ = self._prepare_idle_batch_and_run(None)
|
| 493 |
+
|
| 494 |
+
if batch is None and (
|
| 495 |
+
len(self.disagg_decode_transfer_queue.queue)
|
| 496 |
+
+ len(self.disagg_decode_prealloc_queue.queue)
|
| 497 |
+
== 0
|
| 498 |
+
):
|
| 499 |
+
# When the server is idle, do self-check and re-init some states
|
| 500 |
+
self.check_memory()
|
| 501 |
+
self.new_token_ratio = self.init_new_token_ratio
|
| 502 |
+
|
| 503 |
+
self.last_batch = batch
|
| 504 |
+
|
| 505 |
+
@torch.no_grad()
|
| 506 |
+
def event_loop_overlap_disagg_decode(self: Scheduler):
|
| 507 |
+
result_queue = deque()
|
| 508 |
+
self.last_batch: Optional[ScheduleBatch] = None
|
| 509 |
+
self.last_batch_in_queue = False # last batch is modifed in-place, so we need another variable to track if it's extend
|
| 510 |
+
|
| 511 |
+
while True:
|
| 512 |
+
recv_reqs = self.recv_requests()
|
| 513 |
+
self.process_input_requests(recv_reqs)
|
| 514 |
+
# polling and allocating kv cache
|
| 515 |
+
self.process_decode_queue()
|
| 516 |
+
batch = self.get_next_disagg_decode_batch_to_run()
|
| 517 |
+
self.cur_batch = batch
|
| 518 |
+
last_batch_in_queue = False
|
| 519 |
+
|
| 520 |
+
prepare_dp_attn_flag = (
|
| 521 |
+
self.server_args.enable_dp_attention
|
| 522 |
+
or self.server_args.enable_sp_layernorm
|
| 523 |
+
)
|
| 524 |
+
|
| 525 |
+
if batch:
|
| 526 |
+
# Generate fake extend output.
|
| 527 |
+
if batch.forward_mode.is_extend():
|
| 528 |
+
# Note: Logprobs should be handled on the prefill engine.
|
| 529 |
+
self.stream_output(batch.reqs, False)
|
| 530 |
+
if prepare_dp_attn_flag:
|
| 531 |
+
batch_, result = self._prepare_idle_batch_and_run(
|
| 532 |
+
None, delay_process=True
|
| 533 |
+
)
|
| 534 |
+
if batch_:
|
| 535 |
+
result_queue.append((batch_.copy(), result))
|
| 536 |
+
last_batch_in_queue = True
|
| 537 |
+
else:
|
| 538 |
+
if prepare_dp_attn_flag:
|
| 539 |
+
self.prepare_dp_attn_batch(batch)
|
| 540 |
+
result = self.run_batch(batch)
|
| 541 |
+
result_queue.append((batch.copy(), result))
|
| 542 |
+
last_batch_in_queue = True
|
| 543 |
+
elif prepare_dp_attn_flag:
|
| 544 |
+
batch, result = self._prepare_idle_batch_and_run(
|
| 545 |
+
None, delay_process=True
|
| 546 |
+
)
|
| 547 |
+
if batch:
|
| 548 |
+
result_queue.append((batch.copy(), result))
|
| 549 |
+
last_batch_in_queue = True
|
| 550 |
+
|
| 551 |
+
# Process the results of the previous batch but skip if the last batch is extend
|
| 552 |
+
if self.last_batch and self.last_batch_in_queue:
|
| 553 |
+
tmp_batch, tmp_result = result_queue.popleft()
|
| 554 |
+
self.process_batch_result(tmp_batch, tmp_result)
|
| 555 |
+
|
| 556 |
+
if batch is None and (
|
| 557 |
+
len(self.disagg_decode_transfer_queue.queue)
|
| 558 |
+
+ len(self.disagg_decode_prealloc_queue.queue)
|
| 559 |
+
== 0
|
| 560 |
+
):
|
| 561 |
+
# When the server is idle, do self-check and re-init some states
|
| 562 |
+
self.check_memory()
|
| 563 |
+
self.new_token_ratio = self.init_new_token_ratio
|
| 564 |
+
|
| 565 |
+
self.last_batch = batch
|
| 566 |
+
self.last_batch_in_queue = last_batch_in_queue
|
| 567 |
+
|
| 568 |
+
def get_next_disagg_decode_batch_to_run(
|
| 569 |
+
self: Scheduler,
|
| 570 |
+
) -> Optional[Tuple[ScheduleBatch, bool]]:
|
| 571 |
+
"""Create fake completed prefill if possible and merge with running batch"""
|
| 572 |
+
# Merge the prefill batch into the running batch
|
| 573 |
+
last_batch = self.last_batch
|
| 574 |
+
if last_batch and last_batch.forward_mode.is_extend():
|
| 575 |
+
# chunked prefill doesn't happen in decode instance.
|
| 576 |
+
assert self.chunked_req is None
|
| 577 |
+
# Filter finished batches.
|
| 578 |
+
last_batch.filter_batch()
|
| 579 |
+
if not last_batch.is_empty():
|
| 580 |
+
if self.running_batch.is_empty():
|
| 581 |
+
self.running_batch = last_batch
|
| 582 |
+
else:
|
| 583 |
+
# merge running_batch with prefill batch
|
| 584 |
+
self.running_batch.merge_batch(last_batch)
|
| 585 |
+
|
| 586 |
+
new_prebuilt_batch = self.get_new_prebuilt_batch()
|
| 587 |
+
|
| 588 |
+
ret: Optional[ScheduleBatch] = None
|
| 589 |
+
if new_prebuilt_batch:
|
| 590 |
+
ret = new_prebuilt_batch
|
| 591 |
+
else:
|
| 592 |
+
if self.running_batch.is_empty():
|
| 593 |
+
ret = None
|
| 594 |
+
else:
|
| 595 |
+
self.running_batch = self.update_running_batch(self.running_batch)
|
| 596 |
+
ret = self.running_batch if not self.running_batch.is_empty() else None
|
| 597 |
+
|
| 598 |
+
return ret
|
| 599 |
+
|
| 600 |
+
def get_new_prebuilt_batch(self: Scheduler) -> Optional[ScheduleBatch]:
|
| 601 |
+
"""Create a schedulebatch for fake completed prefill"""
|
| 602 |
+
if len(self.waiting_queue) == 0:
|
| 603 |
+
return None
|
| 604 |
+
|
| 605 |
+
curr_batch_size = self.running_batch.batch_size()
|
| 606 |
+
|
| 607 |
+
batch_size = min(self.req_to_token_pool.size, self.max_running_requests)
|
| 608 |
+
|
| 609 |
+
num_not_used_batch = batch_size - curr_batch_size
|
| 610 |
+
|
| 611 |
+
# pop req from waiting queue
|
| 612 |
+
can_run_list: List[Req] = []
|
| 613 |
+
waiting_queue: List[Req] = []
|
| 614 |
+
|
| 615 |
+
for i in range(len(self.waiting_queue)):
|
| 616 |
+
req = self.waiting_queue[i]
|
| 617 |
+
# we can only add at least `num_not_used_batch` new batch to the running queue
|
| 618 |
+
if i < num_not_used_batch:
|
| 619 |
+
can_run_list.append(req)
|
| 620 |
+
req.init_next_round_input(self.tree_cache)
|
| 621 |
+
else:
|
| 622 |
+
waiting_queue.append(req)
|
| 623 |
+
|
| 624 |
+
self.waiting_queue = waiting_queue
|
| 625 |
+
if len(can_run_list) == 0:
|
| 626 |
+
return None
|
| 627 |
+
# local import to avoid circular import
|
| 628 |
+
from sglang.srt.managers.schedule_batch import ScheduleBatch
|
| 629 |
+
|
| 630 |
+
# construct a schedule batch with those requests and mark as decode
|
| 631 |
+
new_batch = ScheduleBatch.init_new(
|
| 632 |
+
can_run_list,
|
| 633 |
+
self.req_to_token_pool,
|
| 634 |
+
self.token_to_kv_pool_allocator,
|
| 635 |
+
self.tree_cache,
|
| 636 |
+
self.model_config,
|
| 637 |
+
self.enable_overlap,
|
| 638 |
+
self.spec_algorithm,
|
| 639 |
+
self.server_args.enable_custom_logit_processor,
|
| 640 |
+
)
|
| 641 |
+
|
| 642 |
+
# construct fake completed prefill
|
| 643 |
+
new_batch.prepare_for_prebuilt_extend()
|
| 644 |
+
new_batch.process_prebuilt_extend(self.server_args, self.model_config)
|
| 645 |
+
|
| 646 |
+
return new_batch
|
| 647 |
+
|
| 648 |
+
def process_decode_queue(self: Scheduler):
|
| 649 |
+
req_conns = self.disagg_decode_prealloc_queue.pop_preallocated()
|
| 650 |
+
|
| 651 |
+
def _num_pre_alloc(req):
|
| 652 |
+
return len(req.req.origin_input_ids) + max(len(req.req.output_ids) - 1, 0)
|
| 653 |
+
|
| 654 |
+
self.num_tokens_pre_allocated += sum(_num_pre_alloc(req) for req in req_conns)
|
| 655 |
+
self.disagg_decode_transfer_queue.extend(req_conns)
|
| 656 |
+
alloc_reqs = (
|
| 657 |
+
self.disagg_decode_transfer_queue.pop_transferred()
|
| 658 |
+
) # the requests which kv has arrived
|
| 659 |
+
self.num_tokens_pre_allocated -= sum(_num_pre_alloc(req) for req in alloc_reqs)
|
| 660 |
+
|
| 661 |
+
self.waiting_queue.extend([req.req for req in alloc_reqs])
|
lib/python3.12/site-packages/sglang/srt/disaggregation/fake/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
from .conn import FakeKVReceiver, FakeKVSender
|
lib/python3.12/site-packages/sglang/srt/disaggregation/fake/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (281 Bytes). View file
|
|
|
lib/python3.12/site-packages/sglang/srt/disaggregation/fake/__pycache__/conn.cpython-312.pyc
ADDED
|
Binary file (4.13 kB). View file
|
|
|
lib/python3.12/site-packages/sglang/srt/disaggregation/fake/conn.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
from typing import Dict, List, Optional, Tuple, Union
|
| 3 |
+
|
| 4 |
+
import numpy as np
|
| 5 |
+
import numpy.typing as npt
|
| 6 |
+
|
| 7 |
+
from sglang.srt.disaggregation.base.conn import (
|
| 8 |
+
BaseKVManager,
|
| 9 |
+
BaseKVReceiver,
|
| 10 |
+
BaseKVSender,
|
| 11 |
+
KVArgs,
|
| 12 |
+
KVPoll,
|
| 13 |
+
)
|
| 14 |
+
|
| 15 |
+
logger = logging.getLogger(__name__)
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
# For warmup reqs, we don't kv transfer, we use the fake sender and receiver
|
| 19 |
+
class FakeKVSender(BaseKVSender):
|
| 20 |
+
def __init__(self, mgr: BaseKVManager, bootstrap_addr: str, bootstrap_room: int):
|
| 21 |
+
self.has_sent = False
|
| 22 |
+
|
| 23 |
+
def poll(self) -> KVPoll:
|
| 24 |
+
if self.has_sent is False:
|
| 25 |
+
# Assume handshake completed instantly
|
| 26 |
+
return KVPoll.WaitingForInput
|
| 27 |
+
else:
|
| 28 |
+
# Assume transfer completed instantly
|
| 29 |
+
logger.info("FakeKVSender poll success")
|
| 30 |
+
return KVPoll.Success
|
| 31 |
+
|
| 32 |
+
def init(
|
| 33 |
+
self,
|
| 34 |
+
kv_indices: list[int],
|
| 35 |
+
aux_index: Optional[int] = None,
|
| 36 |
+
dest_ranks: Optional[list[int]] = None,
|
| 37 |
+
):
|
| 38 |
+
logger.info(
|
| 39 |
+
f"FakeKVSender init with kv_indices: {kv_indices}, aux_index: {aux_index}, dest_ranks: {dest_ranks}"
|
| 40 |
+
)
|
| 41 |
+
pass
|
| 42 |
+
|
| 43 |
+
def send(
|
| 44 |
+
self,
|
| 45 |
+
kv_indices: npt.NDArray[np.int64],
|
| 46 |
+
index_slice: slice,
|
| 47 |
+
is_last: bool,
|
| 48 |
+
):
|
| 49 |
+
logger.info(
|
| 50 |
+
f"FakeKVSender send with kv_indices: {kv_indices}, index_slice: {index_slice}, is_last: {is_last}"
|
| 51 |
+
)
|
| 52 |
+
if is_last:
|
| 53 |
+
self.has_sent = True
|
| 54 |
+
logger.info(f"FakeKVSender send success")
|
| 55 |
+
else:
|
| 56 |
+
self.has_sent = False
|
| 57 |
+
logger.info(f"FakeKVSender send fake transfering")
|
| 58 |
+
|
| 59 |
+
def failure_exception(self):
|
| 60 |
+
raise Exception("Fake KVSender Exception")
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
class FakeKVReceiver(BaseKVReceiver):
|
| 64 |
+
def __init__(
|
| 65 |
+
self,
|
| 66 |
+
mgr: BaseKVManager,
|
| 67 |
+
bootstrap_addr: str,
|
| 68 |
+
bootstrap_room: Optional[int] = None,
|
| 69 |
+
):
|
| 70 |
+
self.has_init = False
|
| 71 |
+
|
| 72 |
+
def poll(self) -> KVPoll:
|
| 73 |
+
if self.has_init is False:
|
| 74 |
+
# Assume handshake completed instantly
|
| 75 |
+
return KVPoll.WaitingForInput
|
| 76 |
+
else:
|
| 77 |
+
# Assume transfer completed instantly
|
| 78 |
+
logger.info("FakeKVReceiver poll success")
|
| 79 |
+
return KVPoll.Success
|
| 80 |
+
|
| 81 |
+
def init(self, kv_indices: list[int], aux_index: Optional[int] = None):
|
| 82 |
+
self.has_init = True
|
| 83 |
+
logger.info(
|
| 84 |
+
f"FakeKVReceiver init with kv_indices: {kv_indices}, aux_index: {aux_index}"
|
| 85 |
+
)
|
| 86 |
+
|
| 87 |
+
def failure_exception(self):
|
| 88 |
+
raise Exception("Fake KVReceiver Exception")
|
lib/python3.12/site-packages/sglang/srt/disaggregation/mini_lb.py
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Minimal HTTP load balancer for prefill and decode servers for testing.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import asyncio
|
| 6 |
+
import random
|
| 7 |
+
import urllib
|
| 8 |
+
from itertools import chain
|
| 9 |
+
from typing import List
|
| 10 |
+
|
| 11 |
+
import aiohttp
|
| 12 |
+
import orjson
|
| 13 |
+
import uvicorn
|
| 14 |
+
from fastapi import FastAPI, HTTPException
|
| 15 |
+
from fastapi.responses import ORJSONResponse, Response, StreamingResponse
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class PrefillConfig:
|
| 19 |
+
def __init__(self, url: str, bootstrap_port: int):
|
| 20 |
+
self.url = url
|
| 21 |
+
self.bootstrap_port = bootstrap_port
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
class MiniLoadBalancer:
|
| 25 |
+
def __init__(self, prefill_configs: List[PrefillConfig], decode_servers: List[str]):
|
| 26 |
+
self.prefill_configs = prefill_configs
|
| 27 |
+
self.prefill_servers = [p.url for p in prefill_configs]
|
| 28 |
+
self.decode_servers = decode_servers
|
| 29 |
+
|
| 30 |
+
def select_pair(self):
|
| 31 |
+
prefill_config = random.choice(self.prefill_configs)
|
| 32 |
+
decode_server = random.choice(self.decode_servers)
|
| 33 |
+
return prefill_config.url, prefill_config.bootstrap_port, decode_server
|
| 34 |
+
|
| 35 |
+
async def generate(
|
| 36 |
+
self, modified_request, prefill_server, decode_server, endpoint
|
| 37 |
+
) -> ORJSONResponse:
|
| 38 |
+
assert endpoint[0] != "/", f"Endpoint should not start with '/': {endpoint}"
|
| 39 |
+
|
| 40 |
+
async with aiohttp.ClientSession(
|
| 41 |
+
timeout=aiohttp.ClientTimeout(
|
| 42 |
+
total=3600
|
| 43 |
+
) # Add timeout for request reliability
|
| 44 |
+
) as session:
|
| 45 |
+
tasks = [
|
| 46 |
+
session.post(f"{prefill_server}/{endpoint}", json=modified_request),
|
| 47 |
+
session.post(f"{decode_server}/{endpoint}", json=modified_request),
|
| 48 |
+
]
|
| 49 |
+
# Wait for both responses to complete. Prefill should end first.
|
| 50 |
+
prefill_response, decode_response = await asyncio.gather(*tasks)
|
| 51 |
+
|
| 52 |
+
return ORJSONResponse(
|
| 53 |
+
content=await decode_response.json(),
|
| 54 |
+
status_code=decode_response.status,
|
| 55 |
+
)
|
| 56 |
+
|
| 57 |
+
async def generate_stream(
|
| 58 |
+
self, modified_request, prefill_server, decode_server, endpoint="generate"
|
| 59 |
+
):
|
| 60 |
+
assert endpoint[0] != "/", f"Endpoint should not start with '/': {endpoint}"
|
| 61 |
+
|
| 62 |
+
async def stream_results():
|
| 63 |
+
async with aiohttp.ClientSession(
|
| 64 |
+
timeout=aiohttp.ClientTimeout(
|
| 65 |
+
total=3600
|
| 66 |
+
) # Add timeout for request reliability
|
| 67 |
+
) as session:
|
| 68 |
+
try:
|
| 69 |
+
# Create the tasks for both prefill and decode requests
|
| 70 |
+
tasks = [
|
| 71 |
+
session.post(
|
| 72 |
+
f"{prefill_server}/{endpoint}", json=modified_request
|
| 73 |
+
),
|
| 74 |
+
session.post(
|
| 75 |
+
f"{decode_server}/{endpoint}", json=modified_request
|
| 76 |
+
),
|
| 77 |
+
]
|
| 78 |
+
# Wait for both responses to complete. Since this is streaming, they return immediately.
|
| 79 |
+
prefill_response, decode_response = await asyncio.gather(*tasks)
|
| 80 |
+
async for chunk in decode_response.content:
|
| 81 |
+
yield chunk
|
| 82 |
+
except Exception as e:
|
| 83 |
+
error_msg = {
|
| 84 |
+
"error": {"message": f"Stream processing error: {str(e)}"}
|
| 85 |
+
}
|
| 86 |
+
yield b"data: " + orjson.dumps(
|
| 87 |
+
error_msg, option=orjson.OPT_NON_STR_KEYS
|
| 88 |
+
) + b"\n\n"
|
| 89 |
+
finally:
|
| 90 |
+
if prefill_response is not None:
|
| 91 |
+
await prefill_response.release()
|
| 92 |
+
|
| 93 |
+
return StreamingResponse(
|
| 94 |
+
stream_results(),
|
| 95 |
+
media_type="text/event-stream",
|
| 96 |
+
)
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
app = FastAPI()
|
| 100 |
+
load_balancer = None
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
@app.get("/health")
|
| 104 |
+
async def health_check():
|
| 105 |
+
return Response(status_code=200)
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
@app.get("/health_generate")
|
| 109 |
+
async def health_check():
|
| 110 |
+
prefill_servers, decode_servers = (
|
| 111 |
+
load_balancer.prefill_servers,
|
| 112 |
+
load_balancer.decode_servers,
|
| 113 |
+
)
|
| 114 |
+
async with aiohttp.ClientSession() as session:
|
| 115 |
+
# Create the tasks
|
| 116 |
+
tasks = []
|
| 117 |
+
for server in chain(prefill_servers, decode_servers):
|
| 118 |
+
tasks.append(session.post(f"{server}/health_generate"))
|
| 119 |
+
for i, response in enumerate(asyncio.as_completed(tasks)):
|
| 120 |
+
await response
|
| 121 |
+
return Response(status_code=200)
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
@app.post("/flush_cache")
|
| 125 |
+
async def flush_cache():
|
| 126 |
+
prefill_servers, decode_servers = (
|
| 127 |
+
load_balancer.prefill_servers,
|
| 128 |
+
load_balancer.decode_servers,
|
| 129 |
+
)
|
| 130 |
+
async with aiohttp.ClientSession() as session:
|
| 131 |
+
# Create the tasks
|
| 132 |
+
tasks = []
|
| 133 |
+
for server in chain(prefill_servers, decode_servers):
|
| 134 |
+
tasks.append(session.post(f"{server}/flush_cache"))
|
| 135 |
+
for i, response in enumerate(asyncio.as_completed(tasks)):
|
| 136 |
+
await response
|
| 137 |
+
return Response(status_code=200)
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
@app.get("/get_server_info")
|
| 141 |
+
async def get_server_info():
|
| 142 |
+
prefill_servers, decode_servers = (
|
| 143 |
+
load_balancer.prefill_servers,
|
| 144 |
+
load_balancer.decode_servers,
|
| 145 |
+
)
|
| 146 |
+
prefill_infos = []
|
| 147 |
+
decode_infos = []
|
| 148 |
+
async with aiohttp.ClientSession() as session:
|
| 149 |
+
for server in chain(prefill_servers):
|
| 150 |
+
server_info = await session.get(f"{server}/get_server_info")
|
| 151 |
+
prefill_infos.append(await server_info.json())
|
| 152 |
+
for server in chain(decode_servers):
|
| 153 |
+
server_info = await session.get(f"{server}/get_server_info")
|
| 154 |
+
decode_infos.append(await server_info.json())
|
| 155 |
+
|
| 156 |
+
return {"prefill": prefill_infos, "decode": decode_infos}
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
@app.get("/get_model_info")
|
| 160 |
+
async def get_model_info():
|
| 161 |
+
# Dummy model information
|
| 162 |
+
model_info = {
|
| 163 |
+
"model_path": "/path/to/dummy/model",
|
| 164 |
+
"tokenizer_path": "/path/to/dummy/tokenizer",
|
| 165 |
+
"is_generation": True,
|
| 166 |
+
"preferred_sampling_params": {"temperature": 0.7, "max_new_tokens": 128},
|
| 167 |
+
}
|
| 168 |
+
return ORJSONResponse(content=model_info)
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
@app.post("/generate")
|
| 172 |
+
async def handle_generate_request(request_data: dict):
|
| 173 |
+
prefill_server, bootstrap_port, decode_server = load_balancer.select_pair()
|
| 174 |
+
|
| 175 |
+
# Parse and transform prefill_server for bootstrap data
|
| 176 |
+
parsed_url = urllib.parse.urlparse(prefill_server)
|
| 177 |
+
hostname = parsed_url.hostname
|
| 178 |
+
modified_request = request_data.copy()
|
| 179 |
+
|
| 180 |
+
batch_size = _get_request_batch_size(modified_request)
|
| 181 |
+
if batch_size is not None:
|
| 182 |
+
modified_request.update(
|
| 183 |
+
{
|
| 184 |
+
"bootstrap_host": [hostname] * batch_size,
|
| 185 |
+
"bootstrap_port": [bootstrap_port] * batch_size,
|
| 186 |
+
"bootstrap_room": [
|
| 187 |
+
_generate_bootstrap_room() for _ in range(batch_size)
|
| 188 |
+
],
|
| 189 |
+
}
|
| 190 |
+
)
|
| 191 |
+
else:
|
| 192 |
+
modified_request.update(
|
| 193 |
+
{
|
| 194 |
+
"bootstrap_host": hostname,
|
| 195 |
+
"bootstrap_port": bootstrap_port,
|
| 196 |
+
"bootstrap_room": _generate_bootstrap_room(),
|
| 197 |
+
}
|
| 198 |
+
)
|
| 199 |
+
|
| 200 |
+
if request_data.get("stream", False):
|
| 201 |
+
return await load_balancer.generate_stream(
|
| 202 |
+
modified_request, prefill_server, decode_server, "generate"
|
| 203 |
+
)
|
| 204 |
+
else:
|
| 205 |
+
return await load_balancer.generate(
|
| 206 |
+
modified_request, prefill_server, decode_server, "generate"
|
| 207 |
+
)
|
| 208 |
+
|
| 209 |
+
|
| 210 |
+
@app.post("/v1/chat/completions")
|
| 211 |
+
async def handle_completion_request(request_data: dict):
|
| 212 |
+
prefill_server, bootstrap_port, decode_server = load_balancer.select_pair()
|
| 213 |
+
|
| 214 |
+
# Parse and transform prefill_server for bootstrap data
|
| 215 |
+
parsed_url = urllib.parse.urlparse(prefill_server)
|
| 216 |
+
hostname = parsed_url.hostname
|
| 217 |
+
modified_request = request_data.copy()
|
| 218 |
+
modified_request.update(
|
| 219 |
+
{
|
| 220 |
+
"bootstrap_host": hostname,
|
| 221 |
+
"bootstrap_port": bootstrap_port,
|
| 222 |
+
"bootstrap_room": random.randint(0, 2**63 - 1),
|
| 223 |
+
}
|
| 224 |
+
)
|
| 225 |
+
|
| 226 |
+
if request_data.get("stream", False):
|
| 227 |
+
return await load_balancer.generate_stream(
|
| 228 |
+
modified_request,
|
| 229 |
+
prefill_server,
|
| 230 |
+
decode_server,
|
| 231 |
+
endpoint="v1/chat/completions",
|
| 232 |
+
)
|
| 233 |
+
else:
|
| 234 |
+
return await load_balancer.generate(
|
| 235 |
+
modified_request,
|
| 236 |
+
prefill_server,
|
| 237 |
+
decode_server,
|
| 238 |
+
endpoint="v1/chat/completions",
|
| 239 |
+
)
|
| 240 |
+
|
| 241 |
+
|
| 242 |
+
def _generate_bootstrap_room():
|
| 243 |
+
return random.randint(0, 2**63 - 1)
|
| 244 |
+
|
| 245 |
+
|
| 246 |
+
# We may utilize `GenerateReqInput`'s logic later
|
| 247 |
+
def _get_request_batch_size(request):
|
| 248 |
+
if (text := request.get("text")) is not None:
|
| 249 |
+
return None if isinstance(text, str) else len(text)
|
| 250 |
+
if (input_ids := request.get("input_ids")) is not None:
|
| 251 |
+
return None if isinstance(input_ids[0], int) else len(input_ids)
|
| 252 |
+
return None
|
| 253 |
+
|
| 254 |
+
|
| 255 |
+
@app.get("/v1/models")
|
| 256 |
+
async def get_models():
|
| 257 |
+
prefill_server = load_balancer.prefill_servers[0] # Get the first prefill server
|
| 258 |
+
async with aiohttp.ClientSession() as session:
|
| 259 |
+
try:
|
| 260 |
+
response = await session.get(f"{prefill_server}/v1/models")
|
| 261 |
+
if response.status != 200:
|
| 262 |
+
raise HTTPException(
|
| 263 |
+
status_code=response.status,
|
| 264 |
+
detail=f"Prefill server error: Status {response.status}",
|
| 265 |
+
)
|
| 266 |
+
return ORJSONResponse(content=await response.json())
|
| 267 |
+
except Exception as e:
|
| 268 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 269 |
+
|
| 270 |
+
|
| 271 |
+
def run(prefill_configs, decode_addrs, host, port):
|
| 272 |
+
global load_balancer
|
| 273 |
+
load_balancer = MiniLoadBalancer(prefill_configs, decode_addrs)
|
| 274 |
+
uvicorn.run(app, host=host, port=port)
|
| 275 |
+
|
| 276 |
+
|
| 277 |
+
if __name__ == "__main__":
|
| 278 |
+
import argparse
|
| 279 |
+
|
| 280 |
+
parser = argparse.ArgumentParser(description="Mini Load Balancer Server")
|
| 281 |
+
parser.add_argument(
|
| 282 |
+
"--prefill", required=True, help="Comma-separated URLs for prefill servers"
|
| 283 |
+
)
|
| 284 |
+
parser.add_argument(
|
| 285 |
+
"--prefill-bootstrap-ports",
|
| 286 |
+
help="Comma-separated bootstrap ports for prefill servers",
|
| 287 |
+
default="8998",
|
| 288 |
+
)
|
| 289 |
+
parser.add_argument(
|
| 290 |
+
"--decode", required=True, help="Comma-separated URLs for decode servers"
|
| 291 |
+
)
|
| 292 |
+
parser.add_argument(
|
| 293 |
+
"--host", default="0.0.0.0", help="Host to bind the server (default: 0.0.0.0)"
|
| 294 |
+
)
|
| 295 |
+
parser.add_argument(
|
| 296 |
+
"--port", type=int, default=8000, help="Port to bind the server (default: 8000)"
|
| 297 |
+
)
|
| 298 |
+
args = parser.parse_args()
|
| 299 |
+
|
| 300 |
+
prefill_urls = args.prefill.split(",")
|
| 301 |
+
bootstrap_ports = [int(p) for p in args.prefill_bootstrap_ports.split(",")]
|
| 302 |
+
|
| 303 |
+
if len(bootstrap_ports) == 1:
|
| 304 |
+
bootstrap_ports = bootstrap_ports * len(prefill_urls)
|
| 305 |
+
else:
|
| 306 |
+
if len(bootstrap_ports) != len(prefill_urls):
|
| 307 |
+
raise ValueError(
|
| 308 |
+
"Number of prefill URLs must match number of bootstrap ports"
|
| 309 |
+
)
|
| 310 |
+
exit(1)
|
| 311 |
+
|
| 312 |
+
prefill_configs = []
|
| 313 |
+
for url, port in zip(prefill_urls, bootstrap_ports):
|
| 314 |
+
prefill_configs.append(PrefillConfig(url, port))
|
| 315 |
+
|
| 316 |
+
decode_addrs = args.decode.split(",")
|
| 317 |
+
|
| 318 |
+
run(prefill_configs, decode_addrs, args.host, args.port)
|
lib/python3.12/site-packages/sglang/srt/disaggregation/mooncake/__init__.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .conn import (
|
| 2 |
+
MooncakeKVBootstrapServer,
|
| 3 |
+
MooncakeKVManager,
|
| 4 |
+
MooncakeKVReceiver,
|
| 5 |
+
MooncakeKVSender,
|
| 6 |
+
)
|
lib/python3.12/site-packages/sglang/srt/disaggregation/mooncake/__pycache__/transfer_engine.cpython-312.pyc
ADDED
|
Binary file (4.04 kB). View file
|
|
|
lib/python3.12/site-packages/sglang/srt/disaggregation/mooncake/conn.py
ADDED
|
@@ -0,0 +1,752 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import asyncio
|
| 4 |
+
import concurrent.futures
|
| 5 |
+
import dataclasses
|
| 6 |
+
import logging
|
| 7 |
+
import os
|
| 8 |
+
import queue
|
| 9 |
+
import socket
|
| 10 |
+
import struct
|
| 11 |
+
import threading
|
| 12 |
+
from functools import cache
|
| 13 |
+
from typing import Dict, List, Optional, Tuple, Union
|
| 14 |
+
|
| 15 |
+
import numpy as np
|
| 16 |
+
import numpy.typing as npt
|
| 17 |
+
import requests
|
| 18 |
+
import zmq
|
| 19 |
+
from aiohttp import web
|
| 20 |
+
|
| 21 |
+
from sglang.srt.disaggregation.base.conn import (
|
| 22 |
+
BaseKVBootstrapServer,
|
| 23 |
+
BaseKVManager,
|
| 24 |
+
BaseKVReceiver,
|
| 25 |
+
BaseKVSender,
|
| 26 |
+
KVArgs,
|
| 27 |
+
KVPoll,
|
| 28 |
+
)
|
| 29 |
+
from sglang.srt.disaggregation.mooncake.transfer_engine import MooncakeTransferEngine
|
| 30 |
+
from sglang.srt.disaggregation.utils import DisaggregationMode
|
| 31 |
+
from sglang.srt.server_args import ServerArgs
|
| 32 |
+
from sglang.srt.utils import get_free_port, get_ip, get_local_ip_by_remote
|
| 33 |
+
|
| 34 |
+
logger = logging.getLogger(__name__)
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def group_concurrent_contiguous(
|
| 38 |
+
src_indices: npt.NDArray[np.int64], dst_indices: npt.NDArray[np.int64]
|
| 39 |
+
) -> Tuple[List[npt.NDArray[np.int64]], List[npt.NDArray[np.int64]]]:
|
| 40 |
+
src_groups = []
|
| 41 |
+
dst_groups = []
|
| 42 |
+
current_src = [src_indices[0]]
|
| 43 |
+
current_dst = [dst_indices[0]]
|
| 44 |
+
|
| 45 |
+
for i in range(1, len(src_indices)):
|
| 46 |
+
src_contiguous = src_indices[i] == src_indices[i - 1] + 1
|
| 47 |
+
dst_contiguous = dst_indices[i] == dst_indices[i - 1] + 1
|
| 48 |
+
if src_contiguous and dst_contiguous:
|
| 49 |
+
current_src.append(src_indices[i])
|
| 50 |
+
current_dst.append(dst_indices[i])
|
| 51 |
+
else:
|
| 52 |
+
src_groups.append(current_src)
|
| 53 |
+
dst_groups.append(current_dst)
|
| 54 |
+
current_src = [src_indices[i]]
|
| 55 |
+
current_dst = [dst_indices[i]]
|
| 56 |
+
|
| 57 |
+
src_groups.append(current_src)
|
| 58 |
+
dst_groups.append(current_dst)
|
| 59 |
+
|
| 60 |
+
return src_groups, dst_groups
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
@dataclasses.dataclass
|
| 64 |
+
class TransferKVChunk:
|
| 65 |
+
room: int
|
| 66 |
+
prefill_kv_indices: npt.NDArray[np.int64]
|
| 67 |
+
index_slice: slice
|
| 68 |
+
is_last: bool
|
| 69 |
+
prefill_aux_index: Optional[int]
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
@dataclasses.dataclass
|
| 73 |
+
class TransferInfo:
|
| 74 |
+
room: int
|
| 75 |
+
endpoint: str
|
| 76 |
+
dst_port: int
|
| 77 |
+
mooncake_session_id: str
|
| 78 |
+
dst_kv_indices: npt.NDArray[np.int64]
|
| 79 |
+
dst_aux_index: int
|
| 80 |
+
|
| 81 |
+
@classmethod
|
| 82 |
+
def from_zmq(cls, msg: List[bytes]):
|
| 83 |
+
return cls(
|
| 84 |
+
room=int(msg[0].decode("ascii")),
|
| 85 |
+
endpoint=msg[1].decode("ascii"),
|
| 86 |
+
dst_port=int(msg[2].decode("ascii")),
|
| 87 |
+
mooncake_session_id=msg[3].decode("ascii"),
|
| 88 |
+
dst_kv_indices=np.frombuffer(msg[4], dtype=np.int64),
|
| 89 |
+
dst_aux_index=int(msg[5].decode("ascii")),
|
| 90 |
+
)
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
@dataclasses.dataclass
|
| 94 |
+
class KVArgsRegisterInfo:
|
| 95 |
+
room: str
|
| 96 |
+
endpoint: str
|
| 97 |
+
dst_port: int
|
| 98 |
+
mooncake_session_id: str
|
| 99 |
+
dst_kv_ptrs: list[int]
|
| 100 |
+
dst_aux_ptrs: list[int]
|
| 101 |
+
|
| 102 |
+
@classmethod
|
| 103 |
+
def from_zmq(cls, msg: List[bytes]):
|
| 104 |
+
return cls(
|
| 105 |
+
room=str(msg[0].decode("ascii")),
|
| 106 |
+
endpoint=msg[1].decode("ascii"),
|
| 107 |
+
dst_port=int(msg[2].decode("ascii")),
|
| 108 |
+
mooncake_session_id=msg[3].decode("ascii"),
|
| 109 |
+
dst_kv_ptrs=list(struct.unpack(f"{len(msg[4])//8}Q", msg[4])),
|
| 110 |
+
dst_aux_ptrs=list(struct.unpack(f"{len(msg[5])//8}Q", msg[5])),
|
| 111 |
+
)
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
class MooncakeKVManager(BaseKVManager):
|
| 115 |
+
def __init__(
|
| 116 |
+
self,
|
| 117 |
+
args: KVArgs,
|
| 118 |
+
disaggregation_mode: DisaggregationMode,
|
| 119 |
+
server_args: ServerArgs,
|
| 120 |
+
):
|
| 121 |
+
self.kv_args = args
|
| 122 |
+
self.engine = MooncakeTransferEngine(
|
| 123 |
+
hostname=get_local_ip_by_remote(),
|
| 124 |
+
gpu_id=self.kv_args.gpu_id,
|
| 125 |
+
ib_device=self.kv_args.ib_device,
|
| 126 |
+
)
|
| 127 |
+
self.disaggregation_mode = disaggregation_mode
|
| 128 |
+
# for p/d multi node infer
|
| 129 |
+
self.bootstrap_port = server_args.disaggregation_bootstrap_port
|
| 130 |
+
self.dist_init_addr = server_args.dist_init_addr
|
| 131 |
+
self.tp_size = server_args.tp_size
|
| 132 |
+
self.dp_size = server_args.dp_size
|
| 133 |
+
self.enable_dp_attention = server_args.enable_dp_attention
|
| 134 |
+
if not server_args.enable_dp_attention and server_args.dp_size != 1:
|
| 135 |
+
raise ValueError(
|
| 136 |
+
"If dp_attention is not enabled, dp size must be 1 in disaggregation mode."
|
| 137 |
+
)
|
| 138 |
+
self.request_status: Dict[int, KVPoll] = {}
|
| 139 |
+
self.rank_port = None
|
| 140 |
+
self.server_socket = zmq.Context().socket(zmq.PULL)
|
| 141 |
+
self.register_buffer_to_engine()
|
| 142 |
+
if self.disaggregation_mode == DisaggregationMode.PREFILL:
|
| 143 |
+
self.transfer_queue = queue.Queue()
|
| 144 |
+
self.transfer_infos: Dict[int, TransferInfo] = {}
|
| 145 |
+
self.decode_kv_args_table: Dict[str, KVArgsRegisterInfo] = {}
|
| 146 |
+
self.start_prefill_thread()
|
| 147 |
+
self._register_to_bootstrap()
|
| 148 |
+
|
| 149 |
+
# Determine the number of threads to use for kv sender
|
| 150 |
+
cpu_count = os.cpu_count()
|
| 151 |
+
self.executor = concurrent.futures.ThreadPoolExecutor(
|
| 152 |
+
min(cpu_count // 4, 16)
|
| 153 |
+
)
|
| 154 |
+
elif self.disaggregation_mode == DisaggregationMode.DECODE:
|
| 155 |
+
self.start_decode_thread()
|
| 156 |
+
self.connection_pool: Dict[str, Dict[str, Union[str, int]]] = {}
|
| 157 |
+
self.prefill_dp_size_table: Dict[str, int] = {}
|
| 158 |
+
else:
|
| 159 |
+
raise ValueError(
|
| 160 |
+
f"Unsupported DisaggregationMode: {self.disaggregation_mode}"
|
| 161 |
+
)
|
| 162 |
+
|
| 163 |
+
def register_buffer_to_engine(self):
|
| 164 |
+
for kv_data_ptr, kv_data_len in zip(
|
| 165 |
+
self.kv_args.kv_data_ptrs, self.kv_args.kv_data_lens
|
| 166 |
+
):
|
| 167 |
+
self.engine.register(kv_data_ptr, kv_data_len)
|
| 168 |
+
|
| 169 |
+
for aux_data_ptr, aux_data_len in zip(
|
| 170 |
+
self.kv_args.aux_data_ptrs, self.kv_args.aux_data_lens
|
| 171 |
+
):
|
| 172 |
+
self.engine.register(aux_data_ptr, aux_data_len)
|
| 173 |
+
|
| 174 |
+
@cache
|
| 175 |
+
def _connect(self, endpoint: str):
|
| 176 |
+
socket = zmq.Context().socket(zmq.PUSH)
|
| 177 |
+
socket.connect(endpoint)
|
| 178 |
+
return socket
|
| 179 |
+
|
| 180 |
+
def send_kvcache(
|
| 181 |
+
self,
|
| 182 |
+
mooncake_session_id: str,
|
| 183 |
+
prefill_kv_indices: npt.NDArray[np.int64],
|
| 184 |
+
dst_kv_ptrs: list[int],
|
| 185 |
+
dst_kv_indices: npt.NDArray[np.int64],
|
| 186 |
+
):
|
| 187 |
+
# Group by indices
|
| 188 |
+
prefill_kv_blocks, dst_kv_blocks = group_concurrent_contiguous(
|
| 189 |
+
prefill_kv_indices, dst_kv_indices
|
| 190 |
+
)
|
| 191 |
+
|
| 192 |
+
num_layers = len(self.kv_args.kv_data_ptrs)
|
| 193 |
+
layers_params = [
|
| 194 |
+
(
|
| 195 |
+
self.kv_args.kv_data_ptrs[layer_id],
|
| 196 |
+
dst_kv_ptrs[layer_id],
|
| 197 |
+
self.kv_args.kv_item_lens[layer_id],
|
| 198 |
+
)
|
| 199 |
+
for layer_id in range(num_layers)
|
| 200 |
+
]
|
| 201 |
+
|
| 202 |
+
# Worker function for processing a single layer
|
| 203 |
+
def process_layer(src_ptr: int, dst_ptr: int, item_len: int) -> int:
|
| 204 |
+
for prefill_index, decode_index in zip(prefill_kv_blocks, dst_kv_blocks):
|
| 205 |
+
src_addr = src_ptr + int(prefill_index[0]) * item_len
|
| 206 |
+
dst_addr = dst_ptr + int(decode_index[0]) * item_len
|
| 207 |
+
length = item_len * len(prefill_index)
|
| 208 |
+
|
| 209 |
+
status = self.engine.transfer_sync(
|
| 210 |
+
mooncake_session_id, src_addr, dst_addr, length
|
| 211 |
+
)
|
| 212 |
+
if status != 0:
|
| 213 |
+
return status
|
| 214 |
+
return 0
|
| 215 |
+
|
| 216 |
+
futures = [
|
| 217 |
+
self.executor.submit(
|
| 218 |
+
process_layer,
|
| 219 |
+
src_ptr,
|
| 220 |
+
dst_ptr,
|
| 221 |
+
item_len,
|
| 222 |
+
)
|
| 223 |
+
for (src_ptr, dst_ptr, item_len) in layers_params
|
| 224 |
+
]
|
| 225 |
+
|
| 226 |
+
for future in concurrent.futures.as_completed(futures):
|
| 227 |
+
status = future.result()
|
| 228 |
+
if status != 0:
|
| 229 |
+
# Immediate shutdown on first error (existing tasks will finish)
|
| 230 |
+
executor.shutdown(wait=False)
|
| 231 |
+
for f in futures:
|
| 232 |
+
f.cancel()
|
| 233 |
+
return status
|
| 234 |
+
|
| 235 |
+
return 0
|
| 236 |
+
|
| 237 |
+
def send_aux(
|
| 238 |
+
self,
|
| 239 |
+
mooncake_session_id: str,
|
| 240 |
+
prefill_aux_index: int,
|
| 241 |
+
dst_aux_ptrs: list[int],
|
| 242 |
+
dst_aux_index: int,
|
| 243 |
+
):
|
| 244 |
+
aux_item_len = self.kv_args.aux_item_lens[0]
|
| 245 |
+
prefill_aux_addr = (
|
| 246 |
+
self.kv_args.aux_data_ptrs[0] + prefill_aux_index * aux_item_len
|
| 247 |
+
)
|
| 248 |
+
decode_aux_addr = dst_aux_ptrs[0] + dst_aux_index * aux_item_len
|
| 249 |
+
# TODO: mooncake transfer engine can do async transfer. Do async later
|
| 250 |
+
# Not sure about the amount of aux data, maybe transfer it by zmq is more effective
|
| 251 |
+
status = self.engine.transfer_sync(
|
| 252 |
+
mooncake_session_id, prefill_aux_addr, decode_aux_addr, aux_item_len
|
| 253 |
+
)
|
| 254 |
+
return status
|
| 255 |
+
|
| 256 |
+
def sync_status_to_decode_endpoint(self, remote: str, dst_port: int, room: int):
|
| 257 |
+
if ":" in remote:
|
| 258 |
+
remote = remote.split(":")[0]
|
| 259 |
+
self._connect("tcp://" + remote + ":" + str(dst_port)).send_multipart(
|
| 260 |
+
[
|
| 261 |
+
str(room).encode("ascii"),
|
| 262 |
+
str(self.request_status[room]).encode("ascii"),
|
| 263 |
+
]
|
| 264 |
+
)
|
| 265 |
+
|
| 266 |
+
def start_prefill_thread(self):
|
| 267 |
+
self.rank_port = get_free_port()
|
| 268 |
+
self.server_socket.bind(f"tcp://{get_local_ip_by_remote()}:{self.rank_port}")
|
| 269 |
+
|
| 270 |
+
def bootstrap_thread():
|
| 271 |
+
"""This thread recvs pre-alloc notification from the decode engine"""
|
| 272 |
+
# KVPoll.Bootstrapping -> KVPoll.WaitingForInput
|
| 273 |
+
while True:
|
| 274 |
+
waiting_req_bytes = self.server_socket.recv_multipart()
|
| 275 |
+
room = waiting_req_bytes[0].decode("ascii")
|
| 276 |
+
if room == "None":
|
| 277 |
+
mooncake_session_id = waiting_req_bytes[3].decode("ascii")
|
| 278 |
+
self.decode_kv_args_table[mooncake_session_id] = (
|
| 279 |
+
KVArgsRegisterInfo.from_zmq(waiting_req_bytes)
|
| 280 |
+
)
|
| 281 |
+
logger.debug(
|
| 282 |
+
f"Register KVArgs from {mooncake_session_id} successfully"
|
| 283 |
+
)
|
| 284 |
+
continue
|
| 285 |
+
room = int(room)
|
| 286 |
+
self.transfer_infos[room] = TransferInfo.from_zmq(waiting_req_bytes)
|
| 287 |
+
|
| 288 |
+
# NOTE: after bootstrapping we can mark the req as waiting for input
|
| 289 |
+
self.request_status[room] = KVPoll.WaitingForInput
|
| 290 |
+
|
| 291 |
+
def transfer_thread():
|
| 292 |
+
# TODO: Shall we use KVPoll.Transferring state?
|
| 293 |
+
while True:
|
| 294 |
+
try:
|
| 295 |
+
kv_chunk: TransferKVChunk = self.transfer_queue.get(timeout=0.01)
|
| 296 |
+
req = self.transfer_infos[kv_chunk.room]
|
| 297 |
+
chunked_dst_kv_indice = req.dst_kv_indices[kv_chunk.index_slice]
|
| 298 |
+
assert len(chunked_dst_kv_indice) == len(
|
| 299 |
+
kv_chunk.prefill_kv_indices
|
| 300 |
+
), f"len(chunked_dst_kv_indice) = {len(chunked_dst_kv_indice)}, len(kv_chunk.prefill_kv_indices) = {len(kv_chunk.prefill_kv_indices)}"
|
| 301 |
+
|
| 302 |
+
ret = self.send_kvcache(
|
| 303 |
+
req.mooncake_session_id,
|
| 304 |
+
kv_chunk.prefill_kv_indices,
|
| 305 |
+
self.decode_kv_args_table[req.mooncake_session_id].dst_kv_ptrs,
|
| 306 |
+
chunked_dst_kv_indice,
|
| 307 |
+
)
|
| 308 |
+
if ret != 0:
|
| 309 |
+
self.request_status[kv_chunk.room] = KVPoll.Failed
|
| 310 |
+
self.sync_status_to_decode_endpoint(
|
| 311 |
+
req.endpoint, req.dst_port, req.room
|
| 312 |
+
)
|
| 313 |
+
continue
|
| 314 |
+
|
| 315 |
+
if kv_chunk.is_last:
|
| 316 |
+
# Only the last chunk we need to send the aux data
|
| 317 |
+
ret = self.send_aux(
|
| 318 |
+
req.mooncake_session_id,
|
| 319 |
+
kv_chunk.prefill_aux_index,
|
| 320 |
+
self.decode_kv_args_table[
|
| 321 |
+
req.mooncake_session_id
|
| 322 |
+
].dst_aux_ptrs,
|
| 323 |
+
req.dst_aux_index,
|
| 324 |
+
)
|
| 325 |
+
self.request_status[req.room] = (
|
| 326 |
+
KVPoll.Success if ret == 0 else KVPoll.Failed
|
| 327 |
+
)
|
| 328 |
+
self.sync_status_to_decode_endpoint(
|
| 329 |
+
req.endpoint, req.dst_port, req.room
|
| 330 |
+
)
|
| 331 |
+
self.transfer_infos.pop(req.room)
|
| 332 |
+
|
| 333 |
+
except queue.Empty:
|
| 334 |
+
continue
|
| 335 |
+
|
| 336 |
+
threading.Thread(target=bootstrap_thread).start()
|
| 337 |
+
threading.Thread(target=transfer_thread).start()
|
| 338 |
+
|
| 339 |
+
def start_decode_thread(self):
|
| 340 |
+
self.rank_port = get_free_port()
|
| 341 |
+
self.server_socket.bind(f"tcp://{get_local_ip_by_remote()}:{self.rank_port}")
|
| 342 |
+
|
| 343 |
+
def decode_thread():
|
| 344 |
+
while True:
|
| 345 |
+
(bootstrap_room, status) = self.server_socket.recv_multipart()
|
| 346 |
+
status = int(status.decode("ascii"))
|
| 347 |
+
bootstrap_room = int(bootstrap_room.decode("ascii"))
|
| 348 |
+
self.request_status[bootstrap_room] = status
|
| 349 |
+
|
| 350 |
+
threading.Thread(target=decode_thread).start()
|
| 351 |
+
|
| 352 |
+
def add_transfer_request(
|
| 353 |
+
self,
|
| 354 |
+
bootstrap_room: int,
|
| 355 |
+
kv_indices: npt.NDArray[np.int64],
|
| 356 |
+
index_slice: slice,
|
| 357 |
+
is_last: bool,
|
| 358 |
+
aux_index: Optional[int] = None,
|
| 359 |
+
):
|
| 360 |
+
assert self.disaggregation_mode == DisaggregationMode.PREFILL
|
| 361 |
+
assert not is_last or (is_last and aux_index is not None)
|
| 362 |
+
|
| 363 |
+
self.transfer_queue.put(
|
| 364 |
+
TransferKVChunk(
|
| 365 |
+
room=bootstrap_room,
|
| 366 |
+
prefill_kv_indices=kv_indices,
|
| 367 |
+
index_slice=index_slice,
|
| 368 |
+
is_last=is_last,
|
| 369 |
+
prefill_aux_index=aux_index,
|
| 370 |
+
)
|
| 371 |
+
)
|
| 372 |
+
self.request_status[bootstrap_room] = KVPoll.WaitingForInput
|
| 373 |
+
|
| 374 |
+
def check_status(self, bootstrap_room: int):
|
| 375 |
+
# TOOD: do we really need the poll()?
|
| 376 |
+
|
| 377 |
+
return self.request_status[bootstrap_room]
|
| 378 |
+
|
| 379 |
+
def update_status(self, bootstrap_room: int, status: KVPoll):
|
| 380 |
+
if bootstrap_room not in self.request_status:
|
| 381 |
+
self.request_status[bootstrap_room] = status
|
| 382 |
+
else:
|
| 383 |
+
# NOTE: The prefill engine could recv bootstrapping first
|
| 384 |
+
self.request_status[bootstrap_room] = max(
|
| 385 |
+
self.request_status[bootstrap_room], status
|
| 386 |
+
)
|
| 387 |
+
|
| 388 |
+
def get_session_id(self):
|
| 389 |
+
return self.engine.get_session_id()
|
| 390 |
+
|
| 391 |
+
def _register_to_bootstrap(self):
|
| 392 |
+
"""Register KVSender to bootstrap server via HTTP POST."""
|
| 393 |
+
if self.dist_init_addr:
|
| 394 |
+
ip_address = socket.gethostbyname(self.dist_init_addr.split(":")[0])
|
| 395 |
+
else:
|
| 396 |
+
ip_address = get_ip()
|
| 397 |
+
|
| 398 |
+
bootstrap_server_url = f"{ip_address}:{self.bootstrap_port}"
|
| 399 |
+
url = f"http://{bootstrap_server_url}/route"
|
| 400 |
+
payload = {
|
| 401 |
+
"role": "Prefill",
|
| 402 |
+
"tp_size": self.tp_size,
|
| 403 |
+
"dp_size": self.dp_size,
|
| 404 |
+
"rank_ip": get_local_ip_by_remote(),
|
| 405 |
+
"rank_port": self.rank_port,
|
| 406 |
+
"engine_rank": self.kv_args.engine_rank,
|
| 407 |
+
}
|
| 408 |
+
|
| 409 |
+
try:
|
| 410 |
+
response = requests.put(url, json=payload)
|
| 411 |
+
if response.status_code == 200:
|
| 412 |
+
logger.debug("Prefill successfully registered to bootstrap server.")
|
| 413 |
+
else:
|
| 414 |
+
logger.error(
|
| 415 |
+
f"Prefill Failed to connect to bootstrap server: {response.status_code}, {response.text}"
|
| 416 |
+
)
|
| 417 |
+
except Exception as e:
|
| 418 |
+
logger.error(f"Prefill Failed to register to bootstrap server: {e}")
|
| 419 |
+
|
| 420 |
+
|
| 421 |
+
class MooncakeKVSender(BaseKVSender):
|
| 422 |
+
|
| 423 |
+
def __init__(
|
| 424 |
+
self, mgr: MooncakeKVManager, bootstrap_addr: str, bootstrap_room: int
|
| 425 |
+
):
|
| 426 |
+
self.kv_mgr = mgr
|
| 427 |
+
self.bootstrap_room = bootstrap_room
|
| 428 |
+
self.kv_mgr.update_status(bootstrap_room, KVPoll.Bootstrapping)
|
| 429 |
+
self.aux_index = None
|
| 430 |
+
self.bootstrap_server_url = bootstrap_addr
|
| 431 |
+
self.session_id = self.kv_mgr.get_session_id()
|
| 432 |
+
|
| 433 |
+
def init(self, num_kv_indices: int, aux_index: Optional[int] = None):
|
| 434 |
+
self.num_kv_indices = num_kv_indices
|
| 435 |
+
self.aux_index = aux_index
|
| 436 |
+
|
| 437 |
+
def send(
|
| 438 |
+
self,
|
| 439 |
+
kv_indices: npt.NDArray[np.int64],
|
| 440 |
+
index_slice: slice,
|
| 441 |
+
is_last: bool,
|
| 442 |
+
):
|
| 443 |
+
if not is_last:
|
| 444 |
+
self.kv_mgr.add_transfer_request(
|
| 445 |
+
self.bootstrap_room, kv_indices, index_slice, False
|
| 446 |
+
)
|
| 447 |
+
else:
|
| 448 |
+
self.kv_mgr.add_transfer_request(
|
| 449 |
+
self.bootstrap_room,
|
| 450 |
+
kv_indices,
|
| 451 |
+
index_slice,
|
| 452 |
+
True,
|
| 453 |
+
aux_index=self.aux_index,
|
| 454 |
+
)
|
| 455 |
+
|
| 456 |
+
def poll(self) -> KVPoll:
|
| 457 |
+
return self.kv_mgr.check_status(self.bootstrap_room)
|
| 458 |
+
|
| 459 |
+
def failure_exception(self):
|
| 460 |
+
raise Exception("Fake KVSender Exception")
|
| 461 |
+
|
| 462 |
+
|
| 463 |
+
class MooncakeKVReceiver(BaseKVReceiver):
|
| 464 |
+
_ctx = zmq.Context()
|
| 465 |
+
_socket_cache = {}
|
| 466 |
+
_socket_locks = {}
|
| 467 |
+
_global_lock = threading.Lock()
|
| 468 |
+
|
| 469 |
+
def __init__(
|
| 470 |
+
self,
|
| 471 |
+
mgr: MooncakeKVManager,
|
| 472 |
+
bootstrap_addr: str,
|
| 473 |
+
bootstrap_room: Optional[int] = None,
|
| 474 |
+
):
|
| 475 |
+
self.bootstrap_room = bootstrap_room
|
| 476 |
+
self.bootstrap_addr = bootstrap_addr
|
| 477 |
+
self.kv_mgr = mgr
|
| 478 |
+
self.session_id = self.kv_mgr.get_session_id()
|
| 479 |
+
self.kv_mgr.update_status(bootstrap_room, KVPoll.Bootstrapping)
|
| 480 |
+
|
| 481 |
+
if not self.kv_mgr.enable_dp_attention:
|
| 482 |
+
# We assume dp_attention should be activated simultaneously for
|
| 483 |
+
# both prefill role and decode role. If the decode instance does
|
| 484 |
+
# not enable dp_attention, then dp_attention is not enabled on the
|
| 485 |
+
# prefill instance as well. Therefore, we should skip questioning
|
| 486 |
+
# the prefill dp size to reduce bootstrap overhead.
|
| 487 |
+
self.prefill_dp_size = 1
|
| 488 |
+
elif self.bootstrap_addr not in self.kv_mgr.prefill_dp_size_table:
|
| 489 |
+
self.prefill_dp_size, tp_size_per_dp_rank = (
|
| 490 |
+
self._get_prefill_dp_size_from_server()
|
| 491 |
+
)
|
| 492 |
+
# Currently, we don't allow prefill instance and decode instance to
|
| 493 |
+
# have different TP sizes per DP rank.
|
| 494 |
+
assert tp_size_per_dp_rank == self.kv_mgr.tp_size // self.kv_mgr.dp_size
|
| 495 |
+
if self.prefill_dp_size is None:
|
| 496 |
+
logger.error(
|
| 497 |
+
f"Could not fetch prefill dp_size for bootstrap_addr: {self.bootstrap_addr}"
|
| 498 |
+
)
|
| 499 |
+
else:
|
| 500 |
+
self.kv_mgr.prefill_dp_size_table[self.bootstrap_addr] = (
|
| 501 |
+
self.prefill_dp_size
|
| 502 |
+
)
|
| 503 |
+
else:
|
| 504 |
+
self.prefill_dp_size = self.kv_mgr.prefill_dp_size_table[
|
| 505 |
+
self.bootstrap_addr
|
| 506 |
+
]
|
| 507 |
+
|
| 508 |
+
# NOTE: key distinguished by bootstrap_addr and engine_rank
|
| 509 |
+
self.target_dp_group = bootstrap_room % self.prefill_dp_size
|
| 510 |
+
bootstrap_key = f"{self.bootstrap_addr}_{self.kv_mgr.kv_args.engine_rank}"
|
| 511 |
+
|
| 512 |
+
if bootstrap_key not in self.kv_mgr.connection_pool:
|
| 513 |
+
self.bootstrap_info = self._get_bootstrap_info_from_server(
|
| 514 |
+
self.kv_mgr.kv_args.engine_rank,
|
| 515 |
+
self.target_dp_group,
|
| 516 |
+
)
|
| 517 |
+
if self.bootstrap_info is None:
|
| 518 |
+
logger.error(
|
| 519 |
+
f"Could not fetch bootstrap info for engine rank: {self.kv_mgr.kv_args.engine_rank}"
|
| 520 |
+
)
|
| 521 |
+
else:
|
| 522 |
+
self.kv_mgr.connection_pool[bootstrap_key] = self.bootstrap_info
|
| 523 |
+
# Register kv_args only once to prefill KVManager according to the info fetched from the bootstrap server
|
| 524 |
+
self._register_kv_args()
|
| 525 |
+
else:
|
| 526 |
+
self.bootstrap_info = self.kv_mgr.connection_pool[bootstrap_key]
|
| 527 |
+
|
| 528 |
+
assert self.bootstrap_info is not None
|
| 529 |
+
self.kv_mgr.update_status(bootstrap_room, KVPoll.WaitingForInput)
|
| 530 |
+
|
| 531 |
+
def _get_bootstrap_info_from_server(self, engine_rank, target_dp_group):
|
| 532 |
+
"""Fetch the bootstrap info from the bootstrap server."""
|
| 533 |
+
try:
|
| 534 |
+
url = f"http://{self.bootstrap_addr}/route?engine_rank={engine_rank}&target_dp_group={target_dp_group}"
|
| 535 |
+
response = requests.get(url)
|
| 536 |
+
if response.status_code == 200:
|
| 537 |
+
bootstrap_info = response.json()
|
| 538 |
+
return bootstrap_info
|
| 539 |
+
else:
|
| 540 |
+
logger.error(
|
| 541 |
+
f"Failed to get prefill server info: {response.status_code}, {response.text}"
|
| 542 |
+
)
|
| 543 |
+
return None
|
| 544 |
+
except Exception as e:
|
| 545 |
+
logger.error(f"Error fetching prefill info from bootstrap: {e}")
|
| 546 |
+
return None
|
| 547 |
+
|
| 548 |
+
def _get_prefill_dp_size_from_server(self) -> int:
|
| 549 |
+
"""Fetch the prefill parallel info from the bootstrap server."""
|
| 550 |
+
try:
|
| 551 |
+
url = f"http://{self.bootstrap_addr}/route?engine_rank={-1}&target_dp_group={-1}"
|
| 552 |
+
response = requests.get(url)
|
| 553 |
+
if response.status_code == 200:
|
| 554 |
+
prefill_parallel_info = response.json()
|
| 555 |
+
return int(prefill_parallel_info["prefill_dp_size"]), int(
|
| 556 |
+
prefill_parallel_info["tp_size_per_dp_rank"]
|
| 557 |
+
)
|
| 558 |
+
else:
|
| 559 |
+
logger.error(
|
| 560 |
+
f"Failed to get prefill parallel info: {response.status_code}, {response.text}"
|
| 561 |
+
)
|
| 562 |
+
return None
|
| 563 |
+
except Exception as e:
|
| 564 |
+
logger.error(f"Error fetching prefill parallel info from bootstrap: {e}")
|
| 565 |
+
return None
|
| 566 |
+
|
| 567 |
+
def _register_kv_args(self):
|
| 568 |
+
self.prefill_server_url = (
|
| 569 |
+
f"{self.bootstrap_info['rank_ip']}:{self.bootstrap_info['rank_port']}"
|
| 570 |
+
)
|
| 571 |
+
|
| 572 |
+
packed_kv_data_ptrs = b"".join(
|
| 573 |
+
struct.pack("Q", ptr) for ptr in self.kv_mgr.kv_args.kv_data_ptrs
|
| 574 |
+
)
|
| 575 |
+
packed_aux_data_ptrs = b"".join(
|
| 576 |
+
struct.pack("Q", ptr) for ptr in self.kv_mgr.kv_args.aux_data_ptrs
|
| 577 |
+
)
|
| 578 |
+
sock, lock = self._connect("tcp://" + self.prefill_server_url)
|
| 579 |
+
with lock:
|
| 580 |
+
sock.send_multipart(
|
| 581 |
+
[
|
| 582 |
+
"None".encode("ascii"),
|
| 583 |
+
get_local_ip_by_remote().encode("ascii"),
|
| 584 |
+
str(self.kv_mgr.rank_port).encode("ascii"),
|
| 585 |
+
self.session_id.encode("ascii"),
|
| 586 |
+
packed_kv_data_ptrs,
|
| 587 |
+
packed_aux_data_ptrs,
|
| 588 |
+
]
|
| 589 |
+
)
|
| 590 |
+
|
| 591 |
+
@classmethod
|
| 592 |
+
def _connect(cls, endpoint: str):
|
| 593 |
+
with cls._global_lock:
|
| 594 |
+
if endpoint not in cls._socket_cache:
|
| 595 |
+
sock = cls._ctx.socket(zmq.PUSH)
|
| 596 |
+
sock.connect(endpoint)
|
| 597 |
+
cls._socket_cache[endpoint] = sock
|
| 598 |
+
cls._socket_locks[endpoint] = threading.Lock()
|
| 599 |
+
return cls._socket_cache[endpoint], cls._socket_locks[endpoint]
|
| 600 |
+
|
| 601 |
+
def init(self, kv_indices: npt.NDArray[np.int64], aux_index: Optional[int] = None):
|
| 602 |
+
self.prefill_server_url = (
|
| 603 |
+
f"{self.bootstrap_info['rank_ip']}:{self.bootstrap_info['rank_port']}"
|
| 604 |
+
)
|
| 605 |
+
logger.debug(
|
| 606 |
+
f"Fetched bootstrap info: {self.bootstrap_info} for engine rank: {self.kv_mgr.kv_args.engine_rank}"
|
| 607 |
+
)
|
| 608 |
+
|
| 609 |
+
sock, lock = self._connect("tcp://" + self.prefill_server_url)
|
| 610 |
+
with lock:
|
| 611 |
+
sock.send_multipart(
|
| 612 |
+
[
|
| 613 |
+
str(self.bootstrap_room).encode("ascii"),
|
| 614 |
+
get_local_ip_by_remote().encode("ascii"),
|
| 615 |
+
str(self.kv_mgr.rank_port).encode("ascii"),
|
| 616 |
+
self.session_id.encode("ascii"),
|
| 617 |
+
kv_indices.tobytes(),
|
| 618 |
+
str(aux_index).encode("ascii"),
|
| 619 |
+
]
|
| 620 |
+
)
|
| 621 |
+
|
| 622 |
+
def poll(self) -> KVPoll:
|
| 623 |
+
return self.kv_mgr.check_status(self.bootstrap_room)
|
| 624 |
+
|
| 625 |
+
def failure_exception(self):
|
| 626 |
+
raise Exception("Fake KVReceiver Exception")
|
| 627 |
+
|
| 628 |
+
|
| 629 |
+
class MooncakeKVBootstrapServer(BaseKVBootstrapServer):
|
| 630 |
+
def __init__(self, port: int):
|
| 631 |
+
self.port = port
|
| 632 |
+
self.app = web.Application()
|
| 633 |
+
self.store = dict()
|
| 634 |
+
self.lock = asyncio.Lock()
|
| 635 |
+
self._setup_routes()
|
| 636 |
+
self.dp_size = None
|
| 637 |
+
self.tp_size_per_dp_rank = None
|
| 638 |
+
self.prefill_port_table: Dict[int, Dict[int, Dict[str, Union[str, int]]]] = {}
|
| 639 |
+
|
| 640 |
+
# Start bootstrap server
|
| 641 |
+
self.thread = threading.Thread(target=self._run_server, daemon=True)
|
| 642 |
+
self.run()
|
| 643 |
+
|
| 644 |
+
def run(self):
|
| 645 |
+
self.thread.start()
|
| 646 |
+
|
| 647 |
+
def _setup_routes(self):
|
| 648 |
+
self.app.router.add_route("*", "/route", self._handle_route)
|
| 649 |
+
|
| 650 |
+
async def _handle_route(self, request: web.Request):
|
| 651 |
+
method = request.method
|
| 652 |
+
if method == "PUT":
|
| 653 |
+
return await self._handle_route_put(request)
|
| 654 |
+
elif method == "GET":
|
| 655 |
+
return await self._handle_route_get(request)
|
| 656 |
+
else:
|
| 657 |
+
return web.Response(
|
| 658 |
+
text="Method not allowed", status=405, content_type="application/json"
|
| 659 |
+
)
|
| 660 |
+
|
| 661 |
+
async def _handle_route_put(self, request: web.Request):
|
| 662 |
+
data = await request.json()
|
| 663 |
+
role = data["role"]
|
| 664 |
+
tp_size = data["tp_size"]
|
| 665 |
+
dp_size = data["dp_size"]
|
| 666 |
+
rank_ip = data["rank_ip"]
|
| 667 |
+
rank_port = int(data["rank_port"])
|
| 668 |
+
engine_rank = int(data["engine_rank"])
|
| 669 |
+
|
| 670 |
+
if self.dp_size is None:
|
| 671 |
+
self.dp_size = dp_size
|
| 672 |
+
|
| 673 |
+
tp_size_per_dp_rank = tp_size // dp_size
|
| 674 |
+
if self.tp_size_per_dp_rank == None:
|
| 675 |
+
self.tp_size_per_dp_rank = tp_size_per_dp_rank
|
| 676 |
+
|
| 677 |
+
# Add lock to make sure thread-safe
|
| 678 |
+
if role == "Prefill":
|
| 679 |
+
dp_group = engine_rank // tp_size_per_dp_rank
|
| 680 |
+
tp_rank_in_dp_group = engine_rank % tp_size_per_dp_rank
|
| 681 |
+
|
| 682 |
+
async with self.lock:
|
| 683 |
+
if dp_group not in self.prefill_port_table:
|
| 684 |
+
self.prefill_port_table[dp_group] = {}
|
| 685 |
+
|
| 686 |
+
self.prefill_port_table[dp_group][tp_rank_in_dp_group] = {
|
| 687 |
+
"rank_ip": rank_ip,
|
| 688 |
+
"rank_port": rank_port,
|
| 689 |
+
}
|
| 690 |
+
logger.debug(
|
| 691 |
+
f"Register Prefill bootstrap: {engine_rank} with rank_ip: {rank_ip} and rank_port: {rank_port}"
|
| 692 |
+
)
|
| 693 |
+
|
| 694 |
+
return web.Response(text="OK", status=200)
|
| 695 |
+
|
| 696 |
+
async def _handle_route_get(self, request: web.Request):
|
| 697 |
+
engine_rank = request.query.get("engine_rank")
|
| 698 |
+
target_dp_group = request.query.get("target_dp_group")
|
| 699 |
+
if not engine_rank or not target_dp_group:
|
| 700 |
+
return web.Response(text="Missing inputs for bootstrap server.", status=400)
|
| 701 |
+
|
| 702 |
+
# Currently we use engine_rank == -1 and target_dp_group == -1 to sync dp size
|
| 703 |
+
if int(engine_rank) == -1 and int(target_dp_group) == -1:
|
| 704 |
+
prefill_parallel_info = {
|
| 705 |
+
"prefill_dp_size": self.dp_size,
|
| 706 |
+
"tp_size_per_dp_rank": self.tp_size_per_dp_rank,
|
| 707 |
+
}
|
| 708 |
+
return web.json_response(prefill_parallel_info, status=200)
|
| 709 |
+
|
| 710 |
+
# Find corresponding prefill info
|
| 711 |
+
tp_rank_in_dp_group = int(engine_rank) % self.tp_size_per_dp_rank
|
| 712 |
+
|
| 713 |
+
async with self.lock:
|
| 714 |
+
bootstrap_info = self.prefill_port_table[int(target_dp_group)][
|
| 715 |
+
tp_rank_in_dp_group
|
| 716 |
+
]
|
| 717 |
+
|
| 718 |
+
if bootstrap_info is not None:
|
| 719 |
+
return web.json_response(bootstrap_info, status=200)
|
| 720 |
+
else:
|
| 721 |
+
return web.Response(text="Bootstrap info not Found", status=404)
|
| 722 |
+
|
| 723 |
+
def _run_server(self):
|
| 724 |
+
try:
|
| 725 |
+
# Event Loop
|
| 726 |
+
self._loop = asyncio.new_event_loop()
|
| 727 |
+
asyncio.set_event_loop(self._loop)
|
| 728 |
+
|
| 729 |
+
self._runner = web.AppRunner(self.app)
|
| 730 |
+
self._loop.run_until_complete(self._runner.setup())
|
| 731 |
+
|
| 732 |
+
site = web.TCPSite(self._runner, port=self.port)
|
| 733 |
+
self._loop.run_until_complete(site.start())
|
| 734 |
+
self._loop.run_forever()
|
| 735 |
+
except Exception as e:
|
| 736 |
+
logger.error(f"Server error: {str(e)}")
|
| 737 |
+
finally:
|
| 738 |
+
# Cleanup
|
| 739 |
+
self._loop.run_until_complete(self._runner.cleanup())
|
| 740 |
+
self._loop.close()
|
| 741 |
+
|
| 742 |
+
def close(self):
|
| 743 |
+
"""Shutdown"""
|
| 744 |
+
if self._loop is not None and self._loop.is_running():
|
| 745 |
+
self._loop.call_soon_threadsafe(self._loop.stop)
|
| 746 |
+
logger.info("Stopping server loop...")
|
| 747 |
+
|
| 748 |
+
if self.thread.is_alive():
|
| 749 |
+
self.thread.join(timeout=2)
|
| 750 |
+
logger.info("Server thread stopped")
|
| 751 |
+
|
| 752 |
+
def poll(self) -> KVPoll: ...
|
lib/python3.12/site-packages/sglang/srt/disaggregation/mooncake/transfer_engine.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import logging
|
| 3 |
+
from dataclasses import dataclass
|
| 4 |
+
from typing import Optional
|
| 5 |
+
|
| 6 |
+
logger = logging.getLogger(__name__)
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class MooncakeTransferEngine:
|
| 10 |
+
|
| 11 |
+
def __init__(self, hostname: str, gpu_id: int, ib_device: Optional[str] = None):
|
| 12 |
+
try:
|
| 13 |
+
from mooncake.engine import TransferEngine
|
| 14 |
+
except ImportError as e:
|
| 15 |
+
raise ImportError(
|
| 16 |
+
"Please install mooncake by following the instructions at "
|
| 17 |
+
"https://github.com/kvcache-ai/Mooncake/blob/main/doc/en/build.md " # noqa: E501
|
| 18 |
+
"to run SGLang with MooncakeTransferEngine."
|
| 19 |
+
) from e
|
| 20 |
+
|
| 21 |
+
self.engine = TransferEngine()
|
| 22 |
+
self.hostname = hostname
|
| 23 |
+
self.gpu_id = gpu_id
|
| 24 |
+
self.ib_device = ib_device
|
| 25 |
+
|
| 26 |
+
self.initialize(
|
| 27 |
+
hostname=self.hostname,
|
| 28 |
+
device_name=self.ib_device,
|
| 29 |
+
)
|
| 30 |
+
self.session_id = f"{self.hostname}:{self.engine.get_rpc_port()}"
|
| 31 |
+
|
| 32 |
+
def register(self, ptr, length):
|
| 33 |
+
ret_value = self.engine.register_memory(ptr, length)
|
| 34 |
+
if ret_value != 0:
|
| 35 |
+
logger.error("Mooncake memory registration failed.")
|
| 36 |
+
raise RuntimeError("Mooncake memory registration failed.")
|
| 37 |
+
|
| 38 |
+
def deregister(self, ptr):
|
| 39 |
+
ret_value = self.engine.unregister_memory(ptr)
|
| 40 |
+
if ret_value != 0:
|
| 41 |
+
logger.error("Mooncake memory deregistration failed.")
|
| 42 |
+
raise RuntimeError("Mooncake memory deregistration failed.")
|
| 43 |
+
|
| 44 |
+
def initialize(
|
| 45 |
+
self,
|
| 46 |
+
hostname: str,
|
| 47 |
+
device_name: Optional[str],
|
| 48 |
+
) -> None:
|
| 49 |
+
"""Initialize the mooncake instance."""
|
| 50 |
+
ret_value = self.engine.initialize(
|
| 51 |
+
hostname,
|
| 52 |
+
"P2PHANDSHAKE",
|
| 53 |
+
"rdma",
|
| 54 |
+
device_name if device_name is not None else "",
|
| 55 |
+
)
|
| 56 |
+
if ret_value != 0:
|
| 57 |
+
logger.error("Mooncake Transfer Engine initialization failed.")
|
| 58 |
+
raise RuntimeError("Mooncake Transfer Engine initialization failed.")
|
| 59 |
+
|
| 60 |
+
def transfer_sync(
|
| 61 |
+
self, session_id: str, buffer: int, peer_buffer_address: int, length: int
|
| 62 |
+
) -> int:
|
| 63 |
+
"""Synchronously transfer data to the specified address."""
|
| 64 |
+
|
| 65 |
+
ret = self.engine.transfer_sync_write(
|
| 66 |
+
session_id, buffer, peer_buffer_address, length
|
| 67 |
+
)
|
| 68 |
+
if ret < 0:
|
| 69 |
+
logger.error("Mooncake Transfer Engine Return Error.")
|
| 70 |
+
raise RuntimeError("Mooncake Transfer Engine Return Error.")
|
| 71 |
+
return ret
|
| 72 |
+
|
| 73 |
+
def get_localhost(self):
|
| 74 |
+
return self.hostname
|
| 75 |
+
|
| 76 |
+
def get_session_id(self):
|
| 77 |
+
return self.session_id
|
lib/python3.12/site-packages/sglang/srt/disaggregation/prefill.py
ADDED
|
@@ -0,0 +1,417 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Life cycle of a request in the prefill server
|
| 3 |
+
|
| 4 |
+
1. Bootstrap Queue
|
| 5 |
+
a. Initialize a sender for each request
|
| 6 |
+
b. Use the queue to store requests whose bootstrap (handshake and preallocation) has not finished
|
| 7 |
+
c. Poll senders to check bootstrap state
|
| 8 |
+
d. Once bootstrap is complete, move request to Waiting Queue
|
| 9 |
+
|
| 10 |
+
2. Waiting Queue
|
| 11 |
+
a. Use PrefillAdder to pop requests
|
| 12 |
+
b. Run forward
|
| 13 |
+
c. Add the request to Inflight Queue
|
| 14 |
+
|
| 15 |
+
3. Inflight Queue
|
| 16 |
+
a. Poll (non-blocking) the sender of the request
|
| 17 |
+
b. Once the transfer has finished, return the request
|
| 18 |
+
"""
|
| 19 |
+
|
| 20 |
+
from __future__ import annotations
|
| 21 |
+
|
| 22 |
+
import logging
|
| 23 |
+
import threading
|
| 24 |
+
from collections import deque
|
| 25 |
+
from typing import TYPE_CHECKING, List, Optional
|
| 26 |
+
|
| 27 |
+
import torch
|
| 28 |
+
|
| 29 |
+
from sglang.srt.disaggregation.base import BaseKVManager, KVArgs, KVPoll
|
| 30 |
+
from sglang.srt.disaggregation.utils import (
|
| 31 |
+
DisaggregationMode,
|
| 32 |
+
FakeBootstrapHost,
|
| 33 |
+
KVClassType,
|
| 34 |
+
ReqToMetadataIdxAllocator,
|
| 35 |
+
TransferBackend,
|
| 36 |
+
get_kv_class,
|
| 37 |
+
kv_to_page_indices,
|
| 38 |
+
kv_to_page_num,
|
| 39 |
+
poll_and_all_reduce,
|
| 40 |
+
)
|
| 41 |
+
from sglang.srt.managers.schedule_batch import FINISH_LENGTH, Req, ScheduleBatch
|
| 42 |
+
|
| 43 |
+
if TYPE_CHECKING:
|
| 44 |
+
from torch.distributed import ProcessGroup
|
| 45 |
+
|
| 46 |
+
from sglang.srt.managers.scheduler import GenerationBatchResult, Scheduler
|
| 47 |
+
from sglang.srt.mem_cache.memory_pool import KVCache
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
logger = logging.getLogger(__name__)
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
class PrefillBootstrapQueue:
|
| 54 |
+
"""
|
| 55 |
+
Store the requests in bootstrapping
|
| 56 |
+
"""
|
| 57 |
+
|
| 58 |
+
def __init__(
|
| 59 |
+
self,
|
| 60 |
+
token_to_kv_pool: KVCache,
|
| 61 |
+
req_to_metadata_buffer_idx_allocator: ReqToMetadataIdxAllocator,
|
| 62 |
+
metadata_buffers: List[torch.Tensor],
|
| 63 |
+
aux_dtype: torch.dtype,
|
| 64 |
+
tp_rank: int,
|
| 65 |
+
tp_size: int,
|
| 66 |
+
bootstrap_port: int,
|
| 67 |
+
gloo_group: ProcessGroup,
|
| 68 |
+
transfer_backend: TransferBackend,
|
| 69 |
+
scheduler: Scheduler,
|
| 70 |
+
):
|
| 71 |
+
self.token_to_kv_pool = token_to_kv_pool
|
| 72 |
+
self.aux_dtype = aux_dtype
|
| 73 |
+
|
| 74 |
+
self.metadata_buffers = metadata_buffers
|
| 75 |
+
self.req_to_metadata_buffer_idx_allocator = req_to_metadata_buffer_idx_allocator
|
| 76 |
+
self.tp_rank = tp_rank
|
| 77 |
+
self.tp_size = tp_size
|
| 78 |
+
self.transfer_backend = transfer_backend
|
| 79 |
+
self.scheduler = scheduler
|
| 80 |
+
self.kv_manager = self._init_kv_manager()
|
| 81 |
+
self.queue: List[Req] = []
|
| 82 |
+
self.gloo_group = gloo_group
|
| 83 |
+
self.bootstrap_port = bootstrap_port
|
| 84 |
+
|
| 85 |
+
def store_prefill_results(self, idx: int, token_id: int):
|
| 86 |
+
assert token_id >= 0, f"token_id: {token_id} is negative"
|
| 87 |
+
output_id_buffer = self.metadata_buffers[0]
|
| 88 |
+
output_id_buffer[idx] = token_id
|
| 89 |
+
|
| 90 |
+
def _init_kv_manager(self) -> BaseKVManager:
|
| 91 |
+
kv_args = KVArgs()
|
| 92 |
+
kv_args.engine_rank = self.tp_rank
|
| 93 |
+
kv_data_ptrs, kv_data_lens, kv_item_lens = (
|
| 94 |
+
self.token_to_kv_pool.get_contiguous_buf_infos()
|
| 95 |
+
)
|
| 96 |
+
|
| 97 |
+
kv_args.kv_data_ptrs = kv_data_ptrs
|
| 98 |
+
kv_args.kv_data_lens = kv_data_lens
|
| 99 |
+
kv_args.kv_item_lens = kv_item_lens
|
| 100 |
+
|
| 101 |
+
# Define req -> input ids buffer
|
| 102 |
+
kv_args.aux_data_ptrs = [
|
| 103 |
+
metadata_buffer.data_ptr() for metadata_buffer in self.metadata_buffers
|
| 104 |
+
]
|
| 105 |
+
kv_args.aux_data_lens = [
|
| 106 |
+
metadata_buffer.nbytes for metadata_buffer in self.metadata_buffers
|
| 107 |
+
]
|
| 108 |
+
kv_args.aux_item_lens = [
|
| 109 |
+
metadata_buffer[0].nbytes for metadata_buffer in self.metadata_buffers
|
| 110 |
+
]
|
| 111 |
+
kv_args.ib_device = self.scheduler.server_args.disaggregation_ib_device
|
| 112 |
+
kv_args.gpu_id = self.scheduler.gpu_id
|
| 113 |
+
kv_manager_class = get_kv_class(self.transfer_backend, KVClassType.MANAGER)
|
| 114 |
+
kv_manager = kv_manager_class(
|
| 115 |
+
kv_args, DisaggregationMode.PREFILL, self.scheduler.server_args
|
| 116 |
+
)
|
| 117 |
+
return kv_manager
|
| 118 |
+
|
| 119 |
+
def add(self, req: Req) -> None:
|
| 120 |
+
if req.bootstrap_host == FakeBootstrapHost:
|
| 121 |
+
# Fake transfer for warmup reqs
|
| 122 |
+
kv_sender_class = get_kv_class(TransferBackend.FAKE, KVClassType.SENDER)
|
| 123 |
+
else:
|
| 124 |
+
kv_sender_class = get_kv_class(self.transfer_backend, KVClassType.SENDER)
|
| 125 |
+
req.disagg_kv_sender = kv_sender_class(
|
| 126 |
+
mgr=self.kv_manager,
|
| 127 |
+
bootstrap_addr=f"{req.bootstrap_host}:{self.bootstrap_port}",
|
| 128 |
+
bootstrap_room=req.bootstrap_room,
|
| 129 |
+
)
|
| 130 |
+
self._process_req(req)
|
| 131 |
+
self.queue.append(req)
|
| 132 |
+
|
| 133 |
+
def _process_req(self, req: Req) -> None:
|
| 134 |
+
"""
|
| 135 |
+
Set max_new_tokens = 1, so PrefillAdder memory estimation is accurate
|
| 136 |
+
"""
|
| 137 |
+
req.sampling_params.max_new_tokens = 1
|
| 138 |
+
|
| 139 |
+
def pop_bootstrapped(self) -> List[Req]:
|
| 140 |
+
"""pop the reqs which has finished bootstrapping"""
|
| 141 |
+
bootstrapped_reqs = []
|
| 142 |
+
indices_to_remove = set()
|
| 143 |
+
|
| 144 |
+
if len(self.queue) == 0:
|
| 145 |
+
return []
|
| 146 |
+
|
| 147 |
+
polls = poll_and_all_reduce(
|
| 148 |
+
[req.disagg_kv_sender for req in self.queue], self.gloo_group
|
| 149 |
+
)
|
| 150 |
+
|
| 151 |
+
for i, (req, poll) in enumerate(zip(self.queue, polls)):
|
| 152 |
+
if poll == KVPoll.Bootstrapping:
|
| 153 |
+
continue
|
| 154 |
+
elif poll == KVPoll.Failed:
|
| 155 |
+
raise Exception("Bootstrap failed")
|
| 156 |
+
|
| 157 |
+
# KV.WaitingForInput
|
| 158 |
+
num_kv_indices = len(req.origin_input_ids)
|
| 159 |
+
if self.req_to_metadata_buffer_idx_allocator.available_size() == 0:
|
| 160 |
+
break
|
| 161 |
+
|
| 162 |
+
req.metadata_buffer_index = (
|
| 163 |
+
self.req_to_metadata_buffer_idx_allocator.alloc()
|
| 164 |
+
)
|
| 165 |
+
assert req.metadata_buffer_index is not None
|
| 166 |
+
num_pages = kv_to_page_num(num_kv_indices, self.token_to_kv_pool.page_size)
|
| 167 |
+
req.disagg_kv_sender.init(num_pages, req.metadata_buffer_index)
|
| 168 |
+
|
| 169 |
+
bootstrapped_reqs.append(req)
|
| 170 |
+
indices_to_remove.add(i)
|
| 171 |
+
|
| 172 |
+
self.queue = [
|
| 173 |
+
entry for i, entry in enumerate(self.queue) if i not in indices_to_remove
|
| 174 |
+
]
|
| 175 |
+
|
| 176 |
+
return bootstrapped_reqs
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
class SchedulerDisaggregationPrefillMixin:
|
| 180 |
+
"""
|
| 181 |
+
Mixin for Scheduler to handle disaggregation prefill
|
| 182 |
+
"""
|
| 183 |
+
|
| 184 |
+
@torch.no_grad()
|
| 185 |
+
def event_loop_normal_disagg_prefill(self: Scheduler):
|
| 186 |
+
"""A normal scheduler loop for prefill worker in disaggregation mode."""
|
| 187 |
+
|
| 188 |
+
while True:
|
| 189 |
+
recv_reqs = self.recv_requests()
|
| 190 |
+
self.process_input_requests(recv_reqs)
|
| 191 |
+
self.waiting_queue.extend(
|
| 192 |
+
self.disagg_prefill_bootstrap_queue.pop_bootstrapped()
|
| 193 |
+
)
|
| 194 |
+
self.process_prefill_chunk()
|
| 195 |
+
batch = self.get_new_batch_prefill()
|
| 196 |
+
|
| 197 |
+
# Handle DP attention
|
| 198 |
+
if (
|
| 199 |
+
self.server_args.enable_dp_attention
|
| 200 |
+
or self.server_args.enable_sp_layernorm
|
| 201 |
+
):
|
| 202 |
+
batch, _ = self.prepare_dp_attn_batch(batch)
|
| 203 |
+
|
| 204 |
+
self.cur_batch = batch
|
| 205 |
+
|
| 206 |
+
if batch:
|
| 207 |
+
result = self.run_batch(batch)
|
| 208 |
+
self.process_batch_result_disagg_prefill(batch, result)
|
| 209 |
+
|
| 210 |
+
if len(self.disagg_prefill_inflight_queue) > 0:
|
| 211 |
+
self.process_disagg_prefill_inflight_queue()
|
| 212 |
+
|
| 213 |
+
if batch is None and len(self.disagg_prefill_inflight_queue) == 0:
|
| 214 |
+
self.check_memory()
|
| 215 |
+
self.new_token_ratio = self.init_new_token_ratio
|
| 216 |
+
|
| 217 |
+
self.last_batch = batch
|
| 218 |
+
# HACK (byronhsu): reset the batch_is_full flag because we never enter update_running_batch which resets it
|
| 219 |
+
# Otherwise, it hangs under high concurrency
|
| 220 |
+
self.running_batch.batch_is_full = False
|
| 221 |
+
|
| 222 |
+
@torch.no_grad()
|
| 223 |
+
def event_loop_overlap_disagg_prefill(self: Scheduler):
|
| 224 |
+
self.result_queue = deque()
|
| 225 |
+
|
| 226 |
+
while True:
|
| 227 |
+
recv_reqs = self.recv_requests()
|
| 228 |
+
self.process_input_requests(recv_reqs)
|
| 229 |
+
self.waiting_queue.extend(
|
| 230 |
+
self.disagg_prefill_bootstrap_queue.pop_bootstrapped()
|
| 231 |
+
)
|
| 232 |
+
self.process_prefill_chunk()
|
| 233 |
+
batch = self.get_new_batch_prefill()
|
| 234 |
+
|
| 235 |
+
# Handle DP attention
|
| 236 |
+
if (
|
| 237 |
+
self.server_args.enable_dp_attention
|
| 238 |
+
or self.server_args.enable_sp_layernorm
|
| 239 |
+
):
|
| 240 |
+
batch, _ = self.prepare_dp_attn_batch(batch)
|
| 241 |
+
|
| 242 |
+
self.cur_batch = batch
|
| 243 |
+
|
| 244 |
+
if batch:
|
| 245 |
+
result = self.run_batch(batch)
|
| 246 |
+
self.result_queue.append((batch.copy(), result))
|
| 247 |
+
|
| 248 |
+
if self.last_batch:
|
| 249 |
+
tmp_batch, tmp_result = self.result_queue.popleft()
|
| 250 |
+
self.process_batch_result_disagg_prefill(tmp_batch, tmp_result)
|
| 251 |
+
|
| 252 |
+
if len(self.disagg_prefill_inflight_queue) > 0:
|
| 253 |
+
self.process_disagg_prefill_inflight_queue()
|
| 254 |
+
|
| 255 |
+
if batch is None and len(self.disagg_prefill_inflight_queue) == 0:
|
| 256 |
+
self.check_memory()
|
| 257 |
+
self.new_token_ratio = self.init_new_token_ratio
|
| 258 |
+
|
| 259 |
+
self.last_batch = batch
|
| 260 |
+
# HACK (byronhsu): reset the batch_is_full flag because we never enter update_running_batch which resets it
|
| 261 |
+
# Otherwise, it hangs under high concurrency
|
| 262 |
+
self.running_batch.batch_is_full = False
|
| 263 |
+
|
| 264 |
+
def process_batch_result_disagg_prefill(
|
| 265 |
+
self: Scheduler,
|
| 266 |
+
batch: ScheduleBatch,
|
| 267 |
+
result: GenerationBatchResult,
|
| 268 |
+
launch_done: Optional[threading.Event] = None,
|
| 269 |
+
) -> None:
|
| 270 |
+
"""
|
| 271 |
+
Transfer kv for prefill completed requests and add it into disagg_prefill_inflight_queue
|
| 272 |
+
Adapted from process_batch_result_prefill
|
| 273 |
+
"""
|
| 274 |
+
|
| 275 |
+
(
|
| 276 |
+
logits_output,
|
| 277 |
+
next_token_ids,
|
| 278 |
+
extend_input_len_per_req,
|
| 279 |
+
extend_logprob_start_len_per_req,
|
| 280 |
+
bid,
|
| 281 |
+
) = (
|
| 282 |
+
result.logits_output,
|
| 283 |
+
result.next_token_ids,
|
| 284 |
+
result.extend_input_len_per_req,
|
| 285 |
+
result.extend_logprob_start_len_per_req,
|
| 286 |
+
result.bid,
|
| 287 |
+
)
|
| 288 |
+
|
| 289 |
+
# Transfer kv for prefill completed requests and add it into disagg_prefill_infight_queue
|
| 290 |
+
if self.enable_overlap:
|
| 291 |
+
# wait
|
| 292 |
+
_, next_token_ids = self.tp_worker.resolve_last_batch_result(launch_done)
|
| 293 |
+
else:
|
| 294 |
+
next_token_ids = result.next_token_ids.tolist()
|
| 295 |
+
|
| 296 |
+
for req, next_token_id in zip(batch.reqs, next_token_ids, strict=True):
|
| 297 |
+
req: Req
|
| 298 |
+
if req.is_chunked <= 0:
|
| 299 |
+
# There is no output_ids for prefill
|
| 300 |
+
req.output_ids.append(next_token_id)
|
| 301 |
+
self.tree_cache.cache_unfinished_req(req) # update the tree and lock
|
| 302 |
+
self.send_kv_chunk(req, token_id=next_token_id)
|
| 303 |
+
self.disagg_prefill_inflight_queue.append(req)
|
| 304 |
+
else:
|
| 305 |
+
# being chunked reqs' prefill is not finished
|
| 306 |
+
req.is_chunked -= 1
|
| 307 |
+
|
| 308 |
+
if self.enable_overlap:
|
| 309 |
+
self.send_kv_chunk(req, end_idx=req.tmp_end_idx)
|
| 310 |
+
|
| 311 |
+
def process_disagg_prefill_inflight_queue(self: Scheduler) -> None:
|
| 312 |
+
"""
|
| 313 |
+
Poll the requests in the middle of transfer. If done, return the request.
|
| 314 |
+
"""
|
| 315 |
+
assert len(self.disagg_prefill_inflight_queue) > 0
|
| 316 |
+
|
| 317 |
+
done_reqs = []
|
| 318 |
+
|
| 319 |
+
polls = poll_and_all_reduce(
|
| 320 |
+
[req.disagg_kv_sender for req in self.disagg_prefill_inflight_queue],
|
| 321 |
+
self.attn_tp_cpu_group,
|
| 322 |
+
)
|
| 323 |
+
|
| 324 |
+
undone_reqs: List[Req] = []
|
| 325 |
+
# Check .poll() for the reqs in disagg_prefill_inflight_queue. If Success, respond to the client and remove it from the queue
|
| 326 |
+
for req, poll in zip(self.disagg_prefill_inflight_queue, polls):
|
| 327 |
+
if poll in [KVPoll.WaitingForInput, KVPoll.Transferring]:
|
| 328 |
+
undone_reqs.append(req)
|
| 329 |
+
elif poll == KVPoll.Success: # transfer done
|
| 330 |
+
self.tree_cache.cache_finished_req(req) # unlock the tree
|
| 331 |
+
req.finished_reason = FINISH_LENGTH(length=0)
|
| 332 |
+
# FIXME: clean up req's data in transfer engine
|
| 333 |
+
done_reqs.append(req)
|
| 334 |
+
elif poll == KVPoll.Failed:
|
| 335 |
+
raise Exception("Transferring failed")
|
| 336 |
+
|
| 337 |
+
for req in done_reqs:
|
| 338 |
+
self.disagg_prefill_bootstrap_queue.req_to_metadata_buffer_idx_allocator.free(
|
| 339 |
+
req.metadata_buffer_index
|
| 340 |
+
)
|
| 341 |
+
|
| 342 |
+
# Stream requests which have finished transfer
|
| 343 |
+
self.stream_output(done_reqs, False, None)
|
| 344 |
+
|
| 345 |
+
self.disagg_prefill_inflight_queue = undone_reqs
|
| 346 |
+
|
| 347 |
+
def process_prefill_chunk(self: Scheduler) -> None:
|
| 348 |
+
if self.last_batch and self.last_batch.forward_mode.is_extend():
|
| 349 |
+
if self.chunked_req:
|
| 350 |
+
# Move the chunked request out of the batch so that we can merge
|
| 351 |
+
# only finished requests to running_batch.
|
| 352 |
+
self.last_batch.filter_batch(chunked_req_to_exclude=self.chunked_req)
|
| 353 |
+
self.tree_cache.cache_unfinished_req(self.chunked_req)
|
| 354 |
+
if self.enable_overlap:
|
| 355 |
+
# Delay KV transfer to process_batch_result_disagg_prefill when overlap is enabled to ensure results are resolved
|
| 356 |
+
self.chunked_req.tmp_end_idx = min(
|
| 357 |
+
len(self.chunked_req.fill_ids),
|
| 358 |
+
len(self.chunked_req.origin_input_ids),
|
| 359 |
+
)
|
| 360 |
+
else:
|
| 361 |
+
self.send_kv_chunk(self.chunked_req)
|
| 362 |
+
# chunked request keeps its rid but will get a new req_pool_idx
|
| 363 |
+
self.req_to_token_pool.free(self.chunked_req.req_pool_idx)
|
| 364 |
+
self.running_batch.batch_is_full = False
|
| 365 |
+
|
| 366 |
+
def send_kv_chunk(
|
| 367 |
+
self: Scheduler,
|
| 368 |
+
req: Req,
|
| 369 |
+
token_id: Optional[int] = None,
|
| 370 |
+
end_idx: Optional[int] = None,
|
| 371 |
+
) -> None:
|
| 372 |
+
"""
|
| 373 |
+
Send a prefilled chunk to the decode server
|
| 374 |
+
"""
|
| 375 |
+
page_size = self.token_to_kv_pool_allocator.page_size
|
| 376 |
+
start_idx = req.start_send_idx
|
| 377 |
+
# if end_idx is specified, use it as the end index of the kv chunk because in overlap schedule,
|
| 378 |
+
# the resolved length is not the same as fill_ids's length
|
| 379 |
+
end_idx = (
|
| 380 |
+
end_idx
|
| 381 |
+
if end_idx is not None
|
| 382 |
+
else min(len(req.fill_ids), len(req.origin_input_ids))
|
| 383 |
+
)
|
| 384 |
+
last_chunk = token_id is not None
|
| 385 |
+
|
| 386 |
+
if (not last_chunk) and (
|
| 387 |
+
end_idx % page_size != 0
|
| 388 |
+
): # todo: remove the second condition
|
| 389 |
+
# if not the last chunk and the last page is partial, delay the last partial page to the next send
|
| 390 |
+
end_idx = end_idx - end_idx % page_size
|
| 391 |
+
|
| 392 |
+
# Update next start_send_idx
|
| 393 |
+
req.start_send_idx = end_idx
|
| 394 |
+
|
| 395 |
+
kv_indices = (
|
| 396 |
+
self.req_to_token_pool.req_to_token[req.req_pool_idx, start_idx:end_idx]
|
| 397 |
+
.cpu()
|
| 398 |
+
.numpy()
|
| 399 |
+
)
|
| 400 |
+
if last_chunk is True:
|
| 401 |
+
self.disagg_prefill_bootstrap_queue.store_prefill_results(
|
| 402 |
+
req.metadata_buffer_index, token_id
|
| 403 |
+
)
|
| 404 |
+
page_indices = kv_to_page_indices(kv_indices, page_size)
|
| 405 |
+
|
| 406 |
+
page_start_idx = start_idx // page_size
|
| 407 |
+
page_end_idx = page_start_idx + len(page_indices)
|
| 408 |
+
|
| 409 |
+
if len(page_indices) == 0:
|
| 410 |
+
logger.info(
|
| 411 |
+
f"Skip sending kv chunk for request {req.rid=} {req.bootstrap_room=} because page_indices is empty"
|
| 412 |
+
)
|
| 413 |
+
return
|
| 414 |
+
|
| 415 |
+
req.disagg_kv_sender.send(
|
| 416 |
+
page_indices, slice(page_start_idx, page_end_idx), last_chunk
|
| 417 |
+
)
|
lib/python3.12/site-packages/sglang/srt/disaggregation/utils.py
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from collections import deque
|
| 4 |
+
from enum import Enum
|
| 5 |
+
from typing import List
|
| 6 |
+
|
| 7 |
+
import numpy as np
|
| 8 |
+
import torch
|
| 9 |
+
import torch.distributed as dist
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class DisaggregationMode(Enum):
|
| 13 |
+
NULL = "null"
|
| 14 |
+
PREFILL = "prefill"
|
| 15 |
+
DECODE = "decode"
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
FakeBootstrapHost = "2.2.2.2"
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def poll_and_all_reduce(pollers, gloo_group):
|
| 22 |
+
polls = [int(poller.poll()) for poller in pollers]
|
| 23 |
+
tensor_to_reduce = torch.tensor(polls, dtype=torch.uint8, device="cpu")
|
| 24 |
+
dist.all_reduce(tensor_to_reduce, op=dist.ReduceOp.MIN, group=gloo_group)
|
| 25 |
+
return tensor_to_reduce.tolist()
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
class ReqToMetadataIdxAllocator:
|
| 29 |
+
"""A memory pool that maps a request to its first output token location."""
|
| 30 |
+
|
| 31 |
+
def __init__(
|
| 32 |
+
self,
|
| 33 |
+
size: int,
|
| 34 |
+
):
|
| 35 |
+
self.size = size
|
| 36 |
+
self.free_slots = deque(list(range(size)))
|
| 37 |
+
|
| 38 |
+
def available_size(self):
|
| 39 |
+
return len(self.free_slots)
|
| 40 |
+
|
| 41 |
+
def alloc(self) -> List[int]:
|
| 42 |
+
if len(self.free_slots) == 0:
|
| 43 |
+
return None
|
| 44 |
+
|
| 45 |
+
return self.free_slots.popleft()
|
| 46 |
+
|
| 47 |
+
def free(self, free_index: int):
|
| 48 |
+
self.free_slots.append(free_index)
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
class TransferBackend(Enum):
|
| 52 |
+
MOONCAKE = "mooncake"
|
| 53 |
+
NIXL = "nixl"
|
| 54 |
+
FAKE = "fake"
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
class KVClassType(Enum):
|
| 58 |
+
MANAGER = "manager"
|
| 59 |
+
SENDER = "sender"
|
| 60 |
+
RECEIVER = "receiver"
|
| 61 |
+
BOOTSTRAP_SERVER = "bootstrap_server"
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def get_kv_class(transfer_backend: TransferBackend, class_type: KVClassType):
|
| 65 |
+
from sglang.srt.disaggregation.fake import FakeKVReceiver, FakeKVSender
|
| 66 |
+
|
| 67 |
+
if transfer_backend == TransferBackend.MOONCAKE:
|
| 68 |
+
from sglang.srt.disaggregation.mooncake import (
|
| 69 |
+
MooncakeKVBootstrapServer,
|
| 70 |
+
MooncakeKVManager,
|
| 71 |
+
MooncakeKVReceiver,
|
| 72 |
+
MooncakeKVSender,
|
| 73 |
+
)
|
| 74 |
+
|
| 75 |
+
class_mapping = {
|
| 76 |
+
KVClassType.MANAGER: MooncakeKVManager,
|
| 77 |
+
KVClassType.SENDER: MooncakeKVSender,
|
| 78 |
+
KVClassType.RECEIVER: (MooncakeKVReceiver),
|
| 79 |
+
KVClassType.BOOTSTRAP_SERVER: MooncakeKVBootstrapServer,
|
| 80 |
+
}
|
| 81 |
+
return class_mapping.get(class_type)
|
| 82 |
+
if transfer_backend == TransferBackend.NIXL:
|
| 83 |
+
from sglang.srt.disaggregation.nixl import (
|
| 84 |
+
NixlKVBootstrapServer,
|
| 85 |
+
NixlKVManager,
|
| 86 |
+
NixlKVReceiver,
|
| 87 |
+
NixlKVSender,
|
| 88 |
+
)
|
| 89 |
+
|
| 90 |
+
class_mapping = {
|
| 91 |
+
KVClassType.MANAGER: NixlKVManager,
|
| 92 |
+
KVClassType.SENDER: NixlKVSender,
|
| 93 |
+
KVClassType.RECEIVER: (NixlKVReceiver),
|
| 94 |
+
KVClassType.BOOTSTRAP_SERVER: NixlKVBootstrapServer,
|
| 95 |
+
}
|
| 96 |
+
return class_mapping.get(class_type)
|
| 97 |
+
if transfer_backend == TransferBackend.FAKE:
|
| 98 |
+
from sglang.srt.disaggregation.fake import FakeKVReceiver, FakeKVSender
|
| 99 |
+
|
| 100 |
+
class_mapping = {
|
| 101 |
+
KVClassType.SENDER: FakeKVSender,
|
| 102 |
+
KVClassType.RECEIVER: (FakeKVReceiver),
|
| 103 |
+
}
|
| 104 |
+
return class_mapping.get(class_type)
|
| 105 |
+
|
| 106 |
+
raise ValueError(f"Unsupported transfer backend: {transfer_backend}")
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
def kv_to_page_indices(kv_indices: np.ndarray, page_size: int):
|
| 110 |
+
# 1. The page is guaruanteed to be full except the last page.
|
| 111 |
+
# 2. page index = kv_index // page_size
|
| 112 |
+
# The return vector is kv_indices[::page_size] // page_size
|
| 113 |
+
if page_size == 1: # shortcut
|
| 114 |
+
return kv_indices
|
| 115 |
+
|
| 116 |
+
return kv_indices[::page_size] // page_size
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
def kv_to_page_num(num_kv_indices: int, page_size: int):
|
| 120 |
+
# ceil(num_kv_indices / page_size)
|
| 121 |
+
return (num_kv_indices + page_size - 1) // page_size
|
lib/python3.12/site-packages/sglang/srt/function_call_parser.py
ADDED
|
@@ -0,0 +1,761 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import logging
|
| 3 |
+
import re
|
| 4 |
+
from abc import ABC, abstractmethod
|
| 5 |
+
from dataclasses import dataclass
|
| 6 |
+
from json import JSONDecodeError, JSONDecoder
|
| 7 |
+
from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Type
|
| 8 |
+
|
| 9 |
+
import partial_json_parser
|
| 10 |
+
from partial_json_parser.core.exceptions import MalformedJSON
|
| 11 |
+
from partial_json_parser.core.options import Allow
|
| 12 |
+
from pydantic import BaseModel
|
| 13 |
+
|
| 14 |
+
from sglang.srt.openai_api.protocol import (
|
| 15 |
+
StructuralTagResponseFormat,
|
| 16 |
+
StructuresResponseFormat,
|
| 17 |
+
Tool,
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
logger = logging.getLogger(__name__)
|
| 21 |
+
|
| 22 |
+
TOOLS_TAG_LIST = [
|
| 23 |
+
"<|plugin|>",
|
| 24 |
+
"<function=",
|
| 25 |
+
"<tool_call>",
|
| 26 |
+
"<|python_tag|>",
|
| 27 |
+
"[TOOL_CALLS]",
|
| 28 |
+
"<|tool▁calls▁begin|>",
|
| 29 |
+
]
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
class ToolCallItem(BaseModel):
|
| 33 |
+
"""Simple encapsulation of the parsed ToolCall result for easier usage in streaming contexts."""
|
| 34 |
+
|
| 35 |
+
tool_index: int
|
| 36 |
+
name: Optional[str] = None
|
| 37 |
+
parameters: str # JSON string
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def _find_common_prefix(s1: str, s2: str) -> str:
|
| 41 |
+
prefix = ""
|
| 42 |
+
min_length = min(len(s1), len(s2))
|
| 43 |
+
for i in range(0, min_length):
|
| 44 |
+
if s1[i] == s2[i]:
|
| 45 |
+
prefix += s1[i]
|
| 46 |
+
else:
|
| 47 |
+
break
|
| 48 |
+
return prefix
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def _partial_json_loads(input_str: str, flags: Allow) -> Tuple[Any, int]:
|
| 52 |
+
try:
|
| 53 |
+
return (partial_json_parser.loads(input_str, flags), len(input_str))
|
| 54 |
+
except JSONDecodeError as e:
|
| 55 |
+
if "Extra data" in e.msg:
|
| 56 |
+
dec = JSONDecoder()
|
| 57 |
+
return dec.raw_decode(input_str)
|
| 58 |
+
raise
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def _is_complete_json(input_str: str) -> bool:
|
| 62 |
+
try:
|
| 63 |
+
json.loads(input_str)
|
| 64 |
+
return True
|
| 65 |
+
except JSONDecodeError:
|
| 66 |
+
return False
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
class StreamingParseResult:
|
| 70 |
+
"""Result of streaming incremental parsing."""
|
| 71 |
+
|
| 72 |
+
def __init__(
|
| 73 |
+
self, normal_text: str = "", calls: Optional[List[ToolCallItem]] = None
|
| 74 |
+
):
|
| 75 |
+
self.normal_text = normal_text
|
| 76 |
+
self.calls = calls or []
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
@dataclass
|
| 80 |
+
class StructureInfo:
|
| 81 |
+
begin: str
|
| 82 |
+
end: str
|
| 83 |
+
trigger: str
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
_GetInfoFunc = Callable[[str], StructureInfo]
|
| 87 |
+
"""
|
| 88 |
+
helper alias of function
|
| 89 |
+
ususally it is a function that takes a name string and returns a StructureInfo object,
|
| 90 |
+
which can be used to construct a structural_tag object
|
| 91 |
+
"""
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
class BaseFormatDetector(ABC):
|
| 95 |
+
"""Base class providing two sets of interfaces: one-time and streaming incremental."""
|
| 96 |
+
|
| 97 |
+
def __init__(self):
|
| 98 |
+
# initialize properties used for state when parsing tool calls in
|
| 99 |
+
self._buffer = ""
|
| 100 |
+
# streaming mode
|
| 101 |
+
self.prev_tool_call_arr: List[Dict] = []
|
| 102 |
+
self.current_tool_id: int = -1
|
| 103 |
+
self.current_tool_name_sent: bool = False
|
| 104 |
+
self.streamed_args_for_tool: List[str] = (
|
| 105 |
+
[]
|
| 106 |
+
) # map what has been streamed for each tool so far to a list
|
| 107 |
+
self.bot_token = ""
|
| 108 |
+
self.eot_token = ""
|
| 109 |
+
|
| 110 |
+
def parse_base_json(self, action: Any, tools: List[Tool]) -> List[ToolCallItem]:
|
| 111 |
+
tool_indices = {
|
| 112 |
+
tool.function.name: i for i, tool in enumerate(tools) if tool.function.name
|
| 113 |
+
}
|
| 114 |
+
if not isinstance(action, list):
|
| 115 |
+
action = [action]
|
| 116 |
+
|
| 117 |
+
results = []
|
| 118 |
+
for act in action:
|
| 119 |
+
name = act.get("name")
|
| 120 |
+
if name and name in tool_indices:
|
| 121 |
+
results.append(
|
| 122 |
+
ToolCallItem(
|
| 123 |
+
tool_index=tool_indices[name],
|
| 124 |
+
name=name,
|
| 125 |
+
parameters=json.dumps(
|
| 126 |
+
act.get("parameters") or act.get("arguments", {}),
|
| 127 |
+
ensure_ascii=False,
|
| 128 |
+
),
|
| 129 |
+
)
|
| 130 |
+
)
|
| 131 |
+
else:
|
| 132 |
+
logger.warning(f"Model attempted to call undefined function: {name}")
|
| 133 |
+
|
| 134 |
+
return results
|
| 135 |
+
|
| 136 |
+
@abstractmethod
|
| 137 |
+
def detect_and_parse(self, text: str, tools: List[Tool]) -> StreamingParseResult:
|
| 138 |
+
"""
|
| 139 |
+
Parses the text in one go. Returns success=True if the format matches, otherwise False.
|
| 140 |
+
Note that leftover_text here represents "content that this parser will not consume further".
|
| 141 |
+
"""
|
| 142 |
+
action = json.loads(text)
|
| 143 |
+
return StreamingParseResult(calls=self.parse_base_json(action, tools))
|
| 144 |
+
|
| 145 |
+
def parse_streaming_increment(
|
| 146 |
+
self, new_text: str, tools: List[Tool]
|
| 147 |
+
) -> StreamingParseResult:
|
| 148 |
+
"""
|
| 149 |
+
Streaming incremental parsing with tool validation.
|
| 150 |
+
"""
|
| 151 |
+
# Append new text to buffer
|
| 152 |
+
self._buffer += new_text
|
| 153 |
+
current_text = self._buffer
|
| 154 |
+
if not (self.bot_token in current_text or current_text.startswith("{")):
|
| 155 |
+
self._buffer = ""
|
| 156 |
+
if self.eot_token in new_text:
|
| 157 |
+
new_text = new_text.replace(self.eot_token, "")
|
| 158 |
+
return StreamingParseResult(normal_text=new_text)
|
| 159 |
+
|
| 160 |
+
# Build tool indices if not already built
|
| 161 |
+
if not hasattr(self, "_tool_indices"):
|
| 162 |
+
self._tool_indices = {
|
| 163 |
+
tool.function.name: i
|
| 164 |
+
for i, tool in enumerate(tools)
|
| 165 |
+
if tool.function and tool.function.name
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
flags = Allow.ALL if self.current_tool_name_sent else Allow.ALL & ~Allow.STR
|
| 169 |
+
try:
|
| 170 |
+
tool_call_arr = []
|
| 171 |
+
is_complete = []
|
| 172 |
+
try:
|
| 173 |
+
start_idx = (
|
| 174 |
+
len(self.bot_token)
|
| 175 |
+
if current_text.startswith(self.bot_token)
|
| 176 |
+
else 0
|
| 177 |
+
)
|
| 178 |
+
while start_idx < len(current_text):
|
| 179 |
+
(obj, end_idx) = _partial_json_loads(
|
| 180 |
+
current_text[start_idx:], flags
|
| 181 |
+
)
|
| 182 |
+
is_complete.append(
|
| 183 |
+
_is_complete_json(current_text[start_idx : start_idx + end_idx])
|
| 184 |
+
)
|
| 185 |
+
start_idx += end_idx + len("; ")
|
| 186 |
+
|
| 187 |
+
# Validate tool name if present
|
| 188 |
+
if "name" in obj and obj["name"] not in self._tool_indices:
|
| 189 |
+
# Invalid tool name - reset state
|
| 190 |
+
self._buffer = ""
|
| 191 |
+
self.current_tool_id = -1
|
| 192 |
+
self.current_tool_name_sent = False
|
| 193 |
+
if self.streamed_args_for_tool:
|
| 194 |
+
self.streamed_args_for_tool.pop()
|
| 195 |
+
return StreamingParseResult()
|
| 196 |
+
|
| 197 |
+
# Handle parameters/arguments consistency
|
| 198 |
+
if "parameters" in obj:
|
| 199 |
+
assert (
|
| 200 |
+
"arguments" not in obj
|
| 201 |
+
), "model generated both parameters and arguments"
|
| 202 |
+
obj["arguments"] = obj["parameters"]
|
| 203 |
+
tool_call_arr.append(obj)
|
| 204 |
+
|
| 205 |
+
except MalformedJSON:
|
| 206 |
+
return StreamingParseResult()
|
| 207 |
+
|
| 208 |
+
if len(tool_call_arr) == 0:
|
| 209 |
+
return StreamingParseResult()
|
| 210 |
+
|
| 211 |
+
current_tool_call: Dict = (
|
| 212 |
+
tool_call_arr[self.current_tool_id] if len(tool_call_arr) > 0 else {}
|
| 213 |
+
)
|
| 214 |
+
|
| 215 |
+
# Handle new tool in array
|
| 216 |
+
if len(tool_call_arr) > 0 and len(tool_call_arr) > self.current_tool_id + 1:
|
| 217 |
+
if self.current_tool_id >= 0:
|
| 218 |
+
cur_arguments = current_tool_call.get("arguments")
|
| 219 |
+
if cur_arguments:
|
| 220 |
+
cur_args_json = json.dumps(cur_arguments)
|
| 221 |
+
sent = len(self.streamed_args_for_tool[self.current_tool_id])
|
| 222 |
+
argument_diff = cur_args_json[sent:]
|
| 223 |
+
|
| 224 |
+
res = StreamingParseResult(
|
| 225 |
+
calls=[
|
| 226 |
+
ToolCallItem(
|
| 227 |
+
tool_index=self.current_tool_id,
|
| 228 |
+
name="",
|
| 229 |
+
parameters=argument_diff,
|
| 230 |
+
)
|
| 231 |
+
],
|
| 232 |
+
)
|
| 233 |
+
self.streamed_args_for_tool[
|
| 234 |
+
self.current_tool_id
|
| 235 |
+
] += argument_diff
|
| 236 |
+
else:
|
| 237 |
+
res = StreamingParseResult()
|
| 238 |
+
else:
|
| 239 |
+
res = StreamingParseResult()
|
| 240 |
+
|
| 241 |
+
self.current_tool_id = len(tool_call_arr) - 1
|
| 242 |
+
self.current_tool_name_sent = False
|
| 243 |
+
self.streamed_args_for_tool.append("")
|
| 244 |
+
return res
|
| 245 |
+
|
| 246 |
+
# Handle tool name
|
| 247 |
+
elif not self.current_tool_name_sent:
|
| 248 |
+
function_name = current_tool_call.get("name")
|
| 249 |
+
if function_name and function_name in self._tool_indices:
|
| 250 |
+
res = StreamingParseResult(
|
| 251 |
+
calls=[
|
| 252 |
+
ToolCallItem(
|
| 253 |
+
tool_index=self._tool_indices[function_name],
|
| 254 |
+
name=function_name,
|
| 255 |
+
parameters="",
|
| 256 |
+
)
|
| 257 |
+
],
|
| 258 |
+
)
|
| 259 |
+
self.current_tool_name_sent = True
|
| 260 |
+
else:
|
| 261 |
+
res = StreamingParseResult()
|
| 262 |
+
|
| 263 |
+
# Handle streaming arguments
|
| 264 |
+
else:
|
| 265 |
+
cur_arguments = current_tool_call.get("arguments")
|
| 266 |
+
res = StreamingParseResult()
|
| 267 |
+
|
| 268 |
+
if cur_arguments:
|
| 269 |
+
sent = len(self.streamed_args_for_tool[self.current_tool_id])
|
| 270 |
+
cur_args_json = json.dumps(cur_arguments)
|
| 271 |
+
prev_arguments = self.prev_tool_call_arr[self.current_tool_id].get(
|
| 272 |
+
"arguments"
|
| 273 |
+
)
|
| 274 |
+
|
| 275 |
+
argument_diff = None
|
| 276 |
+
if is_complete[self.current_tool_id]:
|
| 277 |
+
argument_diff = cur_args_json[sent:]
|
| 278 |
+
self._buffer = ""
|
| 279 |
+
self.prev_tool_call_arr[self.current_tool_id].clear()
|
| 280 |
+
self.current_tool_name_sent = False
|
| 281 |
+
self.streamed_args_for_tool[self.current_tool_id] = ""
|
| 282 |
+
|
| 283 |
+
elif prev_arguments:
|
| 284 |
+
prev_args_json = json.dumps(prev_arguments)
|
| 285 |
+
if cur_args_json != prev_args_json:
|
| 286 |
+
prefix = _find_common_prefix(prev_args_json, cur_args_json)
|
| 287 |
+
argument_diff = prefix[sent:]
|
| 288 |
+
|
| 289 |
+
if argument_diff is not None:
|
| 290 |
+
res = StreamingParseResult(
|
| 291 |
+
calls=[
|
| 292 |
+
ToolCallItem(
|
| 293 |
+
tool_index=self.current_tool_id,
|
| 294 |
+
parameters=argument_diff,
|
| 295 |
+
)
|
| 296 |
+
],
|
| 297 |
+
)
|
| 298 |
+
if not is_complete[self.current_tool_id]:
|
| 299 |
+
self.streamed_args_for_tool[
|
| 300 |
+
self.current_tool_id
|
| 301 |
+
] += argument_diff
|
| 302 |
+
|
| 303 |
+
self.prev_tool_call_arr = tool_call_arr
|
| 304 |
+
return res
|
| 305 |
+
|
| 306 |
+
except Exception as e:
|
| 307 |
+
logger.error(f"Error in parse_streaming_increment: {e}")
|
| 308 |
+
return StreamingParseResult()
|
| 309 |
+
|
| 310 |
+
@abstractmethod
|
| 311 |
+
def has_tool_call(self, text: str) -> bool:
|
| 312 |
+
raise NotImplementedError()
|
| 313 |
+
|
| 314 |
+
@abstractmethod
|
| 315 |
+
def structure_info(self) -> _GetInfoFunc:
|
| 316 |
+
raise NotImplementedError()
|
| 317 |
+
|
| 318 |
+
|
| 319 |
+
class Qwen25Detector(BaseFormatDetector):
|
| 320 |
+
"""
|
| 321 |
+
Detector for Qwen 2.5 models.
|
| 322 |
+
Assumes function call format:
|
| 323 |
+
<tool_call>{"name":"xxx", "arguments":{...}}</tool_call>
|
| 324 |
+
"""
|
| 325 |
+
|
| 326 |
+
def __init__(self):
|
| 327 |
+
"""
|
| 328 |
+
Initializes the detector with necessary state variables.
|
| 329 |
+
"""
|
| 330 |
+
super().__init__()
|
| 331 |
+
self.bot_token = "<tool_call>"
|
| 332 |
+
self.eot_token = "</tool_call>"
|
| 333 |
+
|
| 334 |
+
def has_tool_call(self, text: str) -> bool:
|
| 335 |
+
"""Check if the text contains a Qwen 2.5 format tool call."""
|
| 336 |
+
return self.bot_token in text
|
| 337 |
+
|
| 338 |
+
def detect_and_parse(self, text: str, tools: List[Tool]) -> StreamingParseResult:
|
| 339 |
+
"""
|
| 340 |
+
One-time parsing: Detects and parses tool calls in the provided text.
|
| 341 |
+
|
| 342 |
+
:param text: The complete text to parse.
|
| 343 |
+
:param tools: List of available tools.
|
| 344 |
+
:return: ParseResult indicating success or failure, consumed text, leftover text, and parsed calls.
|
| 345 |
+
"""
|
| 346 |
+
idx = text.find(self.bot_token)
|
| 347 |
+
normal_text = text[:idx].strip() if idx != -1 else text
|
| 348 |
+
if self.bot_token not in text:
|
| 349 |
+
return StreamingParseResult(normal_text=normal_text, calls=[])
|
| 350 |
+
pattern = rf"{self.bot_token}(.*?){self.eot_token}"
|
| 351 |
+
match_result_list = re.findall(pattern, text, re.DOTALL)
|
| 352 |
+
calls = []
|
| 353 |
+
for match_result in match_result_list:
|
| 354 |
+
match_result = json.loads(match_result)
|
| 355 |
+
calls.extend(self.parse_base_json(match_result, tools))
|
| 356 |
+
return StreamingParseResult(normal_text=normal_text, calls=calls)
|
| 357 |
+
|
| 358 |
+
def structure_info(self) -> _GetInfoFunc:
|
| 359 |
+
return lambda name: StructureInfo(
|
| 360 |
+
begin='<tool_call>{"name":"' + name + '", "arguments":',
|
| 361 |
+
end="}</tool_call>",
|
| 362 |
+
trigger="<tool_call>",
|
| 363 |
+
)
|
| 364 |
+
|
| 365 |
+
|
| 366 |
+
class MistralDetector(BaseFormatDetector):
|
| 367 |
+
"""
|
| 368 |
+
Detector for Mistral models.
|
| 369 |
+
Assumes function call format:
|
| 370 |
+
<|action_start|><|plugin|>{"name":"xxx", "arguments":{...}}<|action_end|>
|
| 371 |
+
"""
|
| 372 |
+
|
| 373 |
+
def __init__(self):
|
| 374 |
+
"""
|
| 375 |
+
Initializes the detector with necessary state variables.
|
| 376 |
+
"""
|
| 377 |
+
super().__init__()
|
| 378 |
+
self.bot_token = "[TOOL_CALLS] ["
|
| 379 |
+
self.tool_call_regex = re.compile(r"\[{.*}\]", re.DOTALL)
|
| 380 |
+
|
| 381 |
+
def has_tool_call(self, text: str) -> bool:
|
| 382 |
+
"""Check if the text contains a Mistral format tool call."""
|
| 383 |
+
return self.bot_token in text
|
| 384 |
+
|
| 385 |
+
def _clean_text(self, text: str) -> str:
|
| 386 |
+
"""
|
| 387 |
+
clean text to only leave ''[TOOL_CALLS] [{"name": xxx, "arguments": {xxx}}]'
|
| 388 |
+
for example,
|
| 389 |
+
text = '[TOOL_CALLS] [{"name": "get_current_weather", "arguments": {"location": "Boston, MA", "unit": "fahrenheit"}}]\n\nToday\'s weather in Boston is :{function call result} (in Fahrenheit)\n\nIf you prefer Celsius, please let me know.'
|
| 390 |
+
return '[TOOL_CALLS] [{"name": "get_current_weather", "arguments": {"location": "Boston, MA", "unit": "fahrenheit"}}]'
|
| 391 |
+
The key pattern is [TOOL_CALLS] [...]
|
| 392 |
+
"""
|
| 393 |
+
find_results = re.findall(r"\[TOOL_CALLS\] \[.*?\]", text, re.DOTALL)
|
| 394 |
+
if len(find_results) > 0:
|
| 395 |
+
return find_results[0]
|
| 396 |
+
else:
|
| 397 |
+
return ""
|
| 398 |
+
|
| 399 |
+
def detect_and_parse(self, text: str, tools: List[Tool]) -> StreamingParseResult:
|
| 400 |
+
"""
|
| 401 |
+
One-time parsing: Detects and parses tool calls in the provided text.
|
| 402 |
+
|
| 403 |
+
:param text: The complete text to parse.
|
| 404 |
+
:param tools: List of available tools.
|
| 405 |
+
:return: ParseResult indicating success or failure, consumed text, leftover text, and parsed calls.
|
| 406 |
+
"""
|
| 407 |
+
idx = text.find(self.bot_token)
|
| 408 |
+
normal_text = text[:idx].strip() if idx != -1 else text
|
| 409 |
+
text = self._clean_text(text)
|
| 410 |
+
tool_content = text.replace("[TOOL_CALLS]", "").strip()
|
| 411 |
+
raw_tool_calls = self.tool_call_regex.findall(tool_content)
|
| 412 |
+
calls = []
|
| 413 |
+
if len(raw_tool_calls) > 0:
|
| 414 |
+
raw_tool_call = raw_tool_calls[0]
|
| 415 |
+
function_call_arr = json.loads(raw_tool_call)
|
| 416 |
+
for match_result in function_call_arr:
|
| 417 |
+
calls.extend(self.parse_base_json(match_result, tools))
|
| 418 |
+
return StreamingParseResult(normal_text=normal_text, calls=calls)
|
| 419 |
+
|
| 420 |
+
def structure_info(self) -> _GetInfoFunc:
|
| 421 |
+
return lambda name: StructureInfo(
|
| 422 |
+
begin='[TOOL_CALLS] [{"name":"' + name + '", "arguments":',
|
| 423 |
+
end="}]",
|
| 424 |
+
trigger="[TOOL_CALLS]",
|
| 425 |
+
)
|
| 426 |
+
|
| 427 |
+
|
| 428 |
+
class Llama32Detector(BaseFormatDetector):
|
| 429 |
+
"""
|
| 430 |
+
Detector for Llama 3.2 models.
|
| 431 |
+
Assumes function call format:
|
| 432 |
+
<|python_tag|>{"name":"xxx", "arguments":{...}}
|
| 433 |
+
"""
|
| 434 |
+
|
| 435 |
+
def __init__(self):
|
| 436 |
+
super().__init__()
|
| 437 |
+
self.bot_token = "<|python_tag|>"
|
| 438 |
+
|
| 439 |
+
def has_tool_call(self, text: str) -> bool:
|
| 440 |
+
"""Check if the text contains a Llama 3.2 format tool call."""
|
| 441 |
+
# depending on the prompt format the Llama model may or may not
|
| 442 |
+
# prefix the output with the <|python_tag|> token
|
| 443 |
+
return "<|python_tag|>" in text or text.startswith("{")
|
| 444 |
+
|
| 445 |
+
def detect_and_parse(self, text: str, tools: List[Tool]) -> StreamingParseResult:
|
| 446 |
+
"""Parse function calls from text, handling multiple JSON objects."""
|
| 447 |
+
if "<|python_tag|>" not in text and not text.startswith("{"):
|
| 448 |
+
return StreamingParseResult(normal_text=text, calls=[])
|
| 449 |
+
|
| 450 |
+
if "<|python_tag|>" in text:
|
| 451 |
+
normal_text, action_text = text.split("<|python_tag|>")
|
| 452 |
+
else:
|
| 453 |
+
normal_text, action_text = "", text
|
| 454 |
+
|
| 455 |
+
# Split by semicolon and process each part
|
| 456 |
+
json_parts = [part.strip() for part in action_text.split(";") if part.strip()]
|
| 457 |
+
all_actions = []
|
| 458 |
+
for part in json_parts:
|
| 459 |
+
try:
|
| 460 |
+
# Parse each individual JSON object
|
| 461 |
+
action = json.loads(part)
|
| 462 |
+
all_actions.append(action)
|
| 463 |
+
except json.JSONDecodeError as e:
|
| 464 |
+
logger.warning(f"Failed to parse JSON part: {part}")
|
| 465 |
+
logger.warning(f"JSON parse error: {str(e)}")
|
| 466 |
+
continue
|
| 467 |
+
calls = []
|
| 468 |
+
# Only process if we found valid JSON objects
|
| 469 |
+
if all_actions:
|
| 470 |
+
calls = self.parse_base_json(all_actions, tools)
|
| 471 |
+
return StreamingParseResult(normal_text=normal_text, calls=calls)
|
| 472 |
+
|
| 473 |
+
def structure_info(self) -> _GetInfoFunc:
|
| 474 |
+
return lambda name: StructureInfo(
|
| 475 |
+
begin='<|python_tag|>{"name":"' + name + '", "arguments":',
|
| 476 |
+
end="}",
|
| 477 |
+
trigger="<|python_tag|>",
|
| 478 |
+
)
|
| 479 |
+
|
| 480 |
+
|
| 481 |
+
class DeepSeekV3Detector(BaseFormatDetector):
|
| 482 |
+
"""
|
| 483 |
+
Detector for DeepSeek models.
|
| 484 |
+
Assumes function call format:
|
| 485 |
+
'<|tool▁calls▁begin|><|tool▁call▁begin|>function<|tool▁sep|>get_current_weather\n```json\n{"location": "Tokyo"}\n```<|tool▁call▁end|>\n<|tool▁call▁begin|>function<|tool▁sep|>get_current_weather\n```json\n{"location": "Paris"}\n```<|tool▁call▁end|><|tool▁calls▁end|><|end▁of▁sentence|>
|
| 486 |
+
"""
|
| 487 |
+
|
| 488 |
+
def __init__(self):
|
| 489 |
+
super().__init__()
|
| 490 |
+
self.bot_token = "<|tool▁calls▁begin|>"
|
| 491 |
+
self.eot_token = "<|tool▁calls▁end|>"
|
| 492 |
+
self.func_call_regex = r"<|tool▁call▁begin|>.*?<|tool▁call▁end|>"
|
| 493 |
+
self.func_detail_regex = r"<|tool▁call▁begin|>(.*)<|tool▁sep|>(.*)\n```json\n(.*)\n```<|tool▁call▁end|>"
|
| 494 |
+
self._last_arguments = ""
|
| 495 |
+
|
| 496 |
+
def has_tool_call(self, text: str) -> bool:
|
| 497 |
+
"""Check if the text contains a deepseek format tool call."""
|
| 498 |
+
return self.bot_token in text
|
| 499 |
+
|
| 500 |
+
def detect_and_parse(self, text: str, tools: List[Tool]) -> StreamingParseResult:
|
| 501 |
+
"""
|
| 502 |
+
One-time parsing: Detects and parses tool calls in the provided text.
|
| 503 |
+
|
| 504 |
+
:param text: The complete text to parse.
|
| 505 |
+
:param tools: List of available tools.
|
| 506 |
+
:return: ParseResult indicating success or failure, consumed text, leftover text, and parsed calls.
|
| 507 |
+
"""
|
| 508 |
+
idx = text.find(self.bot_token)
|
| 509 |
+
normal_text = text[:idx].strip() if idx != -1 else text
|
| 510 |
+
if self.bot_token not in text:
|
| 511 |
+
return StreamingParseResult(normal_text=normal_text, calls=[])
|
| 512 |
+
match_result_list = re.findall(self.func_call_regex, text, re.DOTALL)
|
| 513 |
+
calls = []
|
| 514 |
+
try:
|
| 515 |
+
for match_result in match_result_list:
|
| 516 |
+
# Get function name
|
| 517 |
+
func_detail = re.search(self.func_detail_regex, match_result, re.DOTALL)
|
| 518 |
+
func_name = func_detail.group(2)
|
| 519 |
+
func_args = func_detail.group(3)
|
| 520 |
+
func_args = json.loads(func_args)
|
| 521 |
+
# construct match_result for parse_base_json
|
| 522 |
+
match_result = {"name": func_name, "parameters": func_args}
|
| 523 |
+
calls.extend(self.parse_base_json(match_result, tools))
|
| 524 |
+
return StreamingParseResult(normal_text=normal_text, calls=calls)
|
| 525 |
+
except Exception as e:
|
| 526 |
+
logger.error(f"Error in detect_and_parse: {e}")
|
| 527 |
+
# return the normal text if parsing fails
|
| 528 |
+
return StreamingParseResult(normal_text=text)
|
| 529 |
+
|
| 530 |
+
def structure_info(self) -> _GetInfoFunc:
|
| 531 |
+
return lambda name: StructureInfo(
|
| 532 |
+
begin=">" + name + "\n```json\n",
|
| 533 |
+
end="\n```<",
|
| 534 |
+
trigger=">" + name + "\n```json\n",
|
| 535 |
+
)
|
| 536 |
+
|
| 537 |
+
def parse_streaming_increment(
|
| 538 |
+
self, new_text: str, tools: List[Tool]
|
| 539 |
+
) -> StreamingParseResult:
|
| 540 |
+
"""
|
| 541 |
+
Streaming incremental parsing tool calls for DeepSeekV3 format.
|
| 542 |
+
"""
|
| 543 |
+
self._buffer += new_text
|
| 544 |
+
current_text = self._buffer
|
| 545 |
+
|
| 546 |
+
if self.bot_token not in current_text:
|
| 547 |
+
self._buffer = ""
|
| 548 |
+
for e_token in [self.eot_token, "```", "<|tool▁call▁end|>"]:
|
| 549 |
+
if e_token in new_text:
|
| 550 |
+
new_text = new_text.replace(e_token, "")
|
| 551 |
+
return StreamingParseResult(normal_text=new_text)
|
| 552 |
+
|
| 553 |
+
if not hasattr(self, "_tool_indices"):
|
| 554 |
+
self._tool_indices = {
|
| 555 |
+
tool.function.name: i
|
| 556 |
+
for i, tool in enumerate(tools)
|
| 557 |
+
if tool.function and tool.function.name
|
| 558 |
+
}
|
| 559 |
+
|
| 560 |
+
calls: list[ToolCallItem] = []
|
| 561 |
+
try:
|
| 562 |
+
partial_match = re.search(
|
| 563 |
+
pattern=r"<|tool▁call▁begin|>(.*)<|tool▁sep|>(.*)\n```json\n(.*)",
|
| 564 |
+
string=current_text,
|
| 565 |
+
flags=re.DOTALL,
|
| 566 |
+
)
|
| 567 |
+
if partial_match:
|
| 568 |
+
func_name = partial_match.group(2).strip()
|
| 569 |
+
func_args_raw = partial_match.group(3).strip()
|
| 570 |
+
|
| 571 |
+
if not self.current_tool_name_sent:
|
| 572 |
+
calls.append(
|
| 573 |
+
ToolCallItem(
|
| 574 |
+
tool_index=self._tool_indices.get(func_name, 0),
|
| 575 |
+
name=func_name,
|
| 576 |
+
parameters="",
|
| 577 |
+
)
|
| 578 |
+
)
|
| 579 |
+
self.current_tool_name_sent = True
|
| 580 |
+
else:
|
| 581 |
+
argument_diff = (
|
| 582 |
+
func_args_raw[len(self._last_arguments) :]
|
| 583 |
+
if func_args_raw.startswith(self._last_arguments)
|
| 584 |
+
else func_args_raw
|
| 585 |
+
)
|
| 586 |
+
|
| 587 |
+
if argument_diff:
|
| 588 |
+
calls.append(
|
| 589 |
+
ToolCallItem(
|
| 590 |
+
tool_index=self._tool_indices.get(func_name, 0),
|
| 591 |
+
name=None,
|
| 592 |
+
parameters=argument_diff,
|
| 593 |
+
)
|
| 594 |
+
)
|
| 595 |
+
self._last_arguments += argument_diff
|
| 596 |
+
|
| 597 |
+
if _is_complete_json(func_args_raw):
|
| 598 |
+
result = StreamingParseResult(normal_text="", calls=calls)
|
| 599 |
+
self._buffer = ""
|
| 600 |
+
self._last_arguments = ""
|
| 601 |
+
self.current_tool_name_sent = False
|
| 602 |
+
return result
|
| 603 |
+
|
| 604 |
+
return StreamingParseResult(normal_text="", calls=calls)
|
| 605 |
+
|
| 606 |
+
except Exception as e:
|
| 607 |
+
logger.error(f"Error in parse_streaming_increment: {e}")
|
| 608 |
+
return StreamingParseResult(normal_text=current_text)
|
| 609 |
+
|
| 610 |
+
|
| 611 |
+
class MultiFormatParser:
|
| 612 |
+
def __init__(self, detectors: List[BaseFormatDetector]):
|
| 613 |
+
"""
|
| 614 |
+
:param detectors: A series of available Detector instances passed in
|
| 615 |
+
"""
|
| 616 |
+
self.detectors = detectors
|
| 617 |
+
|
| 618 |
+
def parse_once(
|
| 619 |
+
self, text: str, tools: List[Tool]
|
| 620 |
+
) -> Tuple[str, list[ToolCallItem]]:
|
| 621 |
+
"""
|
| 622 |
+
One-time parsing: Loop through detectors until there are no new matches or text is exhausted
|
| 623 |
+
Return: (final_text, all_calls)
|
| 624 |
+
- final_text: The remaining text after parsing that was not consumed by any Detector (can be treated as normal text)
|
| 625 |
+
- all_calls: All calls parsed by the Detectors
|
| 626 |
+
"""
|
| 627 |
+
final_calls = []
|
| 628 |
+
final_normal_text = text
|
| 629 |
+
for detector in self.detectors:
|
| 630 |
+
parsed_result = detector.detect_and_parse(text, tools)
|
| 631 |
+
tool_call_list = parsed_result.calls
|
| 632 |
+
if len(tool_call_list) > 0: # parsed successfully
|
| 633 |
+
final_calls = tool_call_list
|
| 634 |
+
final_normal_text = parsed_result.normal_text
|
| 635 |
+
break
|
| 636 |
+
|
| 637 |
+
# leftover_text is the normal text not consumed by any Detector
|
| 638 |
+
return final_normal_text, final_calls
|
| 639 |
+
|
| 640 |
+
def parse_streaming_increment(
|
| 641 |
+
self, new_text: str, tools: List[Tool]
|
| 642 |
+
) -> Tuple[str, list[ToolCallItem]]:
|
| 643 |
+
"""
|
| 644 |
+
Streaming incremental parsing: Feed new_text to each detector's parse_streaming_increment
|
| 645 |
+
and merge their produced normal_text/calls to return.
|
| 646 |
+
(The logic here can be "priority-based" or "parallel parsing" based on your needs)
|
| 647 |
+
"""
|
| 648 |
+
final_normal_text = ""
|
| 649 |
+
final_calls = []
|
| 650 |
+
|
| 651 |
+
for detector in self.detectors:
|
| 652 |
+
sp_result = detector.parse_streaming_increment(new_text, tools)
|
| 653 |
+
# Merge normal_text and calls
|
| 654 |
+
# If one sp_result contains result call, this should be a successful parse
|
| 655 |
+
# If one sp_result only contains normal_text, this can either be a successful
|
| 656 |
+
# parse or it is not using the desired parsing tool.
|
| 657 |
+
if sp_result.normal_text:
|
| 658 |
+
final_normal_text = sp_result.normal_text
|
| 659 |
+
if sp_result.calls:
|
| 660 |
+
final_calls.extend(sp_result.calls)
|
| 661 |
+
final_normal_text = sp_result.normal_text
|
| 662 |
+
break
|
| 663 |
+
|
| 664 |
+
return final_normal_text, final_calls
|
| 665 |
+
|
| 666 |
+
|
| 667 |
+
class FunctionCallParser:
|
| 668 |
+
"""
|
| 669 |
+
In streaming scenarios, each time new_text is received, it calls multi_format_parser.parse_streaming_increment
|
| 670 |
+
and returns the resulting normal_text and calls to the upper layer (or SSE).
|
| 671 |
+
"""
|
| 672 |
+
|
| 673 |
+
ToolCallParserEnum: Dict[str, Type[BaseFormatDetector]] = {
|
| 674 |
+
"llama3": Llama32Detector,
|
| 675 |
+
"qwen25": Qwen25Detector,
|
| 676 |
+
"mistral": MistralDetector,
|
| 677 |
+
"deepseekv3": DeepSeekV3Detector,
|
| 678 |
+
}
|
| 679 |
+
|
| 680 |
+
def __init__(self, tools: List[Tool], tool_call_parser: str):
|
| 681 |
+
detectors = []
|
| 682 |
+
if tool_call_parser:
|
| 683 |
+
detector_class = self.ToolCallParserEnum.get(tool_call_parser)
|
| 684 |
+
if detector_class:
|
| 685 |
+
detectors.append(detector_class())
|
| 686 |
+
else:
|
| 687 |
+
raise ValueError(f"Unsupported tool_call_parser: {tool_call_parser}")
|
| 688 |
+
else:
|
| 689 |
+
raise ValueError("Tool Call Parser Not Given!")
|
| 690 |
+
|
| 691 |
+
self.multi_format_parser = MultiFormatParser(detectors)
|
| 692 |
+
self.tools = tools
|
| 693 |
+
|
| 694 |
+
def has_tool_call(self, text: str) -> bool:
|
| 695 |
+
"""
|
| 696 |
+
Check if the given text contains a tool call in the format supported by this parser.
|
| 697 |
+
This delegates to the detector's implementation.
|
| 698 |
+
|
| 699 |
+
:param text: The text to check for tool calls
|
| 700 |
+
:return: True if the text contains a tool call, False otherwise
|
| 701 |
+
"""
|
| 702 |
+
# Check all detectors in the multi_format_parser
|
| 703 |
+
for detector in self.multi_format_parser.detectors:
|
| 704 |
+
if detector.has_tool_call(text):
|
| 705 |
+
return True
|
| 706 |
+
return False
|
| 707 |
+
|
| 708 |
+
def parse_non_stream(self, full_text: str) -> Tuple[str, list[ToolCallItem]]:
|
| 709 |
+
"""
|
| 710 |
+
Non-streaming call: one-time parsing
|
| 711 |
+
"""
|
| 712 |
+
full_normal_text, calls = self.multi_format_parser.parse_once(
|
| 713 |
+
full_text, self.tools
|
| 714 |
+
)
|
| 715 |
+
return full_normal_text, calls
|
| 716 |
+
|
| 717 |
+
def parse_stream_chunk(self, chunk_text: str) -> Tuple[str, list[ToolCallItem]]:
|
| 718 |
+
"""
|
| 719 |
+
Streaming call: incremental parsing
|
| 720 |
+
"""
|
| 721 |
+
normal_text, calls = self.multi_format_parser.parse_streaming_increment(
|
| 722 |
+
chunk_text, self.tools
|
| 723 |
+
)
|
| 724 |
+
return normal_text, calls
|
| 725 |
+
|
| 726 |
+
def structure_infos(self) -> List[_GetInfoFunc]:
|
| 727 |
+
"""
|
| 728 |
+
Returns a list of structure_info functions for each detector
|
| 729 |
+
"""
|
| 730 |
+
return [
|
| 731 |
+
detector.structure_info() for detector in self.multi_format_parser.detectors
|
| 732 |
+
]
|
| 733 |
+
|
| 734 |
+
def get_structure_tag(self) -> StructuralTagResponseFormat:
|
| 735 |
+
tool_structures: List[StructuresResponseFormat] = list()
|
| 736 |
+
tool_trigger_set: Set[str] = set()
|
| 737 |
+
|
| 738 |
+
for wrapper in self.structure_infos():
|
| 739 |
+
for tool in self.tools:
|
| 740 |
+
function = tool.function
|
| 741 |
+
name = function.name
|
| 742 |
+
assert name is not None
|
| 743 |
+
info = wrapper(name)
|
| 744 |
+
|
| 745 |
+
# accept all if not strict, otherwise only accept the schema
|
| 746 |
+
schema = function.parameters if function.strict else {}
|
| 747 |
+
|
| 748 |
+
tool_structures.append(
|
| 749 |
+
StructuresResponseFormat(
|
| 750 |
+
begin=info.begin,
|
| 751 |
+
schema=schema, # type: ignore
|
| 752 |
+
end=info.end,
|
| 753 |
+
)
|
| 754 |
+
)
|
| 755 |
+
tool_trigger_set.add(info.trigger)
|
| 756 |
+
|
| 757 |
+
return StructuralTagResponseFormat(
|
| 758 |
+
type="structural_tag",
|
| 759 |
+
structures=tool_structures,
|
| 760 |
+
triggers=list(tool_trigger_set),
|
| 761 |
+
)
|