ZhengyangZhang commited on
Commit
1603a3b
·
verified ·
1 Parent(s): 00d97e0

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. lib/python3.12/site-packages/sglang/srt/constrained/__pycache__/base_grammar_backend.cpython-312.pyc +0 -0
  2. lib/python3.12/site-packages/sglang/srt/constrained/__pycache__/llguidance_backend.cpython-312.pyc +0 -0
  3. lib/python3.12/site-packages/sglang/srt/constrained/__pycache__/outlines_backend.cpython-312.pyc +0 -0
  4. lib/python3.12/site-packages/sglang/srt/constrained/__pycache__/outlines_jump_forward.cpython-312.pyc +0 -0
  5. lib/python3.12/site-packages/sglang/srt/constrained/__pycache__/reasoner_grammar_backend.cpython-312.pyc +0 -0
  6. lib/python3.12/site-packages/sglang/srt/constrained/__pycache__/xgrammar_backend.cpython-312.pyc +0 -0
  7. lib/python3.12/site-packages/sglang/srt/constrained/base_grammar_backend.py +219 -0
  8. lib/python3.12/site-packages/sglang/srt/constrained/llguidance_backend.py +169 -0
  9. lib/python3.12/site-packages/sglang/srt/constrained/outlines_backend.py +189 -0
  10. lib/python3.12/site-packages/sglang/srt/constrained/outlines_jump_forward.py +200 -0
  11. lib/python3.12/site-packages/sglang/srt/constrained/reasoner_grammar_backend.py +101 -0
  12. lib/python3.12/site-packages/sglang/srt/constrained/triton_ops/__pycache__/bitmask_ops.cpython-312.pyc +0 -0
  13. lib/python3.12/site-packages/sglang/srt/constrained/triton_ops/bitmask_ops.py +141 -0
  14. lib/python3.12/site-packages/sglang/srt/constrained/xgrammar_backend.py +207 -0
  15. lib/python3.12/site-packages/sglang/srt/disaggregation/__pycache__/decode.cpython-312.pyc +0 -0
  16. lib/python3.12/site-packages/sglang/srt/disaggregation/__pycache__/mini_lb.cpython-312.pyc +0 -0
  17. lib/python3.12/site-packages/sglang/srt/disaggregation/__pycache__/prefill.cpython-312.pyc +0 -0
  18. lib/python3.12/site-packages/sglang/srt/disaggregation/__pycache__/utils.cpython-312.pyc +0 -0
  19. lib/python3.12/site-packages/sglang/srt/disaggregation/base/__init__.py +8 -0
  20. lib/python3.12/site-packages/sglang/srt/disaggregation/base/__pycache__/__init__.cpython-312.pyc +0 -0
  21. lib/python3.12/site-packages/sglang/srt/disaggregation/base/__pycache__/conn.cpython-312.pyc +0 -0
  22. lib/python3.12/site-packages/sglang/srt/disaggregation/base/conn.py +113 -0
  23. lib/python3.12/site-packages/sglang/srt/disaggregation/mooncake/__pycache__/__init__.cpython-312.pyc +0 -0
  24. lib/python3.12/site-packages/sglang/srt/disaggregation/mooncake/__pycache__/conn.cpython-312.pyc +0 -0
  25. lib/python3.12/site-packages/sglang/srt/disaggregation/nixl/__init__.py +1 -0
  26. lib/python3.12/site-packages/sglang/srt/disaggregation/nixl/__pycache__/__init__.cpython-312.pyc +0 -0
  27. lib/python3.12/site-packages/sglang/srt/disaggregation/nixl/__pycache__/conn.cpython-312.pyc +0 -0
  28. lib/python3.12/site-packages/sglang/srt/disaggregation/nixl/conn.py +622 -0
  29. lib/python3.12/site-packages/sglang/srt/lora/__pycache__/layers.cpython-312.pyc +0 -0
  30. lib/python3.12/site-packages/sglang/srt/lora/__pycache__/lora.cpython-312.pyc +0 -0
  31. lib/python3.12/site-packages/sglang/srt/lora/__pycache__/lora_config.cpython-312.pyc +0 -0
  32. lib/python3.12/site-packages/sglang/srt/lora/__pycache__/lora_manager.cpython-312.pyc +0 -0
  33. lib/python3.12/site-packages/sglang/srt/lora/__pycache__/mem_pool.cpython-312.pyc +0 -0
  34. lib/python3.12/site-packages/sglang/srt/lora/__pycache__/utils.cpython-312.pyc +0 -0
  35. lib/python3.12/site-packages/sglang/srt/lora/backend/__pycache__/base_backend.cpython-312.pyc +0 -0
  36. lib/python3.12/site-packages/sglang/srt/lora/backend/__pycache__/flashinfer_backend.cpython-312.pyc +0 -0
  37. lib/python3.12/site-packages/sglang/srt/lora/backend/__pycache__/triton_backend.cpython-312.pyc +0 -0
  38. lib/python3.12/site-packages/sglang/srt/lora/backend/base_backend.py +133 -0
  39. lib/python3.12/site-packages/sglang/srt/lora/backend/flashinfer_backend.py +131 -0
  40. lib/python3.12/site-packages/sglang/srt/lora/backend/triton_backend.py +88 -0
  41. lib/python3.12/site-packages/sglang/srt/lora/layers.py +347 -0
  42. lib/python3.12/site-packages/sglang/srt/lora/lora.py +181 -0
  43. lib/python3.12/site-packages/sglang/srt/lora/lora_config.py +47 -0
  44. lib/python3.12/site-packages/sglang/srt/lora/lora_manager.py +228 -0
  45. lib/python3.12/site-packages/sglang/srt/lora/mem_pool.py +239 -0
  46. lib/python3.12/site-packages/sglang/srt/lora/triton_ops/__init__.py +11 -0
  47. lib/python3.12/site-packages/sglang/srt/lora/triton_ops/__pycache__/__init__.cpython-312.pyc +0 -0
  48. lib/python3.12/site-packages/sglang/srt/lora/triton_ops/__pycache__/gate_up_lora_b.cpython-312.pyc +0 -0
  49. lib/python3.12/site-packages/sglang/srt/lora/triton_ops/__pycache__/qkv_lora_b.cpython-312.pyc +0 -0
  50. lib/python3.12/site-packages/sglang/srt/lora/triton_ops/__pycache__/sgemm_lora_a.cpython-312.pyc +0 -0
lib/python3.12/site-packages/sglang/srt/constrained/__pycache__/base_grammar_backend.cpython-312.pyc ADDED
Binary file (11 kB). View file
 
lib/python3.12/site-packages/sglang/srt/constrained/__pycache__/llguidance_backend.cpython-312.pyc ADDED
Binary file (8.31 kB). View file
 
lib/python3.12/site-packages/sglang/srt/constrained/__pycache__/outlines_backend.cpython-312.pyc ADDED
Binary file (10.4 kB). View file
 
lib/python3.12/site-packages/sglang/srt/constrained/__pycache__/outlines_jump_forward.cpython-312.pyc ADDED
Binary file (7.56 kB). View file
 
lib/python3.12/site-packages/sglang/srt/constrained/__pycache__/reasoner_grammar_backend.cpython-312.pyc ADDED
Binary file (6.27 kB). View file
 
lib/python3.12/site-packages/sglang/srt/constrained/__pycache__/xgrammar_backend.cpython-312.pyc ADDED
Binary file (10 kB). View file
 
