执行摘要
- 一句话:为 bench_serving 新增 agentic-trace 多轮数据集加载
- 推荐动作:建议阅读
AgenticTraceDataset 的实现,这是为 bench_serving 添加新数据集的参考范例。重点关注 from_args 工厂方法和 load 方法中偏移量取模、最大轮数截断等逻辑。
功能与动机
bench_serving 已经支持多轮回放,但缺少预构建 agentic traces 的数据集加载器,以便对多轮 agentic 工作负载进行端到端基准测试。
实现拆解
实现分为以下步骤:
- 新增
AgenticTraceDataset 类(agentic_trace.py):继承 BaseDataset,通过 from_args 解析 CLI 参数,load 方法读取 JSON 文件,提取 conversations 列表,支持 offset 旋转对话顺序和 max_turns 截断轮数,最终生成 DatasetRow 列表。每轮输出长度默认为 220。
- 注册数据集(
__init__.py):导入 AgenticTraceDataset 并添加到 DATASET_MAPPING 字典,映射到 'agentic-trace' 键。
- 扩展 CLI 参数(
serving.py):在 --dataset-name 的 choices 中增加 'agentic-trace',并新增 --dataset-offset(旋转偏移量)和 --agentic-max-turns(最大轮数限制)两个可选参数。
- 添加单元测试(
test_benchmark_datasets_api.py):增加三个测试:基础采样、偏移和最大轮数组合、无效输入异常。使用临时 JSON 文件模拟 agentic trace 数据。
- 更新文档(
bench_serving.mdx):在数据集列表中增加 agentic-trace 说明,并详述相关 flags。
关键文件:
python/sglang/benchmark/datasets/agentic_trace.py(模块 基准测试;类别 source;类型 core-logic;符号 AgenticTraceDataset, from_args, load): 核心数据集加载器,定义了 AgenticTraceDataset 类及加载逻辑。
test/registered/bench_fn/test_benchmark_datasets_api.py(模块 测试;类别 test;类型 test-coverage;符号 _write_agentic_trace_json, test_agentic_trace_sampler, test_agentic_trace_offset_and_max_turns, test_agentic_trace_invalid_input_raises): 新增 agentic-trace 数据集加载器的单元测试,覆盖正常采样、偏移和最大轮数、异常输入。
python/sglang/benchmark/serving.py(模块 基准测试;类别 source;类型 core-logic): CLI 入口,新增 dataset-name 选项和两个新参数。
python/sglang/benchmark/datasets/__init__.py(模块 基准测试;类别 source;类型 dependency-wiring): 数据集注册入口,将 agentic-trace 映射到 AgenticTraceDataset。
docs_new/docs/developer_guide/bench_serving.mdx(模块 文档;类别 other;类型 documentation): 更新文档,说明 agentic-trace 数据集的用法和 flags。
关键符号:AgenticTraceDataset.from_args, AgenticTraceDataset.load, test_agentic_trace_sampler, test_agentic_trace_offset_and_max_turns
关键源码片段
python/sglang/benchmark/datasets/agentic_trace.py
核心数据集加载器,定义了 AgenticTraceDataset 类及加载逻辑。
import json
import os
from argparse import Namespace
from dataclasses import dataclass
from typing import List, Optional
import numpy as np
from transformers import PreTrainedTokenizerBase
from sglang.benchmark.datasets.common import BaseDataset, DatasetRow
# 默认每轮输出长度,匹配 OpenHands 风格 trace 的平均 Assistant 回复长度
DEFAULT_AGENTIC_OUTPUT_LEN = 220
@dataclass
class AgenticTraceDataset(BaseDataset):
'''Multi-turn agentic trace loader (e.g. OpenHands / SWE-smith traces).'''
dataset_path: str
num_requests: int
fixed_output_len: Optional[int]
offset: int
max_turns: Optional[int]
@classmethod
def from_args(cls, args: Namespace) -> 'AgenticTraceDataset':
# 从命令行参数构造实例,映射各个 CLI flag
return cls(
dataset_path=args.dataset_path,
num_requests=args.num_prompts,
fixed_output_len=args.sharegpt_output_len,
offset=args.dataset_offset,
max_turns=args.agentic_max_turns,
)
def load(
self, tokenizer: PreTrainedTokenizerBase, model_id=None
) -> List[DatasetRow]:
if not os.path.isfile(self.dataset_path):
raise FileNotFoundError(f'Dataset not found at {self.dataset_path}')
with open(self.dataset_path, 'r', encoding='utf-8') as f:
data = json.load(f)
conversations = data.get('conversations', [])
if not conversations:
raise ValueError(f'No conversations found in {self.dataset_path}.')
# 计算偏移量(取模)并旋转对话列表,使不同 sweep step 从不同起点开始
offset = self.offset % len(conversations)
if offset:
conversations = conversations[offset:] + conversations[:offset]
# 确定每轮输出长度,优先使用用户指定值,否则用默认值 220
output_len = self.fixed_output_len or DEFAULT_AGENTIC_OUTPUT_LEN
filtered_dataset: List[DatasetRow] = []
for conversation in conversations:
if self.num_requests > 0 and len(filtered_dataset) >= self.num_requests:
break
# 提取每轮的消息列表,过滤掉空轮
prompt = [turn['messages'] for turn in conversation if turn.get('messages')]
if self.max_turns:
prompt = prompt[: self.max_turns]
if not prompt:
continue
# 仅用于记录,实际多轮回放忽略此 prompt_len
prompt_len = int(conversation[0].get('prompt_tokens', 0))
filtered_dataset.append(
DatasetRow(
prompt=prompt,
prompt_len=prompt_len,
output_len=output_len,
)
)
if not filtered_dataset:
raise ValueError(
f'No usable conversations loaded from {self.dataset_path}.'
)
# 打印摘要信息
num_turns = [len(row.prompt) for row in filtered_dataset]
print(
f'#Conversations: {len(filtered_dataset)} '
f'(offset={offset}, turns/conv min={min(num_turns)} '
f'max={max(num_turns)} avg={np.mean(num_turns):.1f})'
)
print(f'#Output tokens per turn: {output_len}')
return filtered_dataset
test/registered/bench_fn/test_benchmark_datasets_api.py
新增 agentic-trace 数据集加载器的单元测试,覆盖正常采样、偏移和最大轮数、异常输入。
def _write_agentic_trace_json(self):
trace = {
'metadata': {'source': 'test'},
'conversations': [
[ # 第一个 conversation,包含三个 turn(第三个为空)
{
'messages': [
{'role': 'system', 'content': 'You are an agent.'},
{'role': 'user', 'content': 'Fix the bug.'},
],
'prompt_tokens': 100,
},
{
'messages': [{'role': 'user', 'content': 'Tool output: ok.'}],
'prompt_tokens': 200,
},
{'messages': []}, # 空 turn,应被跳过
],
[ # 第二个 conversation,仅一个 turn
{
'messages': [{'role': 'user', 'content': 'Run the tests.'}],
'prompt_tokens': 50,
},
],
],
}
path = self.tmpdir_path / 'agentic_trace.json'
with open(path, 'w') as f:
json.dump(trace, f)
return str(path)
def test_agentic_trace_sampler(self):
dataset_path = self._write_agentic_trace_json()
args = make_args(
dataset_name='agentic-trace',
dataset_path=dataset_path,
num_prompts=10,
)
dataset = AgenticTraceDataset.from_args(args)
rows = dataset.load(self.tokenizer)
self.assertEqual(len(rows), 2) # 两个 conversation
self.assertTrue(all(isinstance(row, DatasetRow) for row in rows))
# 未指定 output_len,使用默认值 220
self.assertTrue(
all(row.output_len == DEFAULT_AGENTIC_OUTPUT_LEN for row in rows)
)
# 第一个 conversation 的第三个空 turn 被跳过,因此只有 2 个有效 turn
self.assertEqual(len(rows[0].prompt), 2)
# 第二个 conversation 只有 1 个 turn
self.assertEqual(len(rows[1].prompt), 1)
self.assertEqual(rows[0].prompt[0][0]['role'], 'system')
self.assertEqual(rows[0].prompt_len, 100)
self.assertEqual(rows[1].prompt_len, 50)
def test_agentic_trace_offset_and_max_turns(self):
dataset_path = self._write_agentic_trace_json()
args = make_args(
dataset_name='agentic-trace',
dataset_path=dataset_path,
num_prompts=10,
sharegpt_output_len=64,
dataset_offset=1, # 偏移 1,旋转后第二个 conversation 成为第一个
agentic_max_turns=1, # 每个 conversation 只取第一轮
)
dataset = AgenticTraceDataset.from_args(args)
rows = dataset.load(self.tokenizer)
self.assertEqual(len(rows), 2) # 仍有两个 conversation
# 偏移后第一个 conversation 应为原本的第二个(单 turn)
self.assertEqual(len(rows[0].prompt), 1)
# 限制最大 1 轮,所以第二个 conversation(原第一个)也只有 1 轮
self.assertEqual(len(rows[1].prompt), 1)
self.assertEqual(rows[0].prompt_len, 50)
self.assertEqual(rows[0].output_len, 64) # 使用指定的 output_len
评论区精华
无实质技术讨论。Reviewer zijiexia 直接批准,作者 kpham-sgl 评论 'Should be safe to merge'。
- PR 审核与合并决策 (other): PR 被批准并合并。
风险与影响
- 风险:该 PR 仅影响 benchmark 工具链,不改变任何推理代码路径,回归风险较低。主要风险在于 JSON 输入格式异常处理,已通过单元测试覆盖空对话、无效文件等场景。
- 影响:对用户:新增 benchmark 数据集选项,允许对多轮 agentic 工作负载进行端到端性能测试。对系统:无影响。对团队:扩展了 benchmark 框架,易于添加更多数据集。
- 风险标记:benchmark-only, 新增数据集, 无回归风险
关联脉络
参与讨论