Prhub

#39328 Fix first-token metadata and reused attention-layer indexing

原始 PR 作者 merrymercy 合并时间 2026-09-16 06:22 文件变更 4 提交数 2 评论 1 代码增减 +59 / -2

执行摘要

修复首 token 延迟元数据与复用层 attention 索引

PR body 指出两点:客户端需要引擎测得的首 token 延迟(“Clients need the engine's first-token latency”);复用层模型(同一层占据多个执行位置)在初始化前会预展开 attention 查找表,这些表必须被保留(“pre-expanded attention tables must survive initialization when the same layer occupies multiple execution positions”)。原实现一方面没有在响应元数据中暴露 first_token_latency,另一方面 index_attention_layers_by_global_id 会把任何重复 layer_id 一律判为非法,导致复用层模型在 CUDA graph 初始化阶段被误拒绝。

值得快速精读。两个修复都很小但边界处理值得借鉴:index_attention_layers_by_global_id 用对象引用相等区分“复用”与“重复”,并保留位置索引表的语义;convert_to_output_meta_info 用 first_token_time > created_time 的守卫防止脏延迟输出。关注点:复用层返回原始表后,调用方对表长度的假设是否仍然成立。

讨论亮点

该 PR 没有任何 review 评论,唯一的评论是作者合并前发出的 /tag-and-rerun-ci 维护者指令(用于重跑 CI,对应 Run #34944830577 与 Run #34944830585)。因此本次改动的设计权衡未经过公开评审:复用层的放行条件(同一对象 is 判定 + 原始表原样返回)由实现和测试直接固化。分支名 lianmin/request-timing-layer-reuse-sync-20260913 也表明这是一个同步分支 PR,低 review 密度符合预期。

实现拆解

  1. 响应元数据暴露首 token 延迟:在 convert_to_output_meta_info(python/sglang/srt/observability/req_time_stats.py)中新增 created_time > 0.0 and first_token_time > created_time 守卫,满足时才写入 first_token_latency。该条件同时排除未初始化的 0 哨兵与相等/回退时间戳,避免输出无效或负数延迟。
  2. 复用层索引放行:在 index_attention_layers_by_global_id(python/sglang/srt/model_executor/model_runner_components/cuda_graph_setup.py)中把原“重复即抛错”拆分为:layer_id < 0 仍报错;目标位置已有节点时,仅当现有节点与当前节点是同一对象(is 比较、companion 相同)才视为复用并跳过,否则抛 ValueError。任一复用出现时,函数直接返回调用方传入的原始 attention_layers 与 mha_companion_layers,因为预展开表按执行位置索引,压缩会丢失位置语义。
  3. 测试配套:test_req_time_stats.py 新增 TestOutputMetaInfo,用 subTest 覆盖 5 组时间戳边界(正常、created 为 0、first 为 0、两者相等、first 小于 created);test_cuda_graph_setup.py 新增 test_reuse_tables_pass_through_but_distinct_duplicates_raise,覆盖复用表 is 原样返回与不同对象重复 layer_id 抛错两条路径。两个用例均注册 CPU CI。
  4. 无其他配套:无配置、部署或 schema 改动;第二个 commit 为 merge main 以满足 CI 最低基线要求。
文件 模块 状态 重要度
python/sglang/srt/model_executor/model_runner_components/cuda_graph_setup.py 图捕获 modified 6.8
python/sglang/srt/observability/req_time_stats.py 可观测性 modified 4.89
test/registered/unit/observability/test_req_time_stats.py 测试 modified 5.12
test/registered/unit/model_executor/model_runner_components/test_cuda_graph_setup.py 测试 modified 5.49

关键符号

index_attention_layers_by_global_id convert_to_output_meta_info

关键源码片段

python/sglang/srt/model_executor/model_runner_components/cuda_graph_setup.py data-contract

核心修复所在:index_attention_layers_by_global_id 从“只要 layer_id 重复就抛错”改成“同一对象复用放行并返回原始预展开表,不同对象重复仍拒绝”,避免复用层模型在 CUDA graph 初始化时被误拒绝。

# 来自 python/sglang/srt/model_executor/model_runner_components/cuda_graph_setup.py
​
def index_attention_layers_by_global_id(
    attention_layers: list[Any],
    mha_companion_layers: list[Any],
    layer_model=None,
) -> tuple[list[Any], list[Any]]:
    '''把 PP 局部的 attention 元数据按全局 layer_id 对齐。
​
    复用层模型(同一 layer 对象出现在多个执行位置)会预先展开
    查找表;这种表必须原样返回,不能按 layer_id 压缩,否则会
    丢失执行位置语义。
    '''
    if len(attention_layers) != len(mha_companion_layers):
        raise ValueError('attention and MHA companion metadata must be parallel')
​
    populated = [layer for layer in attention_layers if layer is not None]
    if not populated or any(not hasattr(layer, 'layer_id') for layer in populated):
        if layer_model is not None:
            return (
                _align_pipeline_layers(attention_layers, layer_model),
                _align_pipeline_layers(mha_companion_layers, layer_model),
            )
        return attention_layers, mha_companion_layers
​
    max_layer_id = max(int(layer.layer_id) for layer in populated)
    indexed_attention = [None] * (max_layer_id + 1)
    indexed_companions = [None] * (max_layer_id + 1)
    has_reused_layers = False # 标记是否出现同一层对象的复用
​
    for attention, companion in zip(attention_layers, mha_companion_layers):
        if attention is None:
            if companion is not None:
                raise ValueError('MHA companion has no primary attention layer')
            continue
​
        layer_id = int(attention.layer_id)
        if layer_id < 0:
            raise ValueError(f'invalid or duplicate attention layer_id: {layer_id}')
​
        if indexed_attention[layer_id] is not None:
            # 已存在同 ID 节点:只有同一对象复用才放行,不同对象重复仍报错
            if (
                indexed_attention[layer_id] is not attention
                or indexed_companions[layer_id] is not companion
            ):
                raise ValueError(f'invalid or duplicate attention layer_id: {layer_id}')
            has_reused_layers = True
            continue
​
        indexed_attention[layer_id] = attention
        indexed_companions[layer_id] = companion
​
    # 复用层场景下,预展开表以执行位置为索引,长度与 layer_id 无关,
    # 压缩会丢失位置语义,因此原样返回调用方传入的列表
    if has_reused_layers:
        return attention_layers, mha_companion_layers
    return indexed_attention, indexed_companions
python/sglang/srt/observability/req_time_stats.py core-logic

在响应元数据中暴露 first_token_latency,是客户端可观测性需求的核心落点;仅在时间戳有效且有序时输出,避免脏数据。

# 来自 python/sglang/srt/observability/req_time_stats.py
​
def convert_to_output_meta_info(
    self, scheduler_time_stats=None, completion_tokens=0
):
    meta_info = {}
    if self.created_time > 0.0:
        meta_info['request_received_ts'] = convert_time_to_realtime(
            self.created_time
        )
​
    # 仅当 created_time 与 first_token_time 均有效(> 0)且有序
    # (first_token_time > created_time)时才输出,避免 0 哨兵和
    # 乱序时间戳产生负数或错误的延迟值
    if self.created_time > 0.0 and self.first_token_time > self.created_time:
        meta_info['first_token_latency'] = self.get_first_token_latency()
​
    if self.api_server_dispatch_finish_time > 0.0:
        meta_info['api_server_dispatch_finish_ts'] = convert_time_to_realtime(
            self.api_server_dispatch_finish_time
        )
    if self.response_sent_to_client_time > 0.0:
        meta_info['response_sent_to_client_ts'] = convert_time_to_realtime(
            self.response_sent_to_client_time
        )
    if self.finished_time > 0.0:
        meta_info['request_finished_ts'] = convert_time_to_realtime(
            self.finished_time
        )
​
    decode_latency = self.get_decode_latency()
    if decode_latency > 0.0 and completion_tokens > 1:
        meta_info['decode_throughput'] = (completion_tokens - 1) / decode_latency
    return meta_info

评论区精华

合并前重新触发 CI other

作者在合并前通过 /tag-and-rerun-ci 指令重新触发 CI(Run #34944830585 显示 Extra 失败)。PR 没有任何 review 评论,设计权衡未在评论区讨论。

结论:无实质讨论;PR 由作者直接合并,设计决策固化在实现与测试中。分支名提示这是一条同步分支 PR。 · closed

风险与影响

  1. 核心路径变更:cuda_graph_setup.py 位于 CUDA graph 捕获初始化路径,行为从“重复 ID 一律拒绝”放宽为“同一对象复用放行并返回原始表”。若调用方假定返回表长度恒等于 max_layer_id + 1,复用层场景下会不一致,需确认所有调用点只按 layer_id 寻址而不依赖压缩后的表。
  2. 校验语义放宽:错误消息 invalid or duplicate attention layer_id 在新逻辑下对 layer_id < 0 场景略不精确,但不影响行为正确性。
  3. 契约新增字段:meta_info 新增 first_token_latency,属于向后兼容增量,但依赖精确键集合做快照比对的客户端会观察到新字段。
  4. 验证缺口:作者声明未运行 GPU 推理,复用层模型路径(共享层/循环层)未经真实初始化验证;CI Extra 运行失败(Run #34944830585)的根因未在 PR 中说明。

对用户/API:响应 meta_info 新增可选 first_token_latency 字段,客户端可直接读取引擎测得的首 token 延迟;在非法时间戳场景该字段保持缺席,避免脏数据。对系统:修复了复用层模型(同一层对象出现在多个执行位置)在 CUDA graph 初始化阶段被误拒绝的问题,属于初始化路径的正确性恢复。对团队:改动范围小、无 breaking change,但需要在真实 GPU 上补跑复用层模型初始化验证,并跟进 CI Extra 失败。

核心路径变更 校验语义放宽 缺少 GPU 验证 CI Extra 失败

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论