diff --git a/lightllm/server/core/objs/sampling_params.py b/lightllm/server/core/objs/sampling_params.py index 8e31c5062..25ac7f26f 100644 --- a/lightllm/server/core/objs/sampling_params.py +++ b/lightllm/server/core/objs/sampling_params.py @@ -66,31 +66,42 @@ def initialize(self, stop_sequences: Union[str, List[Union[List[int], str]]], to elif isinstance(stop_sequences, str): stop_sequences = [stop_sequences] - groups: List[List[int]] = self.stop_sentences_to_token_ids(stop_sequences, tokenizer) + # 这里必须使用 (token_ids, 原始条目) 的配对结果:空条目(如 "" 或 [])会被过滤掉, + # 若还按原始下标去取 stop_sequences[group_idx],后面的条目就会和 token id 组错位, + # 导致 sequence_str 挂到别的组上(丢失停止字符串,或凭空多出一个停止字符串)。 + groups: List[Tuple[List[int], Union[List[int], str]]] = self._stop_sentences_to_token_id_pairs( + stop_sequences, tokenizer + ) self.size = len(groups) assert self.size <= MAX_STOP_SEQUENCES, "Too many stop sequence groups." - for group_idx in range(self.size): - if isinstance(stop_sequences[group_idx], str): - self.groups[group_idx].initialize(groups[group_idx], sequence_str=stop_sequences[group_idx]) + for group_idx, (token_ids, stop_info) in enumerate(groups): + if isinstance(stop_info, str): + self.groups[group_idx].initialize(token_ids, sequence_str=stop_info) else: - self.groups[group_idx].initialize(groups[group_idx]) + self.groups[group_idx].initialize(token_ids) - def stop_sentences_to_token_ids(self, stop_sequences: List[Union[List[int], str]], tokenizer) -> List[List[int]]: + def _stop_sentences_to_token_id_pairs( + self, stop_sequences: List[Union[List[int], str]], tokenizer + ) -> List[Tuple[List[int], Union[List[int], str]]]: + """返回 (token_ids, 原始 stop 条目) 的列表,保证两者一一对应。""" new_stop_sequences = [] for stop_info in stop_sequences: if isinstance(stop_info, str): stop_str_ids = self._stop_str_to_token_ids(stop_info, tokenizer) if stop_str_ids is not None and len(stop_str_ids) > 0: - new_stop_sequences.append(stop_str_ids) + new_stop_sequences.append((stop_str_ids, stop_info)) if isinstance(stop_info, list): if all(isinstance(x, int) for x in stop_info): if len(stop_info) > 0: - new_stop_sequences.append(stop_info) + new_stop_sequences.append((stop_info, stop_info)) else: assert False, "stop_sequences item must be type List[int] when it is a list." return new_stop_sequences + def stop_sentences_to_token_ids(self, stop_sequences: List[Union[List[int], str]], tokenizer) -> List[List[int]]: + return [token_ids for token_ids, _ in self._stop_sentences_to_token_id_pairs(stop_sequences, tokenizer)] + def _stop_str_to_token_ids(self, stop_str: str, tokenizer) -> List[int]: stop_str_ids = tokenizer.encode(stop_str, add_special_tokens=False) return stop_str_ids @@ -202,7 +213,7 @@ class AllowedTokenIds(ctypes.Structure): def initialize(self, ids: List[int]): self.size = len(ids) assert self.size <= ALLOWED_TOKEN_IDS_MAX_LENGTH, "Too many allowed token IDs." - assert all(isinstance(e, int) for e in self.ids), "all must be int" + assert all(isinstance(e, int) for e in ids), "all must be int" self.ids[: self.size] = ids[:] def to_list(self): diff --git a/unit_tests/server/core/objs/test_sampling_params.py b/unit_tests/server/core/objs/test_sampling_params.py index ef3f08d2f..9ba46c152 100644 --- a/unit_tests/server/core/objs/test_sampling_params.py +++ b/unit_tests/server/core/objs/test_sampling_params.py @@ -5,10 +5,10 @@ RegularConstraint, AllowedTokenIds, ExponentialDecayLengthPenalty, - DecodeNode, SamplingParams, GuidedGrammar, GuidedJsonSchema, + NodeUUId, STOP_SEQUENCE_MAX_LENGTH, REGULAR_CONSTRAINT_MAX_LENGTH, ALLOWED_TOKEN_IDS_MAX_LENGTH, @@ -117,22 +117,41 @@ def test_exponential_decay_length_penalty_initialization(): penalty.initialize((5, 0.5)) -def test_decode_node_initialization(): - node = DecodeNode() - data = { - "node_id": 12345678901234567890, # 示例 UUID - "ip": "192.168.1.1", - "rpyc_port": 8080, - "max_new_tokens": 10, - } - node.initialize(data) - assert node.exists is True - assert node.node_id.node_id_high == (12345678901234567890 >> 64) & 0xFFFFFFFFFFFFFFFF - assert node.node_id.node_id_low == 12345678901234567890 & 0xFFFFFFFFFFFFFFFF - assert node.ip[0] == 192 - assert node.ip[1] == 168 - assert node.ip[2] == 1 - assert node.ip[3] == 1 +def test_node_uuid_roundtrip(): + node_id = 12345678901234567890 + uuid = NodeUUId() + uuid.initialize(node_id) + assert uuid.node_id_high == (node_id >> 64) & 0xFFFFFFFFFFFFFFFF + assert uuid.node_id_low == node_id & 0xFFFFFFFFFFFFFFFF + assert uuid.get() == node_id + + +def test_allowed_token_ids_rejects_non_int(): + allowed_ids = AllowedTokenIds() + with pytest.raises(AssertionError): + allowed_ids.initialize([1, "2", 3]) + + +@pytest.mark.parametrize( + "stop_sequences, expected_token_ids, expected_strings", + [ + # 全部有效,无过滤,token id 组与字符串一一对应。 + (["stop1", "stop2"], [[1, 2], [3, 4]], ["stop1", "stop2"]), + # 前置的空字符串会被过滤掉,后面的条目不能因此错位。 + (["", "stop1"], [[1, 2]], ["stop1"]), + # 前置的空 id 列表同理。 + ([[], "stop2"], [[3, 4]], ["stop2"]), + # 被过滤掉的字符串条目不能把自己的字符串挂到后一个条目的 token id 上。 + (["unknown", "stop2"], [[3, 4]], ["stop2"]), + # 纯 id 条目不携带字符串。 + ([[7, 8], "stop1"], [[7, 8], [1, 2]], ["stop1"]), + ], +) +def test_stop_sequence_groups_keeps_ids_and_strings_aligned(stop_sequences, expected_token_ids, expected_strings): + groups = StopSequenceGroups() + groups.initialize(stop_sequences, MockTokenizer()) + assert groups.to_list() == expected_token_ids + assert sorted(groups.to_strings()) == sorted(expected_strings) def test_sampling_params_initialization():