执行摘要
- 一句话:修复CPU容器内KV缓存大小计算错误
- 推荐动作:建议精读。本PR展示了处理Linux容器内资源感知的经典模式,代码简洁且边界处理完整(v1/v2回退、哨兵值、文件异常)。可作为在vLLM中处理cgroup限制的参考实现。
功能与动机
用户在容器(Docker/Kubernetes Pod)中运行vLLM时遇到 ValueError: Available memory on node 0 (149.11/692.65 GiB) ... is less than requested memory for kv (206.54/207.8 GiB) 错误。根本原因是 gpu_memory_utilization 基于宿主机NUMA节点总内存(692.65 GiB)计算,但容器仅被分配约150 GiB内存,导致KV缓存分配超出限制。
实现拆解
-
新增 _read_int_file 工具函数:安全读取cgroup文件内容,处理空值、'max'及文件异常,返回 int | None。
-
新增 get_cgroup_memory_limit 函数(带 @cache 装饰器):先尝试cgroup v2路径(memory.max + memory.current),若不存在则回退到cgroup v1(memory.limit_in_bytes + memory.usage_in_bytes),并排除v1中大于等于2^62的无穷大哨兵值。非Linux系统直接返回 (None, None)。
-
修改 get_memory_node_info 函数:在计算出NUMA节点的 total_memory 和 available_memory 后,调用 get_cgroup_memory_limit() 获取cgroup限制。若cgroup限制存在且小于NUMA总内存,则将 total_memory 设为cgroup限制值,并基于 cgroup_limit - cgroup_usage 计算cgroup可用内存,取 min(available_memory, cgroup_available) 且最低为0。
-
未新增测试文件:PR仅修改了一个源码文件(vllm/utils/cpu_resource_utils.py),无对应单元测试。变更涉及文件系统读取,测试依赖容器环境,作者在PR描述中提供了手动测试结果。
关键文件:
vllm/utils/cpu_resource_utils.py(模块 资源管理;类别 source;类型 core-logic;符号 _read_int_file, get_cgroup_memory_limit): 唯一的变更文件,新增 cgroup 内存限制读取函数并在 KV 缓存计算中应用钳制逻辑。
关键符号:_read_int_file, get_cgroup_memory_limit
关键源码片段
vllm/utils/cpu_resource_utils.py
唯一的变更文件,新增 cgroup 内存限制读取函数并在 KV 缓存计算中应用钳制逻辑。
def _read_int_file(path: str) -> int | None:
# 安全读取 cgroup 文件,处理 'max'、空值及异常
try:
with open(path) as f:
value = f.read().strip()
if not value or value == "max":
return None # cgroup v2 中用 'max' 表示无限制
return int(value)
except (OSError, ValueError):
return None # 文件不存在或格式错误时不阻塞
@cache
def get_cgroup_memory_limit() -> tuple[int | None, int | None]:
"""Return (limit, usage) in bytes from cgroup, or (None, None).
Supports both cgroup v2 (unified) and v1. Returns (None, None) when
not running under a constrained cgroup (e.g. bare metal, or limit
reported as `max`/an unrealistically large value).
"""
if sys.platform != "linux":
return None, None
# cgroup v2 unified hierarchy
v2_limit = _read_int_file("/sys/fs/cgroup/memory.max")
if v2_limit is not None:
v2_usage = _read_int_file("/sys/fs/cgroup/memory.current")
return v2_limit, v2_usage
# cgroup v1
v1_limit = _read_int_file("/sys/fs/cgroup/memory/memory.limit_in_bytes")
if v1_limit is not None:
# cgroup v1 在无限制时返回接近 PAGE_COUNTER_MAX 的哨兵值
if v1_limit >= (1 << 62):
return None, None
v1_usage = _read_int_file("/sys/fs/cgroup/memory/memory.usage_in_bytes")
return v1_limit, v1_usage
return None, None
def get_memory_node_info(node_id: int = 0) -> MemoryNodeInfo:
# ... 原有逻辑计算 total_memory 和 available_memory ...
# 在返回前根据 cgroup 限制钳制内存值
cgroup_limit, cgroup_usage = get_cgroup_memory_limit()
if cgroup_limit is not None and cgroup_limit < total_memory:
total_memory = cgroup_limit # 总内存不能超过 cgroup 限制
cgroup_available = cgroup_limit - (cgroup_usage or 0)
available_memory = max(0, min(available_memory, cgroup_available))
return MemoryNodeInfo(
total_memory=total_memory,
available_memory=available_memory,
)
评论区精华
审核者 bigPYJ1151 最初建议使用 cgroup/memory.numa_stat 获取每NUMA节点的内存状态,但 maobaolong 尝试后发现cgroup不提供每NUMA节点的限制和空闲大小,只能进行近似分配。最终 bigPYJ1151 建议忽略NUMA细节,仅当cgroup有内存限制时直接钳制总内存大小,maobaolong 采纳并移除了 numa_stat 解析。
- 是否使用 cgroup/memory.numa_stat (design): 弃用 numa_stat 方案,仅对总内存进行全局钳制。
风险与影响
关联脉络
参与讨论