|
26 | 26 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
27 | 27 |
|
28 | 28 | import argparse |
| 29 | +import multiprocessing |
29 | 30 | import os |
30 | 31 | import platform |
31 | 32 | import re |
|
88 | 89 | } |
89 | 90 |
|
90 | 91 |
|
| 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 | + |
91 | 151 | def parse_cuda_arch_list(raw): |
92 | 152 | """ |
93 | 153 | Parse CUDA_ARCH_LIST string to generate CUDA architecture codes: |
@@ -391,16 +451,32 @@ def dockerfile_for_linux(output_file): |
391 | 451 |
|
392 | 452 | df += """ |
393 | 453 | 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 \ |
395 | 455 | --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" |
396 | 456 | """.format( |
397 | 457 | cuda_archs |
398 | 458 | ) |
399 | 459 |
|
| 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 | + ) |
400 | 476 | df += """ |
401 | | -RUN ./build.sh ${{COMMON_BUILD_ARGS}} --update --build {} |
| 477 | +RUN ./build.sh ${{COMMON_BUILD_ARGS}} --parallel {} --nvcc_threads {} --update --build {} |
402 | 478 | """.format( |
403 | | - ep_flags |
| 479 | + ort_jobs, nvcc_threads, ep_flags |
404 | 480 | ) |
405 | 481 |
|
406 | 482 | df += """ |
|
0 commit comments