Repository navigation
Cap CPU accelerator memory at the cgroup limit of the process - #8930
prabhkodes wants to merge 4 commits into
Conversation
CPU_Accelerator.total_memory() and available_memory() returned host RAM from psutil. In a container or Kubernetes pod the process can use only its cgroup limit, so ZeRO 1/2 flattening, the inference v2 KV cache, the deepspeed.compile passes and the autotuner planned for memory the pod does not have. Read /proc/self/cgroup, take the smallest limit from the process cgroup up to the root, and read usage from the same cgroup without its reclaimable inactive page cache. Off by default via RESPECT_CGROUP_MEMORY_BY_DEFAULT; DS_CPU_RESPECT_CGROUP_MEMORY=1 or =0 overrides it. Signed-off-by: Prabhsharan Singh <228956661+prabhkodes@users.noreply.github.com>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: dbc0fd95f5
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
…flag Address review feedback on the CPU accelerator cgroup memory support: - available_memory() now takes the smallest free headroom over every limited cgroup from the process cgroup up to the root. A parent limit also counts the usage of its other children, so a child with the smallest limit can still have less room than its own limit suggests. - Resolve the cgroup mount point and mount root from /proc/self/mountinfo, so mounts that expose a subtree and cgroup v1 co-mounts such as cpu,memory are found. - export_envs() returns DS_CPU_RESPECT_CGROUP_MEMORY so the launcher forwards it to every node, and the accelerator setup guide documents the variable. Signed-off-by: Prabhsharan Singh <228956661+prabhkodes@users.noreply.github.com>
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: dc5b6c1834
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
/proc/self/mountinfo can list several matching cgroup mounts, for example a delegated cgroup2 subtree mounted before the main one. Taking the first match could choose a mount that does not contain the process cgroup, and the memory limit then silently fell back to host RAM. Read the membership from /proc/self/cgroup first, then use the mount whose root contains it, the most specific one first. Fall back to the first match, then to /sys/fs/cgroup, as before. Signed-off-by: Prabhsharan Singh <228956661+prabhkodes@users.noreply.github.com>
When the full cgroup hierarchy and a bind-mounted subtree both contain the process, the most specific mount was used. Walking only that subtree hides a tighter limit on a parent, which the kernel still enforces. Use the containing mount with the shortest root instead. Signed-off-by: Prabhsharan Singh <228956661+prabhkodes@users.noreply.github.com>
Issue
CPU_Accelerator.total_memory()andavailable_memory()return host RAM frompsutil.virtual_memory(). In a container or a Kubernetes pod, the process can use only its cgroup limit. Callers that plan memory from these values on the CPU accelerator include ZeRO 1/2 flattening (stage_1_and_2.py:451) and the autotuner. Fixes #8929.Related: #1891 asks for cgroup-aware CPU memory limits. #3458 (Docker), #2632 and #2788 (Slurm) report processes killed by a cgroup limit without a message.
Fix
Only the CPU accelerator, its tests and its docs change.
total_memory()returns the smallest memory limit from the process cgroup up to the root, capped at host RAM.available_memory()returns the smallest free headroom over every limited cgroup on that path. A parent limit also counts the usage of its other children, so each limited level is checked. Usage excludes the inactive page cache, which the kernel can reclaim./proc/self/mountinfo. When several mounts match, the one whose root contains the process cgroup is used. This supports mounts that expose a subtree, cgroup v1 co-mounts such ascpu,memory, and delegated subtrees mounted before the main mount.RESPECT_CGROUP_MEMORY_BY_DEFAULT = Falseis the one line that sets the default.DS_CPU_RESPECT_CGROUP_MEMORY=1or=0overrides it.export_envs()returns the variable, so the launcher forwards it to every node.abstract_accelerator.pydoes not change.How to test
Needs Docker with cgroup v2. Save the script from the collapsed section at the end as
check_cgroup_memory.sh. One command installsmaster(before) and this branch (after) in the same container with 1 GiB, and printstotal_memory()andavailable_memory()from three cgroups. The script is in the collapsed section below.Output (Apple M4, Docker Desktop VM with 7.8 GiB):
Before, every case reports the host. After,
totalis the limit of each case. In C,availableis the free headroom of the 800 MiB parent, which the 500 MiB sibling also uses. In A, the container's 1 GiB also holds that sibling.Proof
ZeRO training on the CPU accelerator, three Adam steps, flag
0and1, two fresh containers each:deepspeedlauncher, gloo)Performance
Throughput and peak RSS, flag
0and1, 30 timed steps on a 4×1024 MLP. Four runs per cell, two fresh containers, flag alternated. Median steps/s with range:Peak RSS does not change. The throughput differences go in both directions and stay inside the run-to-run range. A call counter shows why: ZeRO 1 and 2 call the memory queries once at setup, ZeRO 3 does not call them, and no configuration calls them during training steps.
Cost of one query, best of 5 × 5000 calls, process three levels below the cgroup root:
total_memory()available_memory()1With the flag set, a query reads
/proc/self/cgroup,/proc/self/mountinfoand the cgroup files: about 70 µs more, once per run.Eleven new tests. They cover a limit on a parent cgroup, the smallest limit, the v1 sentinel, the headroom of every limited level, mountinfo resolution with several mounts, the launcher export, and default × override.
Hardware. Apple M4 host, Docker Desktop Linux 6.10 VM (aarch64, 10 CPUs, 7.8 GiB, cgroup v2). Python 3.12.16, torch 2.14.1+cpu,
DS_BUILD_OPS=0. No GPU.Compatibility matrix.
deepspeed.compile(is_deepcompile_supported()requires CUDA) and the inference v2 KV cache (CUDA kernels only).see_memory_usage()and the timer read host RAM only to log it.No open PR changes the CPU accelerator memory functions (searched
cgroup,cpu_accelerator available_memory,cpu accelerator memory,1891 in:bodyon 2026-10-27).AI assistance was used: an AI coding agent (Claude Code) wrote the change and the tests; I reviewed each changed line and ran the tests, the reproduction and the ZeRO runs.
check_cgroup_memory.sh
Precedent. The same walk is already in production elsewhere. Rust's
std::thread::available_parallelismtakes the smallestcpu.maxfrom the process cgroup up to the mount (quota_v2). The JDK fixed limits on parent nested cgroups in JDK-8322420 (2024) and the hierarchical memory limit in JDK-8370572 (2025). Go 1.25 finds the process cgroup through/proc/self/cgroupand/proc/self/mountinfo(cgroup_linux.go), reading the leaf only. vLLM's CPU-quota reader walks the hierarchy (_cgroup_cpu_limit); vllm-project/vllm#64104 adds the same for memory. The same fix for OpenObserve merged on 2026-10-28 (openobserve/openobserve#15906).