1use std::collections::HashMap;
25
26use rlx_driver::Device;
27use rlx_ir::{Graph, Node, NodeId, Op, OpKind};
28
29use crate::CompileOptions;
30use crate::compiled::CompiledGraph;
31use crate::registry::backend_for;
32use crate::session::Session;
33
34#[derive(Debug, Clone)]
37pub struct DeviceMap {
38 devices: Vec<Device>,
39}
40
41fn is_source(op: &Op) -> bool {
42 matches!(op, Op::Input { .. } | Op::Param { .. })
43}
44
45impl DeviceMap {
46 pub fn single(graph: &Graph, device: Device) -> Self {
48 Self {
49 devices: vec![device; graph.len()],
50 }
51 }
52
53 pub fn from_fn(graph: &Graph, f: impl Fn(&Node) -> Device) -> Self {
55 Self {
56 devices: graph.nodes().iter().map(&f).collect(),
57 }
58 }
59
60 pub fn auto_fallback(graph: &Graph, primary: Device, fallback: Device) -> Self {
67 let supported: Vec<OpKind> = backend_for(primary)
68 .map(|b| b.supported_ops().to_vec())
69 .unwrap_or_default();
70 let accepts = |op: &Op| supported.is_empty() || supported.contains(&op.kind());
71 let devices = graph
72 .nodes()
73 .iter()
74 .map(|n| {
75 if is_source(&n.op) || accepts(&n.op) {
76 primary
77 } else {
78 fallback
79 }
80 })
81 .collect();
82 Self { devices }
83 }
84
85 #[inline]
86 fn get(&self, id: NodeId) -> Device {
87 self.devices[id.0 as usize]
88 }
89}
90
91struct Segment {
94 device: Device,
95 compiled: CompiledGraph,
96 input_ids: Vec<NodeId>,
98 input_names: Vec<String>,
100 output_ids: Vec<NodeId>,
103}
104
105pub struct HeteroExecutable {
109 segments: Vec<Segment>,
110 input_name_to_id: HashMap<String, NodeId>,
111 param_name_to_id: HashMap<String, NodeId>,
112 graph_outputs: Vec<NodeId>,
113 params: HashMap<NodeId, Vec<f32>>,
114}
115
116impl HeteroExecutable {
117 pub fn compile(graph: &Graph, map: &DeviceMap, options: &CompileOptions) -> Self {
119 let n = graph.len();
124 let mut seg_of: Vec<Option<usize>> = vec![None; n];
125 let mut seg_device: Vec<Device> = Vec::new();
126 let mut cur_dev: Option<Device> = None;
127 for node in graph.nodes() {
128 if is_source(&node.op) {
129 continue;
130 }
131 let d = map.get(node.id);
132 if cur_dev != Some(d) {
133 seg_device.push(d);
134 cur_dev = Some(d);
135 }
136 seg_of[node.id.0 as usize] = Some(seg_device.len() - 1);
137 }
138
139 let mut is_seg_output: Vec<bool> = vec![false; n];
142 for node in graph.nodes() {
143 let Some(ns) = seg_of[node.id.0 as usize] else {
144 continue;
145 };
146 for &inp in &node.inputs {
147 if let Some(ps) = seg_of[inp.0 as usize]
148 && ps != ns
149 {
150 is_seg_output[inp.0 as usize] = true;
151 }
152 }
153 }
154 for &out in &graph.outputs {
155 is_seg_output[out.0 as usize] = true;
156 }
157
158 let mut segments = Vec::with_capacity(seg_device.len());
160 for (s, &device) in seg_device.iter().enumerate() {
161 let mut sub = Graph::new(format!("{}#seg{s}", graph.name));
162 let mut remap: HashMap<NodeId, NodeId> = HashMap::new();
163 let mut input_ids: Vec<NodeId> = Vec::new();
164 let mut input_names: Vec<String> = Vec::new();
165
166 for node in graph.nodes() {
167 if seg_of[node.id.0 as usize] != Some(s) {
168 continue;
169 }
170 let mut new_inputs = Vec::with_capacity(node.inputs.len());
171 for &inp in &node.inputs {
172 let nid = if let Some(&existing) = remap.get(&inp) {
173 existing
174 } else {
175 let name = format!("v{}", inp.0);
178 let id = sub.append_node(
179 Op::Input { name: name.clone() },
180 Vec::new(),
181 graph.shape(inp).clone(),
182 Some(name.clone()),
183 );
184 remap.insert(inp, id);
185 input_ids.push(inp);
186 input_names.push(name);
187 id
188 };
189 new_inputs.push(nid);
190 }
191 let new_id = sub.append_node(
192 node.op.clone(),
193 new_inputs,
194 node.shape.clone(),
195 node.name.clone(),
196 );
197 remap.insert(node.id, new_id);
198 }
199
200 let mut output_ids: Vec<NodeId> = Vec::new();
202 let mut sub_outputs: Vec<NodeId> = Vec::new();
203 for node in graph.nodes() {
204 if seg_of[node.id.0 as usize] == Some(s) && is_seg_output[node.id.0 as usize] {
205 output_ids.push(node.id);
206 sub_outputs.push(remap[&node.id]);
207 }
208 }
209 sub.set_outputs(sub_outputs);
210
211 let compiled = Session::new(device).compile_with(sub, options);
212 segments.push(Segment {
213 device,
214 compiled,
215 input_ids,
216 input_names,
217 output_ids,
218 });
219 }
220
221 let mut input_name_to_id = HashMap::new();
223 let mut param_name_to_id = HashMap::new();
224 for node in graph.nodes() {
225 match &node.op {
226 Op::Input { name } => {
227 input_name_to_id.insert(name.clone(), node.id);
228 }
229 Op::Param { name } => {
230 param_name_to_id.insert(name.clone(), node.id);
231 }
232 _ => {}
233 }
234 }
235
236 Self {
237 segments,
238 input_name_to_id,
239 param_name_to_id,
240 graph_outputs: graph.outputs.clone(),
241 params: HashMap::new(),
242 }
243 }
244
245 pub fn with_fallback(
248 graph: &Graph,
249 primary: Device,
250 fallback: Device,
251 options: &CompileOptions,
252 ) -> Self {
253 let map = DeviceMap::auto_fallback(graph, primary, fallback);
254 Self::compile(graph, &map, options)
255 }
256
257 pub fn set_param(&mut self, name: &str, data: &[f32]) {
259 if let Some(&id) = self.param_name_to_id.get(name) {
260 self.params.insert(id, data.to_vec());
261 }
262 }
263
264 pub fn run(&mut self, inputs: &[(&str, &[f32])]) -> Vec<Vec<f32>> {
268 let mut values: HashMap<NodeId, Vec<f32>> = HashMap::new();
269 for (name, data) in inputs {
270 if let Some(&id) = self.input_name_to_id.get(*name) {
271 values.insert(id, data.to_vec());
272 }
273 }
274 for (id, data) in &self.params {
275 values.insert(*id, data.clone());
276 }
277
278 for seg in &mut self.segments {
279 let run_inputs: Vec<(&str, &[f32])> = seg
280 .input_ids
281 .iter()
282 .zip(&seg.input_names)
283 .map(|(id, name)| {
284 (
285 name.as_str(),
286 values.get(id).map(Vec::as_slice).unwrap_or(&[]),
287 )
288 })
289 .collect();
290 let outs = seg.compiled.run(&run_inputs);
291 for (i, &oid) in seg.output_ids.iter().enumerate() {
292 values.insert(oid, outs.get(i).cloned().unwrap_or_default());
293 }
294 }
295
296 self.graph_outputs
297 .iter()
298 .map(|id| values.get(id).cloned().unwrap_or_default())
299 .collect()
300 }
301
302 pub fn num_segments(&self) -> usize {
304 self.segments.len()
305 }
306
307 pub fn segment_devices(&self) -> Vec<Device> {
309 self.segments.iter().map(|s| s.device).collect()
310 }
311}
312
313#[cfg(test)]
314mod tests {
315 use super::*;
316 use rlx_ir::infer::GraphExt;
317 use rlx_ir::{DType, Shape};
318
319 fn alias_cpu_to(device: Device) {
322 crate::registry::register_backend(device, || {
323 backend_for(Device::Cpu).expect("cpu backend (enable feature `cpu`)")
324 });
325 }
326
327 fn build() -> (Graph, NodeId, NodeId, NodeId) {
329 let f = DType::F32;
330 let mut g = Graph::new("hetero_test");
331 let x = g.input("x", Shape::new(&[2, 3], f));
332 let w = g.param("w", Shape::new(&[3, 4], f));
333 let b = g.param("b", Shape::new(&[2, 4], f));
334 let mm = g.matmul(x, w, Shape::new(&[2, 4], f));
335 let z = g.add(mm, b);
336 let two = g.add(z, z); g.set_outputs(vec![two]);
338 (g, w, b, two)
339 }
340
341 fn inputs() -> (Vec<f32>, Vec<f32>, Vec<f32>) {
342 let x = (0..6).map(|i| i as f32 * 0.1).collect();
343 let w = (0..12).map(|i| (i as f32 * 0.05) - 0.3).collect();
344 let b = vec![0.1; 8];
345 (x, w, b)
346 }
347
348 #[test]
349 fn single_segment_matches_plain_compile() {
350 let (g, _w, _b, _) = build();
351 let (x, w, b) = inputs();
352 let mut plain = Session::new(Device::Cpu).compile_with(g.clone(), &CompileOptions::new());
354 plain.set_param("w", &w);
355 plain.set_param("b", &b);
356 let want = plain.run(&[("x", &x)]);
357
358 let map = DeviceMap::single(&g, Device::Cpu);
360 let mut hx = HeteroExecutable::compile(&g, &map, &CompileOptions::new());
361 assert_eq!(hx.num_segments(), 1);
362 hx.set_param("w", &w);
363 hx.set_param("b", &b);
364 let got = hx.run(&[("x", &x)]);
365 assert_eq!(got, want);
366 }
367
368 #[test]
369 fn multi_device_split_matches_single() {
370 alias_cpu_to(Device::Vulkan);
373 let (g, _w, _b, two) = build();
374 let (x, w, b) = inputs();
375
376 let mut plain = Session::new(Device::Cpu).compile_with(g.clone(), &CompileOptions::new());
377 plain.set_param("w", &w);
378 plain.set_param("b", &b);
379 let want = plain.run(&[("x", &x)]);
380
381 let map = DeviceMap::from_fn(&g, |node| {
384 if node.id == two {
385 Device::Vulkan
386 } else {
387 Device::Cpu
388 }
389 });
390 let mut hx = HeteroExecutable::compile(&g, &map, &CompileOptions::new());
391 assert_eq!(hx.num_segments(), 2, "expected a CPU + aliased split");
392 assert_eq!(hx.segment_devices(), vec![Device::Cpu, Device::Vulkan]);
393 hx.set_param("w", &w);
394 hx.set_param("b", &b);
395 let got = hx.run(&[("x", &x)]);
396 for (a, e) in got[0].iter().zip(&want[0]) {
397 assert!((a - e).abs() < 1e-5, "split mismatch: {a} vs {e}");
398 }
399 }
400
401 #[cfg(feature = "cuda")]
405 #[test]
406 fn real_cpu_cuda_split_matches_single() {
407 if !crate::device_ext::is_available(Device::Cuda) {
410 eprintln!("skipping real_cpu_cuda_split_matches_single: no CUDA device");
411 return;
412 }
413 let (g, _w, _b, two) = build();
414 let (x, w, b) = inputs();
415
416 let mut plain = Session::new(Device::Cpu).compile_with(g.clone(), &CompileOptions::new());
417 plain.set_param("w", &w);
418 plain.set_param("b", &b);
419 let want = plain.run(&[("x", &x)]);
420
421 let map = DeviceMap::from_fn(&g, |node| {
422 if node.id == two {
423 Device::Cpu
424 } else {
425 Device::Cuda
426 }
427 });
428 let mut hx = HeteroExecutable::compile(&g, &map, &CompileOptions::new());
429 assert_eq!(hx.num_segments(), 2);
430 assert_eq!(hx.segment_devices(), vec![Device::Cuda, Device::Cpu]);
431 hx.set_param("w", &w);
432 hx.set_param("b", &b);
433 let got = hx.run(&[("x", &x)]);
434 for (a, e) in got[0].iter().zip(&want[0]) {
435 assert!((a - e).abs() < 1e-4, "cpu+cuda split mismatch: {a} vs {e}");
436 }
437 }
438
439 #[test]
440 fn auto_fallback_all_cpu_is_single_segment() {
441 let (g, _, _, _) = build();
443 let map = DeviceMap::auto_fallback(&g, Device::Cpu, Device::Cpu);
444 let hx = HeteroExecutable::compile(&g, &map, &CompileOptions::new());
445 assert_eq!(hx.num_segments(), 1);
446 }
447}