Prhub

#30157 Size KV pool after CUDA graph capture (opt-in)

原始 PR 作者 cctry 合并时间 2026-07-08 03:05 文件变更 10 提交数 7 评论 16 代码增减 +1014 / -157

执行摘要

CUDA 图捕获后动态调整 KV 缓存池大小,减少内存浪费

--mem-fraction-static启发式中,KV池大小在CUDA图捕获前确定,图形内存的占用是猜测值,在大配置下偏差可达~10 GB/GPU,导致部分配置浪费内存,部分配置面临OOM。PR body明确指出:'the KV pool is sized before CUDA-graph capture, so graph memory is a heuristic guess baked into mem_fraction_static. On large configs the guess can be off by ~10 GB/GPU; the padded activation reserve silently absorbs the difference, wasting memory on some configs and risking OOM on others.'因此需要一种机制,在捕获图后根据实际空闲内存精确确定KV池大小。

推荐精读本PR,涉及底层CUDA VMM实现和KV池动态调整的设计权衡。对于大规模推理服务,可评估启用以提升内存效率。注意当前opt-in状态和HND槽计算争议,建议在非默认路径下充分验证后再默认开启。

讨论亮点
  • gemini-code-assist[bot]的高优先级问题
    • cuMemAddressReserve未包_check → 已修复。
    • 多进程库名竞态 → 已添加PID后缀。
    • 单位混淆(字节 vs MB) → 作者澄清无问题。
    • HND分页槽计算错误 → 作者反驳,未完全解决。
    • 释放顺序不安全 → 已调整。
  • merrymercy的设计评论
    • fallback headroom过于宽松 → 后续commit优化为只在必要场景保留。
    • 不要用getattr → 已移除getattr改用显式属性。
  • 未解决争议:HND槽计算逻辑仍需进一步验证。

实现拆解

  1. 新增CUDA虚拟内存管理arena (kv_vmm_backing.py):创建KvVmmArena类,通过CUDA VMM接口预留大块虚拟地址空间(默认256 GiB),不分配物理内存。提供bump分配器(内联C++桩),保证指针按granularity对齐,支持torch.cuda.CUDAPluggableAllocator。每个实例加载独立.so,避免多进程符号冲突。
  2. 修改mem_fraction计算路径 (server_args.py):新增post_capture_kv_sizing_planned()方法,当环境变量启用且满足条件(CUDA、非MLA、非memory_saver等)时,计算mem_fraction_static跳过图占用和激活预留,仅保留基础元数据和并行开销。
  3. KV池关联VMM arena (memory_pool.py):在KVCache及其子类中新增post_capture_active标志和_post_capture_owner引用。启用时_create_buffers使用KvVmmArena创建张量(仅虚拟地址)。新增KvBufferDesc辅助类精确描述缓冲区字节跨度,支持动态计算备份大小。
  4. 新增图捕获后调整池大小的方法 (model_runner_kv_cache_mixin.py):在ModelRunnerKVCacheMixin中添加post_capture_resize_kv_pool(),在CUDA图捕获完成后调用。测量当前空闲GPU内存,减去必要的headroom,计算KV池可用最大token数,调用_finalize_backing物理备份VMA范围。同时修改_profile_available_bytes,在启用时正确处理mamba预捕获预留。
  5. 配套修改分配器和SWA池:更新allocator/base.pyallocator/swa.py添加resize方法;swa_memory_pool.py添加finalize_backing协调子池备份。
  6. 端到端测试 (test_post_capture_kv_sizing.py):通过环境变量启动服务器,断言日志含有'Post-capture KV sizing'、API返回max_total_num_tokens>0、GSM8K准确率≥0.80。注册到base-b CI。
文件 模块 状态 重要度
python/sglang/srt/mem_cache/kv_vmm_backing.py VMM 层 added 8.89
python/sglang/srt/model_executor/model_runner_kv_cache_mixin.py KV 编排 modified 8.73
python/sglang/srt/mem_cache/memory_pool.py 内存池 modified 8.49
python/sglang/srt/server_args.py 配置 modified 8.09
test/registered/mem_cache/test_post_capture_kv_sizing.py 测试 added 6.83

关键符号

KvVmmArena.__init__ post_capture_resize_kv_pool _profile_available_bytes post_capture_kv_sizing_planned KvBufferDesc finalize_backing KVCache._create_buffers SWAKVPool.finalize_backing

关键源码片段

python/sglang/srt/mem_cache/kv_vmm_backing.py dependency-wiring

新增文件,实现 CUDA 虚拟内存管理 arena,是 post-capture sizing 的核心基础。