lib/python3.12/site-packages/sglang/srt/constrained/base_grammar_backend.py ADDED
@@ -0,0 +1,219 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ """The baseclass of a backend for grammar-guided constrained decoding."""
15
+
16
+ import logging
17
+ from abc import ABC, abstractmethod
18
+ from concurrent.futures import Future, ThreadPoolExecutor
19
+ from dataclasses import dataclass
20
+ from threading import Event, Lock
21
+ from typing import Dict, List, Optional, Tuple
22
+
23
+ import torch
24
+
25
+ from sglang.srt.server_args import ServerArgs
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+
30
+ class BaseGrammarObject(ABC):
31
+
32
+ def __init__(self):
33
+ self._finished = False
34
+
35
+ @property
36
+ def finished(self):
37
+ return self._finished
38
+
39
+ @finished.setter
40
+ def finished(self, finished):
41
+ self._finished = finished
42
+
43
+ @abstractmethod
44
+ def try_jump_forward(self, tokenizer) -> Optional[Tuple[List[int], str]]:
45
+ """
46
+ Try to jump forward in the grammar.
47
+
48
+ Returns:
49
+ A jump forward helper which may be used in `jump_forward_str_state`.
50
+ None if the jump forward is not possible.
51
+ """
52
+ raise NotImplementedError
53
+
54
+ @abstractmethod
55
+ def jump_forward_str_state(self, helper: Tuple[List[int], str]) -> Tuple[str, int]:
56
+ """
57
+ Jump forward for the grammar.
58
+
59
+ Returns:
60
+ A tuple of the jump forward string and the next state of the grammar
61
+ (which can be used in `jump_and_retokenize` if needed).
62
+ """
63
+ raise NotImplementedError
64
+
65
+ @abstractmethod
66
+ def jump_and_retokenize(
67
+ self, old_output_ids: List[int], new_output_ids: List[int], next_state: int
68
+ ) -> None:
69
+ """
70
+ Jump forward occurs, and update the grammar state if needed.
71
+ """
72
+ raise NotImplementedError
73
+
74
+ @abstractmethod
75
+ def accept_token(self, token: int) -> None:
76
+ """
77
+ Accept a token in the grammar.
78
+ """
79
+ raise NotImplementedError
80
+
81
+ @abstractmethod
82
+ def allocate_vocab_mask(
83
+ self, vocab_size: int, batch_size: int, device
84
+ ) -> torch.Tensor:
85
+ raise NotImplementedError
86
+
87
+ @abstractmethod
88
+ def fill_vocab_mask(self, vocab_mask: torch.Tensor, idx: int) -> None:
89
+ raise NotImplementedError
90
+
91
+ @staticmethod
92
+ @abstractmethod
93
+ def move_vocab_mask(vocab_mask: torch.Tensor, device) -> torch.Tensor:
94
+ raise NotImplementedError
95
+
96
+ @staticmethod
97
+ @abstractmethod
98
+ def apply_vocab_mask(logits: torch.Tensor, vocab_mask: torch.Tensor) -> None:
99
+ raise NotImplementedError
100
+
101
+ @abstractmethod
102
+ def copy(self) -> "BaseGrammarObject":
103
+ raise NotImplementedError
104
+
105
+
106
+ @dataclass
107
+ class CacheEntry:
108
+ value: Optional[BaseGrammarObject]
109
+ event: Event
110
+
111
+
112
+ class BaseGrammarBackend:
113
+ def __init__(self):
114
+ self.executor = ThreadPoolExecutor()
115
+ self.cache: Dict[Tuple[str, str], CacheEntry] = {}
116
+ self.cache_lock = Lock()
117
+
118
+ def _not_supported(self, key_type: str, key_string: str) -> None:
119
+ logger.warning(f"Skip unsupported {key_type}: {key_type}={key_string}")
120
+
121
+ def dispatch_fallback(
122
+ self, key_type: str, key_string: str
123
+ ) -> Optional[BaseGrammarObject]:
124
+ """
125
+ This function should not be reached in any case.
126
+ """
127
+ raise ValueError(f"Invalid key_type: {key_type}={key_string}")
128
+
129
+ def dispatch_json(self, key_string: str) -> Optional[BaseGrammarObject]:
130
+ return self._not_supported("json", key_string)
131
+
132
+ def dispatch_regex(self, key_string: str) -> Optional[BaseGrammarObject]:
133
+ return self._not_supported("regex", key_string)
134
+
135
+ def dispatch_ebnf(self, key_string: str) -> Optional[BaseGrammarObject]:
136
+ return self._not_supported("ebnf", key_string)
137
+
138
+ def dispatch_structural_tag(self, key_string: str) -> Optional[BaseGrammarObject]:
139
+ return self._not_supported("structural_tag", key_string)
140
+
141
+ def _init_value_dispatch(self, key: Tuple[str, str]) -> Optional[BaseGrammarObject]:
142
+ key_type, key_string = key
143
+ if key_type == "json":
144
+ return self.dispatch_json(key_string)
145
+ elif key_type == "regex":
146
+ return self.dispatch_regex(key_string)
147
+ elif key_type == "ebnf":
148
+ return self.dispatch_ebnf(key_string)
149
+ elif key_type == "structural_tag":
150
+ return self.dispatch_structural_tag(key_string)
151
+ else:
152
+ return self.dispatch_fallback(key_type, key_string)
153
+
154
+ def _init_value(self, key: Tuple[str, str]) -> Optional[BaseGrammarObject]:
155
+ with self.cache_lock:
156
+ if key in self.cache:
157
+ cache_hit = True
158
+ entry = self.cache[key]
159
+ else:
160
+ cache_hit = False
161
+ entry = CacheEntry(None, Event())
162
+ self.cache[key] = entry
163
+
164
+ if cache_hit:
165
+ entry.event.wait()
166
+ else:
167
+ entry.value = self._init_value_dispatch(key)
168
+ entry.event.set()
169
+ return entry.value.copy() if entry.value else None
170
+
171
+ def get_cached_value(self, key: Tuple[str, str]) -> Optional[BaseGrammarObject]:
172
+ with self.cache_lock:
173
+ entry = self.cache.get(key)
174
+ if not entry or not entry.event.is_set():
175
+ return None
176
+ val = self.cache[key].value
177
+ return val.copy() if val else None
178
+
179
+ def get_future_value(self, key: Tuple[str, str]) -> Future:
180
+ return self.executor.submit(self._init_value, key)
181
+
182
+ def reset(self):
183
+ with self.cache_lock:
184
+ self.cache.clear()
185
+
186
+
187
+ def create_grammar_backend(
188
+ server_args: ServerArgs, tokenizer, vocab_size: int
189
+ ) -> Optional[BaseGrammarBackend]:
190
+ if server_args.grammar_backend == "outlines":
191
+ from sglang.srt.constrained.outlines_backend import OutlinesGrammarBackend
192
+
193
+ grammar_backend = OutlinesGrammarBackend(
194
+ tokenizer,
195
+ whitespace_pattern=server_args.constrained_json_whitespace_pattern,
196
+ )
197
+ elif server_args.grammar_backend == "xgrammar":
198
+ from sglang.srt.constrained.xgrammar_backend import XGrammarGrammarBackend
199
+
200
+ grammar_backend = XGrammarGrammarBackend(tokenizer, vocab_size=vocab_size)
201
+ elif server_args.grammar_backend == "llguidance":
202
+ from sglang.srt.constrained.llguidance_backend import GuidanceBackend
203
+
204
+ grammar_backend = GuidanceBackend(
205
+ tokenizer=tokenizer,
206
+ whitespace_pattern=server_args.constrained_json_whitespace_pattern,
207
+ )
208
+ elif server_args.grammar_backend == "none":
209
+ return None
210
+ else:
211
+ raise ValueError(f"Invalid grammar backend: {server_args.grammar_backend}")
212
+
213
+ if server_args.reasoning_parser and hasattr(tokenizer, "think_end_id"):
214
+ from .reasoner_grammar_backend import ReasonerGrammarBackend
215
+
216
+ grammar_backend = ReasonerGrammarBackend(
217
+ grammar_backend, tokenizer.think_end_id
218
+ )
219
+ return grammar_backend
lib/python3.12/site-packages/sglang/srt/constrained/llguidance_backend.py ADDED
@@ -0,0 +1,169 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ """Constrained decoding with llguidance backend."""
15
+
16
+ import json
17
+ import logging
18
+ import os
19
+ from typing import List, Optional, Tuple
20
+
21
+ import torch
22
+ from llguidance import LLMatcher, LLTokenizer, StructTag, grammar_from
23
+ from llguidance.hf import from_tokenizer
24
+ from llguidance.torch import (
25
+ allocate_token_bitmask,
26
+ apply_token_bitmask_inplace,
27
+ fill_next_token_bitmask,
28
+ )
29
+
30
+ from sglang.srt.constrained.base_grammar_backend import (
31
+ BaseGrammarBackend,
32
+ BaseGrammarObject,
33
+ )
34
+
35
+ logger = logging.getLogger(__name__)
36
+
37
+
38
+ class GuidanceGrammar(BaseGrammarObject):
39
+
40
+ def __init__(self, llguidance_tokenizer: LLTokenizer, serialized_grammar: str):
41
+ super().__init__()
42
+ self.llguidance_tokenizer = llguidance_tokenizer
43
+ self.serialized_grammar = serialized_grammar
44
+
45
+ self.ll_matcher = LLMatcher(
46
+ self.llguidance_tokenizer,
47
+ self.serialized_grammar,
48
+ log_level=int(os.environ.get("LLGUIDANCE_LOG_LEVEL", "1")),
49
+ )
50
+ self.finished = False
51
+ self.bitmask = None
52
+
53
+ def try_jump_forward(self, tokenizer) -> Optional[Tuple[List[int], str]]:
54
+ ff_tokens = self.ll_matcher.compute_ff_tokens()
55
+ if ff_tokens:
56
+ return ff_tokens, ""
57
+ else:
58
+ return None
59
+
60
+ def jump_forward_str_state(self, helper: Tuple[List[int], str]) -> Tuple[str, int]:
61
+ return "", -1
62
+
63
+ def jump_and_retokenize(
64
+ self, old_output_ids: List[int], new_output_ids: List[int], next_state: int
65
+ ):
66
+ pass
67
+
68
+ def accept_token(self, token: int):
69
+ if not self.ll_matcher.consume_token(token):
70
+ logger.warning(f"matcher error: {self.ll_matcher.get_error()}")
71
+ self.finished = True
72
+
73
+ def fill_vocab_mask(self, vocab_mask: torch.Tensor, idx: int) -> None:
74
+ if self.ll_matcher.is_stopped():
75
+ self.finished = True
76
+
77
+ fill_next_token_bitmask(self.ll_matcher, vocab_mask, idx)
78
+
79
+ def allocate_vocab_mask(
80
+ self, vocab_size: int, batch_size: int, device
81
+ ) -> torch.Tensor:
82
+ if self.bitmask is None or self.bitmask.shape[0] < batch_size:
83
+ # only create bitmask when batch gets larger
84
+ self.bitmask = allocate_token_bitmask(
85
+ batch_size, self.llguidance_tokenizer.vocab_size
86
+ )
87
+ bitmask = self.bitmask
88
+ else:
89
+ bitmask = self.bitmask[:batch_size]
90
+
91
+ return bitmask
92
+
93
+ @staticmethod
94
+ def move_vocab_mask(vocab_mask: torch.Tensor, device) -> torch.Tensor:
95
+ return vocab_mask.to(device, non_blocking=True)
96
+
97
+ @staticmethod
98
+ def apply_vocab_mask(logits: torch.Tensor, vocab_mask: torch.Tensor) -> None:
99
+ apply_token_bitmask_inplace(logits, vocab_mask)
100
+
101
+ def copy(self):
102
+ return GuidanceGrammar(
103
+ llguidance_tokenizer=self.llguidance_tokenizer,
104
+ serialized_grammar=self.serialized_grammar,
105
+ )
106
+
107
+
108
+ class GuidanceBackend(BaseGrammarBackend):
109
+
110
+ def __init__(
111
+ self,
112
+ tokenizer,
113
+ whitespace_pattern: Optional[str] = None,
114
+ n_vocab: Optional[int] = None,
115
+ ):
116
+ super().__init__()
117
+
118
+ self.tokenizer = tokenizer
119
+ self.whitespace_pattern = whitespace_pattern
120
+ self.llguidance_tokenizer = from_tokenizer(self.tokenizer, n_vocab)
121
+
122
+ def _from_serialized(self, serialized_grammar) -> Optional[GuidanceGrammar]:
123
+ try:
124
+ return GuidanceGrammar(
125
+ llguidance_tokenizer=self.llguidance_tokenizer,
126
+ serialized_grammar=serialized_grammar,
127
+ )
128
+ except Exception as e:
129
+ logger.warning(f"Skip invalid grammar: {serialized_grammar}, {e=}")
130
+ return None
131
+
132
+ def dispatch_json(self, key_string: str) -> Optional[GuidanceGrammar]:
133
+ serialized_grammar = LLMatcher.grammar_from_json_schema(
134
+ key_string,
135
+ defaults={
136
+ "whitespace_pattern": self.whitespace_pattern,
137
+ },
138
+ )
139
+ return self._from_serialized(serialized_grammar)
140
+
141
+ def dispatch_regex(self, key_string: str) -> Optional[GuidanceGrammar]:
142
+ serialized_grammar = grammar_from("regex", key_string)
143
+ return self._from_serialized(serialized_grammar)
144
+
145
+ def dispatch_ebnf(self, key_string: str) -> Optional[GuidanceGrammar]:
146
+ try:
147
+ serialized_grammar = grammar_from("ebnf", key_string)
148
+ return self._from_serialized(serialized_grammar)
149
+ except ValueError as e:
150
+ logger.warning(f"Skip invalid ebnf: regex={key_string}, {e=}")
151
+ return None
152
+
153
+ def dispatch_structural_tag(self, key_string: str) -> Optional[GuidanceGrammar]:
154
+ try:
155
+ structural_tag = json.loads(key_string)
156
+ tags = [
157
+ StructTag(
158
+ begin=structure["begin"],
159
+ grammar=structure["schema"],
160
+ end=structure["end"],
161
+ trigger=structural_tag["triggers"][0], # TODO?
162
+ )
163
+ for structure in structural_tag["structures"]
164
+ ]
165
+ g = StructTag.to_grammar(tags)
166
+ return self._from_serialized(g)
167
+ except Exception as e:
168
+ logging.warning(f"Skip invalid structural_tag: {key_string}, {e=}")
169
+ return None
lib/python3.12/site-packages/sglang/srt/constrained/outlines_backend.py ADDED
@@ -0,0 +1,189 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ """Constrained decoding with outlines backend."""
15
+
16
+ import json
17
+ import logging
18
+ from typing import Dict, List, Optional, Tuple, Union
19
+
20
+ import interegular
21
+ import torch
22
+ from outlines.fsm.guide import RegexGuide
23
+ from outlines.models.transformers import TransformerTokenizer
24
+ from pydantic import BaseModel
25
+
26
+ from sglang.srt.constrained.base_grammar_backend import (
27
+ BaseGrammarBackend,
28
+ BaseGrammarObject,
29
+ )
30
+ from sglang.srt.constrained.outlines_jump_forward import OutlinesJumpForwardMap
31
+
32
+ try:
33
+ from outlines.fsm.json_schema import build_regex_from_schema
34
+ except ImportError:
35
+ from outlines_core.fsm.json_schema import build_regex_from_schema
36
+
37
+
38
+ logger = logging.getLogger(__name__)
39
+
40
+
41
+ class OutlinesGrammar(BaseGrammarObject):
42
+ def __init__(
43
+ self,
44
+ guide: RegexGuide,
45
+ jump_forward_map: Union[OutlinesJumpForwardMap, None],
46
+ ) -> None:
47
+ super().__init__()
48
+ self.guide = guide
49
+ self.jump_forward_map = jump_forward_map
50
+ self.state = 0
51
+ self.finished = False
52
+
53
+ def accept_token(self, token: int):
54
+ self.state = self.guide.get_next_state(self.state, token)
55
+
56
+ def try_jump_forward(self, tokenizer) -> Optional[Tuple]:
57
+ if not self.jump_forward_map:
58
+ return None
59
+
60
+ jump_forward_bytes = self.jump_forward_map.jump_forward_byte(self.state)
61
+ if jump_forward_bytes is None or len(jump_forward_bytes) <= 1:
62
+ return None
63
+
64
+ # preprocess the jump forward string
65
+ suffix_bytes = []
66
+ continuation_range = range(0x80, 0xC0)
67
+ cur_state = self.state
68
+ while (
69
+ len(jump_forward_bytes) and jump_forward_bytes[0][0] in continuation_range
70
+ ):
71
+ # continuation bytes
72
+ byte_edge = jump_forward_bytes.pop(0)
73
+ suffix_bytes.append(byte_edge[0])
74
+ cur_state = byte_edge[1]
75
+
76
+ suffix_tokens = [f"<0x{hex(b)[2:].upper()}>" for b in suffix_bytes]
77
+ suffix_ids = tokenizer.convert_tokens_to_ids(suffix_tokens)
78
+ return suffix_ids, cur_state
79
+
80
+ def jump_forward_str_state(self, helper: Tuple[List[int], str]) -> Tuple[str, int]:
81
+ _, cur_state = helper
82
+ return self.jump_forward_map.jump_forward_symbol(cur_state)
83
+
84
+ def jump_and_retokenize(
85
+ self, old_output_ids: List[int], new_output_ids: List[int], next_state: int
86
+ ):
87
+ self.state = next_state
88
+
89
+ def allocate_vocab_mask(
90
+ self, vocab_size: int, batch_size: int, device
91
+ ) -> torch.Tensor:
92
+ return torch.zeros(batch_size, vocab_size, dtype=torch.bool, device=device)
93
+
94
+ @staticmethod
95
+ def move_vocab_mask(vocab_mask: torch.Tensor, device) -> torch.Tensor:
96
+ return vocab_mask
97
+
98
+ def fill_vocab_mask(self, vocab_mask: torch.Tensor, idx: int) -> None:
99
+ tokens = torch.tensor(
100
+ self.guide.get_next_instruction(self.state).tokens, dtype=torch.int64
101
+ ).to(vocab_mask.device, non_blocking=True)
102
+ vocab_mask = vocab_mask[idx]
103
+ vocab_mask.fill_(1)
104
+ vocab_mask.scatter_(0, tokens, torch.zeros_like(tokens, dtype=torch.bool))
105
+
106
+ @staticmethod
107
+ def apply_vocab_mask(logits: torch.Tensor, vocab_mask: torch.Tensor):
108
+ logits.masked_fill_(vocab_mask, float("-inf"))
109
+
110
+ def copy(self):
111
+ return OutlinesGrammar(self.guide, self.jump_forward_map)
112
+
113
+
114
+ class OutlinesGrammarBackend(BaseGrammarBackend):
115
+ def __init__(
116
+ self,
117
+ tokenizer,
118
+ whitespace_pattern: bool,
119
+ ):
120
+ super().__init__()
121
+
122
+ try:
123
+ self.outlines_tokenizer = TransformerTokenizer(tokenizer)
124
+ except AttributeError:
125
+ # FIXME: tmp fix for chatglm2 & chatglm3 (pad_token_id=0)
126
+ origin_pad_token_id = tokenizer.pad_token_id
127
+
128
+ def fset(self, value):
129
+ self._value = value
130
+
131
+ type(tokenizer).pad_token_id = property(
132
+ fget=type(tokenizer).pad_token_id.fget, fset=fset
133
+ )
134
+ self.outlines_tokenizer = TransformerTokenizer(tokenizer)
135
+ self.outlines_tokenizer.tokenizer.pad_token_id = origin_pad_token_id
136
+ self.outlines_tokenizer.pad_token_id = origin_pad_token_id
137
+ self.outlines_tokenizer.pad_token = (
138
+ self.outlines_tokenizer.tokenizer.pad_token
139
+ )
140
+ self.outlines_tokenizer.vocabulary = (
141
+ self.outlines_tokenizer.tokenizer.get_vocab()
142
+ )
143
+ self.whitespace_pattern = whitespace_pattern
144
+
145
+ def _compile_regex(self, regex: str) -> Optional[OutlinesGrammar]:
146
+ try:
147
+ if hasattr(RegexGuide, "from_regex"):
148
+ # outlines >= 0.1.1
149
+ guide = RegexGuide.from_regex(regex, self.outlines_tokenizer)
150
+ else:
151
+ # outlines <= 0.0.46
152
+ guide = RegexGuide(regex, self.outlines_tokenizer)
153
+ except interegular.patterns.InvalidSyntax as e:
154
+ logger.warning(f"skip invalid regex schema: {regex=}, {e=}")
155
+ return None
156
+
157
+ jump_forward_map = None
158
+ return OutlinesGrammar(guide, jump_forward_map)
159
+
160
+ def dispatch_ebnf(self, key_string: str):
161
+ return super().dispatch_ebnf(key_string)
162
+
163
+ def dispatch_structural_tag(self, key_string: str):
164
+ return super().dispatch_structural_tag(key_string)
165
+
166
+ def dispatch_json(self, key_string: str):
167
+ try:
168
+ regex = build_regex_from_object(
169
+ key_string,
170
+ whitespace_pattern=self.whitespace_pattern,
171
+ )
172
+ except (NotImplementedError, json.decoder.JSONDecodeError) as e:
173
+ logger.warning(f"Skip invalid json_schema: json_schema={key_string}, {e=}")
174
+ return self._compile_regex(regex)
175
+
176
+ def dispatch_regex(self, key_string: str):
177
+ return self._compile_regex(key_string)
178
+
179
+
180
+ def build_regex_from_object(
181
+ object: Union[str, BaseModel, Dict], whitespace_pattern: Optional[str] = None
182
+ ):
183
+ if isinstance(object, type(BaseModel)):
184
+ schema = json.dumps(object.model_json_schema())
185
+ elif isinstance(object, Dict):
186
+ schema = json.dumps(object)
187
+ else:
188
+ schema = object
189
+ return build_regex_from_schema(schema, whitespace_pattern)
lib/python3.12/site-packages/sglang/srt/constrained/outlines_jump_forward.py ADDED
@@ -0,0 +1,200 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ """
15
+ Faster constrained decoding with jump forward decoding / compressed finite state machine.
16
+ Reference: https://lmsys.org/blog/2024-02-05-compressed-fsm/
17
+ """
18
+
19
+ import dataclasses
20
+ import logging
21
+ from collections import defaultdict
22
+ from typing import Optional
23
+
24
+ import interegular
25
+ from interegular import InvalidSyntax
26
+ from outlines.caching import cache
27
+
28
+ from sglang.srt.utils import get_bool_env_var
29
+
30
+ try:
31
+ # outlines >= 0.1.0
32
+ from outlines_core.fsm.outlines_core_rs import FSMInfo
33
+ from outlines_core.fsm.regex import make_byte_level_fsm, make_deterministic_fsm
34
+ except ImportError:
35
+ # outlines <= 0.0.46
36
+ from outlines.fsm.regex import FSMInfo, make_byte_level_fsm, make_deterministic_fsm
37
+
38
+ IP_REGEX = r"((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)"
39
+
40
+ # Env var was set in sglang.srt.server_args.ServerArgs.__post__init__
41
+ DISABLE_DISK_CACHE = get_bool_env_var("SGLANG_DISABLE_OUTLINES_DISK_CACHE", "true")
42
+
43
+ logger = logging.getLogger(__name__)
44
+
45
+
46
+ @dataclasses.dataclass
47
+ class JumpEdge:
48
+ symbol: str = None
49
+ symbol_next_state: int = None
50
+ byte: int = None
51
+ byte_next_state: int = None
52
+
53
+
54
+ def disk_cache(expire: Optional[float] = None, typed=False, ignore=()):
55
+ if not DISABLE_DISK_CACHE:
56
+ return cache(expire, typed, ignore)
57
+ else:
58
+ return lambda fn: None
59
+
60
+
61
+ @disk_cache()
62
+ def init_state_to_jump_forward(regex_string):
63
+ try:
64
+ regex_pattern = interegular.parse_pattern(regex_string)
65
+ except InvalidSyntax as e:
66
+ logger.warning(f"skip invalid regex: {regex_string}, {e=}")
67
+ return
68
+
69
+ byte_fsm = make_byte_level_fsm(regex_pattern.to_fsm().reduce(), keep_utf8=True)
70
+ regex_fsm, _ = make_deterministic_fsm(byte_fsm)
71
+
72
+ fsm_info: FSMInfo = regex_fsm.fsm_info
73
+
74
+ symbol_to_id = fsm_info.alphabet_symbol_mapping
75
+ id_to_symbol = {}
76
+ for symbol, id_ in symbol_to_id.items():
77
+ id_to_symbol.setdefault(id_, []).append(symbol)
78
+
79
+ transitions = fsm_info.transitions
80
+
81
+ outgoings_ct = defaultdict(int)
82
+ # NOTE(lsyin): Final states can lead to terminate, so they have one outgoing edge naturally
83
+ for s in fsm_info.finals:
84
+ outgoings_ct[s] = 1
85
+
86
+ state_to_jump_forward = {}
87
+ for (state, id_), next_state in transitions.items():
88
+ if id_ == fsm_info.alphabet_anything_value:
89
+ # Arbitrarily symbol cannot be recognized as jump forward
90
+ continue
91
+
92
+ symbols = id_to_symbol[id_]
93
+ for c in symbols:
94
+ if len(c) > 1:
95
+ # Skip byte level transitions like c = "5E"
96
+ continue
97
+
98
+ outgoings_ct[state] += 1
99
+ if outgoings_ct[state] > 1:
100
+ if state in state_to_jump_forward:
101
+ del state_to_jump_forward[state]
102
+ break
103
+
104
+ state_to_jump_forward[state] = JumpEdge(
105
+ symbol=c,
106
+ symbol_next_state=next_state,
107
+ )
108
+
109
+ # Process the byte level jump forward
110
+ outgoings_ct = defaultdict(int)
111
+ for s in fsm_info.finals:
112
+ outgoings_ct[s] = 1
113
+
114
+ for (state, id_), next_state in transitions.items():
115
+ if id_ == fsm_info.alphabet_anything_value:
116
+ continue
117
+ symbols = id_to_symbol[id_]
118
+ for c in symbols:
119
+ byte_ = None
120
+ if len(c) == 1 and ord(c) < 0x80:
121
+ # ASCII character
122
+ byte_ = ord(c)
123
+ elif len(c) > 1:
124
+ # FIXME: This logic is due to the leading \x00
125
+ # https://github.com/outlines-dev/outlines/pull/930
126
+ byte_ = int(symbols[0][1:], 16)
127
+
128
+ if byte_ is not None:
129
+ outgoings_ct[state] += 1
130
+ if outgoings_ct[state] > 1:
131
+ if state in state_to_jump_forward:
132
+ del state_to_jump_forward[state]
133
+ break
134
+ e = state_to_jump_forward.get(state, JumpEdge())
135
+ e.byte = byte_
136
+ e.byte_next_state = next_state
137
+ state_to_jump_forward[state] = e
138
+
139
+ return state_to_jump_forward
140
+
141
+
142
+ class OutlinesJumpForwardMap:
143
+ def __init__(self, regex_string):
144
+ self.state_to_jump_forward = init_state_to_jump_forward(regex_string)
145
+
146
+ def jump_forward_symbol(self, state):
147
+ jump_forward_str = ""
148
+ next_state = state
149
+ while state in self.state_to_jump_forward:
150
+ e = self.state_to_jump_forward[state]
151
+ if e.symbol is None:
152
+ break
153
+ jump_forward_str += e.symbol
154
+ next_state = e.symbol_next_state
155
+ state = next_state
156
+
157
+ return jump_forward_str, next_state
158
+
159
+ def jump_forward_byte(self, state):
160
+ if state not in self.state_to_jump_forward:
161
+ return None
162
+
163
+ jump_forward_bytes = []
164
+ next_state = None
165
+ while state in self.state_to_jump_forward:
166
+ e = self.state_to_jump_forward[state]
167
+ assert e.byte is not None and e.byte_next_state is not None
168
+ jump_forward_bytes.append((e.byte, e.byte_next_state))
169
+ next_state = e.byte_next_state
170
+ state = next_state
171
+
172
+ return jump_forward_bytes
173
+
174
+ def is_jump_forward_symbol_state(self, state):
175
+ return (
176
+ state in self.state_to_jump_forward
177
+ and self.state_to_jump_forward[state].symbol is not None
178
+ )
179
+
180
+
181
+ def test_main(regex_string):
182
+ jump_forward_map = OutlinesJumpForwardMap(regex_string)
183
+ for state, e in jump_forward_map.state_to_jump_forward.items():
184
+ if e.symbol is not None:
185
+ jump_forward_str, next_state = jump_forward_map.jump_forward_symbol(state)
186
+ print(f"{state} -> {next_state}", jump_forward_str)
187
+ bytes_ = jump_forward_map.jump_forward_byte(state)
188
+ print(f"{state} -> {bytes_[-1][1]}", [hex(b) for b, _ in bytes_])
189
+
190
+
191
+ if __name__ == "__main__":
192
+ import outlines
193
+
194
+ outlines.caching.clear_cache()
195
+ test_main(r"The google's DNS sever address is " + IP_REGEX)
196
+ test_main(r"霍格沃茨特快列车|霍比特人比尔博")
197
+ # 霍格: \xe9\x9c\x8d \xe6\xa0\xbc ...
198
+ # 霍比: \xe9\x9c\x8d \xe6\xaf\x94 ...
199
+
200
+ test_main(r"[-+]?[0-9]+[ ]*")
lib/python3.12/site-packages/sglang/srt/constrained/reasoner_grammar_backend.py ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ """The baseclass of a backend for reasoner grammar-guided constrained decoding."""
15
+
16
+ from concurrent.futures import Future
17
+ from typing import List, Optional, Tuple
18
+
19
+ import torch
20
+
21
+ from .base_grammar_backend import BaseGrammarBackend, BaseGrammarObject
22
+
23
+
24
+ class ReasonerGrammarObject(BaseGrammarObject):
25
+ def __init__(self, grammar: BaseGrammarObject, think_end_id):
26
+ super().__init__()
27
+ self.grammar = grammar
28
+ self.think_end_id = think_end_id
29
+ self.is_in_reasoning = True
30
+
31
+ @property
32
+ def finished(self):
33
+ return self.grammar.finished
34
+
35
+ @finished.setter
36
+ def finished(self, finished):
37
+ self.grammar.finished = finished
38
+
39
+ def allocate_vocab_mask(
40
+ self, vocab_size: int, batch_size: int, device
41
+ ) -> torch.Tensor:
42
+ return self.grammar.allocate_vocab_mask(vocab_size, batch_size, device)
43
+
44
+ def fill_vocab_mask(self, vocab_mask: torch.Tensor, idx: int) -> None:
45
+ if not self.is_in_reasoning:
46
+ self.grammar.fill_vocab_mask(vocab_mask, idx)
47
+
48
+ def move_vocab_mask(self, vocab_mask: torch.Tensor, device) -> torch.Tensor:
49
+ return self.grammar.move_vocab_mask(vocab_mask, device)
50
+
51
+ @property
52
+ def apply_vocab_mask(self):
53
+ return self.grammar.apply_vocab_mask
54
+
55
+ def accept_token(self, token: int):
56
+ if token == self.think_end_id:
57
+ self.is_in_reasoning = False
58
+
59
+ if not self.is_in_reasoning and token != self.think_end_id:
60
+ self.grammar.accept_token(token)
61
+
62
+ def try_jump_forward(self, tokenizer):
63
+ return self.grammar.try_jump_forward(tokenizer)
64
+
65
+ def jump_forward_str_state(self, helper):
66
+ return self.grammar.jump_forward_str_state(helper)
67
+
68
+ def jump_and_retokenize(
69
+ self, old_output_ids: List[int], new_output_ids: List[int], next_state: int
70
+ ):
71
+ return self.grammar.jump_and_retokenize(
72
+ old_output_ids, new_output_ids, next_state
73
+ )
74
+
75
+ def copy(self) -> BaseGrammarObject:
76
+ return ReasonerGrammarObject(self.grammar.copy(), self.think_end_id)
77
+
78
+
79
+ class ReasonerGrammarBackend(BaseGrammarBackend):
80
+ def __init__(self, grammar_backend: BaseGrammarBackend, think_end_id):
81
+ self.grammar_backend = grammar_backend
82
+ self.think_end_id = think_end_id
83
+
84
+ def get_cached_value(self, key: Tuple[str, str]) -> Optional[ReasonerGrammarObject]:
85
+ grammar = self.grammar_backend.get_cached_value(key)
86
+ return ReasonerGrammarObject(grammar, self.think_end_id) if grammar else None
87
+
88
+ def get_future_value(self, key: Tuple[str, str]) -> Future:
89
+ grammar = Future()
90
+
91
+ def callback(f: Future):
92
+ if result := f.result():
93
+ grammar.set_result(ReasonerGrammarObject(result, self.think_end_id))
94
+ else:
95
+ grammar.set_result(None)
96
+
97
+ self.grammar_backend.get_future_value(key).add_done_callback(callback)
98
+ return grammar
99
+
100
+ def reset(self):
101
+ self.grammar_backend.reset()
lib/python3.12/site-packages/sglang/srt/constrained/triton_ops/__pycache__/bitmask_ops.cpython-312.pyc ADDED
Binary file (5.46 kB). View file
 
