Skip to main content

tenferro_xla/
executor.rs

1use std::fmt;
2
3use tenferro_runtime::GraphProgram;
4use tenferro_tensor::Tensor;
5
6use crate::Error;
7use crate::{lower_to_stablehlo, Result, StableHloModule};
8
9/// Options for the experimental XLA executor.
10///
11/// # Examples
12///
13/// ```
14/// use tenferro_xla::XlaExecutorOptions;
15///
16/// let options = XlaExecutorOptions::default();
17/// assert_eq!(options, XlaExecutorOptions::default());
18/// ```
19#[derive(Clone, Copy, Default, PartialEq, Eq)]
20#[non_exhaustive]
21pub struct XlaExecutorOptions {
22    _private: (),
23}
24
25impl fmt::Debug for XlaExecutorOptions {
26    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
27        f.debug_struct("XlaExecutorOptions").finish()
28    }
29}
30
31/// Experimental peer executor for XLA/PJRT.
32///
33/// # Examples
34///
35/// ```
36/// use tenferro_runtime::{GraphCompiler, TracedTensor};
37/// use tenferro_xla::XlaExecutor;
38///
39/// let x = TracedTensor::from_vec_col_major(vec![1], vec![2.0_f64]).unwrap();
40/// let mut compiler = GraphCompiler::new();
41/// let y = x.neg().unwrap();
42/// let program = compiler.compile(&y).unwrap();
43/// let module = XlaExecutor::default().lower_to_stablehlo(&program).unwrap();
44/// assert!(module.as_str().contains("stablehlo.negate"));
45/// ```
46pub struct XlaExecutor {
47    options: XlaExecutorOptions,
48    #[cfg(feature = "pjrt")]
49    plugin: Option<crate::pjrt::PjrtPlugin>,
50}
51
52impl fmt::Debug for XlaExecutor {
53    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54        f.debug_struct("XlaExecutor")
55            .field("options", &self.options)
56            .field("has_loaded_pjrt_plugin", &self.has_loaded_pjrt_plugin())
57            .finish()
58    }
59}
60
61impl XlaExecutor {
62    /// Create an executor with explicit options.
63    ///
64    /// # Examples
65    ///
66    /// ```
67    /// use tenferro_xla::{XlaExecutor, XlaExecutorOptions};
68    ///
69    /// let executor = XlaExecutor::new(XlaExecutorOptions::default());
70    /// assert_eq!(executor.options(), XlaExecutorOptions::default());
71    /// ```
72    pub fn new(options: XlaExecutorOptions) -> Self {
73        Self {
74            options,
75            #[cfg(feature = "pjrt")]
76            plugin: None,
77        }
78    }
79
80    /// Create an executor by loading PJRT configuration from environment variables.
81    ///
82    /// Without the `pjrt` feature this returns [`Error::PjrtFeatureDisabled`].
83    ///
84    /// # Examples
85    ///
86    /// ```
87    /// use tenferro_xla::{Error, XlaExecutor};
88    ///
89    /// if let Err(err) = XlaExecutor::from_env() {
90    ///     assert!(matches!(err, Error::PjrtFeatureDisabled | Error::MissingEnv { .. } | Error::PluginLoad { .. }));
91    /// }
92    /// ```
93    #[cfg(not(feature = "pjrt"))]
94    pub fn from_env() -> Result<Self> {
95        Err(Error::PjrtFeatureDisabled)
96    }
97
98    /// Create an executor by loading PJRT configuration from environment variables.
99    ///
100    /// # Examples
101    ///
102    /// ```
103    /// use tenferro_xla::XlaExecutor;
104    ///
105    /// let _ = XlaExecutor::from_env();
106    /// ```
107    #[cfg(feature = "pjrt")]
108    pub fn from_env() -> Result<Self> {
109        Self::from_env_var(crate::TENFERRO_PJRT_PLUGIN_ENV)
110    }
111
112    /// Create an executor by loading a PJRT plugin path from a specific
113    /// environment variable.
114    ///
115    /// # Examples
116    ///
117    /// ```
118    /// use tenferro_xla::XlaExecutor;
119    ///
120    /// let _ = XlaExecutor::from_env_var("__TENFERRO_XLA_DOCS_UNSET");
121    /// ```
122    #[cfg(feature = "pjrt")]
123    pub fn from_env_var(var: &'static str) -> Result<Self> {
124        let plugin = crate::pjrt::PjrtPlugin::load_from_env(var)?;
125        Ok(Self {
126            options: XlaExecutorOptions::default(),
127            plugin: Some(plugin),
128        })
129    }
130
131    /// Return the executor options.
132    ///
133    /// # Examples
134    ///
135    /// ```
136    /// use tenferro_xla::XlaExecutor;
137    ///
138    /// assert_eq!(XlaExecutor::default().options(), Default::default());
139    /// ```
140    pub fn options(&self) -> XlaExecutorOptions {
141        self.options
142    }
143
144    /// Return whether this executor owns a loaded PJRT plugin.
145    ///
146    /// # Examples
147    ///
148    /// ```
149    /// use tenferro_xla::XlaExecutor;
150    ///
151    /// assert!(!XlaExecutor::default().has_loaded_pjrt_plugin());
152    /// ```
153    pub fn has_loaded_pjrt_plugin(&self) -> bool {
154        #[cfg(feature = "pjrt")]
155        {
156            self.plugin.is_some()
157        }
158        #[cfg(not(feature = "pjrt"))]
159        {
160            false
161        }
162    }
163
164    /// Lower a graph program to StableHLO without executing it.
165    ///
166    /// # Examples
167    ///
168    /// ```
169    /// use tenferro_runtime::{GraphCompiler, TracedTensor};
170    /// use tenferro_xla::XlaExecutor;
171    ///
172    /// let x = TracedTensor::from_vec_col_major(vec![1], vec![2.0_f64]).unwrap();
173    /// let mut compiler = GraphCompiler::new();
174    /// let y = x.neg().unwrap();
175    /// let program = compiler.compile(&y).unwrap();
176    /// let module = XlaExecutor::default().lower_to_stablehlo(&program).unwrap();
177    /// assert!(module.as_str().contains("stablehlo.negate"));
178    /// ```
179    pub fn lower_to_stablehlo(&self, program: &GraphProgram) -> Result<StableHloModule> {
180        lower_to_stablehlo(program)
181    }
182
183    /// Execute a graph program through a loaded PJRT plugin and return all outputs.
184    ///
185    /// Inputs must match [`GraphProgram::input_specs`] exactly. This
186    /// experimental execution path supports the same exact-static-shape,
187    /// `F32`/`F64` subset as StableHLO lowering.
188    ///
189    /// # Examples
190    ///
191    /// ```
192    /// use tenferro_runtime::{GraphCompiler, TracedTensor};
193    /// use tenferro_tensor::Tensor;
194    /// use tenferro_xla::{Error, XlaExecutor};
195    ///
196    /// let x = TracedTensor::from_vec_col_major(vec![1], vec![2.0_f64]).unwrap();
197    /// let mut compiler = GraphCompiler::new();
198    /// let y = x.neg().unwrap();
199    /// let program = compiler.compile(&y).unwrap();
200    /// let input = Tensor::from_vec_col_major(vec![1], vec![2.0_f64]).unwrap();
201    /// let err = XlaExecutor::default()
202    ///     .run_many_with_inputs(&program, &[&input])
203    ///     .unwrap_err();
204    /// assert!(matches!(err, Error::PjrtFeatureDisabled | Error::PjrtPluginNotLoaded));
205    /// ```
206    pub fn run_many_with_inputs(
207        &self,
208        program: &GraphProgram,
209        inputs: &[&Tensor],
210    ) -> Result<Vec<Tensor>> {
211        #[cfg(feature = "pjrt")]
212        {
213            let Some(plugin) = self.plugin.as_ref() else {
214                return Err(Error::PjrtPluginNotLoaded);
215            };
216            crate::pjrt::run_many_with_inputs(plugin, program, inputs)
217        }
218        #[cfg(not(feature = "pjrt"))]
219        {
220            let _ = (program, inputs);
221            Err(Error::PjrtFeatureDisabled)
222        }
223    }
224
225    /// Execute a single-output graph program through a loaded PJRT plugin.
226    ///
227    /// # Examples
228    ///
229    /// ```
230    /// use tenferro_runtime::{GraphCompiler, TracedTensor};
231    /// use tenferro_tensor::Tensor;
232    /// use tenferro_xla::{Error, XlaExecutor};
233    ///
234    /// let x = TracedTensor::from_vec_col_major(vec![1], vec![2.0_f64]).unwrap();
235    /// let mut compiler = GraphCompiler::new();
236    /// let y = x.neg().unwrap();
237    /// let program = compiler.compile(&y).unwrap();
238    /// let input = Tensor::from_vec_col_major(vec![1], vec![2.0_f64]).unwrap();
239    /// let err = XlaExecutor::default().run_with_inputs(&program, &[&input]).unwrap_err();
240    /// assert!(matches!(err, Error::PjrtFeatureDisabled | Error::PjrtPluginNotLoaded));
241    /// ```
242    pub fn run_with_inputs(&self, program: &GraphProgram, inputs: &[&Tensor]) -> Result<Tensor> {
243        single_output_tensor(self.run_many_with_inputs(program, inputs)?)
244    }
245}
246
247impl Default for XlaExecutor {
248    fn default() -> Self {
249        Self::new(XlaExecutorOptions::default())
250    }
251}
252
253fn single_output_tensor(mut outputs: Vec<Tensor>) -> Result<Tensor> {
254    if outputs.len() != 1 {
255        return Err(crate::Error::InvalidProgram {
256            message: format!(
257                "PJRT single-output execution expected 1 output, got {}",
258                outputs.len()
259            ),
260        });
261    }
262    Ok(outputs.remove(0))
263}
264
265#[cfg(test)]
266mod tests {
267    use super::{single_output_tensor, XlaExecutor, XlaExecutorOptions};
268    use crate::Error;
269    use tenferro_runtime::{GraphCompiler, TracedTensor};
270    use tenferro_tensor::Tensor;
271
272    #[test]
273    fn executor_options_and_debug_are_directly_covered() {
274        let options = XlaExecutorOptions::default();
275        let executor = XlaExecutor::new(options);
276
277        assert_eq!(executor.options(), options);
278        assert!(!executor.has_loaded_pjrt_plugin());
279        assert_eq!(format!("{options:?}"), "XlaExecutorOptions");
280        assert!(format!("{executor:?}").contains("has_loaded_pjrt_plugin"));
281    }
282
283    #[test]
284    fn default_executor_reports_missing_pjrt_before_dispatch() {
285        let x = TracedTensor::from_vec_col_major(vec![1], vec![2.0_f64]).unwrap();
286        let mut compiler = GraphCompiler::new();
287        let program = compiler.compile(&x.neg().unwrap()).unwrap();
288        let input = Tensor::from_vec_col_major(vec![1], vec![2.0_f64]).unwrap();
289
290        let err = XlaExecutor::default()
291            .run_many_with_inputs(&program, &[&input])
292            .unwrap_err();
293        assert!(matches!(
294            err,
295            Error::PjrtFeatureDisabled | Error::PjrtPluginNotLoaded
296        ));
297        let err = XlaExecutor::default()
298            .run_with_inputs(&program, &[&input])
299            .unwrap_err();
300        assert!(matches!(
301            err,
302            Error::PjrtFeatureDisabled | Error::PjrtPluginNotLoaded
303        ));
304    }
305
306    #[test]
307    fn single_output_tensor_rejects_zero_or_multiple_outputs() {
308        let tensor = Tensor::from_vec_col_major(vec![1], vec![1.0_f64]).unwrap();
309        assert_eq!(
310            single_output_tensor(vec![tensor.clone()])
311                .unwrap()
312                .as_slice::<f64>()
313                .unwrap(),
314            &[1.0]
315        );
316
317        let err = single_output_tensor(Vec::new()).unwrap_err();
318        assert!(
319            err.to_string().contains("got 0"),
320            "expected zero-output error, got {err:?}"
321        );
322
323        let err = single_output_tensor(vec![tensor.clone(), tensor]).unwrap_err();
324        assert!(
325            err.to_string().contains("got 2"),
326            "expected multi-output error, got {err:?}"
327        );
328    }
329}