215 lines
6.6 KiB
Python
215 lines
6.6 KiB
Python
import re
|
|
import torch
|
|
from copy import deepcopy
|
|
from dataclasses import dataclass, field
|
|
from enum import Enum
|
|
from modelscope.preprocessors.templates.utils import Messages
|
|
from typing import List
|
|
|
|
from swift.infer_engine.protocol import ChatCompletionResponseChoice
|
|
|
|
|
|
class SampleStatus(Enum):
|
|
INITIAL = 'initial'
|
|
TO_INFER = 'to_infer'
|
|
FINISH_NEXT_INFER = 'finish_next_infer'
|
|
FINISHED = 'finished'
|
|
ROLLBACK = 'rollback'
|
|
|
|
|
|
class FinishedReason(Enum):
|
|
ANSWER = 'finished_with_answer'
|
|
MAX_INFER_STEP = 'finished_with_max_infer_steps'
|
|
UNFINISHED = 'unfinished'
|
|
|
|
|
|
@dataclass
|
|
class DataSampleTree:
|
|
"""
|
|
Attributes:
|
|
tree_idx (str):
|
|
for example 0/1-2/2-3/4-0, root_node = 0, next node = 1-2 infer batch 1 and index 2 sample
|
|
|
|
last_response (ChatCompletionResponseChoice):
|
|
vllm previous round output
|
|
"""
|
|
tree_idx: str
|
|
request_id: str
|
|
|
|
messages: Messages
|
|
logprobs: List[List[float]] = field(default_factory=list)
|
|
|
|
all_response_ids: List[List[int]] = field(default_factory=list)
|
|
last_response: ChatCompletionResponseChoice = None
|
|
|
|
token_count_per_step: List[int] = field(default_factory=list)
|
|
|
|
status: SampleStatus = SampleStatus.INITIAL
|
|
finished_reason: FinishedReason = FinishedReason.UNFINISHED
|
|
|
|
@property
|
|
def root_node(self):
|
|
return int(self.tree_idx.split('/')[0])
|
|
|
|
@property
|
|
def depth(self):
|
|
return len(self.tree_idx.split('/')) - 1
|
|
|
|
@property
|
|
def response_num(self):
|
|
return len(self.all_response_ids)
|
|
|
|
def response_truncate(self, truncate_len: int):
|
|
"""
|
|
Before rollback, truncate the response.
|
|
"""
|
|
|
|
if truncate_len < 1:
|
|
return
|
|
|
|
self.logprobs = self.logprobs[:-truncate_len]
|
|
self.all_response_ids = self.all_response_ids[:-truncate_len]
|
|
self.messages = self.messages[:-(truncate_len * 2 - 1)]
|
|
self.last_response = None
|
|
|
|
def extend_response(self, choice: ChatCompletionResponseChoice):
|
|
self.extend_response_text(choice.message.content)
|
|
self.extend_logprobs([item['logprob'] for item in choice.logprobs['content']])
|
|
|
|
self.all_response_ids.append(choice.token_ids)
|
|
self.token_count_per_step.append(len(choice.token_ids))
|
|
|
|
choice.logprobs = None
|
|
self.last_response = deepcopy(choice)
|
|
|
|
def extend_response_text(self, response_text: str):
|
|
self.messages.append({'role': 'assistant', 'content': response_text})
|
|
|
|
def extend_logprobs(self, logprobs: List[float]):
|
|
self.logprobs.append(logprobs)
|
|
|
|
|
|
def _repeat_list_interleave(any_list, repeat_times):
|
|
# return [item for sublist in [[item] * repeat_times for item in any_list] for item in sublist]
|
|
return [deepcopy(item) for sublist in [[item] * repeat_times for item in any_list] for item in sublist]
|
|
|
|
|
|
def _increment_tree_idx_depth(
|
|
samples: list[DataSampleTree],
|
|
next_infer_step: int,
|
|
) -> list[DataSampleTree]:
|
|
for infer_batch_idx, sample in enumerate(samples):
|
|
sample.tree_idx = sample.tree_idx + '/' + f'{next_infer_step}-{infer_batch_idx}'
|
|
return samples
|
|
|
|
|
|
def extract_last_boxed(text):
|
|
pattern = r'\\boxed\{((?:[^{}]|\{(?:[^{}]|\{[^{}]*\})*\})*)\}'
|
|
|
|
matches = list(re.finditer(pattern, text))
|
|
if matches:
|
|
return matches[-1].group(0)
|
|
return None
|
|
|
|
|
|
class AbstractDivergence:
|
|
|
|
@classmethod
|
|
def calc_weights(cls, root_idx, samples_to_go_deeper, **kwargs) -> List[float]:
|
|
pass
|
|
|
|
@classmethod
|
|
def allocate_with_weights(cls, weights, budget, max_divergence) -> List[int]:
|
|
n = len(weights)
|
|
alloc = [0] * n
|
|
|
|
w = [float(wi) if wi is not None and wi > 0 else 0.0 for wi in weights]
|
|
total_w = sum(w)
|
|
if total_w == 0:
|
|
return alloc
|
|
|
|
# first round of allocation by weight ratio
|
|
ideals = [(w[i] / total_w) * budget if w[i] > 0 else 0.0 for i in range(n)]
|
|
for i in range(n):
|
|
if w[i] <= 0:
|
|
continue
|
|
f = int(ideals[i])
|
|
alloc[i] = min(f, max_divergence)
|
|
|
|
# second round of allocation by greedy allocation
|
|
remain = budget - sum(alloc)
|
|
if remain <= 0:
|
|
return alloc
|
|
|
|
# weights desc, index asc
|
|
remainders = [(ideals[i] - int(ideals[i]), i) for i in range(n) if w[i] > 0 and alloc[i] < max_divergence]
|
|
remainders.sort(key=lambda x: (-x[0], x[1]))
|
|
|
|
idx = 0
|
|
while remain > 0 and remainders:
|
|
frac, i = remainders[idx % len(remainders)]
|
|
if alloc[i] < max_divergence:
|
|
alloc[i] += 1
|
|
remain -= 1
|
|
|
|
if alloc[i] >= max_divergence:
|
|
remainders = [r for r in remainders if r[1] != i]
|
|
idx = 0
|
|
continue
|
|
idx += 1
|
|
|
|
return alloc
|
|
|
|
@classmethod
|
|
def apply(cls, root_idx, samples_to_go_deeper, divergence_budget, max_divergence, **kwargs) -> List[DataSampleTree]:
|
|
"""
|
|
Args:
|
|
root_idx: current root node idx
|
|
samples_to_go_deeper: go deeper samples which root_node = root_idx
|
|
divergence_budget: total divergence
|
|
max_divergence: each sample max divergence
|
|
"""
|
|
weights = cls.calc_weights(root_idx, samples_to_go_deeper, **kwargs)
|
|
allocate_divergence = cls.allocate_with_weights(weights, divergence_budget, max_divergence)
|
|
|
|
divergence_samples = []
|
|
for sample, divergence in zip(samples_to_go_deeper, allocate_divergence):
|
|
for _ in range(divergence):
|
|
divergence_samples.append(deepcopy(sample))
|
|
|
|
return divergence_samples
|
|
|
|
|
|
class LogProbDivergence(AbstractDivergence):
|
|
|
|
@classmethod
|
|
def calc_weights(cls, root_idx, samples_to_go_deeper, **kwargs) -> List[float]:
|
|
"""
|
|
In this strategy, weight is proportional to entropy
|
|
"""
|
|
entropies = []
|
|
for sample in samples_to_go_deeper:
|
|
log_probs = torch.tensor(sample.logprobs[-1])
|
|
|
|
probs = torch.exp(log_probs)
|
|
entropy = -torch.sum(probs * log_probs)
|
|
entropies.append(entropy)
|
|
|
|
entropies_tensor = torch.stack(entropies)
|
|
weights = torch.softmax(entropies_tensor, dim=0)
|
|
|
|
return weights.tolist()
|
|
|
|
|
|
class AvgDivergence(AbstractDivergence):
|
|
|
|
@classmethod
|
|
def calc_weights(cls, root_idx, samples_to_go_deeper, **kwargs) -> List[float]:
|
|
avg = torch.ones(len(samples_to_go_deeper))
|
|
weights = torch.softmax(avg, dim=0)
|
|
|
|
return weights.tolist()
|
|
|
|
|
|
DivergenceStrategyMapping = {'logprobs': LogProbDivergence, 'average': AvgDivergence}
|