lib/python3.12/site-packages/sglang/srt/constrained/triton_ops/bitmask_ops.py ADDED
@@ -0,0 +1,141 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Adapt from
2
+ # https://github.com/mlc-ai/xgrammar/blob/v0.1.17/python/xgrammar/kernels/apply_token_bitmask_inplace_triton.py
3
+
4
+ from typing import List, Optional, Union
5
+
6
+ import torch
7
+ import triton
8
+ import triton.language as tl
9
+
10
+ from sglang.srt.utils import get_device_core_count
11
+
12
+
13
+ @triton.jit
14
+ def apply_token_bitmask_inplace_kernel(
15
+ logits_ptr,
16
+ bitmask_ptr,
17
+ indices_ptr,
18
+ num_rows,
19
+ vocab_size,
20
+ logits_strides,
21
+ bitmask_strides,
22
+ NUM_SMS: tl.constexpr,
23
+ BLOCK_SIZE: tl.constexpr,
24
+ ):
25
+ """Apply a bitmask to logits in-place using Triton. The bitmask is a 01 bitwise compressed tensor,
26
+ where 0 means the token is masked and 1 means the token is not masked. After applying the bitmask,
27
+ the masked logits will be set to -inf.
28
+
29
+ Parameters
30
+ ----------
31
+ logits_ptr : tl.tensor
32
+ Pointer to the logits tensor to apply the bitmask to.
33
+
34
+ bitmask_ptr : tl.tensor
35
+ Pointer to the bitmask tensor to apply.
36
+
37
+ indices_ptr : Optional[tl.tensor]
38
+ Optional pointer to indices tensor specifying which rows to apply the mask to.
39
+
40
+ num_rows : int
41
+ Number of rows to process. If indices_ptr is provided, this is the number of unique indices.
42
+
43
+ vocab_size : int
44
+ Size of the vocabulary dimension. If the logits does not have a vocab padding, this is the
45
+ same as the logits's second dimension. Otherwise, this is the actual size of the vocabulary.
46
+
47
+ logits_strides : int
48
+ Stride between rows in the logits tensor.
49
+
50
+ bitmask_strides : int
51
+ Stride between rows in the bitmask tensor.
52
+
53
+ NUM_SMS : int
54
+ Number of streaming multiprocessors to use.
55
+
56
+ BLOCK_SIZE : int
57
+ Size of processing blocks.
58
+ """
59
+
60
+ pid = tl.program_id(0)
61
+ num_blocks = tl.cdiv(vocab_size, BLOCK_SIZE)
62
+ for work_id in tl.range(pid, num_rows * num_blocks, NUM_SMS):
63
+ row_id = work_id // num_blocks
64
+ block_offset = (work_id % num_blocks) * BLOCK_SIZE
65
+ batch_id = row_id if indices_ptr is None else tl.load(indices_ptr + row_id)
66
+ offsets = block_offset + tl.arange(0, BLOCK_SIZE)
67
+ bitmask_offsets = block_offset // 32 + tl.arange(0, BLOCK_SIZE // 32)
68
+ vocab_mask = offsets < vocab_size
69
+ packed_bitmask_mask = bitmask_offsets < bitmask_strides
70
+ packed_bitmask = tl.load(
71
+ bitmask_ptr + batch_id * bitmask_strides + bitmask_offsets,
72
+ packed_bitmask_mask,
73
+ )
74
+ bitmask = ((packed_bitmask[:, None] >> (tl.arange(0, 32)[None, :])) & 1) == 0
75
+ bitmask = bitmask.reshape(BLOCK_SIZE)
76
+
77
+ tl.store(
78
+ logits_ptr + batch_id * logits_strides + offsets,
79
+ -float("inf"),
80
+ vocab_mask & bitmask,
81
+ )
82
+
83
+
84
+ def apply_token_bitmask_inplace_triton(
85
+ logits: torch.Tensor,
86
+ bitmask: torch.Tensor,
87
+ indices: Optional[Union[List[int], torch.Tensor]] = None,
88
+ ):
89
+ NUM_SMS = get_device_core_count()
90
+ BLOCK_SIZE = 4096
91
+ BITS_PER_BLOCK = 32
92
+
93
+ # Check input dtype
94
+ assert bitmask.dtype == torch.int32, "bitmask must be of type int32"
95
+
96
+ # Check input tensor shapes.
97
+ logits_shape = logits.shape
98
+ bitmask_shape = bitmask.shape
99
+ if logits.ndim == 1:
100
+ logits_shape = (1, logits_shape[0])
101
+ if bitmask.ndim == 1:
102
+ bitmask_shape = (1, bitmask_shape[0])
103
+
104
+ required_bitmask_width = (logits_shape[1] + BITS_PER_BLOCK - 1) // BITS_PER_BLOCK
105
+ assert required_bitmask_width >= bitmask_shape[1], (
106
+ f"Bitmask width too large: allow at most {required_bitmask_width} int32s for "
107
+ f"logits' width {logits_shape[1]}, but got {bitmask_shape[1]}"
108
+ )
109
+
110
+ vocab_size = min(logits_shape[1], bitmask_shape[1] * BITS_PER_BLOCK)
111
+
112
+ num_rows = None
113
+ if isinstance(indices, list) or isinstance(indices, torch.Tensor):
114
+ indices = torch.tensor(indices, dtype=torch.int32, device=logits.device)
115
+ num_rows = indices.shape[0]
116
+ else:
117
+ assert (
118
+ logits_shape[0] == bitmask_shape[0]
119
+ ), f"batch size mismatch: logits {logits_shape[0]} vs bitmask {bitmask_shape[0]}"
120
+ num_rows = logits_shape[0]
121
+
122
+ if NUM_SMS > 0:
123
+ grid = (NUM_SMS,)
124
+ else:
125
+ num_blocks = triton.cdiv(vocab_size, BLOCK_SIZE)
126
+ grid = (num_rows * num_blocks,)
127
+ NUM_SMS = triton.next_power_of_2(grid[0])
128
+
129
+ apply_token_bitmask_inplace_kernel[grid](
130
+ logits,
131
+ bitmask,
132
+ indices,
133
+ num_rows,
134
+ vocab_size,
135
+ logits_shape[1],
136
+ bitmask_shape[1],
137
+ NUM_SMS,
138
+ BLOCK_SIZE,
139
+ num_warps=BLOCK_SIZE // 32 // (16 // logits.element_size()),
140
+ num_stages=3,
141
+ )
lib/python3.12/site-packages/sglang/srt/constrained/xgrammar_backend.py ADDED
@@ -0,0 +1,207 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ """Constrained decoding with xgrammar backend."""
15
+
16
+ import json
17
+ import logging
18
+ from typing import List, Optional, Tuple, Union
19
+
20
+ import torch
21
+ from xgrammar import (
22
+ CompiledGrammar,
23
+ GrammarCompiler,
24
+ GrammarMatcher,
25
+ StructuralTagItem,
26
+ TokenizerInfo,
27
+ allocate_token_bitmask,
28
+ )
29
+
30
+ from sglang.srt.constrained.base_grammar_backend import (
31
+ BaseGrammarBackend,
32
+ BaseGrammarObject,
33
+ )
34
+ from sglang.srt.constrained.triton_ops.bitmask_ops import (
35
+ apply_token_bitmask_inplace_triton,
36
+ )
37
+ from sglang.srt.utils import get_bool_env_var
38
+
39
+ logger = logging.getLogger(__name__)
40
+
41
+
42
+ MAX_ROLLBACK_TOKENS = 200
43
+
44
+
45
+ class XGrammarGrammar(BaseGrammarObject):
46
+
47
+ def __init__(
48
+ self,
49
+ matcher: GrammarMatcher,
50
+ vocab_size: int,
51
+ ctx: CompiledGrammar,
52
+ override_stop_tokens: Optional[Union[List[int], int]],
53
+ ) -> None:
54
+ super().__init__()
55
+ self.matcher = matcher
56
+ self.vocab_size = vocab_size
57
+ self.ctx = ctx
58
+ self.override_stop_tokens = override_stop_tokens
59
+ self.finished = False
60
+
61
+ # Fix (from vLLM team): postpone the import of apply_token_bitmask_inplace_kernels to the
62
+ # class init site to avoid re-initializing CUDA in forked subprocess.
63
+ from xgrammar.kernels import apply_token_bitmask_inplace_kernels
64
+
65
+ self.use_token_bitmask_triton = get_bool_env_var(
66
+ "SGLANG_TOKEN_BITMASK_TRITON", "false"
67
+ )
68
+ self.apply_vocab_mask_cuda = apply_token_bitmask_inplace_kernels.get(
69
+ "cuda", None
70
+ )
71
+ self.apply_vocab_mask_cpu = apply_token_bitmask_inplace_kernels.get("cpu", None)
72
+
73
+ def accept_token(self, token: int):
74
+ assert self.matcher.accept_token(token)
75
+
76
+ def try_jump_forward(self, tokenizer) -> Optional[Tuple[List[int], str]]:
77
+ s = self.matcher.find_jump_forward_string()
78
+ if s:
79
+ return [], s
80
+ return None
81
+
82
+ def jump_forward_str_state(self, helper: Tuple[List[int], str]) -> Tuple[str, int]:
83
+ _, data = helper
84
+ return data, -1
85
+
86
+ def jump_and_retokenize(
87
+ self, old_output_ids: List[int], new_output_ids: List[int], next_state: int
88
+ ):
89
+ k = 0
90
+ for i, old_id in enumerate(old_output_ids):
91
+ if old_id == new_output_ids[i]:
92
+ k = i + 1
93
+ else:
94
+ break
95
+
96
+ # rollback to the last token that is the same
97
+ if k < len(old_output_ids):
98
+ self.matcher.rollback(len(old_output_ids) - k)
99
+
100
+ for i in range(k, len(new_output_ids)):
101
+ assert self.matcher.accept_token(new_output_ids[i])
102
+
103
+ def allocate_vocab_mask(
104
+ self, vocab_size: int, batch_size: int, device
105
+ ) -> torch.Tensor:
106
+ return allocate_token_bitmask(batch_size, vocab_size)
107
+
108
+ def fill_vocab_mask(self, vocab_mask: torch.Tensor, idx: int) -> None:
109
+ self.matcher.fill_next_token_bitmask(vocab_mask, idx)
110
+
111
+ @staticmethod
112
+ def move_vocab_mask(vocab_mask: torch.Tensor, device) -> torch.Tensor:
113
+ return vocab_mask.to(device, non_blocking=True)
114
+
115
+ def apply_vocab_mask(self, logits: torch.Tensor, vocab_mask: torch.Tensor) -> None:
116
+ if (
117
+ not self.use_token_bitmask_triton
118
+ and logits.device.type == "cuda"
119
+ and self.apply_vocab_mask_cuda
120
+ ):
121
+ return self.apply_vocab_mask_cuda(logits, vocab_mask)
122
+ if logits.device.type == "cpu" and self.apply_vocab_mask_cpu:
123
+ return self.apply_vocab_mask_cpu(logits, vocab_mask)
124
+ apply_token_bitmask_inplace_triton(logits, vocab_mask)
125
+
126
+ def copy(self):
127
+ matcher = GrammarMatcher(
128
+ self.ctx,
129
+ max_rollback_tokens=MAX_ROLLBACK_TOKENS,
130
+ override_stop_tokens=self.override_stop_tokens,
131
+ )
132
+ return XGrammarGrammar(
133
+ matcher, self.vocab_size, self.ctx, self.override_stop_tokens
134
+ )
135
+
136
+
137
+ class XGrammarGrammarBackend(BaseGrammarBackend):
138
+ def __init__(
139
+ self,
140
+ tokenizer,
141
+ vocab_size: int,
142
+ ):
143
+ super().__init__()
144
+
145
+ tokenizer_info = TokenizerInfo.from_huggingface(
146
+ tokenizer, vocab_size=vocab_size
147
+ )
148
+ override_stop_tokens = None
149
+
150
+ self.grammar_compiler = GrammarCompiler(tokenizer_info=tokenizer_info)
151
+ self.vocab_size = vocab_size
152
+ self.override_stop_tokens = override_stop_tokens
153
+
154
+ def _from_context(self, ctx: CompiledGrammar) -> XGrammarGrammar:
155
+ matcher = GrammarMatcher(ctx, max_rollback_tokens=MAX_ROLLBACK_TOKENS)
156
+ return XGrammarGrammar(matcher, self.vocab_size, ctx, self.override_stop_tokens)
157
+
158
+ def dispatch_json(self, key_string: str) -> Optional[XGrammarGrammar]:
159
+ try:
160
+ if key_string == "$$ANY$$":
161
+ # Note: This builtin JSON grammar includes *all* valid JSON (including, for example, arrays at the root)
162
+ ctx = self.grammar_compiler.compile_builtin_json_grammar()
163
+ else:
164
+ ctx = self.grammar_compiler.compile_json_schema(schema=key_string)
165
+ except RuntimeError as e:
166
+ logging.warning(f"Skip invalid json_schema: json_schema={key_string}, {e=}")
167
+ return None
168
+ return self._from_context(ctx)
169
+
170
+ def dispatch_ebnf(self, key_string: str) -> Optional[XGrammarGrammar]:
171
+ try:
172
+ ctx = self.grammar_compiler.compile_grammar(key_string)
173
+ except RuntimeError as e:
174
+ logging.warning(f"Skip invalid ebnf: ebnf={key_string}, {e=}")
175
+ return None
176
+ return self._from_context(ctx)
177
+
178
+ def dispatch_regex(self, key_string: str) -> Optional[XGrammarGrammar]:
179
+ try:
180
+ ctx = self.grammar_compiler.compile_regex(key_string)
181
+ except RuntimeError as e:
182
+ logging.warning(f"Skip invalid regex: regex={key_string}, {e=}")
183
+ return None
184
+ return self._from_context(ctx)
185
+
186
+ def dispatch_structural_tag(self, key_string: str) -> Optional[XGrammarGrammar]:
187
+ try:
188
+ structural_tag = json.loads(key_string)
189
+ tags = [
190
+ StructuralTagItem(
191
+ begin=structure["begin"],
192
+ schema=json.dumps(structure["schema"]),
193
+ end=structure["end"],
194
+ )
195
+ for structure in structural_tag["structures"]
196
+ ]
197
+ ctx = self.grammar_compiler.compile_structural_tag(
198
+ tags, structural_tag["triggers"]
199
+ )
200
+ except RuntimeError as e:
201
+ logging.warning(f"Skip invalid regex: regex={key_string}, {e=}")
202
+ return None
203
+ return self._from_context(ctx)
204
+
205
+ def reset(self):
206
+ if self.grammar_compiler:
207
+ self.grammar_compiler.clear_cache()
lib/python3.12/site-packages/sglang/srt/disaggregation/__pycache__/decode.cpython-312.pyc ADDED
Binary file (31.5 kB). View file
 
