Prhub

#33378 [Feature] Add GLM Image usage report

原始 PR 作者 ChefWu551 合并时间 2026-08-05 18:59 文件变更 9 提交数 5 评论 8 代码增减 +249 / -11

执行摘要

GLM-Image 响应新增 token usage 报告

PR body 指出:在 AR/DiT 分离部署中,token usage 由 AR 阶段产生,但最终响应由 DiT 服务返回,此前 /v1/images/generations 只返回图像数据、延迟和内存信息,AR 阶段的 token 用量完全丢失。作者在评论中补充了四点价值:帮助用户检查 prefix cache 是否生效及缓存命中量;cached tokens 可能遵循不同的内部计费规则,暴露出来便于记账统计;使 GLM-Image 的 usage 报告与 LLM chat/completion API 保持一致;帮助调试 AR/DiT 分离部署中 token 由谁产生、由谁返回的问题。

该 PR 值得精读,尤其适合关注以下设计决策:usage 在 AR/DiT 分离部署下的提取、聚合与按输出拆分机制;内部字段(cached_tokens)与公开字段(prompt_tokens_details)的边界处理;--enable-cache-report 与 LLM 侧同名参数的语义对齐方式。阅读时建议重点核对 glm_image.py 的返回类型变化和 gpu_worker.py 的合并语义,并留意合入前文档要求未落实、GPU CI 未验证两个遗留问题。

讨论亮点

review 讨论集中在四个问题上:

  • 动机说明:ping1jing2 要求 PR 描述补充为什么需要 token usage 报告;作者给出四点理由(prefix cache 校验、计费统计、与 LLM API 对齐、AR/DiT 分离部署调试),已被接受。
  • 新 API 是否冗余:ping1jing2 质疑新增的 flag 是用户可见 API 却未被使用;作者澄清该 flag 正被图像响应路径使用,LLM 侧已有同名参数,二者不冲突。
  • 文档更新要求:ping1jing2 在合入前明确要求"if you add one more API, please update the documentation",但最终合入的变更集中没有 docs 文件,该要求未在 PR 内落实(reviewer 最终仍 approved)。
  • CI 合入门禁:mickqian 质疑为何未通过 Nvidia CI 即合入;ping1jing2 解释失败 CI 与该 PR 无关,且当时所有 GPU CI 均被取消,属于流程执行而非技术问题。

实现拆解

实现按以下 5 步拆解:

  1. 数据契约扩展:在 python/sglang/multimodal_gen/runtime/pipelines_core/schedule_batch.py 中给 ReqOutputBatch 新增 usage: dict[string, Any] | None 字段,作为 usage 跨阶段传递的统一载体;在 python/sglang/multimodal_gen/runtime/entrypoints/openai/protocol.py 新增 ImagePromptTokensDetails(cached_tokens)和 ImageUsage(prompt/completion/total/reasoning tokens、prompt_tokens_details、image_count)两个 pydantic 模型,ImageResponse 挂载可选 usage 字段。

  2. AR 阶段提取 usage:在 python/sglang/multimodal_gen/runtime/pipelines_core/stages/model_specific_stages/glm_image.py 新增 _extract_srt_usage(meta_info),从外部 SRT AR 服务响应的 meta_info 中提取 prompt/completion/reasoning/cached tokens 并计算 total_tokens;generate_prior_tokensgenerate_prior_tokens_batch 的返回值由二元组扩展为三元组(增加 usage 或 usages 列表),本地模式(不走外部 SRT)下 usage 恒为 None。

  3. 聚合与透传:新增 _merge_srt_usage_merge_srt_usages 对多输出(num_outputs_per_prompt > 1)的 usage 做整数累加;run_grouped_requests 将聚合结果写入 batch.usage,并通过 batch.extra["usage_by_output"] 按输出拆分,_make_sequential_request 为单个 Req 回填对应 usage;decoding.pyforwardbatch.usage 透传到 OutputBatchgpu_worker.py_req_to_output_batch 保留 usage,_merge_expanded_singletons 在展开批合并时对 int 类型 usage 字段累加。

  4. 响应组装与开关python/sglang/multimodal_gen/runtime/entrypoints/openai/utils.pyadd_common_data_to_response 中,先从内部 usage 中 pop 出 cached_tokens(默认隐藏),仅当 --enable-cache-report 开启且缓存命中数大于 0 时,才写入 prompt_tokens_details.cached_tokenspython/sglang/multimodal_gen/runtime/entrypoints/openai/image_api.py_build_image_response_kwargs 为 usage 追加 image_count = 实际生成图片数

  5. 配置与测试配套python/sglang/multimodal_gen/runtime/server_args/server_args.py 新增 enable_cache_report: bool = False 默认值与同名 CLI 参数;test/unit/test_glm_image_ar.py 新增 4 个用例,覆盖 meta_info 提取、多输出 usage 聚合、image_count 字段、cache-report 开关行为。文档未更新(review 中提出但合入前未见 docs 改动)。

