monarch_hyperactor/
runtime.rs1use std::cell::OnceCell as UnsyncOnceCell;
10use std::future::Future;
11use std::pin::Pin;
12use std::sync::Mutex;
13use std::sync::OnceLock;
14use std::sync::atomic::AtomicUsize;
15use std::sync::atomic::Ordering;
16use std::time::Duration;
17
18use anyhow::Result;
19use hyperactor::runtime_identity::RuntimeKind;
20use hyperactor::runtime_identity::shutdown_data_plane_runtimes;
21use hyperactor::runtime_identity::tag_current_thread;
22pub use monarch_gil::GilSite;
23pub use monarch_gil::force_unsanctioned_gil_on_control_plane;
24pub use monarch_gil::get_gil_on_control_plane;
25pub use monarch_gil::monarch_with_gil;
26pub use monarch_gil::monarch_with_gil_blocking;
27pub use monarch_gil::reset_gil_on_control_plane;
28use pyo3::PyResult;
29use pyo3::Python;
30use pyo3::exceptions::PyRuntimeError;
31use pyo3::prelude::*;
32use pyo3::types::PyAnyMethods;
33use pyo3_async_runtimes::TaskLocals;
34use tokio::runtime::Handle;
35use tokio::task;
36
37use crate::config::TOKIO_WORKER_THREADS;
38
39struct GlobalRuntime {
49 handle: Handle,
50 runtime: Mutex<Option<tokio::runtime::Runtime>>,
51}
52
53static INSTANCE: OnceLock<GlobalRuntime> = OnceLock::new();
54
55fn global_runtime() -> &'static GlobalRuntime {
56 INSTANCE.get_or_init(|| {
57 let worker_threads = hyperactor_config::global::get(TOKIO_WORKER_THREADS);
58 let mut builder = tokio::runtime::Builder::new_multi_thread();
59 if let Some(worker_threads) = worker_threads {
60 builder.worker_threads(worker_threads.get());
61 }
62 let runtime = builder
63 .thread_name_fn(|| {
64 static ATOMIC_ID: AtomicUsize = AtomicUsize::new(0);
65 let id = ATOMIC_ID.fetch_add(1, Ordering::SeqCst);
66 format!("monarch-pytokio-worker-{}", id)
67 })
68 .on_thread_start(|| tag_current_thread(RuntimeKind::ControlPlane))
72 .enable_all()
73 .build()
74 .unwrap();
75 let handle = runtime.handle().clone();
76 GlobalRuntime {
77 handle,
78 runtime: Mutex::new(Some(runtime)),
79 }
80 })
81}
82
83pub fn get_tokio_runtime() -> Handle {
84 global_runtime().handle.clone()
85}
86
87#[pyfunction]
98pub fn shutdown_tokio_runtime(py: Python<'_>) {
99 py.detach(|| {
102 shutdown_data_plane_runtimes(Duration::from_secs(1));
106 let Some(global) = INSTANCE.get() else {
107 return;
108 };
109 let Some(rt) = global.runtime.lock().unwrap().take() else {
110 return;
111 };
112 rt.shutdown_timeout(Duration::from_secs(1));
113 });
114}
115
116static MAIN_THREAD_NATIVE_ID: OnceLock<i64> = OnceLock::new();
119
120fn get_main_thread_native_id() -> i64 {
123 *MAIN_THREAD_NATIVE_ID.get_or_init(|| {
124 monarch_with_gil_blocking(GilSite::Bootstrap, |py| {
125 let threading = py.import("threading").expect("failed to import threading");
126 let main_thread = threading
127 .call_method0("main_thread")
128 .expect("failed to get main_thread");
129 main_thread
130 .getattr("native_id")
131 .expect("failed to get native_id")
132 .extract::<i64>()
133 .expect("native_id is not an i64")
134 })
135 })
136}
137
138#[cfg(target_os = "linux")]
140fn get_current_thread_id() -> i64 {
141 nix::unistd::gettid().as_raw() as i64
142}
143
144#[cfg(target_os = "macos")]
146fn get_current_thread_id() -> i64 {
147 let mut tid: u64 = 0;
148 unsafe {
150 let ret = libc::pthread_threadid_np(0, &mut tid);
151 debug_assert_eq!(
152 ret, 0,
153 "pthread_threadid_np failed with error code: {}",
154 ret
155 );
156 }
157 debug_assert!(tid <= i64::MAX as u64, "thread ID {} exceeds i64::MAX", tid);
159 tid as i64
160}
161
162#[cfg(not(any(target_os = "linux", target_os = "macos")))]
164compile_error!("get_current_thread_id is only implemented for Linux and macOS");
165
166pub fn is_main_thread() -> bool {
169 let current_tid = get_current_thread_id();
170 current_tid == get_main_thread_native_id()
171}
172
173pub fn initialize(py: Python) -> Result<()> {
174 let _ = get_main_thread_native_id();
180
181 let atexit = py.import("atexit")?;
182 let shutdown_fn = wrap_pyfunction!(shutdown_tokio_runtime, py)?;
183 atexit.call_method1("register", (shutdown_fn,))?;
184 Ok(())
185}
186
187pub fn signal_safe_block_on<F>(py: Python, future: F) -> PyResult<F::Output>
200where
201 F: Future + Send + 'static,
202 F::Output: Send + 'static,
203{
204 let runtime = get_tokio_runtime();
205 py.detach(|| {
208 if is_main_thread() {
209 let handle = runtime.spawn(future);
211 runtime.block_on(async {
214 tokio::select! {
215 result = handle => result.map_err(|e| PyRuntimeError::new_err(format!("JoinErr: {:?}", e))),
216 signal = async {
217 let sleep_for = std::time::Duration::from_millis(100);
218 loop {
219 monarch_with_gil_blocking(GilSite::AwaitDrive, |py| py.check_signals())?;
222 tokio::time::sleep(sleep_for).await;
223 }
224 } => signal
225 }
226 })
227 } else {
228 Ok(runtime.block_on(future))
232 }
233 })
234}
235
236#[pyfunction]
240pub fn sleep_indefinitely_for_unit_tests(py: Python) -> PyResult<()> {
241 let future = async {
243 loop {
244 tracing::info!("idef sleeping for 100ms");
245 tokio::time::sleep(Duration::from_millis(100)).await;
246 }
247 };
248
249 signal_safe_block_on(py, future)
252}
253
254pub fn register_python_bindings(runtime_mod: &Bound<'_, PyModule>) -> PyResult<()> {
256 let sleep_indefinitely_fn =
257 wrap_pyfunction!(sleep_indefinitely_for_unit_tests, runtime_mod.py())?;
258 sleep_indefinitely_fn.setattr(
259 "__module__",
260 "monarch._rust_bindings.monarch_hyperactor.runtime",
261 )?;
262 runtime_mod.add_function(sleep_indefinitely_fn)?;
263
264 let get_gil_on_control_plane_fn = wrap_pyfunction!(get_gil_on_control_plane, runtime_mod.py())?;
265 get_gil_on_control_plane_fn.setattr(
266 "__module__",
267 "monarch._rust_bindings.monarch_hyperactor.runtime",
268 )?;
269 runtime_mod.add_function(get_gil_on_control_plane_fn)?;
270
271 let reset_gil_on_control_plane_fn =
272 wrap_pyfunction!(reset_gil_on_control_plane, runtime_mod.py())?;
273 reset_gil_on_control_plane_fn.setattr(
274 "__module__",
275 "monarch._rust_bindings.monarch_hyperactor.runtime",
276 )?;
277 runtime_mod.add_function(reset_gil_on_control_plane_fn)?;
278
279 let force_unsanctioned_gil_on_control_plane_fn =
280 wrap_pyfunction!(force_unsanctioned_gil_on_control_plane, runtime_mod.py())?;
281 force_unsanctioned_gil_on_control_plane_fn.setattr(
282 "__module__",
283 "monarch._rust_bindings.monarch_hyperactor.runtime",
284 )?;
285 runtime_mod.add_function(force_unsanctioned_gil_on_control_plane_fn)?;
286
287 Ok(())
288}
289
290struct SimpleRuntime;
291
292impl pyo3_async_runtimes::generic::Runtime for SimpleRuntime {
293 type JoinError = task::JoinError;
294 type JoinHandle = task::JoinHandle<()>;
295
296 fn spawn<F>(fut: F) -> Self::JoinHandle
297 where
298 F: Future<Output = ()> + Send + 'static,
299 {
300 get_tokio_runtime().spawn(async move {
301 fut.await;
302 })
303 }
304}
305
306tokio::task_local! {
307 static TASK_LOCALS: UnsyncOnceCell<TaskLocals>;
308}
309
310impl pyo3_async_runtimes::generic::ContextExt for SimpleRuntime {
311 fn scope<F, R>(locals: TaskLocals, fut: F) -> Pin<Box<dyn Future<Output = R> + Send>>
312 where
313 F: Future<Output = R> + Send + 'static,
314 {
315 let cell = UnsyncOnceCell::new();
316 cell.set(locals).unwrap();
317
318 Box::pin(TASK_LOCALS.scope(cell, fut))
319 }
320
321 fn get_task_locals() -> Option<TaskLocals> {
322 TASK_LOCALS
323 .try_with(|c| {
324 c.get().map(|locals| {
325 monarch_with_gil_blocking(GilSite::TaskLocals, |py| locals.clone_ref(py))
326 })
327 })
328 .unwrap_or_default()
329 }
330}
331
332pub fn future_into_py<F, T>(py: Python, fut: F) -> PyResult<Bound<PyAny>>
333where
334 F: Future<Output = PyResult<T>> + Send + 'static,
335 T: for<'py> IntoPyObject<'py>,
336{
337 pyo3_async_runtimes::generic::future_into_py::<SimpleRuntime, F, T>(py, fut)
338}
339
340#[cfg(test)]
341mod tests {
342 use hyperactor::runtime_identity::RuntimeKind;
343 use hyperactor::runtime_identity::current_runtime_kind;
344
345 use super::*;
346
347 #[test]
349 fn global_runtime_workers_are_control_plane() {
350 let kind = get_tokio_runtime().block_on(async {
351 tokio::spawn(async { current_runtime_kind() })
352 .await
353 .unwrap()
354 });
355 assert_eq!(kind, Some(RuntimeKind::ControlPlane));
356 }
357
358 #[test]
361 fn global_runtime_blocking_pool_is_control_plane() {
362 let kind = get_tokio_runtime().block_on(async {
363 tokio::task::spawn_blocking(current_runtime_kind)
364 .await
365 .unwrap()
366 });
367 assert_eq!(kind, Some(RuntimeKind::ControlPlane));
368 }
369}