Skip to content

Commit 62d1ed1

Browse files
committed
[monarch] Typed global configuration from python integrated with the Attrs system
Pull Request resolved: #1414 This diff introduces a typed global configuration system in python for monarch, integrated with the hyperactor `Attrs` system and the `hyperactor::global::config` module. To make an attribute configurable via python, first add the meta attribute `PYTHON_CONFIG_KEY`, like so: ``` @meta( CONFIG_ENV_VAR = "HYPERACTOR_MESH_DEFAULT_TRANSPORT".to_string(), PYTHON_CONFIG_KEY = "default_transport".to_string(), ) pub attr DEFAULT_TRANSPORT: ChannelTransport = ChannelTransport::Unix; ``` This makes it so that the `DEFAULT_TRANSPORT` attr can be configured using `monarch.configure(default_transport=...)`. In order to ensure that attrs with type `ChannelTransport` can be configured by passing `PyChannelTransport` values to `monarch.configure`, in `monarch_hyperactor/src/config.rs`, add the macro invocation: ``` declare_py_config_type!(PyChannelTransport as ChannelTransport); ``` For attrs with rust types that can be passed directly to and from python (like `String`), simply add: ``` declare_py_config_type!(String); ``` These macro invocations only need to be added once per unique attr type we want to support -- *not* once per actual attr. From python, the `DEFAULT_TRANSPORT` attr is then configurable in the global config by calling ``` monarch._rust_bindings.monarch_hyperactor.config.configure( default_transport=ChannelTransport.MetaTlsWithHostname, ... ) ``` You can then get the currently configured values with ``` monarch._rust_bindings.monarch_hyperactor.config.get_configuration() ``` which returns ``` { "default_transport": ChannelTransport.MetaTlsWithHostname, ... } ``` ghstack-source-id: 313996274 Differential Revision: [D83701581](https://our.internmc.facebook.com/intern/diff/D83701581/)
1 parent 4a1cc34 commit 62d1ed1

File tree

8 files changed

+345
-4
lines changed

8 files changed

+345
-4
lines changed

hyperactor/src/attrs.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,9 @@ pub struct AttrKeyInfo {
135135
pub parse: fn(&str) -> Result<Box<dyn SerializableValue>, anyhow::Error>,
136136
/// Default value for the attribute, if any.
137137
pub default: Option<&'static dyn SerializableValue>,
138+
/// A reference to the relevant key object with the associated
139+
/// type parameter erased. Can be downcast to a concrete Key<T>.
140+
pub erased: &'static dyn ErasedKey,
138141
}
139142

140143
inventory::collect!(AttrKeyInfo);
@@ -199,6 +202,39 @@ impl<T: 'static> Clone for Key<T> {
199202

200203
impl<T: 'static> Copy for Key<T> {}
201204

205+
/// A trait for type-erased keys.
206+
pub trait ErasedKey: Any + Send + Sync + 'static {
207+
/// The name of the key.
208+
fn name(&self) -> &'static str;
209+
210+
/// The typehash of the key's associated type.
211+
fn typehash(&self) -> u64;
212+
213+
/// The typename of the key's associated type.
214+
fn typename(&self) -> &'static str;
215+
}
216+
217+
impl dyn ErasedKey {
218+
/// Downcast a type-erased key to a specific key type.
219+
pub fn downcast_ref<T: Named + 'static>(&'static self) -> Option<&'static Key<T>> {
220+
(self as &dyn Any).downcast_ref::<Key<T>>()
221+
}
222+
}
223+
224+
impl<T: AttrValue> ErasedKey for Key<T> {
225+
fn name(&self) -> &'static str {
226+
self.name
227+
}
228+
229+
fn typehash(&self) -> u64 {
230+
T::typehash()
231+
}
232+
233+
fn typename(&self) -> &'static str {
234+
T::typename()
235+
}
236+
}
237+
202238
// Enable attr[key] syntax.
203239
impl<T: AttrValue> Index<Key<T>> for Attrs {
204240
type Output = T;
@@ -772,6 +808,7 @@ macro_rules! declare_attrs {
772808
Ok(Box::new(value) as Box<dyn $crate::attrs::SerializableValue>)
773809
},
774810
default: Some($crate::paste! { &[<$name _DEFAULT>] }),
811+
erased: &$name,
775812
}
776813
}
777814
};
@@ -824,6 +861,7 @@ macro_rules! declare_attrs {
824861
Ok(Box::new(value) as Box<dyn $crate::attrs::SerializableValue>)
825862
},
826863
default: None,
864+
erased: &$name,
827865
}
828866
}
829867
};

