ZhengyangZhang commited on
Commit
00d97e0
·
verified ·
1 Parent(s): 07ec547

Add files using upload-large-folder tool

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. bin/activate +76 -0
  2. bin/deepspeed +6 -0
  3. bin/ds_report +6 -0
  4. bin/fastapi +8 -0
  5. bin/huggingface-cli +8 -0
  6. bin/ipython3 +8 -0
  7. bin/isympy +8 -0
  8. bin/markdown_py +8 -0
  9. bin/pip3 +8 -0
  10. bin/pip3.12 +8 -0
  11. bin/proton +8 -0
  12. bin/pyftmerge +8 -0
  13. bin/ray +8 -0
  14. bin/serve +8 -0
  15. bin/wandb +8 -0
  16. bin/wb +8 -0
  17. lib/python3.12/site-packages/decorator.py +459 -0
  18. lib/python3.12/site-packages/ipython_pygments_lexers.py +582 -0
  19. lib/python3.12/site-packages/isympy.py +342 -0
  20. lib/python3.12/site-packages/package_readme.md +97 -0
  21. lib/python3.12/site-packages/pylab.py +3 -0
  22. lib/python3.12/site-packages/sglang/__init__.py +76 -0
  23. lib/python3.12/site-packages/sglang/api.py +279 -0
  24. lib/python3.12/site-packages/sglang/bench_offline_throughput.py +424 -0
  25. lib/python3.12/site-packages/sglang/bench_one_batch.py +568 -0
  26. lib/python3.12/site-packages/sglang/bench_one_batch_server.py +192 -0
  27. lib/python3.12/site-packages/sglang/bench_serving.py +1623 -0
  28. lib/python3.12/site-packages/sglang/check_env.py +305 -0
  29. lib/python3.12/site-packages/sglang/compile_deep_gemm.py +177 -0
  30. lib/python3.12/site-packages/sglang/global_config.py +49 -0
  31. lib/python3.12/site-packages/sglang/launch_server.py +16 -0
  32. lib/python3.12/site-packages/sglang/llama3_eval.py +316 -0
  33. lib/python3.12/site-packages/sglang/srt/_custom_ops.py +115 -0
  34. lib/python3.12/site-packages/sglang/srt/aio_rwlock.py +100 -0
  35. lib/python3.12/site-packages/sglang/srt/code_completion_parser.py +174 -0
  36. lib/python3.12/site-packages/sglang/srt/conversation.py +794 -0
  37. lib/python3.12/site-packages/sglang/srt/custom_op.py +44 -0
  38. lib/python3.12/site-packages/sglang/srt/disaggregation/decode.py +661 -0
  39. lib/python3.12/site-packages/sglang/srt/disaggregation/fake/__init__.py +1 -0
  40. lib/python3.12/site-packages/sglang/srt/disaggregation/fake/__pycache__/__init__.cpython-312.pyc +0 -0
  41. lib/python3.12/site-packages/sglang/srt/disaggregation/fake/__pycache__/conn.cpython-312.pyc +0 -0
  42. lib/python3.12/site-packages/sglang/srt/disaggregation/fake/conn.py +88 -0
  43. lib/python3.12/site-packages/sglang/srt/disaggregation/mini_lb.py +318 -0
  44. lib/python3.12/site-packages/sglang/srt/disaggregation/mooncake/__init__.py +6 -0
  45. lib/python3.12/site-packages/sglang/srt/disaggregation/mooncake/__pycache__/transfer_engine.cpython-312.pyc +0 -0
  46. lib/python3.12/site-packages/sglang/srt/disaggregation/mooncake/conn.py +752 -0
  47. lib/python3.12/site-packages/sglang/srt/disaggregation/mooncake/transfer_engine.py +77 -0
  48. lib/python3.12/site-packages/sglang/srt/disaggregation/prefill.py +417 -0
  49. lib/python3.12/site-packages/sglang/srt/disaggregation/utils.py +121 -0
  50. 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 [![PyPI](https://img.shields.io/pypi/v/wandb)](https://pypi.python.org/pypi/wandb) [![Conda (channel only)](https://img.shields.io/conda/vn/conda-forge/wandb)](https://anaconda.org/conda-forge/wandb) [![CircleCI](https://img.shields.io/circleci/build/github/wandb/wandb/main)](https://circleci.com/gh/wandb/wandb) [![Codecov](https://img.shields.io/codecov/c/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
+ &nbsp;
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
+ &nbsp;
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
+ &nbsp;
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
+ &nbsp;
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
+ &nbsp;
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
+ &nbsp;
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
+ &nbsp;
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
+ )