# 文件 python/sglang/srt/mem_cache/kv_vmm_backing.py ( 新增 )from __future__ import annotations
import ctypes, logging, os, tempfile
from math import prod
from typing import TYPE_CHECKING, List, Optional, Sequence
import torch, torch.utils.cpp_extension
from torch.cuda.memory import CUDAPluggableAllocatorlogger = logging.getLogger(__name__)
_drv = Nonedef _driver():
    """延迟加载 CUDA driver bindings."""
    global _drv
    if _drv is None:
        from cuda.bindings import driver
        _drv = driver
    return _drvdef _check(result, label: str):
    """包装 CUDA API 调用,失败时抛出 RuntimeError."""
    drv = _driver()
    err = result[0] if isinstance(result, tuple) else result
    if err != drv.CUresult.CUDA_SUCCESS:
        raise RuntimeError(f"{label} failed: {err}")
    return result[1] if isinstance(result, tuple) and len(result) > 1 else Nonedef _stub_source(sfx: str) -> str:
    """
    生成 C++ 桩代码,实现线程安全的 bump 分配器。
    符号按后缀区分,每个 arena 实例加载独立 .so,避免多进程/多实例冲突。
    """
    return f"""
#include <cstddef>
#include <cstdint>
#include <mutex>
extern "C" {{
static uintptr_t g_base = 0;
static size_t g_cursor = 0;
static size_t g_reserved = 0;
static size_t g_align = 512;
static std::mutex g_mu;
void kvarena_set_base_{sfx}(uintptr_t b) {{ g_base=b; g_cursor=0; }}
void kvarena_set_reserved_{sfx}(size_t r) {{ g_reserved=r; }}
void* kvarena_malloc_{sfx}(size_t size, int device, void* stream) {{
    size_t need = g_cursor + align_up(size, g_align);
    if (need > g_reserved) return 0;
    void* p = reinterpret_cast<void*>(g_base + g_cursor);
    g_cursor = need;
    return p;
}}
void kvarena_free_{sfx}(void* ptr, size_t size, int device, void* stream) {{}}
}}
"""class KvVmmArena:
    """单个设备上的 CUDA 虚拟内存预留,对外暴露为 torch.cuda.MemPool."""
    _instance_count = 0
​
    def __init__(self, device_id: int, reserve_bytes: int = 256 * 1024**3):
        self.device_id = int(device_id)
        # 使用 PID 避免多进程编译 .so 冲突
        self._sfx = f"{KvVmmArena._instance_count}_{os.getpid()}"
        KvVmmArena._instance_count += 1
        drv = _driver()
        with torch.cuda.device(self.device_id):
            _check(drv.cuInit(0), "cuInit")
            self._prop = drv.CUmemAllocationProp()
            self._prop.type = drv.CUmemAllocationType.CU_MEM_ALLOCATION_TYPE_PINNED
            self._prop.location.type = drv.CUmemLocationType.CU_MEM_LOCATION_TYPE_DEVICE
            self._prop.location.id = self.device_id
            self.granularity = query_granularity(self.device_id)
            self._access = drv.CUmemAccessDesc()
            self._access.location.type = drv.CUmemLocationType.CU_MEM_LOCATION_TYPE_DEVICE
            self._access.location.id = self.device_id
            self._access.flags = drv.CUmemAccess_flags.CU_MEM_ACCESS_FLAGS_PROT_READWRITE
            self.reserved = self._align(reserve_bytes)
            # 预留虚拟地址空间
            self.base = int(
                _check(
                    drv.cuMemAddressReserve(self.reserved, self.granularity, 0, 0),
                    "cuMemAddressReserve"
                )
            )
            # 编译 C++ 桩并设置基址和上限 ( 省略具体编译步骤 )
python/sglang/srt/model_executor/model_runner_kv_cache_mixin.py data-contract

核心修改文件,添加 post_capture_resize_kv_pool 和 post_capture_kv_active 属性,是后调整的编排中心。

# 文件 python/sglang/srt/model_executor/model_runner_kv_cache_mixin.py ( 修改 )class ModelRunnerKVCacheMixin:
​
    @property
    def post_capture_kv_active(self: ModelRunner) -> bool:
        """判断当前是否启用 post-capture KV sizing (在 draft 上禁用)."""
        return (
            self.server_args.post_capture_kv_sizing_planned()
            and current_platform.is_cuda()
            and not self.is_draft_worker
        )
