Prhub

#32078 [feat] Opt-in flat response format for prompt top logprobs

原始 PR 作者 sshleifer 合并时间 2026-07-27 14:44 文件变更 3 提交数 4 评论 2 代码增减 +407 / -9

执行摘要

新增 opt-in 扁平 top logprobs 响应格式,序列化性能提升 1.9 倍

大量使用 prompt top logprobs 的工作负载(RL teacher、蒸馏、离线评分)中,响应序列化占据请求延迟的很大比例。嵌套格式每位置每 topk 生成小列表,json.dumps 类型分派开销大,且 null 文本槽位对批量消费者无用。两个长同质数组消除了所有三个问题,客户端可直接用 np.asarray(...).reshape(shape) 消费。PR body 提供的基准测试(32768 位置、k=2)显示:嵌套格式往返 102.7ms,扁平格式 55.1ms(1.9 倍),payload 大小从 2,417,567 字节降至 1,827,898 字节。

值得精读。该 PR 以极小侵入性实现了显著的序列化性能提升,设计决策清晰(opt-in、null_prefix 处理、缓存复用),且与后续优化形成递进路径。可作为服务端响应格式优化的参考案例,尤其适合对序列化开销敏感的团队借鉴。

讨论亮点

该 PR 没有收到实质性 review comments,仅由维护者 Qiaolin-Yu 直接批准合并。主要设计决策(opt-in 扁平格式、null_prefix 表示、不支持多条目、后续 base64 编码)由作者在 PR body 中详细阐述,未引发额外争论。

实现拆解

  1. 请求参数扩展io_struct.py):在 GenerateReqInput 类中添加 return_flat_raw_top_logprobs: bool = False 字段。在 normalize_batch_and_arguments_validate_inputs 中增加检查:若开启扁平格式同时设置了 multi_item_delimiter_indices,则抛出 ValueError,因为多条目评分会产生不连续的 top logprobs 行,无法用 (shape, null_prefix) 表示。此外,在 __getitem__ 中传播该标志,确保 batch 拆分后每个子请求也携带该设置。

  2. 状态缓存tokenizer_manager.py):在 ReqState 数据类中添加两个缓存字段 input_top_logprobs_flat_fields: Optional[Dict]input_top_logprobs_flat_num_rows: int(默认 -1),用于跨预填充 chunk 复用扁平化结果,避免 streaming decode chunk 重复构建。

  3. 扁平化构建函数tokenizer_manager.py,新增模块级函数 _build_flat_input_top_logprobs_fields):接收 input_top_logprobs_valinput_top_logprobs_idx 列表和 top_logprobs_num。先扫描前导 null 行(None 或空)计入 null_prefix,然后提取有效行;校验有效行的长度等于 k(第一个有效行长度或 top_logprobs_num 提供的兜底值),若出现非 null 但长度不等的行则抛出 ValueError,因为这种不规则模式无法通过 (shape, null_prefix) 映射。最终生成四个字段:input_top_logprobs_val_flat(一维浮点列表)、input_top_logprobs_idx_flat(一维整数列表)、input_top_logprobs_shape[rows, k])、input_top_logprobs_null_prefix(前导无效位置数)。

  4. 主路径集成tokenizer_manager.py,修改 TokenizerManager.add_logprob_to_meta_info 方法):在原有嵌套路径分支旁新增条件:当 state.obj.return_flat_raw_top_logprobs 为 True 时,调用 _build_flat_input_top_logprobs_fields 构建扁平字段,将其直接写入 meta_info,并跳过state.input_top_logprobs(分割化的 detokenize 版本)的后续更新,因为扁平格式不需要文本解码。构建的结果缓存在 state.input_top_logprobs_flat_fields,仅当累积更多预填充 chunk 导致行数增加时才重建。

  5. 单元测试test/registered/unit/managers/test_flat_raw_top_logprobs.py,新增 306 行):通过 _TokenizerManagerStub 代理真实管理器的 add_logprob_to_meta_info 方法,避免全量初始化。覆盖场景包括:标志默认关闭且有效、批量传播、多条目拒绝;扁平 vs 嵌套数值等价性及索引公式重建((i - null_prefix) * k + j);全 null 行返回 shape [0, k];不规则 null(非前缀空行)被 builder 拒绝;还有环境门控的序列化基准测试。