文件 模块 状态 重要度
python/sglang/multimodal_gen/runtime/pipelines_core/stages/model_specific_stages/glm_image.py 图像管线 modified 8.19
python/sglang/multimodal_gen/test/unit/test_glm_image_ar.py 测试配套 modified 7.33
python/sglang/multimodal_gen/runtime/entrypoints/openai/protocol.py 协议模型 modified 6.47
python/sglang/multimodal_gen/runtime/entrypoints/openai/utils.py 响应组装 modified 6.04
python/sglang/multimodal_gen/runtime/entrypoints/openai/image_api.py 接口入口 modified 5.1
python/sglang/multimodal_gen/runtime/server_args/server_args.py 服务配置 modified 5.23
python/sglang/multimodal_gen/runtime/pipelines_core/schedule_batch.py 批处理 modified 5.1
python/sglang/multimodal_gen/runtime/managers/gpu_worker.py 工作节点 modified 5.48
python/sglang/multimodal_gen/runtime/pipelines_core/stages/decoding.py 解码阶段 modified 4.56

关键符号

_extract_srt_usage _merge_srt_usage _merge_srt_usages generate_prior_tokens generate_prior_tokens_batch run_grouped_requests GlmImageAR.forward add_common_data_to_response _build_image_response_kwargs _req_to_output_batch _merge_expanded_singletons

关键源码片段

python/sglang/multimodal_gen/runtime/pipelines_core/stages/model_specific_stages/glm_image.py data-contract

核心实现文件:新增 _extract_srt_usage / _merge_srt_usage / _merge_srt_usages,扩展 generate_prior_tokens 与 generate_prior_tokens_batch 返回值,在 run_grouped_requests 和 forward 中完成 usage 聚合与按输出拆分,是 AR/DiT 分离部署下 usage 传递的源头。

# 从外部 SRT AR 服务的响应 meta_info 中提取 token 用量。
# 外部 SRT 是独立的 LLM 服务,AR 阶段的 token 在这里产生;
# 本地模式(直接调用 vision_language_encoder.generate)没有 meta_info,返回 None。
def _extract_srt_usage(meta_info: dict[str, Any] | None) -> dict[str, int] | None:
    if not isinstance(meta_info, dict):
        return None
​
    usage = {
        "prompt_tokens": int(meta_info.get("prompt_tokens", 0) or 0),
        "completion_tokens": int(meta_info.get("completion_tokens", 0) or 0),
        "reasoning_tokens": int(meta_info.get("reasoning_tokens", 0) or 0),
        "cached_tokens": int(meta_info.get("cached_tokens", 0) or 0),
    }
    # total_tokens 由计算得出,不直接依赖上游字段,保证一致性
    usage["total_tokens"] = usage["prompt_tokens"] + usage["completion_tokens"]
    return usage
​
​
# 把单次 AR 输出的 usage 合并进累计值。cached_tokens / reasoning_tokens
# 都按整数累加,最终由响应层决定是否对外暴露。
def _merge_srt_usage(
    total_usage: dict[str, Any] | None, usage: dict[str, int] | None
) -> dict[str, Any] | None:
    if usage is None:
        return total_usage
    if total_usage is None:
        total_usage = {}
    for key, value in usage.items():
        total_usage[key] = int(total_usage.get(key, 0)) + int(value)
    return total_usage
