Skip to content

Commit 4d34b28

Browse files
authored
Set 4MB stack size for all threads, introduce UV_STACK_SIZE (#12839)
See #12769 for the motivation. We set the 4MB not only for the main thread, but also for all tokio and rayon threads to fix a stack overflow while unpacking wheels in production on Windows. There are two variables for setting the stack size: A new `UV_STACK_SIZE` that takes precedent, and the existing `RUST_MIN_STACK`. When setting the stack size, `UV_STACK_SIZE` should be preferred, since `RUST_MIN_STACK` affects all Rust applications, including build backends we call (e.g., maturin). The minimum stack size is set to 1MB, the lowest stack size we observed on a platform (Windows main thread). Fixes #12769 ## Test Plan Tested manually with the example from #12769
1 parent df35919 commit 4d34b28

File tree

6 files changed

+113
-51
lines changed

6 files changed

+113
-51
lines changed

crates/uv-configuration/src/lib.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@ pub use overrides::*;
1515
pub use package_options::*;
1616
pub use preview::*;
1717
pub use project_build_backend::*;
18-
pub use rayon::*;
1918
pub use required_version::*;
2019
pub use sources::*;
2120
pub use target_triple::*;
21+
pub use threading::*;
2222
pub use trusted_host::*;
2323
pub use trusted_publishing::*;
2424
pub use vcs::*;
@@ -40,10 +40,10 @@ mod overrides;
4040
mod package_options;
4141
mod preview;
4242
mod project_build_backend;
43-
mod rayon;
4443
mod required_version;
4544
mod sources;
4645
mod target_triple;
46+
mod threading;
4747
mod trusted_host;
4848
mod trusted_publishing;
4949
mod vcs;

crates/uv-configuration/src/rayon.rs

Lines changed: 0 additions & 21 deletions
This file was deleted.
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
//! Configure rayon and determine thread stack sizes.
2+
3+
use std::sync::atomic::{AtomicUsize, Ordering};
4+
use std::sync::LazyLock;
5+
use uv_static::EnvVars;
6+
7+
/// The default minimum stack size for uv threads.
8+
pub const UV_DEFAULT_STACK_SIZE: usize = 4 * 1024 * 1024;
9+
/// We don't allow setting a smaller stack size than 1MB.
10+
#[allow(clippy::identity_op)]
11+
pub const UV_MIN_STACK_SIZE: usize = 1 * 1024 * 1024;
12+
13+
/// Running out of stack has been an issue for us. We box types and futures in various places
14+
/// to mitigate this.
15+
///
16+
/// Main thread stack-size has a BIG variety here across platforms and it's harder to control
17+
/// (which is why Rust doesn't by default). Notably on macOS and Linux you will typically get 8MB
18+
/// main thread, while on Windows you will typically get 1MB, which is *tiny*:
19+
/// <https://learn.microsoft.com/en-us/cpp/build/reference/stack-stack-allocations?view=msvc-170>
20+
///
21+
/// To normalize this we just spawn a new thread called main2 with a size we can set
22+
/// ourselves. 2MB is typically too small (especially for our debug builds), while 4MB
23+
/// seems fine. This value can be changed with `UV_STACK_SIZE`, with a fallback to reading
24+
/// `RUST_MIN_STACK`, to allow checking a larger or smaller stack size. There is a hardcoded stack
25+
/// size minimum of 1MB, which is the lowest platform default we observed.
26+
///
27+
/// Non-main threads should all have 2MB, as Rust forces platform consistency there,
28+
/// but even then stack overflows can occur in release mode
29+
/// (<https://github.com/astral-sh/uv/issues/12769>), so rayon and tokio get the same stack size,
30+
/// with the 4MB default.
31+
pub fn min_stack_size() -> usize {
32+
let stack_size = if let Some(uv_stack_size) = std::env::var(EnvVars::UV_STACK_SIZE)
33+
.ok()
34+
.and_then(|var| var.parse::<usize>().ok())
35+
{
36+
uv_stack_size
37+
} else if let Some(uv_stack_size) = std::env::var(EnvVars::RUST_MIN_STACK)
38+
.ok()
39+
.and_then(|var| var.parse::<usize>().ok())
40+
{
41+
uv_stack_size
42+
} else {
43+
UV_DEFAULT_STACK_SIZE
44+
};
45+
46+
if stack_size < UV_MIN_STACK_SIZE {
47+
return UV_DEFAULT_STACK_SIZE;
48+
}
49+
50+
stack_size
51+
}
52+
53+
/// The number of threads for the rayon threadpool.
54+
///
55+
/// The default of 0 makes rayon use its default.
56+
pub static RAYON_PARALLELISM: AtomicUsize = AtomicUsize::new(0);
57+
58+
/// Initialize the threadpool lazily. Always call before using rayon the potentially first time.
59+
///
60+
/// The `uv` crate sets [`RAYON_PARALLELISM`] from the user settings, and the extract and install
61+
/// code initialize the threadpool lazily only if they are actually used by calling
62+
/// `LazyLock::force(&RAYON_INITIALIZE)`.
63+
pub static RAYON_INITIALIZE: LazyLock<()> = LazyLock::new(|| {
64+
rayon::ThreadPoolBuilder::new()
65+
.num_threads(RAYON_PARALLELISM.load(Ordering::SeqCst))
66+
.stack_size(min_stack_size())
67+
.build_global()
68+
.expect("failed to initialize global rayon pool");
69+
});

crates/uv-static/src/env_vars.rs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -598,11 +598,27 @@ impl EnvVars {
598598

599599
/// Use to set the stack size used by uv.
600600
///
601-
/// The value is in bytes, and the default is typically 2MB (2097152).
601+
/// The value is in bytes, and if both `UV_STACK_SIZE` are `RUST_MIN_STACK` unset, uv uses a 4MB
602+
/// (4194304) stack. `UV_STACK_SIZE` takes precedence over `RUST_MIN_STACK`.
603+
///
604+
/// Unlike the normal `RUST_MIN_STACK` semantics, this can affect main thread
605+
/// stack size, because we actually spawn our own main2 thread to work around
606+
/// the fact that Windows' real main thread is only 1MB. That thread has size
607+
/// `max(UV_STACK_SIZE, 1MB)`.
608+
pub const UV_STACK_SIZE: &'static str = "UV_STACK_SIZE";
609+
610+
/// Use to set the stack size used by uv.
611+
///
612+
/// The value is in bytes, and if both `UV_STACK_SIZE` are `RUST_MIN_STACK` unset, uv uses a 4MB
613+
/// (4194304) stack. `UV_STACK_SIZE` takes precedence over `RUST_MIN_STACK`.
614+
///
615+
/// Prefer setting `UV_STACK_SIZE`, since `RUST_MIN_STACK` also affects subprocesses, such as
616+
/// build backends that use Rust code.
617+
///
602618
/// Unlike the normal `RUST_MIN_STACK` semantics, this can affect main thread
603619
/// stack size, because we actually spawn our own main2 thread to work around
604620
/// the fact that Windows' real main thread is only 1MB. That thread has size
605-
/// `max(RUST_MIN_STACK, 4MB)`.
621+
/// `max(RUST_MIN_STACK, 1MB)`.
606622
pub const RUST_MIN_STACK: &'static str = "RUST_MIN_STACK";
607623

608624
/// The directory containing the `Cargo.toml` manifest for a package.

crates/uv/src/lib.rs

Lines changed: 5 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ use uv_cli::{
2626
use uv_cli::{PythonCommand, PythonNamespace, ToolCommand, ToolNamespace, TopLevelArgs};
2727
#[cfg(feature = "self-update")]
2828
use uv_cli::{SelfCommand, SelfNamespace, SelfUpdateArgs};
29+
use uv_configuration::min_stack_size;
2930
use uv_fs::{Simplified, CWD};
3031
use uv_pep508::VersionOrUrl;
3132
use uv_pypi_types::{ParsedDirectoryUrl, ParsedUrl};
@@ -2036,32 +2037,12 @@ where
20362037
}
20372038
};
20382039

2039-
// Running out of stack has been an issue for us. We box types and futures in various places
2040-
// to mitigate this, with this being an especially important case.
2041-
//
2042-
// Non-main threads should all have 2MB, as Rust forces platform consistency there,
2043-
// but that can be overridden with the RUST_MIN_STACK environment variable if you need more.
2044-
//
2045-
// Main thread stack-size is the real issue. There's BIG variety here across platforms
2046-
// and it's harder to control (which is why Rust doesn't by default). Notably
2047-
// on macOS and Linux you will typically get 8MB main thread, while on Windows you will
2048-
// typically get 1MB, which is *tiny*:
2049-
// https://learn.microsoft.com/en-us/cpp/build/reference/stack-stack-allocations?view=msvc-170
2050-
//
2051-
// To normalize this we just spawn a new thread called main2 with a size we can set
2052-
// ourselves. 2MB is typically too small (especially for our debug builds), while 4MB
2053-
// seems fine. Also we still try to respect RUST_MIN_STACK if it's set, in case useful,
2054-
// but don't let it ask for a smaller stack to avoid messy misconfiguration since we
2055-
// know we use quite a bit of main stack space.
2056-
let main_stack_size = std::env::var(EnvVars::RUST_MIN_STACK)
2057-
.ok()
2058-
.and_then(|var| var.parse::<usize>().ok())
2059-
.unwrap_or(0)
2060-
.max(4 * 1024 * 1024);
2061-
2040+
// See `min_stack_size` doc comment about `main2`
2041+
let min_stack_size = min_stack_size();
20622042
let main2 = move || {
20632043
let runtime = tokio::runtime::Builder::new_current_thread()
20642044
.enable_all()
2045+
.thread_stack_size(min_stack_size)
20652046
.build()
20662047
.expect("Failed building the Runtime");
20672048
// Box the large main future to avoid stack overflows.
@@ -2076,7 +2057,7 @@ where
20762057
};
20772058
let result = std::thread::Builder::new()
20782059
.name("main2".to_owned())
2079-
.stack_size(main_stack_size)
2060+
.stack_size(min_stack_size)
20802061
.spawn(main2)
20812062
.expect("Tokio executor failed, was there a panic?")
20822063
.join()

docs/configuration/environment.md

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,18 @@ uv will require that all dependencies have a hash specified in the requirements
386386
Equivalent to the `--resolution` command-line argument. For example, if set to
387387
`lowest-direct`, uv will install the lowest compatible versions of all direct dependencies.
388388

389+
### `UV_STACK_SIZE`
390+
391+
Use to set the stack size used by uv.
392+
393+
The value is in bytes, and if both `UV_STACK_SIZE` are `RUST_MIN_STACK` unset, uv uses a 4MB
394+
(4194304) stack. `UV_STACK_SIZE` takes precedence over `RUST_MIN_STACK`.
395+
396+
Unlike the normal `RUST_MIN_STACK` semantics, this can affect main thread
397+
stack size, because we actually spawn our own main2 thread to work around
398+
the fact that Windows' real main thread is only 1MB. That thread has size
399+
`max(UV_STACK_SIZE, 1MB)`.
400+
389401
### `UV_SYSTEM_PYTHON`
390402

391403
Equivalent to the `--system` command-line argument. If set to `true`, uv will
@@ -574,11 +586,16 @@ for more.
574586

575587
Use to set the stack size used by uv.
576588

577-
The value is in bytes, and the default is typically 2MB (2097152).
589+
The value is in bytes, and if both `UV_STACK_SIZE` are `RUST_MIN_STACK` unset, uv uses a 4MB
590+
(4194304) stack. `UV_STACK_SIZE` takes precedence over `RUST_MIN_STACK`.
591+
592+
Prefer setting `UV_STACK_SIZE`, since `RUST_MIN_STACK` also affects subprocesses, such as
593+
build backends that use Rust code.
594+
578595
Unlike the normal `RUST_MIN_STACK` semantics, this can affect main thread
579596
stack size, because we actually spawn our own main2 thread to work around
580597
the fact that Windows' real main thread is only 1MB. That thread has size
581-
`max(RUST_MIN_STACK, 4MB)`.
598+
`max(RUST_MIN_STACK, 1MB)`.
582599

583600
### `SHELL`
584601

0 commit comments

Comments
 (0)