hyperactor/src/config.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ declare_attrs! {
3535
/// key.
3636
pub attr CONFIG_ENV_VAR: String;
3737

38+
/// This is a meta-attribute specifying the name of the kwarg to pass to monarch.configure()
39+
/// to set the attribute value in the global config.
40+
pub attr PYTHON_CONFIG_KEY: String;
41+
3842
/// Maximum frame length for codec
3943
@meta(CONFIG_ENV_VAR = "HYPERACTOR_CODEC_MAX_FRAME_LENGTH".to_string())
4044
pub attr CODEC_MAX_FRAME_LENGTH: usize = 10 * 1024 * 1024 * 1024; // 10 GiB
@@ -244,6 +248,12 @@ pub mod global {
244248
CONFIG.read().unwrap().get(key).unwrap().clone()
245249
}
246250

251+
/// Get a key from the global configuration by cloning the value,
252+
/// if it exists. Returns None if the key is not present.
253+
pub fn try_get_cloned<T: AttrValue>(key: Key<T>) -> Option<T> {
254+
CONFIG.read().unwrap().get(key).cloned()
255+
}
256+
247257
/// Get the global attrs
248258
pub fn attrs() -> Attrs {
249259
CONFIG.read().unwrap().clone()
@@ -258,6 +268,12 @@ pub mod global {
258268
*config = Attrs::new();
259269
}
260270

271+
/// Set a key in the global configuration.
272+
pub fn set<T: AttrValue>(key: Key<T>, value: T) {
273+
let mut config = CONFIG.write().unwrap();
274+
config.insert_value(key, Box::new(value));
275+
}
276+
261277
/// A guard that holds the global configuration lock and provides override functionality.
262278
///
263279
/// This struct acts as both a lock guard (preventing other tests from modifying global config)

hyperactor_mesh/src/proc_mesh.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ use hyperactor::channel::ChannelAddr;
3333
use hyperactor::channel::ChannelTransport;
3434
use hyperactor::config;
3535
use hyperactor::config::CONFIG_ENV_VAR;
36+
use hyperactor::config::PYTHON_CONFIG_KEY;
3637
use hyperactor::context;
3738
use hyperactor::declare_attrs;
3839
use hyperactor::mailbox;
@@ -85,10 +86,12 @@ use std::sync::RwLock;
8586

8687
declare_attrs! {
8788
/// Default transport type to use across the application.
88-
@meta(CONFIG_ENV_VAR = "HYPERACTOR_MESH_DEFAULT_TRANSPORT".to_string())
89-
attr DEFAULT_TRANSPORT: ChannelTransport = ChannelTransport::Unix;
89+
@meta(
90+
CONFIG_ENV_VAR = "HYPERACTOR_MESH_DEFAULT_TRANSPORT".to_string(),
91+
PYTHON_CONFIG_KEY = "default_transport".to_string(),
92+
)
93+
pub attr DEFAULT_TRANSPORT: ChannelTransport = ChannelTransport::Unix;
9094
}
91-
9295
/// Get the default transport type to use across the application.
9396
pub fn default_transport() -> ChannelTransport {
9497
config::global::get_cloned(DEFAULT_TRANSPORT)

monarch_hyperactor/src/channel.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use hyperactor::channel::ChannelTransport;
1313
use hyperactor::channel::MetaTlsAddr;
1414
use hyperactor::channel::TlsMode;
1515
use pyo3::exceptions::PyRuntimeError;
16+
use pyo3::exceptions::PyValueError;
1617
use pyo3::prelude::*;
1718

1819
/// Python binding for [`hyperactor::channel::ChannelTransport`]
@@ -31,6 +32,33 @@ pub enum PyChannelTransport {
3132
// Sim(/*transport:*/ ChannelTransport), TODO kiuk@ add support
3233
}
3334

35+
#[pymethods]
36+
impl PyChannelTransport {
37+
fn get(&self) -> Self {
38+
self.clone()
39+
}
40+
}
41+
42+
impl TryFrom<ChannelTransport> for PyChannelTransport {
43+
type Error = PyErr;
44+
45+
fn try_from(transport: ChannelTransport) -> PyResult<Self> {
46+
match transport {
47+
ChannelTransport::Tcp => Ok(PyChannelTransport::Tcp),
48+
ChannelTransport::MetaTls(TlsMode::Hostname) => {
49+
Ok(PyChannelTransport::MetaTlsWithHostname)
50+
}
51+
ChannelTransport::MetaTls(TlsMode::IpV6) => Ok(PyChannelTransport::MetaTlsWithIpV6),
52+
ChannelTransport::Local => Ok(PyChannelTransport::Local),
53+
ChannelTransport::Unix => Ok(PyChannelTransport::Unix),
54+
_ => Err(PyValueError::new_err(format!(
55+
"unsupported transport: {}",
56+
transport
57+
))),
58+
}
59+
}
60+
}
61+
3462
#[pyclass(
3563
name = "ChannelAddr",
3664
module = "monarch._rust_bindings.monarch_hyperactor.channel"

monarch_hyperactor/src/config.rs

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,23 @@
1111
//! This module provides monarch-specific configuration attributes that extend
1212
//! the base hyperactor configuration system.
1313
14+
use std::collections::HashMap;
15+
use std::fmt::Debug;
16+
17+
use hyperactor::AttrValue;
18+
use hyperactor::Named;
19+
use hyperactor::attrs::AttrKeyInfo;
20+
use hyperactor::attrs::ErasedKey;
1421
use hyperactor::attrs::declare_attrs;
22+
use hyperactor::channel::ChannelTransport;
23+
use hyperactor::config::PYTHON_CONFIG_KEY;
24+
use pyo3::conversion::IntoPyObjectExt;
25+
use pyo3::exceptions::PyTypeError;
26+
use pyo3::exceptions::PyValueError;
1527
use pyo3::prelude::*;
1628

29+
use crate::channel::PyChannelTransport;
30+
1731
// Declare monarch-specific configuration keys
1832
declare_attrs! {
1933
/// Use a single asyncio runtime for all Python actors, rather than one per actor
@@ -30,6 +44,183 @@ pub fn reload_config_from_env() -> PyResult<()> {
3044
Ok(())
3145
}
3246

47+
/// Map from name of the kwarg that will be passed to `monarch.configure(...)`
48+
/// to the `Key<T>`` associated with that kwarg. This contains all of the
49+
/// attribute keys with meta-attribute `PYTHON_CONFIG_KEY`.
50+
static KEY_BY_NAME: std::sync::LazyLock<HashMap<&'static str, &'static dyn ErasedKey>> =
51+
std::sync::LazyLock::new(|| {
52+
inventory::iter::<AttrKeyInfo>()
53+
.filter_map(|info| {
54+
info.meta
55+
.get(PYTHON_CONFIG_KEY)
56+
.map(|py_name| (py_name.as_str(), info.erased))
57+
})
58+
.collect()
59+
});
60+
61+
/// Map from typehash to an info struct that can be used to downcast an `ErasedKey`
62+
/// to a concrete `Key<T>` and use it to get/set values in the global configl
63+
static TYPEHASH_TO_INFO: std::sync::LazyLock<HashMap<u64, &'static PythonConfigTypeInfo>> =
64+
std::sync::LazyLock::new(|| {
65+
inventory::iter::<PythonConfigTypeInfo>()
66+
.map(|info| ((info.typehash)(), info))
67+
.collect()
68+
});
69+
70+
/// Given a key, get the associated `T`-typed value from the global config, then
71+
/// convert it to a `P`-typed object that can be converted to PyObject, and
72+
/// return that PyObject.
73+
fn get_global_config<'py, P, T>(
74+
py: Python<'py>,
75+
key: &'static dyn ErasedKey,
76+
) -> PyResult<Option<PyObject>>
77+
where
78+
T: AttrValue + TryInto<P>,
79+
P: IntoPyObjectExt<'py>,
80+
PyErr: From<<T as TryInto<P>>::Error>,
81+
{
82+
// Well, it can't fail unless there's a bug in the code in this file.
83+
let key = key.downcast_ref::<T>().expect("cannot fail");
84+
let val: Option<P> = hyperactor::config::global::try_get_cloned(key.clone())
85+
.map(|v| v.try_into())
86+
.transpose()?;
87+
val.map(|v| v.into_py_any(py)).transpose()
88+
}
89+
90+
fn set_global_config<T: AttrValue + Debug>(key: &'static dyn ErasedKey, value: T) -> PyResult<()> {
91+
// Again, can't fail unless there's a bug in the code in this file.
92+
let key = key.downcast_ref().expect("cannot fail");
93+
hyperactor::config::global::set(*key, value.clone());
94+
Ok(())
95+
}
96+
97+
fn set_global_config_from_py_obj(py: Python<'_>, name: &str, val: PyObject) -> PyResult<()> {
98+
// Get the `ErasedKey` from the kwarg `name` passed to `monarch.configure(...)`.
99+
let key = match KEY_BY_NAME.get(name) {
100+
None => {
101+
return Err(PyValueError::new_err(format!(
102+
"invalid configuration key: `{}`",
103+
name
104+
)));
105+
}
106+
Some(key) => *key,
107+
};
108+
109+
// Using the typehash from the erased key, get/call the function that can downcast
110+
// the key and set the value on the global config.
111+
match TYPEHASH_TO_INFO.get(&key.typehash()) {
112+
None => Err(PyTypeError::new_err(format!(
113+
"configuration key `{}` has type `{}`, but configuring with values of this type from Python is not supported.",
114+
name,
115+
key.typename()
116+
))),
117+
Some(info) => (info.set_global_config)(py, key, val),
118+
}
119+
}
120+
121+
/// Struct to associate a typehash with functions for getting/setting
122+
/// values in the global config with keys of type `Key<T>`, where
123+
/// `T::typehash() == PythonConfigTypeInfo::typehash()`.
124+
struct PythonConfigTypeInfo {
125+
typehash: fn() -> u64,
126+
set_global_config:
127+
fn(py: Python<'_>, key: &'static dyn ErasedKey, val: PyObject) -> PyResult<()>,
128+
get_global_config:
129+
fn(py: Python<'_>, key: &'static dyn ErasedKey) -> PyResult<Option<PyObject>>,
130+
}
131+
132+
inventory::collect!(PythonConfigTypeInfo);
133+
134+
/// Macro to declare that keys of this type can be configured
135+
/// from python using `monarch.configure(...)`. For types
136+
/// like `String` that are convertible directly to/from PyObjects,
137+
/// you can just use `declare_py_config_type!(String)`. For types
138+
/// that must first be converted to/from a rust python wrapper
139+
/// (e.g., keys with type `ChannelTransport` must use `PyChannelTransport`
140+
/// as an intermediate step), the usage is
141+
/// `declare_py_config_type!(PyChannelTransport as ChannelTransport)`.
142+
macro_rules! declare_py_config_type {
143+
($($ty:ty),+ $(,)?) => {
144+
hyperactor::paste! {
145+
$(
146+
hyperactor::submit! {
147+
PythonConfigTypeInfo {
148+
typehash: $ty::typehash,
149+
set_global_config: |py, key, val| {
150+
let val: $ty = val.extract::<$ty>(py).map_err(|err| PyTypeError::new_err(format!(
151+
"invalid value `{}` for configuration key `{}` ({})",
152+
val, key.name(), err
153+
)))?;
154+
set_global_config(key, val)
155+
},
156+
get_global_config: |py, key| {
157+
get_global_config::<$ty, $ty>(py, key)
158+
}
159+
}
160+
}
161+
)+
162+
}
163+
};
164+
($py_ty:ty as $ty:ty) => {
165+
hyperactor::paste! {
166+
hyperactor::submit! {
167+
PythonConfigTypeInfo {
168+
typehash: $ty::typehash,
169+
set_global_config: |py, key, val| {
170+
let val: $ty = val.extract::<$py_ty>(py).map_err(|err| PyTypeError::new_err(format!(
171+
"invalid value `{}` for configuration key `{}` ({})",
172+
val, key.name(), err
173+
)))?.into();
174+
set_global_config(key, val)
175+
},
176+
get_global_config: |py, key| {
177+
get_global_config::<$py_ty, $ty>(py, key)
178+
}
179+
}
180+
}
181+
}
182+
};
183+
}
184+
185+
declare_py_config_type!(PyChannelTransport as ChannelTransport);
186+
declare_py_config_type!(
187+
i8, i16, i32, i64, u8, u16, u32, u64, usize, f32, f64, bool, String
188+
);
189+
190+
/// Iterate over each key-value pair. Attempt to retrieve the `Key<T>`
191+
/// associated with the key and convert the value to `T`, then set
192+
/// them on the global config. The association between kwarg and
193+
/// `Key<T>` is specified using the `PYTHON_CONFIG_KEY` meta-attribute.
194+
#[pyfunction]
195+
#[pyo3(signature = (**kwargs))]
196+
fn configure(py: Python<'_>, kwargs: Option<HashMap<String, PyObject>>) -> PyResult<()> {
197+
kwargs
198+
.map(|kwargs| {
199+
kwargs
200+
.into_iter()
201+
.try_for_each(|(key, val)| set_global_config_from_py_obj(py, &key, val))
202+
})
203+
.transpose()?;
204+
Ok(())
205+
}
206+
207+
/// For all attribute keys with meta-attribute `PYTHON_CONFIG_KEY` defined, return the
208+
/// current associated value in the global config. The key will not be present in the
209+
/// result of it has no value in the global config.
210+
#[pyfunction]
211+
fn get_configuration(py: Python<'_>) -> PyResult<HashMap<String, PyObject>> {
212+
KEY_BY_NAME
213+
.iter()
214+
.filter_map(|(name, key)| match TYPEHASH_TO_INFO.get(&key.typehash()) {
215+
None => None,
216+
Some(info) => match (info.get_global_config)(py, *key) {
217+
Err(err) => Some(Err(err)),
218+
Ok(val) => val.map(|val| Ok(((*name).into(), val))),
219+
},
220+
})
221+
.collect()
222+
}
223+
33224
/// Register Python bindings for the config module
34225
pub fn register_python_bindings(module: &Bound<'_, PyModule>) -> PyResult<()> {
35226
let reload = wrap_pyfunction!(reload_config_from_env, module)?;
@@ -38,5 +229,20 @@ pub fn register_python_bindings(module: &Bound<'_, PyModule>) -> PyResult<()> {
38229
"monarch._rust_bindings.monarch_hyperactor.config",
39230
)?;
40231
module.add_function(reload)?;
232+
233+
let configure = wrap_pyfunction!(configure, module)?;
234+
configure.setattr(
235+
"__module__",
236+
"monarch._rust_bindings.monarch_hyperactor.config",
237+
)?;
238+
module.add_function(configure)?;
239+
240+
let get_configuration = wrap_pyfunction!(get_configuration, module)?;
241+
get_configuration.setattr(
242+
"__module__",
243+
"monarch._rust_bindings.monarch_hyperactor.config",
244+
)?;
245+
module.add_function(get_configuration)?;
246+
41247
Ok(())
42248
}

0 commit comments

Comments
 (0)