diff --git a/Cargo.lock b/Cargo.lock index 763688914b..1b299a5a8f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1518,6 +1518,7 @@ dependencies = [ "sha2", "starknet-types-core", "starknet_api", + "starknet_sierra_compile", "strum 0.25.0", "strum_macros 0.25.3", "tempfile", diff --git a/crates/blockifier/Cargo.toml b/crates/blockifier/Cargo.toml index 0df3a1419b..7f05e3127d 100644 --- a/crates/blockifier/Cargo.toml +++ b/crates/blockifier/Cargo.toml @@ -10,7 +10,7 @@ description = "The transaction-executing component in the Starknet sequencer." workspace = true [features] -cairo_native = ["dep:cairo-native"] +cairo_native = ["dep:cairo-native", "starknet_sierra_compile/cairo_native"] jemalloc = ["dep:tikv-jemallocator"] reexecution = ["transaction_serde"] testing = ["rand", "rstest", "starknet_api/testing"] @@ -50,6 +50,7 @@ serde_json = { workspace = true, features = ["arbitrary_precision"] } sha2.workspace = true starknet-types-core.workspace = true starknet_api.workspace = true +starknet_sierra_compile = { workspace = true, optional = true } strum.workspace = true strum_macros.workspace = true tempfile.workspace = true diff --git a/crates/blockifier/src/state.rs b/crates/blockifier/src/state.rs index e027d2b301..8aa857c963 100644 --- a/crates/blockifier/src/state.rs +++ b/crates/blockifier/src/state.rs @@ -1,4 +1,6 @@ pub mod cached_state; +#[cfg(feature = "cairo_native")] +pub mod contract_class_manager; #[cfg(test)] pub mod error_format_test; pub mod errors; diff --git a/crates/blockifier/src/state/contract_class_manager.rs b/crates/blockifier/src/state/contract_class_manager.rs new file mode 100644 index 0000000000..6fcac067c1 --- /dev/null +++ b/crates/blockifier/src/state/contract_class_manager.rs @@ -0,0 +1,129 @@ +use std::sync::mpsc::{sync_channel, Receiver, SyncSender, TrySendError}; +use std::sync::Arc; + +use log::{error, info}; +use starknet_api::core::ClassHash; +use starknet_api::state::SierraContractClass; +use starknet_sierra_compile::command_line_compiler::CommandLineCompiler; +use starknet_sierra_compile::config::SierraToCasmCompilationConfig; +use starknet_sierra_compile::utils::into_contract_class_for_compilation; +use starknet_sierra_compile::SierraToNativeCompiler; + +use crate::execution::contract_class::{CompiledClassV1, RunnableCompiledClass}; +use crate::execution::native::contract_class::NativeCompiledClassV1; +use crate::state::global_cache::{CachedCairoNative, ContractCaches}; + +const CHANNEL_SIZE: usize = 1000; + +/// Represents a request to compile a sierra contract class to a native compiled class. +/// +/// # Fields: +/// * `class_hash` - used to identify the contract class in the cache. +/// * `sierra_contract_class` - the sierra contract class to be compiled. +/// * `casm_compiled_class` - stored in [`NativeCompiledClassV1`] to allow fallback to cairo_vm +/// execution in case of unxecpected failure during native execution. +type CompilationRequest = (ClassHash, Arc, CompiledClassV1); + +/// Manages the global cache of contract classes and handles sierra-to-native compilation requests. +struct ContractClassManager { + // The global cache of contract classes: casm, sierra, and native. + contract_caches: Arc, + // The sending half of the compilation request channel. + sender: SyncSender, +} + +#[allow(dead_code)] +impl ContractClassManager { + /// Creates a new contract class manager and spawns a thread that listens for compilation + /// requests and processes them (a.k.a. the compilation worker). + /// Returns the contract class manager. + pub fn start(contract_caches: ContractCaches) -> ContractClassManager { + // TODO(Avi, 15/12/2024): Add the size of the channel to the config. + let contract_caches = Arc::new(contract_caches); + let (sender, receiver) = sync_channel(CHANNEL_SIZE); + let compiler_config = SierraToCasmCompilationConfig::default(); + let compiler = CommandLineCompiler::new(compiler_config); + + std::thread::spawn({ + let contract_caches = Arc::clone(&contract_caches); + let compiler = Arc::new(compiler); + + move || run_compilation_worker(contract_caches, receiver, compiler) + }); + + ContractClassManager { contract_caches, sender } + } + + /// Sends a compilation request to the compilation worker. Does not block the sender. Logs an + /// error is the channel is full. + pub fn send_compilation_request(&self, request: CompilationRequest) { + self.cache_request_contracts(&request); + // TODO(Avi, 15/12/2024): Check for duplicated requests. + self.sender.try_send(request).unwrap_or_else(|err| match err { + TrySendError::Full((class_hash, _, _)) => { + error!( + "Compilation request channel is full (size: {}). Compilation request for \ + class hash {} was not sent.", + CHANNEL_SIZE, class_hash + ) + } + TrySendError::Disconnected(_) => { + panic!("Compilation request channel is closed.") + } + }); + } + + /// Returns the native compiled class for the given class hash, if it exists in cache. + pub fn get_native(&self, class_hash: &ClassHash) -> Option { + self.contract_caches.get_native(class_hash) + } + + /// Returns the Sierra contract class for the given class hash, if it exists in cache. + pub fn get_sierra(&self, class_hash: &ClassHash) -> Option> { + self.contract_caches.get_sierra(class_hash) + } + + /// Returns the casm compiled class for the given class hash, if it exists in cache. + pub fn get_casm(&self, class_hash: &ClassHash) -> Option { + self.contract_caches.get_casm(class_hash) + } + + /// Caches the sierra and casm contract classes of a compilation request. + fn cache_request_contracts(&self, request: &CompilationRequest) { + let (class_hash, sierra, casm) = request.clone(); + self.contract_caches.set_sierra(class_hash, sierra); + let cached_casm = RunnableCompiledClass::from(casm); + self.contract_caches.set_casm(class_hash, cached_casm); + } +} + +/// Handles compilation requests from the channel, holding the receiver end of the channel. +/// If no request is available, non-busy-waits until a request is available. +/// When the sender is dropped, the worker processes all pending requests and terminates. +fn run_compilation_worker( + contract_caches: Arc, + receiver: Receiver, + compiler: Arc, +) { + info!("Compilation worker started."); + for (class_hash, sierra, casm) in receiver.iter() { + if contract_caches.get_native(&class_hash).is_some() { + // The contract class is already compiled to native - skip the compilation. + continue; + } + let sierra_for_compilation = into_contract_class_for_compilation(sierra.as_ref()); + let compilation_result = compiler.compile_to_native(sierra_for_compilation); + match compilation_result { + Ok(executor) => { + let native_compiled_class = NativeCompiledClassV1::new(executor, casm); + contract_caches + .set_native(class_hash, CachedCairoNative::Compiled(native_compiled_class)); + } + Err(err) => { + error!("Error compiling contract class: {}", err); + contract_caches.set_native(class_hash, CachedCairoNative::CompilationFailed); + } + } + } + info!("Compilation worker terminated."); +} diff --git a/crates/blockifier/src/state/global_cache.rs b/crates/blockifier/src/state/global_cache.rs index 46534a1839..f60df02f01 100644 --- a/crates/blockifier/src/state/global_cache.rs +++ b/crates/blockifier/src/state/global_cache.rs @@ -1,27 +1,27 @@ use std::sync::{Arc, Mutex, MutexGuard}; use cached::{Cached, SizedCache}; -#[cfg(feature = "cairo_native")] -use cairo_native::executor::AotContractExecutor; use starknet_api::core::ClassHash; #[cfg(feature = "cairo_native")] use starknet_api::state::SierraContractClass; #[cfg(feature = "cairo_native")] use crate::execution::contract_class::RunnableCompiledClass; +#[cfg(feature = "cairo_native")] +use crate::execution::native::contract_class::NativeCompiledClassV1; -type ContractClassLRUCache = SizedCache; -pub type LockedContractClassCache<'a, T> = MutexGuard<'a, ContractClassLRUCache>; +type ContractLRUCache = SizedCache; +pub type LockedClassCache<'a, T> = MutexGuard<'a, ContractLRUCache>; #[derive(Debug, Clone)] -// Thread-safe LRU cache for contract classes, optimized for inter-language sharing when -// `blockifier` compiles as a shared library. +// Thread-safe LRU cache for contract classes (Seirra or compiled Casm/Native), optimized for +// inter-language sharing when `blockifier` compiles as a shared library. // TODO(Yoni, 1/1/2025): consider defining CachedStateReader. -pub struct GlobalContractCache(pub Arc>>); +pub struct GlobalContractCache(pub Arc>>); #[cfg(feature = "cairo_native")] #[derive(Debug, Clone)] pub enum CachedCairoNative { - Compiled(AotContractExecutor), + Compiled(NativeCompiledClassV1), CompilationFailed, } @@ -30,7 +30,7 @@ pub const GLOBAL_CONTRACT_CACHE_SIZE_FOR_TEST: usize = 400; impl GlobalContractCache { /// Locks the cache for atomic access. Although conceptually shared, writing to this cache is /// only possible for one writer at a time. - pub fn lock(&self) -> LockedContractClassCache<'_, T> { + pub fn lock(&self) -> LockedClassCache<'_, T> { self.0.lock().expect("Global contract cache is poisoned.") } @@ -47,25 +47,25 @@ impl GlobalContractCache { } pub fn new(cache_size: usize) -> Self { - Self(Arc::new(Mutex::new(ContractClassLRUCache::::with_size(cache_size)))) + Self(Arc::new(Mutex::new(ContractLRUCache::::with_size(cache_size)))) } } #[cfg(feature = "cairo_native")] -pub struct GlobalContractCacheManager { +pub struct ContractCaches { pub casm_cache: GlobalContractCache, pub native_cache: GlobalContractCache, pub sierra_cache: GlobalContractCache>, } #[cfg(feature = "cairo_native")] -impl GlobalContractCacheManager { +impl ContractCaches { pub fn get_casm(&self, class_hash: &ClassHash) -> Option { self.casm_cache.get(class_hash) } - pub fn set_casm(&self, class_hash: ClassHash, contract_class: RunnableCompiledClass) { - self.casm_cache.set(class_hash, contract_class); + pub fn set_casm(&self, class_hash: ClassHash, compiled_class: RunnableCompiledClass) { + self.casm_cache.set(class_hash, compiled_class); } pub fn get_native(&self, class_hash: &ClassHash) -> Option {