Files
wehub-resource-sync a203934033
Lint test / lint (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:34:58 +08:00

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}