From 1488767a248b5357e936b73fa85cb44243c853aa Mon Sep 17 00:00:00 2001 From: Krzysztof Lis Date: Tue, 16 Jun 2026 19:05:27 +0200 Subject: [PATCH 1/7] feat(rpc/compiler): new compiler concurrency limit computation formula --- Cargo.lock | 92 ++++++++++++++++++++ Cargo.toml | 1 + crates/pathfinder/Cargo.toml | 1 + crates/pathfinder/src/bin/pathfinder/main.rs | 61 ++++++++++++- 4 files changed, 151 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 702ca0df3d..4e58cbecb3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5044,6 +5044,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.11.1", + "objc2", +] + [[package]] name = "displaydoc" version = "0.2.6" @@ -8220,6 +8230,15 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21" +[[package]] +name = "ntapi" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae" +dependencies = [ + "winapi", +] + [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -8380,6 +8399,63 @@ dependencies = [ "smallvec", ] +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.11.1", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.11.1", + "objc2", +] + +[[package]] +name = "objc2-io-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33fafba39597d6dc1fb709123dfa8289d39406734be322956a69f0931c73bb15" +dependencies = [ + "libc", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-open-directory" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb82bed227edf5201dfedf072bba4015a33d3d4a98519837295a90f0a23f676d" +dependencies = [ + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + [[package]] name = "oid-registry" version = "0.8.1" @@ -8695,6 +8771,7 @@ dependencies = [ "starknet-gateway-test-fixtures", "starknet-gateway-types", "starknet_api 0.19.0-rc.2 (registry+https://github.com/rust-lang/crates.io-index)", + "sysinfo", "tempfile", "test-log", "thiserror 2.0.18", @@ -11465,6 +11542,21 @@ dependencies = [ "syn 2.0.118", ] +[[package]] +name = "sysinfo" +version = "0.39.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21d0d938c10fcda3e897e28aaddf4ab462375d411f4378cd63b1c945f69aba96" +dependencies = [ + "libc", + "memchr", + "ntapi", + "objc2-core-foundation", + "objc2-io-kit", + "objc2-open-directory", + "windows", +] + [[package]] name = "system-configuration" version = "0.7.0" diff --git a/Cargo.toml b/Cargo.toml index cf8e25400a..8d503fd96e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -137,6 +137,7 @@ starknet-types-core = "=0.2.4" # This one needs to match the version used by blockifier starknet_api = { version = "0.19.0-rc.2" } syn = "2.0" +sysinfo = "0.39.3" tempfile = "3.27" test-log = { version = "0.2.20", features = ["trace"] } thiserror = "2.0.18" diff --git a/crates/pathfinder/Cargo.toml b/crates/pathfinder/Cargo.toml index c4733de49c..49f396db58 100644 --- a/crates/pathfinder/Cargo.toml +++ b/crates/pathfinder/Cargo.toml @@ -70,6 +70,7 @@ sha3 = { workspace = true } starknet-gateway-client = { path = "../gateway-client" } starknet-gateway-types = { path = "../gateway-types" } starknet_api = { workspace = true } +sysinfo = { workspace = true } tempfile = { workspace = true } thiserror = { workspace = true } tikv-jemallocator = { workspace = true } diff --git a/crates/pathfinder/src/bin/pathfinder/main.rs b/crates/pathfinder/src/bin/pathfinder/main.rs index aca13b0bf5..aae6379884 100644 --- a/crates/pathfinder/src/bin/pathfinder/main.rs +++ b/crates/pathfinder/src/bin/pathfinder/main.rs @@ -1,7 +1,7 @@ #![deny(rust_2018_idioms)] use std::net::SocketAddr; -use std::num::NonZeroU32; +use std::num::{NonZeroU32, NonZeroUsize}; use std::path::{Path, PathBuf}; use std::sync::atomic::AtomicBool; use std::sync::Arc; @@ -250,6 +250,9 @@ Hint: This is usually caused by exceeding the file descriptor limit of your syst PendingDataCache::new().with_inactivity_timeout(config.pre_confirmed_idle_timeout), ); + let compiler_concurrency_limit = + determine_compiler_concurrency_limit(&config, available_parallelism)?; + let rpc_config = pathfinder_rpc::context::RpcConfig { request_max_size: config.rpc_request_max_size, request_timeout: config.rpc_request_timeout, @@ -268,9 +271,7 @@ Hint: This is usually caused by exceeding the file descriptor limit of your syst submission_tracker_time_limit: config.submission_tracker_time_limit, submission_tracker_size_limit: config.submission_tracker_size_limit, block_trace_cache_size: config.rpc_block_trace_cache_size, - compiler_concurrency_limit: config - .compiler_concurrency_limit - .unwrap_or(available_parallelism), + compiler_concurrency_limit, compiler_resource_limits: config.compiler_resource_limits, blockifier_libfuncs: config.blockifier_libfuncs, }; @@ -612,6 +613,58 @@ fn compile_main(config: CompileConfig) -> anyhow::Result<()> { .context("writing CASM to stdout") } +fn determine_compiler_concurrency_limit( + config: &config::Config, + available_parallelism: std::num::NonZero, +) -> Result, anyhow::Error> { + if let Some(limit) = config.compiler_concurrency_limit { + return Ok(limit); + } + + const PATHFINDER_RAM_SAFE_OPERATION_MARGIN_BYTES: u64 = 4 * 1024 * 1024 * 1024; + + let mut system = sysinfo::System::new_with_specifics( + sysinfo::RefreshKind::nothing() + .with_memory(sysinfo::MemoryRefreshKind::nothing().with_ram()), + ); + system.refresh_memory_specifics(sysinfo::MemoryRefreshKind::nothing().with_ram()); + let total_memory_bytes = system.total_memory(); + let compiler_memory_limit_bytes = config.compiler_resource_limits.memory_usage; + + // Concurrency_Limit = min(floor((RAM - 4GiB) / RAM_Limit), n_CPUs) + let concurrency_limit = total_memory_bytes + .saturating_sub(PATHFINDER_RAM_SAFE_OPERATION_MARGIN_BYTES) + / compiler_memory_limit_bytes; + let num_cpus = u64::try_from(available_parallelism.get())?; + let concurrency_limit = usize::try_from(num_cpus.min(concurrency_limit))?; + + let total_memory_mib = total_memory_bytes / (1024 * 1024); + let compiler_memory_limit_mib = compiler_memory_limit_bytes / (1024 * 1024); + + if concurrency_limit == 0 { + anyhow::ensure!( + !config.is_rpc_enabled, + "Computed compiler concurrency limit is 0, which is insufficient for a running RPC \ + server. Please decrease the compiler memory usage limit ({compiler_memory_limit_mib} \ + MiB), increase system memory ({total_memory_mib} MiB), set a fixed compiler \ + concurrency limit via --rpc.compiler.concurrency-limit, or disable the RPC server \ + via --rpc.enable=false." + ); + + // Use a dummy value, the RPC server will not be started anyway, yet we need to + // construct a valid RPC config beforehand + Ok(NonZeroUsize::new(1).expect("1>0")) + } else { + tracing::info!( + "Computed compiler concurrency limit: {concurrency_limit}, based on total memory \ + ({total_memory_mib} MiB), compiler memory limit ({compiler_memory_limit_mib} MiB), \ + and number of CPUs ({num_cpus})" + ); + + Ok(NonZeroUsize::new(concurrency_limit).expect("Nonzero value")) + } +} + #[cfg(feature = "tokio-console")] fn setup_tracing(color: config::Color, pretty_log: bool, json_log: bool) { use tracing_subscriber::prelude::*; From cbefea915bbf844faee7e877913cf098d5406463 Mon Sep 17 00:00:00 2001 From: Krzysztof Lis Date: Tue, 16 Jun 2026 21:18:50 +0200 Subject: [PATCH 2/7] feat(config): increase margin in formula --- crates/pathfinder/src/bin/pathfinder/main.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/pathfinder/src/bin/pathfinder/main.rs b/crates/pathfinder/src/bin/pathfinder/main.rs index aae6379884..eb456faae9 100644 --- a/crates/pathfinder/src/bin/pathfinder/main.rs +++ b/crates/pathfinder/src/bin/pathfinder/main.rs @@ -621,7 +621,7 @@ fn determine_compiler_concurrency_limit( return Ok(limit); } - const PATHFINDER_RAM_SAFE_OPERATION_MARGIN_BYTES: u64 = 4 * 1024 * 1024 * 1024; + const PATHFINDER_RAM_SAFE_OPERATION_MARGIN_BYTES: u64 = 8 * 1024 * 1024 * 1024; let mut system = sysinfo::System::new_with_specifics( sysinfo::RefreshKind::nothing() @@ -631,7 +631,7 @@ fn determine_compiler_concurrency_limit( let total_memory_bytes = system.total_memory(); let compiler_memory_limit_bytes = config.compiler_resource_limits.memory_usage; - // Concurrency_Limit = min(floor((RAM - 4GiB) / RAM_Limit), n_CPUs) + // Concurrency_Limit = min(floor((RAM - Margin) / RAM_Limit), n_CPUs) let concurrency_limit = total_memory_bytes .saturating_sub(PATHFINDER_RAM_SAFE_OPERATION_MARGIN_BYTES) / compiler_memory_limit_bytes; From 3bd02603988781da8f37874e946b6f1728c2ca52 Mon Sep 17 00:00:00 2001 From: Krzysztof Lis Date: Wed, 17 Jun 2026 11:11:13 +0200 Subject: [PATCH 3/7] test: add tests for computation edgecases --- crates/pathfinder/src/bin/pathfinder/main.rs | 92 +++++++++++++++++++- 1 file changed, 89 insertions(+), 3 deletions(-) diff --git a/crates/pathfinder/src/bin/pathfinder/main.rs b/crates/pathfinder/src/bin/pathfinder/main.rs index eb456faae9..3de4f0ff90 100644 --- a/crates/pathfinder/src/bin/pathfinder/main.rs +++ b/crates/pathfinder/src/bin/pathfinder/main.rs @@ -621,8 +621,6 @@ fn determine_compiler_concurrency_limit( return Ok(limit); } - const PATHFINDER_RAM_SAFE_OPERATION_MARGIN_BYTES: u64 = 8 * 1024 * 1024 * 1024; - let mut system = sysinfo::System::new_with_specifics( sysinfo::RefreshKind::nothing() .with_memory(sysinfo::MemoryRefreshKind::nothing().with_ram()), @@ -631,6 +629,31 @@ fn determine_compiler_concurrency_limit( let total_memory_bytes = system.total_memory(); let compiler_memory_limit_bytes = config.compiler_resource_limits.memory_usage; + compute_compiler_concurrency_limit( + total_memory_bytes, + compiler_memory_limit_bytes, + available_parallelism, + config.is_rpc_enabled, + ) +} + +/// Computes the compiler concurrency limit from the available memory and CPUs. +/// +/// `Concurrency_Limit = min(floor((RAM - Margin) / RAM_Limit), n_CPUs)` +/// +/// Returns an error only when the computed limit is 0 and the RPC server is +/// enabled, since a running RPC server requires at least one compiler. When the +/// limit is 0 but RPC is disabled, a dummy value of 1 is returned, because a +/// valid RPC config still needs to be constructed even though the RPC server +/// will not be started. +fn compute_compiler_concurrency_limit( + total_memory_bytes: u64, + compiler_memory_limit_bytes: u64, + available_parallelism: std::num::NonZero, + is_rpc_enabled: bool, +) -> Result, anyhow::Error> { + const PATHFINDER_RAM_SAFE_OPERATION_MARGIN_BYTES: u64 = 8 * 1024 * 1024 * 1024; + // Concurrency_Limit = min(floor((RAM - Margin) / RAM_Limit), n_CPUs) let concurrency_limit = total_memory_bytes .saturating_sub(PATHFINDER_RAM_SAFE_OPERATION_MARGIN_BYTES) @@ -643,7 +666,7 @@ fn determine_compiler_concurrency_limit( if concurrency_limit == 0 { anyhow::ensure!( - !config.is_rpc_enabled, + !is_rpc_enabled, "Computed compiler concurrency limit is 0, which is insufficient for a running RPC \ server. Please decrease the compiler memory usage limit ({compiler_memory_limit_mib} \ MiB), increase system memory ({total_memory_mib} MiB), set a fixed compiler \ @@ -1260,3 +1283,66 @@ fn handle_critical_task_result( } } } + +#[cfg(test)] +mod tests { + use std::num::NonZeroUsize; + + use super::compute_compiler_concurrency_limit; + + const GIB: u64 = 1024 * 1024 * 1024; + + fn nonzero(n: usize) -> NonZeroUsize { + NonZeroUsize::new(n).unwrap() + } + + #[test] + fn limited_by_memory() { + // (31 - 8) / 4 = 5, less than the 32 CPUs. + let limit = + compute_compiler_concurrency_limit(31 * GIB, 4 * GIB, nonzero(32), true).unwrap(); + assert_eq!(limit.get(), 5); + } + + #[test] + fn limited_by_cpus() { + // (256 - 8) / 4 = 62, but only 8 CPUs are available. + let limit = + compute_compiler_concurrency_limit(256 * GIB, 4 * GIB, nonzero(8), true).unwrap(); + assert_eq!(limit.get(), 8); + } + + #[test] + fn zero_limit_with_rpc_enabled_returns_error() { + // (10 - 8) / 4 = 0, and RPC is enabled, so this is an error. + compute_compiler_concurrency_limit(10 * GIB, 4 * GIB, nonzero(32), true).unwrap_err(); + } + + #[test] + fn zero_limit_with_rpc_disabled_returns_dummy() { + // (10 - 8) / 4 = 0, but RPC is disabled, so a dummy value of 1 is returned. + let limit = + compute_compiler_concurrency_limit(10 * GIB, 4 * GIB, nonzero(32), false).unwrap(); + assert_eq!(limit.get(), 1); + } + + #[test] + fn memory_below_margin_with_rpc_enabled_returns_error() { + // Total memory below the margin saturates to 0, yielding a 0 limit. + compute_compiler_concurrency_limit(4 * GIB, 4 * GIB, nonzero(32), true).unwrap_err(); + } + + #[test] + fn memory_below_margin_with_rpc_disabled_returns_dummy() { + let limit = + compute_compiler_concurrency_limit(4 * GIB, 4 * GIB, nonzero(32), false).unwrap(); + assert_eq!(limit.get(), 1); + } + + #[test] + fn single_cpu_caps_limit() { + let limit = + compute_compiler_concurrency_limit(256 * GIB, 4 * GIB, nonzero(1), true).unwrap(); + assert_eq!(limit.get(), 1); + } +} From d34ad7f5fca6feb20d8484b3ad032585d280e141 Mon Sep 17 00:00:00 2001 From: Krzysztof Lis Date: Wed, 17 Jun 2026 12:38:46 +0200 Subject: [PATCH 4/7] feat(config): make the margin configurable --- crates/pathfinder/src/bin/pathfinder/main.rs | 73 ++++++++++++-------- crates/pathfinder/src/config.rs | 36 +++++++++- 2 files changed, 76 insertions(+), 33 deletions(-) diff --git a/crates/pathfinder/src/bin/pathfinder/main.rs b/crates/pathfinder/src/bin/pathfinder/main.rs index 3de4f0ff90..ecf8da7dd2 100644 --- a/crates/pathfinder/src/bin/pathfinder/main.rs +++ b/crates/pathfinder/src/bin/pathfinder/main.rs @@ -181,7 +181,7 @@ Hint: This is usually caused by exceeding the file descriptor limit of your syst )?; let execution_storage_pool_size = config.execution_concurrency.unwrap_or_else(|| { - std::num::NonZeroU32::new(available_parallelism.get() as u32) + NonZeroU32::new(available_parallelism.get() as u32) .expect("The number of CPU cores should be non-zero") }); let execution_storage = storage_manager @@ -615,8 +615,8 @@ fn compile_main(config: CompileConfig) -> anyhow::Result<()> { fn determine_compiler_concurrency_limit( config: &config::Config, - available_parallelism: std::num::NonZero, -) -> Result, anyhow::Error> { + available_parallelism: NonZeroUsize, +) -> anyhow::Result { if let Some(limit) = config.compiler_concurrency_limit { return Ok(limit); } @@ -627,11 +627,11 @@ fn determine_compiler_concurrency_limit( ); system.refresh_memory_specifics(sysinfo::MemoryRefreshKind::nothing().with_ram()); let total_memory_bytes = system.total_memory(); - let compiler_memory_limit_bytes = config.compiler_resource_limits.memory_usage; compute_compiler_concurrency_limit( total_memory_bytes, - compiler_memory_limit_bytes, + config.compiler_resource_limits.memory_usage, + config.compiler_concurrency_memory_margin, available_parallelism, config.is_rpc_enabled, ) @@ -639,7 +639,8 @@ fn determine_compiler_concurrency_limit( /// Computes the compiler concurrency limit from the available memory and CPUs. /// -/// `Concurrency_Limit = min(floor((RAM - Margin) / RAM_Limit), n_CPUs)` +/// `Concurrency_Limit = min(floor((System_RAM - Margin) / Compiler_RAM_Limit), +/// n_CPUs)` /// /// Returns an error only when the computed limit is 0 and the RPC server is /// enabled, since a running RPC server requires at least one compiler. When the @@ -649,29 +650,31 @@ fn determine_compiler_concurrency_limit( fn compute_compiler_concurrency_limit( total_memory_bytes: u64, compiler_memory_limit_bytes: u64, - available_parallelism: std::num::NonZero, + compiler_concurrency_memory_margin_bytes: u64, + available_parallelism: NonZeroUsize, is_rpc_enabled: bool, -) -> Result, anyhow::Error> { - const PATHFINDER_RAM_SAFE_OPERATION_MARGIN_BYTES: u64 = 8 * 1024 * 1024 * 1024; - - // Concurrency_Limit = min(floor((RAM - Margin) / RAM_Limit), n_CPUs) +) -> anyhow::Result { + // Concurrency_Limit = min(floor((System_RAM - Margin) / Compiler_RAM_Limit), + // n_CPUs) let concurrency_limit = total_memory_bytes - .saturating_sub(PATHFINDER_RAM_SAFE_OPERATION_MARGIN_BYTES) + .saturating_sub(compiler_concurrency_memory_margin_bytes) / compiler_memory_limit_bytes; let num_cpus = u64::try_from(available_parallelism.get())?; let concurrency_limit = usize::try_from(num_cpus.min(concurrency_limit))?; - let total_memory_mib = total_memory_bytes / (1024 * 1024); - let compiler_memory_limit_mib = compiler_memory_limit_bytes / (1024 * 1024); + const BYTES_IN_MIB: u64 = 1024 * 1024; + let total_memory_mib = total_memory_bytes / BYTES_IN_MIB; + let compiler_memory_limit_mib = compiler_memory_limit_bytes / BYTES_IN_MIB; + let margin_mib = compiler_concurrency_memory_margin_bytes / BYTES_IN_MIB; if concurrency_limit == 0 { anyhow::ensure!( !is_rpc_enabled, "Computed compiler concurrency limit is 0, which is insufficient for a running RPC \ server. Please decrease the compiler memory usage limit ({compiler_memory_limit_mib} \ - MiB), increase system memory ({total_memory_mib} MiB), set a fixed compiler \ - concurrency limit via --rpc.compiler.concurrency-limit, or disable the RPC server \ - via --rpc.enable=false." + MiB), increase system memory ({total_memory_mib} MiB), decrease normal operation \ + margin ({margin_mib} MiB), set a fixed compiler concurrency limit via \ + --rpc.compiler.concurrency-limit, or disable the RPC server via --rpc.enable=false." ); // Use a dummy value, the RPC server will not be started anyway, yet we need to @@ -681,7 +684,7 @@ fn compute_compiler_concurrency_limit( tracing::info!( "Computed compiler concurrency limit: {concurrency_limit}, based on total memory \ ({total_memory_mib} MiB), compiler memory limit ({compiler_memory_limit_mib} MiB), \ - and number of CPUs ({num_cpus})" + normal operation margin ({margin_mib} MiB), and number of CPUs ({num_cpus})" ); Ok(NonZeroUsize::new(concurrency_limit).expect("Nonzero value")) @@ -1292,57 +1295,67 @@ mod tests { const GIB: u64 = 1024 * 1024 * 1024; + /// The default compiler concurrency memory margin in the config. + const MARGIN: u64 = 4 * GIB; + fn nonzero(n: usize) -> NonZeroUsize { NonZeroUsize::new(n).unwrap() } #[test] fn limited_by_memory() { - // (31 - 8) / 4 = 5, less than the 32 CPUs. + // (31 - 4) / 4 = 6, less than the 32 CPUs. let limit = - compute_compiler_concurrency_limit(31 * GIB, 4 * GIB, nonzero(32), true).unwrap(); - assert_eq!(limit.get(), 5); + compute_compiler_concurrency_limit(31 * GIB, 4 * GIB, MARGIN, nonzero(32), true) + .unwrap(); + assert_eq!(limit.get(), 6); } #[test] fn limited_by_cpus() { - // (256 - 8) / 4 = 62, but only 8 CPUs are available. + // (256 - 4) / 4 = 63, but only 8 CPUs are available. let limit = - compute_compiler_concurrency_limit(256 * GIB, 4 * GIB, nonzero(8), true).unwrap(); + compute_compiler_concurrency_limit(256 * GIB, 4 * GIB, MARGIN, nonzero(8), true) + .unwrap(); assert_eq!(limit.get(), 8); } #[test] fn zero_limit_with_rpc_enabled_returns_error() { - // (10 - 8) / 4 = 0, and RPC is enabled, so this is an error. - compute_compiler_concurrency_limit(10 * GIB, 4 * GIB, nonzero(32), true).unwrap_err(); + // (5 - 4) / 4 = 0, and RPC is enabled, so this is an error. + compute_compiler_concurrency_limit(5 * GIB, 4 * GIB, MARGIN, nonzero(32), true) + .unwrap_err(); } #[test] fn zero_limit_with_rpc_disabled_returns_dummy() { - // (10 - 8) / 4 = 0, but RPC is disabled, so a dummy value of 1 is returned. + // (5 - 4) / 4 = 0, but RPC is disabled, so a dummy value of 1 is returned. let limit = - compute_compiler_concurrency_limit(10 * GIB, 4 * GIB, nonzero(32), false).unwrap(); + compute_compiler_concurrency_limit(5 * GIB, 4 * GIB, MARGIN, nonzero(32), false) + .unwrap(); assert_eq!(limit.get(), 1); } #[test] fn memory_below_margin_with_rpc_enabled_returns_error() { // Total memory below the margin saturates to 0, yielding a 0 limit. - compute_compiler_concurrency_limit(4 * GIB, 4 * GIB, nonzero(32), true).unwrap_err(); + compute_compiler_concurrency_limit(3 * GIB, 4 * GIB, MARGIN, nonzero(32), true) + .unwrap_err(); } #[test] fn memory_below_margin_with_rpc_disabled_returns_dummy() { let limit = - compute_compiler_concurrency_limit(4 * GIB, 4 * GIB, nonzero(32), false).unwrap(); + compute_compiler_concurrency_limit(3 * GIB, 4 * GIB, MARGIN, nonzero(32), false) + .unwrap(); assert_eq!(limit.get(), 1); } #[test] fn single_cpu_caps_limit() { let limit = - compute_compiler_concurrency_limit(256 * GIB, 4 * GIB, nonzero(1), true).unwrap(); + compute_compiler_concurrency_limit(256 * GIB, 4 * GIB, MARGIN, nonzero(1), true) + .unwrap(); assert_eq!(limit.get(), 1); } } diff --git a/crates/pathfinder/src/config.rs b/crates/pathfinder/src/config.rs index d831829920..8ac09cbdc3 100644 --- a/crates/pathfinder/src/config.rs +++ b/crates/pathfinder/src/config.rs @@ -25,6 +25,11 @@ pub mod p2p; use p2p::cli::{P2PConsensusCli, P2PSyncCli}; use p2p::{P2PConsensusConfig, P2PSyncConfig}; +const COMPILER_CONCURRENCY_LIMIT_DEFAULT_MEMORY_MARGIN_MIB: u64 = 4 * 1024; + +const COMPILER_CONCURRENCY_LIMIT_MEMORY_MARGIN_ALLOWED_RANGE: std::ops::RangeFrom = + (COMPILER_CONCURRENCY_LIMIT_DEFAULT_MEMORY_MARGIN_MIB / 2)..; + const COMPILER_MEMORY_USAGE_ALLOWED_RANGE: std::ops::RangeInclusive = (pathfinder_compiler::ResourceLimits::RECOMMENDED_MEMORY_USAGE_LIMIT_MIB / 2) ..=(4 * pathfinder_compiler::ResourceLimits::RECOMMENDED_MEMORY_USAGE_LIMIT_MIB); @@ -511,12 +516,29 @@ This should only be enabled for debugging purposes as it adds substantial proces #[arg( long = "rpc.compiler.concurrency-limit", - long_help = "Maximum number of concurrent Sierra to CASM compilations for RPC calls. \ - Defaults to the number of CPU cores available.", + long_help = "Maximum number of concurrent Sierra to CASM compilations for RPC calls. +Default value is computed based on the formula: + + min(floor((Total_RAM - Margin) / Compiler_Max_Memory_Usage), Num_CPU_Cores) + +where: + Margin is the value of --compiler.concurrency-memory-margin-mib + Compiler_Max_Memory_Usage is the value of --compiler.max-memory-usage-mib option.", env = "PATHFINDER_RPC_COMPILER_CONCURRENCY_LIMIT" )] compiler_concurrency_limit: Option, + #[arg( + long = "compiler.concurrency-memory-margin-mib", + long_help = "Memory margin assumed for normal operation when computing compiler concurrency limit in MiB. + +See --rpc.compiler.concurrency-limit for more details.", + env = "PATHFINDER_COMPILER_CONCURRENCY_MEMORY_MARGIN_MIB", + default_value_t = COMPILER_CONCURRENCY_LIMIT_DEFAULT_MEMORY_MARGIN_MIB, + value_parser = clap::value_parser!(u64).range(COMPILER_CONCURRENCY_LIMIT_MEMORY_MARGIN_ALLOWED_RANGE), + )] + compiler_concurrency_memory_margin_mib: u64, + #[arg( long = "compiler.max-memory-usage-mib", long_help = "Maximum memory usage for the compiler in MiB. @@ -1159,6 +1181,9 @@ pub struct Config { pub state_tries: Option, pub versioned_constants_map: VersionedConstantsMap, pub compiler_concurrency_limit: Option, + // Margin in bytes to subtract from total RAM when computing default compiler concurrency + // limit. + pub compiler_concurrency_memory_margin: u64, pub compiler_resource_limits: pathfinder_compiler::ResourceLimits, pub blockifier_libfuncs: pathfinder_compiler::BlockifierLibfuncs, pub feeder_gateway_fetch_concurrency: NonZeroUsize, @@ -1419,6 +1444,8 @@ impl Config { pub fn parse(args: Box) -> Self { let network = NetworkConfig::from_components(args.network); + const BYTES_IN_MIB: u64 = 1024 * 1024; + Config { data_directory: args.data_directory, ethereum: Ethereum { @@ -1472,10 +1499,13 @@ impl Config { .unwrap_or_default(), compiler_resource_limits: pathfinder_compiler::ResourceLimits::new( // Convert MiB to bytes for the general config. - args.compiler_max_memory_usage_mib * 1024 * 1024, + args.compiler_max_memory_usage_mib * BYTES_IN_MIB, args.compiler_max_cpu_time_secs, ), compiler_concurrency_limit: args.compiler_concurrency_limit, + // Convert MiB to bytes for the general config. + compiler_concurrency_memory_margin: args.compiler_concurrency_memory_margin_mib + * BYTES_IN_MIB, blockifier_libfuncs: args.compile_config.blockifier_libfuncs.into(), fetch_casm_from_fgw: args.fetch_casm_from_fgw, shutdown_grace_period: Duration::from_secs(args.shutdown_grace_period.get()), From 0154721222a44247e52195ac6479e0a06a347e9a Mon Sep 17 00:00:00 2001 From: Krzysztof Lis Date: Wed, 17 Jun 2026 22:51:18 +0200 Subject: [PATCH 5/7] doc: update changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e88ac0bbd5..1045de6bfd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `CANDIDATE` transaction status is deprecated and will never be returned by gateway or pathfinder. Using it as a filter in `subscribeNewTransactions` will fall back to `PRE_CONFIRMED` with a warning. - The `blockifier` and `starknet_api` crates have been upgraded to 0.19.0-rc.2. +- The default value of `--rpc.compiler.concurrency-limit` is now bound to system memory via this equation: `min(floor((Total_RAM - Margin) / Compiler_Max_Memory_Usage), Num_CPU_Cores)`, where: + - `Margin` is the value of the new CLI option `--compiler.concurrency-memory-margin-mib`, which defaults to 4GiB, + - `Compiler_Max_Memory_Usage` is the value of the `--compiler.max-memory-usage-mib` CLI option, which defaults to 4GiB. ## [0.22.5] - 2026-06-08 From 16150a864ac1ae0395b71d88367376333e24448d Mon Sep 17 00:00:00 2001 From: Krzysztof Lis Date: Mon, 22 Jun 2026 13:28:54 +0200 Subject: [PATCH 6/7] doc: use snake_case --- CHANGELOG.md | 6 +++--- crates/pathfinder/src/bin/pathfinder/main.rs | 8 ++++---- crates/pathfinder/src/config.rs | 6 +++--- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1045de6bfd..aca7a16998 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,9 +17,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `CANDIDATE` transaction status is deprecated and will never be returned by gateway or pathfinder. Using it as a filter in `subscribeNewTransactions` will fall back to `PRE_CONFIRMED` with a warning. - The `blockifier` and `starknet_api` crates have been upgraded to 0.19.0-rc.2. -- The default value of `--rpc.compiler.concurrency-limit` is now bound to system memory via this equation: `min(floor((Total_RAM - Margin) / Compiler_Max_Memory_Usage), Num_CPU_Cores)`, where: - - `Margin` is the value of the new CLI option `--compiler.concurrency-memory-margin-mib`, which defaults to 4GiB, - - `Compiler_Max_Memory_Usage` is the value of the `--compiler.max-memory-usage-mib` CLI option, which defaults to 4GiB. +- The default value of `--rpc.compiler.concurrency-limit` is now bound to system memory via this equation: `min(floor((total_ram - margin) / compiler_max_memory_usage), num_cpu_cores)`, where: + - `margin` is the value of the new CLI option `--compiler.concurrency-memory-margin-mib`, which defaults to 4GiB, + - `compiler_max_memory_usage` is the value of the `--compiler.max-memory-usage-mib` CLI option, which defaults to 4GiB. ## [0.22.5] - 2026-06-08 diff --git a/crates/pathfinder/src/bin/pathfinder/main.rs b/crates/pathfinder/src/bin/pathfinder/main.rs index ecf8da7dd2..ce291e6442 100644 --- a/crates/pathfinder/src/bin/pathfinder/main.rs +++ b/crates/pathfinder/src/bin/pathfinder/main.rs @@ -639,8 +639,8 @@ fn determine_compiler_concurrency_limit( /// Computes the compiler concurrency limit from the available memory and CPUs. /// -/// `Concurrency_Limit = min(floor((System_RAM - Margin) / Compiler_RAM_Limit), -/// n_CPUs)` +/// `concurrency_limit = min(floor((system_ram - margin) / compiler_ram_limit), +/// n_cpus)` /// /// Returns an error only when the computed limit is 0 and the RPC server is /// enabled, since a running RPC server requires at least one compiler. When the @@ -654,8 +654,8 @@ fn compute_compiler_concurrency_limit( available_parallelism: NonZeroUsize, is_rpc_enabled: bool, ) -> anyhow::Result { - // Concurrency_Limit = min(floor((System_RAM - Margin) / Compiler_RAM_Limit), - // n_CPUs) + // concurrency_limit = min(floor((system_ram - margin) / compiler_ram_limit), + // n_cpus) let concurrency_limit = total_memory_bytes .saturating_sub(compiler_concurrency_memory_margin_bytes) / compiler_memory_limit_bytes; diff --git a/crates/pathfinder/src/config.rs b/crates/pathfinder/src/config.rs index 8ac09cbdc3..168ad74348 100644 --- a/crates/pathfinder/src/config.rs +++ b/crates/pathfinder/src/config.rs @@ -519,11 +519,11 @@ This should only be enabled for debugging purposes as it adds substantial proces long_help = "Maximum number of concurrent Sierra to CASM compilations for RPC calls. Default value is computed based on the formula: - min(floor((Total_RAM - Margin) / Compiler_Max_Memory_Usage), Num_CPU_Cores) + min(floor((total_ram - margin) / compiler_max_memory_usage), num_cpu_cores) where: - Margin is the value of --compiler.concurrency-memory-margin-mib - Compiler_Max_Memory_Usage is the value of --compiler.max-memory-usage-mib option.", + `margin` is the value of --compiler.concurrency-memory-margin-mib + `compiler_max_memory_usage` is the value of --compiler.max-memory-usage-mib option.", env = "PATHFINDER_RPC_COMPILER_CONCURRENCY_LIMIT" )] compiler_concurrency_limit: Option, From f3cc9a3e697289d675f417338be08495afb0a421 Mon Sep 17 00:00:00 2001 From: Krzysztof Lis Date: Mon, 22 Jun 2026 13:32:41 +0200 Subject: [PATCH 7/7] chore: update lockfile --- Cargo.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4e58cbecb3..f11dfc8c3d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5050,7 +5050,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "objc2", ] @@ -8414,7 +8414,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "dispatch2", "objc2", ] @@ -8431,7 +8431,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "objc2", ]