执行摘要
- 一句话:为 Diffusion 服务添加请求日志记录功能
- 推荐动作:值得快速浏览,了解如何复用 srt 的日志基础设施为 diffusion 路径添加可观测性。重点关注
DiffusionRequestLogger 的设计(采样配置白名单、级别语义)以及钩子插入点(scheduler_client.forward)。该 PR 为后续 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 服务请求级日志的需求。
实现拆解
-
新建 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 和文件)输出。
-
添加 CLI 参数(python/sglang/multimodal_gen/runtime/server_args.py):在 ServerArgs dataclass 中新增 log_requests、log_requests_level、log_requests_format、log_requests_target 四个字段,并在 add_cli_args 中注册对应的 argparse 参数。
-
在 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 类也做了相同的改造,保证同步和异步路径的一致性。
-
编写集成测试(python/sglang/multimodal_gen/test/server/test_request_logger.py):使用 ServerManager 启动带有不同日志格式(text/json)的 diffusion 服务,分别测试图像和视频生成场景,验证 stdout 和文件两种目标是否正确输出 request.received 和 request.finished 事件。
-
更新 CLI 文档(docs_new/docs/sglang-diffusion/api/cli.mdx):在 Common Options 下新增 "Request logging" 小节,说明四个参数的用法和默认值。
关键文件:
python/sglang/multimodal_gen/runtime/utils/request_logger.py(模块 日志记录器;类别 source;类型 core-logic;符号 DiffusionRequestLogger, init, from_server_args, _request_id): 核心新增文件,实现 DiffusionRequestLogger 类,定义了日志级别、采样配置白名单、接收/完成记录的格式化逻辑。
python/sglang/multimodal_gen/runtime/scheduler_client.py(模块 调度客户端;类别 source;类型 dependency-wiring;符号 SchedulerClient.initialize, SchedulerClient.forward, AsyncSchedulerClient.initialize, AsyncSchedulerClient.forward): 日志钩子插入点,在 SchedulerClient 和 AsyncSchedulerClient 的 forward() 中调用日志记录,是日志功能与调度路径结合的关键位置。
python/sglang/multimodal_gen/runtime/server_args.py(模块 服务参数;类别 source;类型 dependency-wiring;符号 log_requests, log_requests_level, log_requests_format, log_requests_target): 添加了四个日志相关的 CLI 参数定义和默认值,是用户启用日志的配置入口。
python/sglang/multimodal_gen/test/server/test_request_logger.py(模块 集成测试;类别 test;类型 test-coverage;符号 _start_server, _cleanup_server, _create_client, _wait_for_video_completion): 集成测试,验证日志输出到 stdout 和文件,覆盖 text/json 格式和图像/视频模型。
docs_new/docs/sglang-diffusion/api/cli.mdx(模块 文档;类别 other;类型 entrypoint): 更新 CLI 文档,新增 "Request logging" 小节,方便用户了解日志参数。
关键符号: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
核心新增文件,实现 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
日志钩子插入点,在 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)
评论区精华
Review 中核心讨论包括:
风险与影响
关联脉络
- PR #30107 [diffusion] perf: add unified SP shard helpers and zero-copy tail-pad attention: 同属 diffusion 模块的基础设施改进,共享部分运行时依赖(如 scheduler_client.py 等)。该 PR 的日志功能与 SP 重构都是 diffusion 可观测性与性能演进的一部分。
- PR #30110 [diffusion] fix: shut down diffusion workers on serve exit: 同为 diffusion 运行时改进,涉及服务生命周期管理,与请求日志均属于服务可运维性提升。
参与讨论