​
    def post_capture_resize_kv_pool(self: ModelRunner) -> None:
        """
        在 CUDA 图捕获完成后调用,根据实际空闲内存重新确定 KV 池大小并物理备份。
        """
        pool = self.token_to_kv_pool
        torch.cuda.synchronize()
        free_gb = get_available_gpu_memory(
            self.device, self.gpu_id,
            distributed=get_world_group().world_size > 1,
            cpu_group=get_world_group().cpu_group,
        )
        headroom_gb = self.pre_model_load_memory * (1 - self.mem_fraction_static)
        decode_cfg = self.server_args.cuda_graph_config.decode
        eager_gap = (
            self.server_args.disaggregation_mode != "prefill"
            and decode_cfg.backend != Backend.DISABLED
            and decode_cfg.max_bs < self.max_running_requests
        )
        if eager_gap or self.mambaish_config is not None:
            headroom_gb = max(
                headroom_gb,
                self.server_args.mamba_pre_capture_reserve_mb(
                    get_device_memory_capacity(self.device)
                ) / 1024,
            )
        pool_gb = max(free_gb - headroom_gb, 0)
        # 将可用 GB 转换为 token 数 ( 具体计算略 )
        max_total_num_tokens = int(pool_gb * 1024**3 / self.pool_token_size_bytes)
        logger.info("Post-capture KV sizing: max_total_num_tokens=%d", max_total_num_tokens)
        if hasattr(pool, "finalize_backing"):
            pool.finalize_backing(max_total_num_tokens)
        self.dynamic_max_total_num_tokens = max_total_num_tokens

评论区精华

cuMemAddressReserve 缺少错误检查 正确性

gemini-code-assist[bot] 指出 cuMemAddressReserve 调用未包装 _check,失败时返回无效地址导致难调试的崩溃。建议包装。

结论:已在后续 commit 中修复,添加 _check 包装。 · 已解决

多进程环境下库名竞态 正确性

使用全局计数器 _instance_count 生成 .so 后缀,多进程间冲突导致编译 / 加载失败。建议加入 PID 后缀。

结论:已添加 os.getpid() 到后缀,保证唯一性。 · 已解决

get_device_memory_capacity 单位混淆 正确性

gemini 误认为 get_device_memory_capacity 返回字节,但实际返回 MiB,导致条件始终为真强制保留 10GB。作者澄清无问题。

结论:无实际操作问题,但文档需澄清。 · 已解决

HND 分页 KV 缓存槽计算错误 正确性

gemini 质疑 num_slots 应为 self.size 而非 self.size+page_size,否则导致过度分配。作者反驳认为 comment 不准确但逻辑正确。

结论:未完全达成一致,但代码保持原样。需要进一步验证。 · unresolved

释放顺序不安全 正确性

释放 _post_capture_owner 前未删除 PyTorch 张量,可能导致悬空指针。

结论:已调整删除顺序,先删张量再释放 owner。 · 已解决

fallback headroom 过于宽松 设计

merrymercy 指出 post_capture_resize 中 fallback 预留 >10GB 过多,应只在必要时保留。cctry 解释常见场景不会触发但接受优化。

结论:后续 commit 优化了条件,仅在 eager gap 或 mamba 场景保留 headroom。 · 已解决

不要使用 getattr 访问 post_capture_active style

merrymercy 要求移除 getattr,改为显式属性定义。cctry 同意并重构。

结论:已移除 getattr,所有子类显式声明 post_capture_active 属性。 · 已解决

风险与影响

  • CUDA VMM兼容性:依赖较新驱动和GPU,在旧硬件或非NVIDIA平台上可能启动失败(已有条件检查)。
  • 多进程竞态:虽已加入PID后缀,但多个进程共享临时目录时仍可能冲突(低概率)。
  • 激活headroom逻辑:headroom计算依赖mem_fraction_static准确性,若估算不足可能导致OOM;已有eager decode和mamba保护,但未覆盖全部边缘。
  • HND槽计算争议:若review中问题未被正确修复,可能导致物理内存过度分配或不足,影响大量用户。
  • 测试覆盖不足:仅测试了Llama-3.1-8B单模型,未覆盖MLA、DeepSeek V4、MoE等架构,也未测试多GPU场景。
  • CI负担:测试注册在base-b,每PR执行约4分钟,可能增加CI排队时间。
  • 用户:opt-in特性,默认关闭,对现有用户无影响。启用用户可在大GPU上节省约10 GB内存,或避免OOM。需了解CUDA VMM兼容性。
  • 系统:修改KV池分配核心路径和mem_fraction计算。启用时KV池初始化延迟到图捕获后,物理备份在后期单次完成,可能短暂增大抖动。
  • 团队:新增424行核心代码和测试,需要熟悉CUDA VMM的开发者维护。讨论成果提高了代码质量。
CUDA VMM 兼容性 多进程竞态 HND 槽计算争议 激活 headroom 逻辑 测试覆盖有限

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论