Prhub

#23049 [Diffusion] Diffusion model support log-requests

原始 PR 作者 LLThomas 合并时间 2026-07-05 17:01 文件变更 5 提交数 5 评论 10 代码增减 +493 / -2

执行摘要

为 Diffusion 服务添加请求日志记录功能

来自 PR body 的表述:'Diffusion model serving (image/video generation) did not support --log-requests, --log-requests-level, --log-requests-format, or --log-requests-target, so these flags were rejected at startup.' 此 PR 将这些功能引入 diffusion 运行时,满足用户对 diffusion 服务请求级日志的需求。

值得快速浏览,了解如何复用 srt 的日志基础设施为 diffusion 路径添加可观测性。重点关注 DiffusionRequestLogger 的设计(采样配置白名单、级别语义)以及钩子插入点(scheduler_client.forward)。该 PR 为后续 diffusion 服务的运维监控奠定了基础。

讨论亮点

Review 中核心讨论包括:

  • 测试文件位置争议:mickqian 指出初始测试文件位于 test/registered/utils/test_request_logger.py,要求迁至 python/sglang/multimodal_gen/test。最终 PR 将测试文件置于正确位置,该争议已解决。
  • --log-requests 默认值冗余:ping1jing2 指出 action='store_true' 应隐含 default=false,无需显式设置。但最终代码中仍保留了 default=ServerArgs.log_requests,该评论未导致修改。
  • 日志应循环所有请求:gemini-code-assist 建议在 scheduler 的事件循环中遍历所有 reqs 记录完成日志,而不仅是第一条。当前实现中 log_finished_request 本身已对列表循环,但在 scheduler.py 的事件循环里仍只调用了一次。该建议未确认采纳,属于潜在的正确性关注点。

实现拆解

  1. 新建 DiffusionRequestLogger 类python/sglang/multimodal_gen/runtime/utils/request_logger.py):该类镜像 srt 的 RequestLogger 设计,定义了 10 个采样配置白名单字段(_SAMPLING_CONFIG_FIELDS),并通过 log_requests_level 控制日志详略(0-3 级)。通过 from_server_args 工厂方法从 ServerArgs 初始化,底层使用 srt 的 create_log_targets 支持多目标(stdout 和文件)输出。

  2. 添加 CLI 参数python/sglang/multimodal_gen/runtime/server_args.py):在 ServerArgs dataclass 中新增 log_requestslog_requests_levellog_requests_formatlog_requests_target 四个字段,并在 add_cli_args 中注册对应的 argparse 参数。

  3. 在 scheduler_client 中插入日志钩子python/sglang/multimodal_gen/runtime/scheduler_client.py):在 SchedulerClient.initialize 中创建 DiffusionRequestLogger 实例;在 forward() 方法开始前调用 log_received_request(batch),在成功收到响应后调用 log_finished_request(batch, output_batch)AsyncSchedulerClient 类也做了相同的改造,保证同步和异步路径的一致性。

  4. 编写集成测试python/sglang/multimodal_gen/test/server/test_request_logger.py):使用 ServerManager 启动带有不同日志格式(text/json)的 diffusion 服务,分别测试图像和视频生成场景,验证 stdout 和文件两种目标是否正确输出 request.receivedrequest.finished 事件。

  5. 更新 CLI 文档docs_new/docs/sglang-diffusion/api/cli.mdx):在 Common Options 下新增 "Request logging" 小节,说明四个参数的用法和默认值。

文件 模块 状态 重要度
python/sglang/multimodal_gen/runtime/utils/request_logger.py 日志记录器 added 9.21
python/sglang/multimodal_gen/runtime/scheduler_client.py 调度客户端 modified 6.49
python/sglang/multimodal_gen/runtime/server_args.py 服务参数 modified 5.95
python/sglang/multimodal_gen/test/server/test_request_logger.py 集成测试 added 7.45
docs_new/docs/sglang-diffusion/api/cli.mdx 文档 modified 2.7

关键符号

DiffusionRequestLogger.__init__ DiffusionRequestLogger.from_server_args DiffusionRequestLogger.log_received_request DiffusionRequestLogger.log_finished_request SchedulerClient.forward AsyncSchedulerClient.forward

关键源码片段