文件 模块 状态 重要度
python/sglang/srt/managers/tokenizer_manager.py 管理器 modified 7.64
python/sglang/srt/managers/io_struct.py 请求定义 modified 6.16
test/registered/unit/managers/test_flat_raw_top_logprobs.py 单元测试 added 7.76

关键符号

_build_flat_input_top_logprobs_fields TokenizerManager.add_logprob_to_meta_info GenerateReqInput._validate_inputs

关键源码片段

python/sglang/srt/managers/tokenizer_manager.py core-logic

核心实现文件:新增 `_build_flat_input_top_logprobs_fields` 函数负责将嵌套行拍平为同质数组并计算元信息;修改 `add_logprob_to_meta_info` 方法根据 opt-in 标志选择扁平或嵌套路径;在 `ReqState` 中添加缓存字段以复用跨 chunk 的扁平化结果。

def _build_flat_input_top_logprobs_fields(
    input_top_logprobs_val: List[Optional[List[float]]],
    input_top_logprobs_idx: List[Optional[List[int]]],
    top_logprobs_num: int,
) -> Dict[str, Any]:
    """构建扁平化 prompt top logprob 响应字段。    `input_top_logprobs_shape` 是字面量 `[rows, k]` 数组形状。
    `input_top_logprobs_null_prefix` 计数前导无效位置(无 top logprobs 的位置),
    这些位置在数组之前,有效位置 i 的 entry j 位于 `flat[(i - null_prefix) * k + j]`。
    """
    num_rows = len(input_top_logprobs_val)
    null_prefix = 0
    # 扫描前导空行(None 或空列表),计入 null_prefix
    while null_prefix < num_rows and not input_top_logprobs_val[null_prefix]:
        null_prefix += 1
    val_rows = input_top_logprobs_val[null_prefix:]
    idx_rows = input_top_logprobs_idx[null_prefix:]
    # k 取第一个有效行长度,若全空则 fallback 到 top_logprobs_num
    k = len(val_rows[0]) if val_rows else top_logprobs_num
    for offset, row in enumerate(val_rows):
        if row is None or len(row) != k:
            # 不规则行(非前导空行、长度不一致)无法用 (shape, null_prefix) 表示
            raise ValueError(
                "return_flat_raw_top_logprobs requires rectangular top logprob "
                f"rows with nulls only in the leading prefix; row {null_prefix + offset} "
                f"has {None if row is None else len(row)} entries (expected {k})."
            )
    fields: Dict[str, Any] = {}
    fields["input_top_logprobs_val_flat"] = [v for row in val_rows for v in row]
    fields["input_top_logprobs_idx_flat"] = [i for row in idx_rows for i in row]
    fields["input_top_logprobs_shape"] = [len(val_rows), k]
    fields["input_top_logprobs_null_prefix"] = null_prefix
    return fields

# 在 add_logprob_to_meta_info 中,约第 2264 行附近
if top_logprobs_num > 0:
    use_flat = state.obj.return_flat_raw_top_logprobs # type: ignore[union-attr]
    if use_flat:
        # 扁平路径:跳过 detokenize,直接构建扁平字段并缓存
        if state.input_top_logprobs_flat_fields is None or \
           len(state.input_top_logprobs_val) != state.input_top_logprobs_flat_num_rows:
            state.input_top_logprobs_flat_fields = _build_flat_input_top_logprobs_fields(
                state.input_top_logprobs_val,
                state.input_top_logprobs_idx,
                top_logprobs_num,
            )
            state.input_top_logprobs_flat_num_rows = len(state.input_top_logprobs_val)
        meta_info.update(state.input_top_logprobs_flat_fields)
    else:
        # 嵌套路径(原逻辑):detokenize 后写入 input_top_logprobs
        if len(state.input_top_logprobs_val) > len(state.input_top_logprobs):
            state.input_top_logprobs.extend(
                self.detokenize_top_logprobs_tokens(
                    state.input_top_logprobs_val[len(state.input_top_logprobs):],
                    state.input_top_logprobs_idx[len(state.input_top_logprobs):],
                    return_text_in_logprobs,
                )
            )
        meta_info["input_top_logprobs"] = state.input_top_logprobs

test/registered/unit/managers/test_flat_raw_top_logprobs.py test-coverage

完整的单元测试套件(306 行),通过 `_TokenizerManagerStub` 模拟 TokenizerManager 的 logprob 方法,覆盖标志验证、批量传播、多条目拒绝、扁平与嵌套数值等价、全 null 行处理、不规则行拒绝等场景,并包含环境门控基准测试,确保实现正确性。

