Skip to content

Commit e28926d

Browse files
authored
fix(build): cap ONNX Runtime build parallelism to avoid runner OOM (#348) (#349)
The generated Dockerfile passed a bare `--parallel` to ONNX Runtime's build.sh, which expands to multiprocessing.cpu_count(). Combined with per-nvcc multi-arch compilation, fully-uncached builds exhausted memory on swap-less builders and the OOM killer terminated runner/system daemons instead of the build. Compute the parallelism in Python at Dockerfile-generation time and bake literal `--parallel <jobs> --nvcc_threads 2` into the build step: - usable_cpu_count() honors CPU affinity/cgroup pinning (Linux), falling back to cpu_count() elsewhere. - available_memory_gb() reads MemAvailable from /proc/meminfo. - build_parallelism() budgets ~2 GB of RAM per concurrent compiler slot and bounds the product `parallel_jobs * nvcc_threads` by that budget. - The assumed budget is capped at MAX_BUILD_MEMORY_GB (64 GB): never plan for more than that, nor for more than is actually available; assume the cap when memory is unknown. Baking literals keeps the build stage free of shell logic, so it is portable across the Debian (apt) and RHEL (dnf) base-image paths. Relates to TRI-1550.
1 parent 71bf0fb commit e28926d

1 file changed

Lines changed: 79 additions & 3 deletions

File tree

tools/gen_ort_dockerfile.py

Lines changed: 79 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
2727

2828
import argparse
29+
import multiprocessing
2930
import os
3031
import platform
3132
import re
@@ -88,6 +89,65 @@
8889
}
8990

9091

92+
def usable_cpu_count():
93+
# Honor CPU affinity / cgroup pinning where available (Linux); fall back to
94+
# the logical core count on platforms without sched_getaffinity.
95+
try:
96+
return len(os.sched_getaffinity(0))
97+
except AttributeError:
98+
return multiprocessing.cpu_count() or 1
99+
100+
101+
def available_memory_gb():
102+
# Best-effort available RAM in GiB, or None if it cannot be determined.
103+
# Reads MemAvailable from /proc/meminfo (Linux); returns None elsewhere so
104+
# callers fall back to a CPU-only parallelism default.
105+
try:
106+
with open("/proc/meminfo") as f:
107+
for line in f:
108+
if line.startswith("MemAvailable:"):
109+
return int(line.split()[1]) / (1024 * 1024)
110+
except (OSError, ValueError, IndexError):
111+
pass
112+
return None
113+
114+
115+
# Memory budget the build is assumed to have for compilation. Parallelism is
116+
# sized so the ONNX Runtime build's peak memory stays within this, leaving the
117+
# rest of the host free and avoiding OOM on swap-less builders (see TRI-1550).
118+
MAX_BUILD_MEMORY_GB = 64.0
119+
120+
# Approximate RAM headroom reserved per concurrent compiler slot.
121+
MEM_GB_PER_SLOT = 2.0
122+
123+
124+
def build_parallelism(nvcc_threads):
125+
# Cap ONNX Runtime build parallelism to avoid OOM on swap-less builders
126+
# (see TRI-1550). Bare `--parallel` expands to cpu_count(), which combined
127+
# with per-nvcc multi-arch compilation exhausts memory. Each concurrent
128+
# compiler slot is budgeted ~MEM_GB_PER_SLOT of RAM, and the product
129+
# `parallel_jobs * nvcc_threads` is bounded by that budget.
130+
#
131+
# The assumed memory budget is capped at MAX_BUILD_MEMORY_GB: we never plan
132+
# for more than that (so a big-RAM host doesn't over-subscribe), nor for
133+
# more than is actually available (so a small builder doesn't OOM). When the
134+
# available memory is unknown, we fall back to the MAX_BUILD_MEMORY_GB
135+
# assumption rather than to an unbounded core count.
136+
#
137+
# Computed at Dockerfile-generation time (same host that runs `docker
138+
# build`), matching server/build.py; the resulting values are baked into
139+
# the Dockerfile as literals so no shell logic runs in the build stage.
140+
cpus = usable_cpu_count()
141+
142+
mem_gb = available_memory_gb()
143+
budget_gb = (
144+
min(mem_gb, MAX_BUILD_MEMORY_GB) if mem_gb is not None else MAX_BUILD_MEMORY_GB
145+
)
146+
147+
mem_slots = max(1, int(budget_gb // MEM_GB_PER_SLOT))
148+
return max(1, min(cpus, mem_slots // max(1, nvcc_threads)))
149+
150+
91151
def parse_cuda_arch_list(raw):
92152
"""
93153
Parse CUDA_ARCH_LIST string to generate CUDA architecture codes:
@@ -391,16 +451,32 @@ def dockerfile_for_linux(output_file):
391451

392452
df += """
393453
WORKDIR /workspace/onnxruntime
394-
ARG COMMON_BUILD_ARGS="--config ${{ONNXRUNTIME_BUILD_CONFIG}} --parallel --skip_submodule_sync --build_shared_lib \
454+
ARG COMMON_BUILD_ARGS="--config ${{ONNXRUNTIME_BUILD_CONFIG}} --skip_submodule_sync --build_shared_lib \
395455
--compile_no_warning_as_error --build_dir /workspace/build --cmake_extra_defines CMAKE_CUDA_ARCHITECTURES='{}' --cmake_extra_defines CMAKE_POLICY_VERSION_MINIMUM=3.5 --build_wheel"
396456
""".format(
397457
cuda_archs
398458
)
399459

460+
# Cap ONNX Runtime build parallelism to avoid OOM on swap-less builders
461+
# (see TRI-1550). Values are computed in Python and baked in as literals so
462+
# the build stage stays free of shell logic (portable across Debian/RHEL).
463+
nvcc_threads = 2
464+
ort_jobs = build_parallelism(nvcc_threads)
465+
print(
466+
"[INFO] ONNX Runtime build parallelism: --parallel {} --nvcc_threads {} "
467+
"(usable cores {}, MemAvailable {})".format(
468+
ort_jobs,
469+
nvcc_threads,
470+
usable_cpu_count(),
471+
"{:.1f} GB".format(available_memory_gb())
472+
if available_memory_gb() is not None
473+
else "unknown",
474+
)
475+
)
400476
df += """
401-
RUN ./build.sh ${{COMMON_BUILD_ARGS}} --update --build {}
477+
RUN ./build.sh ${{COMMON_BUILD_ARGS}} --parallel {} --nvcc_threads {} --update --build {}
402478
""".format(
403-
ep_flags
479+
ort_jobs, nvcc_threads, ep_flags
404480
)
405481

406482
df += """

0 commit comments

Comments
 (0)