python/sglang/multimodal_gen/runtime/utils/request_logger.py core-logic

核心新增文件,实现 DiffusionRequestLogger 类,定义了日志级别、采样配置白名单、接收 / 完成记录的格式化逻辑。

# python/sglang/multimodal_gen/runtime/utils/request_logger.py ( 关键部分 )from typing import Any, Optional
from sglang.srt.utils.log_utils import create_log_targets
from sglang.srt.utils.request_logger import (
    _dataclass_to_string_truncated,
    _transform_data_for_logging,
)# 记录到日志中的采样配置白名单字段(不含 prompt)
_SAMPLING_CONFIG_FIELDS = (
    "data_type", "seed", "num_inference_steps", "guidance_scale",
    "true_cfg_scale", "width", "height", "num_frames", "fps",
    "num_outputs_per_prompt",
)class DiffusionRequestLogger:
    def __init__(self, log_requests: bool, log_requests_level: int,
                 log_requests_format: str, log_requests_target: Optional[list]):
        self.log_requests = log_requests
        self.log_requests_level = log_requests_level
        self.log_requests_format = log_requests_format
        self.log_requests_target = log_requests_target
        # 使用 srt 的 create_log_targets 支持 stdout 和文件多目标
        self.targets = create_log_targets(
            targets=log_requests_target, name_prefix=__name__
        )
        self.log_exceeded_ms = envs.SGLANG_LOG_REQUEST_EXCEEDED_MS.get()
        # level 2 时截断 prompt 为 2 KiB,否则不限制
        self._max_length = (
            _TRUNCATE_LENGTH if self.log_requests_level == 2 else _UNLIMITED
        )
​
    @classmethod
    def from_server_args(cls, server_args: Any) -> "DiffusionRequestLogger":
        """从 ServerArgs 构造日志记录器实例"""
        return cls(
            log_requests=server_args.log_requests,
            log_requests_level=server_args.log_requests_level,
            log_requests_format=server_args.log_requests_format,
            log_requests_target=server_args.log_requests_target,
        )
​
    def log_received_request(self, reqs: list) -> None:
        """记录请求接收事件,为每个 req 生成一条日志"""
        if not self.log_requests:
            return
        for req in reqs:
            rid, obj = self._batch_record([req]) # 单请求处理
            self._write_event("request.received", rid, obj)
​
    def log_finished_request(self, reqs: list, result: Any) -> None:
        """记录请求完成事件,包含 e2e 延迟和可能的错误"""
        if not self.log_requests:
            return
        for req in reqs:
            rid, obj = self._batch_record([req])
            result_view = self._result_view(result)
            self._write_event("request.finished", rid, obj, extra=result_view)
​
    def _config_view(self, req: Any, *, drop_seed: bool = False) -> dict:
        """根据日志级别生成配置视图,低于 level 1 返回空"""
        sp = getattr(req, "sampling_params", None)
        if sp is None or self.log_requests_level < 1:
            return {}
        cfg = {name: getattr(sp, name, None) for name in _SAMPLING_CONFIG_FIELDS}
        if drop_seed:
            cfg.pop("seed", None)
        view: dict = {"sampling_params": cfg}
        if self.log_requests_level >= 2:
            view["prompt"] = getattr(sp, "prompt", None)
            view["negative_prompt"] = getattr(sp, "negative_prompt", None)
        return view
​
    def _result_view(self, result: Any) -> dict:
        """从 result 提取 e2e 延迟和错误信息"""
        e2e_latency = 0.0
        metrics = getattr(result, "metrics", None) if result is not None else None
        if metrics is not None:
            e2e_latency = getattr(metrics, "total_duration_s", 0.0) or 0.0
        return {
            "meta_info": {"e2e_latency": e2e_latency},
            "error": getattr(result, "error", None) if result is not None else None,
        }
python/sglang/multimodal_gen/runtime/scheduler_client.py dependency-wiring

日志钩子插入点,在 SchedulerClient 和 AsyncSchedulerClient 的 forward() 中调用日志记录,是日志功能与调度路径结合的关键位置。