lib/python3.12/site-packages/sglang/srt/disaggregation/__pycache__/mini_lb.cpython-312.pyc ADDED
Binary file (15.8 kB). View file
 
lib/python3.12/site-packages/sglang/srt/disaggregation/__pycache__/prefill.cpython-312.pyc ADDED
Binary file (18.4 kB). View file
 
lib/python3.12/site-packages/sglang/srt/disaggregation/__pycache__/utils.cpython-312.pyc ADDED
Binary file (5.29 kB). View file
 
lib/python3.12/site-packages/sglang/srt/disaggregation/base/__init__.py ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ from .conn import (
2
+ BaseKVBootstrapServer,
3
+ BaseKVManager,
4
+ BaseKVReceiver,
5
+ BaseKVSender,
6
+ KVArgs,
7
+ KVPoll,
8
+ )
lib/python3.12/site-packages/sglang/srt/disaggregation/base/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (380 Bytes). View file
 
lib/python3.12/site-packages/sglang/srt/disaggregation/base/__pycache__/conn.cpython-312.pyc ADDED
Binary file (4.72 kB). View file
 
lib/python3.12/site-packages/sglang/srt/disaggregation/base/conn.py ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from abc import ABC, abstractmethod
2
+ from typing import Optional
3
+
4
+ import numpy as np
5
+ import numpy.typing as npt
6
+
7
+ from sglang.srt.disaggregation.utils import DisaggregationMode
8
+ from sglang.srt.server_args import ServerArgs
9
+
10
+
11
+ class KVArgs:
12
+ engine_rank: int
13
+ kv_data_ptrs: list[int]
14
+ kv_data_lens: list[int]
15
+ kv_item_lens: list[int]
16
+ aux_data_ptrs: list[int]
17
+ aux_data_lens: list[int]
18
+ aux_item_lens: list[int]
19
+ ib_device: str
20
+ gpu_id: int
21
+
22
+
23
+ class KVPoll:
24
+ Failed = 0
25
+ Bootstrapping = 1
26
+ WaitingForInput = 2
27
+ Transferring = 3
28
+ Success = 4
29
+
30
+
31
+ class BaseKVManager(ABC):
32
+ """Base class for managing transfers states"""
33
+
34
+ @abstractmethod
35
+ def __init__(
36
+ self,
37
+ args: KVArgs,
38
+ disaggregation_mode: DisaggregationMode,
39
+ server_args: ServerArgs,
40
+ ): ...
41
+
42
+
43
+ class BaseKVSender(ABC):
44
+
45
+ @abstractmethod
46
+ def __init__(
47
+ self, mgr: BaseKVManager, bootstrap_addr: str, bootstrap_room: int
48
+ ): ...
49
+
50
+ @abstractmethod
51
+ def init(self, num_kv_indices: int, aux_index: Optional[int] = None):
52
+ """
53
+ Notify the decoder server about the kv indices length and aux index
54
+ """
55
+ ...
56
+
57
+ @abstractmethod
58
+ def send(self, kv_indices: npt.NDArray[np.int64]):
59
+ """
60
+ Send the kv cache at the given kv indices to the decoder server
61
+ """
62
+ ...
63
+
64
+ @abstractmethod
65
+ def poll(self) -> KVPoll:
66
+ """
67
+ Check the status of the kv cache transfer
68
+ """
69
+ ...
70
+
71
+ @abstractmethod
72
+ def failure_exception(self):
73
+ """
74
+ Raise an exception if the kv cache transfer fails
75
+ """
76
+ ...
77
+
78
+
79
+ class BaseKVReceiver(ABC):
80
+
81
+ @abstractmethod
82
+ def __init__(
83
+ self,
84
+ mgr: BaseKVManager,
85
+ bootstrap_addr: str,
86
+ bootstrap_room: Optional[int] = None,
87
+ ): ...
88
+
89
+ @abstractmethod
90
+ def init(self, kv_indices: npt.NDArray[np.int64], aux_index: Optional[int] = None):
91
+ """
92
+ Notify the prefill server about the kv indices and aux index
93
+ """
94
+ ...
95
+
96
+ @abstractmethod
97
+ def poll(self) -> KVPoll:
98
+ """
99
+ Check the status of the kv cache transfer
100
+ """
101
+ ...
102
+
103
+ @abstractmethod
104
+ def failure_exception(self):
105
+ """
106
+ Raise an exception if the kv cache transfer fails
107
+ """
108
+ ...
109
+
110
+
111
+ class BaseKVBootstrapServer(ABC):
112
+ @abstractmethod
113
+ def __init__(self, port: int): ...
lib/python3.12/site-packages/sglang/srt/disaggregation/mooncake/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (361 Bytes). View file
 
lib/python3.12/site-packages/sglang/srt/disaggregation/mooncake/__pycache__/conn.cpython-312.pyc ADDED
Binary file (38.8 kB). View file
 
lib/python3.12/site-packages/sglang/srt/disaggregation/nixl/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ from .conn import NixlKVBootstrapServer, NixlKVManager, NixlKVReceiver, NixlKVSender
lib/python3.12/site-packages/sglang/srt/disaggregation/nixl/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (337 Bytes). View file
 
lib/python3.12/site-packages/sglang/srt/disaggregation/nixl/__pycache__/conn.cpython-312.pyc ADDED
Binary file (34.6 kB). View file
 
lib/python3.12/site-packages/sglang/srt/disaggregation/nixl/conn.py ADDED
@@ -0,0 +1,622 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import dataclasses
5
+ import logging
6
+ import queue
7
+ import socket
8
+ import struct
9
+ import threading
10
+ import uuid
11
+ from collections import defaultdict
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.utils import DisaggregationMode
30
+ from sglang.srt.server_args import ServerArgs
31
+ from sglang.srt.utils import get_free_port, get_ip, get_local_ip_by_remote
32
+
33
+ logger = logging.getLogger(__name__)
34
+
35
+
36
+ @dataclasses.dataclass
37
+ class TransferInfo:
38
+ room: int
39
+ endpoint: str
40
+ dst_port: int
41
+ agent_metadata: bytes
42
+ dst_kv_ptrs: list[int]
43
+ dst_kv_indices: npt.NDArray[np.int64]
44
+ dst_aux_ptrs: list[int]
45
+ dst_aux_index: int
46
+ dst_gpu_id: int
47
+
48
+ @classmethod
49
+ def from_zmq(cls, msg: List[bytes]):
50
+ return cls(
51
+ room=int(msg[0].decode("ascii")),
52
+ endpoint=msg[1].decode("ascii"),
53
+ dst_port=int(msg[2].decode("ascii")),
54
+ agent_metadata=msg[3],
55
+ dst_kv_ptrs=list(struct.unpack(f"{len(msg[4])//8}Q", msg[4])),
56
+ dst_kv_indices=np.frombuffer(msg[5], dtype=np.int64),
57
+ dst_aux_ptrs=list(struct.unpack(f"{len(msg[6])//8}Q", msg[6])),
58
+ dst_aux_index=int(msg[7].decode("ascii")),
59
+ dst_gpu_id=int(msg[8].decode("ascii")),
60
+ )
61
+
62
+
63
+ @dataclasses.dataclass
64
+ class TransferStatus:
65
+ """Used by KV Receiver to know when a transfer is done."""
66
+
67
+ # KV chunk IDs that have been received.
68
+ received_kvs: Set[int] = dataclasses.field(default_factory=set)
69
+ # Number of kv chunks to expect, will know this after last chunk is received.
70
+ num_kvs_expected: Optional[int] = None
71
+ # Whether aux data has been received.
72
+ received_aux: bool = False
73
+
74
+ def is_done(self):
75
+ if self.num_kvs_expected is None:
76
+ return False
77
+ return self.num_kvs_expected == len(self.received_kvs) and self.received_aux
78
+
79
+
80
+ class NixlKVManager(BaseKVManager):
81
+ def __init__(
82
+ self,
83
+ args: KVArgs,
84
+ disaggregation_mode: DisaggregationMode,
85
+ server_args: ServerArgs,
86
+ ):
87
+ try:
88
+ from nixl._api import nixl_agent
89
+ except ImportError as e:
90
+ raise ImportError(
91
+ "Please install NIXL by following the instructions at "
92
+ "https://github.com/ai-dynamo/nixl/blob/main/README.md "
93
+ "to run SGLang with NixlTransferEngine."
94
+ ) from e
95
+ self.agent = nixl_agent(str(uuid.uuid4()))
96
+ self.kv_args = args
97
+ self.disaggregation_mode = disaggregation_mode
98
+ # for p/d multi node infer
99
+ self.bootstrap_port = server_args.disaggregation_bootstrap_port
100
+ self.dist_init_addr = server_args.dist_init_addr
101
+ self.rank_port = None
102
+ self.server_socket = zmq.Context().socket(zmq.PULL)
103
+ self.register_buffer_to_engine()
104
+
105
+ self.rank_port = get_free_port()
106
+ if self.disaggregation_mode == DisaggregationMode.PREFILL:
107
+ self.transfer_infos: Dict[int, TransferInfo] = {}
108
+ self.condition = threading.Condition()
109
+ self.peer_names: Dict[int, str] = {}
110
+ self._start_bootstrap_thread()
111
+ self._register_to_bootstrap()
112
+ elif self.disaggregation_mode == DisaggregationMode.DECODE:
113
+ self.connection_pool: Dict[str, Dict[str, Union[str, int]]] = {}
114
+ self.transfer_statuses: Dict[int, TransferStatus] = defaultdict(
115
+ TransferStatus
116
+ )
117
+ else:
118
+ raise ValueError(
119
+ f"Unsupported DisaggregationMode: {self.disaggregation_mode}"
120
+ )
121
+
122
+ def register_buffer_to_engine(self):
123
+ kv_addrs = []
124
+ for kv_data_ptr, kv_data_len in zip(
125
+ self.kv_args.kv_data_ptrs, self.kv_args.kv_data_lens
126
+ ):
127
+ kv_addrs.append((kv_data_ptr, kv_data_len, self.kv_args.gpu_id, ""))
128
+ self.kv_descs = self.agent.register_memory(kv_addrs, "VRAM", is_sorted=True)
129
+ if not self.kv_descs:
130
+ raise Exception("NIXL memory registration failed for kv tensors")
131
+ aux_addrs = []
132
+ for aux_data_ptr, aux_data_len in zip(
133
+ self.kv_args.aux_data_ptrs, self.kv_args.aux_data_lens
134
+ ):
135
+ aux_addrs.append((aux_data_ptr, aux_data_len, 0, ""))
136
+ self.aux_descs = self.agent.register_memory(aux_addrs, "DRAM", is_sorted=True)
137
+ if not self.aux_descs:
138
+ raise Exception("NIXL memory registration failed for aux tensors")
139
+
140
+ @cache
141
+ def _connect(self, endpoint: str):
142
+ socket = zmq.Context().socket(zmq.PUSH)
143
+ socket.connect(endpoint)
144
+ return socket
145
+
146
+ def _add_remote(self, room: int, agent_metadata: bytes):
147
+ if room not in self.peer_names:
148
+ self.peer_names[room] = self.agent.add_remote_agent(agent_metadata)
149
+ return self.peer_names[room]
150
+
151
+ def send_kvcache(
152
+ self,
153
+ peer_name: str,
154
+ prefill_kv_indices: npt.NDArray[np.int64],
155
+ dst_kv_ptrs: list[int],
156
+ dst_kv_indices: npt.NDArray[np.int64],
157
+ dst_gpu_id: int,
158
+ notif: str,
159
+ ):
160
+ # Make descs
161
+ num_layers = len(self.kv_args.kv_data_ptrs)
162
+ src_addrs = []
163
+ dst_addrs = []
164
+ for layer_id in range(num_layers):
165
+ src_ptr = self.kv_args.kv_data_ptrs[layer_id]
166
+ dst_ptr = dst_kv_ptrs[layer_id]
167
+ item_len = self.kv_args.kv_item_lens[layer_id]
168
+
169
+ for prefill_index, decode_index in zip(prefill_kv_indices, dst_kv_indices):
170
+ src_addr = src_ptr + int(prefill_index) * item_len
171
+ dst_addr = dst_ptr + int(decode_index) * item_len
172
+ length = item_len
173
+ src_addrs.append((src_addr, length, self.kv_args.gpu_id))
174
+ dst_addrs.append((dst_addr, length, dst_gpu_id))
175
+ src_descs = self.agent.get_xfer_descs(src_addrs, "VRAM", is_sorted=True)
176
+ dst_descs = self.agent.get_xfer_descs(dst_addrs, "VRAM", is_sorted=True)
177
+ # Transfer data
178
+ xfer_handle = self.agent.initialize_xfer(
179
+ "WRITE",
180
+ src_descs,
181
+ dst_descs,
182
+ peer_name,
183
+ notif.encode("ascii"),
184
+ )
185
+ if not xfer_handle:
186
+ raise Exception("KVSender failed to create transfer")
187
+ state = self.agent.transfer(xfer_handle)
188
+ if state == "ERR":
189
+ raise Exception("KVSender failed to post transfer")
190
+ return xfer_handle
191
+
192
+ def send_aux(
193
+ self,
194
+ peer_name: str,
195
+ prefill_aux_index: int,
196
+ dst_aux_ptrs: list[int],
197
+ dst_aux_index: int,
198
+ notif: str,
199
+ ):
200
+ # Make descs
201
+ aux_item_len = self.kv_args.aux_item_lens[0]
202
+ prefill_aux_addr = (
203
+ self.kv_args.aux_data_ptrs[0] + prefill_aux_index * aux_item_len
204
+ )
205
+ decode_aux_addr = dst_aux_ptrs[0] + dst_aux_index * aux_item_len
206
+ src_addrs = [(prefill_aux_addr, aux_item_len, 0)]
207
+ dst_addrs = [(decode_aux_addr, aux_item_len, 0)]
208
+ src_descs = self.agent.get_xfer_descs(src_addrs, "DRAM", is_sorted=True)
209
+ dst_descs = self.agent.get_xfer_descs(dst_addrs, "DRAM", is_sorted=True)
210
+ # Transfer data
211
+ xfer_handle = self.agent.initialize_xfer(
212
+ "WRITE",
213
+ src_descs,
214
+ dst_descs,
215
+ peer_name,
216
+ notif.encode("ascii"),
217
+ )
218
+ if not xfer_handle:
219
+ raise Exception("KVSender failed to create transfer")
220
+ state = self.agent.transfer(xfer_handle)
221
+ if state == "ERR":
222
+ raise Exception("KVSender failed to post transfer")
223
+ return xfer_handle
224
+
225
+ def add_transfer_request(
226
+ self,
227
+ bootstrap_room: int,
228
+ kv_indices: npt.NDArray[np.int64],
229
+ index_slice: slice,
230
+ is_last: bool,
231
+ chunk_id: int,
232
+ aux_index: Optional[int] = None,
233
+ ):
234
+ assert self.disaggregation_mode == DisaggregationMode.PREFILL
235
+ assert not is_last or (is_last and aux_index is not None)
236
+
237
+ # Wait for transfer info to be populated by bootstrap thread.
238
+ with self.condition:
239
+ self.condition.wait_for(lambda: bootstrap_room in self.transfer_infos)
240
+ req = self.transfer_infos[bootstrap_room]
241
+ assert bootstrap_room == req.room
242
+
243
+ peer_name = self._add_remote(bootstrap_room, req.agent_metadata)
244
+ chunked_dst_kv_indice = req.dst_kv_indices[index_slice]
245
+ assert len(chunked_dst_kv_indice) == len(kv_indices)
246
+
247
+ notif = "_".join([str(req.room), "kv", str(chunk_id), str(int(is_last))])
248
+ kv_xfer_handle = self.send_kvcache(
249
+ peer_name,
250
+ kv_indices,
251
+ req.dst_kv_ptrs,
252
+ chunked_dst_kv_indice,
253
+ req.dst_gpu_id,
254
+ notif,
255
+ )
256
+ handles = [kv_xfer_handle]
257
+ # Only the last chunk we need to send the aux data.
258
+ if is_last:
259
+ aux_xfer_handle = self.send_aux(
260
+ peer_name,
261
+ aux_index,
262
+ req.dst_aux_ptrs,
263
+ req.dst_aux_index,
264
+ str(req.room) + "_aux",
265
+ )
266
+ handles.append(aux_xfer_handle)
267
+ return handles
268
+
269
+ def update_transfer_status(self):
270
+ # Process notifications from received transfers.
271
+ notif_map = self.agent.get_new_notifs()
272
+ for peer_name, messages in notif_map.items():
273
+ # We could also check that self.bootstrap_info['agent_name'] matches
274
+ # the message sender. But the bootstrap room alone should be
275
+ # sufficient to map the status.
276
+ for msg in messages:
277
+ components = msg.decode("ascii").split("_")
278
+ room = int(components[0])
279
+ if components[1] == "kv":
280
+ chunk_id = int(components[2])
281
+ is_last = bool(components[3])
282
+ self.transfer_statuses[room].received_kvs.add(chunk_id)
283
+ if is_last:
284
+ self.transfer_statuses[room].num_kvs_expected = chunk_id + 1
285
+ elif components[1] == "aux":
286
+ self.transfer_statuses[room].received_aux = True
287
+
288
+ def check_transfer_done(self, room: int):
289
+ if room not in self.transfer_statuses:
290
+ return False
291
+ return self.transfer_statuses[room].is_done()
292
+
293
+ def _register_to_bootstrap(self):
294
+ """Register KVSender to bootstrap server via HTTP POST."""
295
+ if self.dist_init_addr:
296
+ ip_address = socket.gethostbyname(self.dist_init_addr.split(":")[0])
297
+ else:
298
+ ip_address = get_ip()
299
+
300
+ bootstrap_server_url = f"{ip_address}:{self.bootstrap_port}"
301
+ url = f"http://{bootstrap_server_url}/route"
302
+ payload = {
303
+ "role": "Prefill",
304
+ "rank_ip": get_local_ip_by_remote(),
305
+ "rank_port": self.rank_port,
306
+ "engine_rank": self.kv_args.engine_rank,
307
+ "agent_name": self.agent.name,
308
+ }
309
+
310
+ try:
311
+ response = requests.put(url, json=payload)
312
+ if response.status_code == 200:
313
+ logger.debug("Prefill successfully registered to bootstrap server.")
314
+ else:
315
+ logger.error(
316
+ f"Prefill Failed to connect to bootstrap server: {response.status_code}, {response.text}"
317
+ )
318
+ except Exception as e:
319
+ logger.error(f"Prefill Failed to register to bootstrap server: {e}")
320
+
321
+ def _start_bootstrap_thread(self):
322
+ self.server_socket.bind(f"tcp://{get_local_ip_by_remote()}:{self.rank_port}")
323
+
324
+ def bootstrap_thread():
325
+ """This thread recvs transfer info from the decode engine"""
326
+ while True:
327
+ waiting_req_bytes = self.server_socket.recv_multipart()
328
+ room = waiting_req_bytes[0].decode("ascii")
329
+ if room == "None":
330
+ continue
331
+ room = int(room)
332
+ with self.condition:
333
+ self.transfer_infos[room] = TransferInfo.from_zmq(waiting_req_bytes)
334
+ self.condition.notify_all()
335
+
336
+ threading.Thread(target=bootstrap_thread).start()
337
+
338
+
339
+ class NixlKVSender(BaseKVSender):
340
+
341
+ def __init__(self, mgr: NixlKVManager, bootstrap_addr: str, bootstrap_room: int):
342
+ self.kv_mgr = mgr
343
+ self.bootstrap_room = bootstrap_room
344
+ self.aux_index = None
345
+ self.bootstrap_server_url = bootstrap_addr
346
+ self.xfer_handles = []
347
+ self.has_sent = False
348
+ self.chunk_id = 0
349
+
350
+ def init(self, num_kv_indices: int, aux_index: Optional[int] = None):
351
+ self.num_kv_indices = num_kv_indices
352
+ self.aux_index = aux_index
353
+
354
+ def send(
355
+ self,
356
+ kv_indices: npt.NDArray[np.int64],
357
+ index_slice: slice,
358
+ is_last: bool,
359
+ ):
360
+ new_xfer_handles = self.kv_mgr.add_transfer_request(
361
+ self.bootstrap_room,
362
+ kv_indices,
363
+ index_slice,
364
+ is_last,
365
+ self.chunk_id,
366
+ self.aux_index,
367
+ )
368
+ self.xfer_handles.extend(new_xfer_handles)
369
+ self.chunk_id += 1
370
+ if is_last:
371
+ self.has_sent = True
372
+
373
+ def poll(self) -> KVPoll:
374
+ if not self.has_sent:
375
+ return KVPoll.WaitingForInput
376
+
377
+ states = [self.kv_mgr.agent.check_xfer_state(x) for x in self.xfer_handles]
378
+ if all([x == "DONE" for x in states]):
379
+ return KVPoll.Success
380
+ if any([x == "ERR" for x in states]):
381
+ raise Exception("KVSender transfer encountered an error.")
382
+ return KVPoll.WaitingForInput
383
+
384
+ def failure_exception(self):
385
+ raise Exception("Fake KVSender Exception")
386
+
387
+
388
+ class NixlKVReceiver(BaseKVReceiver):
389
+
390
+ def __init__(
391
+ self,
392
+ mgr: NixlKVManager,
393
+ bootstrap_addr: str,
394
+ bootstrap_room: Optional[int] = None,
395
+ ):
396
+ self.bootstrap_room = bootstrap_room
397
+ self.bootstrap_addr = bootstrap_addr
398
+ self.kv_mgr = mgr
399
+ self.started_transfer = False
400
+
401
+ # NOTE: key distinguished by bootstrap_addr and engine_rank
402
+ bootstrap_key = f"{self.bootstrap_addr}_{self.kv_mgr.kv_args.engine_rank}"
403
+
404
+ if bootstrap_key not in self.kv_mgr.connection_pool:
405
+ self.bootstrap_info = self._get_bootstrap_info_from_server(
406
+ self.kv_mgr.kv_args.engine_rank
407
+ )
408
+ if self.bootstrap_info is None:
409
+ logger.error(
410
+ f"Could not fetch bootstrap info for engine rank: {self.kv_mgr.kv_args.engine_rank}"
411
+ )
412
+ else:
413
+ self.kv_mgr.connection_pool[bootstrap_key] = self.bootstrap_info
414
+ else:
415
+ self.bootstrap_info = self.kv_mgr.connection_pool[bootstrap_key]
416
+
417
+ assert self.bootstrap_info is not None
418
+
419
+ def _get_bootstrap_info_from_server(self, engine_rank):
420
+ """Fetch the bootstrap info from the bootstrap server."""
421
+ try:
422
+ url = f"http://{self.bootstrap_addr}/route?engine_rank={engine_rank}"
423
+ response = requests.get(url)
424
+ if response.status_code == 200:
425
+ bootstrap_info = response.json()
426
+ return bootstrap_info
427
+ else:
428
+ logger.error(
429
+ f"Failed to get prefill server info: {response.status_code}, {response.text}"
430
+ )
431
+ return None
432
+ except Exception as e:
433
+ logger.error(f"Error fetching prefill info from bootstrap: {e}")
434
+ return None
435
+
436
+ @cache
437
+ def _connect(self, endpoint: str):
438
+ socket = zmq.Context().socket(zmq.PUSH)
439
+ socket.connect(endpoint)
440
+ return socket
441
+
442
+ def init(self, kv_indices: npt.NDArray[np.int64], aux_index: Optional[int] = None):
443
+ self.prefill_server_url = (
444
+ f"{self.bootstrap_info['rank_ip']}:{self.bootstrap_info['rank_port']}"
445
+ )
446
+ logger.debug(
447
+ f"Fetched bootstrap info: {self.bootstrap_info} for engine rank: {self.kv_mgr.kv_args.engine_rank}"
448
+ )
449
+
450
+ packed_kv_data_ptrs = b"".join(
451
+ struct.pack("Q", ptr) for ptr in self.kv_mgr.kv_args.kv_data_ptrs
452
+ )
453
+ packed_aux_data_ptrs = b"".join(
454
+ struct.pack("Q", ptr) for ptr in self.kv_mgr.kv_args.aux_data_ptrs
455
+ )
456
+ self._connect("tcp://" + self.prefill_server_url).send_multipart(
457
+ [
458
+ str(self.bootstrap_room).encode("ascii"),
459
+ get_local_ip_by_remote().encode("ascii"),
460
+ str(self.kv_mgr.rank_port).encode("ascii"),
461
+ self.kv_mgr.agent.get_agent_metadata(),
462
+ packed_kv_data_ptrs,
463
+ kv_indices.tobytes(),
464
+ packed_aux_data_ptrs,
465
+ str(aux_index).encode("ascii"),
466
+ str(self.kv_mgr.kv_args.gpu_id).encode("ascii"),
467
+ ]
468
+ )
469
+ self.started_transfer = True
470
+
471
+ def poll(self) -> KVPoll:
472
+ if not self.started_transfer:
473
+ return KVPoll.WaitingForInput
474
+
475
+ self.kv_mgr.update_transfer_status()
476
+
477
+ if self.kv_mgr.check_transfer_done(self.bootstrap_room):
478
+ return KVPoll.Success
479
+ return KVPoll.WaitingForInput
480
+
481
+ def failure_exception(self):
482
+ raise Exception("Fake KVReceiver Exception")
483
+
484
+
485
+ class NixlKVBootstrapServer(BaseKVBootstrapServer):
486
+ def __init__(self, port: int):
487
+ self.port = port
488
+ self.app = web.Application()
489
+ self.store = dict()
490
+ self.lock = asyncio.Lock()
491
+ self._setup_routes()
492
+ self.prefill_port_table: Dict[int, Dict[str, Union[str, int]]] = {}
493
+
494
+ # Start bootstrap server
495
+ self.thread = threading.Thread(target=self._run_server, daemon=True)
496
+ self.run()
497
+
498
+ def run(self):
499
+ self.thread.start()
500
+
501
+ def _setup_routes(self):
502
+ self.app.router.add_route("*", "/metadata", self._handle_metadata)
503
+ self.app.router.add_route("*", "/route", self._handle_route)
504
+
505
+ async def _handle_metadata(self, request: web.Request):
506
+ key = request.query.get("key", "")
507
+
508
+ if request.method == "GET":
509
+ return await self._handle_metadata_get(key)
510
+ elif request.method == "PUT":
511
+ return await self._handle_metadata_put(key, request)
512
+ elif request.method == "DELETE":
513
+ return await self._handle_metadata_delete(key)
514
+ return web.Response(
515
+ text="Method not allowed", status=405, content_type="application/json"
516
+ )
517
+
518
+ async def _handle_metadata_get(self, key):
519
+ async with self.lock:
520
+ value = self.store.get(key)
521
+ if value is None:
522
+ return web.Response(
523
+ text="metadata not found", status=404, content_type="application/json"
524
+ )
525
+ return web.Response(body=value, status=200, content_type="application/json")
526
+
527
+ async def _handle_metadata_put(self, key, request):
528
+ data = await request.read()
529
+ async with self.lock:
530
+ self.store[key] = data
531
+ return web.Response(
532
+ text="metadata updated", status=200, content_type="application/json"
533
+ )
534
+
535
+ async def _handle_metadata_delete(self, key):
536
+ async with self.lock:
537
+ if key not in self.store:
538
+ return web.Response(
539
+ text="metadata not found",
540
+ status=404,
541
+ content_type="application/json",
542
+ )
543
+ del self.store[key]
544
+ return web.Response(
545
+ text="metadata deleted", status=200, content_type="application/json"
546
+ )
547
+
548
+ async def _handle_route(self, request: web.Request):
549
+ method = request.method
550
+ if method == "PUT":
551
+ return await self._handle_route_put(request)
552
+ elif method == "GET":
553
+ return await self._handle_route_get(request)
554
+ else:
555
+ return web.Response(
556
+ text="Method not allowed", status=405, content_type="application/json"
557
+ )
558
+
559
+ async def _handle_route_put(self, request: web.Request):
560
+ data = await request.json()
561
+ role = data["role"]
562
+ rank_ip = data["rank_ip"]
563
+ rank_port = int(data["rank_port"])
564
+ engine_rank = int(data["engine_rank"])
565
+ agent_name = data["agent_name"]
566
+
567
+ # Add lock to make sure thread-safe
568
+ if role == "Prefill":
569
+ self.prefill_port_table[engine_rank] = {
570
+ "rank_ip": rank_ip,
571
+ "rank_port": rank_port,
572
+ "agent_name": agent_name,
573
+ }
574
+ logger.info(
575
+ f"Registered Prefill boostrap: {engine_rank} with rank_ip: {rank_ip} and rank_port: {rank_port} and name: {agent_name}"
576
+ )
577
+
578
+ return web.Response(text="OK", status=200)
579
+
580
+ async def _handle_route_get(self, request: web.Request):
581
+ engine_rank = request.query.get("engine_rank")
582
+ if not engine_rank:
583
+ return web.Response(text="Missing rank", status=400)
584
+
585
+ # Find corresponding prefill info
586
+ async with self.lock:
587
+ bootstrap_info = self.prefill_port_table.get(int(engine_rank))
588
+ if bootstrap_info is not None:
589
+ return web.json_response(bootstrap_info, status=200)
590
+ else:
591
+ return web.Response(text="Not Found", status=404)
592
+
593
+ def _run_server(self):
594
+ try:
595
+ # Event Loop
596
+ self._loop = asyncio.new_event_loop()
597
+ asyncio.set_event_loop(self._loop)
598
+
599
+ self._runner = web.AppRunner(self.app)
600
+ self._loop.run_until_complete(self._runner.setup())
601
+
602
+ site = web.TCPSite(self._runner, port=self.port)
603
+ self._loop.run_until_complete(site.start())
604
+ self._loop.run_forever()
605
+ except Exception as e:
606
+ logger.error(f"Server error: {str(e)}")
607
+ finally:
608
+ # Cleanup
609
+ self._loop.run_until_complete(self._runner.cleanup())
610
+ self._loop.close()
611
+
612
+ def close(self):
613
+ """Shutdown"""
614
+ if self._loop is not None and self._loop.is_running():
615
+ self._loop.call_soon_threadsafe(self._loop.stop)
616
+ logger.info("Stopping server loop...")
617
+
618
+ if self.thread.is_alive():
619
+ self.thread.join(timeout=2)
620
+ logger.info("Server thread stopped")
621
+
622
+ def poll(self) -> KVPoll: ...
lib/python3.12/site-packages/sglang/srt/lora/__pycache__/layers.cpython-312.pyc ADDED
Binary file (16.1 kB). View file
 
