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