​
​
# 多个输出的 usage 依次合并,供 num_outputs_per_prompt > 1 的场景使用
# (例如一次请求生成多张图时,把每张图对应 AR 调用的 token 数加总)。
def _merge_srt_usages(
    usages: list[dict[str, int] | None],
) -> dict[str, Any] | None:
    total_usage = None
    for usage in usages:
        total_usage = _merge_srt_usage(total_usage, usage)
    return total_usage
python/sglang/multimodal_gen/test/unit/test_glm_image_ar.py test-coverage

测试配套:新增 4 个用例覆盖 meta_info 提取、多输出 usage 聚合、image_count 字段、cache-report 开关行为,并调整既有用例适配新的三元组返回值。

# 验证从外部 SRT 响应的 meta_info 中正确提取 usage:
# prompt/completion/reasoning/cached 全部按 int 读取,total 由计算得出。
def test_srt_ar_extracts_usage_from_meta_info(self, mock_post, _mock_device):
    set_global_server_args(self._server_args())
    mock_post.return_value = _FakeResponse(
        list(range(1025)),
        meta_info={"prompt_tokens": 13, "completion_tokens": 25,
                   "reasoning_tokens": 0, "cached_tokens": 5},
    )
    stage = GlmImageAR(processor=_FakeProcessor(), vision_language_encoder=None)
​
    _, _, usage = stage.generate_prior_tokens(
        prompt="A simple product sketch",
        height=1024,
        width=1024,
        server_args=self._server_args(),
    )
​
    self.assertEqual(
        usage,
        {"prompt_tokens": 13, "completion_tokens": 25,
         "reasoning_tokens": 0, "cached_tokens": 5, "total_tokens": 38},
    )
​
​
# 验证多输出聚合:同一请求生成多张图时,usage 按整数累加
# (两个输出各 13 prompt + 25 completion,聚合后为 26 + 50)。
def test_srt_ar_forward_aggregates_usage(self, mock_post, _mock_device):
    set_global_server_args(self._server_args())
    mock_post.side_effect = [
        _FakeResponse(list(range(1025)),
                      meta_info={"prompt_tokens": 13, "completion_tokens": 25}),
        _FakeResponse(list(range(1025)),
                      meta_info={"prompt_tokens": 13, "completion_tokens": 25}),
    ]
    stage = GlmImageAR(processor=_FakeProcessor(), vision_language_encoder=None)
    batch = SimpleNamespace(prompt="A simple product sketch", height=1025,
                            width=1001, image_path=None,
                            num_outputs_per_prompt=2, seed=None)
​
    batch = stage.forward(batch, self._server_args())
​
    self.assertEqual(batch.usage["prompt_tokens"], 26)
    self.assertEqual(batch.usage["completion_tokens"], 50)
    self.assertEqual(batch.usage["total_tokens"], 76)
python/sglang/multimodal_gen/runtime/entrypoints/openai/protocol.py core-logic

协议模型层:新增 ImagePromptTokensDetails 与 ImageUsage,ImageResponse 挂载可选 usage,定义了对外的 OpenAI 兼容契约。

# OpenAI 兼容的图像生成 usage 协议模型。
# prompt_tokens_details 只在 --enable-cache-report 时由响应层填充,
# 协议层保持可选,避免破坏既有客户端。
class ImagePromptTokensDetails(BaseModel):
    cached_tokens: int = 0
​
​
class ImageUsage(BaseModel):
    prompt_tokens: Optional[int] = None
    total_tokens: Optional[int] = None
    completion_tokens: Optional[int] = None
    prompt_tokens_details: Optional[ImagePromptTokensDetails] = None
    reasoning_tokens: Optional[int] = 0
    # image_count 是 SGLang 对图像 API 的扩展字段,表示本次实际生成的图片数
    image_count: Optional[int] = None
​
​
class ImageResponse(BaseModel):
    id: str
    created: int = Field(default_factory=lambda: int(time.time()))
    data: List[ImageResponseData]
    peak_memory_mb: Optional[float] = None
    inference_time_s: Optional[float] = None
    # usage 可选:本地模式或旧版本响应可能不携带该字段
    usage: Optional[ImageUsage] = None

