Files
ray-project--ray/ci/build/container_resource_utils.py
2026-07-13 13:17:40 +08:00

100 lines
3.1 KiB
Python

#! /usr/bin/env python3
"""
Generates Bazel resource flags by cross-referencing cgroup limits with a
RAM-per-job ratio to prevent OOM kills in containerized environments.
"""
import argparse
import math
import os
from pathlib import Path
DEFAULT_RESERVE_MB = 2048
DEFAULT_MB_PER_JOB = 3072
def get_system_ram_mb() -> int:
# Fallback: os.sysconf reports host RAM, ignoring container quotas.
try:
pages = os.sysconf("SC_PHYS_PAGES")
page_size = os.sysconf("SC_PAGE_SIZE")
return (pages * page_size) // (1024**2)
except (ValueError, AttributeError):
return 8192
def get_container_mem_limit_mb() -> int:
# Cgroup v2 is preferred because it's more accurate and portable.
paths = ["/sys/fs/cgroup/memory.max", "/sys/fs/cgroup/memory/memory.limit_in_bytes"]
for path in paths:
p = Path(path)
if p.exists():
val = p.read_text().strip()
if val and val != "max":
try:
limit_bytes = int(val)
if limit_bytes < 1024**5: # Filter unlimited host values
return limit_bytes // (1024**2)
except ValueError:
pass
return get_system_ram_mb()
def get_container_cpu_limit() -> int:
v2_cpu = Path("/sys/fs/cgroup/cpu.max")
if v2_cpu.exists():
parts = v2_cpu.read_text().split()
if len(parts) == 2 and parts[0] != "max":
try:
return max(1, math.ceil(int(parts[0]) / int(parts[1])))
except ValueError:
pass
quota_p = Path("/sys/fs/cgroup/cpu/cpu.cfs_quota_us")
period_p = Path("/sys/fs/cgroup/cpu/cpu.cfs_period_us")
if quota_p.exists() and period_p.exists():
try:
quota, period = int(quota_p.read_text()), int(period_p.read_text())
if quota > 0:
return max(1, math.ceil(quota / period))
except ValueError:
pass
return os.cpu_count() or 1
def main():
parser = argparse.ArgumentParser(description="Generate Bazel resource flags.")
parser.add_argument(
"--reserve-mb",
type=int,
default=os.getenv("RESERVE_MB"),
help=f"RAM to reserve for the OS/Container overhead. Defaults to {DEFAULT_RESERVE_MB}",
)
parser.add_argument(
"--mb-per-job",
type=int,
default=os.getenv("BAZEL_MB_PER_JOB"),
help=f"Estimated RAM usage per concurrent Bazel job. Defaults to {DEFAULT_MB_PER_JOB}",
)
args = parser.parse_args()
# Convert env var strings to int, or use defaults if not set
args.reserve_mb = int(args.reserve_mb) if args.reserve_mb else DEFAULT_RESERVE_MB
args.mb_per_job = int(args.mb_per_job) if args.mb_per_job else DEFAULT_MB_PER_JOB
mem_limit = get_container_mem_limit_mb()
cpu_limit = get_container_cpu_limit()
usable_mem = max(mem_limit - args.reserve_mb, args.mb_per_job)
jobs_by_ram = usable_mem // args.mb_per_job
bazel_jobs = max(1, min(cpu_limit, jobs_by_ram))
print(
f"--jobs={bazel_jobs} --local_resources=cpu={cpu_limit} --local_resources=memory={mem_limit}"
)
if __name__ == "__main__":
main()