-
Notifications
You must be signed in to change notification settings - Fork 466
[WIP] feat(perf): add --max-turn-tokens for per-turn max_tokens override in multi-turn mode #1359
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 2 commits
7a06dba
3a8be9e
9b28a0f
f06de36
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -19,6 +19,7 @@ | |||||
| | `--min-turns` | `int` | 每个对话最少用户轮数,仅 `random_multi_turn` 使用 | `1` | | ||||||
| | `--max-turns` | `int` | 每个对话最多用户轮数;`random_multi_turn` **必须设置**;ShareGPT / `custom_multi_turn` 等数据集可选,用于截断过长对话;`swe_smith` live 构建时每条对话轮次从 `[min_turns, max_turns]` 随机采样 | `None` | | ||||||
| | `--dataset-offset` | `int` | 跳过数据集前 N 条对话,用于分片测试或避免缓存命中 | `0` | | ||||||
| | `--max-turn-tokens` | `list[int]` | 逐轮 `max_tokens` 覆盖值;接受一个整数列表,按 turn index(从 0 开始)指定每轮的最大输出 token 数。列表短于实际轮数时,复用最后一个值。仅在 `--multi-turn` 模式下生效 | `None` | | ||||||
|
|
||||||
| ### `multi_turn_args`(`swe_smith` 专属参数) | ||||||
|
|
||||||
|
|
@@ -266,6 +267,44 @@ evalscope perf \ | |||||
|
|
||||||
| > **说明**:数据集中的 `assistant` 消息仅用于标识对话结构,**不会**被直接发送给模型。运行时 worker 始终将模型的实际输出追加到上下文,保证历史准确。 | ||||||
|
|
||||||
| ### 逐轮控制输出长度(`--max-turn-tokens`) | ||||||
|
|
||||||
| 在模拟 Agent 工具调用性能的场景中,开源模型无法像实际模型那样输出工具调用结构,导致每轮输出长度与实际模型不同。通过 `--max-turn-tokens` 可以逐轮限制模型的输出长度,从而近似模拟实际模型的上下文增长行为。 | ||||||
|
|
||||||
| **使用示例**:10 轮对话,前 9 轮模拟工具调用(各 150 token),最后一轮输出完整回答(1000 token)。 | ||||||
|
|
||||||
| 首先准备 JSONL 数据文件(每行一条 10 轮对话,system prompt 约 4000 token): | ||||||
|
|
||||||
| ```json | ||||||
| [{"role": "system", "content": "<4000 token 的系统提示>"}, {"role": "user", "content": "帮我分析这段代码"}, {"role": "assistant", "content": "x"}, {"role": "user", "content": "继续"}, {"role": "assistant", "content": "x"}, {"role": "user", "content": "继续"}, {"role": "assistant", "content": "x"}, {"role": "user", "content": "继续"}, {"role": "assistant", "content": "x"}, {"role": "user", "content": "继续"}, {"role": "assistant", "content": "x"}, {"role": "user", "content": "继续"}, {"role": "assistant", "content": "x"}, {"role": "user", "content": "继续"}, {"role": "assistant", "content": "x"}, {"role": "user", "content": "继续"}, {"role": "assistant", "content": "x"}, {"role": "user", "content": "继续"}, {"role": "assistant", "content": "x"}, {"role": "user", "content": "请给出完整的最终回答"}] | ||||||
| ``` | ||||||
|
|
||||||
| > **说明**:assistant 消息仅定义对话结构,实际运行中会被模型的真实输出替换。 | ||||||
|
|
||||||
| 然后运行压测: | ||||||
|
|
||||||
| ```bash | ||||||
| evalscope perf \\ | ||||||
| --model YOUR_MODEL \\ | ||||||
| --url OPENAI_API_COMPAT_URL \\ | ||||||
| --api openai \\ | ||||||
| --dataset custom_multi_turn \\ | ||||||
| --dataset-path /path/to/tool_call_sim.jsonl \\ | ||||||
| --multi-turn \\ | ||||||
| --max-turn-tokens 150 150 150 150 150 150 150 150 150 1000 \\ | ||||||
| --number 50 \\ | ||||||
| --parallel 10 \\ | ||||||
| --extra-args '{"ignore_eos": true}' | ||||||
| ``` | ||||||
|
|
||||||
| | 轮次 | `max_tokens` | 模拟效果 | | ||||||
| |------|-------------|---------| | ||||||
| | 第 1 轮 | 150 | 模拟首次工具调用 | | ||||||
| | 第 2-9 轮 | 150 | 模拟中间轮工具调用 | | ||||||
| | 第 10 轮 | 1000 | 最终完整回答 | | ||||||
|
|
||||||
| > **提示**:列表长度不足时自动复用最后一个值。例如 `--max-turn-tokens 150 1000` 在 10 轮对话中效果为 `[150, 150, 150, 150, 150, 150, 150, 150, 150, 1000]`。 | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 此处提示中的示例与代码实现逻辑不符。代码逻辑是复用列表的最后一个元素给后续所有轮次。因此
Suggested change
|
||||||
|
|
||||||
| **使用示例**:适用场景:已有 OpenAI messages 格式的对话数据,直接用于多轮压测,无需转换格式。 | ||||||
|
|
||||||
| 首先准备 JSONL 数据文件(每行一条对话): | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -275,6 +275,19 @@ def total_count(self) -> int: | |||||||||||||||||||||||||||||||||||||||||||||
| Accepts an int or a ``[min, max]`` list for uniform sampling per request. | ||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| max_turn_tokens: Optional[List[int]] = None | ||||||||||||||||||||||||||||||||||||||||||||||
| """Per-turn max_tokens override for multi-turn mode. | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| A list of integers specifying max_tokens for each turn index (0-based). | ||||||||||||||||||||||||||||||||||||||||||||||
| Example: ``[150, 150, 150, 150, 150, 150, 150, 150, 150, 1000]`` for a | ||||||||||||||||||||||||||||||||||||||||||||||
| 10-turn conversation where the first 9 turns are limited to 150 tokens | ||||||||||||||||||||||||||||||||||||||||||||||
| and the final turn allows 1000 tokens. | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| When set, this overrides ``--max-tokens`` on a per-turn basis in | ||||||||||||||||||||||||||||||||||||||||||||||
| ``--multi-turn`` mode. If the list is shorter than the actual turn count, | ||||||||||||||||||||||||||||||||||||||||||||||
| the last element is reused for remaining turns. | ||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| min_tokens: Optional[int] = None | ||||||||||||||||||||||||||||||||||||||||||||||
| """Minimum number of tokens in the response.""" | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -360,6 +373,18 @@ def _validate_max_tokens(cls, v): | |||||||||||||||||||||||||||||||||||||||||||||
| raise ValueError(f'--max-tokens range values must be >= 0, got {v}') | ||||||||||||||||||||||||||||||||||||||||||||||
| return v | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| @field_validator('max_turn_tokens', mode='before') | ||||||||||||||||||||||||||||||||||||||||||||||
| @classmethod | ||||||||||||||||||||||||||||||||||||||||||||||
| def _validate_max_turn_tokens(cls, v): | ||||||||||||||||||||||||||||||||||||||||||||||
| if v is None: | ||||||||||||||||||||||||||||||||||||||||||||||
| return v | ||||||||||||||||||||||||||||||||||||||||||||||
| if isinstance(v, list): | ||||||||||||||||||||||||||||||||||||||||||||||
| if not v: | ||||||||||||||||||||||||||||||||||||||||||||||
| raise ValueError('--max-turn-tokens must contain at least one value') | ||||||||||||||||||||||||||||||||||||||||||||||
| if any(x < 1 for x in v): | ||||||||||||||||||||||||||||||||||||||||||||||
| raise ValueError(f'--max-turn-tokens values must be >= 1, got {v}') | ||||||||||||||||||||||||||||||||||||||||||||||
| return v | ||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+378
to
+389
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There are two improvements for this validator:
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| @field_validator('multi_turn_args', mode='before') | ||||||||||||||||||||||||||||||||||||||||||||||
| @classmethod | ||||||||||||||||||||||||||||||||||||||||||||||
| def _validate_multi_turn_args(cls, v): | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -642,6 +667,12 @@ def add_argument(parser: argparse.ArgumentParser): | |||||||||||||||||||||||||||||||||||||||||||||
| parser.add_argument( | ||||||||||||||||||||||||||||||||||||||||||||||
| '--max-tokens', type=int, nargs='+', help='The maximum number of tokens that can be generated. ' | ||||||||||||||||||||||||||||||||||||||||||||||
| 'Accepts 1 value (fixed) or 2 values min max for uniform sampling per request.', default=2048) | ||||||||||||||||||||||||||||||||||||||||||||||
| parser.add_argument( | ||||||||||||||||||||||||||||||||||||||||||||||
| '--max-turn-tokens', type=int, nargs='+', default=None, | ||||||||||||||||||||||||||||||||||||||||||||||
| help='Per-turn max_tokens override for multi-turn mode. ' | ||||||||||||||||||||||||||||||||||||||||||||||
| 'Pass a list of integers, one per turn (0-based). ' | ||||||||||||||||||||||||||||||||||||||||||||||
| 'If shorter than the turn count, the last value is reused. ' | ||||||||||||||||||||||||||||||||||||||||||||||
| 'Example: --max-turn-tokens 150 150 150 150 150 150 150 150 150 1000') | ||||||||||||||||||||||||||||||||||||||||||||||
| parser.add_argument( | ||||||||||||||||||||||||||||||||||||||||||||||
| '--min-tokens', type=int, help='The minimum number of tokens that can be generated', default=None) | ||||||||||||||||||||||||||||||||||||||||||||||
| parser.add_argument('--n-choices', type=int, help='How many completion choices to generate', default=None) | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -37,7 +37,7 @@ def __init__(self, param: Arguments): | |||||
| else: | ||||||
| self.tokenizer = None | ||||||
|
|
||||||
| def build_request(self, messages: Union[List[Dict], str], param: Arguments = None) -> Dict: | ||||||
| def build_request(self, messages: Union[List[Dict], str], param: Arguments = None, turn_index: Optional[int] = None) -> Dict: | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The
Suggested change
|
||||||
| """Build a custom API request body based on the input messages and parameters. | ||||||
|
|
||||||
| This method formats the input messages into the expected request format | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -17,7 +17,7 @@ class DashScopeApiPlugin(ApiPluginBase): | |||||
| def __init__(self, param: Arguments): | ||||||
| super().__init__(param) | ||||||
|
|
||||||
| def build_request(self, messages: List[Dict], param: Arguments = None) -> Dict: | ||||||
| def build_request(self, messages: List[Dict], param: Arguments = None, turn_index: Optional[int] = None) -> Dict: | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The
Suggested change
|
||||||
| """Build the openai format request based on prompt, dataset | ||||||
|
|
||||||
| Args: | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -41,7 +41,7 @@ def __init__(self, param: Arguments): | |||||
| else: | ||||||
| self.tokenizer = None | ||||||
|
|
||||||
| def build_request(self, messages: Union[List[Dict], str, List[str]], param: Arguments = None) -> Dict: | ||||||
| def build_request(self, messages: Union[List[Dict], str, List[str]], param: Arguments = None, turn_index: Optional[int] = None) -> Dict: | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The
Suggested change
|
||||||
| """Build the OpenAI embedding format request. | ||||||
|
|
||||||
| Args: | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -42,7 +42,7 @@ def __init__(self, param: Arguments): | |||||
| else: | ||||||
| self.tokenizer = None | ||||||
|
|
||||||
| def build_request(self, messages: Union[List[Dict], str, Dict], param: Arguments = None) -> Dict: | ||||||
| def build_request(self, messages: Union[List[Dict], str, Dict], param: Arguments = None, turn_index: Optional[int] = None) -> Dict: | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The
Suggested change
|
||||||
| """Build the rerank format request. | ||||||
|
|
||||||
| Args: | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -30,7 +30,7 @@ def __init__(self, param: Arguments): | |||||
| else: | ||||||
| self.tokenizer = None | ||||||
|
|
||||||
| def build_request(self, messages: Union[List[Dict], str, Dict], param: Arguments = None) -> Dict: | ||||||
| def build_request(self, messages: Union[List[Dict], str, Dict], param: Arguments = None, turn_index: Optional[int] = None) -> Dict: | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The
Suggested change
|
||||||
| param = param or self.param | ||||||
| try: | ||||||
| if param.query_template is not None: | ||||||
|
|
@@ -42,7 +42,7 @@ def build_request(self, messages: Union[List[Dict], str, Dict], param: Arguments | |||||
| query['input'] = normalize_responses_input(query.pop('messages')) | ||||||
| else: | ||||||
| query = {'input': normalize_responses_input(messages)} | ||||||
| return self._compose_query_from_parameter(query, param) | ||||||
| return self._compose_query_from_parameter(query, param, turn_index) | ||||||
| except Exception as e: | ||||||
| logger.exception(e) | ||||||
| return None | ||||||
|
|
@@ -190,9 +190,12 @@ def _set_cached_tokens(output: Any, usage: Dict[str, Any]) -> None: | |||||
| if cached is not None: | ||||||
| output.real_cached_tokens = cached | ||||||
|
|
||||||
| def _compose_query_from_parameter(self, payload: Dict, param: Arguments) -> Dict: | ||||||
| def _compose_query_from_parameter(self, payload: Dict, param: Arguments, turn_index: Optional[int] = None) -> Dict: | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The
Suggested change
|
||||||
| payload['model'] = param.model | ||||||
| if param.max_tokens is not None: | ||||||
| if param.max_turn_tokens is not None and turn_index is not None: | ||||||
| idx = min(turn_index, len(param.max_turn_tokens) - 1) | ||||||
| payload['max_output_tokens'] = param.max_turn_tokens[idx] | ||||||
| elif param.max_tokens is not None: | ||||||
| payload['max_output_tokens'] = _sample_int_or_range(param.max_tokens) | ||||||
| if param.stream is not None: | ||||||
| payload['stream'] = param.stream | ||||||
|
|
||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The example in this tip is incorrect based on the implementation logic. The code reuses the last element of the list for all subsequent turns. Therefore,
--max-turn-tokens 150 1000would result in[150, 1000, 1000, ...](where turn 0 is 150 and all others are 1000), not the sequence shown in the tip. To achieve the sequence in the tip, the user must provide the full list.