# python/sglang/multimodal_gen/runtime/scheduler_client.py ( 关键改动 )from sglang.multimodal_gen.runtime.utils.request_logger import (
    DiffusionRequestLogger,
)class SchedulerClient:
    def __init__(self):
        self.context = None
        self.scheduler_socket = None
        self.server_args = None
        self.request_logger: Optional[DiffusionRequestLogger] = None # 新增 logger 属性
​
    def initialize(self, server_args: ServerArgs):
        self.server_args = server_args
        # 从 server_args 创建日志记录器(默认不启用)
        self.request_logger = DiffusionRequestLogger.from_server_args(server_args)
        self.context = zmq.Context()
        self.scheduler_socket = self.context.socket(zmq.REQ)
        # ... 原有初始化代码 ...
​
    def forward(self, batch: Any, timeout_ms: int | None = None) -> Any:
        """发送请求并等待响应,前后记录日志"""
        # 记录请求进入(scheduler 处理前)
        self.request_logger.log_received_request(batch)
        previous_timeout_ms = None
        if timeout_ms is not None:
            previous_timeout_ms = self.scheduler_socket.getsockopt(zmq.RCVTIMEO)
            self.scheduler_socket.setsockopt(zmq.RCVTIMEO, timeout_ms)
        try:
            self.scheduler_socket.send_pyobj(batch)
            output_batch = self.scheduler_socket.recv_pyobj()
            _materialize_output_batch_file_refs(output_batch)
            # 记录请求完成(收到 scheduler 响应后)
            self.request_logger.log_finished_request(batch, output_batch)
            return output_batch
        except zmq.error.Again:
            logger.error("Timeout waiting for response from scheduler.")
            raise TimeoutError("Scheduler did not respond in time.")
        finally:
            if previous_timeout_ms is not None and self.scheduler_socket is not None:
                self.scheduler_socket.setsockopt(zmq.RCVTIMEO, previous_timeout_ms)

评论区精华

Diffusion 测试文件应移至正确位置 测试

mickqian 指出初始测试文件位于 `test/registered/utils/test_request_logger.py` 位置错误,应归于 diffusion 模块。

结论:采纳,最终测试文件移至 `python/sglang/multimodal_gen/test/server/test_request_logger.py`。 · 已解决

`--log-requests` 使用 `action='store_true'` 时默认值冗余 style

ping1jing2 指出 `store_true` 的 argparse 参数不应显式设置 `default=ServerArgs.log_requests`,因为默认即为 `False`。

结论:未修改,最终代码仍保留了 `default=ServerArgs.log_requests`。 · unresolved

`log_finished_request` 应循环所有请求 正确性

gemini-code-assist 建议在 scheduler.py 的事件循环中遍历所有 `reqs` 记录完成日志,而不仅限于第一条。当前 `log_finished_request` 已对列表循环,但调用点可能只传入单个元素。

结论:未明确采纳,但 PR 最终代码在 `scheduler_client.forward` 中已对 batch 列表循环,而 scheduler.py 中的调用点未修改。 · unresolved

风险与影响

  1. 性能开销:当 --log-requests-level >= 2 时会序列化提示词(最大 2 KiB),高频请求下可能增加 IPC 和 I/O 负载。但日志默认关闭(False),用户需主动启用。
  2. 敏感信息泄露:提示词可能包含用户敏感信息,日志文件若未妥善保护可能造成泄露。默认仅记录元数据,级别 3 才输出完整提示。
  3. 调度器路径侵入:在 SchedulerClient.forward() 中增加日志钩子,若 logger 初始化失败可能阻塞请求路径。当前 logger 初始化不依赖外部资源,风险较低。
  4. 测试覆盖:集成测试覆盖了 text/json 格式和图像/视频模型,但未覆盖异步路径和错误场景(如超时、目标目录不可写)。

用户:现在可以通过指定 --log-requests 等参数为 diffusion 服务启用请求级日志,便于调试、监控和审计。日志格式与 LLM 路径一致,降低了运维学习成本。
系统:新增的日志输出会增加磁盘/网络 I/O,但默认关闭,按需启用。MTP 等辅助输出不受影响。
团队:维护一个独立的 DiffusionRequestLogger,与 SRT 的 RequestLogger 代码路径平行,后续功能演进需要同步更新。

日志性能开销 敏感信息泄露风险 调度器路径侵入

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论