Prhub

#24397 [diffusion] chore: clean CUDA cache only at explicit release points

原始 PR 作者 mickqian 合并时间 2026-05-05 22:30 文件变更 5 提交数 6 评论 3 代码增减 +50 / -8

执行摘要

明确 CUDA cache 清理时机,移除 stage 边界隐式清理

旧debug内存日志在每次stage边界隐式调用empty_cache(),导致debug日志改变请求延迟和stage计时;完全移除后又可能在请求失败或大组件释放后留下内存压力。本PR将cache清理移动到明确的点:请求失败/OOM处理,以及组件释放(_empty_cache_after_large_release)时。

推荐阅读,尤其是以下设计决策:

  • 将隐式副作用(debug日志中的empty_cache)显式化,分离观测和清理职责,是良好的工程实践。
  • 只在memory_intensive组件释放且满足特定条件(CUDA存储释放或LayerwiseOffloadStrategy)时清理,避免频繁empty_cache
  • 性能基线的同步更新策略以及为有波动的stage设置动态容差的方法值得借鉴。
讨论亮点

本PR无review评论,由作者直接合并。

实现拆解

  1. 组件管理器 (component_manager.py):新增_module_on_cuda_empty_cache_after_large_release方法,在_finish_usefinish_request中调用。仅对memory_intensive组件且在释放CUDA存储或使用LayerwiseOffloadStrategy时执行empty_cache(),替代原先每个stage边界隐式清理。
  2. 性能日志 (perf_logger.py):修改StageProfiler.__enter__中的debug内存日志,调用current_platform.get_available_gpu_memory(empty_cache=False),保持观察性而不触发清理。
  3. GPU工作器 (gpu_worker.py):在异常捕获片段中,若为OOM异常,添加torch.cuda.empty_cache(),确保失败后即时释放。
  4. 性能基线 (perf_baselines.json):收紧joyai_image_edit_ti2iqwen_image_edit_2511_ti2i等模型的多个stage期望值(如ImageEncodingStage从948ms降至740ms,ImageVAEEncodingStage从96ms降至84ms等)。
  5. 测试工具 (test_server_utils.py):在stage验证逻辑中,对DecodingStage增加90%相对容差和250ms最小绝对容差,以容忍因异步work归属导致的波动;其他stage保持原120ms绝对容差。
文件 模块 状态 重要度
python/sglang/multimodal_gen/runtime/managers/component_manager.py 组件管理器 modified 7.16
python/sglang/multimodal_gen/runtime/utils/perf_logger.py 性能日志 modified 5.27
python/sglang/multimodal_gen/runtime/managers/gpu_worker.py GPU 工作器 modified 4.99
python/sglang/multimodal_gen/test/server/perf_baselines.json 性能基线 modified 4.33
python/sglang/multimodal_gen/test/server/test_server_utils.py 测试工具 modified 4.33

关键符号

_module_on_cuda _empty_cache_after_large_release StageProfiler.__enter__ _execute_forward_common (OOM 处理 )

关键源码片段

python/sglang/multimodal_gen/runtime/managers/component_manager.py core-logic

核心变更,新增 `_module_on_cuda` 和 `_empty_cache_after_large_release` 方法,控制 cache 清理时机。

# 在 ComponentResidencyManager 类中def _module_on_cuda(self, module: nn.Module | None) -> bool:
    """返回模块是否在 CUDA 上"""
    return self._module_device(module) == "cuda"def _empty_cache_after_large_release(
    self,
    use: ComponentUse,
    strategy: ComponentResidencyStrategy,
    module: nn.Module,
    was_on_cuda: bool,
) -> None:
    """在可能释放大组件后显式清理 CUDA 缓存"""
    if not use.memory_intensive: # 仅针对标记为 memory intensive 的组件
        return
    # 检查是否从 CUDA 移出(释放了 CUDA 存储)
    released_cuda_storage = was_on_cuda and not self._module_on_cuda(module)
    # 或者是 LayerwiseOffloadStrategy(逐层卸载)
    released_layerwise_storage = isinstance(strategy, LayerwiseOffloadStrategy)
    if not (released_cuda_storage or released_layerwise_storage):
        return
    if not torch.get_device_module().is_available():
        return
    torch.get_device_module().empty_cache()
    self._trace("empty_cache", use, strategy, module, detail="after_release")def _finish_use(self, use, *, module=None, keep_on_warmup=False):
    module = module or self.get_module(use.component_name)
    if module is None:
        self._trace("skip_missing", use)
        return
    should_keep = (keep_on_warmup and self.state.batch_is_warmup) \
                  or self._should_keep_after_use(use)
    if should_keep:
        self._trace("keep", use, self.strategy_for(use.component_name, module), module)
        return
    strategy = self.strategy_for(use.component_name, module)
    self._trace("finish", use, strategy, module)
    # 新增:记录当前模块是否在 CUDA 上
    was_on_cuda = self._module_on_cuda(module)
    strategy.finish_use(module, use, self.state)
    # 新增:在可能释放大组件后清理 cache
    self._empty_cache_after_large_release(use, strategy, module, was_on_cuda)
python/sglang/multimodal_gen/runtime/utils/perf_logger.py core-logic

修改 debug 内存日志,不再隐式调用 empty_cache,保持观测性。

def __enter__(self):
    if self.log_stage_start_end:
        msg = f"[{self.stage_name}] started..."
        if self.logger.isEnabledFor(logging.DEBUG):
            # 保持观测性:获取可用内存但不隐式调用 empty_cache
            available_memory = current_platform.get_available_gpu_memory(
                empty_cache=False
            )
            msg += f" ({round(available_memory, 2)} GB left)"
        self.logger.info(msg)
    # ... 其余部分

评论区精华

没有提炼出高价值讨论线程

当前评论区没有形成足够清晰的争议点或结论,后续有更多讨论时会体现在这里。

风险与影响

  • memory_intensive标记不准确,某些大组件释放后可能不会触发cache清理,但OOM路径始终会清理,降低了风险。
  • 放宽DecodingStage容差(90% + 250ms)可能掩盖该stage的真实性能回归,需关注CI长期趋势。
  • 收紧的性能基线在硬件或驱动变化时可能误报,需定期维护。
  • 整体风险较低,变更经过CI验证(NVIDIA CI job 25357009336和74363515606均通过)。
  • 用户:成功请求不再支付per-stage empty_cache开销,延迟降低(如qwen_image_edit_2511_ti2i E2E从23525ms降至23405ms,joyai_image_edit_ti2i ImageEncodingStage从948ms降至740ms)。
  • 系统:GPU内存清理时机从“每个stage”变为“失败或大组件释放后”,减少不必要的同步开销。
  • 团队:需要同步本地测试基线;CI中解码阶段容差放宽降低flaky,但需注意不掩盖回归。
内存回收时机变更 测试容差放宽 性能基线收紧 核心路径变更

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论