评论区精华

PR 描述缺少动机说明 question

ping1jing2 提出:could you add more words in the description to explain why we need to add token usage report?

结论:作者补充四点理由:检查 prefix cache 是否生效、cached tokens 计费规则差异、与 LLM API 对齐、AR/DiT 分离部署调试。 · 已解决

新增用户可见 API 是否未使用 设计

ping1jing2 质疑:seems like we add one more user-facing API but don't use it, right?

结论:作者澄清:--enable-cache-report 用于图像响应路径,LLM API 侧 SGLang 已支持同名参数,并非未使用的 API。 · 已解决

新增 API 需同步更新文档 documentation

ping1jing2 要求:please update the documentation if you add one more API。

结论:合入的变更集中未见 docs 文件改动,该要求未在 PR 内落实;reviewer 最终仍 approving(pending 遗留)。 · 待处理

未通过 Nvidia CI 即合入 other

mickqian 质疑:why is this merged without passing Nvidia CI?

结论:ping1jing2 解释:失败 CI 与该 PR 无关,且 GPU CI 均被取消,属于流程执行问题而非代码缺陷。 · 已解决

风险与影响

具体风险点如下:

  • 返回类型变更(二元组 → 三元组)glm_image.pygenerate_prior_tokens / generate_prior_tokens_batch 返回值结构变化,若仓库内存在未更新的调用方会直接解包崩溃;当前已确认 forward 内两处调用和测试均同步更新,但这是数据契约变更,外部扩展(如自定义 stage)存在遗漏可能。
  • usage 合并逻辑的覆盖语义gpu_worker.py::_merge_expanded_singletons 对非 int 类型的 usage 值直接覆盖而非合并,未来若 usage 中出现嵌套结构(如 prompt_tokens_details)且多输出合并时,非缓存字段可能被覆盖;当前内部 usage 仅含 int 字段,风险有限。
  • cached_tokens 剥离的隐式依赖utils.py 无条件 pop 顶层 cached_tokens 并仅在 --enable-cache-report 时转为 prompt_tokens_details.cached_tokens,如果上游模块已填充 prompt_tokens_details,则不会重复注入但会丢掉顶层字段,语义需要文档明确。
  • 同名参数配置分离:multimodal_gen 的 --enable-cache-report 与 SRT LLM 服务的同名参数是两套独立配置对象,用户若只在 AR SRT 服务上开启,图像响应不会生效,存在配置混淆风险。
  • 本地模式行为差异:未配置 srt_encoder_url 的本地模式(直接调用 vision_language_encoder.generate)usage 恒为 None,响应中不出现 usage 字段,调用方需兼容有无 usage 两种响应。
  • CI 验证不足:合入时 Nvidia GPU CI 被取消,GLM-Image 真实 GPU 回归未完整验证;单元测试只覆盖 mock 路径。

影响范围集中在 multimodal_gen 模块的 GLM-Image 路径,但波及 9 个文件、多个子系统:

  • 用户侧:GLM-Image 调用方(尤其 AR/DiT 分离部署)首次获得 token 用量反馈,可用于计费、prefix cache 命中率观测和成本核算;默认响应新增 usage 字段为向后兼容扩展,cached_tokens 默认隐藏避免内部成本信息泄漏。
  • 系统侧:usage 从 AR stage 的 Req / OutputBatch 一路透传到 HTTP 响应层,涉及 schedule_batch、decoding、gpu_worker、image_api 等多个模块,属于跨模块数据契约变更,影响后续所有 GLM-Image 执行路径。
  • 团队侧:为 multimodal_gen 统一 OpenAI 兼容 usage 语义奠定基础,ImageUsage / ImagePromptTokensDetails 协议模型可被其他图像/视频模型复用,聚合与拆分逻辑(_merge_srt_usage 系列)也有通用价值。
  • 影响程度:中等。默认行为仅是响应新增字段,不破坏既有客户端;但契约变化和文档缺失需要后续跟进。
跨模块数据契约变更 返回类型变更(二元组→三元组) 文档更新未落实 Nvidia GPU CI 未验证即合入 同名配置参数存在混淆风险

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论