Skip to content

Size default thread pools from ORT_*_NUM_THREADS environment variables#29688

Open
OscarFree wants to merge 3 commits into
microsoft:mainfrom
OscarFree:env-var-ort-thread-count
Open

Size default thread pools from ORT_*_NUM_THREADS environment variables#29688
OscarFree wants to merge 3 commits into
microsoft:mainfrom
OscarFree:env-var-ort-thread-count

Conversation

@OscarFree

Copy link
Copy Markdown

What

When a thread pool's size is not set programmatically (thread_pool_size <= 0), size it from ORT_INTRA_OP_NUM_THREADS / ORT_INTER_OP_NUM_THREADS before falling back to the machine-sized default. Explicit SessionOptions / global threading options are completely unaffected.

  • Strict parsing — a non-integer or negative value fails loudly (ORT_ENFORCE).
  • = 0 escape hatch — an explicit 0 opts back into the machine-sized default (useful when a platform sets these but you want host-sized pools).
  • Nothing set → current behavior, unchanged.

Implements #29510.

Why

ORT's default intra-op pool sizes to the host's physical cores (GetNumPhysicalCpuCores()). In CPU-limited containers (Kubernetes, Modal, Cloud Run, …) the cgroup CPU reservation is invisible to core detection, so a container reserved N cores on a much larger host gets an N′-sized pool sized to the host — oversubscribing the reservation and degrading tail latency badly under concurrency, with no environment-level way to bound ORT today. The only current fix is passing SessionOptions at every InferenceSession call site, which is unreachable when sessions are created inside third-party libraries.

Scope note (re: @skottmckay's feedback on #29510)

The original proposal also honored OMP_NUM_THREADS as an intra-op fallback. Per the discussion on #29510, this PR is deliberately scoped to ORT-specific variables only — that keeps the semantics under ORT's control and avoids the "why not OPENBLAS_NUM_THREADS / MKL_NUM_THREADS next" ambiguity. An OMP_NUM_THREADS fallback can be a follow-up if there's demand.

Tests

Adds 4 ThreadPoolTest cases (all pin the env vars via ScopedEnvironmentVariables so ambient values can't leak): intra + inter sizing from the ORT vars, explicit size wins over env, the 0 escape hatch, and invalid-value-throws. Comparisons are made against a reference pool of the intended explicit size because DegreeOfParallelism scales by a granularity factor on hybrid CPUs.

I was not able to build/run the ORT test suite locally on this machine, so I'm relying on CI to validate the build + ThreadPoolTest.

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.

@OscarFree

OscarFree commented Jul 13, 2026 via email

Copy link
Copy Markdown
Author

Comment thread onnxruntime/core/util/thread_utils.cc
skottmckay
skottmckay previously approved these changes Jul 15, 2026
@OscarFree

Copy link
Copy Markdown
Author

@skottmckay the ci failure is unrelated, could you re-run please?

@skottmckay

skottmckay commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

It's related. The code change is bumping the minimal Android build binary size past the current max. It's fine to add 10K to that in .github\workflows\android.yml

        echo "Binary size threshold in bytes: 1440768"
        BINARY_SIZE_THRESHOLD_ARGS="--threshold_size_in_bytes 1440768"

You'll also need to sync with the latest main to pickup some workflow config changes. Without that a number of CIs will fail.

When thread_pool_size is not set programmatically (<= 0), consult
ORT_INTRA_OP_NUM_THREADS / ORT_INTER_OP_NUM_THREADS before falling back to
the machine-sized default. Parsing is strict (a negative or non-integer
value fails loudly); an explicit 0 opts back into the machine-sized default.
Explicit session/global threading options are unaffected.

Motivation: ONNX Runtime's default intra-op pool sizes to the host's
physical cores, which oversubscribes CPU-limited containers where the cgroup
CPU reservation is invisible to core detection. These variables let a
container platform bound ORT's pools without reaching every InferenceSession
call site.

Addresses microsoft#29510.
thread_pool_size's contract is 0 = default, 1 = no pool, n = n threads;
negatives are not a valid input. Tightening the guard to == 0 means the
env-var path only affects the default case and never overwrites a non-zero
(or negative) caller-provided value. Anything < 0 falls through to
CreateThreadPoolHelper's existing default handling, unchanged.
The env-var thread-pool sizing added to core (ParseEnvironmentVariable in
thread_utils.cc) grows the minimal Android build ~just past the current
1440768-byte ceiling. Bump the threshold by 10K per maintainer guidance.
@OscarFree

Copy link
Copy Markdown
Author

ah, thanks for the tip!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants