Source code for trinity.common.workflows.thinking_budget_workflow
"""OpenAI-compatible workflow with vLLM thinking-budget support."""
from __future__ import annotations
from dataclasses import asdict
from typing import TYPE_CHECKING, List, Optional
import torch
from transformers import AutoTokenizer
from trinity.common.experience import Experience
from trinity.common.workflows.workflow import Task, Workflow
if TYPE_CHECKING:
from trinity.common.models.model import ModelWrapper
[docs]
class ThinkingBudgetWorkflow(Workflow):
"""Run raw OpenAI messages with a per-request reasoning-token budget.
vLLM forces the configured end-of-reasoning token sequence when the budget
is exhausted. The workflow locates that sequence in the returned completion
token IDs and masks it out so it does not contribute to the policy loss.
"""
can_repeat = True
DEFAULT_REASONING_END_STR = "</think>"
[docs]
def __init__(
self,
*,
task: Task,
model: ModelWrapper,
auxiliary_models: Optional[List[ModelWrapper]] = None,
):
super().__init__(task=task, model=model, auxiliary_models=auxiliary_models)
raw_task = task.raw_task or {}
self.messages = raw_task.get("messages")
if not isinstance(self.messages, list):
raise ValueError("ThinkingBudgetWorkflow requires raw_task['messages'] to be a list.")
self.thinking_token_budget = int(task.workflow_args.get("thinking_token_budget")) # type: ignore [arg-type]
if not isinstance(self.thinking_token_budget, int) or self.thinking_token_budget < 0:
raise ValueError(
"workflow_args['thinking_token_budget'] must be a non-negative integer."
)
self.reasoning_end_str = task.workflow_args.get(
"reasoning_end_str", self.DEFAULT_REASONING_END_STR
)
if not isinstance(self.reasoning_end_str, str) or not self.reasoning_end_str:
raise ValueError("workflow_args['reasoning_end_str'] must be a non-empty string.")
if not model.enable_history:
raise ValueError(
"ThinkingBudgetWorkflow requires explorer.rollout_model.enable_history=true."
)
self.repeat_times = task.rollout_args.n
self.run_id_base = 0
self.client = model.get_openai_client()
self.tokenizer = AutoTokenizer.from_pretrained(
model.model_path,
trust_remote_code=model.config.trust_remote_code,
)
self.reasoning_end_token_ids = self.tokenizer.encode(
self.reasoning_end_str, add_special_tokens=False
)
if not self.reasoning_end_token_ids:
raise ValueError("workflow_args['reasoning_end_str'] encodes to no tokens.")
[docs]
def set_repeat_times(self, repeat_times: int, run_id_base: int) -> None:
self.repeat_times = repeat_times
self.run_id_base = run_id_base
@staticmethod
def _mask_forced_reasoning_end(
exp: Experience,
reasoning_end_token_ids: List[int],
) -> None:
"""Set the vLLM-forced reasoning-end token action entries to zero."""
response_len = len(exp.tokens) - exp.prompt_length # type: ignore [arg-type]
if exp.action_mask is None or len(exp.action_mask) != response_len:
exp.action_mask = torch.ones(response_len, dtype=torch.bool)
response_token_ids = exp.tokens[exp.prompt_length :].tolist() # type: ignore [index]
for start in range(response_len - len(reasoning_end_token_ids) + 1):
end = start + len(reasoning_end_token_ids)
if response_token_ids[start:end] == reasoning_end_token_ids:
exp.action_mask[start:end] = False
return
[docs]
def run(self) -> List[Experience]:
rollout_args = {
key: value
for key, value in asdict(self.task.rollout_args).items()
if value is not None and key not in {"top_k", "logprobs", "n"}
}
rollout_args["n"] = self.repeat_times
extra_body = {"thinking_token_budget": self.thinking_token_budget}
if self.task.rollout_args.top_k >= 0:
extra_body["top_k"] = self.task.rollout_args.top_k
self.client.chat.completions.create(
model=self.client.model_path,
messages=self.messages,
extra_body=extra_body,
**rollout_args,
)
experiences = self.model.extract_experience_from_history()
for index, exp in enumerate(experiences):
self._mask_forced_reasoning_end(
exp,
reasoning_end_token_ids=self.reasoning_end_token_ids,
)
exp.eid.run = self.run_id_base + index
return experiences