1use std::collections::HashMap;
8
9use rlx_driver::Device;
10use rlx_ir::{DType, Graph, Op};
11
12use crate::compiled::CompiledGraph;
13use crate::cost::fastest_device_for_with_policy;
14use crate::device_bench::{DeviceBenchResult, benchmark_devices, warm_all};
15use crate::device_ext::is_available;
16use crate::device_policy::{
17 DeviceCandidate, DeviceFallbackError, DevicePickStrategy, DevicePolicy, device_chain_from_env,
18 device_report, devices_for_with_policy, resolve_device, resolve_device_chain,
19};
20use crate::session::Session;
21
22pub fn graph_param_names(graph: &Graph) -> Vec<String> {
24 graph
25 .nodes()
26 .iter()
27 .filter_map(|n| match &n.op {
28 Op::Param { name } => Some(name.clone()),
29 _ => None,
30 })
31 .collect()
32}
33
34#[derive(Debug, Clone)]
35enum CachedParam {
36 F32(Vec<f32>),
37 Typed { bytes: Vec<u8>, dtype: DType },
38}
39
40fn apply_cached_params(compiled: &mut CompiledGraph, params: &HashMap<String, CachedParam>) {
41 for (name, param) in params {
42 match param {
43 CachedParam::F32(data) => compiled.set_param(name, data),
44 CachedParam::Typed { bytes, dtype } => compiled.set_param_typed(name, bytes, *dtype),
45 }
46 }
47}
48
49pub struct GraphDevices {
51 graph: Graph,
52 policy: DevicePolicy,
53 pick: DevicePickStrategy,
54 supported: Vec<Device>,
55 params: HashMap<String, CachedParam>,
56 benchmark_winner: Option<Device>,
57 cache: HashMap<Device, CompiledGraph>,
58}
59
60impl GraphDevices {
61 pub fn new(graph: Graph) -> Self {
62 Self::with_policy(graph, DevicePolicy::default())
63 }
64
65 pub fn with_policy(graph: Graph, policy: DevicePolicy) -> Self {
66 let pick = policy.pick_strategy();
67 let supported = devices_for_with_policy(&graph, &policy);
68 Self {
69 graph,
70 policy,
71 pick,
72 supported,
73 params: HashMap::new(),
74 benchmark_winner: None,
75 cache: HashMap::new(),
76 }
77 }
78
79 pub fn from_env(graph: Graph) -> Self {
80 Self::with_policy(graph, DevicePolicy::from_env())
81 }
82
83 pub fn policy(&self) -> &DevicePolicy {
84 &self.policy
85 }
86
87 pub fn graph(&self) -> &Graph {
88 &self.graph
89 }
90
91 pub fn devices(&self) -> &[Device] {
92 &self.supported
93 }
94
95 pub fn report(&self) -> Vec<DeviceCandidate> {
96 device_report(&self.graph, &self.policy)
97 }
98
99 pub fn fastest(&self) -> Device {
100 fastest_device_for_with_policy(&self.graph, &self.policy)
101 }
102
103 pub fn resolve(&self, hint: Option<Device>) -> Result<Device, String> {
104 resolve_device(&self.graph, hint, &self.policy)
105 }
106
107 pub fn resolve_chain(&self, hint: Option<Device>) -> Result<Device, String> {
109 if let Some(device) = hint {
110 return self.resolve(Some(device));
111 }
112 let chain = device_chain_from_env();
113 if chain.is_empty() {
114 return self.resolve(None);
115 }
116 resolve_device_chain(&self.graph, &chain, &self.policy)
117 }
118
119 pub fn set_param(&mut self, name: &str, data: &[f32]) {
121 self.params
122 .insert(name.to_string(), CachedParam::F32(data.to_vec()));
123 for compiled in self.cache.values_mut() {
124 compiled.set_param(name, data);
125 }
126 }
127
128 pub fn set_param_typed(&mut self, name: &str, data: &[u8], dtype: DType) {
130 self.params.insert(
131 name.to_string(),
132 CachedParam::Typed {
133 bytes: data.to_vec(),
134 dtype,
135 },
136 );
137 for compiled in self.cache.values_mut() {
138 compiled.set_param_typed(name, data, dtype);
139 }
140 }
141
142 pub fn sync_params_to_all(&mut self) {
144 for compiled in self.cache.values_mut() {
145 apply_cached_params(compiled, &self.params);
146 }
147 }
148
149 pub fn resolve_with_inputs(
151 &mut self,
152 hint: Option<Device>,
153 inputs: &[(&str, &[f32])],
154 ) -> Result<Device, String> {
155 if hint.is_some() {
156 return self.resolve(hint);
157 }
158 match self.pick {
159 DevicePickStrategy::CostModel => self.resolve(None),
160 DevicePickStrategy::Benchmark { runs } => {
161 if let Some(device) = self.benchmark_winner {
162 return Ok(device);
163 }
164 let ranked = self.benchmark(inputs, runs)?;
165 let device = ranked
166 .first()
167 .map(|r| r.device)
168 .unwrap_or_else(|| self.fastest());
169 self.benchmark_winner = Some(device);
170 Ok(device)
171 }
172 }
173 }
174
175 pub fn compile(&mut self, device: Device) -> Result<&mut CompiledGraph, String> {
176 Self::ensure_supported(&self.supported, device)?;
177 if !self.cache.contains_key(&device) {
178 let mut compiled = Session::new(device).compile(self.graph.clone());
179 apply_cached_params(&mut compiled, &self.params);
180 self.cache.insert(device, compiled);
181 }
182 Ok(self.cache.get_mut(&device).expect("just inserted"))
183 }
184
185 pub fn compile_fastest(&mut self) -> Result<&mut CompiledGraph, String> {
186 self.compile(self.fastest())
187 }
188
189 pub fn compile_resolved(&mut self, hint: Option<Device>) -> Result<&mut CompiledGraph, String> {
190 self.compile(self.resolve(hint)?)
191 }
192
193 pub fn compile_chain(&mut self, hint: Option<Device>) -> Result<&mut CompiledGraph, String> {
194 self.compile(self.resolve_chain(hint)?)
195 }
196
197 pub fn warm_all(&mut self) -> Result<Vec<Device>, String> {
198 warm_all(self)
199 }
200
201 pub fn benchmark(
202 &mut self,
203 inputs: &[(&str, &[f32])],
204 runs: usize,
205 ) -> Result<Vec<DeviceBenchResult>, String> {
206 benchmark_devices(self, inputs, runs)
207 }
208
209 pub fn run(
210 &mut self,
211 device: Device,
212 inputs: &[(&str, &[f32])],
213 ) -> Result<Vec<Vec<f32>>, String> {
214 Ok(self.compile(device)?.run(inputs))
215 }
216
217 pub fn run_resolved(
218 &mut self,
219 hint: Option<Device>,
220 inputs: &[(&str, &[f32])],
221 ) -> Result<Vec<Vec<f32>>, String> {
222 Ok(self.compile_resolved(hint)?.run(inputs))
223 }
224
225 pub fn run_fastest(&mut self, inputs: &[(&str, &[f32])]) -> Result<Vec<Vec<f32>>, String> {
226 Ok(self.compile_fastest()?.run(inputs))
227 }
228
229 pub fn run_try(
231 &mut self,
232 chain: &[Device],
233 inputs: &[(&str, &[f32])],
234 ) -> Result<(Device, Vec<Vec<f32>>), DeviceFallbackError> {
235 let viable: Vec<Device> = self.devices().to_vec();
236 let mut attempts = Vec::new();
237 for &device in chain {
238 if !viable.contains(&device) {
239 attempts.push((device, "not viable for this graph under policy".into()));
240 continue;
241 }
242 match self.run(device, inputs) {
243 Ok(value) => return Ok((device, value)),
244 Err(err) => attempts.push((device, err)),
245 }
246 }
247 if attempts.is_empty() {
248 attempts.push((Device::Cpu, "empty fallback chain".into()));
249 }
250 Err(DeviceFallbackError { attempts })
251 }
252
253 pub fn run_chain(
255 &mut self,
256 hint: Option<Device>,
257 inputs: &[(&str, &[f32])],
258 ) -> Result<(Device, Vec<Vec<f32>>), DeviceFallbackError> {
259 if let Some(device) = hint {
260 self.run(device, inputs)
261 .map(|v| (device, v))
262 .map_err(|e| DeviceFallbackError {
263 attempts: vec![(device, e)],
264 })
265 } else {
266 let chain = device_chain_from_env();
267 if chain.is_empty() {
268 let device = self.resolve(None).map_err(|e| DeviceFallbackError {
269 attempts: vec![(Device::Cpu, e)],
270 })?;
271 self.run(device, inputs)
272 .map(|v| (device, v))
273 .map_err(|e| DeviceFallbackError {
274 attempts: vec![(device, e)],
275 })
276 } else {
277 self.run_try(&chain, inputs)
278 }
279 }
280 }
281
282 pub fn compile_resolved_with_inputs(
283 &mut self,
284 hint: Option<Device>,
285 inputs: &[(&str, &[f32])],
286 ) -> Result<&mut CompiledGraph, String> {
287 let device = self.resolve_with_inputs(hint, inputs)?;
288 self.compile(device)
289 }
290
291 pub fn run_resolved_with_inputs(
292 &mut self,
293 hint: Option<Device>,
294 inputs: &[(&str, &[f32])],
295 ) -> Result<Vec<Vec<f32>>, String> {
296 Ok(self.compile_resolved_with_inputs(hint, inputs)?.run(inputs))
297 }
298
299 pub fn invalidate_cache(&mut self) {
300 self.cache.clear();
301 self.benchmark_winner = None;
302 self.supported = devices_for_with_policy(&self.graph, &self.policy);
303 }
304
305 pub fn set_policy(&mut self, policy: DevicePolicy) {
306 self.policy = policy.clone();
307 self.pick = policy.pick_strategy();
308 self.invalidate_cache();
309 }
310
311 fn ensure_supported(supported: &[Device], device: Device) -> Result<(), String> {
312 if !is_available(device) {
313 return Err(format!(
314 "device {device} is not available — enable the matching Cargo feature"
315 ));
316 }
317 if !supported.contains(&device) {
318 return Err(format!(
319 "device {device} cannot lower this graph under the active policy"
320 ));
321 }
322 Ok(())
323 }
324}
325
326#[cfg(test)]
327mod tests {
328 use super::*;
329 use rlx_ir::{DType, Shape};
330
331 fn identity_graph() -> Graph {
332 let mut g = Graph::new("id");
333 let x = g.input("x", Shape::new(&[4], DType::F32));
334 g.set_outputs(vec![x]);
335 g
336 }
337
338 #[test]
339 fn set_param_applies_to_new_compile() {
340 let mut g = Graph::new("p");
341 let x = g.input("x", Shape::new(&[2], DType::F32));
342 let w = g.param("w", Shape::new(&[2], DType::F32));
343 let y = g.binary(
344 rlx_ir::op::BinaryOp::Add,
345 x,
346 w,
347 Shape::new(&[2], DType::F32),
348 );
349 g.set_outputs(vec![y]);
350
351 let mut runner = GraphDevices::new(g);
352 runner.set_param("w", &[1.0, 2.0]);
353 let out = runner.run(Device::Cpu, &[("x", &[3.0, 4.0])]).unwrap();
354 assert_eq!(out[0], vec![4.0, 6.0]);
355 }
356
357 #[test]
358 fn run_on_cpu_roundtrip() {
359 let mut runner = GraphDevices::new(identity_graph());
360 let out = runner
361 .run(Device::Cpu, &[("x", &[1.0, 2.0, 3.0, 4.0])])
362 .expect("cpu run");
363 assert_eq!(out[0], vec![1.0, 2.0, 3.0, 4.0]);
364 }
365
366 #[test]
367 fn run_try_falls_back_to_cpu() {
368 let mut runner = GraphDevices::new(identity_graph());
369 let chain: Vec<Device> = match [Device::Cuda, Device::Rocm, Device::Vulkan, Device::Metal]
374 .into_iter()
375 .find(|d| !crate::is_available(*d))
376 {
377 Some(unavailable) => vec![unavailable, Device::Cpu],
378 None => vec![Device::Cpu],
379 };
380 let (dev, out) = runner
381 .run_try(&chain, &[("x", &[1.0, 2.0, 3.0, 4.0])])
382 .expect("fallback");
383 assert_eq!(dev, Device::Cpu);
384 assert_eq!(out[0][0], 1.0);
385 }
386}