lib/python3.12/site-packages/sglang/srt/lora/__pycache__/lora.cpython-312.pyc ADDED
Binary file (7.4 kB). View file
 
lib/python3.12/site-packages/sglang/srt/lora/__pycache__/lora_config.cpython-312.pyc ADDED
Binary file (2.1 kB). View file
 
lib/python3.12/site-packages/sglang/srt/lora/__pycache__/lora_manager.cpython-312.pyc ADDED
Binary file (10.6 kB). View file
 
lib/python3.12/site-packages/sglang/srt/lora/__pycache__/mem_pool.cpython-312.pyc ADDED
Binary file (9.85 kB). View file
 
lib/python3.12/site-packages/sglang/srt/lora/__pycache__/utils.cpython-312.pyc ADDED
Binary file (5.51 kB). View file
 
lib/python3.12/site-packages/sglang/srt/lora/backend/__pycache__/base_backend.cpython-312.pyc ADDED
Binary file (6.46 kB). View file
 
lib/python3.12/site-packages/sglang/srt/lora/backend/__pycache__/flashinfer_backend.cpython-312.pyc ADDED
Binary file (5.75 kB). View file
 
lib/python3.12/site-packages/sglang/srt/lora/backend/__pycache__/triton_backend.cpython-312.pyc ADDED
Binary file (3.43 kB). View file
 
