执行摘要
- 一句话:GLM-Image 响应新增 token usage 报告
- 推荐动作:该 PR 值得精读,尤其适合关注以下设计决策:usage 在 AR/DiT 分离部署下的提取、聚合与按输出拆分机制;内部字段(cached_tokens)与公开字段(prompt_tokens_details)的边界处理;
--enable-cache-report 与 LLM 侧同名参数的语义对齐方式。阅读时建议重点核对 glm_image.py 的返回类型变化和 gpu_worker.py 的合并语义,并留意合入前文档要求未落实、GPU CI 未验证两个遗留问题。
功能与动机
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 由谁产生、由谁返回的问题。
实现拆解
实现按以下 5 步拆解:
-
数据契约扩展:在 python/sglang/multimodal_gen/runtime/pipelines_core/schedule_batch.py 中给 Req 和 OutputBatch 新增 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 字段。
-
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_tokens 与 generate_prior_tokens_batch 的返回值由二元组扩展为三元组(增加 usage 或 usages 列表),本地模式(不走外部 SRT)下 usage 恒为 None。
-
聚合与透传:新增 _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.py 的 forward 把 batch.usage 透传到 OutputBatch;gpu_worker.py 的 _req_to_output_batch 保留 usage,_merge_expanded_singletons 在展开批合并时对 int 类型 usage 字段累加。
-
响应组装与开关:python/sglang/multimodal_gen/runtime/entrypoints/openai/utils.py 的 add_common_data_to_response 中,先从内部 usage 中 pop 出 cached_tokens(默认隐藏),仅当 --enable-cache-report 开启且缓存命中数大于 0 时,才写入 prompt_tokens_details.cached_tokens;python/sglang/multimodal_gen/runtime/entrypoints/openai/image_api.py 的 _build_image_response_kwargs 为 usage 追加 image_count = 实际生成图片数。
-
配置与测试配套: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(模块 图像管线;类别 source;类型 data-contract;符号 _extract_srt_usage, _merge_srt_usage, _merge_srt_usages, generate_prior_tokens): 核心实现文件:新增 _extract_srt_usage / _merge_srt_usage / _merge_srt_usages,扩展 generate_prior_tokens 与 generate_prior_tokens_batch 返回值,在 run_grouped_requests 和 forward 中完成 usage 聚合与按输出拆分,是 AR/DiT 分离部署下 usage 传递的源头。
python/sglang/multimodal_gen/test/unit/test_glm_image_ar.py(模块 测试配套;类别 test;类型 test-coverage;符号 test_srt_ar_extracts_usage_from_meta_info, test_srt_ar_forward_aggregates_usage, test_image_response_adds_image_count_to_usage, test_image_response_reports_cached_tokens_when_cache_report_enabled): 测试配套:新增 4 个用例覆盖 meta_info 提取、多输出 usage 聚合、image_count 字段、cache-report 开关行为,并调整既有用例适配新的三元组返回值。
python/sglang/multimodal_gen/runtime/entrypoints/openai/protocol.py(模块 协议模型;类别 source;类型 core-logic;符号 ImagePromptTokensDetails, ImageUsage, ImageResponse): 协议模型层:新增 ImagePromptTokensDetails 与 ImageUsage,ImageResponse 挂载可选 usage,定义了对外的 OpenAI 兼容契约。
python/sglang/multimodal_gen/runtime/entrypoints/openai/utils.py(模块 响应组装;类别 source;类型 core-logic;符号 add_common_data_to_response): 响应组装层:在 add_common_data_to_response 中剥离内部 cached_tokens,按 enable_cache_report 条件转为 prompt_tokens_details.cached_tokens,是对外可见性与内部信息保护的边界。
python/sglang/multimodal_gen/runtime/entrypoints/openai/image_api.py(模块 接口入口;类别 source;类型 entrypoint;符号 _build_image_response_kwargs): HTTP 入口层:在 _build_image_response_kwargs 中为 usage 追加 image_count 字段,实现响应中图片数量上报。
python/sglang/multimodal_gen/runtime/server_args/server_args.py(模块 服务配置;类别 source;类型 configuration;符号 ServerArgs, add_cli_args): 配置层:新增 enable_cache_report 默认值及 --enable-cache-report CLI 参数,用于控制 cached_tokens 是否对外暴露。
python/sglang/multimodal_gen/runtime/pipelines_core/schedule_batch.py(模块 批处理;类别 source;类型 core-logic;符号 Req, OutputBatch): 数据结构层:Req 与 OutputBatch 新增 usage 字段,作为 usage 跨阶段透传的统一载体。
python/sglang/multimodal_gen/runtime/managers/gpu_worker.py(模块 工作节点;类别 source;类型 core-logic;符号 _req_to_output_batch, _merge_expanded_singletons): 工作节点层:_req_to_output_batch 保留 usage,_merge_expanded_singletons 在多输出合并时对 int 型 usage 字段累加,保证展开/合并路径不丢数据。
python/sglang/multimodal_gen/runtime/pipelines_core/stages/decoding.py(模块 解码阶段;类别 source;类型 core-logic;符号 forward): 解码阶段:forward 将 batch.usage 透传到 OutputBatch,完成 usage 从 Req 到最终响应的关键一跳。
关键符号:_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
核心实现文件:新增 _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
测试配套:新增 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
协议模型层:新增 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
评论区精华
review 讨论集中在四个问题上:
风险与影响
-
风险:具体风险点如下:
-
返回类型变更(二元组 → 三元组):glm_image.py 的 generate_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 未验证即合入, 同名配置参数存在混淆风险
关联脉络
- PR #33731 [CI] Fix GLM-Image usage unit tests: 该 PR 修复 GLM-Image usage 单测夹具以匹配本 PR 引入的新契约(如 OutputBatch.usage),是直接由本 PR 触发的后续修复,验证了 usage 字段对测试体系的影响。
- PR #31483 [AMD] ci: run vetted nested multimodal_gen unit tests on AMD: 同为 multimodal_gen 测试域扩展,GLM-Image 相关单测在 AMD CI 上的覆盖与该 PR 的测试配套形成连续脉络。
- PR #33655 [diffusion] Prefer cuDNN SDPA over FA4 for dense attention on sm_100 (B200): 同为 diffusion/multimodal_gen 运行时管线演进,反映该模块持续优化性能与观测能力的整体方向。
参与讨论