class TestFlatAssembly(CustomTestCase):
    """验证 _build_flat_input_top_logprobs_fields 的正确性。"""
​
    def test_flat_matches_nested_rows(self):
        # 模拟 4 个 prompt 位置,前 1 个为空,后 3 个各含 k=2 个 top logprobs
        val_rows = [None, [-0.1, -2.5], [-0.3, -1.5], [-0.05, -4.0]]
        idx_rows = [None, [11, 22], [33, 44], [55, 66]]
        fields = _build_flat_input_top_logprobs_fields(val_rows, idx_rows, top_logprobs_num=2)
​
        # 验证元信息
        self.assertEqual(fields["input_top_logprobs_shape"], [3, 2]) # 3 个有效行
        self.assertEqual(fields["input_top_logprobs_null_prefix"], 1) # 前 1 个空行
​
        # 验证扁平数组内容:跳过前导空行,依次连接有效行
        expected_vals = [-0.1, -2.5, -0.3, -1.5, -0.05, -4.0]
        expected_idxs = [11, 22, 33, 44, 55, 66]
        self.assertEqual(fields["input_top_logprobs_val_flat"], expected_vals)
        self.assertEqual(fields["input_top_logprobs_idx_flat"], expected_idxs)
​
        # 验证索引公式:位置 i 的 entry j 位于 flat[(i - null_prefix) * k + j]
        rows, k = fields["input_top_logprobs_shape"]
        null_prefix = fields["input_top_logprobs_null_prefix"]
        flat_val = fields["input_top_logprobs_val_flat"]
        self.assertEqual(len(flat_val), rows * k)
        for i in range(null_prefix, null_prefix + rows):
            start = (i - null_prefix) * k
            self.assertEqual(flat_val[start: start + k], val_rows[i])
​
    def test_all_null_rows(self):
        # 所有行均为 null,应返回 shape [0, k] 和空数组
        fields = _build_flat_input_top_logprobs_fields([None], [None], top_logprobs_num=2)
        self.assertEqual(fields["input_top_logprobs_shape"], [0, 2])
        self.assertEqual(fields["input_top_logprobs_null_prefix"], 1)
        self.assertEqual(fields["input_top_logprobs_val_flat"], [])
        self.assertEqual(fields["input_top_logprobs_idx_flat"], [])
​
    def test_rejects_null_row_after_prefix(self):
        # 非前导空行(如第 3 行为 None)应该被拒绝
        with self.assertRaisesRegex(ValueError, "leading prefix"):
            _build_flat_input_top_logprobs_fields(
                [None, [-0.1, -2.5], None, [-0.3, -1.5]],
                [None, [11, 22], None, [33, 44]],
                top_logprobs_num=2,
            )

评论区精华

无实质讨论 other

该 PR 没有收到 review comments,仅有维护者 Qiaolin-Yu 批准合并。

结论:直接合并 · 已解决

风险与影响

  1. 仅覆盖 prompt 侧:新标志仅影响 input_top_logprobs(prompt 侧),output 侧保持嵌套格式,可能让用户误以为全面扁平化。需在文档中明确。
  2. 多条目评分不兼容:若用户同时启用 return_flat_raw_top_logprobs 和多条目,会在验证阶段抛出 ValueError,不会静默产生错误输出。
  3. 消费者依赖:扁平字段不包含文本(text),要求消费者已具备 token 解码能力或不需要文本。若消费者需要文本,需继续使用嵌套格式。
  4. 默认行为不变:标志默认 False,旧客户端不受影响,升级安全。
  5. 测试覆盖完善:单元测试覆盖了正常路径、边界条件和不合法输入,回归风险较低。但缺少端到端集成测试(需后端运行)。

用户:启用 return_flat_raw_top_logprobs 的请求,响应序列化速度提升约 2 倍(扁平)乃至 7.8 倍(配合 base64),payload 减少 30%–65%,对于高频 top logprobs 消费工作负载(RL 训练、离线评估)显著改善吞吐和延迟。
系统:默认无影响;新增字段仅在一次函数调用中计算,逻辑轻量。
团队:代码增量小(3 文件 +407/-9),维护成本低;设计与后续 base64 编码(#31960)直接兼容,为渐进优化留出空间。

opt-in 特性 多条目不兼容 输入侧仅 prompt

关联 Issue

未识别关联 Issue

当前没有检测到明确关联的 Issue 链接,后续同步到相关引用后会出现在这里。

完整报告

参与讨论