lib/python3.12/site-packages/sglang/srt/lora/backend/base_backend.py ADDED
@@ -0,0 +1,133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Tuple, Union
2
+
3
+ import torch
4
+
5
+ from sglang.srt.lora.utils import LoRABatchInfo
6
+
7
+
8
+ def get_fuse_output_add_from_name(name: str) -> bool:
9
+ mapping = {
10
+ "triton": True,
11
+ "flashinfer": False,
12
+ }
13
+ return mapping.get(name, False)
14
+
15
+
16
+ def get_fuse_stacked_lora_b_from_name(name: str) -> bool:
17
+ mapping = {
18
+ "triton": True,
19
+ "flashinfer": False,
20
+ }
21
+ return mapping.get(name, False)
22
+
23
+
24
+ class BaseLoRABackend:
25
+ """Base class for different Lora backends.
26
+ Each backend has its own implementation of Lora kernels.
27
+
28
+ Args:
29
+ name: name of backend
30
+ batch_info: information of current batch for use
31
+ fuse_output_add: if set to True, the output buffer for storing result will be passed in when doing lora_b forward,
32
+ and the operation of adding will be fused into kernel
33
+ """
34
+
35
+ def __init__(self, name: str, batch_info: LoRABatchInfo = None):
36
+ self.name = name
37
+ self.batch_info = batch_info
38
+ self.fuse_output_add = get_fuse_output_add_from_name(name)
39
+ self.fuse_stacked_lora_b = get_fuse_stacked_lora_b_from_name(name)
40
+
41
+ def run_lora_a_sgemm(
42
+ self, x: torch.Tensor, weights: torch.Tensor, *args, **kwargs
43
+ ) -> torch.Tensor:
44
+ """Run segment Gemm of lora a modules with current backend.
45
+ The definition of segment Gemm can be referred to https://docs.flashinfer.ai/api/gemm.html.
46
+
47
+ Args:
48
+ x: input matrix with shape (s, input_dim), here s is the sum of all sequence lengths
49
+ weights: a set of lora weights with shape (num_lora, c * r, input_dim),
50
+ here r is lora rank, c is a multiplier for stacked modules (e.g., c=3 for qkv_proj, c=2 for gate_up_proj)
51
+ usually input_dim is much larger than r
52
+ Returns:
53
+ result with shape (s, c * r)
54
+ """
55
+ pass
56
+
57
+ def run_lora_b_sgemm(
58
+ self, x: torch.Tensor, weights: torch.Tensor, *args, **kwargs
59
+ ) -> torch.Tensor:
60
+ """Run segment Gemm of lora b modules with current backend.
61
+ The definition of segment Gemm can be referred to https://docs.flashinfer.ai/api/gemm.html.
62
+
63
+ Args:
64
+ x: input matrix with shape (s, r), here s is the sum of all sequence lengths, r is lora rank
65
+ weights: a set of lora weights with shape (num_lora, output_dim, r)
66
+ usually output_dim is much larger than r
67
+ Returns:
68
+ result with shape (s, output_dim)
69
+ """
70
+ pass
71
+
72
+ def run_qkv_lora(
73
+ self,
74
+ x: torch.Tensor,
75
+ qkv_lora_a: torch.Tensor,
76
+ qkv_lora_b: Union[torch.Tensor, Tuple[torch.Tensor]],
77
+ *args,
78
+ **kwargs,
79
+ ) -> torch.Tensor:
80
+ """Run the lora pass for QKV Layer.
81
+
82
+ Args:
83
+ x: input matrix with shape (s, input_dim), here s is the sum of all sequence lengths
84
+ qkv_lora_a: lora_a module for qkv, with shape (num_lora, 3 * r, input_dim)
85
+ qkv_lora_b: lora_b module for qkv.
86
+ If passed in as a tensor, its shape should be (num_lora,output_dim_q + 2 * output_dim_kv, r)
87
+ If passed in as a tuple of two tensors, it should contain:
88
+ a lora_b module for q, with shape (1, num_lora, output_dim_q, r)
89
+ and a combined lora_b module for kv, with shape (2, num_lora, output_dim_kv, r)
90
+ Returns:
91
+ result with shape (s, output_dim_q + 2 * output_dim_kv)
92
+ """
93
+ pass
94
+
95
+ def run_gate_up_lora(
96
+ self,
97
+ x: torch.Tensor,
98
+ gate_up_lora_a: torch.Tensor,
99
+ gate_up_lora_b: Union[torch.Tensor, Tuple[torch.Tensor]],
100
+ *args,
101
+ **kwargs,
102
+ ) -> torch.Tensor:
103
+ """Run the lora pass for gate_up_proj, usually attached to MergedColumnParallelLayer.
104
+
105
+ Args:
106
+ x: input matrix with shape (s, input_dim), here s is the sum of all sequence lengths
107
+ gate_up_lora_a: lora_a module for gate_up_proj, with shape (num_lora, 2 * r, input_dim)
108
+ gate_up_lora_b: lora_b module for qkv.
109
+ If passed in as a tensor, its shape should be (num_lora, 2 * output_dim, r)
110
+ If passed in as a tuple, it should contain two tensors with shape (num_lora, output_dim, r)
111
+ Returns:
112
+ result with shape (s, 2 * output_dim)
113
+ """
114
+ pass
115
+
116
+ def set_batch_info(self, batch_info: LoRABatchInfo):
117
+ self.batch_info = batch_info
118
+
119
+
120
+ def get_backend_from_name(name: str) -> BaseLoRABackend:
121
+ """
122
+ Get corresponding backend class from backend's name
123
+ """
124
+ if name == "triton":
125
+ from sglang.srt.lora.backend.triton_backend import TritonLoRABackend
126
+
127
+ return TritonLoRABackend
128
+ elif name == "flashinfer":
129
+ from sglang.srt.lora.backend.flashinfer_backend import FlashInferLoRABackend
130
+
131
+ return FlashInferLoRABackend
132
+ else:
133
+ raise ValueError(f"Invalid backend: {name}")
lib/python3.12/site-packages/sglang/srt/lora/backend/flashinfer_backend.py ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Tuple
2
+
3
+ import torch
4
+
5
+ from sglang.srt.lora.backend.base_backend import BaseLoRABackend
6
+ from sglang.srt.lora.utils import LoRABatchInfo
7
+ from sglang.srt.utils import is_flashinfer_available
8
+
9
+ if is_flashinfer_available():
10
+ from flashinfer import SegmentGEMMWrapper
11
+
12
+
13
+ class FlashInferLoRABackend(BaseLoRABackend):
14
+
15
+ def __init__(self, name: str, batch_info: LoRABatchInfo = None):
16
+ super().__init__(name, batch_info)
17
+
18
+ # Set up SGemm Wrapper from flashinfer
19
+ # FIXME wait for flashinfer segment gemm update
20
+ workspace_buffer = torch.empty(1 * 1024 * 1024, dtype=torch.int8, device="cuda")
21
+ self.segment_gemm = SegmentGEMMWrapper(workspace_buffer)
22
+
23
+ def run_lora_a_sgemm(
24
+ self, x: torch.Tensor, weights: torch.Tensor, *args, **kwargs
25
+ ) -> torch.Tensor:
26
+
27
+ return self.segment_gemm.run(
28
+ x=x,
29
+ weights=weights,
30
+ batch_size=self.batch_info.bs,
31
+ weight_column_major=True,
32
+ seg_indptr=self.batch_info.seg_indptr,
33
+ weight_indices=self.batch_info.weight_indices,
34
+ )
35
+
36
+ def run_lora_b_sgemm(
37
+ self, x: torch.Tensor, weights: torch.Tensor, *args, **kwargs
38
+ ) -> torch.Tensor:
39
+
40
+ return (
41
+ self.segment_gemm.run(
42
+ x=x,
43
+ weights=weights,
44
+ batch_size=self.batch_info.bs,
45
+ weight_column_major=True,
46
+ seg_indptr=self.batch_info.seg_indptr,
47
+ weight_indices=self.batch_info.weight_indices,
48
+ )
49
+ * self.batch_info.scalings[0]
50
+ )
51
+
52
+ def run_qkv_lora(
53
+ self,
54
+ x: torch.Tensor,
55
+ qkv_lora_a: torch.Tensor,
56
+ qkv_lora_b: Tuple[torch.Tensor],
57
+ *args,
58
+ **kwargs,
59
+ ) -> torch.Tensor:
60
+
61
+ assert isinstance(qkv_lora_b, tuple) and len(qkv_lora_b) == 2
62
+
63
+ # Shape of lora_a_output: (s, 3 * r)
64
+ lora_a_output = self.run_lora_a_sgemm(x=x, weights=qkv_lora_a)
65
+
66
+ q_lora_b, kv_lora_b = qkv_lora_b
67
+ lora_rank = kv_lora_b.shape[-1]
68
+ output_dim_q = q_lora_b.shape[-2]
69
+ output_dim_kv = kv_lora_b.shape[-2]
70
+ lora_output = torch.empty(
71
+ (x.shape[0], output_dim_q + 2 * output_dim_kv),
72
+ device=x.device,
73
+ dtype=x.dtype,
74
+ )
75
+
76
+ # q
77
+ lora_output[:, :output_dim_q] = self.run_lora_b_sgemm(
78
+ x=lora_a_output[:, :lora_rank].contiguous(), weights=q_lora_b[0]
79
+ )
80
+
81
+ # kv
82
+ lora_output[:, output_dim_q : output_dim_q + output_dim_kv] = (
83
+ self.run_lora_b_sgemm(
84
+ x=lora_a_output[:, lora_rank : 2 * lora_rank].contiguous(),
85
+ weights=kv_lora_b[0],
86
+ )
87
+ )
88
+
89
+ lora_output[
90
+ :, output_dim_q + output_dim_kv : output_dim_q + 2 * output_dim_kv
91
+ ] = self.run_lora_b_sgemm(
92
+ x=lora_a_output[:, 2 * lora_rank : 3 * lora_rank].contiguous(),
93
+ weights=kv_lora_b[1],
94
+ )
95
+
96
+ return lora_output * self.batch_info.scalings[0]
97
+
98
+ def run_gate_up_lora(
99
+ self,
100
+ x: torch.Tensor,
101
+ gate_up_lora_a: torch.Tensor,
102
+ gate_up_lora_b: Tuple[torch.Tensor],
103
+ *args,
104
+ **kwargs,
105
+ ) -> torch.Tensor:
106
+
107
+ assert isinstance(gate_up_lora_b, tuple) and len(gate_up_lora_b) == 2
108
+ lora_rank = gate_up_lora_b[0].shape[-1]
109
+ output_dim = gate_up_lora_b[0].shape[-2]
110
+
111
+ # Shape of lora_a_output: (s, 2 * r)
112
+ lora_a_output = self.run_lora_a_sgemm(x=x, weights=gate_up_lora_a)
113
+
114
+ lora_output = torch.empty(
115
+ (x.shape[0], 2 * output_dim),
116
+ device=x.device,
117
+ dtype=x.dtype,
118
+ )
119
+
120
+ # Compute lora for gate and up proj respectively
121
+ lora_output[:, :output_dim] = self.run_lora_b_sgemm(
122
+ x=lora_a_output[:, :lora_rank].contiguous(),
123
+ weights=gate_up_lora_b[0],
124
+ )
125
+
126
+ lora_output[:, output_dim:] = self.run_lora_b_sgemm(
127
+ x=lora_a_output[:, lora_rank:].contiguous(),
128
+ weights=gate_up_lora_b[1],
129
+ )
130
+
131
+ return lora_output * self.batch_info.scalings[0]
lib/python3.12/site-packages/sglang/srt/lora/backend/triton_backend.py ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+
3
+ from sglang.srt.lora.backend.base_backend import BaseLoRABackend
4
+ from sglang.srt.lora.triton_ops import (
5
+ gate_up_lora_b_fwd,
6
+ qkv_lora_b_fwd,
7
+ sgemm_lora_a_fwd,
8
+ sgemm_lora_b_fwd,
9
+ )
10
+ from sglang.srt.lora.utils import LoRABatchInfo
11
+
12
+
13
+ class TritonLoRABackend(BaseLoRABackend):
14
+
15
+ def __init__(self, name: str, batch_info: LoRABatchInfo = None):
16
+ super().__init__(name, batch_info)
17
+
18
+ def run_lora_a_sgemm(
19
+ self, x: torch.Tensor, weights: torch.Tensor, *args, **kwargs
20
+ ) -> torch.Tensor:
21
+ return sgemm_lora_a_fwd(x, weights, self.batch_info)
22
+
23
+ def run_lora_b_sgemm(
24
+ self,
25
+ x: torch.Tensor,
26
+ weights: torch.Tensor,
27
+ base_output: torch.Tensor = None,
28
+ *args,
29
+ **kwargs
30
+ ) -> torch.Tensor:
31
+ return sgemm_lora_b_fwd(x, weights, self.batch_info, base_output)
32
+
33
+ def run_qkv_lora(
34
+ self,
35
+ x: torch.Tensor,
36
+ qkv_lora_a: torch.Tensor,
37
+ qkv_lora_b: torch.Tensor,
38
+ output_offset: torch.Tensor,
39
+ max_qkv_out_dim: int,
40
+ base_output: torch.Tensor = None,
41
+ *args,
42
+ **kwargs
43
+ ) -> torch.Tensor:
44
+
45
+ # x: (s, input_dim)
46
+ # qkv_lora_a: (num_lora, 3 * r, input_dim)
47
+ # qkv_lora_b: (num_lora, output_dim_q + 2 * output_dim_kv, r)
48
+ assert isinstance(qkv_lora_b, torch.Tensor)
49
+
50
+ lora_a_output = sgemm_lora_a_fwd(x, qkv_lora_a, self.batch_info, stack_num=3)
51
+ lora_output = qkv_lora_b_fwd(
52
+ lora_a_output,
53
+ qkv_lora_b,
54
+ self.batch_info,
55
+ output_offset,
56
+ max_qkv_out_dim,
57
+ base_output,
58
+ )
59
+ return lora_output
60
+
61
+ def run_gate_up_lora(
62
+ self,
63
+ x: torch.Tensor,
64
+ gate_up_lora_a: torch.Tensor,
65
+ gate_up_lora_b: torch.Tensor,
66
+ base_output: torch.Tensor = None,
67
+ *args,
68
+ **kwargs
69
+ ) -> torch.Tensor:
70
+
71
+ # x: (s, input_dim)
72
+ # gate_up_lora_a: (num_lora, 2 * r, input_dim)
73
+ # gate_up_lora_b: (num_lora, 2 * output_dim, r)
74
+ assert isinstance(gate_up_lora_b, torch.Tensor)
75
+ output_dim = gate_up_lora_b.shape[-2] // 2
76
+
77
+ # lora_a_output: (s, 2 * r)
78
+ lora_a_output = sgemm_lora_a_fwd(
79
+ x, gate_up_lora_a, self.batch_info, stack_num=2
80
+ )
81
+ lora_output = gate_up_lora_b_fwd(
82
+ lora_a_output,
83
+ gate_up_lora_b,
84
+ self.batch_info,
85
+ output_dim,
86
+ base_output,
87
+ )
88
+ return lora_output
lib/python3.12/site-packages/sglang/srt/lora/layers.py ADDED
@@ -0,0 +1,347 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import List, Tuple
2
+
3
+ import torch
4
+ from torch import nn
5
+
6
+ from sglang.srt.distributed import (
7
+ get_tensor_model_parallel_rank,
8
+ split_tensor_along_last_dim,
9
+ tensor_model_parallel_all_gather,
10
+ tensor_model_parallel_all_reduce,
11
+ )
12
+ from sglang.srt.layers.linear import (
13
+ ColumnParallelLinear,
14
+ MergedColumnParallelLinear,
15
+ QKVParallelLinear,
16
+ RowParallelLinear,
17
+ )
18
+ from sglang.srt.layers.vocab_parallel_embedding import VocabParallelEmbedding
19
+ from sglang.srt.lora.backend.base_backend import BaseLoRABackend
20
+
21
+
22
+ class BaseLayerWithLoRA(nn.Module):
23
+ def __init__(
24
+ self,
25
+ base_layer: nn.Module,
26
+ lora_backend: BaseLoRABackend,
27
+ ):
28
+ super().__init__()
29
+ self.base_layer: nn.Module = base_layer
30
+ self.set_lora: bool = False
31
+ self.lora_backend: BaseLoRABackend = lora_backend
32
+
33
+ def forward(self, x: torch.Tensor):
34
+ return self.base_layer.forward(x)
35
+
36
+ def set_lora_info(self, *args):
37
+ pass
38
+
39
+ def slice_lora_a_weights(self, A: torch.Tensor, tp_rank: int):
40
+ pass
41
+
42
+ def slice_lora_b_weights(self, B: torch.Tensor, tp_rank: int):
43
+ pass
44
+
45
+
46
+ class VocabParallelEmbeddingWithLoRA(BaseLayerWithLoRA):
47
+ """
48
+ Vocab parallel embedding layer with support for LoRA (Low-Rank Adaptation).
49
+
50
+ Note: The current version does not yet implement the LoRA functionality.
51
+ This class behaves exactly the same as the base VocabParallelEmbedding.
52
+ Future versions will integrate LoRA functionality to support efficient parameter fine-tuning.
53
+ """
54
+
55
+ def __init__(
56
+ self,
57
+ base_layer: VocabParallelEmbedding,
58
+ lora_backend: BaseLoRABackend,
59
+ ) -> None:
60
+ super().__init__(base_layer, lora_backend)
61
+ self.weight = base_layer.weight
62
+
63
+
64
+ class ColumnParallelLinearWithLoRA(BaseLayerWithLoRA):
65
+ def __init__(
66
+ self,
67
+ base_layer: ColumnParallelLinear,
68
+ lora_backend: BaseLoRABackend,
69
+ ) -> None:
70
+ super().__init__(base_layer, lora_backend)
71
+
72
+ def set_lora_info(
73
+ self,
74
+ A_buffer: torch.Tensor,
75
+ B_buffer: torch.Tensor,
76
+ ):
77
+ self.set_lora = True
78
+ self.A_buffer = A_buffer
79
+ self.B_buffer = B_buffer
80
+
81
+ def apply_lora(self, base_output: torch.Tensor, x: torch.Tensor) -> torch.Tensor:
82
+ backend_kwargs = {"base_output": base_output}
83
+ lora_a_output = self.lora_backend.run_lora_a_sgemm(x, self.A_buffer)
84
+ lora_output = self.lora_backend.run_lora_b_sgemm(
85
+ lora_a_output,
86
+ self.B_buffer[0],
87
+ **backend_kwargs,
88
+ )
89
+ return (
90
+ lora_output
91
+ if self.lora_backend.fuse_output_add
92
+ else base_output + lora_output
93
+ )
94
+
95
+ def forward(self, input_: torch.Tensor):
96
+ # duplicate the logic in ColumnParallelLinear
97
+ bias = self.base_layer.bias if not self.base_layer.skip_bias_add else None
98
+ output_parallel = self.base_layer.quant_method.apply(
99
+ self.base_layer, input_, bias
100
+ )
101
+
102
+ if self.set_lora:
103
+ output_parallel = self.apply_lora(output_parallel, input_)
104
+
105
+ if self.base_layer.gather_output:
106
+ output = tensor_model_parallel_all_gather(output_parallel)
107
+ else:
108
+ output = output_parallel
109
+ output_bias = self.base_layer.bias if self.base_layer.skip_bias_add else None
110
+ return output, output_bias
111
+
112
+ def slice_lora_a_weights(self, A: torch.Tensor, tp_rank: int):
113
+ return A
114
+
115
+ def slice_lora_b_weights(self, B: torch.Tensor, tp_rank: int):
116
+ shard_size = self.base_layer.output_partition_sizes[0]
117
+ start_idx = tp_rank * shard_size
118
+ end_idx = (tp_rank + 1) * shard_size
119
+ B = B[start_idx:end_idx, :]
120
+ return B
121
+
122
+
123
+ class MergedColumnParallelLinearWithLoRA(ColumnParallelLinearWithLoRA):
124
+ def __init__(
125
+ self,
126
+ base_layer: MergedColumnParallelLinear,
127
+ lora_backend: BaseLoRABackend,
128
+ ) -> None:
129
+ super().__init__(base_layer, lora_backend)
130
+
131
+ def set_lora_info(
132
+ self,
133
+ A_buffer: torch.Tensor,
134
+ B_buffer: torch.Tensor,
135
+ ):
136
+ self.set_lora = True
137
+ self.A_buffer_gate_up = A_buffer
138
+ if self.lora_backend.fuse_stacked_lora_b:
139
+ # TODO: avoid using contiguous() in GPU.
140
+ # B_buffer_gate_up: (num_lora, 2 * output_dim, r)
141
+ self.B_buffer_gate_up = torch.cat(
142
+ (B_buffer[0], B_buffer[1]), dim=-2
143
+ ).contiguous()
144
+ else:
145
+ self.B_buffer_gate_up = (B_buffer[0], B_buffer[1])
146
+
147
+ def apply_lora(self, base_output: torch.Tensor, x: torch.Tensor) -> torch.Tensor:
148
+ backend_kwargs = {"base_output": base_output}
149
+
150
+ lora_output = self.lora_backend.run_gate_up_lora(
151
+ x,
152
+ self.A_buffer_gate_up,
153
+ self.B_buffer_gate_up,
154
+ **backend_kwargs,
155
+ )
156
+ return (
157
+ lora_output
158
+ if self.lora_backend.fuse_output_add
159
+ else base_output + lora_output
160
+ )
161
+
162
+ def slice_lora_a_weights(self, A: torch.Tensor, tp_rank: int):
163
+ return A
164
+
165
+ def slice_lora_b_weights(self, B: torch.Tensor, tp_rank: int):
166
+ # Since the outputs for both gate and up are identical, we use a random one.
167
+ shard_size = self.base_layer.output_partition_sizes[0]
168
+ start_idx = tp_rank * shard_size
169
+ end_idx = (tp_rank + 1) * shard_size
170
+ return B[:, start_idx:end_idx, :]
171
+
172
+
173
+ class QKVParallelLinearWithLoRA(ColumnParallelLinearWithLoRA):
174
+ def init__(
175
+ self,
176
+ base_layer: QKVParallelLinear,
177
+ lora_backend: BaseLoRABackend,
178
+ ) -> None:
179
+ super().__init__(base_layer, lora_backend)
180
+
181
+ def set_lora_info(
182
+ self,
183
+ A_buffer_qkv: torch.Tensor,
184
+ B_buffer_q: torch.Tensor,
185
+ B_buffer_kv: torch.Tensor,
186
+ ):
187
+ self.set_lora = True
188
+ self.A_buffer_qkv = A_buffer_qkv
189
+
190
+ if self.lora_backend.fuse_stacked_lora_b:
191
+ assert (
192
+ B_buffer_q.shape[-1] == B_buffer_kv.shape[-1]
193
+ ), "The lora rank of q and kv should be the same when enabling fusion of qkv lora_b"
194
+ output_dim_q, output_dim_kv = B_buffer_q.shape[-2], B_buffer_kv.shape[-2]
195
+
196
+ # B_buffer_qkv: (num_lora, output_dim_q + 2 * output_dim_kv, r)
197
+ self.B_buffer_qkv = torch.cat(
198
+ (B_buffer_q[0], B_buffer_kv[0], B_buffer_kv[1]), dim=-2
199
+ ).contiguous()
200
+
201
+ # Offsets of q/k/v in output dimension
202
+ self.output_offset = torch.tensor(
203
+ [
204
+ 0,
205
+ output_dim_q,
206
+ output_dim_q + output_dim_kv,
207
+ output_dim_q + 2 * output_dim_kv,
208
+ ],
209
+ dtype=torch.int32,
210
+ device=B_buffer_q.device,
211
+ )
212
+ # For computing number of launched blocks
213
+ self.max_qkv_out_dim = max(output_dim_q, output_dim_kv)
214
+ else:
215
+ self.B_buffer_qkv = (
216
+ B_buffer_q,
217
+ B_buffer_kv,
218
+ )
219
+
220
+ def apply_lora(self, base_output: torch.Tensor, x: torch.Tensor) -> torch.Tensor:
221
+ backend_kwargs = {"base_output": base_output}
222
+ if self.lora_backend.fuse_stacked_lora_b:
223
+ backend_kwargs["output_offset"] = self.output_offset
224
+ backend_kwargs["max_qkv_out_dim"] = self.max_qkv_out_dim
225
+
226
+ lora_output = self.lora_backend.run_qkv_lora(
227
+ x,
228
+ self.A_buffer_qkv,
229
+ self.B_buffer_qkv,
230
+ **backend_kwargs,
231
+ )
232
+ return (
233
+ lora_output
234
+ if self.lora_backend.fuse_output_add
235
+ else base_output + lora_output
236
+ )
237
+
238
+ def slice_lora_a_weights(self, A: torch.Tensor, tp_rank: int):
239
+ return A
240
+
241
+ def slice_lora_b_weights(
242
+ self, B: List[torch.Tensor], tp_rank: int
243
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
244
+ B_q, B_kv = B
245
+ base_layer = self.base_layer
246
+ q_proj_shard_size = base_layer.q_proj_shard_size
247
+ kv_proj_shard_size = base_layer.kv_proj_shard_size
248
+ num_kv_head_replicas = base_layer.num_kv_head_replicas
249
+
250
+ q_start_idx = q_proj_shard_size * tp_rank
251
+ q_end_idx = q_start_idx + q_proj_shard_size
252
+
253
+ kv_shard_id = tp_rank // num_kv_head_replicas
254
+ kv_start_idx = kv_proj_shard_size * kv_shard_id
255
+ kv_end_idx = kv_start_idx + kv_proj_shard_size
256
+
257
+ return B_q[q_start_idx:q_end_idx, :], B_kv[:, kv_start_idx:kv_end_idx, :]
258
+
259
+
260
+ class RowParallelLinearWithLoRA(BaseLayerWithLoRA):
261
+ def __init__(
262
+ self,
263
+ base_layer: RowParallelLinear,
264
+ lora_backend: BaseLoRABackend,
265
+ ) -> None:
266
+ super().__init__(base_layer, lora_backend)
267
+
268
+ def set_lora_info(self, A_buffer: torch.Tensor, B_buffer: torch.Tensor):
269
+ self.set_lora = True
270
+ self.A_buffer = A_buffer
271
+ self.B_buffer = B_buffer
272
+
273
+ def apply_lora(self, base_output: torch.Tensor, x: torch.Tensor) -> torch.Tensor:
274
+ backend_kwargs = {"base_output": base_output}
275
+ lora_a_output = self.lora_backend.run_lora_a_sgemm(x, self.A_buffer)
276
+ lora_output = self.lora_backend.run_lora_b_sgemm(
277
+ lora_a_output,
278
+ self.B_buffer[0],
279
+ **backend_kwargs,
280
+ )
281
+ return (
282
+ lora_output
283
+ if self.lora_backend.fuse_output_add
284
+ else base_output + lora_output
285
+ )
286
+
287
+ def forward(self, input_: torch.Tensor):
288
+ # duplicate the logic in RowParallelLinear
289
+ if self.base_layer.input_is_parallel:
290
+ input_parallel = input_
291
+ else:
292
+ tp_rank = get_tensor_model_parallel_rank()
293
+ splitted_input = split_tensor_along_last_dim(
294
+ input_, num_partitions=self.base_layer.tp_size
295
+ )
296
+ input_parallel = splitted_input[tp_rank].contiguous()
297
+ output_parallel = self.base_layer.quant_method.apply(
298
+ self.base_layer, input_parallel
299
+ )
300
+
301
+ if self.set_lora:
302
+ output_parallel = self.apply_lora(output_parallel, input_parallel)
303
+
304
+ if self.base_layer.reduce_results and self.base_layer.tp_size > 1:
305
+ output_ = tensor_model_parallel_all_reduce(output_parallel)
306
+ else:
307
+ output_ = output_parallel
308
+
309
+ if not self.base_layer.skip_bias_add:
310
+ output = (
311
+ output_ + self.base_layer.bias
312
+ if self.base_layer.bias is not None
313
+ else output_
314
+ )
315
+ output_bias = None
316
+ else:
317
+ output = output_
318
+ output_bias = self.base_layer.bias
319
+ return output, output_bias
320
+
321
+ def slice_lora_a_weights(self, A: torch.Tensor, tp_rank: int):
322
+ shard_size = self.base_layer.input_size_per_partition
323
+ start_idx = tp_rank * shard_size
324
+ end_idx = (tp_rank + 1) * shard_size
325
+ A = A[:, start_idx:end_idx].contiguous()
326
+ return A
327
+
328
+ def slice_lora_b_weights(self, B: torch.Tensor, tp_rank: int):
329
+ return B
330
+
331
+
332
+ def get_lora_layer(
333
+ layer: nn.Module, lora_backend: BaseLoRABackend
334
+ ) -> BaseLayerWithLoRA:
335
+ supported_layer_types = {
336
+ # the order matters
337
+ VocabParallelEmbedding: VocabParallelEmbeddingWithLoRA,
338
+ QKVParallelLinear: QKVParallelLinearWithLoRA,
339
+ MergedColumnParallelLinear: MergedColumnParallelLinearWithLoRA,
340
+ ColumnParallelLinear: ColumnParallelLinearWithLoRA,
341
+ RowParallelLinear: RowParallelLinearWithLoRA,
342
+ }
343
+ for src_layer_type, lora_layer_type in supported_layer_types.items():
344
+ if isinstance(layer, src_layer_type): # pylint: disable=unidiomatic-typecheck
345
+ ret = lora_layer_type(layer, lora_backend)
346
+ return ret
347
+ raise Exception(f"No corresponding LoRA layer supported for {type(layer)}.")
lib/python3.12/site-packages/sglang/srt/lora/lora.py ADDED
@@ -0,0 +1,181 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+
15
+ # Integrates "S-LoRA: Serving Thousands of Concurrent LoRA Adapters"
16
+ # and "Punica: Multi-Tenant LoRA Serving"
17
+
18
+ # LoRA layers class inheritance adapted from:
19
+ # https://github.com/vllm-project/vllm/blob/4abf6336ec65c270343eb895e7b18786e9274176/vllm/lora/layers.py
20
+
21
+ import logging
22
+ import re
23
+ from typing import Dict, List
24
+
25
+ import torch
26
+ from torch import nn
27
+
28
+ from sglang.srt.configs.load_config import LoadConfig
29
+ from sglang.srt.hf_transformers_utils import AutoConfig
30
+ from sglang.srt.lora.backend.base_backend import BaseLoRABackend
31
+ from sglang.srt.lora.lora_config import LoRAConfig
32
+ from sglang.srt.model_loader.loader import DefaultModelLoader
33
+
34
+ logger = logging.getLogger(__name__)
35
+
36
+
37
+ class LoRALayer(nn.Module):
38
+ def __init__(self, config: LoRAConfig, base_hf_config: AutoConfig):
39
+ super().__init__()
40
+ self.config: LoRAConfig = config
41
+ self.base_hf_config: AutoConfig = base_hf_config
42
+
43
+ # lora weights in cpu. The weights are loaded from checkpoint.
44
+ self.weights: Dict[str, torch.Tensor] = {}
45
+
46
+
47
+ class LoRAAdapter(nn.Module):
48
+ def __init__(
49
+ self,
50
+ uid: str,
51
+ config: LoRAConfig,
52
+ base_hf_config: AutoConfig,
53
+ load_config: LoadConfig,
54
+ lora_backend: BaseLoRABackend,
55
+ ):
56
+ super().__init__()
57
+ self.uid: str = uid
58
+ self.config: LoRAConfig = config
59
+ assert self.config.hf_config["peft_type"].lower() == "lora"
60
+ self.base_hf_config: AutoConfig = base_hf_config
61
+ self.load_config: LoadConfig = load_config
62
+ self.lora_backend: BaseLoRABackend = lora_backend
63
+ self.scaling: float = self.config.lora_alpha / self.config.r
64
+
65
+ self.layers: List[LoRALayer] = nn.ModuleList(
66
+ [
67
+ LoRALayer(config, base_hf_config)
68
+ for i in range(base_hf_config.num_hidden_layers)
69
+ ]
70
+ )
71
+
72
+ self.weights: Dict[str, torch.Tensor] = {}
73
+
74
+ # initialize the LoRA weights to cpu
75
+ def initialize_weights(self):
76
+ model_path = self.config.path
77
+ loader = DefaultModelLoader(self.load_config)
78
+ revision = getattr(self.config.hf_config, "revision", None)
79
+ for name, loaded_weight in loader._get_weights_iterator(
80
+ DefaultModelLoader.Source(
81
+ model_path, revision=revision, fall_back_to_pt=True
82
+ )
83
+ ):
84
+ match = re.search(r"layers\.(\d+)\.", name)
85
+ if match is not None:
86
+ layer_id = int(match.group(1))
87
+ self.layers[layer_id].weights[name] = loaded_weight.cpu()
88
+ else:
89
+ self.weights[name] = loaded_weight.cpu()
90
+
91
+ # stack kv_proj and gate_up_proj
92
+ for i in range(self.base_hf_config.num_hidden_layers):
93
+ layer = self.layers[i]
94
+ weight_names = [name for name, _ in layer.weights.items()]
95
+ self.stack_qkv_proj(weight_names, layer.weights)
96
+ self.stack_gate_up_proj(weight_names, layer.weights)
97
+
98
+ def stack_qkv_proj(self, weight_names: List[str], weights: Dict[str, torch.Tensor]):
99
+
100
+ # Collect target q/k/v modules. This process is necessary since there might be no lora attached to k_proj
101
+ target_module = set()
102
+ for weight_name in weight_names:
103
+ if "k_proj" in weight_name:
104
+ target_module.add("k_proj")
105
+ if "q_proj" in weight_name:
106
+ target_module.add("q_proj")
107
+ if "v_proj" in weight_name:
108
+ target_module.add("v_proj")
109
+ if len(target_module) == 0:
110
+ return
111
+
112
+ for weight_name in weight_names:
113
+ # We assume every lora adaptor should contain lora modules for q_proj
114
+ if "q_proj" in weight_name:
115
+ q_name = weight_name
116
+ k_name = weight_name.replace("q_proj", "k_proj")
117
+ v_name = weight_name.replace("q_proj", "v_proj")
118
+ kv_name = weight_name.replace("q_proj", "kv_proj")
119
+ qkv_name = weight_name.replace("q_proj", "qkv_proj")
120
+
121
+ # If k_proj doesn't have lora, initialize it to zero
122
+ k_proj_weight = (
123
+ weights[k_name]
124
+ if "k_proj" in target_module
125
+ else torch.zeros_like(weights[v_name])
126
+ )
127
+ if "lora_A" in weight_name:
128
+ weights[qkv_name] = torch.cat(
129
+ (
130
+ weights[q_name],
131
+ k_proj_weight,
132
+ weights[v_name],
133
+ ),
134
+ 0,
135
+ )
136
+ weights.pop(q_name)
137
+ if "k_proj" in target_module:
138
+ weights.pop(k_name)
139
+ weights.pop(v_name)
140
+ else:
141
+ weights[kv_name] = torch.stack(
142
+ [
143
+ k_proj_weight,
144
+ weights[v_name],
145
+ ],
146
+ dim=0,
147
+ )
148
+ if "k_proj" in target_module:
149
+ weights.pop(k_name)
150
+ weights.pop(v_name)
151
+
152
+ def stack_gate_up_proj(
153
+ self, weight_names: List[str], weights: Dict[str, torch.Tensor]
154
+ ):
155
+ for weight_name in weight_names:
156
+ if "gate_proj" in weight_name:
157
+ up_name = weight_name.replace("gate_proj", "up_proj")
158
+ gate_up_name = weight_name.replace("gate_proj", "gate_up_proj")
159
+ if up_name not in weights:
160
+ logger.warning(
161
+ f"Gate projection {weight_name} does not have a corresponding up projection {up_name}. "
162
+ f"Initializing up projection to zero."
163
+ )
164
+ weights[up_name] = torch.zeros_like(weights[weight_name])
165
+ # FIXME: Add gate-only support for flashinfer in future implementations
166
+ assert self.lora_backend.name == "triton", (
167
+ f"LoRA weight initialization currently only supported for 'triton' backend. "
168
+ f"Received backend: {self.lora_backend.name}. Please verify your backend configuration "
169
+ f"or consider implementing custom initialization logic for other backends."
170
+ )
171
+ if "lora_A" in weight_name:
172
+ weights[gate_up_name] = torch.cat(
173
+ (weights[weight_name], weights[up_name]), 0
174
+ )
175
+ else:
176
+ weights[gate_up_name] = torch.stack(
177
+ [weights[weight_name], weights[up_name]], dim=0
178
+ )
179
+ weights.pop(weight_name)
180
+ if up_name in weights:
181
+ weights.pop(up_name)
lib/python3.12/site-packages/sglang/srt/lora/lora_config.py ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+
15
+ import json
16
+ import os
17
+
18
+ from huggingface_hub import snapshot_download
19
+
20
+
21
+ class LoRAConfig:
22
+ def __init__(
23
+ self,
24
+ path: str,
25
+ ) -> None:
26
+ self.path = path
27
+ self.hf_config = self.get_lora_config()
28
+ self.target_modules = self.hf_config["target_modules"]
29
+
30
+ # TODO: Support more modules
31
+ if any(module in self.target_modules for module in ["embed_tokens", "lm_head"]):
32
+ raise ValueError("Not supported yet")
33
+
34
+ self.r = self.hf_config["r"]
35
+ self.lora_alpha = self.hf_config["lora_alpha"]
36
+
37
+ def get_lora_config(self, dummy=False):
38
+ if dummy:
39
+ raise NotImplementedError()
40
+ else:
41
+ if not os.path.isdir(self.path):
42
+ weights_dir = snapshot_download(self.path, allow_patterns=["*.json"])
43
+ else:
44
+ weights_dir = self.path
45
+ config_name = "adapter_config.json"
46
+ with open(os.path.join(weights_dir, config_name), "r") as f:
47
+ return json.load(f)
lib/python3.12/site-packages/sglang/srt/lora/lora_manager.py ADDED
@@ -0,0 +1,228 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+
15
+ # Integrates "S-LoRA: Serving Thousands of Concurrent LoRA Adapters"
16
+ # and "Punica: Multi-Tenant LoRA Serving"
17
+
18
+ import logging
19
+ from typing import Dict, List, Set, Tuple
20
+
21
+ import torch
22
+
23
+ from sglang.srt.configs.load_config import LoadConfig
24
+ from sglang.srt.hf_transformers_utils import AutoConfig
25
+ from sglang.srt.lora.backend.base_backend import BaseLoRABackend, get_backend_from_name
26
+ from sglang.srt.lora.layers import BaseLayerWithLoRA, get_lora_layer
27
+ from sglang.srt.lora.lora import LoRAAdapter
28
+ from sglang.srt.lora.lora_config import LoRAConfig
29
+ from sglang.srt.lora.mem_pool import LoRAMemoryPool
30
+ from sglang.srt.lora.utils import (
31
+ LoRABatchInfo,
32
+ LoRAType,
33
+ get_customized_names_from_hf_names,
34
+ get_layer_id,
35
+ get_stacked_name,
36
+ get_weight_name,
37
+ )
38
+ from sglang.srt.model_executor.forward_batch_info import ForwardBatch
39
+ from sglang.srt.utils import replace_submodule
40
+
41
+ logger = logging.getLogger(__name__)
42
+
43
+
44
+ class LoRAManager:
45
+ def __init__(
46
+ self,
47
+ base_model: torch.nn.Module,
48
+ lora_paths: Dict[str, str],
49
+ base_hf_config: AutoConfig,
50
+ max_loras_per_batch: int,
51
+ load_config: LoadConfig,
52
+ dtype: torch.dtype,
53
+ lora_backend: str = "triton",
54
+ tp_size: int = 1,
55
+ tp_rank: int = 0,
56
+ ):
57
+ self.base_model: torch.nn.Module = base_model
58
+ self.lora_paths: Dict[str, str] = lora_paths
59
+ self.base_hf_config: AutoConfig = base_hf_config
60
+ self.max_loras_per_batch: int = max_loras_per_batch
61
+ self.load_config: LoadConfig = load_config
62
+ self.dtype: torch.dtype = dtype
63
+ self.device: torch.device = next(self.base_model.parameters()).device
64
+ self.tp_size: int = tp_size
65
+ self.tp_rank: int = tp_rank
66
+
67
+ # LoRA backend for running sgemm kernels
68
+ logger.info(f"Using {lora_backend} as backend of LoRA kernels.")
69
+ backend_type = get_backend_from_name(lora_backend)
70
+ self.lora_backend: BaseLoRABackend = backend_type(lora_backend)
71
+
72
+ self.init_loras()
73
+ self.init_lora_memory_pool()
74
+
75
+ def init_loras(self):
76
+ # Config of each LoRA adapter
77
+ self.configs: Dict[str, LoRAConfig] = {}
78
+
79
+ # Target module names in huggingface lora configs.
80
+ # e.g., {"k_proj", "q_proj", "v_proj", "o_proj"}
81
+ self.hf_target_names: Set[str] = set()
82
+ for name, path in self.lora_paths.items():
83
+ self.configs[name] = LoRAConfig(path)
84
+ self.hf_target_names.update(self.configs[name].target_modules)
85
+
86
+ # Target lora weight names for lora_a and lora_b modules repectively.
87
+ # e.g., {("qkv_proj", "q_proj"), ("qkv_proj", "kv_proj")}
88
+ self.lora_weight_names: Set[Tuple[str]] = set(
89
+ [get_stacked_name(module) for module in self.hf_target_names]
90
+ )
91
+
92
+ # load all weights to cpu
93
+ self.loras: Dict[str, LoRAAdapter] = {}
94
+ for name in self.lora_paths.keys():
95
+ lora_adapter = LoRAAdapter(
96
+ name,
97
+ self.configs[name],
98
+ self.base_hf_config,
99
+ self.load_config,
100
+ self.lora_backend,
101
+ )
102
+ lora_adapter.initialize_weights()
103
+ self.loras[name] = lora_adapter
104
+
105
+ # misc lora configs
106
+ self.max_lora_dim: int = max([x.hf_config["r"] for x in self.configs.values()])
107
+
108
+ if self.lora_backend == "flashinfer":
109
+ # FIXME remove the restrictions after supporting multi-rank for flashinfer backend
110
+ max_lora_dim = max([x.hf_config["r"] for x in self.configs.values()])
111
+ scaling = list(self.loras.values())[0].scaling
112
+ assert all(x.hf_config["r"] == max_lora_dim for x in self.configs.values())
113
+ assert all(x.scaling == scaling for x in self.loras.values())
114
+
115
+ # Convert original model layers to layers with LoRA
116
+ self.convert_to_lora_layers()
117
+
118
+ def init_lora_memory_pool(self):
119
+ # Initialize memory pool
120
+ self.memory_pool = LoRAMemoryPool(
121
+ self.base_hf_config,
122
+ self.max_loras_per_batch,
123
+ self.max_lora_dim,
124
+ self.dtype,
125
+ self.tp_size,
126
+ self.tp_rank,
127
+ self.lora_modules,
128
+ )
129
+
130
+ # Initialize target lora modules in memory pool
131
+ self.memory_pool.init_buffers(self.lora_weight_names, self.base_model)
132
+
133
+ def prepare_lora_batch(self, forward_batch: ForwardBatch):
134
+ # load active loras into lora memory pool
135
+ cur_uids = set(forward_batch.lora_paths)
136
+ assert len(cur_uids) <= self.max_loras_per_batch
137
+ self.memory_pool.prepare_lora_batch(cur_uids, self.loras)
138
+
139
+ # FIXME: Handle lora uid with None more safely
140
+ if cur_uids == set([None]):
141
+ return
142
+
143
+ # set up batch info shared by all lora moruldes
144
+ bs = forward_batch.batch_size
145
+ seg_lens = (
146
+ forward_batch.extend_seq_lens
147
+ if forward_batch.forward_mode.is_extend()
148
+ else torch.ones(bs, device=self.device)
149
+ )
150
+ seg_indptr = torch.zeros((bs + 1,), dtype=torch.int32, device=self.device)
151
+ seg_indptr[1:] = torch.cumsum(seg_lens, dim=0)
152
+ max_len = int(torch.max(seg_lens))
153
+ weight_indices = torch.empty((bs,), dtype=torch.int64, device=self.device)
154
+
155
+ lora_ranks = torch.empty(
156
+ (self.max_loras_per_batch,), dtype=torch.int64, device="cuda"
157
+ )
158
+ scalings = torch.empty(
159
+ (self.max_loras_per_batch,), dtype=torch.float, device="cuda"
160
+ )
161
+ for i, lora_path in enumerate(forward_batch.lora_paths):
162
+ weight_indices[i] = self.memory_pool.get_buffer_id(lora_path)
163
+ lora = self.loras[lora_path]
164
+ lora_ranks[weight_indices[i]] = lora.config.hf_config["r"]
165
+ scalings[weight_indices[i]] = lora.scaling
166
+
167
+ batch_info = LoRABatchInfo(
168
+ bs=bs,
169
+ seg_lens=seg_lens,
170
+ seg_indptr=seg_indptr,
171
+ max_len=max_len,
172
+ weight_indices=weight_indices,
173
+ lora_ranks=lora_ranks,
174
+ scalings=scalings,
175
+ )
176
+ self.lora_backend.set_batch_info(batch_info)
177
+
178
+ # call set_lora_info for each lora modules
179
+ for layer_id, modules in self.lora_modules.items():
180
+ for module_name, module in modules:
181
+ if "qkv_proj" in module_name:
182
+ module.set_lora_info(
183
+ self.memory_pool.get_tensor(
184
+ "qkv_proj", layer_id, LoRAType.LORA_A
185
+ ),
186
+ self.memory_pool.get_tensor(
187
+ "q_proj", layer_id, LoRAType.LORA_B
188
+ ),
189
+ self.memory_pool.get_tensor(
190
+ "kv_proj", layer_id, LoRAType.LORA_B
191
+ ),
192
+ )
193
+ else:
194
+ weight_name = get_weight_name(
195
+ module_name, self.lora_weight_names, LoRAType.LORA_A
196
+ )
197
+ module.set_lora_info(
198
+ self.memory_pool.get_tensor(
199
+ weight_name, layer_id, LoRAType.LORA_A
200
+ ),
201
+ self.memory_pool.get_tensor(
202
+ weight_name, layer_id, LoRAType.LORA_B
203
+ ),
204
+ )
205
+
206
+ def set_lora_module(self, module_name, module):
207
+ lora_module = get_lora_layer(module, self.lora_backend)
208
+ replace_submodule(self.base_model, module_name, lora_module)
209
+ return lora_module
210
+
211
+ def convert_to_lora_layers(self):
212
+ # Target module names of customized layers defined in python/sglang/srt/layers
213
+ # e.g., {"qkv_proj", "o_proj"}
214
+ customized_target_names = get_customized_names_from_hf_names(
215
+ self.hf_target_names, self.base_model
216
+ )
217
+
218
+ # Monkey patch to use the LoRA version layers
219
+ self.lora_modules: Dict[int, List[Tuple[str, BaseLayerWithLoRA]]] = {
220
+ i: [] for i in range(self.base_hf_config.num_hidden_layers)
221
+ }
222
+ for module_name, module in self.base_model.named_modules():
223
+ # The module should be converted if it is included in target_names
224
+ if module_name.split(".")[-1] in customized_target_names:
225
+ layer_id = get_layer_id(module_name)
226
+ self.lora_modules[layer_id].append(
227
+ (module_name, self.set_lora_module(module_name, module))
228
+ )
lib/python3.12/site-packages/sglang/srt/lora/mem_pool.py ADDED
@@ -0,0 +1,239 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Dict, List, Optional, Set, Tuple
2
+
3
+ import torch
4
+
5
+ from sglang.srt.distributed import divide
6
+ from sglang.srt.hf_transformers_utils import AutoConfig
7
+ from sglang.srt.lora.layers import BaseLayerWithLoRA
8
+ from sglang.srt.lora.lora import LoRAAdapter
9
+ from sglang.srt.lora.utils import (
10
+ ROW_PARALLELISM_LINEAR_LORA_NAMES,
11
+ LoRAType,
12
+ get_hidden_dim,
13
+ get_stacked_multiply,
14
+ get_weight_name,
15
+ )
16
+
17
+
18
+ class LoRAMemoryPool:
19
+ """Class for memory pool management of lora modules"""
20
+
21
+ def __init__(
22
+ self,
23
+ base_hf_config: AutoConfig,
24
+ max_loras_per_batch: int,
25
+ max_lora_dim: int,
26
+ dtype: torch.dtype,
27
+ tp_size: int,
28
+ tp_rank: int,
29
+ lora_modules: Dict[int, List[Tuple[str, BaseLayerWithLoRA]]],
30
+ ):
31
+
32
+ self.base_hf_config: AutoConfig = base_hf_config
33
+ self.num_layer: int = base_hf_config.num_hidden_layers
34
+ self.max_loras_per_batch: int = max_loras_per_batch
35
+ self.max_lora_dim: int = max_lora_dim
36
+ self.dtype: torch.dtype = dtype
37
+ self.tp_size: int = tp_size
38
+ self.tp_rank: int = tp_rank
39
+ self.lora_modules: Dict[int, List[Tuple[str, BaseLayerWithLoRA]]] = lora_modules
40
+
41
+ # Both A_buffer and B_buffer maps lora weight names to its buffer space.
42
+ # A_buffer contains num_layer number of row-major tensors with shape
43
+ # (max_loras_per_batch, stacked_num * max_lora_dim, input_dim)
44
+ # B_buffer contains num_layer number of column-major tensors with shape
45
+ # (stacked_num, max_loras_per_batch, output_dim, max_lora_dim)
46
+ self.A_buffer: Dict[str, List[torch.Tensor]] = {}
47
+ self.B_buffer: Dict[str, List[torch.Tensor]] = {}
48
+
49
+ # Lora uid -> buffer idx in memory pool
50
+ self.uid_to_buffer_id: Dict[Optional[str], int] = {}
51
+
52
+ # Buffer idx -> lora uid in memory pool
53
+ # All uids are initalized as empty strings for empty buffer slots
54
+ # Here we don't initalize to None since None is a valid uid
55
+ self.buffer_id_to_uid: List[Optional[str]] = [""] * self.max_loras_per_batch
56
+
57
+ def get_lora_A_shape(
58
+ self, module_name: str, base_model: torch.nn.Module
59
+ ) -> Tuple[int]:
60
+ """
61
+ Given a module_name (might be a stacked name), return the hidden dims of modules's input and output.
62
+ """
63
+ input_dim, _ = get_hidden_dim(module_name, self.base_hf_config, base_model)
64
+ c = get_stacked_multiply(module_name)
65
+ if self.tp_size > 1:
66
+ if module_name in ROW_PARALLELISM_LINEAR_LORA_NAMES:
67
+ input_dim = divide(input_dim, self.tp_size)
68
+ return (
69
+ self.max_loras_per_batch,
70
+ self.max_lora_dim * c,
71
+ input_dim,
72
+ )
73
+
74
+ def get_lora_B_shape(
75
+ self, module_name: str, base_model: torch.nn.Module
76
+ ) -> Tuple[int]:
77
+ """
78
+ Given a module_name (might be a stacked name), return the hidden dims of modules's input and output.
79
+ """
80
+ _, output_dim = get_hidden_dim(module_name, self.base_hf_config, base_model)
81
+ c = get_stacked_multiply(module_name)
82
+ if self.tp_size > 1:
83
+ if module_name not in ROW_PARALLELISM_LINEAR_LORA_NAMES:
84
+ output_dim = divide(output_dim, self.tp_size)
85
+ return (
86
+ c,
87
+ self.max_loras_per_batch,
88
+ output_dim,
89
+ self.max_lora_dim,
90
+ )
91
+
92
+ def init_buffers(
93
+ self,
94
+ lora_weight_names: Set[Tuple[str]],
95
+ base_model: torch.nn.Module,
96
+ ):
97
+
98
+ # lora_weight_names is a set of name pairs indicating each pair of lora modules to load
99
+ # e.g., {("qkv_proj", "q_proj"), ("qkv_proj", "kv_proj"), ("o_proj", "o_proj")}
100
+ self.lora_weight_names: Set[Tuple[str]] = lora_weight_names
101
+ device = next(base_model.parameters()).device
102
+ lora_module_A_names = set([name[0] for name in lora_weight_names])
103
+ lora_module_B_names = set([name[1] for name in lora_weight_names])
104
+ # Init A tensor, column_major=False
105
+ for module_A in lora_module_A_names:
106
+ lora_A_shape = self.get_lora_A_shape(module_A, base_model)
107
+ self.A_buffer[module_A] = [
108
+ torch.empty(
109
+ lora_A_shape,
110
+ dtype=self.dtype,
111
+ device=device,
112
+ )
113
+ for i in range(self.num_layer)
114
+ ]
115
+ # Init B tensor, column_major=True
116
+ for module_B in lora_module_B_names:
117
+ lora_B_shape = self.get_lora_B_shape(module_B, base_model)
118
+ self.B_buffer[module_B] = [
119
+ torch.empty(
120
+ lora_B_shape,
121
+ dtype=self.dtype,
122
+ device=device,
123
+ )
124
+ for _ in range(self.num_layer)
125
+ ]
126
+
127
+ def prepare_lora_batch(
128
+ self,
129
+ cur_uids: Set[Optional[str]],
130
+ lora_adapters: Dict[str, LoRAAdapter],
131
+ ):
132
+
133
+ def get_available_buffer_slot():
134
+ for buffer_id in range(self.max_loras_per_batch):
135
+ # Prioritize empty slots
136
+ if self.buffer_id_to_uid[buffer_id] == "":
137
+ return buffer_id, ""
138
+
139
+ for buffer_id in range(self.max_loras_per_batch):
140
+ # Evict unneeded lora
141
+ if self.buffer_id_to_uid[buffer_id] not in cur_uids:
142
+ return buffer_id, self.buffer_id_to_uid[buffer_id]
143
+
144
+ raise ValueError(
145
+ "No available buffer slots found. Please ensure the number of active loras is less than max_loras_per_batch."
146
+ )
147
+
148
+ for uid in cur_uids:
149
+ if uid not in self.uid_to_buffer_id:
150
+ buffer_id, evicted_lora_uid = get_available_buffer_slot()
151
+ if evicted_lora_uid != "":
152
+ self.uid_to_buffer_id.pop(evicted_lora_uid)
153
+ self.load_lora_weight_to_buffer(
154
+ uid, buffer_id, lora_adapters.get(uid, None)
155
+ )
156
+ self.uid_to_buffer_id[uid] = buffer_id
157
+ self.buffer_id_to_uid[buffer_id] = uid
158
+
159
+ def load_lora_weight_to_buffer(
160
+ self, uid: str, buffer_id: int, lora_adapter: LoRAAdapter = None
161
+ ):
162
+
163
+ if uid is None:
164
+ for i in range(self.num_layer):
165
+ for k in self.A_buffer.keys():
166
+ self.A_buffer[k][i][buffer_id] = 0
167
+ return
168
+
169
+ assert lora_adapter is not None
170
+ lora_rank = lora_adapter.config.hf_config["r"]
171
+ for layer_id in range(self.num_layer):
172
+ layer_weights = lora_adapter.layers[layer_id].weights
173
+ temp_A_buffer: Dict[str, torch.Tensor] = {}
174
+ temp_B_buffer: Dict[str, torch.Tensor] = {}
175
+ for name, weights in layer_weights.items():
176
+ if "lora_A" in name:
177
+ lora_weight_name = get_weight_name(
178
+ name, self.lora_weight_names, LoRAType.LORA_A
179
+ )
180
+ temp_A_buffer[lora_weight_name] = weights
181
+ else:
182
+ lora_weight_name = get_weight_name(
183
+ name, self.lora_weight_names, LoRAType.LORA_B
184
+ )
185
+ temp_B_buffer[lora_weight_name] = weights
186
+
187
+ if self.tp_size > 1:
188
+ cur_layer_modules = self.lora_modules[layer_id]
189
+ for module_name, module in cur_layer_modules:
190
+ if "qkv_proj" in module_name:
191
+ temp_A_buffer["qkv_proj"] = module.slice_lora_a_weights(
192
+ temp_A_buffer["qkv_proj"], self.tp_rank
193
+ )
194
+ temp_B_buffer["q_proj"], temp_B_buffer["kv_proj"] = (
195
+ module.slice_lora_b_weights(
196
+ [temp_B_buffer["q_proj"], temp_B_buffer["kv_proj"]],
197
+ self.tp_rank,
198
+ )
199
+ )
200
+ else:
201
+ weight_name = get_weight_name(
202
+ module_name, self.lora_weight_names, LoRAType.LORA_A
203
+ )
204
+ temp_A_buffer[weight_name] = module.slice_lora_a_weights(
205
+ temp_A_buffer[weight_name], self.tp_rank
206
+ )
207
+ temp_B_buffer[weight_name] = module.slice_lora_b_weights(
208
+ temp_B_buffer[weight_name], self.tp_rank
209
+ )
210
+
211
+ for name, weights in temp_A_buffer.items():
212
+ c = get_stacked_multiply(name)
213
+ self.A_buffer[name][layer_id][buffer_id][: lora_rank * c, :].copy_(
214
+ weights
215
+ )
216
+
217
+ for name, weights in temp_B_buffer.items():
218
+ c = get_stacked_multiply(name)
219
+ if c > 1:
220
+ for stacked_id in range(c):
221
+ self.B_buffer[name][layer_id][stacked_id][buffer_id][
222
+ :, :lora_rank
223
+ ].copy_(weights[stacked_id])
224
+ else:
225
+ self.B_buffer[name][layer_id][0][buffer_id][:, :lora_rank].copy_(
226
+ weights
227
+ )
228
+
229
+ def get_tensor(
230
+ self, weight_name: str, layer_id: int, lora_type: LoRAType
231
+ ) -> torch.Tensor:
232
+
233
+ if lora_type == LoRAType.LORA_A:
234
+ return self.A_buffer[weight_name][layer_id]
235
+
236
+ return self.B_buffer[weight_name][layer_id]
237
+
238
+ def get_buffer_id(self, lora_uid: str):
239
+ return self.uid_to_buffer_id[lora_uid]
lib/python3.12/site-packages/sglang/srt/lora/triton_ops/__init__.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from .gate_up_lora_b import gate_up_lora_b_fwd
2
+ from .qkv_lora_b import qkv_lora_b_fwd
3
+ from .sgemm_lora_a import sgemm_lora_a_fwd
4
+ from .sgemm_lora_b import sgemm_lora_b_fwd
5
+
6
+ __all__ = [
7
+ "gate_up_lora_b_fwd",
8
+ "qkv_lora_b_fwd",
9
+ "sgemm_lora_a_fwd",
10
+ "sgemm_lora_b_fwd",
11
+ ]
lib/python3.12/site-packages/sglang/srt/lora/triton_ops/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (469 Bytes). View file
 
lib/python3.12/site-packages/sglang/srt/lora/triton_ops/__pycache__/gate_up_lora_b.cpython-312.pyc ADDED
Binary file (5.47 kB). View file
 
lib/python3.12/site-packages/sglang/srt/lora/triton_ops/__pycache__/qkv_lora_b.cpython-312.pyc ADDED
Binary file (5.8 kB). View file
 
lib/python3.12/site-packages/sglang/srt/lora/triton_ops/__pycache__/sgemm_lora_a.cpython-312.pyc ADDED
Binary file (5.2 kB). View file