1use std::collections::{HashMap, HashSet};
39use std::sync::mpsc::{Sender, channel};
40use std::sync::{Arc, RwLock};
41use std::thread;
42
43use fuel_graph::opt::{execution_plan, insert_safety_copies};
44use fuel_graph::registry::FusedOpParams;
45use fuel_graph::{Graph, Node, NodeId, Op, PickedRoute};
46use fuel_ir::dispatch::OpKind;
47use fuel_ir::probe::BackendId;
48use fuel_ir::{DType, DeviceLocation, Error, Layout, Result, SymEnv};
49
50use crate::compiled::{
51 CompiledNode, CompletionHandle, compile_node, execute_compiled_with_wait_hint,
52};
53use crate::dispatch::global_bindings;
54use crate::kernel::{KernelBindingTable, KernelDTypes, MatmulM, OpParams};
55use crate::optimize::OptimizedGraph;
56use crate::ranker::{BackendRuntimeLookup, RuntimeSelector, resolve_branch};
57#[cfg(test)]
60use crate::ranker::pick_route;
61use fuel_memory::Storage;
62
63enum OrderSource<'a> {
90 Default,
91 Optimized {
92 optimized: &'a OptimizedGraph,
93 route: Option<&'a PickedRoute>,
96 },
97 Streaming {
108 optimized: &'a OptimizedGraph,
109 pick: &'a StreamingPick,
110 },
111 Cached {
128 optimized: &'a OptimizedGraph,
129 order_cache: &'a std::sync::OnceLock<Vec<NodeId>>,
130 },
131}
132
133#[derive(Clone)]
149pub(crate) struct StreamingPick {
150 pub selector: Arc<dyn RuntimeSelector>,
155}
156
157pub type StorageCache = HashMap<NodeId, Arc<RwLock<Storage>>>;
161
162pub struct PersistentOutputs {
178 map: HashMap<NodeId, Arc<RwLock<Storage>>>,
179 mode: PersistentMode,
180}
181
182#[derive(Clone, Copy, PartialEq, Eq)]
183enum PersistentMode {
184 Record,
186 Reuse,
188}
189
190impl PersistentOutputs {
191 pub fn recording() -> Self {
193 Self {
194 map: HashMap::new(),
195 mode: PersistentMode::Record,
196 }
197 }
198
199 pub fn reuse(map: HashMap<NodeId, Arc<RwLock<Storage>>>) -> Self {
202 Self {
203 map,
204 mode: PersistentMode::Reuse,
205 }
206 }
207
208 pub fn into_map(self) -> HashMap<NodeId, Arc<RwLock<Storage>>> {
211 self.map
212 }
213}
214
215#[cfg(feature = "cuda")]
292fn op_kind_is_capture_writeinto(op: OpKind) -> bool {
293 matches!(
294 op,
295 OpKind::AddElementwise
297 | OpKind::SubElementwise
298 | OpKind::MulElementwise
299 | OpKind::DivElementwise
300 | OpKind::MaximumElementwise
301 | OpKind::MinimumElementwise
302 | OpKind::PowElementwise
303 | OpKind::RemElementwise
304 | OpKind::SoftmaxLastDim
306 | OpKind::LogSoftmaxLastDim
307 | OpKind::Affine
309 | OpKind::MatMul
312 | OpKind::IndexSelect
314 | OpKind::RmsNormLastDim
317 | OpKind::SiluElementwise
319 | OpKind::GeluTanhElementwise
320 | OpKind::GeluElementwise
321 | OpKind::NegElementwise
325 | OpKind::Concat
329 | OpKind::Rope
332 )
333}
334
335#[cfg(feature = "cuda")]
346pub struct CapturedDecode {
347 pub run: fuel_cuda_backend::CapturedRun,
349 pub output: Arc<RwLock<Storage>>,
351 pub output_layout: Layout,
353 pub persistent: HashMap<NodeId, Arc<RwLock<Storage>>>,
356 pub retained_inputs: Vec<Arc<RwLock<Storage>>>,
364}
365
366#[cfg(feature = "cuda")]
386pub struct CapturedDecodeSession {
387 device: fuel_cuda_backend::CudaDevice,
388 captured: CapturedDecode,
389 per_token_inputs: HashMap<NodeId, Arc<RwLock<Storage>>>,
393 _retained_inputs: Vec<Arc<RwLock<Storage>>>,
399}
400
401#[cfg(feature = "cuda")]
402impl CapturedDecodeSession {
403 pub fn capture(
413 graph: Arc<RwLock<Graph>>,
414 target: NodeId,
415 inputs: StorageCache,
416 per_token_input_ids: &[NodeId],
417 sym_env: SymEnv,
418 ) -> Result<Self> {
419 use fuel_memory::BackendStorage;
420 let retained_inputs: Vec<Arc<RwLock<Storage>>> = inputs.values().cloned().collect();
424 let mut per_token_inputs = HashMap::with_capacity(per_token_input_ids.len());
425 for &id in per_token_input_ids {
426 let arc = inputs.get(&id).cloned().ok_or_else(|| {
427 Error::Msg(format!(
428 "CapturedDecodeSession::capture: per-token input {:?} not in inputs",
429 id,
430 ))
431 .bt()
432 })?;
433 per_token_inputs.insert(id, arc);
434 }
435 let device = {
437 let any = per_token_inputs
438 .values()
439 .next()
440 .or_else(|| inputs.values().next())
441 .ok_or_else(|| {
442 Error::Msg("CapturedDecodeSession::capture: no inputs".into()).bt()
443 })?;
444 let guard = any.read().map_err(|_| poisoned("input storage"))?;
445 match &guard.inner {
446 BackendStorage::Cuda(c) => c.device().clone(),
447 other => {
448 return Err(Error::Msg(format!(
449 "CapturedDecodeSession::capture: input is BackendStorage::{:?}, not Cuda",
450 std::mem::discriminant(other),
451 ))
452 .bt());
453 }
454 }
455 };
456 let captured = PipelinedExecutor::capture_decode(graph, target, inputs, sym_env)?;
457 Ok(Self {
458 device,
459 captured,
460 per_token_inputs,
461 _retained_inputs: retained_inputs,
462 })
463 }
464
465 pub fn node_outputs(&self) -> &HashMap<NodeId, Arc<RwLock<Storage>>> {
496 &self.captured.persistent
497 }
498
499 pub fn replay_token(&self, updates: &[(NodeId, &[u8])]) -> Result<Arc<RwLock<Storage>>> {
500 use fuel_memory::BackendStorage;
501 for (id, bytes) in updates {
502 let arc = self.per_token_inputs.get(id).ok_or_else(|| {
503 Error::Msg(format!(
504 "CapturedDecodeSession::replay_token: {:?} is not a per-token input",
505 id,
506 ))
507 .bt()
508 })?;
509 let guard = arc.read().map_err(|_| poisoned("per-token input"))?;
510 match &guard.inner {
511 BackendStorage::Cuda(c) => c.write_from_host(bytes)?,
512 other => {
513 return Err(Error::Msg(format!(
514 "CapturedDecodeSession::replay_token: input {:?} is \
515 BackendStorage::{:?}, not Cuda",
516 id,
517 std::mem::discriminant(other),
518 ))
519 .bt());
520 }
521 }
522 }
523 self.captured.run.replay(&self.device)?;
524 self.device.synchronize()?;
525 Ok(Arc::clone(&self.captured.output))
526 }
527
528 pub fn output(&self) -> &Arc<RwLock<Storage>> {
532 &self.captured.output
533 }
534
535 pub fn output_layout(&self) -> &Layout {
537 &self.captured.output_layout
538 }
539}
540
541enum WorkItemKind {
544 ConstAdopt,
547 ViewOf { input: NodeId },
552 ContiguizeOf { input: NodeId },
559 Kernel,
562 Iota { len: usize },
568 ReleaseMarker,
579 WriteSlice { dest: NodeId, source: NodeId },
597 WriteSliceRotating {
603 dest: NodeId,
604 source: NodeId,
605 position: NodeId,
606 },
607 WriteSliceDoff {
614 dest: NodeId,
615 source: NodeId,
616 offset: NodeId,
617 },
618 Copy { target_location: DeviceLocation },
636 Move { target_location: DeviceLocation },
660 Alloc { target_location: DeviceLocation },
678 ZeroFill,
697 InplaceKernel {
717 target_idx: usize,
721 },
722 SlotView { producer: NodeId },
732 SlotOwn { producer: NodeId, slot: u32 },
741}
742
743fn work_item_kind_name(kind: &WorkItemKind) -> &'static str {
749 match kind {
750 WorkItemKind::ConstAdopt => "item.ConstAdopt",
751 WorkItemKind::ViewOf { .. } => "item.ViewOf",
752 WorkItemKind::ContiguizeOf { .. } => "item.ContiguizeOf",
753 WorkItemKind::Kernel => "item.Kernel",
754 WorkItemKind::Iota { .. } => "item.Iota",
755 WorkItemKind::ReleaseMarker => "item.ReleaseMarker",
756 WorkItemKind::WriteSlice { .. } => "item.WriteSlice",
757 WorkItemKind::WriteSliceRotating { .. } => "item.WriteSliceRotating",
758 WorkItemKind::WriteSliceDoff { .. } => "item.WriteSliceDoff",
759 WorkItemKind::Copy { .. } => "item.Copy",
760 WorkItemKind::Move { .. } => "item.Move",
761 WorkItemKind::Alloc { .. } => "item.Alloc",
762 WorkItemKind::ZeroFill => "item.ZeroFill",
763 WorkItemKind::InplaceKernel { .. } => "item.InplaceKernel",
764 WorkItemKind::SlotView { .. } => "item.SlotView",
765 WorkItemKind::SlotOwn { .. } => "item.SlotOwn",
766 }
767}
768
769fn is_backend_neutral(kind: &WorkItemKind) -> bool {
782 matches!(
783 kind,
784 WorkItemKind::ConstAdopt
785 | WorkItemKind::ViewOf { .. }
786 | WorkItemKind::SlotView { .. }
787 | WorkItemKind::ContiguizeOf { .. }
788 )
789}
790
791struct WorkItem {
792 node_id: NodeId,
793 inputs: Vec<NodeId>,
794 elem_count: usize,
797 dtype: DType,
798 target_backend: BackendId,
799 kind: WorkItemKind,
801 compiled: Option<CompiledNode>,
804 output_layout: Layout,
811 destructive_input: Option<usize>,
818 output_bundle: Option<Arc<[fuel_ir::storage::OutputView]>>,
826}
827
828fn extend_with_side_effect_roots(graph: &Graph, user_roots: &[NodeId]) -> Vec<NodeId> {
837 let side = graph.side_effect_roots();
838 if side.is_empty() {
839 return user_roots.to_vec();
840 }
841 let mut out = Vec::with_capacity(user_roots.len() + side.len());
842 out.extend_from_slice(user_roots);
843 for &s in side {
844 if !out.contains(&s) {
845 out.push(s);
846 }
847 }
848 out
849}
850
851pub struct PipelinedExecutor;
909
910impl PipelinedExecutor {
911 pub fn realize(
934 graph: Arc<RwLock<Graph>>,
935 target: NodeId,
936 inputs: StorageCache,
937 ) -> Result<(Arc<RwLock<Storage>>, Layout)> {
938 Self::realize_inner(
939 graph,
940 target,
941 inputs,
942 OrderSource::Default,
943 SymEnv::default(),
944 )
945 .map(|(s, l, _produced)| (s, l))
946 }
947
948 pub fn realize_with_env(
961 graph: Arc<RwLock<Graph>>,
962 target: NodeId,
963 inputs: StorageCache,
964 sym_env: SymEnv,
965 ) -> Result<(Arc<RwLock<Storage>>, Layout)> {
966 Self::realize_inner(graph, target, inputs, OrderSource::Default, sym_env)
967 .map(|(s, l, _produced)| (s, l))
968 }
969
970 pub fn realize_with_env_producing(
979 graph: Arc<RwLock<Graph>>,
980 target: NodeId,
981 inputs: StorageCache,
982 sym_env: SymEnv,
983 ) -> Result<(Arc<RwLock<Storage>>, Layout, SymEnv)> {
984 Self::realize_inner(graph, target, inputs, OrderSource::Default, sym_env)
985 }
986
987 pub fn realize_with_optimized(
999 graph: Arc<RwLock<Graph>>,
1000 target: NodeId,
1001 inputs: StorageCache,
1002 optimized: &OptimizedGraph,
1003 ) -> Result<(Arc<RwLock<Storage>>, Layout)> {
1004 Self::realize_inner(
1005 graph,
1006 target,
1007 inputs,
1008 OrderSource::Optimized {
1009 optimized,
1010 route: None,
1011 },
1012 SymEnv::default(),
1013 )
1014 .map(|(s, l, _produced)| (s, l))
1015 }
1016
1017 pub fn realize_with_optimized_env(
1024 graph: Arc<RwLock<Graph>>,
1025 target: NodeId,
1026 inputs: StorageCache,
1027 optimized: &OptimizedGraph,
1028 sym_env: SymEnv,
1029 ) -> Result<(Arc<RwLock<Storage>>, Layout)> {
1030 Self::realize_inner(
1031 graph,
1032 target,
1033 inputs,
1034 OrderSource::Optimized {
1035 optimized,
1036 route: None,
1037 },
1038 sym_env,
1039 )
1040 .map(|(s, l, _produced)| (s, l))
1041 }
1042
1043 pub fn realize_with_optimized_route(
1053 graph: Arc<RwLock<Graph>>,
1054 target: NodeId,
1055 inputs: StorageCache,
1056 optimized: &OptimizedGraph,
1057 route: &PickedRoute,
1058 ) -> Result<(Arc<RwLock<Storage>>, Layout)> {
1059 Self::realize_inner(
1060 graph,
1061 target,
1062 inputs,
1063 OrderSource::Optimized {
1064 optimized,
1065 route: Some(route),
1066 },
1067 SymEnv::default(),
1068 )
1069 .map(|(s, l, _produced)| (s, l))
1070 }
1071
1072 pub fn realize_with_optimized_route_env(
1076 graph: Arc<RwLock<Graph>>,
1077 target: NodeId,
1078 inputs: StorageCache,
1079 optimized: &OptimizedGraph,
1080 route: &PickedRoute,
1081 sym_env: SymEnv,
1082 ) -> Result<(Arc<RwLock<Storage>>, Layout)> {
1083 Self::realize_inner(
1084 graph,
1085 target,
1086 inputs,
1087 OrderSource::Optimized {
1088 optimized,
1089 route: Some(route),
1090 },
1091 sym_env,
1092 )
1093 .map(|(s, l, _produced)| (s, l))
1094 }
1095
1096 pub fn realize_with_optimized_picking_env(
1116 graph: Arc<RwLock<Graph>>,
1117 target: NodeId,
1118 inputs: StorageCache,
1119 optimized: &OptimizedGraph,
1120 selector: Option<Arc<dyn RuntimeSelector>>,
1121 lookup: Option<BackendRuntimeLookup>,
1122 sym_env: SymEnv,
1123 ) -> Result<(Arc<RwLock<Storage>>, Layout)> {
1124 match Self::streaming_pick_for(&graph, selector, lookup)? {
1125 Some(pick) => Self::realize_inner(
1126 graph,
1127 target,
1128 inputs,
1129 OrderSource::Streaming {
1130 optimized,
1131 pick: &pick,
1132 },
1133 sym_env,
1134 )
1135 .map(|(s, l, _produced)| (s, l)),
1136 None => Self::realize_with_optimized_env(graph, target, inputs, optimized, sym_env),
1137 }
1138 }
1139
1140 pub fn realize_with_optimized_picking_cached_env(
1153 graph: Arc<RwLock<Graph>>,
1154 target: NodeId,
1155 inputs: StorageCache,
1156 optimized: &OptimizedGraph,
1157 order_cache: &std::sync::OnceLock<Vec<NodeId>>,
1158 selector: Option<Arc<dyn RuntimeSelector>>,
1159 lookup: Option<BackendRuntimeLookup>,
1160 sym_env: SymEnv,
1161 ) -> Result<(Arc<RwLock<Storage>>, Layout)> {
1162 match Self::streaming_pick_for(&graph, selector, lookup)? {
1163 Some(pick) => Self::realize_inner(
1164 graph,
1165 target,
1166 inputs,
1167 OrderSource::Streaming {
1168 optimized,
1169 pick: &pick,
1170 },
1171 sym_env,
1172 )
1173 .map(|(s, l, _produced)| (s, l)),
1174 None => Self::realize_inner(
1175 graph,
1176 target,
1177 inputs,
1178 OrderSource::Cached {
1179 optimized,
1180 order_cache,
1181 },
1182 sym_env,
1183 )
1184 .map(|(s, l, _produced)| (s, l)),
1185 }
1186 }
1187
1188 fn streaming_pick_for(
1201 graph: &Arc<RwLock<Graph>>,
1202 selector: Option<Arc<dyn RuntimeSelector>>,
1203 _lookup: Option<BackendRuntimeLookup>,
1207 ) -> Result<Option<StreamingPick>> {
1208 let Some(selector) = selector else {
1209 return Ok(None);
1210 };
1211 let g = graph
1212 .read()
1213 .map_err(|_| Error::Msg("graph lock poisoned".into()).bt())?;
1214 let has_branch = (0..g.len()).any(|i| matches!(g.node(NodeId(i)).op, Op::Branch { .. }));
1215 if !has_branch {
1216 return Ok(None);
1217 }
1218 Ok(Some(StreamingPick { selector }))
1219 }
1220
1221 #[cfg(feature = "cuda")]
1247 pub fn capture_decode(
1248 graph: Arc<RwLock<Graph>>,
1249 target: NodeId,
1250 inputs: StorageCache,
1251 sym_env: SymEnv,
1252 ) -> Result<CapturedDecode> {
1253 use fuel_memory::BackendStorage;
1254
1255 {
1257 let mut g = graph.write().map_err(|_| poisoned("graph lock"))?;
1258 let effective_roots = extend_with_side_effect_roots(&g, &[target]);
1259 insert_safety_copies(&mut g, &effective_roots);
1260 }
1261 let (compiler_work, _wait_set, seed_layouts) = {
1262 let g = graph.read().map_err(|_| poisoned("graph lock"))?;
1263 let effective_roots = extend_with_side_effect_roots(&g, &[target]);
1264 let (work, wait_set) =
1265 compiler_work_and_wait_set_for(&g, &effective_roots, &OrderSource::Default);
1266 let mut layouts: HashMap<NodeId, Layout> = HashMap::with_capacity(inputs.len());
1267 for &id in inputs.keys() {
1268 layouts.insert(id, g.layout(id));
1269 }
1270 (work, wait_set, layouts)
1271 };
1272
1273 let (tx, rx) = channel::<Result<WorkItem>>();
1276 let graph_for_compiler = Arc::clone(&graph);
1277 let compiler = thread::spawn(move || {
1278 compiler_thread_body(graph_for_compiler, compiler_work, sym_env, tx);
1279 });
1280 let mut items: Vec<WorkItem> = Vec::new();
1281 for item in rx {
1282 items.push(item?);
1283 }
1284 compiler
1285 .join()
1286 .map_err(|_| Error::Msg("compiler thread panicked".to_string()).bt())?;
1287
1288 {
1294 let off: Vec<String> = items
1295 .iter()
1296 .filter_map(|it| match &it.kind {
1297 WorkItemKind::Kernel if it.target_backend != BackendId::Cuda => {
1298 let desc = it
1299 .compiled
1300 .as_ref()
1301 .map(|c| format!("{:?} dtypes={:?}", c.op, c.dtypes))
1302 .unwrap_or_else(|| "?".to_string());
1303 Some(format!(
1304 "node {:?}: {desc} on {:?}",
1305 it.node_id, it.target_backend
1306 ))
1307 }
1308 _ => None,
1309 })
1310 .collect();
1311 if !off.is_empty() {
1312 eprintln!(
1313 "[capture-diag] {} kernel(s) placed OFF Cuda (these block capture): {}",
1314 off.len(),
1315 off.join("; "),
1316 );
1317 }
1318 }
1319
1320 for item in &items {
1323 match &item.kind {
1324 WorkItemKind::Move { .. } => {
1333 return Err(Error::Msg(format!(
1334 "capture_decode: graph has an Op::Move at node {:?}; \
1335 Move's destructive source-release half is driven by \
1336 per-realize cleanup, not per-replay, so capture is \
1337 not supported for Move (out of scope — see the \
1338 `capture_decode` validation loop's Move arm).",
1339 item.node_id,
1340 ))
1341 .bt());
1342 }
1343 WorkItemKind::Copy { target_location } => {
1355 if !matches!(target_location, DeviceLocation::Cuda { .. }) {
1356 return Err(Error::Msg(format!(
1357 "capture_decode: graph has a cross-device Op::Copy at \
1358 node {:?} (target {:?}); capture requires a \
1359 single-device CUDA decode step — only same-device \
1360 CUDA→CUDA copies are capture-safe.",
1361 item.node_id, target_location,
1362 ))
1363 .bt());
1364 }
1365 }
1366 WorkItemKind::Kernel => {
1367 if item.target_backend != BackendId::Cuda {
1368 return Err(Error::Msg(format!(
1369 "capture_decode: kernel node {:?} targets {:?}, not Cuda",
1370 item.node_id, item.target_backend,
1371 ))
1372 .bt());
1373 }
1374 let compiled = item.compiled.as_ref().ok_or_else(|| {
1375 Error::Msg(format!(
1376 "capture_decode: kernel node {:?} has no compiled op",
1377 item.node_id,
1378 ))
1379 .bt()
1380 })?;
1381 let op = compiled.op;
1382 if !op_kind_is_capture_writeinto(op) {
1383 return Err(Error::Msg(format!(
1384 "capture_decode: op {:?} (node {:?}) has no write-into CUDA \
1385 wrapper yet — capture unsupported for this op. Extend \
1386 op_kind_is_capture_writeinto once its write-into variant lands.",
1387 op, item.node_id,
1388 ))
1389 .bt());
1390 }
1391 if op == OpKind::Concat {
1399 if let OpParams::Concat {
1400 input_dim_sizes, ..
1401 } = &compiled.op_params
1402 {
1403 if input_dim_sizes.len() != 2 {
1404 return Err(Error::Msg(format!(
1405 "capture_decode: Concat node {:?} has {} inputs; only \
1406 2-input concat (the rope rotate-half shape) has a \
1407 write-into CUDA path today — N-ary (N>2) concat would \
1408 allocate mid-capture.",
1409 item.node_id,
1410 input_dim_sizes.len(),
1411 ))
1412 .bt());
1413 }
1414 }
1415 }
1416 }
1417 _ => {}
1419 }
1420 }
1421
1422 let mut persistent = PersistentOutputs::recording();
1424 let mut warm_cache: StorageCache = inputs.clone();
1425 let mut warm_layouts = seed_layouts.clone();
1426 let mut warm_syms = SymEnv::new();
1427 for item in &items {
1428 let _handle = execute_work_item(
1429 item,
1430 &mut warm_cache,
1431 &mut warm_layouts,
1432 &mut warm_syms,
1433 false,
1434 Some(&mut persistent),
1435 )
1436 .map_err(|e| with_node_location(&graph, item.node_id, e))?;
1437 }
1438 sync_active_cuda_devices(&warm_cache)?;
1440
1441 let output = warm_cache.get(&target).cloned().ok_or_else(|| {
1442 Error::Msg(format!(
1443 "capture_decode: target {:?} not realized in warm run",
1444 target,
1445 ))
1446 .bt()
1447 })?;
1448 let output_layout = warm_layouts.get(&target).cloned().ok_or_else(|| {
1449 Error::Msg(format!("capture_decode: target {:?} has no layout", target)).bt()
1450 })?;
1451
1452 let device = {
1454 let guard = output.read().map_err(|_| poisoned("output storage"))?;
1455 match &guard.inner {
1456 BackendStorage::Cuda(c) => c.device().clone(),
1457 other => {
1458 return Err(Error::Msg(format!(
1459 "capture_decode: target {:?} is BackendStorage::{:?}, not Cuda",
1460 target,
1461 std::mem::discriminant(other),
1462 ))
1463 .bt());
1464 }
1465 }
1466 };
1467
1468 let mut reuse = PersistentOutputs::reuse(persistent.into_map());
1470 let retained_inputs: Vec<Arc<RwLock<Storage>>> = inputs.values().cloned().collect();
1474 let mut cap_cache: StorageCache = inputs;
1475 let mut cap_layouts = seed_layouts;
1476 let mut cap_syms = SymEnv::new();
1477 let mut inner_err: Option<Error> = None;
1478 let items_ref = &items;
1479 let graph_ref = &graph;
1480 let run = device.capture_run(|_stream| {
1481 for item in items_ref {
1485 match execute_work_item(
1486 item,
1487 &mut cap_cache,
1488 &mut cap_layouts,
1489 &mut cap_syms,
1490 false,
1491 Some(&mut reuse),
1492 ) {
1493 Ok(_handle) => {}
1494 Err(e) => {
1495 inner_err = Some(with_node_location(graph_ref, item.node_id, e));
1496 break;
1497 }
1498 }
1499 }
1500 Ok(())
1501 })?;
1502 if let Some(e) = inner_err {
1503 return Err(e);
1504 }
1505
1506 Ok(CapturedDecode {
1507 run,
1508 output,
1509 output_layout,
1510 retained_inputs,
1511 persistent: reuse.into_map(),
1512 })
1513 }
1514
1515 fn realize_inner(
1516 graph: Arc<RwLock<Graph>>,
1517 target: NodeId,
1518 inputs: StorageCache,
1519 order_source: OrderSource<'_>,
1520 sym_env: SymEnv,
1521 ) -> Result<(Arc<RwLock<Storage>>, Layout, SymEnv)> {
1522 {
1527 let _t = crate::compiled::op_profile::PhaseTimer::start("realize.safety_copies");
1528 let mut g = graph.write().map_err(|_| poisoned("graph lock"))?;
1529 let effective_roots = extend_with_side_effect_roots(&g, &[target]);
1530 insert_safety_copies(&mut g, &effective_roots);
1531 }
1532
1533 let (compiler_work, wait_set, mut layout_cache): (
1543 CompilerWork,
1544 WaitSet,
1545 HashMap<NodeId, Layout>,
1546 ) = {
1547 let _t = crate::compiled::op_profile::PhaseTimer::start("realize.plan_order");
1548 let g = graph.read().map_err(|_| poisoned("graph lock"))?;
1549 let effective_roots = extend_with_side_effect_roots(&g, &[target]);
1550 let (work, wait_set) =
1566 compiler_work_and_wait_set_for(&g, &effective_roots, &order_source);
1567 let mut layouts = HashMap::with_capacity(inputs.len());
1568 for &id in inputs.keys() {
1569 layouts.insert(id, g.layout(id));
1570 }
1571 (work, wait_set, layouts)
1572 };
1573
1574 let (tx, rx) = channel::<Result<WorkItem>>();
1575 let graph_for_compiler = Arc::clone(&graph);
1576
1577 let compiler = {
1583 let _t = crate::compiled::op_profile::PhaseTimer::start("realize.spawn_compiler");
1584 thread::spawn(move || {
1585 let _t = crate::compiled::op_profile::PhaseTimer::start(
1586 "compiler_thread (runs concurrently)",
1587 );
1588 compiler_thread_body(graph_for_compiler, compiler_work, sym_env, tx);
1589 })
1590 };
1591
1592 let plan_generation: Option<u64> = generation_for(&order_source);
1605 let mut current_chunk_backend: Option<BackendId> = None;
1606 let mut cache: StorageCache = inputs;
1607 let mut produced_syms = SymEnv::new();
1613 let mut handles: HashMap<NodeId, CompletionHandle> = HashMap::new();
1620 let mut inflight_vulkan: Vec<InflightVulkan> = Vec::new();
1630 let mut multi_backend = false;
1636 let loop_total = crate::compiled::op_profile::PhaseTimer::start("realize.loop_total");
1637 loop {
1638 let t_wait =
1639 crate::compiled::op_profile::PhaseTimer::start("realize.wait_for_compiler");
1640 let Ok(item) = rx.recv() else {
1641 break;
1642 };
1643 drop(t_wait);
1644 let item = item?;
1645 if !is_backend_neutral(&item.kind) && current_chunk_backend != Some(item.target_backend)
1652 {
1653 if let Some(plan_gen) = plan_generation {
1654 let live = crate::dispatch::topology_generation();
1655 if live != plan_gen {
1656 return Err(Error::TopologyChanged {
1657 plan_generation: plan_gen,
1658 current_generation: live,
1659 }
1660 .bt());
1661 }
1662 }
1663 let leaving = current_chunk_backend;
1664 if let Some(prev) = leaving
1665 && prev != item.target_backend
1666 {
1667 multi_backend = true;
1670 if prev == BackendId::Vulkan {
1676 {
1677 let _pt = crate::compiled::op_profile::PhaseTimer::start(
1678 "realize.eager_submit_vulkan",
1679 );
1680 eager_submit_all_vulkan(&cache, &mut inflight_vulkan)?;
1681 }
1682 }
1683 }
1684 current_chunk_backend = Some(item.target_backend);
1685 }
1686 if item.target_backend == BackendId::Vulkan && matches!(item.kind, WorkItemKind::Kernel)
1690 {
1691 multi_backend = true;
1692 }
1693 if matches!(
1705 item.kind,
1706 WorkItemKind::Copy { .. } | WorkItemKind::Move { .. }
1707 ) {
1708 if multi_backend {
1713 {
1714 let _pt = crate::compiled::op_profile::PhaseTimer::start(
1715 "realize.eager_submit_vulkan",
1716 );
1717 eager_submit_all_vulkan(&cache, &mut inflight_vulkan)?;
1718 }
1719 }
1720 if let Some(&producer) = item.inputs.first() {
1721 if multi_backend && copy_source_is_vulkan(&cache, producer)? {
1730 {
1731 let _pt = crate::compiled::op_profile::PhaseTimer::start(
1732 "realize.drain_inflight_vulkan",
1733 );
1734 drain_inflight_vulkan(&mut inflight_vulkan)?;
1735 }
1736 }
1737 wait_producer_handle(&mut handles, producer)?;
1742 }
1743 }
1744 let t_exec =
1745 crate::compiled::op_profile::PhaseTimer::start(work_item_kind_name(&item.kind));
1746 let handle = execute_work_item(
1747 &item,
1748 &mut cache,
1749 &mut layout_cache,
1750 &mut produced_syms,
1751 will_be_waited(&wait_set, item.node_id),
1752 None,
1755 )
1756 .map_err(|e| with_node_location(&graph, item.node_id, e))?;
1757 drop(t_exec);
1758 store_handle(&mut handles, item.node_id, handle);
1759 if let Some(d_idx) = item.destructive_input
1760 && let Some(&destroyed) = item.inputs.get(d_idx)
1761 && destroyed != target
1762 {
1763 let _t =
1787 crate::compiled::op_profile::PhaseTimer::start("realize.evict_destroyed_input");
1788 if multi_backend {
1789 defer_evicted_vulkan_buffer(&cache, destroyed, &mut inflight_vulkan)?;
1790 } else if let Some(d_arc) = cache.get(&destroyed) {
1791 let _f = crate::compiled::op_profile::PhaseTimer::start(
1792 "realize.evict_force_flush_vulkan",
1793 );
1794 force_flush_vulkan(d_arc)?;
1795 }
1796 cache.remove(&destroyed);
1797 layout_cache.remove(&destroyed);
1798 handles.remove(&destroyed);
1802 }
1803 }
1804
1805 drop(loop_total);
1806 {
1807 let _t = crate::compiled::op_profile::PhaseTimer::start("realize.join_compiler");
1808 compiler
1809 .join()
1810 .map_err(|_| Error::Msg("compiler thread panicked".to_string()).bt())?;
1811 }
1812
1813 let post_loop = crate::compiled::op_profile::PhaseTimer::start("realize.post_loop_drains");
1818 drain_handles(&mut handles)?;
1819 sync_active_cuda_devices(&cache)?;
1826 {
1830 let _pt =
1831 crate::compiled::op_profile::PhaseTimer::start("realize.drain_inflight_vulkan");
1832 drain_inflight_vulkan(&mut inflight_vulkan)?;
1833 }
1834 drain_vulkan_pending(&cache)?;
1840 debug_assert!(
1841 inflight_vulkan.is_empty(),
1842 "A4b-4: in-flight Vulkan batch list must be empty after realize-end drain",
1843 );
1844
1845 drop(post_loop);
1846 let storage = cache.remove(&target).ok_or_else(|| {
1847 Error::Msg(format!(
1848 "PipelinedExecutor::realize: target slot {:?} not populated after execution",
1849 target
1850 ))
1851 .bt()
1852 })?;
1853 let layout = layout_cache.remove(&target).ok_or_else(|| {
1854 Error::Msg(format!(
1855 "PipelinedExecutor::realize: target layout {:?} not populated after execution",
1856 target
1857 ))
1858 .bt()
1859 })?;
1860 {
1861 let _t = crate::compiled::op_profile::PhaseTimer::start("realize.drop_cache");
1862 drop(cache);
1863 drop(handles);
1864 drop(layout_cache);
1865 }
1866 Ok((storage, layout, produced_syms))
1867 }
1868
1869 pub fn realize_many(
1884 graph: Arc<RwLock<Graph>>,
1885 targets: &[NodeId],
1886 inputs: StorageCache,
1887 ) -> Result<Vec<(Arc<RwLock<Storage>>, Layout)>> {
1888 Self::realize_many_inner(
1889 graph,
1890 targets,
1891 inputs,
1892 OrderSource::Default,
1893 SymEnv::default(),
1894 )
1895 }
1896
1897 pub fn realize_many_with_optimized(
1903 graph: Arc<RwLock<Graph>>,
1904 targets: &[NodeId],
1905 inputs: StorageCache,
1906 optimized: &OptimizedGraph,
1907 ) -> Result<Vec<(Arc<RwLock<Storage>>, Layout)>> {
1908 Self::realize_many_inner(
1909 graph,
1910 targets,
1911 inputs,
1912 OrderSource::Optimized {
1913 optimized,
1914 route: None,
1915 },
1916 SymEnv::default(),
1917 )
1918 }
1919
1920 pub fn realize_many_with_optimized_env(
1923 graph: Arc<RwLock<Graph>>,
1924 targets: &[NodeId],
1925 inputs: StorageCache,
1926 optimized: &OptimizedGraph,
1927 sym_env: SymEnv,
1928 ) -> Result<Vec<(Arc<RwLock<Storage>>, Layout)>> {
1929 Self::realize_many_inner(
1930 graph,
1931 targets,
1932 inputs,
1933 OrderSource::Optimized {
1934 optimized,
1935 route: None,
1936 },
1937 sym_env,
1938 )
1939 }
1940
1941 pub fn realize_many_with_optimized_route(
1946 graph: Arc<RwLock<Graph>>,
1947 targets: &[NodeId],
1948 inputs: StorageCache,
1949 optimized: &OptimizedGraph,
1950 route: &PickedRoute,
1951 ) -> Result<Vec<(Arc<RwLock<Storage>>, Layout)>> {
1952 Self::realize_many_inner(
1953 graph,
1954 targets,
1955 inputs,
1956 OrderSource::Optimized {
1957 optimized,
1958 route: Some(route),
1959 },
1960 SymEnv::default(),
1961 )
1962 }
1963
1964 pub fn realize_many_with_optimized_route_env(
1967 graph: Arc<RwLock<Graph>>,
1968 targets: &[NodeId],
1969 inputs: StorageCache,
1970 optimized: &OptimizedGraph,
1971 route: &PickedRoute,
1972 sym_env: SymEnv,
1973 ) -> Result<Vec<(Arc<RwLock<Storage>>, Layout)>> {
1974 Self::realize_many_inner(
1975 graph,
1976 targets,
1977 inputs,
1978 OrderSource::Optimized {
1979 optimized,
1980 route: Some(route),
1981 },
1982 sym_env,
1983 )
1984 }
1985
1986 pub fn realize_many_with_optimized_picking_env(
1992 graph: Arc<RwLock<Graph>>,
1993 targets: &[NodeId],
1994 inputs: StorageCache,
1995 optimized: &OptimizedGraph,
1996 selector: Option<Arc<dyn RuntimeSelector>>,
1997 lookup: Option<BackendRuntimeLookup>,
1998 sym_env: SymEnv,
1999 ) -> Result<Vec<(Arc<RwLock<Storage>>, Layout)>> {
2000 match Self::streaming_pick_for(&graph, selector, lookup)? {
2001 Some(pick) => Self::realize_many_inner(
2002 graph,
2003 targets,
2004 inputs,
2005 OrderSource::Streaming {
2006 optimized,
2007 pick: &pick,
2008 },
2009 sym_env,
2010 ),
2011 None => {
2012 Self::realize_many_with_optimized_env(graph, targets, inputs, optimized, sym_env)
2013 }
2014 }
2015 }
2016
2017 fn realize_many_inner(
2018 graph: Arc<RwLock<Graph>>,
2019 targets: &[NodeId],
2020 inputs: StorageCache,
2021 order_source: OrderSource<'_>,
2022 sym_env: SymEnv,
2023 ) -> Result<Vec<(Arc<RwLock<Storage>>, Layout)>> {
2024 if targets.is_empty() {
2025 return Ok(Vec::new());
2026 }
2027
2028 {
2033 let mut g = graph.write().map_err(|_| poisoned("graph lock"))?;
2034 let effective_roots = extend_with_side_effect_roots(&g, targets);
2035 insert_safety_copies(&mut g, &effective_roots);
2036 }
2037
2038 let (compiler_work, wait_set, mut layout_cache): (
2045 CompilerWork,
2046 WaitSet,
2047 HashMap<NodeId, Layout>,
2048 ) = {
2049 let g = graph.read().map_err(|_| poisoned("graph lock"))?;
2050 let effective_roots = extend_with_side_effect_roots(&g, targets);
2051 let (work, wait_set) =
2061 compiler_work_and_wait_set_for(&g, &effective_roots, &order_source);
2062 let mut layouts = HashMap::with_capacity(inputs.len());
2063 for &id in inputs.keys() {
2064 layouts.insert(id, g.layout(id));
2065 }
2066 (work, wait_set, layouts)
2067 };
2068
2069 let target_set: std::collections::HashSet<NodeId> = targets.iter().copied().collect();
2072
2073 let (tx, rx) = channel::<Result<WorkItem>>();
2074 let graph_for_compiler = Arc::clone(&graph);
2075
2076 let compiler = thread::spawn(move || {
2077 compiler_thread_body(graph_for_compiler, compiler_work, sym_env, tx);
2078 });
2079
2080 let plan_generation: Option<u64> = generation_for(&order_source);
2084 let mut current_chunk_backend: Option<BackendId> = None;
2085 let mut cache: StorageCache = inputs;
2086 let mut produced_syms = SymEnv::new();
2091 let mut handles: HashMap<NodeId, CompletionHandle> = HashMap::new();
2095 let mut inflight_vulkan: Vec<InflightVulkan> = Vec::new();
2102 let mut multi_backend = false;
2103 for item in rx {
2104 let item = item?;
2105 if !is_backend_neutral(&item.kind) && current_chunk_backend != Some(item.target_backend)
2108 {
2109 if let Some(plan_gen) = plan_generation {
2110 let live = crate::dispatch::topology_generation();
2111 if live != plan_gen {
2112 return Err(Error::TopologyChanged {
2113 plan_generation: plan_gen,
2114 current_generation: live,
2115 }
2116 .bt());
2117 }
2118 }
2119 let leaving = current_chunk_backend;
2120 if let Some(prev) = leaving
2121 && prev != item.target_backend
2122 {
2123 multi_backend = true;
2124 if prev == BackendId::Vulkan {
2125 eager_submit_all_vulkan(&cache, &mut inflight_vulkan)?;
2126 }
2127 }
2128 current_chunk_backend = Some(item.target_backend);
2129 }
2130 if item.target_backend == BackendId::Vulkan && matches!(item.kind, WorkItemKind::Kernel)
2134 {
2135 multi_backend = true;
2136 }
2137 if matches!(
2142 item.kind,
2143 WorkItemKind::Copy { .. } | WorkItemKind::Move { .. }
2144 ) {
2145 if multi_backend {
2148 eager_submit_all_vulkan(&cache, &mut inflight_vulkan)?;
2149 }
2150 if let Some(&producer) = item.inputs.first() {
2151 if multi_backend && copy_source_is_vulkan(&cache, producer)? {
2155 drain_inflight_vulkan(&mut inflight_vulkan)?;
2156 }
2157 wait_producer_handle(&mut handles, producer)?;
2159 }
2160 }
2161 let handle = execute_work_item(
2162 &item,
2163 &mut cache,
2164 &mut layout_cache,
2165 &mut produced_syms,
2166 will_be_waited(&wait_set, item.node_id),
2167 None,
2170 )
2171 .map_err(|e| with_node_location(&graph, item.node_id, e))?;
2172 store_handle(&mut handles, item.node_id, handle);
2173 if let Some(d_idx) = item.destructive_input
2174 && let Some(&destroyed) = item.inputs.get(d_idx)
2175 && !target_set.contains(&destroyed)
2176 {
2177 if multi_backend {
2192 defer_evicted_vulkan_buffer(&cache, destroyed, &mut inflight_vulkan)?;
2193 } else if let Some(d_arc) = cache.get(&destroyed) {
2194 force_flush_vulkan(d_arc)?;
2195 }
2196 cache.remove(&destroyed);
2197 layout_cache.remove(&destroyed);
2198 handles.remove(&destroyed);
2201 }
2202 }
2203
2204 compiler
2205 .join()
2206 .map_err(|_| Error::Msg("compiler thread panicked".to_string()).bt())?;
2207
2208 drain_handles(&mut handles)?;
2211 sync_active_cuda_devices(&cache)?;
2215 drain_inflight_vulkan(&mut inflight_vulkan)?;
2218 drain_vulkan_pending(&cache)?;
2222 debug_assert!(
2223 inflight_vulkan.is_empty(),
2224 "A4b-4: in-flight Vulkan batch list must be empty after realize-end drain",
2225 );
2226
2227 let mut out = Vec::with_capacity(targets.len());
2232 for &target in targets {
2233 let storage = cache.get(&target).cloned().ok_or_else(|| {
2234 Error::Msg(format!(
2235 "PipelinedExecutor::realize_many: target {:?} not populated after execution",
2236 target
2237 ))
2238 .bt()
2239 })?;
2240 let layout = layout_cache.get(&target).cloned().ok_or_else(|| {
2241 Error::Msg(format!(
2242 "PipelinedExecutor::realize_many: target layout {:?} not populated after execution",
2243 target
2244 ))
2245 .bt()
2246 })?;
2247 out.push((storage, layout));
2248 }
2249 Ok(out)
2250 }
2251}
2252
2253fn order_for(
2261 graph: &Graph,
2262 effective_roots: &[NodeId],
2263 order_source: &OrderSource<'_>,
2264) -> Vec<NodeId> {
2265 match order_source {
2266 OrderSource::Default => execution_plan(graph, effective_roots),
2267 OrderSource::Streaming {
2275 optimized: _optimized,
2276 pick,
2277 } => {
2278 let bindings = global_bindings();
2279 fuel_graph::lower_picked_route_streaming(graph, effective_roots, |branch| {
2280 resolve_branch(graph, branch, &bindings, pick.selector.as_ref())
2281 })
2282 }
2283 OrderSource::Optimized { optimized, route } => {
2284 match route {
2296 Some(route) => fuel_graph::lower_picked_route(graph, effective_roots, route),
2297 None => {
2298 let view = OptimizedGraph {
2302 roots: effective_roots.to_vec(),
2303 generation: optimized.generation,
2304 placements: None,
2305 };
2306 view.dispatch_order(graph)
2307 }
2308 }
2309 }
2310 OrderSource::Cached { .. } => {
2315 unreachable!("OrderSource::Cached is resolved before order_for is called")
2316 }
2317 }
2318}
2319
2320fn build_wait_set(graph: &Graph, order: &[NodeId]) -> HashSet<NodeId> {
2336 let mut set = HashSet::new();
2337 for &id in order {
2338 let node = graph.node(id);
2339 if matches!(node.op, Op::Copy { .. } | Op::Move { .. })
2340 && let Some(&producer) = node.inputs.first()
2341 {
2342 set.insert(producer);
2343 }
2344 }
2345 set
2346}
2347
2348type WaitSet = Option<HashSet<NodeId>>;
2354
2355#[inline]
2359fn will_be_waited(wait_set: &WaitSet, node_id: NodeId) -> bool {
2360 match wait_set {
2361 None => true,
2362 Some(set) => set.contains(&node_id),
2363 }
2364}
2365
2366fn compiler_work_and_wait_set_for(
2380 graph: &Graph,
2381 effective_roots: &[NodeId],
2382 order_source: &OrderSource<'_>,
2383) -> (CompilerWork, WaitSet) {
2384 match order_source {
2385 OrderSource::Streaming { pick, .. } => (
2386 CompilerWork::Streaming {
2387 roots: effective_roots.to_vec(),
2388 pick: (*pick).clone(),
2389 },
2390 None,
2391 ),
2392 OrderSource::Cached {
2393 optimized,
2394 order_cache,
2395 } => {
2396 let order = order_cache.get_or_init(|| {
2397 order_for(
2398 graph,
2399 effective_roots,
2400 &OrderSource::Optimized {
2401 optimized,
2402 route: None,
2403 },
2404 )
2405 });
2406 let wait_set = build_wait_set(graph, order);
2407 (CompilerWork::Order(order.clone()), Some(wait_set))
2408 }
2409 other => {
2410 let order = order_for(graph, effective_roots, other);
2411 let wait_set = build_wait_set(graph, &order);
2412 (CompilerWork::Order(order), Some(wait_set))
2413 }
2414 }
2415}
2416
2417fn generation_for(order_source: &OrderSource<'_>) -> Option<u64> {
2422 match order_source {
2423 OrderSource::Optimized { optimized, .. } => Some(optimized.generation),
2424 OrderSource::Streaming { optimized, .. } => Some(optimized.generation),
2425 OrderSource::Cached { optimized, .. } => Some(optimized.generation),
2426 OrderSource::Default => None,
2427 }
2428}
2429
2430enum CompilerWork {
2435 Order(Vec<NodeId>),
2438 Streaming {
2446 roots: Vec<NodeId>,
2447 pick: StreamingPick,
2448 },
2449}
2450
2451fn compiler_thread_body(
2458 graph: Arc<RwLock<Graph>>,
2459 work: CompilerWork,
2460 sym_env: SymEnv,
2461 tx: Sender<Result<WorkItem>>,
2462) {
2463 let bindings = global_bindings();
2464
2465 let g = match graph.read() {
2466 Ok(g) => g,
2467 Err(_) => {
2468 let _ = tx.send(Err(poisoned("graph lock in compiler")));
2469 return;
2470 }
2471 };
2472
2473 let mut layout_cache: HashMap<NodeId, Layout> = HashMap::new();
2479 let compile_and_send = |id: NodeId, layout_cache: &mut HashMap<NodeId, Layout>| -> bool {
2483 let item = compile_one(&g, id, layout_cache, &bindings, &sym_env);
2484 let stop_on_err = item.is_err();
2485 if tx.send(item).is_err() {
2486 return false;
2487 }
2488 !stop_on_err
2489 };
2490
2491 match work {
2492 CompilerWork::Order(order) => {
2493 for id in order {
2494 if !compile_and_send(id, &mut layout_cache) {
2495 return;
2496 }
2497 }
2498 }
2499 CompilerWork::Streaming { roots, pick } => {
2500 let mut stopped = false;
2512 fuel_graph::walk_picked_route_streaming(
2513 &g,
2514 &roots,
2515 |branch| resolve_branch(&g, branch, &bindings, pick.selector.as_ref()),
2516 |members| {
2517 if stopped {
2518 return;
2519 }
2520 for &id in members {
2521 if !compile_and_send(id, &mut layout_cache) {
2522 stopped = true;
2523 return;
2524 }
2525 }
2526 },
2527 );
2528 }
2529 }
2530}
2531
2532fn resolve_compiled(
2541 op_kind: OpKind,
2542 dtypes: &[DType],
2543 target_backend: BackendId,
2544 op_params: OpParams,
2545 bindings: &KernelBindingTable,
2546) -> Result<CompiledNode> {
2547 compile_node(op_kind, dtypes, target_backend, op_params, bindings)
2548}
2549
2550fn compile_one(
2565 graph: &Graph,
2566 id: NodeId,
2567 layout_cache: &mut HashMap<NodeId, Layout>,
2568 bindings: &KernelBindingTable,
2569 sym_env: &SymEnv,
2570) -> Result<WorkItem> {
2571 let node = graph.node(id);
2572 let elem_count = node.shape.elem_count();
2573 let inputs = node.inputs.clone();
2574 let destructive_input = node.op.destructive_input();
2578
2579 if matches!(node.op, Op::Const) {
2580 let output_layout = graph.layout(id);
2581 layout_cache.insert(id, output_layout.clone());
2582 return Ok(WorkItem {
2583 node_id: id,
2584 inputs,
2585 elem_count,
2586 dtype: node.dtype,
2587 target_backend: BackendId::Cpu,
2588 kind: WorkItemKind::ConstAdopt,
2589 compiled: None,
2590 output_layout,
2591 destructive_input,
2592 output_bundle: None,
2593 });
2594 }
2595
2596 if node.op.is_view_op() {
2597 if inputs.len() != 1 {
2598 return Err(Error::Msg(format!(
2599 "view op {:?} expects 1 input, got {}",
2600 node.op,
2601 inputs.len(),
2602 ))
2603 .bt());
2604 }
2605 let output_layout = graph.layout(id);
2611 layout_cache.insert(id, output_layout.clone());
2612 let target_backend = graph
2617 .target_backend(id)
2618 .or_else(|| graph.target_backend(inputs[0]))
2619 .unwrap_or(BackendId::Cpu);
2620 return Ok(WorkItem {
2621 node_id: id,
2622 inputs,
2623 elem_count,
2624 dtype: node.dtype,
2625 target_backend,
2626 kind: WorkItemKind::ViewOf {
2627 input: node.inputs[0],
2628 },
2629 compiled: None,
2630 output_layout,
2631 destructive_input,
2632 output_bundle: None,
2633 });
2634 }
2635
2636 if let Op::View { .. } = node.op {
2644 if inputs.len() != 1 {
2645 return Err(Error::Msg(format!(
2646 "Op::View expects 1 input (the producer), got {}",
2647 inputs.len(),
2648 ))
2649 .bt());
2650 }
2651 let output_layout = graph.layout(id);
2652 layout_cache.insert(id, output_layout.clone());
2653 let target_backend = graph
2654 .target_backend(id)
2655 .or_else(|| graph.target_backend(inputs[0]))
2656 .unwrap_or(BackendId::Cpu);
2657 return Ok(WorkItem {
2658 node_id: id,
2659 inputs: inputs.clone(),
2660 elem_count,
2661 dtype: node.dtype,
2662 target_backend,
2663 kind: WorkItemKind::SlotView {
2664 producer: inputs[0],
2665 },
2666 compiled: None,
2667 output_layout,
2668 destructive_input,
2669 output_bundle: None,
2670 });
2671 }
2672
2673 if let Op::ViewOwned { slot } = node.op {
2678 if inputs.len() != 1 {
2679 return Err(Error::Msg(format!(
2680 "Op::ViewOwned expects 1 input (the producer), got {}",
2681 inputs.len(),
2682 ))
2683 .bt());
2684 }
2685 let output_layout = Layout::contiguous(node.shape.clone());
2686 layout_cache.insert(id, output_layout.clone());
2687 let target_backend = graph
2688 .target_backend(id)
2689 .or_else(|| graph.target_backend(inputs[0]))
2690 .unwrap_or(BackendId::Cpu);
2691 return Ok(WorkItem {
2692 node_id: id,
2693 inputs: inputs.clone(),
2694 elem_count,
2695 dtype: node.dtype,
2696 target_backend,
2697 kind: WorkItemKind::SlotOwn {
2698 producer: inputs[0],
2699 slot,
2700 },
2701 compiled: None,
2702 output_layout,
2703 destructive_input,
2704 output_bundle: None,
2705 });
2706 }
2707
2708 if matches!(node.op, Op::Release) {
2709 if inputs.len() != 1 {
2715 return Err(
2716 Error::Msg(format!("Op::Release expects 1 input, got {}", inputs.len(),)).bt(),
2717 );
2718 }
2719 let output_layout = Layout::contiguous(node.shape.clone());
2720 layout_cache.insert(id, output_layout.clone());
2721 let target_backend = graph
2725 .target_backend(id)
2726 .or_else(|| graph.target_backend(inputs[0]))
2727 .unwrap_or(BackendId::Cpu);
2728 return Ok(WorkItem {
2729 node_id: id,
2730 inputs,
2731 elem_count,
2732 dtype: node.dtype,
2733 target_backend,
2734 kind: WorkItemKind::ReleaseMarker,
2735 compiled: None,
2736 output_layout,
2737 destructive_input,
2738 output_bundle: None,
2739 });
2740 }
2741
2742 if !matches!(
2750 node.op,
2751 Op::WriteSlice { .. }
2752 | Op::WriteSliceRotating { .. }
2753 | Op::WriteSliceDoff { .. }
2754 | Op::ZeroFill
2755 | Op::Release
2756 | Op::Move { .. },
2757 ) && let Some(target_idx) = node.op.destructive_input()
2758 {
2759 if inputs.len() <= target_idx {
2760 return Err(Error::Msg(format!(
2761 "in-place op {:?} declares destructive_input={target_idx} \
2762 but has only {} input(s)",
2763 node.op,
2764 inputs.len(),
2765 ))
2766 .bt());
2767 }
2768 let target_backend = graph.target_backend(id).ok_or_else(|| {
2769 Error::Msg(format!(
2770 "PipelinedExecutor: in-place op node {id:?} has no target_backend set",
2771 ))
2772 .bt()
2773 })?;
2774 let op_kind = op_to_op_kind(&node.op).ok_or_else(|| {
2775 Error::Msg(format!(
2776 "PipelinedExecutor: in-place op {:?} has no op_to_op_kind mapping",
2777 node.op,
2778 ))
2779 .bt()
2780 })?;
2781 let op_params = op_to_op_params(graph, node, id, layout_cache, sym_env)?;
2782 let dtypes = build_lookup_dtypes(graph, node);
2783 let compiled = resolve_compiled(op_kind, &dtypes, target_backend, op_params, bindings)?;
2784 let output_layout = graph.layout(inputs[target_idx]);
2786 layout_cache.insert(id, output_layout.clone());
2787 return Ok(WorkItem {
2788 node_id: id,
2789 inputs: inputs.clone(),
2790 elem_count,
2791 dtype: node.dtype,
2792 target_backend,
2793 kind: WorkItemKind::InplaceKernel { target_idx },
2794 compiled: Some(compiled),
2795 output_layout,
2796 destructive_input,
2797 output_bundle: None,
2798 });
2799 }
2800
2801 if matches!(node.op, Op::WriteSlice { .. }) {
2802 if inputs.len() != 2 {
2808 return Err(Error::Msg(format!(
2809 "Op::WriteSlice expects 2 inputs (destination, source), got {}",
2810 inputs.len(),
2811 ))
2812 .bt());
2813 }
2814 let target_backend = graph.target_backend(id).ok_or_else(|| {
2815 Error::Msg(format!(
2816 "PipelinedExecutor: WriteSlice node {:?} has no target_backend set",
2817 id
2818 ))
2819 .bt()
2820 })?;
2821 let op_params = op_to_op_params(graph, node, id, layout_cache, sym_env)?;
2822 let dtypes = build_lookup_dtypes(graph, node);
2823 let compiled = resolve_compiled(
2824 OpKind::WriteSlice,
2825 &dtypes,
2826 target_backend,
2827 op_params,
2828 bindings,
2829 )?;
2830 let output_layout = graph.layout(inputs[0]);
2835 layout_cache.insert(id, output_layout.clone());
2836 return Ok(WorkItem {
2837 node_id: id,
2838 inputs: inputs.clone(),
2839 elem_count,
2840 dtype: node.dtype,
2841 target_backend,
2842 kind: WorkItemKind::WriteSlice {
2843 dest: inputs[0],
2844 source: inputs[1],
2845 },
2846 compiled: Some(compiled),
2847 output_layout,
2848 destructive_input,
2849 output_bundle: None,
2850 });
2851 }
2852
2853 if matches!(node.op, Op::WriteSliceRotating { .. }) {
2854 if inputs.len() != 3 {
2860 return Err(Error::Msg(format!(
2861 "Op::WriteSliceRotating expects 3 inputs (destination, source, position), got {}",
2862 inputs.len(),
2863 ))
2864 .bt());
2865 }
2866 let target_backend = graph.target_backend(id).ok_or_else(|| {
2867 Error::Msg(format!(
2868 "PipelinedExecutor: WriteSliceRotating node {:?} has no target_backend set",
2869 id
2870 ))
2871 .bt()
2872 })?;
2873 let op_params = op_to_op_params(graph, node, id, layout_cache, sym_env)?;
2874 let dtypes = build_lookup_dtypes(graph, node);
2875 let compiled = resolve_compiled(
2876 OpKind::WriteSliceRotating,
2877 &dtypes,
2878 target_backend,
2879 op_params,
2880 bindings,
2881 )?;
2882 let output_layout = graph.layout(inputs[0]);
2883 layout_cache.insert(id, output_layout.clone());
2884 return Ok(WorkItem {
2885 node_id: id,
2886 inputs: inputs.clone(),
2887 elem_count,
2888 dtype: node.dtype,
2889 target_backend,
2890 kind: WorkItemKind::WriteSliceRotating {
2891 dest: inputs[0],
2892 source: inputs[1],
2893 position: inputs[2],
2894 },
2895 compiled: Some(compiled),
2896 output_layout,
2897 destructive_input,
2898 output_bundle: None,
2899 });
2900 }
2901
2902 if matches!(node.op, Op::WriteSliceDoff { .. }) {
2903 if inputs.len() != 3 {
2909 return Err(Error::Msg(format!(
2910 "Op::WriteSliceDoff expects 3 inputs (destination, source, offset), got {}",
2911 inputs.len(),
2912 ))
2913 .bt());
2914 }
2915 let target_backend = graph.target_backend(id).ok_or_else(|| {
2916 Error::Msg(format!(
2917 "PipelinedExecutor: WriteSliceDoff node {:?} has no target_backend set",
2918 id
2919 ))
2920 .bt()
2921 })?;
2922 let op_params = op_to_op_params(graph, node, id, layout_cache, sym_env)?;
2923 let dtypes = build_lookup_dtypes(graph, node);
2924 let compiled = resolve_compiled(
2925 OpKind::WriteSliceDoff,
2926 &dtypes,
2927 target_backend,
2928 op_params,
2929 bindings,
2930 )?;
2931 let output_layout = graph.layout(inputs[0]);
2932 layout_cache.insert(id, output_layout.clone());
2933 return Ok(WorkItem {
2934 node_id: id,
2935 inputs: inputs.clone(),
2936 elem_count,
2937 dtype: node.dtype,
2938 target_backend,
2939 kind: WorkItemKind::WriteSliceDoff {
2940 dest: inputs[0],
2941 source: inputs[1],
2942 offset: inputs[2],
2943 },
2944 compiled: Some(compiled),
2945 output_layout,
2946 destructive_input,
2947 output_bundle: None,
2948 });
2949 }
2950
2951 if let Op::Alloc { target } = node.op {
2952 if !inputs.is_empty() {
2958 return Err(
2959 Error::Msg(format!("Op::Alloc expects 0 inputs, got {}", inputs.len(),)).bt(),
2960 );
2961 }
2962 let output_layout = Layout::contiguous(node.shape.clone());
2963 layout_cache.insert(id, output_layout.clone());
2964 let target_backend = match target {
2969 DeviceLocation::Cpu => BackendId::Cpu,
2970 DeviceLocation::Cuda { .. } => BackendId::Cuda,
2971 DeviceLocation::Vulkan { .. } => BackendId::Vulkan,
2972 DeviceLocation::Metal { .. } => BackendId::Metal,
2973 };
2974 return Ok(WorkItem {
2975 node_id: id,
2976 inputs,
2977 elem_count,
2978 dtype: node.dtype,
2979 target_backend,
2980 kind: WorkItemKind::Alloc {
2981 target_location: target,
2982 },
2983 compiled: None,
2984 output_layout,
2985 destructive_input,
2986 output_bundle: None,
2987 });
2988 }
2989
2990 if let Op::Iota { len } = node.op {
2991 if !inputs.is_empty() {
2996 return Err(
2997 Error::Msg(format!("Op::Iota expects 0 inputs, got {}", inputs.len(),)).bt(),
2998 );
2999 }
3000 let output_layout = Layout::contiguous(node.shape.clone());
3001 layout_cache.insert(id, output_layout.clone());
3002 let target_backend = graph.target_backend(id).unwrap_or(BackendId::Cpu);
3006 return Ok(WorkItem {
3007 node_id: id,
3008 inputs,
3009 elem_count,
3010 dtype: node.dtype,
3011 target_backend,
3012 kind: WorkItemKind::Iota { len },
3013 compiled: None,
3014 output_layout,
3015 destructive_input,
3016 output_bundle: None,
3017 });
3018 }
3019
3020 if matches!(node.op, Op::ZeroFill) {
3021 if inputs.len() != 1 {
3025 return Err(Error::Msg(format!(
3026 "Op::ZeroFill expects 1 input, got {}",
3027 inputs.len(),
3028 ))
3029 .bt());
3030 }
3031 let output_layout = Layout::contiguous(node.shape.clone());
3037 layout_cache.insert(id, output_layout.clone());
3038 let target_backend = graph
3039 .target_backend(id)
3040 .or_else(|| graph.target_backend(inputs[0]))
3041 .unwrap_or(BackendId::Cpu);
3042 return Ok(WorkItem {
3043 node_id: id,
3044 inputs,
3045 elem_count,
3046 dtype: node.dtype,
3047 target_backend,
3048 kind: WorkItemKind::ZeroFill,
3049 compiled: None,
3050 output_layout,
3051 destructive_input,
3052 output_bundle: None,
3053 });
3054 }
3055
3056 if let Op::Copy { target } = node.op {
3057 if inputs.len() != 1 {
3066 return Err(
3067 Error::Msg(format!("Op::Copy expects 1 input, got {}", inputs.len(),)).bt(),
3068 );
3069 }
3070 let target_backend = graph.target_backend(id).ok_or_else(|| {
3071 Error::Msg(format!(
3072 "PipelinedExecutor: Op::Copy node {:?} has no target_backend \
3073 set (= source backend, the one whose kernel runs the \
3074 download)",
3075 id
3076 ))
3077 .bt()
3078 })?;
3079 let op_params = OpParams::None;
3080 let dtypes = build_lookup_dtypes(graph, node);
3081 let compiled =
3082 resolve_compiled(OpKind::Copy, &dtypes, target_backend, op_params, bindings)?;
3083 let output_layout = Layout::contiguous(node.shape.clone());
3089 layout_cache.insert(id, output_layout.clone());
3090 return Ok(WorkItem {
3091 node_id: id,
3092 inputs,
3093 elem_count,
3094 dtype: node.dtype,
3095 target_backend,
3096 kind: WorkItemKind::Copy {
3097 target_location: target,
3098 },
3099 compiled: Some(compiled),
3100 output_layout,
3101 destructive_input,
3102 output_bundle: None,
3103 });
3104 }
3105
3106 if let Op::Move { target } = node.op {
3107 if inputs.len() != 1 {
3119 return Err(
3120 Error::Msg(format!("Op::Move expects 1 input, got {}", inputs.len(),)).bt(),
3121 );
3122 }
3123 let target_backend = graph.target_backend(id).ok_or_else(|| {
3124 Error::Msg(format!(
3125 "PipelinedExecutor: Op::Move node {:?} has no target_backend \
3126 set (= source backend, the one whose kernel runs the \
3127 transfer)",
3128 id
3129 ))
3130 .bt()
3131 })?;
3132 let op_params = OpParams::None;
3133 let dtypes = build_lookup_dtypes(graph, node);
3134 let compiled =
3135 resolve_compiled(OpKind::Copy, &dtypes, target_backend, op_params, bindings)?;
3136 let output_layout = Layout::contiguous(node.shape.clone());
3140 layout_cache.insert(id, output_layout.clone());
3141 return Ok(WorkItem {
3142 node_id: id,
3143 inputs,
3144 elem_count,
3145 dtype: node.dtype,
3146 target_backend,
3147 kind: WorkItemKind::Move {
3148 target_location: target,
3149 },
3150 compiled: Some(compiled),
3151 output_layout,
3152 destructive_input,
3153 output_bundle: None,
3154 });
3155 }
3156
3157 if matches!(node.op, Op::Reshape(_)) {
3158 if inputs.len() != 1 {
3159 return Err(
3160 Error::Msg(format!("Op::Reshape expects 1 input, got {}", inputs.len(),)).bt(),
3161 );
3162 }
3163 let input_layout = graph.layout(inputs[0]);
3164 let output_layout = Layout::contiguous(node.shape.clone());
3169 layout_cache.insert(id, output_layout.clone());
3170 let target_backend = graph
3171 .target_backend(id)
3172 .or_else(|| graph.target_backend(inputs[0]))
3173 .unwrap_or(BackendId::Cpu);
3174 let in_elem_count = input_layout.shape().elem_count();
3176 if in_elem_count != elem_count {
3177 return Err(Error::Msg(format!(
3178 "Op::Reshape changes element count: input {} → output {}",
3179 in_elem_count, elem_count,
3180 ))
3181 .bt());
3182 }
3183 return Ok(WorkItem {
3184 node_id: id,
3185 inputs,
3186 elem_count,
3187 dtype: node.dtype,
3188 target_backend,
3189 kind: WorkItemKind::ContiguizeOf {
3190 input: node.inputs[0],
3191 },
3192 compiled: None,
3193 output_layout,
3194 destructive_input,
3195 output_bundle: None,
3196 });
3197 }
3198
3199 if matches!(node.op, Op::Contiguize) {
3200 if inputs.len() != 1 {
3207 return Err(Error::Msg(format!(
3208 "Op::Contiguize expects 1 input, got {}",
3209 inputs.len(),
3210 ))
3211 .bt());
3212 }
3213 let output_layout = Layout::contiguous(node.shape.clone());
3214 layout_cache.insert(id, output_layout.clone());
3215 let target_backend = graph
3216 .target_backend(id)
3217 .or_else(|| graph.target_backend(inputs[0]))
3218 .unwrap_or(BackendId::Cpu);
3219 return Ok(WorkItem {
3220 node_id: id,
3221 inputs,
3222 elem_count,
3223 dtype: node.dtype,
3224 target_backend,
3225 kind: WorkItemKind::ContiguizeOf {
3226 input: node.inputs[0],
3227 },
3228 compiled: None,
3229 output_layout,
3230 destructive_input,
3231 output_bundle: None,
3232 });
3233 }
3234
3235 if let Op::Fused(fid, _) = &node.op
3245 && fid.is_runtime()
3246 {
3247 let target_backend = graph.target_backend(id).ok_or_else(|| {
3248 Error::Msg(format!(
3249 "PipelinedExecutor: runtime fused op {:?} (node {:?}) has no \
3250 target_backend — the arm emitter pins it at emission",
3251 fid, id,
3252 ))
3253 .bt()
3254 })?;
3255 if graph.output_views_arc(id).is_some() {
3258 return Err(Error::Msg(format!(
3259 "PipelinedExecutor: runtime fused op {:?} (node {:?}) carries a \
3260 multi-output bundle — synthesized kernels are single-output in v1",
3261 fid, id,
3262 ))
3263 .bt());
3264 }
3265 let dtypes = build_lookup_dtypes(graph, node);
3266 let (kernel, caps) = bindings.lookup_with_caps(
3267 crate::kernel::BindingKey::RuntimeFused(*fid),
3268 &dtypes,
3269 target_backend,
3270 )?;
3271 let output_layout = Layout::contiguous(node.shape.clone());
3272 layout_cache.insert(id, output_layout.clone());
3273 let op_params = match &node.op {
3280 Op::Fused(_, FusedOpParams::Runtime { scalars }) if !scalars.is_empty() => {
3281 OpParams::JitScalars {
3282 scalars: scalars.clone(),
3283 }
3284 }
3285 _ => OpParams::None,
3286 };
3287 let compiled = CompiledNode {
3288 op: OpKind::RuntimeFused,
3289 dtypes: KernelDTypes::from_slice(&dtypes),
3290 backend: target_backend,
3291 kernel,
3292 caps,
3293 op_params,
3294 };
3295 return Ok(WorkItem {
3296 node_id: id,
3297 inputs,
3298 elem_count,
3299 dtype: node.dtype,
3300 target_backend,
3301 kind: WorkItemKind::Kernel,
3302 compiled: Some(compiled),
3303 output_layout,
3304 destructive_input,
3305 output_bundle: None,
3306 });
3307 }
3308
3309 let target_backend = graph.target_backend(id).ok_or_else(|| {
3310 Error::Msg(format!(
3311 "PipelinedExecutor: node {:?} ({:?}) has no target_backend set",
3312 id, node.op
3313 ))
3314 .bt()
3315 })?;
3316
3317 let op_kind = op_to_op_kind(&node.op).ok_or_else(|| {
3318 Error::Msg(format!(
3319 "PipelinedExecutor: op {:?} not yet mapped to an OpKind \
3320 (Phase C migrates more ops as they're registered)",
3321 node.op,
3322 ))
3323 .bt()
3324 })?;
3325
3326 let op_params = op_to_op_params(graph, node, id, layout_cache, sym_env)?;
3327 let dtypes = build_lookup_dtypes(graph, node);
3332 let compiled = resolve_compiled(op_kind, &dtypes, target_backend, op_params, bindings)?;
3333 let output_layout = Layout::contiguous(node.shape.clone());
3334 layout_cache.insert(id, output_layout.clone());
3335 let output_bundle = graph.output_views_arc(id);
3341 Ok(WorkItem {
3342 node_id: id,
3343 inputs,
3344 elem_count,
3345 dtype: node.dtype,
3346 target_backend,
3347 kind: WorkItemKind::Kernel,
3348 compiled: Some(compiled),
3349 output_layout,
3350 destructive_input,
3351 output_bundle,
3352 })
3353}
3354
3355pub(crate) fn build_lookup_dtypes(graph: &Graph, node: &Node) -> Vec<DType> {
3365 if matches!(node.op, Op::Concat { .. }) {
3366 let in_dt = node
3368 .inputs
3369 .first()
3370 .map(|&id| graph.node(id).dtype)
3371 .unwrap_or(node.dtype);
3372 return vec![in_dt, node.dtype];
3373 }
3374 if matches!(node.op, Op::WriteSlice { .. }) {
3375 let src_dt = node
3381 .inputs
3382 .get(1)
3383 .map(|&id| graph.node(id).dtype)
3384 .unwrap_or(node.dtype);
3385 return vec![src_dt, node.dtype];
3386 }
3387 if matches!(node.op, Op::WriteSliceRotating { .. }) {
3388 let src_dt = node
3392 .inputs
3393 .get(1)
3394 .map(|&id| graph.node(id).dtype)
3395 .unwrap_or(node.dtype);
3396 return vec![src_dt, node.dtype];
3397 }
3398 if matches!(node.op, Op::WriteSliceDoff { .. }) {
3399 let src_dt = node
3403 .inputs
3404 .get(1)
3405 .map(|&id| graph.node(id).dtype)
3406 .unwrap_or(node.dtype);
3407 return vec![src_dt, node.dtype];
3408 }
3409 let mut dts: Vec<DType> = node.inputs.iter().map(|&id| graph.node(id).dtype).collect();
3410 dts.push(node.dtype);
3411 dts
3412}
3413
3414fn op_is_softmax_last_dim(op: &Op) -> bool {
3420 matches!(op, Op::Fused(fid, _) if *fid == fuel_graph::registry::FusedOps::SOFTMAX_LAST_DIM)
3421}
3422
3423fn op_is_fused_linear(op: &Op) -> bool {
3426 matches!(op, Op::Fused(fid, _) if *fid == fuel_graph::registry::FusedOps::FUSED_LINEAR)
3427}
3428
3429pub(crate) fn op_to_op_kind(op: &Op) -> Option<OpKind> {
3437 match op {
3438 Op::Add => Some(OpKind::AddElementwise),
3439 Op::Sub => Some(OpKind::SubElementwise),
3440 Op::Mul => Some(OpKind::MulElementwise),
3441 Op::Div => Some(OpKind::DivElementwise),
3442 Op::Relu => Some(OpKind::ReluElementwise),
3443 Op::Neg => Some(OpKind::NegElementwise),
3444 Op::Sqr => Some(OpKind::SqrElementwise),
3445 Op::Sqrt => Some(OpKind::SqrtElementwise),
3446 Op::Tanh => Some(OpKind::TanhElementwise),
3447 Op::Exp => Some(OpKind::ExpElementwise),
3448 Op::Log => Some(OpKind::LogElementwise),
3449 Op::Sin => Some(OpKind::SinElementwise),
3450 Op::Cos => Some(OpKind::CosElementwise),
3451 Op::Sigmoid => Some(OpKind::SigmoidElementwise),
3452 Op::Silu => Some(OpKind::SiluElementwise),
3453 Op::GeluTanh => Some(OpKind::GeluTanhElementwise),
3454 Op::Step => Some(OpKind::StepElementwise),
3455 Op::Recip => Some(OpKind::RecipElementwise),
3456 Op::Abs => Some(OpKind::AbsElementwise),
3457 Op::Equal => Some(OpKind::EqualElementwise),
3458 Op::Ne => Some(OpKind::NotEqualElementwise),
3459 Op::Lt => Some(OpKind::LessElementwise),
3460 Op::Le => Some(OpKind::LessEqualElementwise),
3461 Op::Gt => Some(OpKind::GreaterElementwise),
3462 Op::Ge => Some(OpKind::GreaterEqualElementwise),
3463 Op::Where => Some(OpKind::Where),
3464 Op::Floor => Some(OpKind::FloorElementwise),
3465 Op::Ceil => Some(OpKind::CeilElementwise),
3466 Op::Round => Some(OpKind::RoundElementwise),
3467 Op::Sign => Some(OpKind::SignElementwise),
3468 Op::Erf => Some(OpKind::ErfElementwise),
3469 Op::Gelu => Some(OpKind::GeluElementwise),
3470 Op::Pow => Some(OpKind::PowElementwise),
3471 Op::Rsqrt => Some(OpKind::RsqrtElementwise),
3472 Op::Rem => Some(OpKind::RemElementwise),
3473 Op::Flip { .. } => Some(OpKind::Flip),
3474 Op::Roll { .. } => Some(OpKind::Roll),
3475 Op::CumSum { .. } => Some(OpKind::CumSum),
3476 Op::Triu { .. } => Some(OpKind::Triu),
3477 Op::Tril { .. } => Some(OpKind::Tril),
3478 Op::LogSoftmaxLastDim => Some(OpKind::LogSoftmaxLastDim),
3479 Op::LogSoftmaxLastDimBackward => Some(OpKind::LogSoftmaxLastDimBackward),
3480 Op::MaskedFill { .. } => Some(OpKind::MaskedFill),
3481 Op::Pad { .. } => Some(OpKind::Pad),
3482 Op::PadBackward { .. } => Some(OpKind::PadBackward),
3483 Op::SumDim(_) => Some(OpKind::SumReduce),
3484 Op::MaxDim(_) => Some(OpKind::MaxReduce),
3485 Op::MinDim(_) => Some(OpKind::MinReduce),
3486 Op::MeanDim(_) => Some(OpKind::MeanReduce),
3487 Op::SumAll => Some(OpKind::SumReduce),
3488 Op::MaxAll => Some(OpKind::MaxReduce),
3489 Op::MinAll => Some(OpKind::MinReduce),
3490 Op::MeanAll => Some(OpKind::MeanReduce),
3491 Op::MatMul => Some(OpKind::MatMul),
3492 Op::Cast(_) => Some(OpKind::Cast),
3493 Op::Fused(fid, _) if *fid == fuel_graph::registry::FusedOps::CONV2D => Some(OpKind::Conv2D),
3499 Op::Fused(fid, _) if *fid == fuel_graph::registry::FusedOps::CONV_TRANSPOSE2D => {
3503 Some(OpKind::ConvTranspose2D)
3504 }
3505 Op::ReduceSumTo(_) => Some(OpKind::ReduceSumTo),
3506 Op::ReduceMaxTo(_) => Some(OpKind::ReduceMaxTo),
3507 Op::Fused(fid, _) if *fid == fuel_graph::registry::FusedOps::FLASH_ATTN => {
3508 Some(OpKind::FlashAttn)
3509 }
3510 Op::Fused(fid, _) if *fid == fuel_graph::registry::FusedOps::PAGED_ATTN => {
3511 Some(OpKind::PagedAttn)
3512 }
3513 Op::AddScalar(_) => Some(OpKind::Affine),
3514 Op::MulScalar(_) => Some(OpKind::Affine),
3515 Op::Clamp { .. } => Some(OpKind::ClampElementwise),
3516 Op::PowI(_) => Some(OpKind::PowIElementwise),
3517 Op::Maximum => Some(OpKind::MaximumElementwise),
3518 Op::Minimum => Some(OpKind::MinimumElementwise),
3519 Op::Concat { .. } => Some(OpKind::Concat),
3520 Op::Fused(fid, _) if *fid == fuel_graph::registry::FusedOps::SOFTMAX_LAST_DIM => {
3521 Some(OpKind::SoftmaxLastDim)
3522 }
3523 Op::Fused(fid, _) if *fid == fuel_graph::registry::FusedOps::SOFTMAX_LAST_DIM_BACKWARD => {
3526 Some(OpKind::SoftmaxLastDimBackward)
3527 }
3528 Op::Fused(fid, _)
3529 if *fid == fuel_graph::registry::FusedOps::LAYER_NORM_LAST_DIM_BACKWARD =>
3530 {
3531 Some(OpKind::LayerNormLastDimBackward)
3532 }
3533 Op::Fused(fid, _) if *fid == fuel_graph::registry::FusedOps::RMS_NORM_LAST_DIM_BACKWARD => {
3534 Some(OpKind::RmsNormLastDimBackward)
3535 }
3536 Op::Fused(fid, _) if *fid == fuel_graph::registry::FusedOps::REDUCE_MAX_TO_BACKWARD => {
3537 Some(OpKind::ReduceMaxToBackward)
3538 }
3539 Op::Fused(fid, _) if *fid == fuel_graph::registry::FusedOps::POWI_BACKWARD => {
3540 Some(OpKind::PowIElementwiseBackward)
3541 }
3542 Op::Fused(fid, _) if *fid == fuel_graph::registry::FusedOps::FUSED_LINEAR => {
3543 Some(OpKind::FusedLinear)
3544 }
3545 Op::Fused(fid, _) if *fid == fuel_graph::registry::FusedOps::RMS_NORM_LAST_DIM => {
3546 Some(OpKind::RmsNormLastDim)
3547 }
3548 Op::Fused(fid, _) if *fid == fuel_graph::registry::FusedOps::LAYER_NORM_LAST_DIM => {
3549 Some(OpKind::LayerNormLastDim)
3550 }
3551 Op::IndexSelect { .. } => Some(OpKind::IndexSelect),
3552 Op::Gather { .. } => Some(OpKind::Gather),
3553 Op::Fused(fid, _) if *fid == fuel_graph::registry::FusedOps::ROPE => Some(OpKind::Rope),
3554 Op::IndexAdd { .. } => Some(OpKind::IndexAdd),
3555 Op::ScatterAdd { .. } => Some(OpKind::ScatterAdd),
3556 Op::NonZeroIndices { .. } => Some(OpKind::NonZeroIndices),
3557 Op::ArgMaxDim(_) => Some(OpKind::ArgMaxDim),
3558 Op::ArgMinDim(_) => Some(OpKind::ArgMinDim),
3559 Op::Fused(fid, _) if *fid == fuel_graph::registry::FusedOps::QMATMUL => {
3561 Some(OpKind::QMatMul)
3562 }
3563 Op::WriteSlice { .. } => Some(OpKind::WriteSlice),
3564 Op::WriteSliceRotating { .. } => Some(OpKind::WriteSliceRotating),
3565 Op::WriteSliceDoff { .. } => Some(OpKind::WriteSliceDoff),
3566 Op::Copy { .. } => Some(OpKind::Copy),
3574 Op::Move { .. } => Some(OpKind::Copy),
3581 Op::ReluInplace => Some(OpKind::ReluInplace),
3586 Op::SiluInplace => Some(OpKind::SiluInplace),
3587 Op::GeluTanhInplace => Some(OpKind::GeluTanhInplace),
3588 Op::TanhInplace => Some(OpKind::TanhInplace),
3589 Op::SigmoidInplace => Some(OpKind::SigmoidInplace),
3590 Op::NegInplace => Some(OpKind::NegInplace),
3591 Op::AbsInplace => Some(OpKind::AbsInplace),
3592 Op::SqrInplace => Some(OpKind::SqrInplace),
3593 Op::SqrtInplace => Some(OpKind::SqrtInplace),
3594 Op::RsqrtInplace => Some(OpKind::RsqrtInplace),
3595 Op::RecipInplace => Some(OpKind::RecipInplace),
3596 Op::ExpInplace => Some(OpKind::ExpInplace),
3597 Op::LogInplace => Some(OpKind::LogInplace),
3598 Op::SinInplace => Some(OpKind::SinInplace),
3599 Op::CosInplace => Some(OpKind::CosInplace),
3600 Op::SignInplace => Some(OpKind::SignInplace),
3601 Op::FloorInplace => Some(OpKind::FloorInplace),
3602 Op::CeilInplace => Some(OpKind::CeilInplace),
3603 Op::RoundInplace => Some(OpKind::RoundInplace),
3604 Op::ErfInplace => Some(OpKind::ErfInplace),
3605 Op::GeluInplace => Some(OpKind::GeluInplace),
3606 Op::ClampInplace { .. } => Some(OpKind::ClampInplace),
3607 Op::PowIInplace(_) => Some(OpKind::PowIInplace),
3608 Op::Fused(fid, _) if *fid == fuel_graph::registry::FusedOps::INPLACE_AFFINE => {
3614 Some(OpKind::InplaceAffine)
3615 }
3616 Op::Fused(fid, _)
3617 if *fid == fuel_graph::registry::FusedOps::FUSED_SOFTMAX_CROSS_ENTROPY =>
3618 {
3619 Some(OpKind::FusedSoftmaxCrossEntropy)
3620 }
3621 Op::Fused(fid, _) if *fid == fuel_graph::registry::FusedOps::CAUSAL_CONV1D => {
3622 Some(OpKind::CausalConv1d)
3623 }
3624 Op::Fused(fid, _) if *fid == fuel_graph::registry::FusedOps::SELECTIVE_SCAN => {
3625 Some(OpKind::SelectiveScan)
3626 }
3627 Op::Fused(fid, _) if *fid == fuel_graph::registry::FusedOps::SSD_CHUNK_SCAN => {
3628 Some(OpKind::SsdChunkScan)
3629 }
3630 Op::Fused(fid, _) if *fid == fuel_graph::registry::FusedOps::NF4_MATMUL => {
3631 Some(OpKind::Nf4Matmul)
3632 }
3633 Op::Fused(fid, _) if *fid == fuel_graph::registry::FusedOps::AWQ_MATMUL => {
3634 Some(OpKind::AwqMatmul)
3635 }
3636 Op::Alloc { .. } | Op::ZeroFill => None,
3641 _ => None,
3642 }
3643}
3644
3645fn encode_value_to_bytes(dtype: DType, value: f64) -> Result<Vec<u8>> {
3658 match dtype {
3659 DType::F32 => Ok((value as f32).to_le_bytes().to_vec()),
3660 DType::F64 => Ok(value.to_le_bytes().to_vec()),
3661 DType::BF16 => Ok(half::bf16::from_f32(value as f32).to_le_bytes().to_vec()),
3662 DType::F16 => Ok(half::f16::from_f32(value as f32).to_le_bytes().to_vec()),
3663 DType::U8 => Ok(vec![value as u8]),
3664 DType::U32 => Ok((value as u32).to_le_bytes().to_vec()),
3665 other => Err(Error::Msg(format!(
3666 "encode_value_to_bytes: dtype {other:?} not yet supported for Pad fill",
3667 ))
3668 .bt()),
3669 }
3670}
3671
3672fn scalar_to_bytes(s: fuel_ir::Scalar) -> Vec<u8> {
3676 use fuel_ir::Scalar;
3677 match s {
3678 Scalar::U8(v) => vec![v],
3679 Scalar::I8(v) => vec![v as u8],
3680 Scalar::U32(v) => v.to_le_bytes().to_vec(),
3681 Scalar::I16(v) => v.to_le_bytes().to_vec(),
3682 Scalar::I32(v) => v.to_le_bytes().to_vec(),
3683 Scalar::I64(v) => v.to_le_bytes().to_vec(),
3684 Scalar::BF16(v) => v.to_le_bytes().to_vec(),
3685 Scalar::F16(v) => v.to_le_bytes().to_vec(),
3686 Scalar::F32(v) => v.to_le_bytes().to_vec(),
3687 Scalar::F64(v) => v.to_le_bytes().to_vec(),
3688 Scalar::Bool(b) => vec![b as u8],
3689 Scalar::F8E4M3(v) => vec![v.to_bits()],
3690 Scalar::F8E5M2(v) => vec![v.to_bits()],
3691 Scalar::F8E8M0(v) => vec![v],
3694 }
3695}
3696
3697fn op_to_op_params(
3698 graph: &Graph,
3699 node: &Node,
3700 node_id: NodeId,
3701 layout_cache: &HashMap<NodeId, Layout>,
3702 sym_env: &SymEnv,
3703) -> Result<OpParams> {
3704 let input_layout = |input_id: NodeId| -> Layout {
3710 layout_cache
3711 .get(&input_id)
3712 .cloned()
3713 .unwrap_or_else(|| graph.layout(input_id))
3714 };
3715 Ok(match &node.op {
3716 Op::Fused(_, fuel_graph::registry::FusedOpParams::QMatMul { quant_type, k, n }) => {
3718 if node.inputs.len() != 2 {
3722 return Err(Error::Msg(format!(
3723 "Op::QMatMul expects 2 inputs (activations, weight_bytes), got {}",
3724 node.inputs.len(),
3725 ))
3726 .bt());
3727 }
3728 let act_layout = input_layout(node.inputs[0]);
3729 let act_dims = act_layout.shape().dims();
3730 if act_dims.len() < 2 {
3731 return Err(Error::Msg(format!(
3732 "Op::QMatMul: activations must be rank ≥ 2, got {act_dims:?}",
3733 ))
3734 .bt());
3735 }
3736 let m = act_dims[act_dims.len() - 2];
3737 let k_act = act_dims[act_dims.len() - 1];
3738 if k_act != *k {
3739 return Err(Error::Msg(format!(
3740 "Op::QMatMul: activation last dim ({k_act}) must equal Op's k ({k})",
3741 ))
3742 .bt());
3743 }
3744 let batch_count: usize = act_dims[..act_dims.len() - 2].iter().product();
3745 OpParams::QMatMul {
3746 quant_type: *quant_type,
3747 batch_count,
3748 m,
3749 n: *n,
3750 k: *k,
3751 }
3752 }
3753 Op::ArgMaxDim(d) | Op::ArgMinDim(d) => {
3754 OpParams::Reduce {
3758 dims: vec![*d],
3759 keepdim: false,
3760 }
3761 }
3762 Op::SumDim(d) | Op::MaxDim(d) | Op::MinDim(d) | Op::MeanDim(d) => OpParams::Reduce {
3763 dims: vec![*d],
3764 keepdim: false,
3765 },
3766 Op::SumAll | Op::MaxAll | Op::MinAll | Op::MeanAll => {
3767 let il = input_layout(node.inputs[0]);
3768 let rank = il.shape().rank();
3769 OpParams::Reduce {
3770 dims: (0..rank).collect(),
3771 keepdim: false,
3772 }
3773 }
3774 Op::MatMul => {
3775 if node.inputs.len() != 2 {
3781 return Err(Error::Msg(format!(
3782 "Op::MatMul expects 2 inputs, got {}",
3783 node.inputs.len(),
3784 ))
3785 .bt());
3786 }
3787 let lhs = input_layout(node.inputs[0]);
3788 let rhs = input_layout(node.inputs[1]);
3789 let lhs_dims = lhs.shape().dims();
3790 let rhs_dims = rhs.shape().dims();
3791 if lhs_dims.len() < 2 || rhs_dims.len() < 2 {
3792 return Err(Error::Msg(format!(
3793 "Op::MatMul requires both inputs rank ≥ 2; got lhs={:?} rhs={:?}",
3794 lhs_dims, rhs_dims,
3795 ))
3796 .bt());
3797 }
3798 if lhs_dims.len() != rhs_dims.len() {
3799 return Err(Error::Msg(format!(
3800 "Op::MatMul: ranks must match (auto-broadcast happens at \
3801 graph construction time); got lhs rank {} vs rhs rank {}",
3802 lhs_dims.len(),
3803 rhs_dims.len(),
3804 ))
3805 .bt());
3806 }
3807 let rank = lhs_dims.len();
3808 let batch_rank = rank - 2;
3809 for i in 0..batch_rank {
3811 let la = lhs_dims[i];
3812 let ra = rhs_dims[i];
3813 let ok = la == ra || (ra > 0 && la > ra && la % ra == 0);
3814 if !ok {
3815 return Err(Error::Msg(format!(
3816 "Op::MatMul: batch dim {i} disallowed combination \
3817 (lhs={la}, rhs={ra}); must be equal or \
3818 GQA-divisible (lhs > rhs && lhs % rhs == 0)",
3819 ))
3820 .bt());
3821 }
3822 }
3823 let lhs_batch_dims: Vec<usize> = lhs_dims[..batch_rank].to_vec();
3824 let rhs_batch_dims: Vec<usize> = rhs_dims[..batch_rank].to_vec();
3825 let (m, k_lhs) = (lhs_dims[rank - 2], lhs_dims[rank - 1]);
3826 let (k_rhs, n) = (rhs_dims[rank - 2], rhs_dims[rank - 1]);
3827 if k_lhs != k_rhs {
3828 return Err(Error::Msg(format!(
3829 "Op::MatMul: contracting dims disagree — lhs trailing is \
3830 [{m}, {k_lhs}], rhs trailing is [{k_rhs}, {n}]",
3831 ))
3832 .bt());
3833 }
3834 let m_compute = match graph.matmul_row_count(node_id) {
3840 None => MatmulM::All,
3841 Some(dyn_m) => match dyn_m.resolve(sym_env) {
3842 Some(c) => {
3843 if c > m {
3844 return Err(Error::Msg(format!(
3845 "Op::MatMul: resolved row count {c} exceeds capacity {m}",
3846 ))
3847 .bt());
3848 }
3849 MatmulM::Rows(c)
3850 }
3851 None => match dyn_m {
3852 fuel_ir::DynScalar::Sym(s) if graph.binds_data_determined_sym(s) => {
3853 MatmulM::Deferred(dyn_m)
3854 }
3855 _ => {
3856 return Err(Error::Msg(format!(
3857 "Op::MatMul: dynamic row count {dyn_m:?} is unbound in the \
3858 SymEnv at compile and no producer binds it (not \
3859 data-determined)",
3860 ))
3861 .bt());
3862 }
3863 },
3864 },
3865 };
3866 OpParams::Matmul {
3867 lhs_batch_dims,
3868 rhs_batch_dims,
3869 m,
3870 n,
3871 k: k_lhs,
3872 m_compute,
3873 }
3874 }
3875 op if op_is_fused_linear(op) => {
3879 if node.inputs.len() != 3 {
3884 return Err(Error::Msg(format!(
3885 "Op::FusedLinear expects 3 inputs (a, b, bias), got {}",
3886 node.inputs.len(),
3887 ))
3888 .bt());
3889 }
3890 let lhs = input_layout(node.inputs[0]);
3891 let rhs = input_layout(node.inputs[1]);
3892 let bias = input_layout(node.inputs[2]);
3893 let lhs_dims = lhs.shape().dims();
3894 let rhs_dims = rhs.shape().dims();
3895 let bias_dims = bias.shape().dims();
3896 if lhs_dims.len() < 2 || rhs_dims.len() < 2 {
3897 return Err(Error::Msg(format!(
3898 "Op::FusedLinear requires both a, b rank ≥ 2; got a={:?} b={:?}",
3899 lhs_dims, rhs_dims,
3900 ))
3901 .bt());
3902 }
3903 if lhs_dims.len() != rhs_dims.len() {
3904 return Err(Error::Msg(format!(
3905 "Op::FusedLinear: ranks must match (auto-broadcast happens at \
3906 graph construction time); got a rank {} vs b rank {}",
3907 lhs_dims.len(),
3908 rhs_dims.len(),
3909 ))
3910 .bt());
3911 }
3912 let rank = lhs_dims.len();
3913 let batch_rank = rank - 2;
3914 for i in 0..batch_rank {
3915 let la = lhs_dims[i];
3916 let ra = rhs_dims[i];
3917 let ok = la == ra || (ra > 0 && la > ra && la % ra == 0);
3918 if !ok {
3919 return Err(Error::Msg(format!(
3920 "Op::FusedLinear: batch dim {i} disallowed (a={la}, b={ra})",
3921 ))
3922 .bt());
3923 }
3924 }
3925 let lhs_batch_dims: Vec<usize> = lhs_dims[..batch_rank].to_vec();
3926 let rhs_batch_dims: Vec<usize> = rhs_dims[..batch_rank].to_vec();
3927 let (m, k_lhs) = (lhs_dims[rank - 2], lhs_dims[rank - 1]);
3928 let (k_rhs, n) = (rhs_dims[rank - 2], rhs_dims[rank - 1]);
3929 if k_lhs != k_rhs {
3930 return Err(Error::Msg(format!(
3931 "Op::FusedLinear: contracting dims disagree — a trailing is \
3932 [{m}, {k_lhs}], b trailing is [{k_rhs}, {n}]",
3933 ))
3934 .bt());
3935 }
3936 if bias_dims.len() != 1 || bias_dims[0] != n {
3937 return Err(Error::Msg(format!(
3938 "Op::FusedLinear: bias must be rank-1 [{n}], got {bias_dims:?}",
3939 ))
3940 .bt());
3941 }
3942 OpParams::Matmul {
3943 lhs_batch_dims,
3944 rhs_batch_dims,
3945 m,
3946 n,
3947 k: k_lhs,
3948 m_compute: MatmulM::All,
3950 }
3951 }
3952 Op::Fused(
3957 _,
3958 fuel_graph::registry::FusedOpParams::FusedSoftmaxCrossEntropy {
3959 reduction,
3960 ignore_index,
3961 },
3962 ) => {
3963 if node.inputs.len() != 2 {
3964 return Err(Error::Msg(format!(
3965 "FusedSoftmaxCrossEntropy expects 2 inputs (logits, targets), got {}",
3966 node.inputs.len(),
3967 ))
3968 .bt());
3969 }
3970 let logits_layout = input_layout(node.inputs[0]);
3971 let logits_dims = logits_layout.shape().dims();
3972 if logits_dims.is_empty() {
3973 return Err(Error::Msg(
3974 "FusedSoftmaxCrossEntropy: logits must have rank ≥ 1".to_string(),
3975 )
3976 .bt());
3977 }
3978 let vocab = *logits_dims.last().unwrap();
3979 let n_rows: usize = logits_dims[..logits_dims.len() - 1]
3980 .iter()
3981 .product::<usize>()
3982 .max(1);
3983 let targets_layout = input_layout(node.inputs[1]);
3984 let target_count: usize = targets_layout
3985 .shape()
3986 .dims()
3987 .iter()
3988 .product::<usize>()
3989 .max(1);
3990 if target_count != n_rows {
3991 return Err(Error::Msg(format!(
3992 "FusedSoftmaxCrossEntropy: targets element count {target_count} must \
3993 equal logits row count {n_rows} (logits {logits_dims:?}, targets {:?})",
3994 targets_layout.shape().dims(),
3995 ))
3996 .bt());
3997 }
3998 OpParams::FusedSoftmaxCrossEntropy {
3999 n_rows,
4000 vocab,
4001 reduction: *reduction,
4002 ignore_index: *ignore_index,
4003 }
4004 }
4005 Op::Fused(_, fuel_graph::registry::FusedOpParams::CausalConv1d { use_silu }) => {
4010 if node.inputs.len() != 3 {
4011 return Err(Error::Msg(format!(
4012 "CausalConv1d expects 3 inputs (x, weight, bias), got {}",
4013 node.inputs.len(),
4014 ))
4015 .bt());
4016 }
4017 let x_layout = input_layout(node.inputs[0]);
4018 let w_layout = input_layout(node.inputs[1]);
4019 let x_dims = x_layout.shape().dims();
4020 let w_dims = w_layout.shape().dims();
4021 if x_dims.len() != 3 {
4022 return Err(Error::Msg(format!(
4023 "CausalConv1d: x must be rank 3 [batch, channels, seq+pad], got {x_dims:?}",
4024 ))
4025 .bt());
4026 }
4027 if w_dims.len() != 3 {
4028 return Err(Error::Msg(format!(
4029 "CausalConv1d: weight must be rank 3 [channels, 1, kernel], got {w_dims:?}",
4030 ))
4031 .bt());
4032 }
4033 let batch = x_dims[0];
4034 let channels = x_dims[1];
4035 let seq_in = x_dims[2];
4036 let kernel = w_dims[2];
4037 if seq_in < kernel - 1 {
4038 return Err(Error::Msg(format!(
4039 "CausalConv1d: x time dim {seq_in} must be ≥ kernel-1 = {} \
4040 (caller must pre-pad with kernel-1 zeros)",
4041 kernel - 1,
4042 ))
4043 .bt());
4044 }
4045 let seq_out = seq_in - (kernel - 1);
4046 OpParams::CausalConv1d {
4047 batch,
4048 channels,
4049 seq_in,
4050 seq_out,
4051 kernel,
4052 use_silu: *use_silu,
4053 }
4054 }
4055 Op::Fused(_, fuel_graph::registry::FusedOpParams::SelectiveScan { delta_softplus }) => {
4058 if node.inputs.len() != 5 {
4059 return Err(Error::Msg(format!(
4060 "SelectiveScan expects 5 inputs (u, delta, a, b, c), got {}",
4061 node.inputs.len(),
4062 ))
4063 .bt());
4064 }
4065 let u_layout = input_layout(node.inputs[0]);
4066 let a_layout = input_layout(node.inputs[2]);
4067 let u_dims = u_layout.shape().dims();
4068 let a_dims = a_layout.shape().dims();
4069 if u_dims.len() != 3 {
4070 return Err(Error::Msg(format!(
4071 "SelectiveScan: u must be rank 3 [batch, seqlen, dim], got {u_dims:?}",
4072 ))
4073 .bt());
4074 }
4075 if a_dims.len() != 2 {
4076 return Err(Error::Msg(format!(
4077 "SelectiveScan: a must be rank 2 [dim, dstate], got {a_dims:?}",
4078 ))
4079 .bt());
4080 }
4081 let batch = u_dims[0];
4082 let seqlen = u_dims[1];
4083 let dim = u_dims[2];
4084 let dstate = a_dims[1];
4085 if a_dims[0] != dim {
4086 return Err(Error::Msg(format!(
4087 "SelectiveScan: a's first dim {} must equal dim {dim}",
4088 a_dims[0],
4089 ))
4090 .bt());
4091 }
4092 OpParams::SelectiveScan {
4093 batch,
4094 seqlen,
4095 dim,
4096 dstate,
4097 delta_softplus: *delta_softplus,
4098 }
4099 }
4100 Op::Fused(_, fuel_graph::registry::FusedOpParams::SsdChunkScan { chunk_size }) => {
4104 if node.inputs.len() != 5 {
4105 return Err(Error::Msg(format!(
4106 "SsdChunkScan expects 5 inputs (x, dt, a, b, c), got {}",
4107 node.inputs.len(),
4108 ))
4109 .bt());
4110 }
4111 let x_layout = input_layout(node.inputs[0]);
4112 let b_layout = input_layout(node.inputs[3]);
4113 let x_dims = x_layout.shape().dims();
4114 let b_dims = b_layout.shape().dims();
4115 if x_dims.len() != 4 {
4116 return Err(Error::Msg(format!(
4117 "SsdChunkScan: x must be rank 4 [batch, seqlen, heads, head_dim], got {x_dims:?}",
4118 ))
4119 .bt());
4120 }
4121 if b_dims.len() != 4 {
4122 return Err(Error::Msg(format!(
4123 "SsdChunkScan: b must be rank 4 [batch, seqlen, heads, state_dim], got {b_dims:?}",
4124 ))
4125 .bt());
4126 }
4127 let batch = x_dims[0];
4128 let seqlen = x_dims[1];
4129 let heads = x_dims[2];
4130 let head_dim = x_dims[3];
4131 let state_dim = b_dims[3];
4132 OpParams::SsdChunkScan {
4133 batch,
4134 seqlen,
4135 heads,
4136 head_dim,
4137 state_dim,
4138 chunk_size: *chunk_size,
4139 }
4140 }
4141 Op::Fused(_, fuel_graph::registry::FusedOpParams::Nf4Matmul { block_size }) => {
4145 if node.inputs.len() != 3 {
4146 return Err(Error::Msg(format!(
4147 "Nf4Matmul expects 3 inputs (activations, w_packed, absmax), got {}",
4148 node.inputs.len(),
4149 ))
4150 .bt());
4151 }
4152 let a_layout = input_layout(node.inputs[0]);
4153 let w_layout = input_layout(node.inputs[1]);
4154 let a_dims = a_layout.shape().dims();
4155 let w_dims = w_layout.shape().dims();
4156 if a_dims.len() < 2 {
4157 return Err(Error::Msg(format!(
4158 "Nf4Matmul: activations must be rank ≥ 2, got {a_dims:?}",
4159 ))
4160 .bt());
4161 }
4162 if w_dims.len() != 2 {
4163 return Err(Error::Msg(format!(
4164 "Nf4Matmul: w_packed must be rank 2 [n, k/2], got {w_dims:?}",
4165 ))
4166 .bt());
4167 }
4168 let m = a_dims[a_dims.len() - 2];
4169 let k = a_dims[a_dims.len() - 1];
4170 let n = w_dims[0];
4171 let batch: usize = a_dims[..a_dims.len() - 2].iter().product::<usize>().max(1);
4172 if k % 2 != 0 {
4173 return Err(Error::Msg(format!(
4174 "Nf4Matmul: k={k} must be even (w_packed holds 2 nibbles per byte along k)",
4175 ))
4176 .bt());
4177 }
4178 if k % *block_size != 0 {
4179 return Err(Error::Msg(format!(
4180 "Nf4Matmul: k={k} must be a multiple of block_size={block_size}",
4181 ))
4182 .bt());
4183 }
4184 if w_dims[1] != k / 2 {
4185 return Err(Error::Msg(format!(
4186 "Nf4Matmul: w_packed second dim {} must equal k/2 = {}",
4187 w_dims[1],
4188 k / 2,
4189 ))
4190 .bt());
4191 }
4192 OpParams::Nf4Matmul {
4193 batch,
4194 m,
4195 n,
4196 k,
4197 block_size: *block_size,
4198 }
4199 }
4200 Op::Fused(
4204 _,
4205 fuel_graph::registry::FusedOpParams::AwqMatmul {
4206 group_size,
4207 split_k_iters,
4208 },
4209 ) => {
4210 if node.inputs.len() != 4 {
4211 return Err(Error::Msg(format!(
4212 "AwqMatmul expects 4 inputs (activations, weight, zeros, scales), got {}",
4213 node.inputs.len(),
4214 ))
4215 .bt());
4216 }
4217 let a_layout = input_layout(node.inputs[0]);
4218 let w_layout = input_layout(node.inputs[1]);
4219 let a_dims = a_layout.shape().dims();
4220 let w_dims = w_layout.shape().dims();
4221 if a_dims.len() < 2 {
4222 return Err(Error::Msg(format!(
4223 "AwqMatmul: activations must be rank ≥ 2, got {a_dims:?}",
4224 ))
4225 .bt());
4226 }
4227 if w_dims.len() != 2 {
4228 return Err(Error::Msg(format!(
4229 "AwqMatmul: weight must be rank 2 [n, k/8], got {w_dims:?}",
4230 ))
4231 .bt());
4232 }
4233 let m = a_dims[a_dims.len() - 2];
4234 let k = a_dims[a_dims.len() - 1];
4235 let n = w_dims[0];
4236 let batch: usize = a_dims[..a_dims.len() - 2].iter().product::<usize>().max(1);
4237 if k % 8 != 0 || w_dims[1] != k / 8 {
4238 return Err(Error::Msg(format!(
4239 "AwqMatmul: weight second dim {} must equal k/8 for k={k} (a multiple of 8)",
4240 w_dims[1],
4241 ))
4242 .bt());
4243 }
4244 OpParams::AwqMatmul {
4245 batch,
4246 m,
4247 n,
4248 k,
4249 group_size: *group_size,
4250 split_k_iters: *split_k_iters,
4251 }
4252 }
4253 op if op_is_softmax_last_dim(op)
4261 || matches!(op, Op::Fused(fid, _) if *fid == fuel_graph::registry::FusedOps::SOFTMAX_LAST_DIM_BACKWARD) =>
4262 {
4263 let il = input_layout(node.inputs[0]);
4264 let dims = il.shape().dims();
4265 if dims.is_empty() {
4266 return Err(Error::Msg("Op::SoftmaxLastDim requires rank ≥ 1".to_string()).bt());
4267 }
4268 let last_dim = *dims.last().unwrap();
4269 let outer_count: usize = dims[..dims.len() - 1].iter().product();
4270 OpParams::SoftmaxLastDim {
4271 outer_count,
4272 last_dim,
4273 }
4274 }
4275 Op::Flip { dim } => {
4276 if node.inputs.len() != 1 {
4279 return Err(Error::Msg(format!(
4280 "Op::Flip expects 1 input, got {}",
4281 node.inputs.len(),
4282 ))
4283 .bt());
4284 }
4285 let in_layout = input_layout(node.inputs[0]);
4286 let in_dims = in_layout.shape().dims();
4287 if *dim >= in_dims.len() {
4288 return Err(Error::Msg(format!(
4289 "Op::Flip: dim {dim} out of range for rank {}",
4290 in_dims.len(),
4291 ))
4292 .bt());
4293 }
4294 let outer_count: usize = in_dims[..*dim].iter().product();
4295 let dim_size = in_dims[*dim];
4296 let inner_count: usize = in_dims[*dim + 1..].iter().product();
4297 OpParams::Flip {
4298 outer_count,
4299 dim_size,
4300 inner_count,
4301 axis: *dim,
4302 }
4303 }
4304 Op::Roll { dim, shift } => {
4305 if node.inputs.len() != 1 {
4306 return Err(Error::Msg(format!(
4307 "Op::Roll expects 1 input, got {}",
4308 node.inputs.len(),
4309 ))
4310 .bt());
4311 }
4312 let in_layout = input_layout(node.inputs[0]);
4313 let in_dims = in_layout.shape().dims();
4314 if *dim >= in_dims.len() {
4315 return Err(Error::Msg(format!(
4316 "Op::Roll: dim {dim} out of range for rank {}",
4317 in_dims.len(),
4318 ))
4319 .bt());
4320 }
4321 let outer_count: usize = in_dims[..*dim].iter().product();
4322 let dim_size = in_dims[*dim];
4323 let inner_count: usize = in_dims[*dim + 1..].iter().product();
4324 OpParams::Roll {
4325 outer_count,
4326 dim_size,
4327 inner_count,
4328 shift: *shift,
4329 axis: *dim,
4330 }
4331 }
4332 Op::CumSum { dim } => {
4333 if node.inputs.len() != 1 {
4334 return Err(Error::Msg(format!(
4335 "Op::CumSum expects 1 input, got {}",
4336 node.inputs.len(),
4337 ))
4338 .bt());
4339 }
4340 let in_layout = input_layout(node.inputs[0]);
4341 let in_dims = in_layout.shape().dims();
4342 if *dim >= in_dims.len() {
4343 return Err(Error::Msg(format!(
4344 "Op::CumSum: dim {dim} out of range for rank {}",
4345 in_dims.len(),
4346 ))
4347 .bt());
4348 }
4349 let outer_count: usize = in_dims[..*dim].iter().product();
4350 let dim_size = in_dims[*dim];
4351 let inner_count: usize = in_dims[*dim + 1..].iter().product();
4352 OpParams::CumSum {
4353 outer_count,
4354 dim_size,
4355 inner_count,
4356 axis: *dim,
4357 }
4358 }
4359 Op::Triu { diagonal } | Op::Tril { diagonal } => {
4360 if node.inputs.len() != 1 {
4361 return Err(Error::Msg(format!(
4362 "Op::Triu/Tril expects 1 input, got {}",
4363 node.inputs.len(),
4364 ))
4365 .bt());
4366 }
4367 let in_layout = input_layout(node.inputs[0]);
4368 let in_dims = in_layout.shape().dims();
4369 if in_dims.len() < 2 {
4370 return Err(Error::Msg(format!(
4371 "Op::Triu/Tril requires rank >= 2, got {}",
4372 in_dims.len(),
4373 ))
4374 .bt());
4375 }
4376 let rows = in_dims[in_dims.len() - 2];
4377 let cols = in_dims[in_dims.len() - 1];
4378 let batch_count: usize = in_dims[..in_dims.len() - 2]
4379 .iter()
4380 .product::<usize>()
4381 .max(1);
4382 OpParams::Triangular {
4383 batch_count,
4384 rows,
4385 cols,
4386 diagonal: *diagonal,
4387 }
4388 }
4389 Op::LogSoftmaxLastDim => {
4390 if node.inputs.len() != 1 {
4391 return Err(Error::Msg(format!(
4392 "Op::LogSoftmaxLastDim expects 1 input, got {}",
4393 node.inputs.len(),
4394 ))
4395 .bt());
4396 }
4397 let in_layout = input_layout(node.inputs[0]);
4398 let in_dims = in_layout.shape().dims();
4399 if in_dims.is_empty() {
4400 return Err(Error::Msg("Op::LogSoftmaxLastDim requires rank ≥ 1".to_string()).bt());
4401 }
4402 let last_dim = *in_dims.last().unwrap();
4403 let outer_count: usize = in_dims[..in_dims.len() - 1].iter().product();
4404 OpParams::LogSoftmaxLastDim {
4405 outer_count,
4406 last_dim,
4407 }
4408 }
4409 Op::LogSoftmaxLastDimBackward => {
4410 if node.inputs.len() != 2 {
4413 return Err(Error::Msg(format!(
4414 "Op::LogSoftmaxLastDimBackward expects 2 inputs, got {}",
4415 node.inputs.len(),
4416 ))
4417 .bt());
4418 }
4419 let in_layout = input_layout(node.inputs[0]);
4420 let in_dims = in_layout.shape().dims();
4421 if in_dims.is_empty() {
4422 return Err(Error::Msg(
4423 "Op::LogSoftmaxLastDimBackward requires rank ≥ 1".to_string(),
4424 )
4425 .bt());
4426 }
4427 let last_dim = *in_dims.last().unwrap();
4428 let outer_count: usize = in_dims[..in_dims.len() - 1].iter().product();
4429 OpParams::LogSoftmaxLastDim {
4430 outer_count,
4431 last_dim,
4432 }
4433 }
4434 Op::MaskedFill { value } => {
4435 if node.inputs.len() != 2 {
4436 return Err(Error::Msg(format!(
4437 "Op::MaskedFill expects 2 inputs (x, mask), got {}",
4438 node.inputs.len(),
4439 ))
4440 .bt());
4441 }
4442 let fill_bytes = scalar_to_bytes(*value);
4443 OpParams::MaskedFill { fill_bytes }
4444 }
4445 Op::PadBackward {
4448 in_shape,
4449 padding,
4450 mode,
4451 } => {
4452 if node.inputs.len() != 1 {
4455 return Err(Error::Msg(format!(
4456 "Op::PadBackward expects 1 input, got {}",
4457 node.inputs.len(),
4458 ))
4459 .bt());
4460 }
4461 let in_dims: Vec<usize> = in_shape.dims().to_vec();
4462 let out_dims: Vec<usize> = in_dims
4465 .iter()
4466 .zip(padding.iter())
4467 .map(|(&d, &(b, a))| d + b + a)
4468 .collect();
4469 let mode_tag: u8 = match mode {
4470 fuel_graph::PadMode::Constant => 0,
4471 fuel_graph::PadMode::Reflect => 1,
4472 fuel_graph::PadMode::Replicate => 2,
4473 };
4474 OpParams::PadBackward {
4475 in_shape: in_dims,
4476 out_shape: out_dims,
4477 padding: padding.clone(),
4478 mode_tag,
4479 }
4480 }
4481 Op::Pad {
4482 padding,
4483 mode,
4484 value,
4485 } => {
4486 if node.inputs.len() != 1 {
4487 return Err(Error::Msg(format!(
4488 "Op::Pad expects 1 input, got {}",
4489 node.inputs.len(),
4490 ))
4491 .bt());
4492 }
4493 let in_layout = input_layout(node.inputs[0]);
4494 let in_dims: Vec<usize> = in_layout.shape().dims().to_vec();
4495 if padding.len() != in_dims.len() {
4496 return Err(Error::Msg(format!(
4497 "Op::Pad: padding.len() ({}) != input rank ({})",
4498 padding.len(),
4499 in_dims.len(),
4500 ))
4501 .bt());
4502 }
4503 let out_dims: Vec<usize> = in_dims
4504 .iter()
4505 .zip(padding.iter())
4506 .map(|(&d, &(b, a))| d + b + a)
4507 .collect();
4508 let mode_tag: u8 = match mode {
4509 fuel_graph::PadMode::Constant => 0,
4510 fuel_graph::PadMode::Reflect => 1,
4511 fuel_graph::PadMode::Replicate => 2,
4512 };
4513 let fill_bytes = encode_value_to_bytes(node.dtype, *value)?;
4518 OpParams::Pad {
4519 in_shape: in_dims,
4520 out_shape: out_dims,
4521 padding: padding.clone(),
4522 mode_tag,
4523 fill_bytes,
4524 }
4525 }
4526 Op::IndexAdd { dim } => {
4527 if node.inputs.len() != 3 {
4530 return Err(Error::Msg(format!(
4531 "Op::IndexAdd expects 3 inputs (base, indices, src), got {}",
4532 node.inputs.len(),
4533 ))
4534 .bt());
4535 }
4536 let base_layout = input_layout(node.inputs[0]);
4537 let idx_layout = input_layout(node.inputs[1]);
4538 let src_layout = input_layout(node.inputs[2]);
4539 let base_dims = base_layout.shape().dims();
4540 let idx_dims = idx_layout.shape().dims();
4541 let src_dims = src_layout.shape().dims();
4542 if *dim >= base_dims.len() {
4543 return Err(Error::Msg(format!(
4544 "Op::IndexAdd: dim {dim} out of range for base rank {}",
4545 base_dims.len(),
4546 ))
4547 .bt());
4548 }
4549 if idx_dims.len() != 1 {
4550 return Err(Error::Msg(format!(
4551 "Op::IndexAdd: indices must be rank 1, got {idx_dims:?}",
4552 ))
4553 .bt());
4554 }
4555 if base_dims.len() != src_dims.len() {
4556 return Err(Error::Msg(format!(
4557 "Op::IndexAdd: base rank ({}) != src rank ({})",
4558 base_dims.len(),
4559 src_dims.len(),
4560 ))
4561 .bt());
4562 }
4563 let outer_count: usize = base_dims[..*dim].iter().product();
4564 let base_dim_size = base_dims[*dim];
4565 let inner_count: usize = base_dims[*dim + 1..].iter().product();
4566 let n_indices = idx_dims[0];
4567 OpParams::IndexAdd {
4568 outer_count,
4569 base_dim_size,
4570 n_indices,
4571 inner_count,
4572 }
4573 }
4574 Op::ScatterAdd { dim } => {
4575 if node.inputs.len() != 3 {
4576 return Err(Error::Msg(format!(
4577 "Op::ScatterAdd expects 3 inputs (base, indices, src), got {}",
4578 node.inputs.len(),
4579 ))
4580 .bt());
4581 }
4582 let base_layout = input_layout(node.inputs[0]);
4583 let src_layout = input_layout(node.inputs[2]);
4584 let base_shape: Vec<usize> = base_layout.shape().dims().to_vec();
4585 let src_shape: Vec<usize> = src_layout.shape().dims().to_vec();
4586 if base_shape.len() != src_shape.len() {
4587 return Err(Error::Msg(format!(
4588 "Op::ScatterAdd: base rank ({}) != src rank ({})",
4589 base_shape.len(),
4590 src_shape.len(),
4591 ))
4592 .bt());
4593 }
4594 if *dim >= base_shape.len() {
4595 return Err(Error::Msg(format!(
4596 "Op::ScatterAdd: dim {dim} out of range for rank {}",
4597 base_shape.len(),
4598 ))
4599 .bt());
4600 }
4601 OpParams::ScatterAdd {
4602 base_shape,
4603 src_shape,
4604 dim: *dim,
4605 }
4606 }
4607 Op::Fused(_, fuel_graph::registry::FusedOpParams::Rope) => {
4610 if node.inputs.len() != 3 {
4613 return Err(Error::Msg(format!(
4614 "Op::Rope expects 3 inputs (x, cos, sin), got {}",
4615 node.inputs.len(),
4616 ))
4617 .bt());
4618 }
4619 let x_layout = input_layout(node.inputs[0]);
4620 let x_dims = x_layout.shape().dims();
4621 if x_dims.len() < 2 {
4622 return Err(
4623 Error::Msg(format!("Op::Rope: x must have rank ≥ 2, got {x_dims:?}",)).bt(),
4624 );
4625 }
4626 let head_dim = *x_dims.last().unwrap();
4627 let seq = x_dims[x_dims.len() - 2];
4628 let outer_count: usize = x_dims[..x_dims.len() - 2].iter().product();
4629 OpParams::Rope {
4630 outer_count,
4631 seq,
4632 head_dim,
4633 }
4634 }
4635 Op::Gather { dim } => {
4636 if node.inputs.len() != 2 {
4638 return Err(Error::Msg(format!(
4639 "Op::Gather expects 2 inputs (source, indices), got {}",
4640 node.inputs.len(),
4641 ))
4642 .bt());
4643 }
4644 let src_layout = input_layout(node.inputs[0]);
4645 let idx_layout = input_layout(node.inputs[1]);
4646 let source_shape: Vec<usize> = src_layout.shape().dims().to_vec();
4647 let output_shape: Vec<usize> = idx_layout.shape().dims().to_vec();
4648 if source_shape.len() != output_shape.len() {
4649 return Err(Error::Msg(format!(
4650 "Op::Gather: source rank ({}) != indices rank ({})",
4651 source_shape.len(),
4652 output_shape.len(),
4653 ))
4654 .bt());
4655 }
4656 if *dim >= source_shape.len() {
4657 return Err(Error::Msg(format!(
4658 "Op::Gather: dim {dim} out of range for rank {}",
4659 source_shape.len(),
4660 ))
4661 .bt());
4662 }
4663 OpParams::Gather {
4664 source_shape,
4665 output_shape,
4666 dim: *dim,
4667 }
4668 }
4669 Op::NonZeroIndices { count_sym } => {
4670 if node.inputs.len() != 1 {
4673 return Err(Error::Msg(format!(
4674 "Op::NonZeroIndices expects 1 input, got {}",
4675 node.inputs.len(),
4676 ))
4677 .bt());
4678 }
4679 let x_layout = input_layout(node.inputs[0]);
4680 let capacity = x_layout.shape().elem_count();
4681 OpParams::NonZeroIndices {
4682 capacity,
4683 count_sym: *count_sym,
4684 }
4685 }
4686 Op::IndexSelect { dim } => {
4687 if node.inputs.len() != 2 {
4689 return Err(Error::Msg(format!(
4690 "Op::IndexSelect expects 2 inputs (source, indices), got {}",
4691 node.inputs.len(),
4692 ))
4693 .bt());
4694 }
4695 let src_layout = input_layout(node.inputs[0]);
4696 let idx_layout = input_layout(node.inputs[1]);
4697 let src_dims = src_layout.shape().dims();
4698 let idx_dims = idx_layout.shape().dims();
4699 if *dim >= src_dims.len() {
4700 return Err(Error::Msg(format!(
4701 "Op::IndexSelect: dim {dim} out of range for source rank {}",
4702 src_dims.len(),
4703 ))
4704 .bt());
4705 }
4706 if idx_dims.len() != 1 {
4707 return Err(Error::Msg(format!(
4708 "Op::IndexSelect: indices must be rank 1, got shape {idx_dims:?}",
4709 ))
4710 .bt());
4711 }
4712 let outer_count: usize = src_dims[..*dim].iter().product();
4713 let source_dim_size = src_dims[*dim];
4714 let inner_count: usize = src_dims[*dim + 1..].iter().product();
4715 let n_indices = idx_dims[0];
4716 OpParams::IndexSelect {
4717 outer_count,
4718 source_dim_size,
4719 n_indices,
4720 inner_count,
4721 }
4722 }
4723 Op::Fused(fid, params)
4729 if *fid == fuel_graph::registry::FusedOps::RMS_NORM_LAST_DIM
4730 || *fid == fuel_graph::registry::FusedOps::LAYER_NORM_LAST_DIM
4731 || *fid == fuel_graph::registry::FusedOps::RMS_NORM_LAST_DIM_BACKWARD
4732 || *fid == fuel_graph::registry::FusedOps::LAYER_NORM_LAST_DIM_BACKWARD =>
4733 {
4734 let eps = match params {
4735 fuel_graph::registry::FusedOpParams::RmsNormLastDim { eps } => *eps,
4736 fuel_graph::registry::FusedOpParams::LayerNormLastDim { eps } => *eps,
4737 fuel_graph::registry::FusedOpParams::RmsNormLastDimBackward { eps } => *eps,
4738 fuel_graph::registry::FusedOpParams::LayerNormLastDimBackward { eps } => *eps,
4739 _ => {
4740 return Err(Error::Msg(format!(
4741 "Op::Fused(NORM_LAST_DIM[_BACKWARD], _) expected \
4742 RmsNormLastDim[Backward] or LayerNormLastDim[Backward] params, got {params:?}",
4743 ))
4744 .bt());
4745 }
4746 };
4747 let il = input_layout(node.inputs[0]);
4748 let dims = il.shape().dims();
4749 if dims.is_empty() {
4750 return Err(Error::Msg(
4751 "Op::Fused(NORM_LAST_DIM[_BACKWARD], _) requires rank ≥ 1".to_string(),
4752 )
4753 .bt());
4754 }
4755 let last_dim = *dims.last().unwrap();
4756 let outer_count: usize = dims[..dims.len() - 1].iter().product();
4757 OpParams::NormLastDim {
4758 outer_count,
4759 last_dim,
4760 eps,
4761 }
4762 }
4763 Op::Fused(fid, _) if *fid == fuel_graph::registry::FusedOps::REDUCE_MAX_TO_BACKWARD => {
4767 if node.inputs.len() != 2 {
4768 return Err(Error::Msg(format!(
4769 "Op::Fused(REDUCE_MAX_TO_BACKWARD) expects 2 inputs, got {}",
4770 node.inputs.len(),
4771 ))
4772 .bt());
4773 }
4774 let x_layout = input_layout(node.inputs[0]);
4775 let up_layout = input_layout(node.inputs[1]);
4776 let input_shape: Vec<usize> = x_layout.shape().dims().to_vec();
4777 let output_shape: Vec<usize> = up_layout.shape().dims().to_vec();
4778 OpParams::ReduceMaxToBackward {
4779 input_shape,
4780 output_shape,
4781 }
4782 }
4783 Op::Concat { dim } => {
4784 if node.inputs.is_empty() {
4789 return Err(Error::Msg("Op::Concat requires at least 1 input".to_string()).bt());
4790 }
4791 let out_dims = node.shape.dims();
4792 if *dim >= out_dims.len() {
4793 return Err(Error::Msg(format!(
4794 "Op::Concat: dim {dim} out of range for output rank {}",
4795 out_dims.len(),
4796 ))
4797 .bt());
4798 }
4799 let outer_count: usize = out_dims[..*dim].iter().product();
4800 let inner_count: usize = out_dims[*dim + 1..].iter().product();
4801 let mut input_dim_sizes: Vec<usize> = Vec::with_capacity(node.inputs.len());
4802 for in_id in &node.inputs {
4803 let il = input_layout(*in_id);
4804 let il_dims = il.shape().dims();
4805 if *dim >= il_dims.len() {
4806 return Err(Error::Msg(format!(
4807 "Op::Concat: input {in_id:?} has rank {} but concat dim is {dim}",
4808 il_dims.len(),
4809 ))
4810 .bt());
4811 }
4812 input_dim_sizes.push(il_dims[*dim]);
4813 }
4814 OpParams::Concat {
4815 outer_count,
4816 input_dim_sizes,
4817 inner_count,
4818 axis: *dim,
4819 }
4820 }
4821 Op::AddScalar(c) => OpParams::Affine { mul: 1.0, add: *c },
4822 Op::MulScalar(c) => OpParams::Affine { mul: *c, add: 0.0 },
4823 Op::Clamp { min, max } => OpParams::Clamp {
4824 min: *min,
4825 max: *max,
4826 },
4827 Op::PowI(exp) => OpParams::PowI { exp: *exp },
4828 Op::ClampInplace { min, max } => OpParams::Clamp {
4829 min: *min,
4830 max: *max,
4831 },
4832 Op::PowIInplace(exp) => OpParams::PowI { exp: *exp },
4833 Op::Fused(_, fuel_graph::registry::FusedOpParams::PowIBackward { exp }) => {
4837 OpParams::PowI { exp: *exp }
4838 }
4839 Op::Fused(_, fuel_graph::registry::FusedOpParams::InplaceAffine { mul, add }) => {
4845 OpParams::Affine {
4846 mul: *mul,
4847 add: *add,
4848 }
4849 }
4850 Op::Fused(
4853 _,
4854 fuel_graph::registry::FusedOpParams::Conv2D {
4855 stride,
4856 padding,
4857 groups,
4858 },
4859 ) => {
4860 if node.inputs.len() != 2 && node.inputs.len() != 3 {
4864 return Err(Error::Msg(format!(
4865 "Op::Conv2D expects 2 or 3 inputs, got {}",
4866 node.inputs.len(),
4867 ))
4868 .bt());
4869 }
4870 let x_layout = input_layout(node.inputs[0]);
4871 let w_layout = input_layout(node.inputs[1]);
4872 let x_dims = x_layout.shape().dims();
4873 let w_dims = w_layout.shape().dims();
4874 if x_dims.len() != 4 || w_dims.len() != 4 {
4875 return Err(Error::Msg(format!(
4876 "Op::Conv2D requires rank-4 x and weight; got x={x_dims:?} w={w_dims:?}",
4877 ))
4878 .bt());
4879 }
4880 let out_dims = node.shape.dims();
4881 if out_dims.len() != 4 {
4882 return Err(Error::Msg(format!(
4883 "Op::Conv2D output must be rank 4, got {out_dims:?}",
4884 ))
4885 .bt());
4886 }
4887 let x_shape = [x_dims[0], x_dims[1], x_dims[2], x_dims[3]];
4888 let w_shape = [w_dims[0], w_dims[1], w_dims[2], w_dims[3]];
4889 let out_shape = [out_dims[0], out_dims[1], out_dims[2], out_dims[3]];
4890 OpParams::Conv2D {
4891 x_shape,
4892 w_shape,
4893 out_shape,
4894 stride: *stride,
4895 padding: *padding,
4896 dilation: (1, 1),
4897 groups: *groups,
4898 }
4899 }
4900 Op::Fused(
4902 _,
4903 fuel_graph::registry::FusedOpParams::PagedAttn {
4904 softmax_scale,
4905 block_size,
4906 softcap,
4907 },
4908 ) => {
4909 if node.inputs.len() != 5 && node.inputs.len() != 6 {
4914 return Err(Error::Msg(format!(
4915 "Op::PagedAttn expects 5 or 6 inputs, got {}",
4916 node.inputs.len(),
4917 ))
4918 .bt());
4919 }
4920 let q_layout = input_layout(node.inputs[0]);
4921 let kc_layout = input_layout(node.inputs[1]);
4922 let vc_layout = input_layout(node.inputs[2]);
4923 let bt_layout = input_layout(node.inputs[3]);
4924 let q_dims = q_layout.shape().dims();
4925 let kc_dims = kc_layout.shape().dims();
4926 let vc_dims = vc_layout.shape().dims();
4927 let bt_dims = bt_layout.shape().dims();
4928 if q_dims.len() != 4 || kc_dims.len() != 4 || vc_dims.len() != 4 {
4929 return Err(Error::Msg(format!(
4930 "Op::PagedAttn requires rank-4 q/k_cache/v_cache; \
4931 got q={q_dims:?} k_cache={kc_dims:?} v_cache={vc_dims:?}",
4932 ))
4933 .bt());
4934 }
4935 if kc_dims != vc_dims {
4936 return Err(Error::Msg(format!(
4937 "Op::PagedAttn: k_cache {kc_dims:?} and v_cache {vc_dims:?} must match",
4938 ))
4939 .bt());
4940 }
4941 if bt_dims.len() != 2 {
4942 return Err(Error::Msg(format!(
4943 "Op::PagedAttn: block_table must be rank 2 [B, max_blocks_per_seq], got {bt_dims:?}",
4944 ))
4945 .bt());
4946 }
4947 if kc_dims[1] != *block_size {
4948 return Err(Error::Msg(format!(
4949 "Op::PagedAttn: k_cache block_size dim ({}) must equal Op's block_size ({})",
4950 kc_dims[1], block_size,
4951 ))
4952 .bt());
4953 }
4954 OpParams::PagedAttn {
4955 b: q_dims[0],
4956 hq: q_dims[1],
4957 hkv: kc_dims[2],
4958 sq: q_dims[2],
4959 d: q_dims[3],
4960 block_size: *block_size,
4961 max_blocks_per_seq: bt_dims[1],
4962 num_blocks: kc_dims[0],
4963 softmax_scale: *softmax_scale,
4964 softcap: *softcap,
4965 }
4966 }
4967 Op::Fused(
4969 _,
4970 fuel_graph::registry::FusedOpParams::FlashAttn {
4971 softmax_scale,
4972 causal,
4973 window_size_left,
4974 window_size_right,
4975 softcap,
4976 k_len,
4977 },
4978 ) => {
4979 if node.inputs.len() != 3 && node.inputs.len() != 4 {
4984 return Err(Error::Msg(format!(
4985 "Op::FlashAttn expects 3 or 4 inputs, got {}",
4986 node.inputs.len(),
4987 ))
4988 .bt());
4989 }
4990 let q_layout = input_layout(node.inputs[0]);
4991 let k_layout = input_layout(node.inputs[1]);
4992 let v_layout = input_layout(node.inputs[2]);
4993 let q_dims = q_layout.shape().dims();
4994 let k_dims = k_layout.shape().dims();
4995 let v_dims = v_layout.shape().dims();
4996 if q_dims.len() != 4 || k_dims.len() != 4 || v_dims.len() != 4 {
4997 return Err(Error::Msg(format!(
4998 "Op::FlashAttn requires rank-4 q/k/v; got q={q_dims:?} k={k_dims:?} v={v_dims:?}",
4999 ))
5000 .bt());
5001 }
5002 if k_dims != v_dims {
5003 return Err(Error::Msg(format!(
5004 "Op::FlashAttn: k {k_dims:?} and v {v_dims:?} must share shape",
5005 ))
5006 .bt());
5007 }
5008 if q_dims[0] != k_dims[0] || q_dims[3] != k_dims[3] {
5009 return Err(Error::Msg(format!(
5010 "Op::FlashAttn: q {q_dims:?} and k {k_dims:?} must share B and D",
5011 ))
5012 .bt());
5013 }
5014 let sk = k_dims[2];
5015 let attended_k_len = match k_len {
5020 None => sk,
5021 Some(dyn_k) => {
5022 let kl = dyn_k.resolve(sym_env).ok_or_else(|| {
5023 Error::Msg(format!(
5024 "Op::FlashAttn: dynamic k_len {dyn_k:?} is unbound in the SymEnv at realize",
5025 ))
5026 .bt()
5027 })?;
5028 if kl > sk {
5029 return Err(Error::Msg(format!(
5030 "Op::FlashAttn: resolved k_len ({kl}) exceeds K capacity ({sk})",
5031 ))
5032 .bt());
5033 }
5034 if kl < q_dims[2] {
5035 return Err(Error::Msg(format!(
5036 "Op::FlashAttn: resolved k_len ({kl}) < Sq ({}) — not a valid causal prefix",
5037 q_dims[2],
5038 ))
5039 .bt());
5040 }
5041 kl
5042 }
5043 };
5044 OpParams::FlashAttn {
5045 b: q_dims[0],
5046 hq: q_dims[1],
5047 hkv: k_dims[1],
5048 sq: q_dims[2],
5049 sk,
5050 d: q_dims[3],
5051 k_len: attended_k_len,
5052 softmax_scale: *softmax_scale,
5053 causal: *causal,
5054 window_size_left: *window_size_left,
5055 window_size_right: *window_size_right,
5056 softcap: *softcap,
5057 }
5058 }
5059 Op::ReduceSumTo(target_shape) => {
5060 if node.inputs.len() != 1 {
5061 return Err(Error::Msg(format!(
5062 "Op::ReduceSumTo expects 1 input, got {}",
5063 node.inputs.len(),
5064 ))
5065 .bt());
5066 }
5067 let in_layout = input_layout(node.inputs[0]);
5068 let input_shape: Vec<usize> = in_layout.shape().dims().to_vec();
5069 let output_shape: Vec<usize> = target_shape.dims().to_vec();
5070 OpParams::ReduceSumTo {
5071 input_shape,
5072 output_shape,
5073 }
5074 }
5075 Op::ReduceMaxTo(target_shape) => {
5076 if node.inputs.len() != 1 {
5077 return Err(Error::Msg(format!(
5078 "Op::ReduceMaxTo expects 1 input, got {}",
5079 node.inputs.len(),
5080 ))
5081 .bt());
5082 }
5083 let in_layout = input_layout(node.inputs[0]);
5084 let input_shape: Vec<usize> = in_layout.shape().dims().to_vec();
5085 let output_shape: Vec<usize> = target_shape.dims().to_vec();
5086 OpParams::ReduceMaxTo {
5087 input_shape,
5088 output_shape,
5089 }
5090 }
5091 Op::Fused(
5093 _,
5094 fuel_graph::registry::FusedOpParams::ConvTranspose2D {
5095 stride,
5096 padding,
5097 output_padding,
5098 dilation,
5099 groups,
5100 },
5101 ) => {
5102 if node.inputs.len() != 2 && node.inputs.len() != 3 {
5106 return Err(Error::Msg(format!(
5107 "Op::ConvTranspose2D expects 2 or 3 inputs, got {}",
5108 node.inputs.len(),
5109 ))
5110 .bt());
5111 }
5112 let x_layout = input_layout(node.inputs[0]);
5113 let w_layout = input_layout(node.inputs[1]);
5114 let x_dims = x_layout.shape().dims();
5115 let w_dims = w_layout.shape().dims();
5116 if x_dims.len() != 4 || w_dims.len() != 4 {
5117 return Err(Error::Msg(format!(
5118 "Op::ConvTranspose2D requires rank-4 x and weight; got x={x_dims:?} w={w_dims:?}",
5119 ))
5120 .bt());
5121 }
5122 let out_dims = node.shape.dims();
5123 if out_dims.len() != 4 {
5124 return Err(Error::Msg(format!(
5125 "Op::ConvTranspose2D output must be rank 4, got {out_dims:?}",
5126 ))
5127 .bt());
5128 }
5129 let x_shape = [x_dims[0], x_dims[1], x_dims[2], x_dims[3]];
5130 let w_shape = [w_dims[0], w_dims[1], w_dims[2], w_dims[3]];
5131 let out_shape = [out_dims[0], out_dims[1], out_dims[2], out_dims[3]];
5132 OpParams::ConvTranspose2D {
5133 x_shape,
5134 w_shape,
5135 out_shape,
5136 stride: *stride,
5137 padding: *padding,
5138 output_padding: *output_padding,
5139 dilation: *dilation,
5140 groups: *groups,
5141 }
5142 }
5143 Op::WriteSlice { ranges, dyn_offset } => {
5144 if node.inputs.len() != 2 {
5150 return Err(Error::Msg(format!(
5151 "Op::WriteSlice expects 2 inputs (destination, source), got {}",
5152 node.inputs.len(),
5153 ))
5154 .bt());
5155 }
5156 let dest_dims = node.shape.dims().to_vec();
5157 if ranges.len() != dest_dims.len() {
5158 return Err(Error::Msg(format!(
5159 "Op::WriteSlice: ranges.len() ({}) must equal destination rank ({})",
5160 ranges.len(),
5161 dest_dims.len(),
5162 ))
5163 .bt());
5164 }
5165 let mut ranges = ranges.clone();
5186 let mut deferred_dyn_offset: Option<(usize, fuel_ir::DynScalar)> = None;
5187 if let Some((axis, off)) = dyn_offset {
5188 if *axis >= ranges.len() {
5189 return Err(Error::Msg(format!(
5190 "Op::WriteSlice: dyn_offset axis ({axis}) out of bounds for rank {}",
5191 ranges.len(),
5192 ))
5193 .bt());
5194 }
5195 match off.resolve(sym_env) {
5196 Some(base) => {
5197 let (start, end) = ranges[*axis];
5198 let width = end - start;
5199 ranges[*axis] = (base, base + width);
5200 }
5201 None => {
5202 let is_data_determined = matches!(off, fuel_ir::DynScalar::Sym(s)
5203 if graph.binds_data_determined_sym(*s));
5204 if is_data_determined {
5205 deferred_dyn_offset = Some((*axis, *off));
5206 } else {
5207 return Err(Error::Msg(format!(
5208 "Op::WriteSlice: dynamic offset {off:?} on axis {axis} is \
5209 unbound in the SymEnv at realize (and no producer in the \
5210 graph binds it, so it is not data-determined)",
5211 ))
5212 .bt());
5213 }
5214 }
5215 }
5216 }
5217 for (i, &(start, end)) in ranges.iter().enumerate() {
5218 if deferred_dyn_offset.is_some_and(|(a, _)| a == i) {
5222 continue;
5223 }
5224 if end < start || end > dest_dims[i] {
5225 return Err(Error::Msg(format!(
5226 "Op::WriteSlice: ranges[{i}] = ({start}, {end}) invalid \
5227 for destination dim {i} = {}",
5228 dest_dims[i],
5229 ))
5230 .bt());
5231 }
5232 }
5233 OpParams::WriteSlice {
5234 dest_shape: dest_dims,
5235 ranges,
5236 deferred_dyn_offset,
5237 }
5238 }
5239 Op::WriteSliceRotating {
5240 axis,
5241 modulus,
5242 ranges,
5243 } => {
5244 if node.inputs.len() != 3 {
5250 return Err(Error::Msg(format!(
5251 "Op::WriteSliceRotating expects 3 inputs (destination, source, position), got {}",
5252 node.inputs.len(),
5253 ))
5254 .bt());
5255 }
5256 let dest_dims = node.shape.dims().to_vec();
5257 if ranges.len() != dest_dims.len() {
5258 return Err(Error::Msg(format!(
5259 "Op::WriteSliceRotating: ranges.len() ({}) must equal destination rank ({})",
5260 ranges.len(),
5261 dest_dims.len(),
5262 ))
5263 .bt());
5264 }
5265 if *axis >= dest_dims.len() {
5266 return Err(Error::Msg(format!(
5267 "Op::WriteSliceRotating: axis {axis} out of bounds for rank {}",
5268 dest_dims.len(),
5269 ))
5270 .bt());
5271 }
5272 if *modulus == 0 || *modulus > dest_dims[*axis] {
5273 return Err(Error::Msg(format!(
5274 "Op::WriteSliceRotating: modulus {modulus} invalid for dest_shape[{axis}] = {}",
5275 dest_dims[*axis],
5276 ))
5277 .bt());
5278 }
5279 OpParams::WriteSliceRotating {
5280 dest_shape: dest_dims,
5281 axis: *axis,
5282 modulus: *modulus,
5283 ranges: ranges.clone(),
5284 }
5285 }
5286 Op::WriteSliceDoff { axis, ranges } => {
5287 if node.inputs.len() != 3 {
5294 return Err(Error::Msg(format!(
5295 "Op::WriteSliceDoff expects 3 inputs (destination, source, offset), got {}",
5296 node.inputs.len(),
5297 ))
5298 .bt());
5299 }
5300 let dest_dims = node.shape.dims().to_vec();
5301 if ranges.len() != dest_dims.len() {
5302 return Err(Error::Msg(format!(
5303 "Op::WriteSliceDoff: ranges.len() ({}) must equal destination rank ({})",
5304 ranges.len(),
5305 dest_dims.len(),
5306 ))
5307 .bt());
5308 }
5309 if *axis >= dest_dims.len() {
5310 return Err(Error::Msg(format!(
5311 "Op::WriteSliceDoff: axis {axis} out of bounds for rank {}",
5312 dest_dims.len(),
5313 ))
5314 .bt());
5315 }
5316 OpParams::WriteSliceDoff {
5317 dest_shape: dest_dims,
5318 axis: *axis,
5319 ranges: ranges.clone(),
5320 }
5321 }
5322 _ => OpParams::None,
5323 })
5324}
5325
5326fn with_node_location(graph: &Arc<RwLock<Graph>>, node_id: NodeId, err: Error) -> Error {
5348 match graph.read() {
5349 Ok(g) => err.context(g.describe_node(node_id)),
5350 Err(_) => err,
5351 }
5352}
5353
5354fn bind_data_determined_count(
5374 arc: &Arc<RwLock<Storage>>,
5375 capacity: usize,
5376 count_sym: fuel_ir::SymId,
5377 produced_syms: &mut SymEnv,
5378) -> Result<()> {
5379 let guard = arc
5380 .read()
5381 .map_err(|_| poisoned("NonZeroIndices output storage"))?;
5382 let count_byte_off = capacity * std::mem::size_of::<u32>();
5383 let end = count_byte_off + std::mem::size_of::<u32>();
5384 #[allow(unreachable_patterns)]
5388 let count = match &guard.inner {
5389 fuel_memory::BackendStorage::Cpu(cpu_bytes) => {
5390 let bytes = cpu_bytes.bytes();
5391 if end > bytes.len() {
5392 return Err(Error::Msg(format!(
5393 "NonZeroIndices: count slot byte range [{count_byte_off}..{end}) \
5394 exceeds bundle byte length {}",
5395 bytes.len(),
5396 ))
5397 .bt());
5398 }
5399 u32::from_le_bytes(
5400 bytes[count_byte_off..end]
5401 .try_into()
5402 .expect("4 bytes → u32"),
5403 )
5404 }
5405 other => {
5406 return Err(Error::Msg(format!(
5407 "NonZeroIndices: data-determined count read is CPU-only today; got \
5408 BackendStorage::{:?} (device backends need a D2H after the completion \
5409 handle is waited — a documented follow-up)",
5410 std::mem::discriminant(other),
5411 ))
5412 .bt());
5413 }
5414 };
5415 produced_syms.bind(count_sym, count as usize)?;
5416 Ok(())
5417}
5418
5419fn resolve_deferred_write_slice(
5430 compiled: &CompiledNode,
5431 produced_syms: &SymEnv,
5432 node_id: NodeId,
5433) -> Result<Option<CompiledNode>> {
5434 let OpParams::WriteSlice {
5435 dest_shape,
5436 ranges,
5437 deferred_dyn_offset,
5438 } = &compiled.op_params
5439 else {
5440 return Ok(None);
5441 };
5442 let Some((axis, off)) = deferred_dyn_offset else {
5443 return Ok(None);
5444 };
5445 let base = off.resolve(produced_syms).ok_or_else(|| {
5446 Error::Msg(format!(
5447 "Op::WriteSlice {node_id:?}: data-determined offset {off:?} on axis {axis} is \
5448 unbound in produced_syms at execute (its producer did not run / bind it)",
5449 ))
5450 .bt()
5451 })?;
5452 let mut ranges = ranges.clone();
5453 let (start, end) = ranges[*axis];
5454 let width = end - start;
5455 let resolved = (base, base + width);
5456 if resolved.1 > dest_shape[*axis] {
5457 return Err(Error::Msg(format!(
5458 "Op::WriteSlice {node_id:?}: resolved data-determined range {resolved:?} on axis \
5459 {axis} exceeds destination dim = {}",
5460 dest_shape[*axis],
5461 ))
5462 .bt());
5463 }
5464 ranges[*axis] = resolved;
5465 let mut patched = compiled.clone();
5466 patched.op_params = OpParams::WriteSlice {
5467 dest_shape: dest_shape.clone(),
5468 ranges,
5469 deferred_dyn_offset: None,
5470 };
5471 Ok(Some(patched))
5472}
5473
5474fn resolve_deferred_matmul(
5482 compiled: &CompiledNode,
5483 produced_syms: &SymEnv,
5484 node_id: NodeId,
5485) -> Result<Option<CompiledNode>> {
5486 let OpParams::Matmul {
5487 lhs_batch_dims,
5488 rhs_batch_dims,
5489 m,
5490 n,
5491 k,
5492 m_compute,
5493 } = &compiled.op_params
5494 else {
5495 return Ok(None);
5496 };
5497 let MatmulM::Deferred(dyn_m) = m_compute else {
5498 return Ok(None);
5499 };
5500 let rows = dyn_m.resolve(produced_syms).ok_or_else(|| {
5501 Error::Msg(format!(
5502 "Op::MatMul {node_id:?}: data-determined row count {dyn_m:?} is unbound in \
5503 produced_syms at execute (its producer did not run / bind it)",
5504 ))
5505 .bt()
5506 })?;
5507 if rows > *m {
5508 return Err(Error::Msg(format!(
5509 "Op::MatMul {node_id:?}: resolved row count {rows} exceeds capacity {m}",
5510 ))
5511 .bt());
5512 }
5513 let mut patched = compiled.clone();
5514 patched.op_params = OpParams::Matmul {
5515 lhs_batch_dims: lhs_batch_dims.clone(),
5516 rhs_batch_dims: rhs_batch_dims.clone(),
5517 m: *m,
5518 n: *n,
5519 k: *k,
5520 m_compute: MatmulM::Rows(rows),
5521 };
5522 Ok(Some(patched))
5523}
5524
5525fn execute_work_item(
5526 item: &WorkItem,
5527 cache: &mut StorageCache,
5528 layout_cache: &mut HashMap<NodeId, Layout>,
5529 produced_syms: &mut SymEnv,
5530 will_be_waited: bool,
5536 mut persistent: Option<&mut PersistentOutputs>,
5543) -> Result<CompletionHandle> {
5544 match &item.kind {
5545 WorkItemKind::ConstAdopt => {
5546 if !cache.contains_key(&item.node_id) {
5547 return Err(Error::Msg(format!(
5548 "PipelinedExecutor: Const node {:?} not in input cache",
5549 item.node_id
5550 ))
5551 .bt());
5552 }
5553 layout_cache.insert(item.node_id, item.output_layout.clone());
5557 Ok(CompletionHandle::Ready)
5558 }
5559 WorkItemKind::ViewOf { input } => {
5560 let input_arc = cache.get(input).cloned().ok_or_else(|| {
5561 Error::Msg(format!(
5562 "PipelinedExecutor: view-op input {:?} of {:?} not realized",
5563 input, item.node_id,
5564 ))
5565 .bt()
5566 })?;
5567 cache.insert(item.node_id, input_arc);
5568 layout_cache.insert(item.node_id, item.output_layout.clone());
5569 Ok(CompletionHandle::Ready)
5570 }
5571 WorkItemKind::SlotView { producer } => {
5572 let producer_arc = cache.get(producer).cloned().ok_or_else(|| {
5581 Error::Msg(format!(
5582 "PipelinedExecutor: Op::View producer {:?} of {:?} not realized",
5583 producer, item.node_id,
5584 ))
5585 .bt()
5586 })?;
5587 cache.insert(item.node_id, producer_arc);
5588 layout_cache.insert(item.node_id, item.output_layout.clone());
5589 Ok(CompletionHandle::Ready)
5590 }
5591 WorkItemKind::SlotOwn { producer, slot } => {
5592 let producer_arc = cache.get(producer).cloned().ok_or_else(|| {
5606 Error::Msg(format!(
5607 "PipelinedExecutor: Op::ViewOwned producer {:?} of {:?} not realized",
5608 producer, item.node_id,
5609 ))
5610 .bt()
5611 })?;
5612 let new_storage = {
5613 let guard = producer_arc
5614 .read()
5615 .map_err(|_| poisoned("ViewOwned producer storage"))?;
5616 let bundle = guard.bundle().ok_or_else(|| {
5617 Error::Msg(format!(
5618 "Op::ViewOwned: producer {:?} has no bundle side-table \
5619 — the multi-output authoring contract requires \
5620 Storage::with_bundle / new_bundled at allocation \
5621 time. Node {:?} slot {} cannot resolve its source.",
5622 producer, item.node_id, slot,
5623 ))
5624 .bt()
5625 })?;
5626 let sv = bundle.get(*slot as usize).cloned().ok_or_else(|| {
5627 Error::Msg(format!(
5628 "Op::ViewOwned: slot {} out of range for producer {:?} \
5629 (bundle has {} slots)",
5630 slot,
5631 producer,
5632 bundle.len(),
5633 ))
5634 .bt()
5635 })?;
5636 use fuel_memory::BackendStorage;
5637 match &guard.inner {
5638 BackendStorage::Cpu(cpu_bytes) => {
5639 let src_bytes = cpu_bytes.bytes();
5640 let end = sv.byte_offset + sv.len_bytes();
5641 if end > src_bytes.len() {
5642 return Err(Error::Msg(format!(
5643 "Op::ViewOwned: slot {} byte range [{}..{}) \
5644 exceeds producer's CPU byte length {}",
5645 slot,
5646 sv.byte_offset,
5647 end,
5648 src_bytes.len(),
5649 ))
5650 .bt());
5651 }
5652 let owned = fuel_cpu_backend::CpuStorageBytes::from_bytes(
5653 &src_bytes[sv.byte_offset..end],
5654 );
5655 fuel_memory::Storage::new(BackendStorage::Cpu(owned), sv.dtype)
5656 }
5657 #[cfg(feature = "cuda")]
5658 BackendStorage::Cuda(cuda_bytes) => {
5659 let dst = cuda_bytes.slot_copy_to_new(sv.byte_offset, sv.len_bytes())?;
5660 fuel_memory::Storage::new(BackendStorage::Cuda(dst), sv.dtype)
5661 }
5662 #[cfg(feature = "vulkan")]
5663 BackendStorage::Vulkan(vk_bytes) => {
5664 let backend = vk_bytes.backend().ok_or_else(|| {
5665 Error::Msg(format!(
5666 "Op::ViewOwned: producer {:?} is on Vulkan but \
5667 its storage has no VulkanBackend handle. \
5668 Storages flowing through the pipelined executor \
5669 must be constructed via alloc_bytes_handle / \
5670 upload_bytes_handle so the bundle's slot extraction \
5671 path can reach the backend.",
5672 producer,
5673 ))
5674 .bt()
5675 })?;
5676 let dst = backend.slot_copy_to_new_handle(
5677 vk_bytes,
5678 sv.byte_offset,
5679 sv.len_bytes(),
5680 )?;
5681 fuel_memory::Storage::new(BackendStorage::Vulkan(dst), sv.dtype)
5682 }
5683 #[allow(unreachable_patterns)]
5684 _ => {
5685 return Err(Error::Msg(format!(
5686 "Op::ViewOwned: producer {:?} is on a backend \
5687 without a slot-copy-with-offset hook \
5688 (CPU + CUDA + Vulkan wired). Add a per-backend \
5689 `slot_copy_to_new`-equivalent and extend the \
5690 match arm.",
5691 producer,
5692 ))
5693 .bt());
5694 }
5695 }
5696 };
5697 cache.insert(item.node_id, Arc::new(RwLock::new(new_storage)));
5698 layout_cache.insert(item.node_id, item.output_layout.clone());
5699 Ok(CompletionHandle::Ready)
5703 }
5704 WorkItemKind::WriteSlice { dest, source } => {
5705 let compiled = item.compiled.as_ref().ok_or_else(|| {
5706 Error::Msg(format!(
5707 "PipelinedExecutor: WriteSlice work item {:?} has no compiled node",
5708 item.node_id,
5709 ))
5710 .bt()
5711 })?;
5712 let patched_ws = resolve_deferred_write_slice(compiled, produced_syms, item.node_id)?;
5716 let compiled = patched_ws.as_ref().unwrap_or(compiled);
5717 let dest_arc = cache.get(dest).cloned().ok_or_else(|| {
5718 Error::Msg(format!(
5719 "PipelinedExecutor: WriteSlice destination {:?} of {:?} not realized",
5720 dest, item.node_id,
5721 ))
5722 .bt()
5723 })?;
5724 let source_arc = cache.get(source).cloned().ok_or_else(|| {
5725 Error::Msg(format!(
5726 "PipelinedExecutor: WriteSlice source {:?} of {:?} not realized",
5727 source, item.node_id,
5728 ))
5729 .bt()
5730 })?;
5731 let dest_layout = layout_cache.get(dest).cloned().ok_or_else(|| {
5732 Error::Msg(format!(
5733 "PipelinedExecutor: WriteSlice destination {:?} of {:?} has no cached layout",
5734 dest, item.node_id,
5735 ))
5736 .bt()
5737 })?;
5738 let source_layout = layout_cache.get(source).cloned().ok_or_else(|| {
5739 Error::Msg(format!(
5740 "PipelinedExecutor: WriteSlice source {:?} of {:?} has no cached layout",
5741 source, item.node_id,
5742 ))
5743 .bt()
5744 })?;
5745 if !dest_layout.is_contiguous() || dest_layout.start_offset() != 0 {
5752 return Err(Error::Msg(format!(
5753 "Op::WriteSlice (Phase E.3.2 v1): destination {:?} must be \
5754 contiguous + zero-offset; got Layout {:?}",
5755 dest, dest_layout,
5756 ))
5757 .bt());
5758 }
5759 let source_arc_contig = {
5760 let s_dtype = source_arc
5761 .read()
5762 .map_err(|_| poisoned("WriteSlice source storage"))?
5763 .dtype;
5764 let s_len_bytes = source_arc
5765 .read()
5766 .map_err(|_| poisoned("WriteSlice source storage"))?
5767 .inner
5768 .len_bytes();
5769 let layout_bytes = source_layout.shape().elem_count() * s_dtype.size_in_bytes();
5770 let bytes_match_shape = s_len_bytes == layout_bytes;
5771 let already_contig = source_layout.is_contiguous()
5772 && source_layout.start_offset() == 0
5773 && bytes_match_shape;
5774 if already_contig {
5775 source_arc
5776 } else {
5777 auto_contiguize(&source_arc, &source_layout)?
5778 }
5779 };
5780 let source_layout_kernel = fuel_ir::Layout::contiguous(source_layout.shape().clone());
5781 let input_arcs = vec![source_arc_contig];
5786 let mut output_arcs = vec![dest_arc.clone()];
5787 let kernel_layouts = vec![source_layout_kernel, dest_layout.clone()];
5788 let handle = execute_compiled_with_wait_hint(
5790 compiled,
5791 &input_arcs,
5792 &mut output_arcs,
5793 &kernel_layouts,
5794 will_be_waited,
5795 )?;
5796 cache.insert(item.node_id, dest_arc);
5801 layout_cache.insert(item.node_id, item.output_layout.clone());
5802 Ok(handle)
5803 }
5804 WorkItemKind::WriteSliceRotating {
5805 dest,
5806 source,
5807 position,
5808 } => {
5809 let compiled = item.compiled.as_ref().ok_or_else(|| {
5813 Error::Msg(format!(
5814 "PipelinedExecutor: WriteSliceRotating work item {:?} has no compiled node",
5815 item.node_id,
5816 ))
5817 .bt()
5818 })?;
5819 let dest_arc = cache.get(dest).cloned().ok_or_else(|| {
5820 Error::Msg(format!(
5821 "PipelinedExecutor: WriteSliceRotating destination {:?} of {:?} not realized",
5822 dest, item.node_id,
5823 ))
5824 .bt()
5825 })?;
5826 let source_arc = cache.get(source).cloned().ok_or_else(|| {
5827 Error::Msg(format!(
5828 "PipelinedExecutor: WriteSliceRotating source {:?} of {:?} not realized",
5829 source, item.node_id,
5830 ))
5831 .bt()
5832 })?;
5833 let position_arc = cache.get(position).cloned().ok_or_else(|| {
5834 Error::Msg(format!(
5835 "PipelinedExecutor: WriteSliceRotating position {:?} of {:?} not realized",
5836 position, item.node_id,
5837 ))
5838 .bt()
5839 })?;
5840 let dest_layout = layout_cache.get(dest).cloned().ok_or_else(|| {
5841 Error::Msg(format!(
5842 "PipelinedExecutor: WriteSliceRotating destination {:?} of {:?} has no cached layout",
5843 dest, item.node_id,
5844 ))
5845 .bt()
5846 })?;
5847 let source_layout = layout_cache.get(source).cloned().ok_or_else(|| {
5848 Error::Msg(format!(
5849 "PipelinedExecutor: WriteSliceRotating source {:?} of {:?} has no cached layout",
5850 source, item.node_id,
5851 ))
5852 .bt()
5853 })?;
5854 if !dest_layout.is_contiguous() || dest_layout.start_offset() != 0 {
5856 return Err(Error::Msg(format!(
5857 "Op::WriteSliceRotating (v1): destination {:?} must be \
5858 contiguous + zero-offset; got Layout {:?}",
5859 dest, dest_layout,
5860 ))
5861 .bt());
5862 }
5863 let source_arc_contig = {
5864 let s_dtype = source_arc
5865 .read()
5866 .map_err(|_| poisoned("WriteSliceRotating source storage"))?
5867 .dtype;
5868 let s_len_bytes = source_arc
5869 .read()
5870 .map_err(|_| poisoned("WriteSliceRotating source storage"))?
5871 .inner
5872 .len_bytes();
5873 let layout_bytes = source_layout.shape().elem_count() * s_dtype.size_in_bytes();
5874 let bytes_match_shape = s_len_bytes == layout_bytes;
5875 let already_contig = source_layout.is_contiguous()
5876 && source_layout.start_offset() == 0
5877 && bytes_match_shape;
5878 if already_contig {
5879 source_arc
5880 } else {
5881 auto_contiguize(&source_arc, &source_layout)?
5882 }
5883 };
5884 let source_layout_kernel = fuel_ir::Layout::contiguous(source_layout.shape().clone());
5885 let position_layout = fuel_ir::Layout::contiguous(fuel_ir::Shape::from_dims(&[]));
5886 let input_arcs = vec![source_arc_contig, position_arc];
5887 let mut output_arcs = vec![dest_arc.clone()];
5888 let kernel_layouts = vec![source_layout_kernel, position_layout, dest_layout.clone()];
5889 let handle = execute_compiled_with_wait_hint(
5891 compiled,
5892 &input_arcs,
5893 &mut output_arcs,
5894 &kernel_layouts,
5895 will_be_waited,
5896 )?;
5897 cache.insert(item.node_id, dest_arc);
5898 layout_cache.insert(item.node_id, item.output_layout.clone());
5899 Ok(handle)
5900 }
5901 WorkItemKind::WriteSliceDoff {
5902 dest,
5903 source,
5904 offset,
5905 } => {
5906 let compiled = item.compiled.as_ref().ok_or_else(|| {
5914 Error::Msg(format!(
5915 "PipelinedExecutor: WriteSliceDoff work item {:?} has no compiled node",
5916 item.node_id,
5917 ))
5918 .bt()
5919 })?;
5920 let dest_arc = cache.get(dest).cloned().ok_or_else(|| {
5921 Error::Msg(format!(
5922 "PipelinedExecutor: WriteSliceDoff destination {:?} of {:?} not realized",
5923 dest, item.node_id,
5924 ))
5925 .bt()
5926 })?;
5927 let source_arc = cache.get(source).cloned().ok_or_else(|| {
5928 Error::Msg(format!(
5929 "PipelinedExecutor: WriteSliceDoff source {:?} of {:?} not realized",
5930 source, item.node_id,
5931 ))
5932 .bt()
5933 })?;
5934 let offset_arc = cache.get(offset).cloned().ok_or_else(|| {
5935 Error::Msg(format!(
5936 "PipelinedExecutor: WriteSliceDoff offset {:?} of {:?} not realized",
5937 offset, item.node_id,
5938 ))
5939 .bt()
5940 })?;
5941 let dest_layout = layout_cache.get(dest).cloned().ok_or_else(|| {
5942 Error::Msg(format!(
5943 "PipelinedExecutor: WriteSliceDoff destination {:?} of {:?} has no cached layout",
5944 dest, item.node_id,
5945 ))
5946 .bt()
5947 })?;
5948 let source_layout = layout_cache.get(source).cloned().ok_or_else(|| {
5949 Error::Msg(format!(
5950 "PipelinedExecutor: WriteSliceDoff source {:?} of {:?} has no cached layout",
5951 source, item.node_id,
5952 ))
5953 .bt()
5954 })?;
5955 if !dest_layout.is_contiguous() || dest_layout.start_offset() != 0 {
5957 return Err(Error::Msg(format!(
5958 "Op::WriteSliceDoff (v1): destination {:?} must be \
5959 contiguous + zero-offset; got Layout {:?}",
5960 dest, dest_layout,
5961 ))
5962 .bt());
5963 }
5964 let source_arc_contig = {
5965 let s_dtype = source_arc
5966 .read()
5967 .map_err(|_| poisoned("WriteSliceDoff source storage"))?
5968 .dtype;
5969 let s_len_bytes = source_arc
5970 .read()
5971 .map_err(|_| poisoned("WriteSliceDoff source storage"))?
5972 .inner
5973 .len_bytes();
5974 let layout_bytes = source_layout.shape().elem_count() * s_dtype.size_in_bytes();
5975 let bytes_match_shape = s_len_bytes == layout_bytes;
5976 let already_contig = source_layout.is_contiguous()
5977 && source_layout.start_offset() == 0
5978 && bytes_match_shape;
5979 if already_contig {
5980 source_arc
5981 } else {
5982 auto_contiguize(&source_arc, &source_layout)?
5983 }
5984 };
5985 let source_layout_kernel = fuel_ir::Layout::contiguous(source_layout.shape().clone());
5986 let offset_layout = fuel_ir::Layout::contiguous(fuel_ir::Shape::from_dims(&[]));
5987 let input_arcs = vec![source_arc_contig, offset_arc];
5988 let mut output_arcs = vec![dest_arc.clone()];
5989 let kernel_layouts = vec![source_layout_kernel, offset_layout, dest_layout.clone()];
5990 let handle = execute_compiled_with_wait_hint(
5992 compiled,
5993 &input_arcs,
5994 &mut output_arcs,
5995 &kernel_layouts,
5996 will_be_waited,
5997 )?;
5998 cache.insert(item.node_id, dest_arc);
5999 layout_cache.insert(item.node_id, item.output_layout.clone());
6000 Ok(handle)
6001 }
6002 WorkItemKind::Alloc { target_location } => {
6003 #[cfg_attr(not(feature = "vulkan"), allow(unused_variables))]
6015 let n_bytes = item.elem_count * item.dtype.size_in_bytes();
6016 let alloced: Storage = match target_location {
6017 DeviceLocation::Cpu => fuel_memory::alloc_cpu_zeroed(item.dtype, item.elem_count)?,
6023 #[cfg(feature = "cuda")]
6024 DeviceLocation::Cuda { gpu_id } => {
6025 let cuda_dev = find_cuda_device_in_cache(cache, *gpu_id).ok_or_else(|| {
6026 Error::Msg(format!(
6027 "Op::Alloc on Cuda {{ gpu_id: {} }}: no CUDA \
6028 storage in input cache to derive the device \
6029 handle from. The caller must seed the cache \
6030 (e.g. via `fuel-core::pipelined_bridge::\
6031 device_seed_storage`) before realizing.",
6032 gpu_id,
6033 ))
6034 .bt()
6035 })?;
6036 let cuda_bytes =
6043 fuel_cuda_backend::CudaStorageBytes::alloc_uninit(&cuda_dev, n_bytes)?;
6044 Storage::new(fuel_memory::BackendStorage::Cuda(cuda_bytes), item.dtype)
6045 }
6046 #[cfg(not(feature = "cuda"))]
6047 DeviceLocation::Cuda { .. } => {
6048 return Err(Error::Msg(
6049 "Op::Alloc on Cuda but fuel-storage wasn't built \
6050 with --features cuda"
6051 .to_string(),
6052 )
6053 .bt());
6054 }
6055 #[cfg(feature = "vulkan")]
6056 DeviceLocation::Vulkan { gpu_id } => {
6057 let backend =
6058 find_vulkan_backend_in_cache(cache, *gpu_id).ok_or_else(|| {
6059 Error::Msg(format!(
6060 "Op::Alloc on Vulkan {{ gpu_id: {} }}: no Vulkan \
6061 storage in input cache to derive the backend \
6062 handle from. The caller must seed the cache \
6063 (e.g. via `fuel-core::pipelined_bridge::\
6064 device_seed_storage`) before realizing.",
6065 gpu_id,
6066 ))
6067 .bt()
6068 })?;
6069 let vk_bytes = backend.alloc_bytes_handle(n_bytes)?;
6076 Storage::new(fuel_memory::BackendStorage::Vulkan(vk_bytes), item.dtype)
6077 }
6078 #[cfg(not(feature = "vulkan"))]
6079 DeviceLocation::Vulkan { .. } => {
6080 return Err(Error::Msg(
6081 "Op::Alloc on Vulkan but fuel-storage wasn't built \
6082 with --features vulkan"
6083 .to_string(),
6084 )
6085 .bt());
6086 }
6087 other => {
6088 return Err(Error::Msg(format!(
6089 "Op::Alloc on {:?}: target not yet wired (CPU + \
6090 CUDA + Vulkan land in Phase 3a of bridge-retirement; \
6091 Metal extends when its byte-storage substrate is \
6092 ready).",
6093 other,
6094 ))
6095 .bt());
6096 }
6097 };
6098 cache.insert(item.node_id, Arc::new(RwLock::new(alloced)));
6099 layout_cache.insert(item.node_id, item.output_layout.clone());
6100 Ok(CompletionHandle::Ready)
6101 }
6102 WorkItemKind::Iota { len } => {
6103 if item.target_backend != BackendId::Cpu {
6109 return Err(Error::Msg(format!(
6110 "WorkItemKind::Iota: only CPU is wired today, got {:?}",
6111 item.target_backend,
6112 ))
6113 .bt());
6114 }
6115 let v: Vec<f32> = (0..*len).map(|i| i as f32).collect();
6116 let iota = fuel_memory::from_slice_cpu(&v);
6117 cache.insert(item.node_id, Arc::new(RwLock::new(iota)));
6118 layout_cache.insert(item.node_id, item.output_layout.clone());
6119 Ok(CompletionHandle::Ready)
6120 }
6121 WorkItemKind::ZeroFill => {
6122 if item.inputs.len() != 1 {
6127 return Err(Error::Msg(format!(
6128 "PipelinedExecutor: ZeroFill work item {:?} expects \
6129 1 input, got {}",
6130 item.node_id,
6131 item.inputs.len(),
6132 ))
6133 .bt());
6134 }
6135 let src_id = item.inputs[0];
6136 let src_arc = cache.get(&src_id).cloned().ok_or_else(|| {
6137 Error::Msg(format!(
6138 "PipelinedExecutor: ZeroFill input {:?} of {:?} \
6139 not realized",
6140 src_id, item.node_id,
6141 ))
6142 .bt()
6143 })?;
6144 {
6150 let mut guard = src_arc
6151 .write()
6152 .map_err(|_| poisoned("ZeroFill destination storage"))?;
6153 match &mut guard.inner {
6154 fuel_memory::BackendStorage::Cpu(c) => {
6155 let bytes = c.bytes_mut();
6156 bytes.fill(0);
6157 }
6158 #[cfg(feature = "cuda")]
6159 fuel_memory::BackendStorage::Cuda(c) => {
6160 c.zero_async()?;
6161 }
6162 #[cfg(feature = "vulkan")]
6163 fuel_memory::BackendStorage::Vulkan(v) => {
6164 let backend = v
6165 .backend()
6166 .ok_or_else(|| {
6167 Error::Msg(
6168 "Op::ZeroFill on Vulkan: input has no \
6169 attached backend handle. Storage must \
6170 come from VulkanBackend::alloc_bytes_handle \
6171 / upload_bytes_handle."
6172 .to_string(),
6173 )
6174 .bt()
6175 })?
6176 .clone();
6177 backend.fill_bytes_zero(v)?;
6178 }
6179 #[allow(unreachable_patterns)]
6180 other => {
6181 return Err(Error::Msg(format!(
6182 "Op::ZeroFill: backend not wired ({other:?}); \
6183 CPU + CUDA + Vulkan covered, Metal extends \
6184 when its byte-storage substrate is ready.",
6185 ))
6186 .bt());
6187 }
6188 }
6189 }
6190 cache.insert(item.node_id, src_arc);
6195 layout_cache.insert(item.node_id, item.output_layout.clone());
6196 Ok(CompletionHandle::Ready)
6201 }
6202 WorkItemKind::Copy { target_location } | WorkItemKind::Move { target_location } => {
6203 let op_label = if matches!(item.kind, WorkItemKind::Move { .. }) {
6216 "Move"
6217 } else {
6218 "Copy"
6219 };
6220 let compiled = item.compiled.as_ref().ok_or_else(|| {
6221 Error::Msg(format!(
6222 "PipelinedExecutor: {op_label} work item {:?} has no compiled node",
6223 item.node_id,
6224 ))
6225 .bt()
6226 })?;
6227 if item.inputs.len() != 1 {
6228 return Err(Error::Msg(format!(
6229 "PipelinedExecutor: {op_label} work item {:?} expects 1 input, got {}",
6230 item.node_id,
6231 item.inputs.len(),
6232 ))
6233 .bt());
6234 }
6235 let src_id = item.inputs[0];
6236 let src_arc = cache.get(&src_id).cloned().ok_or_else(|| {
6237 Error::Msg(format!(
6238 "PipelinedExecutor: {op_label} input {:?} of {:?} not realized",
6239 src_id, item.node_id,
6240 ))
6241 .bt()
6242 })?;
6243 {
6261 let guard = src_arc
6262 .read()
6263 .map_err(|_| poisoned("Copy source storage"))?;
6264 if guard.is_bundled() {
6265 let layout_bytes_for_guard = layout_cache
6266 .get(&src_id)
6267 .map(|l| l.shape().elem_count() * guard.dtype.size_in_bytes())
6268 .unwrap_or(usize::MAX);
6269 let bundle_bytes = guard.inner.len_bytes();
6270 let is_whole_bundle = layout_bytes_for_guard >= bundle_bytes;
6271 if is_whole_bundle {
6272 return Err(Error::Msg(format!(
6273 "PipelinedExecutor: Op::{op_label} on a bundled producer \
6274 ({:?}) is not yet supported (Option C followup). \
6275 Use Op::View → Op::Copy → Op::ViewOwned for \
6276 per-slot cross-device moves, or wait for the \
6277 per-backend whole-bundle-Copy hook.",
6278 src_id,
6279 ))
6280 .bt());
6281 }
6282 }
6283 }
6284 let src_layout = layout_cache.get(&src_id).cloned().ok_or_else(|| {
6285 Error::Msg(format!(
6286 "PipelinedExecutor: {op_label} input {:?} of {:?} has no cached layout",
6287 src_id, item.node_id,
6288 ))
6289 .bt()
6290 })?;
6291 let src_dtype = src_arc
6292 .read()
6293 .map_err(|_| poisoned("Copy source storage"))?
6294 .dtype;
6295 let src_len_bytes = src_arc
6296 .read()
6297 .map_err(|_| poisoned("Copy source storage"))?
6298 .inner
6299 .len_bytes();
6300 let layout_bytes = src_layout.shape().elem_count() * src_dtype.size_in_bytes();
6301 let bytes_match_shape = src_len_bytes == layout_bytes;
6302 let already_contig =
6303 src_layout.is_contiguous() && src_layout.start_offset() == 0 && bytes_match_shape;
6304 let src_input = if already_contig {
6305 src_arc
6306 } else {
6307 auto_contiguize(&src_arc, &src_layout)?
6308 };
6309 let kernel_input_layout = fuel_ir::Layout::contiguous(src_layout.shape().clone());
6310 let reuse_arc = match &persistent {
6321 Some(p) if p.mode == PersistentMode::Reuse => match target_location {
6322 DeviceLocation::Cuda { .. } => p.map.get(&item.node_id).cloned(),
6323 _ => None,
6324 },
6325 _ => None,
6326 };
6327 #[cfg_attr(not(feature = "vulkan"), allow(unused_variables))]
6341 let n_bytes = item.elem_count * item.dtype.size_in_bytes();
6342 let input_arcs = vec![src_input];
6343 let mut output_arcs = if let Some(arc) = reuse_arc {
6344 vec![arc]
6345 } else {
6346 let output = match target_location {
6347 DeviceLocation::Cpu => {
6348 fuel_memory::alloc_cpu_zeroed(item.dtype, item.elem_count)?
6349 }
6350 #[cfg(feature = "cuda")]
6351 DeviceLocation::Cuda { gpu_id } => {
6352 let cuda_dev =
6353 find_cuda_device_in_cache(cache, *gpu_id).ok_or_else(|| {
6354 Error::Msg(format!(
6355 "Op::{op_label} on Cuda {{ gpu_id: {} }}: no CUDA \
6356 storage in input cache to derive the device \
6357 handle from. The caller must seed the cache \
6358 (e.g. via `fuel-core::pipelined_bridge::\
6359 device_seed_storage`) before realizing an \
6360 H2D Op::{op_label}.",
6361 gpu_id,
6362 ))
6363 .bt()
6364 })?;
6365 let cuda_bytes =
6366 fuel_cuda_backend::CudaStorageBytes::alloc_uninit(&cuda_dev, n_bytes)?;
6367 Storage::new(fuel_memory::BackendStorage::Cuda(cuda_bytes), item.dtype)
6368 }
6369 #[cfg(not(feature = "cuda"))]
6370 DeviceLocation::Cuda { .. } => {
6371 return Err(Error::Msg(format!(
6372 "Op::{op_label} target Cuda but fuel-storage wasn't built \
6373 with --features cuda",
6374 ))
6375 .bt());
6376 }
6377 #[cfg(feature = "vulkan")]
6378 DeviceLocation::Vulkan { gpu_id } => {
6379 let backend =
6380 find_vulkan_backend_in_cache(cache, *gpu_id).ok_or_else(|| {
6381 Error::Msg(format!(
6382 "Op::{op_label} on Vulkan {{ gpu_id: {} }}: no Vulkan \
6383 storage in input cache to derive the backend \
6384 handle from. The caller must seed the cache \
6385 (e.g. via `fuel-core::pipelined_bridge::\
6386 device_seed_storage`) before realizing an \
6387 H2D Op::{op_label}.",
6388 gpu_id,
6389 ))
6390 .bt()
6391 })?;
6392 let vk_bytes = backend.alloc_bytes_handle(n_bytes)?;
6393 Storage::new(fuel_memory::BackendStorage::Vulkan(vk_bytes), item.dtype)
6394 }
6395 #[cfg(not(feature = "vulkan"))]
6396 DeviceLocation::Vulkan { .. } => {
6397 return Err(Error::Msg(format!(
6398 "Op::{op_label} target Vulkan but fuel-storage wasn't built \
6399 with --features vulkan",
6400 ))
6401 .bt());
6402 }
6403 other => {
6404 return Err(Error::Msg(format!(
6405 "PipelinedExecutor: Op::{op_label} target_location {:?} not yet \
6406 wired (CPU + CUDA + Vulkan covered; Metal extends when \
6407 its byte-storage substrate is ready).",
6408 other
6409 ))
6410 .bt());
6411 }
6412 };
6413 vec![Arc::new(RwLock::new(output))]
6414 };
6415 let kernel_layouts = vec![kernel_input_layout, item.output_layout.clone()];
6416 let handle = execute_compiled_with_wait_hint(
6418 compiled,
6419 &input_arcs,
6420 &mut output_arcs,
6421 &kernel_layouts,
6422 will_be_waited,
6423 )?;
6424 let arc = output_arcs.into_iter().next().expect("one output");
6425 if let Some(p) = persistent.as_deref_mut()
6431 && p.mode == PersistentMode::Record
6432 {
6433 p.map.insert(item.node_id, Arc::clone(&arc));
6434 }
6435 cache.insert(item.node_id, arc);
6436 layout_cache.insert(item.node_id, item.output_layout.clone());
6437 Ok(handle)
6438 }
6439 WorkItemKind::ReleaseMarker => {
6440 let marker = fuel_memory::alloc_cpu_zeroed(item.dtype, 0)?;
6447 cache.insert(item.node_id, Arc::new(RwLock::new(marker)));
6448 layout_cache.insert(item.node_id, item.output_layout.clone());
6449 Ok(CompletionHandle::Ready)
6450 }
6451 WorkItemKind::ContiguizeOf { input } => {
6452 let input_arc = cache.get(input).cloned().ok_or_else(|| {
6453 Error::Msg(format!(
6454 "PipelinedExecutor: reshape input {:?} of {:?} not realized",
6455 input, item.node_id,
6456 ))
6457 .bt()
6458 })?;
6459 let input_layout = layout_cache.get(input).cloned().ok_or_else(|| {
6460 Error::Msg(format!(
6461 "PipelinedExecutor: reshape input {:?} of {:?} has no cached layout",
6462 input, item.node_id,
6463 ))
6464 .bt()
6465 })?;
6466 let reuse_arc = match &persistent {
6477 Some(p) if p.mode == PersistentMode::Reuse => p.map.get(&item.node_id).cloned(),
6478 _ => None,
6479 };
6480 let out_arc = if let Some(dest_arc) = reuse_arc {
6481 contiguize_into(&dest_arc, &input_arc, &input_layout)?;
6482 dest_arc
6483 } else if input_layout.is_contiguous() && input_layout.start_offset() == 0 {
6484 input_arc
6488 } else {
6489 let arc = auto_contiguize(&input_arc, &input_layout)?;
6491 if let Some(p) = persistent.as_deref_mut()
6492 && p.mode == PersistentMode::Record
6493 {
6494 p.map.insert(item.node_id, Arc::clone(&arc));
6495 }
6496 arc
6497 };
6498 cache.insert(item.node_id, out_arc);
6499 layout_cache.insert(item.node_id, item.output_layout.clone());
6500 Ok(CompletionHandle::Ready)
6504 }
6505 WorkItemKind::Kernel => {
6506 let compiled = item.compiled.as_ref().ok_or_else(|| {
6507 Error::Msg(format!(
6508 "PipelinedExecutor: Kernel work item {:?} has no compiled node",
6509 item.node_id,
6510 ))
6511 .bt()
6512 })?;
6513 let patched_mm = resolve_deferred_matmul(compiled, produced_syms, item.node_id)?;
6517 let compiled = patched_mm.as_ref().unwrap_or(compiled);
6518 let mut input_arcs: Vec<Arc<RwLock<Storage>>> = Vec::with_capacity(item.inputs.len());
6530 let mut kernel_layouts: Vec<fuel_ir::Layout> =
6531 Vec::with_capacity(item.inputs.len() + 1);
6532 let kernel_handles_strided = compiled.caps.strided_input;
6539 let t_gather = crate::compiled::op_profile::PhaseTimer::start("kernel.gather_inputs");
6540 for in_id in &item.inputs {
6541 let in_arc = cache.get(in_id).cloned().ok_or_else(|| {
6542 Error::Msg(format!(
6543 "PipelinedExecutor: input {:?} of {:?} not realized",
6544 in_id, item.node_id,
6545 ))
6546 .bt()
6547 })?;
6548 let in_layout = layout_cache.get(in_id).cloned().ok_or_else(|| {
6549 Error::Msg(format!(
6550 "PipelinedExecutor: input {:?} of {:?} has no cached layout",
6551 in_id, item.node_id,
6552 ))
6553 .bt()
6554 })?;
6555 let in_dtype = in_arc.read().map_err(|_| poisoned("input storage"))?.dtype;
6572 let in_len_bytes = in_arc
6573 .read()
6574 .map_err(|_| poisoned("input storage"))?
6575 .inner
6576 .len_bytes();
6577 let layout_bytes = in_layout.shape().elem_count() * in_dtype.size_in_bytes();
6578 let bytes_match_shape = in_len_bytes == layout_bytes;
6579 let already_contig =
6580 in_layout.is_contiguous() && in_layout.start_offset() == 0 && bytes_match_shape;
6581 let strided_ok =
6582 kernel_handles_strided && in_layout.start_offset() == 0 && bytes_match_shape;
6583 if already_contig || strided_ok {
6584 input_arcs.push(in_arc);
6585 kernel_layouts.push(in_layout);
6586 } else {
6587 let contig_arc = auto_contiguize(&in_arc, &in_layout)?;
6588 input_arcs.push(contig_arc);
6589 kernel_layouts.push(fuel_ir::Layout::contiguous(in_layout.shape().clone()));
6590 }
6591 }
6592 drop(t_gather);
6593 kernel_layouts.push(item.output_layout.clone());
6594
6595 let (alloc_elem_count, bundle_to_attach) = match &item.output_bundle {
6608 Some(bundle) => {
6609 let dtype_bytes = item.dtype.size_in_bytes().max(1);
6610 let total_bytes = bundle.iter().fold(0usize, |acc, s| {
6611 let end = s.byte_offset.saturating_add(s.len_bytes());
6612 acc.max(end)
6613 });
6614 let elems = total_bytes.div_ceil(dtype_bytes);
6615 (elems, Some(Arc::clone(bundle)))
6616 }
6617 None => (item.elem_count, None),
6618 };
6619 let reuse_arc = match &persistent {
6626 Some(p) if p.mode == PersistentMode::Reuse => {
6627 Some(p.map.get(&item.node_id).cloned().ok_or_else(|| {
6628 Error::Msg(format!(
6629 "PipelinedExecutor: capture (reuse) mode has no persistent \
6630 output buffer for kernel node {:?}; the warm (record) run \
6631 must realize every kernel node before capture",
6632 item.node_id,
6633 ))
6634 .bt()
6635 })?)
6636 }
6637 _ => None,
6638 };
6639 let t_alloc = crate::compiled::op_profile::PhaseTimer::start("kernel.alloc_output");
6640 let mut output_arcs = if let Some(arc) = reuse_arc {
6641 vec![arc]
6642 } else {
6643 let output = match item.target_backend {
6644 BackendId::Cpu => fuel_memory::alloc_cpu_zeroed(item.dtype, alloc_elem_count)?,
6645 #[cfg(feature = "cuda")]
6646 BackendId::Cuda => {
6647 let first_in = input_arcs.first().ok_or_else(|| {
6648 Error::Msg(format!(
6649 "PipelinedExecutor: kernel {:?} on Cuda has no inputs; \
6650 cannot derive device for output allocation",
6651 item.node_id,
6652 ))
6653 .bt()
6654 })?;
6655 let guard = first_in.read().map_err(|_| poisoned("input storage"))?;
6656 let cuda_in = match &guard.inner {
6657 fuel_memory::BackendStorage::Cuda(c) => c,
6658 other => {
6659 return Err(Error::Msg(format!(
6660 "PipelinedExecutor: kernel {:?} target_backend=Cuda but \
6661 input has BackendStorage::{:?}; mixed-backend kernels \
6662 require an explicit Op::Copy first",
6663 item.node_id,
6664 std::mem::discriminant(other),
6665 ))
6666 .bt());
6667 }
6668 };
6669 let n_bytes = alloc_elem_count * item.dtype.size_in_bytes();
6670 let cuda_bytes =
6671 fuel_cuda_backend::CudaStorageBytes::alloc(cuda_in.device(), n_bytes)?;
6672 fuel_memory::Storage::new(
6673 fuel_memory::BackendStorage::Cuda(cuda_bytes),
6674 item.dtype,
6675 )
6676 }
6677 #[cfg(feature = "vulkan")]
6678 BackendId::Vulkan => {
6679 let first_in = input_arcs.first().ok_or_else(|| {
6687 Error::Msg(format!(
6688 "PipelinedExecutor: kernel {:?} on Vulkan has no inputs; \
6689 cannot derive backend handle for output allocation",
6690 item.node_id,
6691 ))
6692 .bt()
6693 })?;
6694 let guard = first_in.read().map_err(|_| poisoned("input storage"))?;
6695 let vk_in = match &guard.inner {
6696 fuel_memory::BackendStorage::Vulkan(v) => v,
6697 other => {
6698 return Err(Error::Msg(format!(
6699 "PipelinedExecutor: kernel {:?} target_backend=Vulkan but \
6700 input has BackendStorage::{:?}; mixed-backend kernels \
6701 require an explicit Op::Copy first",
6702 item.node_id,
6703 std::mem::discriminant(other),
6704 ))
6705 .bt());
6706 }
6707 };
6708 let backend = vk_in.backend().ok_or_else(|| {
6709 Error::Msg(format!(
6710 "PipelinedExecutor: Vulkan kernel {:?} input has no backend \
6711 handle. Storages flowing through the pipelined executor's \
6712 Vulkan path must be constructed via \
6713 VulkanBackend::alloc_bytes_handle / upload_bytes_handle \
6714 (not the legacy alloc_bytes / upload_bytes which leave \
6715 VulkanStorageBytes::backend = None).",
6716 item.node_id,
6717 ))
6718 .bt()
6719 })?;
6720 let n_bytes = alloc_elem_count * item.dtype.size_in_bytes();
6721 let vk_bytes = backend.alloc_bytes_handle(n_bytes)?;
6722 fuel_memory::Storage::new(
6723 fuel_memory::BackendStorage::Vulkan(vk_bytes),
6724 item.dtype,
6725 )
6726 }
6727 other => {
6728 return Err(Error::Msg(format!(
6729 "PipelinedExecutor: target_backend {:?} output allocation \
6730 not yet implemented (CPU + CUDA + Vulkan wired; Metal extends later)",
6731 other
6732 ))
6733 .bt());
6734 }
6735 };
6736 let output = match bundle_to_attach {
6741 Some(bundle) => output.with_bundle(bundle)?,
6742 None => output,
6743 };
6744 vec![Arc::new(RwLock::new(output))]
6745 };
6746 drop(t_alloc);
6747
6748 let t_launch =
6750 crate::compiled::op_profile::PhaseTimer::start("kernel.execute_compiled");
6751 let handle = execute_compiled_with_wait_hint(
6752 compiled,
6753 &input_arcs,
6754 &mut output_arcs,
6755 &kernel_layouts,
6756 will_be_waited,
6757 )?;
6758 drop(t_launch);
6759
6760 let arc = output_arcs.into_iter().next().expect("one output");
6761
6762 if let Some(p) = persistent
6767 && p.mode == PersistentMode::Record
6768 {
6769 p.map.insert(item.node_id, Arc::clone(&arc));
6770 }
6771
6772 if let OpParams::NonZeroIndices {
6782 capacity,
6783 count_sym,
6784 } = &compiled.op_params
6785 {
6786 bind_data_determined_count(&arc, *capacity, *count_sym, produced_syms)?;
6787 }
6788
6789 cache.insert(item.node_id, arc);
6790 layout_cache.insert(item.node_id, item.output_layout.clone());
6791 Ok(handle)
6792 }
6793 WorkItemKind::InplaceKernel { target_idx } => {
6794 let compiled = item.compiled.as_ref().ok_or_else(|| {
6795 Error::Msg(format!(
6796 "PipelinedExecutor: InplaceKernel work item {:?} has no compiled node",
6797 item.node_id,
6798 ))
6799 .bt()
6800 })?;
6801 let target_id = *item.inputs.get(*target_idx).ok_or_else(|| {
6802 Error::Msg(format!(
6803 "PipelinedExecutor: InplaceKernel work item {:?} target_idx={} \
6804 out of bounds (inputs.len()={})",
6805 item.node_id,
6806 target_idx,
6807 item.inputs.len(),
6808 ))
6809 .bt()
6810 })?;
6811 let target_arc = cache.get(&target_id).cloned().ok_or_else(|| {
6812 Error::Msg(format!(
6813 "PipelinedExecutor: InplaceKernel target {:?} of {:?} not realized",
6814 target_id, item.node_id,
6815 ))
6816 .bt()
6817 })?;
6818 let target_layout = layout_cache.get(&target_id).cloned().ok_or_else(|| {
6819 Error::Msg(format!(
6820 "PipelinedExecutor: InplaceKernel target {:?} of {:?} has no cached layout",
6821 target_id, item.node_id,
6822 ))
6823 .bt()
6824 })?;
6825 if !target_layout.is_contiguous() || target_layout.start_offset() != 0 {
6830 return Err(Error::Msg(format!(
6831 "InplaceKernel (Phase 3 v1): target {:?} must be contiguous + \
6832 zero-offset; got Layout {:?}",
6833 target_id, target_layout,
6834 ))
6835 .bt());
6836 }
6837 let mut input_arcs: Vec<Arc<RwLock<Storage>>> = Vec::new();
6847 let mut kernel_layouts: Vec<Layout> = Vec::new();
6848 for (i, &input_id) in item.inputs.iter().enumerate() {
6849 if i == *target_idx {
6850 continue;
6851 }
6852 let arc = cache.get(&input_id).cloned().ok_or_else(|| {
6853 Error::Msg(format!(
6854 "PipelinedExecutor: InplaceKernel non-target input {:?} of {:?} \
6855 not realized",
6856 input_id, item.node_id,
6857 ))
6858 .bt()
6859 })?;
6860 let lay = layout_cache.get(&input_id).cloned().ok_or_else(|| {
6861 Error::Msg(format!(
6862 "PipelinedExecutor: InplaceKernel non-target input {:?} of {:?} \
6863 has no cached layout",
6864 input_id, item.node_id,
6865 ))
6866 .bt()
6867 })?;
6868 input_arcs.push(arc);
6869 kernel_layouts.push(lay);
6870 }
6871 let mut output_arcs = vec![target_arc.clone()];
6872 kernel_layouts.push(target_layout.clone());
6873 let handle = execute_compiled_with_wait_hint(
6875 compiled,
6876 &input_arcs,
6877 &mut output_arcs,
6878 &kernel_layouts,
6879 will_be_waited,
6880 )?;
6881 cache.insert(item.node_id, target_arc);
6885 layout_cache.insert(item.node_id, item.output_layout.clone());
6886 Ok(handle)
6887 }
6888 }
6889}
6890
6891fn poisoned(what: &'static str) -> Error {
6892 Error::Msg(format!("PipelinedExecutor: {} poisoned", what)).bt()
6893}
6894
6895#[inline]
6904fn store_handle(
6905 handles: &mut HashMap<NodeId, CompletionHandle>,
6906 node_id: NodeId,
6907 handle: CompletionHandle,
6908) {
6909 if matches!(handle, CompletionHandle::Ready) {
6910 if let Some(prev) = handles.remove(&node_id) {
6914 let _ = prev.wait();
6915 }
6916 return;
6917 }
6918 if let Some(prev) = handles.insert(node_id, handle) {
6919 let _ = prev.wait();
6920 }
6921}
6922
6923fn wait_producer_handle(
6950 handles: &mut HashMap<NodeId, CompletionHandle>,
6951 producer: NodeId,
6952) -> Result<()> {
6953 if let Some(handle) = handles.remove(&producer) {
6954 handle.wait()?;
6955 }
6956 Ok(())
6957}
6958
6959fn drain_handles(handles: &mut HashMap<NodeId, CompletionHandle>) -> Result<()> {
6986 for (_node, handle) in handles.drain() {
6987 handle.wait()?;
6988 }
6989 debug_assert!(
6995 handles.is_empty(),
6996 "A4b-1: handle map must be empty after drain"
6997 );
6998 Ok(())
6999}
7000
7001#[cfg(feature = "cuda")]
7030fn sync_active_cuda_devices(cache: &StorageCache) -> Result<()> {
7031 let mut devices: HashMap<fuel_ir::DeviceLocation, fuel_cuda_backend::CudaDevice> =
7032 HashMap::new();
7033 for arc in cache.values() {
7034 let guard = arc
7035 .read()
7036 .map_err(|_| poisoned("cache storage (cuda realize-end sync scan)"))?;
7037 if let fuel_memory::BackendStorage::Cuda(c) = &guard.inner {
7038 devices
7039 .entry(c.device().location())
7040 .or_insert_with(|| c.device().clone());
7041 }
7042 }
7043 for dev in devices.values() {
7044 dev.synchronize()?;
7045 }
7046 Ok(())
7047}
7048
7049#[cfg(not(feature = "cuda"))]
7052fn sync_active_cuda_devices(_cache: &StorageCache) -> Result<()> {
7053 Ok(())
7054}
7055
7056#[cfg(feature = "cuda")]
7067fn find_cuda_device_in_cache(
7068 cache: &StorageCache,
7069 gpu_id: usize,
7070) -> Option<fuel_cuda_backend::CudaDevice> {
7071 for arc in cache.values() {
7072 let guard = arc.read().ok()?;
7073 if let fuel_memory::BackendStorage::Cuda(c) = &guard.inner {
7074 if matches!(c.device().location(), DeviceLocation::Cuda { gpu_id: g } if g == gpu_id) {
7079 return Some(c.device().clone());
7080 }
7081 }
7082 }
7083 None
7084}
7085
7086#[cfg(feature = "vulkan")]
7111struct VulkanCompletion {
7112 backend: Arc<fuel_vulkan_backend::VulkanBackend>,
7113 batch: Option<fuel_vulkan_backend::SubmittedBatch>,
7114 loc: fuel_ir::DeviceLocation,
7117}
7118
7119#[cfg(feature = "vulkan")]
7125type InflightVulkan = VulkanCompletion;
7126#[cfg(not(feature = "vulkan"))]
7127type InflightVulkan = ();
7128
7129#[cfg(feature = "vulkan")]
7142fn copy_source_is_vulkan(cache: &StorageCache, producer: NodeId) -> Result<bool> {
7143 if let Some(arc) = cache.get(&producer) {
7144 let guard = arc
7145 .read()
7146 .map_err(|_| poisoned("storage lock probing copy-source backend"))?;
7147 return Ok(matches!(
7148 guard.inner,
7149 fuel_memory::BackendStorage::Vulkan(_)
7150 ));
7151 }
7152 Ok(false)
7153}
7154
7155#[cfg(not(feature = "vulkan"))]
7156#[inline]
7157fn copy_source_is_vulkan(_cache: &StorageCache, _producer: NodeId) -> Result<bool> {
7158 Ok(false)
7159}
7160
7161#[cfg(feature = "vulkan")]
7162impl crate::compiled::Completion for VulkanCompletion {
7163 fn wait(self: Box<Self>) -> Result<()> {
7164 (*self).into_wait()
7167 }
7168}
7169
7170#[cfg(feature = "vulkan")]
7171impl VulkanCompletion {
7172 fn new(
7176 backend: Arc<fuel_vulkan_backend::VulkanBackend>,
7177 batch: fuel_vulkan_backend::SubmittedBatch,
7178 loc: fuel_ir::DeviceLocation,
7179 ) -> Self {
7180 crate::dispatch::inflight_inc(loc);
7181 Self {
7182 backend,
7183 batch: Some(batch),
7184 loc,
7185 }
7186 }
7187
7188 fn into_wait(mut self) -> Result<()> {
7199 if let Some(batch) = self.batch.take() {
7204 self.backend.wait_submitted(batch)?;
7205 }
7206 Ok(())
7207 }
7209
7210 fn retain_data(&mut self, buf: std::sync::Arc<fuel_vulkan_backend::VulkanBuffer>) {
7221 if let Some(batch) = self.batch.as_mut() {
7222 batch.retain_buffer(buf);
7223 }
7224 }
7225}
7226
7227#[cfg(feature = "vulkan")]
7228impl Drop for VulkanCompletion {
7229 fn drop(&mut self) {
7230 crate::dispatch::inflight_dec(self.loc);
7235 }
7236}
7237
7238#[cfg(feature = "vulkan")]
7265fn eager_submit_all_vulkan(
7266 cache: &StorageCache,
7267 inflight: &mut Vec<VulkanCompletion>,
7268) -> Result<()> {
7269 let mut seen: Vec<usize> = Vec::new();
7271 let mut backends: Vec<Arc<fuel_vulkan_backend::VulkanBackend>> = Vec::new();
7272 for arc in cache.values() {
7273 let guard = arc
7274 .read()
7275 .map_err(|_| poisoned("storage lock during eager vulkan submit"))?;
7276 if let fuel_memory::BackendStorage::Vulkan(v) = &guard.inner
7277 && let Some(backend) = v.backend()
7278 && !seen.contains(&backend.gpu_id)
7279 {
7280 seen.push(backend.gpu_id);
7281 backends.push(backend.clone());
7282 }
7283 }
7284 for backend in backends {
7285 let loc = DeviceLocation::Vulkan {
7289 gpu_id: backend.gpu_id,
7290 };
7291 if let Some(batch) = backend.submit_pending()? {
7292 inflight.push(VulkanCompletion::new(backend, batch, loc));
7293 }
7294 }
7295 Ok(())
7296}
7297
7298#[cfg(not(feature = "vulkan"))]
7299#[inline]
7300fn eager_submit_all_vulkan(
7301 _cache: &StorageCache,
7302 _inflight: &mut Vec<InflightVulkan>,
7303) -> Result<()> {
7304 Ok(())
7305}
7306
7307#[cfg(feature = "vulkan")]
7324fn drain_inflight_vulkan(inflight: &mut Vec<VulkanCompletion>) -> Result<()> {
7325 for vc in inflight.drain(..) {
7326 vc.into_wait()?;
7327 }
7328 Ok(())
7329}
7330
7331#[cfg(not(feature = "vulkan"))]
7332#[inline]
7333fn drain_inflight_vulkan(_inflight: &mut Vec<InflightVulkan>) -> Result<()> {
7334 Ok(())
7335}
7336
7337#[cfg(feature = "vulkan")]
7353fn drain_vulkan_pending(cache: &StorageCache) -> Result<()> {
7354 use crate::compiled::CompletionHandle;
7355 for arc in cache.values() {
7356 let backend = {
7357 let guard = arc
7358 .read()
7359 .map_err(|_| poisoned("storage lock during vulkan submit_pending"))?;
7360 match &guard.inner {
7361 fuel_memory::BackendStorage::Vulkan(v) => v.backend().cloned(),
7362 _ => None,
7363 }
7364 };
7365 let Some(backend) = backend else { continue };
7366 let loc = DeviceLocation::Vulkan {
7372 gpu_id: backend.gpu_id,
7373 };
7374 if let Some(batch) = backend.submit_pending()? {
7375 let handle: CompletionHandle =
7376 CompletionHandle::Pending(Box::new(VulkanCompletion::new(backend, batch, loc)));
7377 handle.wait()?;
7378 }
7379 }
7380 Ok(())
7381}
7382
7383#[cfg(not(feature = "vulkan"))]
7384fn drain_vulkan_pending(_cache: &StorageCache) -> Result<()> {
7385 Ok(())
7386}
7387
7388#[cfg(feature = "vulkan")]
7405fn force_flush_vulkan(arc: &Arc<RwLock<Storage>>) -> Result<()> {
7406 let guard = arc
7407 .read()
7408 .map_err(|_| poisoned("storage lock during vulkan force_flush"))?;
7409 if let fuel_memory::BackendStorage::Vulkan(v) = &guard.inner
7410 && let Some(backend) = v.backend()
7411 {
7412 backend.force_flush()?;
7413 }
7414 Ok(())
7415}
7416
7417#[cfg(not(feature = "vulkan"))]
7418fn force_flush_vulkan(_arc: &Arc<RwLock<Storage>>) -> Result<()> {
7419 Ok(())
7420}
7421
7422#[cfg(feature = "vulkan")]
7465const EVICT_SUBMIT_MIN_DISPATCHES: usize = 48;
7466
7467#[cfg(feature = "vulkan")]
7468fn defer_evicted_vulkan_buffer(
7469 cache: &StorageCache,
7470 destroyed: NodeId,
7471 inflight: &mut Vec<VulkanCompletion>,
7472) -> Result<()> {
7473 let (buf, backend) = {
7478 let Some(arc) = cache.get(&destroyed) else {
7479 return Ok(());
7480 };
7481 let guard = arc
7482 .read()
7483 .map_err(|_| poisoned("storage lock during deferred-eviction retain"))?;
7484 match &guard.inner {
7485 fuel_memory::BackendStorage::Vulkan(v) => (v.device_buffer_arc(), v.backend().cloned()),
7486 _ => (None, None),
7487 }
7488 };
7489 let Some(buf) = buf else { return Ok(()) };
7490
7491 if let Some(backend) = &backend {
7496 backend.retain_in_open_batch(buf.clone());
7497 }
7498 for vc in inflight.iter_mut() {
7503 vc.retain_data(buf.clone());
7504 }
7505 if let Some(backend) = backend
7507 && backend.open_batch_len() >= EVICT_SUBMIT_MIN_DISPATCHES
7508 {
7509 eager_submit_all_vulkan(cache, inflight)?;
7510 }
7511 if !inflight.is_empty() {
7521 DEFERRED_EVICTION_RETAINS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
7522 }
7523 Ok(())
7524}
7525
7526#[cfg(feature = "vulkan")]
7532pub static DEFERRED_EVICTION_RETAINS: std::sync::atomic::AtomicUsize =
7533 std::sync::atomic::AtomicUsize::new(0);
7534
7535#[cfg(feature = "vulkan")]
7539pub fn deferred_eviction_retains() -> usize {
7540 DEFERRED_EVICTION_RETAINS.load(std::sync::atomic::Ordering::Relaxed)
7541}
7542
7543#[cfg(not(feature = "vulkan"))]
7544#[inline]
7545fn defer_evicted_vulkan_buffer(
7546 _cache: &StorageCache,
7547 _destroyed: NodeId,
7548 _inflight: &mut Vec<InflightVulkan>,
7549) -> Result<()> {
7550 Ok(())
7551}
7552
7553#[cfg(feature = "vulkan")]
7557fn find_vulkan_backend_in_cache(
7558 cache: &StorageCache,
7559 gpu_id: usize,
7560) -> Option<std::sync::Arc<fuel_vulkan_backend::VulkanBackend>> {
7561 for arc in cache.values() {
7562 let guard = arc.read().ok()?;
7563 if let fuel_memory::BackendStorage::Vulkan(v) = &guard.inner
7564 && let Some(backend) = v.backend()
7565 && backend.gpu_id == gpu_id
7566 {
7567 return Some(backend.clone());
7568 }
7569 }
7570 None
7571}
7572
7573fn auto_contiguize(arc: &Arc<RwLock<Storage>>, layout: &Layout) -> Result<Arc<RwLock<Storage>>> {
7583 let in_guard = arc
7584 .read()
7585 .map_err(|_| poisoned("input storage lock during auto_contiguize"))?;
7586 let dtype = in_guard.dtype;
7587 let dtype_size = dtype.size_in_bytes();
7588 let new_storage = match &in_guard.inner {
7589 fuel_memory::BackendStorage::Cpu(c) => {
7590 let new_bytes = fuel_cpu_backend::byte_kernels::contiguize_cpu(c, layout, dtype_size)?;
7591 Storage::new(fuel_memory::BackendStorage::Cpu(new_bytes), dtype)
7592 }
7593 #[cfg(feature = "cuda")]
7594 fuel_memory::BackendStorage::Cuda(c) => {
7595 let contig = fuel_cuda_backend::baracuda::contiguize::contiguize_to_fresh(
7607 c, layout, dtype_size,
7608 )?;
7609 Storage::new(fuel_memory::BackendStorage::Cuda(contig), dtype)
7610 }
7611 #[cfg(feature = "vulkan")]
7612 fuel_memory::BackendStorage::Vulkan(v) => {
7613 let backend = v.backend().ok_or_else(|| {
7618 Error::Msg(
7619 "auto_contiguize: Vulkan input has no backend handle. \
7620 Storages flowing through the pipelined executor's Vulkan \
7621 path must be constructed via VulkanBackend::alloc_bytes_handle \
7622 / upload_bytes_handle (not the legacy alloc_bytes / \
7623 upload_bytes which leave VulkanStorageBytes::backend = None)."
7624 .to_string(),
7625 )
7626 .bt()
7627 })?;
7628 if let Some(native) = crate::vulkan_dispatch::contiguize_vulkan(v, layout, dtype_size)?
7629 {
7630 return Ok(Arc::new(RwLock::new(Storage::new(
7631 fuel_memory::BackendStorage::Vulkan(native),
7632 dtype,
7633 ))));
7634 }
7635 let _t = crate::compiled::op_profile::PhaseTimer::start(
7636 "auto_contiguize.vulkan_host_roundtrip",
7637 );
7638 let host_bytes = backend.download_bytes(v)?;
7639 let host_cpu = fuel_cpu_backend::byte_storage::CpuStorageBytes::from_bytes(&host_bytes);
7640 let contig_cpu =
7641 fuel_cpu_backend::byte_kernels::contiguize_cpu(&host_cpu, layout, dtype_size)?;
7642 let host_contig_bytes = contig_cpu.bytes().to_vec();
7643 let vk_bytes = backend.upload_bytes_handle(&host_contig_bytes)?;
7644 Storage::new(fuel_memory::BackendStorage::Vulkan(vk_bytes), dtype)
7645 }
7646 #[allow(unreachable_patterns)]
7647 _ => {
7648 return Err(Error::Msg(
7649 "auto_contiguize: backend not wired (CPU + CUDA + Vulkan are \
7650 wired today; Metal extends this match when its byte-storage \
7651 substrate lands)"
7652 .to_string(),
7653 )
7654 .bt());
7655 }
7656 };
7657 Ok(Arc::new(RwLock::new(new_storage)))
7658}
7659
7660#[cfg_attr(not(feature = "cuda"), allow(unused_variables))]
7677fn contiguize_into(
7678 dest: &Arc<RwLock<Storage>>,
7679 arc: &Arc<RwLock<Storage>>,
7680 layout: &Layout,
7681) -> Result<()> {
7682 let in_guard = arc
7683 .read()
7684 .map_err(|_| poisoned("input storage lock during contiguize_into"))?;
7685 match &in_guard.inner {
7686 #[cfg(feature = "cuda")]
7687 fuel_memory::BackendStorage::Cuda(src_cuda) => {
7688 let dtype_size = in_guard.dtype.size_in_bytes();
7689 let mut dest_guard = dest
7690 .write()
7691 .map_err(|_| poisoned("dest storage lock during contiguize_into"))?;
7692 let dest_cuda = match &mut dest_guard.inner {
7693 fuel_memory::BackendStorage::Cuda(d) => d,
7694 other => {
7695 return Err(Error::Msg(format!(
7696 "contiguize_into: dest is BackendStorage::{:?}, not Cuda",
7697 std::mem::discriminant(other),
7698 ))
7699 .bt());
7700 }
7701 };
7702 fuel_cuda_backend::baracuda::contiguize::contiguize_into(
7703 src_cuda, layout, dest_cuda, dtype_size,
7704 )
7705 }
7706 #[allow(unreachable_patterns)]
7707 _ => Err(Error::Msg(
7708 "contiguize_into: only CUDA is wired (PersistentOutputs::reuse / \
7709 capture_decode is CUDA-only today — no CPU/Vulkan consumer exists \
7710 yet; extend this match if one appears)"
7711 .to_string(),
7712 )
7713 .bt()),
7714 }
7715}
7716
7717#[cfg(test)]
7718mod backend_neutral_tests {
7719 use super::*;
7720
7721 #[test]
7722 fn only_deviceless_items_are_backend_neutral() {
7723 let n = NodeId(0);
7724 assert!(is_backend_neutral(&WorkItemKind::ConstAdopt));
7726 assert!(is_backend_neutral(&WorkItemKind::ViewOf { input: n }));
7727 assert!(is_backend_neutral(&WorkItemKind::SlotView { producer: n }));
7728 assert!(is_backend_neutral(&WorkItemKind::ContiguizeOf { input: n }));
7731 assert!(!is_backend_neutral(&WorkItemKind::Kernel));
7735 assert!(!is_backend_neutral(&WorkItemKind::Copy {
7736 target_location: DeviceLocation::Cpu
7737 }));
7738 assert!(!is_backend_neutral(&WorkItemKind::WriteSlice {
7739 dest: n,
7740 source: n
7741 }));
7742 }
7743}
7744
7745#[cfg(test)]
7746mod tests {
7747 use super::*;
7748 use fuel_graph::Node;
7749 use fuel_ir::Shape;
7750
7751 #[test]
7757 fn scan_has_no_dispatch_op_kind() {
7758 use fuel_graph::{Op, ScanEmit};
7759 assert_eq!(
7760 crate::pipelined::op_to_op_kind(&Op::Scan {
7761 n_carries: 1,
7762 n_xs: 0,
7763 bound: 2,
7764 emit: ScanEmit::All,
7765 early_exit: None
7766 }),
7767 None,
7768 );
7769 }
7770
7771 #[test]
7777 fn build_wait_set_collects_only_cross_device_copy_producers() {
7778 let mut g = Graph::new();
7779 let const_id = g.push(Node {
7780 op: Op::Const,
7781 inputs: vec![],
7782 shape: Shape::from_dims(&[4]),
7783 dtype: DType::F32,
7784 });
7785 let same_device_id = g.push(Node {
7787 op: Op::Contiguize,
7788 inputs: vec![const_id],
7789 shape: Shape::from_dims(&[4]),
7790 dtype: DType::F32,
7791 });
7792 let copy_id = g.push(Node {
7794 op: Op::Copy {
7795 target: DeviceLocation::Cuda { gpu_id: 0 },
7796 },
7797 inputs: vec![same_device_id],
7798 shape: Shape::from_dims(&[4]),
7799 dtype: DType::F32,
7800 });
7801 let order = vec![const_id, same_device_id, copy_id];
7802
7803 let set = build_wait_set(&g, &order);
7804 assert!(
7805 set.contains(&same_device_id),
7806 "the Copy's producer must be in the wait set"
7807 );
7808 assert!(
7809 !set.contains(&const_id),
7810 "a node with no Copy/Move consumer must be absent from the wait set"
7811 );
7812 assert!(
7813 !set.contains(©_id),
7814 "the Copy node itself is not its own producer"
7815 );
7816 assert_eq!(
7817 set.len(),
7818 1,
7819 "only the one true cross-device producer is in the set"
7820 );
7821 }
7822
7823 #[test]
7825 fn build_wait_set_collects_move_producers_too() {
7826 let mut g = Graph::new();
7827 let const_id = g.push(Node {
7828 op: Op::Const,
7829 inputs: vec![],
7830 shape: Shape::from_dims(&[4]),
7831 dtype: DType::F32,
7832 });
7833 let move_id = g.push(Node {
7834 op: Op::Move {
7835 target: DeviceLocation::Cpu,
7836 },
7837 inputs: vec![const_id],
7838 shape: Shape::from_dims(&[4]),
7839 dtype: DType::F32,
7840 });
7841 let order = vec![const_id, move_id];
7842
7843 let set = build_wait_set(&g, &order);
7844 assert!(
7845 set.contains(&const_id),
7846 "the Move's producer must be in the wait set"
7847 );
7848 assert_eq!(set.len(), 1);
7849 }
7850
7851 #[test]
7854 fn build_wait_set_is_empty_for_cpu_only_order() {
7855 let mut g = Graph::new();
7856 let const_id = g.push(Node {
7857 op: Op::Const,
7858 inputs: vec![],
7859 shape: Shape::from_dims(&[4]),
7860 dtype: DType::F32,
7861 });
7862 let contig_id = g.push(Node {
7863 op: Op::Contiguize,
7864 inputs: vec![const_id],
7865 shape: Shape::from_dims(&[4]),
7866 dtype: DType::F32,
7867 });
7868 let order = vec![const_id, contig_id];
7869
7870 let set = build_wait_set(&g, &order);
7871 assert!(
7872 set.is_empty(),
7873 "a CPU-only order (no Copy/Move) yields an empty wait set"
7874 );
7875 }
7876
7877 #[test]
7881 fn will_be_waited_none_is_conservative_true_for_any_node() {
7882 let ws: WaitSet = None;
7883 assert!(will_be_waited(&ws, NodeId(0)));
7884 assert!(will_be_waited(&ws, NodeId(12345)));
7885 }
7886
7887 #[test]
7888 fn will_be_waited_some_checks_membership() {
7889 let mut set = HashSet::new();
7890 set.insert(NodeId(3));
7891 let ws: WaitSet = Some(set);
7892 assert!(will_be_waited(&ws, NodeId(3)), "member must be waited");
7893 assert!(!will_be_waited(&ws, NodeId(4)), "non-member must be elided");
7894 }
7895
7896 struct FlagCompletion(std::sync::Arc<std::sync::atomic::AtomicUsize>);
7899 impl crate::compiled::Completion for FlagCompletion {
7900 fn wait(self: Box<Self>) -> Result<()> {
7901 self.0.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
7902 Ok(())
7903 }
7904 }
7905
7906 #[test]
7910 fn handle_map_stores_pending_and_drains_once() {
7911 use std::sync::Arc as StdArc;
7912 use std::sync::atomic::{AtomicUsize, Ordering};
7913
7914 let mut handles: HashMap<NodeId, CompletionHandle> = HashMap::new();
7915
7916 store_handle(&mut handles, NodeId(1), CompletionHandle::Ready);
7918 assert!(handles.is_empty(), "Ready handles must not be stored");
7919
7920 let waited = StdArc::new(AtomicUsize::new(0));
7922 store_handle(
7923 &mut handles,
7924 NodeId(2),
7925 CompletionHandle::Pending(Box::new(FlagCompletion(waited.clone()))),
7926 );
7927 store_handle(
7928 &mut handles,
7929 NodeId(3),
7930 CompletionHandle::Pending(Box::new(FlagCompletion(waited.clone()))),
7931 );
7932 assert_eq!(handles.len(), 2);
7933
7934 drain_handles(&mut handles).expect("drain");
7935 assert_eq!(
7936 waited.load(Ordering::SeqCst),
7937 2,
7938 "each pending handle waited once"
7939 );
7940 assert!(handles.is_empty(), "map empty after drain");
7941 }
7942
7943 #[test]
7947 fn handle_map_rekey_waits_previous() {
7948 use std::sync::Arc as StdArc;
7949 use std::sync::atomic::{AtomicUsize, Ordering};
7950
7951 let mut handles: HashMap<NodeId, CompletionHandle> = HashMap::new();
7952 let waited = StdArc::new(AtomicUsize::new(0));
7953
7954 store_handle(
7955 &mut handles,
7956 NodeId(7),
7957 CompletionHandle::Pending(Box::new(FlagCompletion(waited.clone()))),
7958 );
7959 store_handle(
7961 &mut handles,
7962 NodeId(7),
7963 CompletionHandle::Pending(Box::new(FlagCompletion(waited.clone()))),
7964 );
7965 assert_eq!(
7966 waited.load(Ordering::SeqCst),
7967 1,
7968 "rekey waited the prior pending"
7969 );
7970
7971 store_handle(&mut handles, NodeId(7), CompletionHandle::Ready);
7973 assert_eq!(
7974 waited.load(Ordering::SeqCst),
7975 2,
7976 "Ready-over-Pending waited it"
7977 );
7978 assert!(handles.is_empty());
7979
7980 drain_handles(&mut handles).expect("drain empty");
7981 }
7982
7983 #[test]
7990 fn wait_producer_handle_consumes_named_handle_once() {
7991 use std::sync::Arc as StdArc;
7992 use std::sync::atomic::{AtomicUsize, Ordering};
7993
7994 let mut handles: HashMap<NodeId, CompletionHandle> = HashMap::new();
7995 let waited = StdArc::new(AtomicUsize::new(0));
7996
7997 store_handle(
7999 &mut handles,
8000 NodeId(2),
8001 CompletionHandle::Pending(Box::new(FlagCompletion(waited.clone()))),
8002 );
8003 store_handle(
8004 &mut handles,
8005 NodeId(5),
8006 CompletionHandle::Pending(Box::new(FlagCompletion(waited.clone()))),
8007 );
8008
8009 wait_producer_handle(&mut handles, NodeId(99)).expect("absent is no-op");
8011 assert_eq!(
8012 waited.load(Ordering::SeqCst),
8013 0,
8014 "absent producer waited nothing"
8015 );
8016 assert_eq!(handles.len(), 2);
8017
8018 wait_producer_handle(&mut handles, NodeId(2)).expect("wait source");
8020 assert_eq!(
8021 waited.load(Ordering::SeqCst),
8022 1,
8023 "source producer waited once"
8024 );
8025 assert!(!handles.contains_key(&NodeId(2)), "source handle consumed");
8026 assert!(
8027 handles.contains_key(&NodeId(5)),
8028 "unrelated handle untouched"
8029 );
8030
8031 wait_producer_handle(&mut handles, NodeId(2)).expect("re-wait is no-op");
8033 assert_eq!(waited.load(Ordering::SeqCst), 1, "no double-wait");
8034
8035 drain_handles(&mut handles).expect("drain");
8037 assert_eq!(
8038 waited.load(Ordering::SeqCst),
8039 2,
8040 "drain waited the remaining one"
8041 );
8042 assert!(handles.is_empty());
8043 }
8044
8045 #[test]
8048 fn op_contiguize_zero_copy_on_contiguous_input() {
8049 let storage = fuel_memory::from_slice_cpu(&[1.0_f32, 2.0, 3.0, 4.0]);
8050
8051 let graph = Arc::new(RwLock::new(Graph::new()));
8052 let (const_id, contig_id) = {
8053 let mut g = graph.write().unwrap();
8054 let const_id = g.push(Node {
8055 op: Op::Const,
8056 inputs: vec![],
8057 shape: Shape::from_dims(&[2, 2]),
8058 dtype: DType::F32,
8059 });
8060 let contig_id = g.push(Node {
8061 op: Op::Contiguize,
8062 inputs: vec![const_id],
8063 shape: Shape::from_dims(&[2, 2]),
8064 dtype: DType::F32,
8065 });
8066 (const_id, contig_id)
8069 };
8070
8071 let mut inputs = StorageCache::new();
8072 let input_arc = Arc::new(RwLock::new(storage));
8073 let input_arc_for_compare = Arc::clone(&input_arc);
8074 inputs.insert(const_id, input_arc);
8075
8076 let (result_arc, _) =
8077 PipelinedExecutor::realize(graph, contig_id, inputs).expect("realize");
8078
8079 assert!(
8081 Arc::ptr_eq(&result_arc, &input_arc_for_compare),
8082 "Op::Contiguize on contiguous input must adopt the input Arc (zero copy)",
8083 );
8084 }
8085
8086 #[test]
8091 fn op_contiguize_materializes_on_strided_input() {
8092 let storage = fuel_memory::from_slice_cpu(&[1.0_f32, 2.0, 3.0, 4.0, 5.0, 6.0]);
8098
8099 let graph = Arc::new(RwLock::new(Graph::new()));
8100 let (const_id, _transpose_id, contig_id) = {
8101 let mut g = graph.write().unwrap();
8102 let const_id = g.push(Node {
8103 op: Op::Const,
8104 inputs: vec![],
8105 shape: Shape::from_dims(&[2, 3]),
8106 dtype: DType::F32,
8107 });
8108 let transpose_id = g.push(Node {
8109 op: Op::Transpose,
8110 inputs: vec![const_id],
8111 shape: Shape::from_dims(&[3, 2]),
8112 dtype: DType::F32,
8113 });
8114 let contig_id = g.push(Node {
8115 op: Op::Contiguize,
8116 inputs: vec![transpose_id],
8117 shape: Shape::from_dims(&[3, 2]),
8118 dtype: DType::F32,
8119 });
8120 (const_id, transpose_id, contig_id)
8121 };
8122
8123 let mut inputs = StorageCache::new();
8124 inputs.insert(const_id, Arc::new(RwLock::new(storage)));
8125
8126 let (result_arc, _) =
8127 PipelinedExecutor::realize(graph, contig_id, inputs).expect("realize");
8128
8129 let guard = result_arc.read().unwrap();
8130 let cpu = if let fuel_memory::BackendStorage::Cpu(c) = &guard.inner {
8131 c
8132 } else {
8133 panic!("expected Cpu storage");
8134 };
8135 let typed: &[f32] = cpu.as_slice().expect("f32 cast");
8136 assert_eq!(
8138 typed,
8139 &[1.0, 4.0, 2.0, 5.0, 3.0, 6.0],
8140 "contiguized transpose must reflect logical (transposed) byte order",
8141 );
8142 }
8143
8144 #[test]
8148 fn pipelined_realize_const_const_add() {
8149 let lhs_storage = fuel_memory::from_slice_cpu(&[1.0_f32, 2.0, 3.0]);
8150 let rhs_storage = fuel_memory::from_slice_cpu(&[10.0_f32, 20.0, 30.0]);
8151
8152 let graph = Arc::new(RwLock::new(Graph::new()));
8153 let (lhs_id, rhs_id, add_id) = {
8154 let mut g = graph.write().unwrap();
8155 let lhs_id = g.push(Node {
8156 op: Op::Const,
8157 inputs: vec![],
8158 shape: Shape::from_dims(&[3]),
8159 dtype: DType::F32,
8160 });
8161 let rhs_id = g.push(Node {
8162 op: Op::Const,
8163 inputs: vec![],
8164 shape: Shape::from_dims(&[3]),
8165 dtype: DType::F32,
8166 });
8167 let add_id = g.push(Node {
8168 op: Op::Add,
8169 inputs: vec![lhs_id, rhs_id],
8170 shape: Shape::from_dims(&[3]),
8171 dtype: DType::F32,
8172 });
8173 g.set_target_backend(add_id, BackendId::Cpu);
8174 (lhs_id, rhs_id, add_id)
8175 };
8176
8177 let mut inputs = StorageCache::new();
8178 inputs.insert(lhs_id, Arc::new(RwLock::new(lhs_storage)));
8179 inputs.insert(rhs_id, Arc::new(RwLock::new(rhs_storage)));
8180
8181 let (result_arc, _result_layout) =
8182 PipelinedExecutor::realize(graph, add_id, inputs).expect("realize");
8183
8184 let guard = result_arc.read().unwrap();
8185 if let fuel_memory::BackendStorage::Cpu(c) = &guard.inner {
8186 let typed: &[f32] = c.as_slice().expect("f32 cast");
8187 assert_eq!(typed, &[11.0, 22.0, 33.0]);
8188 } else {
8189 panic!("expected CPU output");
8190 }
8191 }
8192
8193 #[test]
8206 #[ignore = "requires a live CUDA device"]
8207 #[cfg(feature = "cuda")]
8208 fn capture_decode_binary_chain_replays_bit_exact_cuda() {
8209 use fuel_cuda_backend::{CudaDevice, CudaStorageBytes};
8210 use fuel_ir::Shape;
8211 use fuel_memory::BackendStorage;
8212
8213 let Ok(dev) = CudaDevice::new(0) else {
8214 return fuel_test_support::hardware::skip(
8215 fuel_test_support::hardware::Hardware::Cuda,
8216 fuel_test_support::hardware::Missing::device("CudaDevice::new(0) failed"),
8217 );
8218 };
8219
8220 let build = || -> (Arc<RwLock<Graph>>, NodeId, NodeId, NodeId, NodeId) {
8223 let graph = Arc::new(RwLock::new(Graph::new()));
8224 let (a, b, c, z) = {
8225 let mut g = graph.write().unwrap();
8226 let a = g.push(Node {
8227 op: Op::Const,
8228 inputs: vec![],
8229 shape: Shape::from_dims(&[4]),
8230 dtype: DType::F32,
8231 });
8232 let b = g.push(Node {
8233 op: Op::Const,
8234 inputs: vec![],
8235 shape: Shape::from_dims(&[4]),
8236 dtype: DType::F32,
8237 });
8238 let c = g.push(Node {
8239 op: Op::Const,
8240 inputs: vec![],
8241 shape: Shape::from_dims(&[4]),
8242 dtype: DType::F32,
8243 });
8244 let y = g.push(Node {
8245 op: Op::Add,
8246 inputs: vec![a, b],
8247 shape: Shape::from_dims(&[4]),
8248 dtype: DType::F32,
8249 });
8250 let z = g.push(Node {
8251 op: Op::Mul,
8252 inputs: vec![y, c],
8253 shape: Shape::from_dims(&[4]),
8254 dtype: DType::F32,
8255 });
8256 g.set_target_backend(y, BackendId::Cuda);
8257 g.set_target_backend(z, BackendId::Cuda);
8258 (a, b, c, z)
8259 };
8260 (graph, a, b, c, z)
8261 };
8262
8263 let up = |v: &[f32]| -> Arc<RwLock<Storage>> {
8264 let bytes: &[u8] = bytemuck::cast_slice(v);
8265 let cb = CudaStorageBytes::from_cpu_bytes(&dev, bytes).expect("h2d");
8266 Arc::new(RwLock::new(Storage::new(
8267 BackendStorage::Cuda(cb),
8268 DType::F32,
8269 )))
8270 };
8271 let read = |arc: &Arc<RwLock<Storage>>| -> Vec<f32> {
8272 let g = arc.read().unwrap();
8273 let BackendStorage::Cuda(c) = &g.inner else {
8274 panic!("not cuda")
8275 };
8276 let host = c.to_cpu_bytes().expect("d2h");
8277 bytemuck::cast_slice(&host).to_vec()
8278 };
8279
8280 let a_data = [1.0_f32, 2.0, 3.0, 4.0];
8281 let b_data = [10.0_f32, 20.0, 30.0, 40.0];
8282 let c_data = [2.0_f32, 2.0, 2.0, 2.0];
8283 let expect0 = [22.0_f32, 44.0, 66.0, 88.0]; let a_arc = up(&a_data);
8287 let b_arc = up(&b_data);
8288 let c_arc = up(&c_data);
8289
8290 let (graph, a, b, c, z) = build();
8291 let mut inputs = StorageCache::new();
8292 inputs.insert(a, a_arc.clone());
8293 inputs.insert(b, b_arc.clone());
8294 inputs.insert(c, c_arc.clone());
8295
8296 let captured =
8297 PipelinedExecutor::capture_decode(Arc::clone(&graph), z, inputs, SymEnv::default())
8298 .expect("capture_decode");
8299
8300 assert_eq!(
8302 read(&captured.output),
8303 &expect0,
8304 "warm-run result after capture"
8305 );
8306
8307 captured.run.replay(&dev).expect("replay");
8309 dev.synchronize().expect("sync");
8310 assert_eq!(read(&captured.output), &expect0, "replay reproduces result");
8311
8312 let a2 = [100.0_f32, 200.0, 300.0, 400.0];
8316 {
8317 let g = a_arc.read().unwrap();
8318 let BackendStorage::Cuda(c) = &g.inner else {
8319 panic!("a not cuda")
8320 };
8321 c.write_from_host(bytemuck::cast_slice(&a2))
8322 .expect("in-place H2D");
8323 }
8324 captured.run.replay(&dev).expect("replay 2");
8325 dev.synchronize().expect("sync 2");
8326 let got = read(&captured.output);
8327
8328 let (graph_ref, ar, br, cr, zr) = build();
8329 let mut inputs_ref = StorageCache::new();
8330 inputs_ref.insert(ar, up(&a2));
8331 inputs_ref.insert(br, up(&b_data));
8332 inputs_ref.insert(cr, up(&c_data));
8333 let (ref_arc, _) =
8334 PipelinedExecutor::realize(graph_ref, zr, inputs_ref).expect("ref realize");
8335 let expect1 = read(&ref_arc); assert_eq!(
8337 got, expect1,
8338 "replay after in-place input update matches uncaptured realize"
8339 );
8340 assert_eq!(expect1, vec![220.0, 440.0, 660.0, 880.0]);
8341 }
8342
8343 #[test]
8356 #[ignore = "requires a live CUDA device"]
8357 #[cfg(feature = "cuda")]
8358 fn capture_decode_matmul_replays_bit_exact_cuda() {
8359 use fuel_cuda_backend::{CudaDevice, CudaStorageBytes};
8360 use fuel_ir::Shape;
8361 use fuel_memory::BackendStorage;
8362 let Ok(dev) = CudaDevice::new(0) else { return };
8363 const H: usize = 4;
8364 const F: usize = 8;
8365 let a_data: Vec<f32> = (0..H).map(|i| 0.5 + i as f32).collect();
8366 let w_data: Vec<f32> = (0..H * F).map(|i| 0.01 * (i as f32 + 1.0)).collect();
8367 let build = || -> (Arc<RwLock<Graph>>, NodeId, NodeId, NodeId) {
8368 let graph = Arc::new(RwLock::new(Graph::new()));
8369 let (a, w, y) = {
8370 let mut g = graph.write().unwrap();
8371 let a = g.push(Node {
8372 op: Op::Const,
8373 inputs: vec![],
8374 shape: Shape::from_dims(&[1, H]),
8375 dtype: DType::F32,
8376 });
8377 let w = g.push(Node {
8378 op: Op::Const,
8379 inputs: vec![],
8380 shape: Shape::from_dims(&[H, F]),
8381 dtype: DType::F32,
8382 });
8383 let y = g.push(Node {
8384 op: Op::MatMul,
8385 inputs: vec![a, w],
8386 shape: Shape::from_dims(&[1, F]),
8387 dtype: DType::F32,
8388 });
8389 g.set_target_backend(y, BackendId::Cuda);
8390 (a, w, y)
8391 };
8392 (graph, a, w, y)
8393 };
8394 let up = |v: &[f32]| {
8395 let b: Vec<u8> = v.iter().flat_map(|f| f.to_le_bytes()).collect();
8396 Arc::new(RwLock::new(Storage::new(
8397 BackendStorage::Cuda(CudaStorageBytes::from_cpu_bytes(&dev, &b).unwrap()),
8398 DType::F32,
8399 )))
8400 };
8401 let read = |arc: &Arc<RwLock<Storage>>| -> Vec<f32> {
8402 let g = arc.read().unwrap();
8403 let BackendStorage::Cuda(c) = &g.inner else {
8404 panic!("not cuda")
8405 };
8406 c.to_cpu_bytes()
8407 .unwrap()
8408 .chunks_exact(4)
8409 .map(|b| f32::from_le_bytes([b[0], b[1], b[2], b[3]]))
8410 .collect()
8411 };
8412 let (gr, ar, wr, yr) = build();
8414 let mut insr = StorageCache::new();
8415 insr.insert(ar, up(&a_data));
8416 insr.insert(wr, up(&w_data));
8417 let (refarc, _) = PipelinedExecutor::realize(gr, yr, insr).unwrap();
8418 let expect = read(&refarc);
8419 let (graph, a, w, y) = build();
8421 let mut inputs = StorageCache::new();
8422 inputs.insert(a, up(&a_data));
8423 inputs.insert(w, up(&w_data));
8424 let captured =
8425 PipelinedExecutor::capture_decode(Arc::clone(&graph), y, inputs, SymEnv::default())
8426 .expect("capture");
8427 assert_eq!(read(&captured.output), expect, "warm result after capture");
8428 let mut squatters = Vec::new();
8431 for _ in 0..8 {
8432 let g: Vec<u8> = (0..H * F).flat_map(|_| 999.0f32.to_le_bytes()).collect();
8433 squatters.push(CudaStorageBytes::from_cpu_bytes(&dev, &g).unwrap());
8434 }
8435 dev.synchronize().unwrap();
8436 let mut bad = 0usize;
8437 for _ in 0..50 {
8438 captured.run.replay(&dev).unwrap();
8439 dev.synchronize().unwrap();
8440 if read(&captured.output) != expect {
8441 bad += 1;
8442 }
8443 }
8444 assert_eq!(
8445 bad, 0,
8446 "cuBLAS matmul capture replays bit-exact ({bad}/50 wrong)"
8447 );
8448 }
8449
8450 #[test]
8453 #[ignore = "requires a live CUDA device"]
8454 #[cfg(feature = "cuda")]
8455 fn capture_decode_affine_replays_bit_exact_cuda() {
8456 use fuel_cuda_backend::{CudaDevice, CudaStorageBytes};
8457 use fuel_ir::Shape;
8458 use fuel_memory::BackendStorage;
8459 let Ok(dev) = CudaDevice::new(0) else { return };
8460 const N: usize = 6;
8461 let x_data: Vec<f32> = (0..N).map(|i| 1.0 + i as f32).collect();
8462 let expect: Vec<f32> = x_data.iter().map(|v| v * 0.5).collect();
8463 let graph = Arc::new(RwLock::new(Graph::new()));
8464 let (x, y) = {
8465 let mut g = graph.write().unwrap();
8466 let x = g.push(Node {
8467 op: Op::Const,
8468 inputs: vec![],
8469 shape: Shape::from_dims(&[1, N]),
8470 dtype: DType::F32,
8471 });
8472 let y = g.push(Node {
8473 op: Op::MulScalar(0.5),
8474 inputs: vec![x],
8475 shape: Shape::from_dims(&[1, N]),
8476 dtype: DType::F32,
8477 });
8478 g.set_target_backend(y, BackendId::Cuda);
8479 (x, y)
8480 };
8481 let up = |v: &[f32]| {
8482 let b: Vec<u8> = v.iter().flat_map(|f| f.to_le_bytes()).collect();
8483 Arc::new(RwLock::new(Storage::new(
8484 BackendStorage::Cuda(CudaStorageBytes::from_cpu_bytes(&dev, &b).unwrap()),
8485 DType::F32,
8486 )))
8487 };
8488 let read = |arc: &Arc<RwLock<Storage>>| -> Vec<f32> {
8489 let g = arc.read().unwrap();
8490 let BackendStorage::Cuda(c) = &g.inner else {
8491 panic!("not cuda")
8492 };
8493 c.to_cpu_bytes()
8494 .unwrap()
8495 .chunks_exact(4)
8496 .map(|b| f32::from_le_bytes([b[0], b[1], b[2], b[3]]))
8497 .collect()
8498 };
8499 let mut inputs = StorageCache::new();
8500 inputs.insert(x, up(&x_data));
8501 let captured =
8502 PipelinedExecutor::capture_decode(Arc::clone(&graph), y, inputs, SymEnv::default())
8503 .expect("capture");
8504 assert_eq!(read(&captured.output), expect, "warm result after capture");
8505 let mut sq = Vec::new();
8506 for _ in 0..8 {
8507 let g: Vec<u8> = (0..N).flat_map(|_| 999.0f32.to_le_bytes()).collect();
8508 sq.push(CudaStorageBytes::from_cpu_bytes(&dev, &g).unwrap());
8509 }
8510 dev.synchronize().unwrap();
8511 let mut bad = 0usize;
8512 for _ in 0..50 {
8513 captured.run.replay(&dev).unwrap();
8514 dev.synchronize().unwrap();
8515 if read(&captured.output) != expect {
8516 bad += 1;
8517 }
8518 }
8519 assert_eq!(bad, 0, "affine capture replays bit-exact ({bad}/50 wrong)");
8520 }
8521
8522 #[test]
8529 #[ignore = "requires a live CUDA device"]
8530 #[cfg(feature = "cuda")]
8531 fn capture_decode_index_select_replays_bit_exact_cuda() {
8532 use fuel_cuda_backend::{CudaDevice, CudaStorageBytes};
8533 use fuel_ir::Shape;
8534 use fuel_memory::BackendStorage;
8535 let Ok(dev) = CudaDevice::new(0) else { return };
8536 const V: usize = 3;
8537 const H: usize = 4;
8538 let wte_data: Vec<f32> = (0..V)
8539 .flat_map(|v| (0..H).map(move |c| 1.0 + 10.0 * v as f32 + c as f32))
8540 .collect();
8541 let graph = Arc::new(RwLock::new(Graph::new()));
8542 let (tok, wte, emb) = {
8543 let mut g = graph.write().unwrap();
8544 let tok = g.push(Node {
8545 op: Op::Const,
8546 inputs: vec![],
8547 shape: Shape::from_dims(&[1]),
8548 dtype: DType::U32,
8549 });
8550 let wte = g.push(Node {
8551 op: Op::Const,
8552 inputs: vec![],
8553 shape: Shape::from_dims(&[V, H]),
8554 dtype: DType::F32,
8555 });
8556 let emb = g.push(Node {
8557 op: Op::IndexSelect { dim: 0 },
8558 inputs: vec![wte, tok],
8559 shape: Shape::from_dims(&[1, H]),
8560 dtype: DType::F32,
8561 });
8562 g.set_target_backend(emb, BackendId::Cuda);
8563 (tok, wte, emb)
8564 };
8565 let up_f32 = |v: &[f32]| {
8566 let b: Vec<u8> = v.iter().flat_map(|f| f.to_le_bytes()).collect();
8567 Arc::new(RwLock::new(Storage::new(
8568 BackendStorage::Cuda(CudaStorageBytes::from_cpu_bytes(&dev, &b).unwrap()),
8569 DType::F32,
8570 )))
8571 };
8572 let up_u32 = |v: &[u32]| {
8573 let b: Vec<u8> = v.iter().flat_map(|x| x.to_le_bytes()).collect();
8574 Arc::new(RwLock::new(Storage::new(
8575 BackendStorage::Cuda(CudaStorageBytes::from_cpu_bytes(&dev, &b).unwrap()),
8576 DType::U32,
8577 )))
8578 };
8579 let read = |arc: &Arc<RwLock<Storage>>| -> Vec<f32> {
8580 let g = arc.read().unwrap();
8581 let BackendStorage::Cuda(c) = &g.inner else {
8582 panic!("not cuda")
8583 };
8584 c.to_cpu_bytes()
8585 .unwrap()
8586 .chunks_exact(4)
8587 .map(|b| f32::from_le_bytes([b[0], b[1], b[2], b[3]]))
8588 .collect()
8589 };
8590 let tok_arc = up_u32(&[0]);
8591 let mut inputs = StorageCache::new();
8592 inputs.insert(tok, tok_arc.clone());
8593 inputs.insert(wte, up_f32(&wte_data));
8594 let captured =
8595 PipelinedExecutor::capture_decode(Arc::clone(&graph), emb, inputs, SymEnv::default())
8596 .expect("capture");
8597 assert_eq!(
8598 read(&captured.output),
8599 vec![1.0, 2.0, 3.0, 4.0],
8600 "warm result after capture"
8601 );
8602 let mut squatters = Vec::new();
8605 for _ in 0..8 {
8606 let g: Vec<u8> = (0..V * H).flat_map(|_| 999.0f32.to_le_bytes()).collect();
8607 squatters.push(CudaStorageBytes::from_cpu_bytes(&dev, &g).unwrap());
8608 }
8609 dev.synchronize().unwrap();
8610 let mut bad = 0usize;
8611 for _ in 0..50 {
8612 captured.run.replay(&dev).unwrap();
8613 dev.synchronize().unwrap();
8614 if read(&captured.output) != vec![1.0, 2.0, 3.0, 4.0] {
8615 bad += 1;
8616 }
8617 }
8618 assert_eq!(
8619 bad, 0,
8620 "index_select capture replays bit-exact ({bad}/50 wrong)"
8621 );
8622 }
8623
8624 #[test]
8636 #[ignore = "requires a live CUDA device"]
8637 #[cfg(feature = "cuda")]
8638 fn capture_decode_norm_silu_softmax_replays_bit_exact_cuda() {
8639 use fuel_cuda_backend::{CudaDevice, CudaStorageBytes};
8640 use fuel_graph::registry::{FusedOpParams, FusedOps};
8641 use fuel_ir::Shape;
8642 use fuel_memory::BackendStorage;
8643
8644 let Ok(dev) = CudaDevice::new(0) else {
8645 return fuel_test_support::hardware::skip(
8646 fuel_test_support::hardware::Hardware::Cuda,
8647 fuel_test_support::hardware::Missing::device("CudaDevice::new(0) failed"),
8648 );
8649 };
8650 const H: usize = 6;
8651 let eps = 1e-5f64;
8652
8653 let build = || -> (Arc<RwLock<Graph>>, NodeId, NodeId) {
8654 let graph = Arc::new(RwLock::new(Graph::new()));
8655 let (x, y) = {
8656 let mut g = graph.write().unwrap();
8657 let mk = |g: &mut Graph, op: Op, ins: Vec<NodeId>, dims: &[usize]| {
8658 g.push(Node {
8659 op,
8660 inputs: ins,
8661 shape: Shape::from_dims(dims),
8662 dtype: DType::F32,
8663 })
8664 };
8665 let rms = || {
8666 Op::Fused(
8667 FusedOps::RMS_NORM_LAST_DIM,
8668 FusedOpParams::RmsNormLastDim { eps },
8669 )
8670 };
8671 let sm = || Op::Fused(FusedOps::SOFTMAX_LAST_DIM, FusedOpParams::SoftmaxLastDim);
8672 let x = mk(&mut g, Op::Const, vec![], &[1, H]);
8673 let n1 = mk(&mut g, rms(), vec![x], &[1, H]);
8674 let s = mk(&mut g, Op::Silu, vec![n1], &[1, H]);
8675 let p = mk(&mut g, sm(), vec![s], &[1, H]);
8676 let r = mk(&mut g, Op::Add, vec![p, x], &[1, H]);
8677 let n2 = mk(&mut g, rms(), vec![r], &[1, H]);
8678 let y = mk(&mut g, Op::Add, vec![n2, x], &[1, H]);
8679 for id in [n1, s, p, r, n2, y] {
8680 g.set_target_backend(id, BackendId::Cuda);
8681 }
8682 (x, y)
8683 };
8684 (graph, x, y)
8685 };
8686
8687 let up = |v: &[f32]| -> Arc<RwLock<Storage>> {
8688 let cb = CudaStorageBytes::from_cpu_bytes(&dev, bytemuck::cast_slice(v)).expect("h2d");
8689 Arc::new(RwLock::new(Storage::new(
8690 BackendStorage::Cuda(cb),
8691 DType::F32,
8692 )))
8693 };
8694 let read = |arc: &Arc<RwLock<Storage>>| -> Vec<f32> {
8695 let g = arc.read().unwrap();
8696 let BackendStorage::Cuda(c) = &g.inner else {
8697 panic!("not cuda")
8698 };
8699 bytemuck::cast_slice(&c.to_cpu_bytes().expect("d2h")).to_vec()
8700 };
8701
8702 let x_data: Vec<f32> = (0..H).map(|c| 0.1 * (c as f32 + 1.0)).collect();
8703 let x_arc = up(&x_data);
8704 let (graph, x, y) = build();
8705 let mut inputs = StorageCache::new();
8706 inputs.insert(x, x_arc.clone());
8707
8708 let captured =
8709 PipelinedExecutor::capture_decode(Arc::clone(&graph), y, inputs, SymEnv::default())
8710 .expect("capture_decode norm/silu/softmax");
8711
8712 let realize_ref = |xv: &[f32]| -> Vec<f32> {
8713 let (g, xr, yr) = build();
8714 let mut ins = StorageCache::new();
8715 ins.insert(xr, up(xv));
8716 let (arc, _) = PipelinedExecutor::realize(g, yr, ins).expect("ref realize");
8717 read(&arc)
8718 };
8719 let expect0 = realize_ref(&x_data);
8720
8721 assert_eq!(read(&captured.output), expect0, "warm result after capture");
8723 captured.run.replay(&dev).expect("replay");
8725 dev.synchronize().expect("sync");
8726 assert_eq!(read(&captured.output), expect0, "replay reproduces result");
8727
8728 let x2: Vec<f32> = (0..H).map(|c| 0.3 * (c as f32 + 1.0)).collect();
8731 {
8732 let g = x_arc.read().unwrap();
8733 let BackendStorage::Cuda(c) = &g.inner else {
8734 panic!("x not cuda")
8735 };
8736 c.write_from_host(bytemuck::cast_slice(&x2))
8737 .expect("in-place H2D");
8738 }
8739 captured.run.replay(&dev).expect("replay 2");
8740 dev.synchronize().expect("sync 2");
8741 assert_eq!(
8742 read(&captured.output),
8743 realize_ref(&x2),
8744 "replay after x update matches realize"
8745 );
8746 }
8747
8748 #[test]
8760 #[ignore = "requires a live CUDA device"]
8761 #[cfg(feature = "cuda")]
8762 fn capture_decode_contiguize_then_concat_replays_bit_exact_cuda() {
8763 use fuel_cuda_backend::{CudaDevice, CudaStorageBytes};
8764 use fuel_ir::Shape;
8765 use fuel_memory::BackendStorage;
8766
8767 let Ok(dev) = CudaDevice::new(0) else {
8768 return fuel_test_support::hardware::skip(
8769 fuel_test_support::hardware::Hardware::Cuda,
8770 fuel_test_support::hardware::Missing::device("CudaDevice::new(0) failed"),
8771 );
8772 };
8773
8774 let build = || -> (Arc<RwLock<Graph>>, NodeId, NodeId, NodeId) {
8775 let graph = Arc::new(RwLock::new(Graph::new()));
8776 let (x, b, y) = {
8777 let mut g = graph.write().unwrap();
8778 let x = g.push(Node {
8779 op: Op::Const,
8780 inputs: vec![],
8781 shape: Shape::from_dims(&[2, 3, 4]),
8782 dtype: DType::F32,
8783 });
8784 let p = g.push(Node {
8785 op: Op::Permute(vec![1, 0, 2]),
8786 inputs: vec![x],
8787 shape: Shape::from_dims(&[3, 2, 4]),
8788 dtype: DType::F32,
8789 });
8790 let r = g.push(Node {
8791 op: Op::Reshape(Shape::from_dims(&[3, 8])),
8792 inputs: vec![p],
8793 shape: Shape::from_dims(&[3, 8]),
8794 dtype: DType::F32,
8795 });
8796 let b = g.push(Node {
8797 op: Op::Const,
8798 inputs: vec![],
8799 shape: Shape::from_dims(&[3, 8]),
8800 dtype: DType::F32,
8801 });
8802 let y = g.push(Node {
8803 op: Op::Concat { dim: 1 },
8804 inputs: vec![r, b],
8805 shape: Shape::from_dims(&[3, 16]),
8806 dtype: DType::F32,
8807 });
8808 g.set_target_backend(p, BackendId::Cuda);
8809 g.set_target_backend(r, BackendId::Cuda);
8810 g.set_target_backend(y, BackendId::Cuda);
8811 (x, b, y)
8812 };
8813 (graph, x, b, y)
8814 };
8815
8816 let up = |v: &[f32]| -> Arc<RwLock<Storage>> {
8817 let cb = CudaStorageBytes::from_cpu_bytes(&dev, bytemuck::cast_slice(v)).expect("h2d");
8818 Arc::new(RwLock::new(Storage::new(
8819 BackendStorage::Cuda(cb),
8820 DType::F32,
8821 )))
8822 };
8823 let read = |arc: &Arc<RwLock<Storage>>| -> Vec<f32> {
8824 let g = arc.read().unwrap();
8825 let BackendStorage::Cuda(c) = &g.inner else {
8826 panic!("not cuda")
8827 };
8828 bytemuck::cast_slice(&c.to_cpu_bytes().expect("d2h")).to_vec()
8829 };
8830
8831 let x_data: Vec<f32> = (0..24).map(|i| i as f32).collect();
8832 let b_data: Vec<f32> = (0..24).map(|i| 100.0 + i as f32).collect();
8833 let x_arc = up(&x_data);
8834 let (graph, x, b, y) = build();
8835 let mut inputs = StorageCache::new();
8836 inputs.insert(x, x_arc.clone());
8837 inputs.insert(b, up(&b_data));
8841
8842 let captured =
8843 PipelinedExecutor::capture_decode(Arc::clone(&graph), y, inputs, SymEnv::default())
8844 .expect("capture_decode contiguize+concat");
8845
8846 let realize_ref = |xv: &[f32], bv: &[f32]| -> Vec<f32> {
8847 let (g, xr, br, yr) = build();
8848 let mut ins = StorageCache::new();
8849 ins.insert(xr, up(xv));
8850 ins.insert(br, up(bv));
8851 let (arc, _) = PipelinedExecutor::realize(g, yr, ins).expect("ref realize");
8852 read(&arc)
8853 };
8854 let expect0 = realize_ref(&x_data, &b_data);
8855
8856 assert_eq!(read(&captured.output), expect0, "warm result after capture");
8859
8860 for i in 0..2 {
8864 captured
8865 .run
8866 .replay(&dev)
8867 .unwrap_or_else(|e| panic!("replay {i}: {e:?}"));
8868 dev.synchronize().expect("sync");
8869 assert_eq!(
8870 read(&captured.output),
8871 expect0,
8872 "replay {i} reproduces result"
8873 );
8874 }
8875
8876 let x2: Vec<f32> = (0..24).map(|i| 1000.0 + i as f32).collect();
8881 {
8882 let g = x_arc.read().unwrap();
8883 let BackendStorage::Cuda(c) = &g.inner else {
8884 panic!("x not cuda")
8885 };
8886 c.write_from_host(bytemuck::cast_slice(&x2))
8887 .expect("in-place H2D");
8888 }
8889 captured.run.replay(&dev).expect("replay 3");
8890 dev.synchronize().expect("sync 3");
8891 assert_eq!(
8892 read(&captured.output),
8893 realize_ref(&x2, &b_data),
8894 "replay after x update matches independent uncaptured realize"
8895 );
8896 }
8897
8898 #[test]
8909 #[ignore = "requires a live CUDA device"]
8910 #[cfg(feature = "cuda")]
8911 fn capture_decode_same_device_copy_replays_bit_exact_cuda() {
8912 use fuel_cuda_backend::{CudaDevice, CudaStorageBytes};
8913 use fuel_ir::Shape;
8914 use fuel_memory::BackendStorage;
8915
8916 let Ok(dev) = CudaDevice::new(0) else {
8917 return fuel_test_support::hardware::skip(
8918 fuel_test_support::hardware::Hardware::Cuda,
8919 fuel_test_support::hardware::Missing::device("CudaDevice::new(0) failed"),
8920 );
8921 };
8922
8923 let build = || -> (Arc<RwLock<Graph>>, NodeId, NodeId, NodeId) {
8924 let graph = Arc::new(RwLock::new(Graph::new()));
8925 let (x, b, y) = {
8926 let mut g = graph.write().unwrap();
8927 let x = g.push(Node {
8928 op: Op::Const,
8929 inputs: vec![],
8930 shape: Shape::from_dims(&[2, 4]),
8931 dtype: DType::F32,
8932 });
8933 let b = g.push(Node {
8934 op: Op::Const,
8935 inputs: vec![],
8936 shape: Shape::from_dims(&[2, 4]),
8937 dtype: DType::F32,
8938 });
8939 let c = g.push(Node {
8941 op: Op::Copy {
8942 target: DeviceLocation::Cuda { gpu_id: 0 },
8943 },
8944 inputs: vec![x],
8945 shape: Shape::from_dims(&[2, 4]),
8946 dtype: DType::F32,
8947 });
8948 let y = g.push(Node {
8949 op: Op::Add,
8950 inputs: vec![c, b],
8951 shape: Shape::from_dims(&[2, 4]),
8952 dtype: DType::F32,
8953 });
8954 g.set_target_backend(c, BackendId::Cuda);
8956 g.set_target_backend(y, BackendId::Cuda);
8957 (x, b, y)
8958 };
8959 (graph, x, b, y)
8960 };
8961
8962 let up = |v: &[f32]| -> Arc<RwLock<Storage>> {
8963 let cb = CudaStorageBytes::from_cpu_bytes(&dev, bytemuck::cast_slice(v)).expect("h2d");
8964 Arc::new(RwLock::new(Storage::new(
8965 BackendStorage::Cuda(cb),
8966 DType::F32,
8967 )))
8968 };
8969 let read = |arc: &Arc<RwLock<Storage>>| -> Vec<f32> {
8970 let g = arc.read().unwrap();
8971 let BackendStorage::Cuda(c) = &g.inner else {
8972 panic!("not cuda")
8973 };
8974 bytemuck::cast_slice(&c.to_cpu_bytes().expect("d2h")).to_vec()
8975 };
8976
8977 let x_data: Vec<f32> = (0..8).map(|i| i as f32).collect();
8978 let b_data: Vec<f32> = (0..8).map(|i| 100.0 + i as f32).collect();
8979 let x_arc = up(&x_data);
8980 let (graph, x, b, y) = build();
8981 let mut inputs = StorageCache::new();
8982 inputs.insert(x, x_arc.clone());
8983 inputs.insert(b, up(&b_data));
8986
8987 let captured =
8988 PipelinedExecutor::capture_decode(Arc::clone(&graph), y, inputs, SymEnv::default())
8989 .expect("capture_decode same-device copy");
8990
8991 let realize_ref = |xv: &[f32], bv: &[f32]| -> Vec<f32> {
8992 let (g, xr, br, yr) = build();
8993 let mut ins = StorageCache::new();
8994 ins.insert(xr, up(xv));
8995 ins.insert(br, up(bv));
8996 let (arc, _) = PipelinedExecutor::realize(g, yr, ins).expect("ref realize");
8997 read(&arc)
8998 };
8999 let expect0 = realize_ref(&x_data, &b_data);
9000
9001 assert_eq!(read(&captured.output), expect0, "warm result after capture");
9003
9004 for i in 0..2 {
9007 captured
9008 .run
9009 .replay(&dev)
9010 .unwrap_or_else(|e| panic!("replay {i}: {e:?}"));
9011 dev.synchronize().expect("sync");
9012 assert_eq!(
9013 read(&captured.output),
9014 expect0,
9015 "replay {i} reproduces result"
9016 );
9017 }
9018
9019 let x2: Vec<f32> = (0..8).map(|i| 1000.0 + i as f32).collect();
9023 {
9024 let g = x_arc.read().unwrap();
9025 let BackendStorage::Cuda(c) = &g.inner else {
9026 panic!("x not cuda")
9027 };
9028 c.write_from_host(bytemuck::cast_slice(&x2))
9029 .expect("in-place H2D");
9030 }
9031 captured.run.replay(&dev).expect("replay 3");
9032 dev.synchronize().expect("sync 3");
9033 assert_eq!(
9034 read(&captured.output),
9035 realize_ref(&x2, &b_data),
9036 "replay after x update matches independent uncaptured realize"
9037 );
9038 }
9039
9040 #[test]
9046 #[ignore = "requires a live CUDA device"]
9047 #[cfg(feature = "cuda")]
9048 fn capture_decode_rejects_cross_device_copy_cuda() {
9049 use fuel_cuda_backend::{CudaDevice, CudaStorageBytes};
9050 use fuel_ir::Shape;
9051 use fuel_memory::BackendStorage;
9052
9053 let Ok(dev) = CudaDevice::new(0) else {
9054 return fuel_test_support::hardware::skip(
9055 fuel_test_support::hardware::Hardware::Cuda,
9056 fuel_test_support::hardware::Missing::device("CudaDevice::new(0) failed"),
9057 );
9058 };
9059
9060 let graph = Arc::new(RwLock::new(Graph::new()));
9061 let (x, y) = {
9062 let mut g = graph.write().unwrap();
9063 let x = g.push(Node {
9064 op: Op::Const,
9065 inputs: vec![],
9066 shape: Shape::from_dims(&[2, 4]),
9067 dtype: DType::F32,
9068 });
9069 let y = g.push(Node {
9071 op: Op::Copy {
9072 target: DeviceLocation::Cpu,
9073 },
9074 inputs: vec![x],
9075 shape: Shape::from_dims(&[2, 4]),
9076 dtype: DType::F32,
9077 });
9078 g.set_target_backend(y, BackendId::Cuda); (x, y)
9080 };
9081
9082 let x_data: Vec<f32> = (0..8).map(|i| i as f32).collect();
9083 let cb =
9084 CudaStorageBytes::from_cpu_bytes(&dev, bytemuck::cast_slice(&x_data)).expect("h2d");
9085 let x_arc = Arc::new(RwLock::new(Storage::new(
9086 BackendStorage::Cuda(cb),
9087 DType::F32,
9088 )));
9089 let mut inputs = StorageCache::new();
9090 inputs.insert(x, x_arc);
9091
9092 let result = PipelinedExecutor::capture_decode(graph, y, inputs, SymEnv::default());
9093 let msg = match result {
9094 Ok(_) => panic!("D2H Op::Copy must be rejected by capture_decode"),
9095 Err(e) => e.to_string(),
9096 };
9097 assert!(
9098 msg.contains("cross-device"),
9099 "error should describe the cross-device rejection: {msg}"
9100 );
9101 }
9102
9103 #[test]
9111 #[ignore = "requires a live CUDA device"]
9112 #[cfg(feature = "cuda")]
9113 fn captured_decode_session_replays_many_tokens_cuda() {
9114 use fuel_cuda_backend::{CudaDevice, CudaStorageBytes};
9115 use fuel_graph::registry::{FusedOpParams, FusedOps};
9116 use fuel_ir::Shape;
9117 use fuel_memory::BackendStorage;
9118
9119 let Ok(dev) = CudaDevice::new(0) else {
9120 return fuel_test_support::hardware::skip(
9121 fuel_test_support::hardware::Hardware::Cuda,
9122 fuel_test_support::hardware::Missing::device("CudaDevice::new(0) failed"),
9123 );
9124 };
9125 const H: usize = 6;
9126 let eps = 1e-5f64;
9127
9128 let build = || -> (Arc<RwLock<Graph>>, NodeId, NodeId) {
9129 let graph = Arc::new(RwLock::new(Graph::new()));
9130 let (x, y) = {
9131 let mut g = graph.write().unwrap();
9132 let mk = |g: &mut Graph, op: Op, ins: Vec<NodeId>, dims: &[usize]| {
9133 g.push(Node {
9134 op,
9135 inputs: ins,
9136 shape: Shape::from_dims(dims),
9137 dtype: DType::F32,
9138 })
9139 };
9140 let rms = || {
9141 Op::Fused(
9142 FusedOps::RMS_NORM_LAST_DIM,
9143 FusedOpParams::RmsNormLastDim { eps },
9144 )
9145 };
9146 let sm = || Op::Fused(FusedOps::SOFTMAX_LAST_DIM, FusedOpParams::SoftmaxLastDim);
9147 let x = mk(&mut g, Op::Const, vec![], &[1, H]);
9148 let n1 = mk(&mut g, rms(), vec![x], &[1, H]);
9149 let s = mk(&mut g, Op::Silu, vec![n1], &[1, H]);
9150 let p = mk(&mut g, sm(), vec![s], &[1, H]);
9151 let r = mk(&mut g, Op::Add, vec![p, x], &[1, H]);
9152 let n2 = mk(&mut g, rms(), vec![r], &[1, H]);
9153 let y = mk(&mut g, Op::Add, vec![n2, x], &[1, H]);
9154 for id in [n1, s, p, r, n2, y] {
9155 g.set_target_backend(id, BackendId::Cuda);
9156 }
9157 (x, y)
9158 };
9159 (graph, x, y)
9160 };
9161 let up = |v: &[f32]| -> Arc<RwLock<Storage>> {
9162 let cb = CudaStorageBytes::from_cpu_bytes(&dev, bytemuck::cast_slice(v)).expect("h2d");
9163 Arc::new(RwLock::new(Storage::new(
9164 BackendStorage::Cuda(cb),
9165 DType::F32,
9166 )))
9167 };
9168 let read = |arc: &Arc<RwLock<Storage>>| -> Vec<f32> {
9169 let g = arc.read().unwrap();
9170 let BackendStorage::Cuda(c) = &g.inner else {
9171 panic!("not cuda")
9172 };
9173 bytemuck::cast_slice(&c.to_cpu_bytes().expect("d2h")).to_vec()
9174 };
9175 let realize_ref = |xv: &[f32]| -> Vec<f32> {
9176 let (g, xr, yr) = build();
9177 let mut ins = StorageCache::new();
9178 ins.insert(xr, up(xv));
9179 let (arc, _) = PipelinedExecutor::realize(g, yr, ins).expect("ref realize");
9180 read(&arc)
9181 };
9182
9183 let tokens: Vec<Vec<f32>> = (0..5)
9185 .map(|t| {
9186 (0..H)
9187 .map(|c| 0.1 * (t as f32 + 1.0) * (c as f32 + 1.0))
9188 .collect()
9189 })
9190 .collect();
9191
9192 let x_arc = up(&tokens[0]);
9194 let (graph, x, y) = build();
9195 let mut inputs = StorageCache::new();
9196 inputs.insert(x, x_arc.clone());
9197 let session =
9198 CapturedDecodeSession::capture(Arc::clone(&graph), y, inputs, &[x], SymEnv::default())
9199 .expect("capture session");
9200
9201 assert_eq!(
9203 read(session.output()),
9204 realize_ref(&tokens[0]),
9205 "warm token 0"
9206 );
9207
9208 for (t, tok) in tokens.iter().enumerate() {
9211 let out = session
9212 .replay_token(&[(x, bytemuck::cast_slice(tok))])
9213 .expect("replay_token");
9214 assert_eq!(
9215 read(&out),
9216 realize_ref(tok),
9217 "replayed token {t} matches uncaptured realize"
9218 );
9219 }
9220 assert_ne!(
9222 realize_ref(&tokens[0]),
9223 realize_ref(&tokens[1]),
9224 "tokens must differ"
9225 );
9226 }
9227
9228 #[test]
9234 fn realize_with_optimized_route_empty_equals_arm0() {
9235 use crate::optimize::OptimizedGraph;
9236
9237 let build = || {
9238 let graph = Arc::new(RwLock::new(Graph::new()));
9239 let (lhs_id, rhs_id, add_id) = {
9240 let mut g = graph.write().unwrap();
9241 let lhs_id = g.push(Node {
9242 op: Op::Const,
9243 inputs: vec![],
9244 shape: Shape::from_dims(&[3]),
9245 dtype: DType::F32,
9246 });
9247 let rhs_id = g.push(Node {
9248 op: Op::Const,
9249 inputs: vec![],
9250 shape: Shape::from_dims(&[3]),
9251 dtype: DType::F32,
9252 });
9253 let add_id = g.push(Node {
9254 op: Op::Add,
9255 inputs: vec![lhs_id, rhs_id],
9256 shape: Shape::from_dims(&[3]),
9257 dtype: DType::F32,
9258 });
9259 g.set_target_backend(add_id, BackendId::Cpu);
9260 (lhs_id, rhs_id, add_id)
9261 };
9262 let mut inputs = StorageCache::new();
9263 inputs.insert(
9264 lhs_id,
9265 Arc::new(RwLock::new(fuel_memory::from_slice_cpu(&[
9266 1.0_f32, 2.0, 3.0,
9267 ]))),
9268 );
9269 inputs.insert(
9270 rhs_id,
9271 Arc::new(RwLock::new(fuel_memory::from_slice_cpu(&[
9272 10.0_f32, 20.0, 30.0,
9273 ]))),
9274 );
9275 (graph, add_id, inputs)
9276 };
9277
9278 let read_bytes = |arc: &Arc<RwLock<Storage>>| -> Vec<f32> {
9279 let guard = arc.read().unwrap();
9280 match &guard.inner {
9281 fuel_memory::BackendStorage::Cpu(c) => {
9282 c.as_slice::<f32>().expect("f32 cast").to_vec()
9283 }
9284 _ => panic!("expected CPU output"),
9285 }
9286 };
9287
9288 drop(global_bindings());
9300 let live_gen = crate::dispatch::topology_generation();
9301
9302 let (g_a, add_a, in_a) = build();
9304 let opt_a = OptimizedGraph {
9305 roots: vec![add_a],
9306 generation: live_gen,
9307 placements: None,
9308 };
9309 let (arm0_arc, _) = PipelinedExecutor::realize_with_optimized(g_a, add_a, in_a, &opt_a)
9310 .expect("arm-0 realize");
9311 let arm0 = read_bytes(&arm0_arc);
9312
9313 let (g_r, add_r, in_r) = build();
9315 let opt_r = OptimizedGraph {
9316 roots: vec![add_r],
9317 generation: live_gen,
9318 placements: None,
9319 };
9320 let empty_route = PickedRoute::new();
9321 let (route_arc, _) =
9322 PipelinedExecutor::realize_with_optimized_route(g_r, add_r, in_r, &opt_r, &empty_route)
9323 .expect("empty-route realize");
9324 let routed = read_bytes(&route_arc);
9325
9326 assert_eq!(arm0, vec![11.0, 22.0, 33.0]);
9327 assert_eq!(
9328 routed, arm0,
9329 "an empty route realizes byte-identically to arm-0 (Phase B contract)",
9330 );
9331 }
9332
9333 #[test]
9340 fn realize_with_optimized_picking_cached_env_matches_arm0_cold_and_warm() {
9341 use crate::optimize::OptimizedGraph;
9342 use std::sync::OnceLock;
9343
9344 let build = || {
9345 let graph = Arc::new(RwLock::new(Graph::new()));
9346 let (lhs_id, rhs_id, add_id) = {
9347 let mut g = graph.write().unwrap();
9348 let lhs_id = g.push(Node {
9349 op: Op::Const,
9350 inputs: vec![],
9351 shape: Shape::from_dims(&[3]),
9352 dtype: DType::F32,
9353 });
9354 let rhs_id = g.push(Node {
9355 op: Op::Const,
9356 inputs: vec![],
9357 shape: Shape::from_dims(&[3]),
9358 dtype: DType::F32,
9359 });
9360 let add_id = g.push(Node {
9361 op: Op::Add,
9362 inputs: vec![lhs_id, rhs_id],
9363 shape: Shape::from_dims(&[3]),
9364 dtype: DType::F32,
9365 });
9366 g.set_target_backend(add_id, BackendId::Cpu);
9367 (lhs_id, rhs_id, add_id)
9368 };
9369 let mut inputs = StorageCache::new();
9370 inputs.insert(
9371 lhs_id,
9372 Arc::new(RwLock::new(fuel_memory::from_slice_cpu(&[
9373 1.0_f32, 2.0, 3.0,
9374 ]))),
9375 );
9376 inputs.insert(
9377 rhs_id,
9378 Arc::new(RwLock::new(fuel_memory::from_slice_cpu(&[
9379 10.0_f32, 20.0, 30.0,
9380 ]))),
9381 );
9382 (graph, add_id, inputs)
9383 };
9384 let read_bytes = |arc: &Arc<RwLock<Storage>>| -> Vec<f32> {
9385 let guard = arc.read().unwrap();
9386 match &guard.inner {
9387 fuel_memory::BackendStorage::Cpu(c) => {
9388 c.as_slice::<f32>().expect("f32 cast").to_vec()
9389 }
9390 _ => panic!("expected CPU output"),
9391 }
9392 };
9393
9394 drop(global_bindings());
9395 let live_gen = crate::dispatch::topology_generation();
9396
9397 let (g, add, inputs) = build();
9398 let optimized = OptimizedGraph {
9399 roots: vec![add],
9400 generation: live_gen,
9401 placements: None,
9402 };
9403 let order_cache: OnceLock<Vec<NodeId>> = OnceLock::new();
9404 assert!(order_cache.get().is_none(), "sanity: cache starts empty");
9405
9406 let (cold_arc, _) = PipelinedExecutor::realize_with_optimized_picking_cached_env(
9408 g.clone(),
9409 add,
9410 inputs,
9411 &optimized,
9412 &order_cache,
9413 None,
9414 None,
9415 SymEnv::default(),
9416 )
9417 .expect("cold cached realize");
9418 assert_eq!(read_bytes(&cold_arc), vec![11.0, 22.0, 33.0]);
9419 assert!(
9420 order_cache.get().is_some(),
9421 "the cold call must populate order_cache, not leave it empty"
9422 );
9423 let cached_order_after_cold = order_cache.get().unwrap().clone();
9424
9425 let mut warm_inputs = StorageCache::new();
9429 warm_inputs.insert(
9430 g.read().unwrap().node(add).inputs[0],
9431 Arc::new(RwLock::new(fuel_memory::from_slice_cpu(&[
9432 100.0_f32, 200.0, 300.0,
9433 ]))),
9434 );
9435 warm_inputs.insert(
9436 g.read().unwrap().node(add).inputs[1],
9437 Arc::new(RwLock::new(fuel_memory::from_slice_cpu(&[
9438 1.0_f32, 2.0, 3.0,
9439 ]))),
9440 );
9441 let (warm_arc, _) = PipelinedExecutor::realize_with_optimized_picking_cached_env(
9442 g,
9443 add,
9444 warm_inputs,
9445 &optimized,
9446 &order_cache,
9447 None,
9448 None,
9449 SymEnv::default(),
9450 )
9451 .expect("warm cached realize");
9452 assert_eq!(
9453 read_bytes(&warm_arc),
9454 vec![101.0, 202.0, 303.0],
9455 "warm call must realize its OWN rebound inputs, not replay stale output",
9456 );
9457 assert_eq!(
9458 order_cache.get().unwrap(),
9459 &cached_order_after_cold,
9460 "the warm call must reuse the SAME cached order, not recompute a different one",
9461 );
9462 }
9463
9464 #[test]
9480 fn cached_generation_for_matches_optimized_generation_for() {
9481 let optimized = OptimizedGraph {
9482 roots: vec![NodeId(0)],
9483 generation: 42,
9484 placements: None,
9485 };
9486 let order_cache: std::sync::OnceLock<Vec<NodeId>> = std::sync::OnceLock::new();
9487
9488 let optimized_gen = generation_for(&OrderSource::Optimized {
9489 optimized: &optimized,
9490 route: None,
9491 });
9492 let cached_gen = generation_for(&OrderSource::Cached {
9493 optimized: &optimized,
9494 order_cache: &order_cache,
9495 });
9496 assert_eq!(
9497 optimized_gen,
9498 Some(42),
9499 "sanity: Optimized reports the OptimizedGraph's own generation",
9500 );
9501 assert_eq!(
9502 cached_gen, optimized_gen,
9503 "Cached must report the SAME generation as Optimized for the same \
9504 OptimizedGraph -- the TopologyChanged check cannot tell the two apart, \
9505 by construction, so caching cannot silently weaken it",
9506 );
9507 }
9508
9509 #[test]
9526 fn c3_reorder_is_a_valid_topo_permutation() {
9527 let n = |g: &mut Graph, op: Op, inputs: Vec<NodeId>| {
9538 g.push(Node {
9539 op,
9540 inputs,
9541 shape: Shape::from_dims(&[3]),
9542 dtype: DType::F32,
9543 })
9544 };
9545 let mut g = Graph::new();
9546 let ca = n(&mut g, Op::Const, vec![]);
9547 let cb = n(&mut g, Op::Const, vec![]);
9548 let mut cprev = ca;
9550 for _ in 0..4 {
9551 cprev = n(&mut g, Op::Relu, vec![cprev]);
9552 g.set_target_backend(cprev, BackendId::Cuda);
9553 }
9554 let c2 = cprev; let mut vprev = cb;
9557 for _ in 0..2 {
9558 vprev = n(&mut g, Op::Relu, vec![vprev]);
9559 g.set_target_backend(vprev, BackendId::Vulkan);
9560 }
9561 let v2 = vprev; let copy = g.push(Node {
9565 op: Op::Copy {
9566 target: DeviceLocation::Cuda { gpu_id: 0 },
9567 },
9568 inputs: vec![v2],
9569 shape: Shape::from_dims(&[3]),
9570 dtype: DType::F32,
9571 });
9572 g.set_target_backend(copy, BackendId::Cuda);
9573 let out = n(&mut g, Op::Add, vec![c2, copy]);
9574 g.set_target_backend(out, BackendId::Cuda);
9575
9576 let unreordered = execution_plan(&g, &[out]);
9577 let reordered = fuel_graph::lower_runs_arm0(&g, &[out]);
9578
9579 assert_ne!(
9582 reordered, unreordered,
9583 "the C3 reorder must change the order on a two-device graph; \
9584 reordered={reordered:?} unreordered={unreordered:?}",
9585 );
9586
9587 let mut a = unreordered.clone();
9589 let mut b = reordered.clone();
9590 a.sort();
9591 b.sort();
9592 assert_eq!(a, b, "the reorder is a permutation of the same node set");
9593
9594 let pos: std::collections::HashMap<NodeId, usize> = reordered
9597 .iter()
9598 .enumerate()
9599 .map(|(i, &node)| (node, i))
9600 .collect();
9601 for (i, &node) in reordered.iter().enumerate() {
9602 for &inp in &g.node(node).inputs {
9603 if let Some(&j) = pos.get(&inp) {
9604 assert!(
9605 j < i,
9606 "input {inp:?} of {node:?} must precede it in the reordered \
9607 order; reordered={reordered:?}",
9608 );
9609 }
9610 }
9611 }
9612
9613 let p = |node: NodeId| pos[&node];
9620 assert!(p(c2) < p(copy), "CUDA chunk before the cross-device copy");
9621 assert!(p(v2) < p(copy), "Vulkan chunk before the cross-device copy");
9622 assert!(
9623 p(c2) < p(v2),
9624 "the heavier CUDA chunk is emitted before the lighter Vulkan chunk \
9625 (critical-path heavy-first); reordered={reordered:?}",
9626 );
9627 }
9628
9629 #[test]
9650 fn c3_reorder_is_operand_order_invariant() {
9651 let build = |cuda_first: bool| -> (Graph, NodeId) {
9655 let n = |g: &mut Graph, op: Op, inputs: Vec<NodeId>| {
9656 g.push(Node {
9657 op,
9658 inputs,
9659 shape: Shape::from_dims(&[3]),
9660 dtype: DType::F32,
9661 })
9662 };
9663 let mut g = Graph::new();
9664 let ca = n(&mut g, Op::Const, vec![]);
9665 let cb = n(&mut g, Op::Const, vec![]);
9666 let mut cprev = ca;
9667 for _ in 0..4 {
9668 cprev = n(&mut g, Op::Relu, vec![cprev]);
9669 g.set_target_backend(cprev, BackendId::Cuda);
9670 }
9671 let c2 = cprev;
9672 let mut vprev = cb;
9673 for _ in 0..2 {
9674 vprev = n(&mut g, Op::Relu, vec![vprev]);
9675 g.set_target_backend(vprev, BackendId::Vulkan);
9676 }
9677 let v2 = vprev;
9678 let copy = g.push(Node {
9679 op: Op::Copy {
9680 target: DeviceLocation::Cuda { gpu_id: 0 },
9681 },
9682 inputs: vec![v2],
9683 shape: Shape::from_dims(&[3]),
9684 dtype: DType::F32,
9685 });
9686 g.set_target_backend(copy, BackendId::Cuda);
9687 let inputs = if cuda_first {
9689 vec![c2, copy]
9690 } else {
9691 vec![copy, c2]
9692 };
9693 let out = n(&mut g, Op::Add, inputs);
9694 g.set_target_backend(out, BackendId::Cuda);
9695 (g, out)
9696 };
9697
9698 let (g_cf, out_cf) = build(true);
9699 let (g_vf, out_vf) = build(false);
9700
9701 let order_cf = fuel_graph::lower_runs_arm0(&g_cf, &[out_cf]);
9704 let order_vf = fuel_graph::lower_runs_arm0(&g_vf, &[out_vf]);
9705
9706 let tag = |g: &Graph, ids: &[NodeId]| -> Vec<(String, Option<BackendId>)> {
9712 ids.iter()
9713 .map(|&id| (format!("{:?}", g.node(id).op), g.target_backend(id)))
9714 .collect()
9715 };
9716 let seq_cf = tag(&g_cf, &order_cf);
9717 let seq_vf = tag(&g_vf, &order_vf);
9718 assert_eq!(
9719 seq_cf, seq_vf,
9720 "the C3-reordered dispatch order MUST be identical for both \
9721 reconverge operand orders (operand-order-invariance — the \
9722 executor then sees the same WorkItem stream regardless of \
9723 operand order, so overlap is operand-insensitive).\n\
9724 cuda-first = {seq_cf:?}\nvulkan-first = {seq_vf:?}",
9725 );
9726 }
9727
9728 #[cfg(test)]
9731 #[derive(Debug)]
9732 struct PickLast;
9733 #[cfg(test)]
9734 impl RuntimeSelector for PickLast {
9735 fn select<'a>(
9736 &self,
9737 set: &'a crate::ranker::AlternativeSet,
9738 ) -> Option<&'a crate::ranker::Candidate> {
9739 set.alternatives().last()
9740 }
9741 }
9742
9743 #[cfg(test)]
9748 fn picking_diamond() -> (Arc<RwLock<Graph>>, NodeId, NodeId) {
9749 let node = |g: &mut Graph, op: Op, inputs: Vec<NodeId>| {
9750 g.push(Node {
9751 op,
9752 inputs,
9753 shape: Shape::from_dims(&[2]),
9754 dtype: DType::F32,
9755 })
9756 };
9757 let mut g = Graph::new();
9758 let pre = node(&mut g, Op::Const, vec![]);
9759 let diverge = node(&mut g, Op::Relu, vec![pre]);
9760 let arm0 = node(&mut g, Op::Silu, vec![diverge]);
9761 let arm1 = node(&mut g, Op::GeluTanh, vec![diverge]);
9762 g.set_target_backend(arm0, BackendId::Cuda);
9763 g.set_target_backend(arm1, BackendId::Cpu);
9764 let reconverge = node(&mut g, Op::Relu, vec![arm0]);
9765 let mut b = g.open_branch(diverge);
9766 b.add_arm(arm0);
9767 b.add_arm(arm1);
9768 let branch = b
9769 .finalize_branches(&mut g, reconverge)
9770 .expect("well-formed 2-arm branch")
9771 .expect("2 arms survive");
9772 let post = node(&mut g, Op::Tanh, vec![reconverge]);
9773 (Arc::new(RwLock::new(g)), branch, post)
9774 }
9775
9776 #[test]
9784 fn streaming_pick_for_gates() {
9785 let (graph, _branch, post) = picking_diamond();
9786 let _ = post;
9787
9788 let sel: Arc<dyn RuntimeSelector> = Arc::new(PickLast);
9790 assert!(
9791 PipelinedExecutor::streaming_pick_for(&graph, Some(sel.clone()), None)
9792 .expect("ok")
9793 .is_some(),
9794 "selector + branch ⇒ a streaming pick",
9795 );
9796
9797 assert!(
9799 PipelinedExecutor::streaming_pick_for(&graph, None, None)
9800 .expect("ok")
9801 .is_none(),
9802 "no selector ⇒ no streaming pick (arm-0 lowering)",
9803 );
9804
9805 let node = |g: &mut Graph, op: Op, inputs: Vec<NodeId>| {
9807 g.push(Node {
9808 op,
9809 inputs,
9810 shape: Shape::from_dims(&[2]),
9811 dtype: DType::F32,
9812 })
9813 };
9814 let bl_graph = {
9815 let mut g = Graph::new();
9816 let c1 = node(&mut g, Op::Const, vec![]);
9817 let c2 = node(&mut g, Op::Const, vec![]);
9818 let add = node(&mut g, Op::Add, vec![c1, c2]);
9819 g.set_target_backend(add, BackendId::Cpu);
9820 Arc::new(RwLock::new(g))
9821 };
9822 assert!(
9823 PipelinedExecutor::streaming_pick_for(&bl_graph, Some(sel), None)
9824 .expect("ok")
9825 .is_none(),
9826 "branchless graph ⇒ no branches ⇒ no streaming pick (fast-path)",
9827 );
9828 }
9829
9830 #[test]
9840 fn streamed_order_equals_one_shot() {
9841 let (graph, _branch, post) = picking_diamond();
9842 let g = graph.read().unwrap();
9843 let bindings = global_bindings();
9844
9845 for sel in [
9846 Arc::new(crate::ranker::WinnerSelector) as Arc<dyn RuntimeSelector>,
9847 Arc::new(PickLast) as Arc<dyn RuntimeSelector>,
9848 ] {
9849 let one_shot_route = pick_route(&g, &[post], &bindings, sel.as_ref(), None);
9851 let one_shot = fuel_graph::lower_picked_route(&g, &[post], &one_shot_route);
9852
9853 let streamed = fuel_graph::lower_picked_route_streaming(&g, &[post], |branch| {
9856 resolve_branch(&g, branch, &bindings, sel.as_ref())
9857 });
9858
9859 assert_eq!(
9860 streamed, one_shot,
9861 "streamed order must equal one-shot for selector {sel:?}",
9862 );
9863 }
9864 }
9865
9866 #[test]
9869 fn pipelined_errors_on_unset_target_backend() {
9870 let graph = Arc::new(RwLock::new(Graph::new()));
9871 let (lhs_id, rhs_id, add_id) = {
9872 let mut g = graph.write().unwrap();
9873 let lhs = g.push(Node {
9874 op: Op::Const,
9875 inputs: vec![],
9876 shape: Shape::from_dims(&[2]),
9877 dtype: DType::F32,
9878 });
9879 let rhs = g.push(Node {
9880 op: Op::Const,
9881 inputs: vec![],
9882 shape: Shape::from_dims(&[2]),
9883 dtype: DType::F32,
9884 });
9885 let add = g.push(Node {
9887 op: Op::Add,
9888 inputs: vec![lhs, rhs],
9889 shape: Shape::from_dims(&[2]),
9890 dtype: DType::F32,
9891 });
9892 (lhs, rhs, add)
9893 };
9894
9895 let mut inputs = StorageCache::new();
9896 inputs.insert(
9897 lhs_id,
9898 Arc::new(RwLock::new(fuel_memory::from_slice_cpu(&[1.0_f32, 2.0]))),
9899 );
9900 inputs.insert(
9901 rhs_id,
9902 Arc::new(RwLock::new(fuel_memory::from_slice_cpu(&[3.0_f32, 4.0]))),
9903 );
9904
9905 let result = PipelinedExecutor::realize(graph, add_id, inputs);
9906 assert!(result.is_err(), "missing target_backend must error");
9907 let msg = result.unwrap_err().to_string();
9908 assert!(
9909 msg.contains("target_backend"),
9910 "error names the unmet precondition: {msg}"
9911 );
9912 }
9913
9914 #[test]
9917 fn pipelined_realize_const_only() {
9918 let storage = fuel_memory::from_slice_cpu(&[5.0_f32, 6.0, 7.0]);
9919
9920 let graph = Arc::new(RwLock::new(Graph::new()));
9921 let const_id = {
9922 let mut g = graph.write().unwrap();
9923 g.push(Node {
9924 op: Op::Const,
9925 inputs: vec![],
9926 shape: Shape::from_dims(&[3]),
9927 dtype: DType::F32,
9928 })
9929 };
9930
9931 let mut inputs = StorageCache::new();
9932 inputs.insert(const_id, Arc::new(RwLock::new(storage)));
9933
9934 let (result_arc, _) =
9935 PipelinedExecutor::realize(graph, const_id, inputs).expect("realize const");
9936 let guard = result_arc.read().unwrap();
9937 if let fuel_memory::BackendStorage::Cpu(c) = &guard.inner {
9938 let typed: &[f32] = c.as_slice().unwrap();
9939 assert_eq!(typed, &[5.0, 6.0, 7.0]);
9940 }
9941 }
9942
9943 #[test]
9945 fn pipelined_errors_on_missing_const_input() {
9946 let graph = Arc::new(RwLock::new(Graph::new()));
9947 let const_id = {
9948 let mut g = graph.write().unwrap();
9949 g.push(Node {
9950 op: Op::Const,
9951 inputs: vec![],
9952 shape: Shape::from_dims(&[2]),
9953 dtype: DType::F32,
9954 })
9955 };
9956 let inputs = StorageCache::new();
9958 let result = PipelinedExecutor::realize(graph, const_id, inputs);
9959 assert!(result.is_err());
9960 }
9961
9962 #[test]
9965 fn pipelined_realize_const_relu() {
9966 let storage = fuel_memory::from_slice_cpu(&[-1.0_f32, 0.0, 0.5, -3.5, 7.25]);
9967 let graph = Arc::new(RwLock::new(Graph::new()));
9968 let (in_id, relu_id) = {
9969 let mut g = graph.write().unwrap();
9970 let in_id = g.push(Node {
9971 op: Op::Const,
9972 inputs: vec![],
9973 shape: Shape::from_dims(&[5]),
9974 dtype: DType::F32,
9975 });
9976 let relu_id = g.push(Node {
9977 op: Op::Relu,
9978 inputs: vec![in_id],
9979 shape: Shape::from_dims(&[5]),
9980 dtype: DType::F32,
9981 });
9982 g.set_target_backend(relu_id, BackendId::Cpu);
9983 (in_id, relu_id)
9984 };
9985 let mut inputs = StorageCache::new();
9986 inputs.insert(in_id, Arc::new(RwLock::new(storage)));
9987
9988 let (result_arc, _) = PipelinedExecutor::realize(graph, relu_id, inputs).expect("realize");
9989 let guard = result_arc.read().unwrap();
9990 if let fuel_memory::BackendStorage::Cpu(c) = &guard.inner {
9991 let typed: &[f32] = c.as_slice().expect("f32 cast");
9992 assert_eq!(typed, &[0.0, 0.0, 0.5, 0.0, 7.25]);
9993 }
9994 }
9995
9996 #[test]
10000 fn pipelined_realize_chained_binary_ops() {
10001 let a_storage = fuel_memory::from_slice_cpu(&[10.0_f32, 20.0]);
10002 let b_storage = fuel_memory::from_slice_cpu(&[3.0_f32, 5.0]);
10003 let c_storage = fuel_memory::from_slice_cpu(&[2.0_f32, 4.0]);
10004
10005 let graph = Arc::new(RwLock::new(Graph::new()));
10006 let (a_id, b_id, c_id, sub_id, mul_id, div_id) = {
10007 let mut g = graph.write().unwrap();
10008 let a = g.push(Node {
10009 op: Op::Const,
10010 inputs: vec![],
10011 shape: Shape::from_dims(&[2]),
10012 dtype: DType::F32,
10013 });
10014 let b = g.push(Node {
10015 op: Op::Const,
10016 inputs: vec![],
10017 shape: Shape::from_dims(&[2]),
10018 dtype: DType::F32,
10019 });
10020 let c = g.push(Node {
10021 op: Op::Const,
10022 inputs: vec![],
10023 shape: Shape::from_dims(&[2]),
10024 dtype: DType::F32,
10025 });
10026 let sub = g.push(Node {
10028 op: Op::Sub,
10029 inputs: vec![a, b],
10030 shape: Shape::from_dims(&[2]),
10031 dtype: DType::F32,
10032 });
10033 let mul = g.push(Node {
10035 op: Op::Mul,
10036 inputs: vec![sub, c],
10037 shape: Shape::from_dims(&[2]),
10038 dtype: DType::F32,
10039 });
10040 let div = g.push(Node {
10042 op: Op::Div,
10043 inputs: vec![mul, b],
10044 shape: Shape::from_dims(&[2]),
10045 dtype: DType::F32,
10046 });
10047 g.set_target_backend(sub, BackendId::Cpu);
10048 g.set_target_backend(mul, BackendId::Cpu);
10049 g.set_target_backend(div, BackendId::Cpu);
10050 (a, b, c, sub, mul, div)
10051 };
10052
10053 let _ = (sub_id, mul_id);
10054 let mut inputs = StorageCache::new();
10055 inputs.insert(a_id, Arc::new(RwLock::new(a_storage)));
10056 inputs.insert(b_id, Arc::new(RwLock::new(b_storage)));
10057 inputs.insert(c_id, Arc::new(RwLock::new(c_storage)));
10058
10059 let (result_arc, _) = PipelinedExecutor::realize(graph, div_id, inputs).expect("realize");
10060 let guard = result_arc.read().unwrap();
10061 if let fuel_memory::BackendStorage::Cpu(c) = &guard.inner {
10062 let typed: &[f32] = c.as_slice().unwrap();
10063 assert!((typed[0] - (14.0_f32 / 3.0)).abs() < 1e-6);
10064 assert!((typed[1] - 12.0).abs() < 1e-6);
10065 }
10066 }
10067
10068 #[test]
10072 fn pipelined_realize_transpose_is_metadata_only() {
10073 let storage = fuel_memory::from_slice_cpu(&[1.0_f32, 2.0, 3.0, 4.0, 5.0, 6.0]);
10075 let graph = Arc::new(RwLock::new(Graph::new()));
10076 let (in_id, t_id) = {
10077 let mut g = graph.write().unwrap();
10078 let in_id = g.push(Node {
10079 op: Op::Const,
10080 inputs: vec![],
10081 shape: Shape::from_dims(&[2, 3]),
10082 dtype: DType::F32,
10083 });
10084 let t_id = g.push(Node {
10085 op: Op::Transpose,
10086 inputs: vec![in_id],
10087 shape: Shape::from_dims(&[3, 2]),
10088 dtype: DType::F32,
10089 });
10090 (in_id, t_id)
10093 };
10094 let mut inputs = StorageCache::new();
10095 let in_arc = Arc::new(RwLock::new(storage));
10096 inputs.insert(in_id, Arc::clone(&in_arc));
10097
10098 let (result_arc, result_layout) =
10099 PipelinedExecutor::realize(graph, t_id, inputs).expect("realize");
10100
10101 assert!(
10104 Arc::ptr_eq(&result_arc, &in_arc),
10105 "transpose must share input bytes"
10106 );
10107
10108 assert_eq!(result_layout.shape().dims(), &[3, 2]);
10110 assert_eq!(result_layout.stride(), &[1, 3]);
10111 assert!(!result_layout.is_contiguous());
10112 }
10113
10114 #[test]
10119 fn pipelined_realize_flip_is_metadata_only() {
10120 let data: Vec<f32> = (1..=12).map(|x| x as f32).collect();
10122 let storage = fuel_memory::from_slice_cpu(&data);
10123 let graph = Arc::new(RwLock::new(Graph::new()));
10124 let (in_id, f_id) = {
10125 let mut g = graph.write().unwrap();
10126 let in_id = g.push(Node {
10127 op: Op::Const,
10128 inputs: vec![],
10129 shape: Shape::from_dims(&[3, 4]),
10130 dtype: DType::F32,
10131 });
10132 let f_id = g.push(Node {
10133 op: Op::Flip { dim: 0 },
10134 inputs: vec![in_id],
10135 shape: Shape::from_dims(&[3, 4]),
10136 dtype: DType::F32,
10137 });
10138 (in_id, f_id)
10139 };
10140 let mut inputs = StorageCache::new();
10141 let in_arc = Arc::new(RwLock::new(storage));
10142 inputs.insert(in_id, Arc::clone(&in_arc));
10143
10144 let (result_arc, result_layout) =
10145 PipelinedExecutor::realize(graph, f_id, inputs).expect("realize");
10146
10147 assert!(
10148 Arc::ptr_eq(&result_arc, &in_arc),
10149 "flip must share input bytes"
10150 );
10151 assert_eq!(result_layout.shape().dims(), &[3, 4]);
10152 assert_eq!(result_layout.stride(), &[-4_isize, 1]);
10153 assert_eq!(result_layout.start_offset(), 8);
10154 assert!(!result_layout.is_contiguous());
10155 }
10156
10157 #[test]
10160 fn pipelined_realize_permute_is_metadata_only() {
10161 let data: Vec<f32> = (1..=24).map(|x| x as f32).collect();
10163 let storage = fuel_memory::from_slice_cpu(&data);
10164 let graph = Arc::new(RwLock::new(Graph::new()));
10165 let (in_id, p_id) = {
10166 let mut g = graph.write().unwrap();
10167 let in_id = g.push(Node {
10168 op: Op::Const,
10169 inputs: vec![],
10170 shape: Shape::from_dims(&[2, 3, 4]),
10171 dtype: DType::F32,
10172 });
10173 let p_id = g.push(Node {
10174 op: Op::Permute(vec![2, 0, 1]),
10175 inputs: vec![in_id],
10176 shape: Shape::from_dims(&[4, 2, 3]),
10177 dtype: DType::F32,
10178 });
10179 (in_id, p_id)
10180 };
10181 let mut inputs = StorageCache::new();
10182 let in_arc = Arc::new(RwLock::new(storage));
10183 inputs.insert(in_id, Arc::clone(&in_arc));
10184
10185 let (result_arc, result_layout) =
10186 PipelinedExecutor::realize(graph, p_id, inputs).expect("realize");
10187
10188 assert!(Arc::ptr_eq(&result_arc, &in_arc));
10189 assert_eq!(result_layout.shape().dims(), &[4, 2, 3]);
10190 assert_eq!(result_layout.stride(), &[1, 12, 4]);
10193 }
10194
10195 #[test]
10199 fn pipelined_realize_cast_f32_to_f64() {
10200 let storage = fuel_memory::from_slice_cpu(&[1.5_f32, -2.25, 100.0]);
10201 let graph = Arc::new(RwLock::new(Graph::new()));
10202 let (in_id, c_id) = {
10203 let mut g = graph.write().unwrap();
10204 let in_id = g.push(Node {
10205 op: Op::Const,
10206 inputs: vec![],
10207 shape: Shape::from_dims(&[3]),
10208 dtype: DType::F32,
10209 });
10210 let c_id = g.push(Node {
10211 op: Op::Cast(DType::F64),
10212 inputs: vec![in_id],
10213 shape: Shape::from_dims(&[3]),
10214 dtype: DType::F64,
10215 });
10216 g.set_target_backend(c_id, BackendId::Cpu);
10217 (in_id, c_id)
10218 };
10219 let mut inputs = StorageCache::new();
10220 inputs.insert(in_id, Arc::new(RwLock::new(storage)));
10221
10222 let (result_arc, result_layout) =
10223 PipelinedExecutor::realize(graph, c_id, inputs).expect("realize");
10224 assert_eq!(result_layout.shape().dims(), &[3]);
10225
10226 let guard = result_arc.read().unwrap();
10227 assert_eq!(guard.dtype, DType::F64);
10228 let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
10229 panic!("expected Cpu storage");
10230 };
10231 assert_eq!(c.as_slice::<f64>().unwrap(), &[1.5_f64, -2.25, 100.0]);
10232 }
10233
10234 #[test]
10239 fn pipelined_realize_cast_round_trip_via_bf16() {
10240 let storage = fuel_memory::from_slice_cpu(&[1.0_f32, 2.0, -3.0, 0.5]);
10241 let graph = Arc::new(RwLock::new(Graph::new()));
10242 let (in_id, c1_id, c2_id) = {
10243 let mut g = graph.write().unwrap();
10244 let in_id = g.push(Node {
10245 op: Op::Const,
10246 inputs: vec![],
10247 shape: Shape::from_dims(&[4]),
10248 dtype: DType::F32,
10249 });
10250 let c1_id = g.push(Node {
10251 op: Op::Cast(DType::BF16),
10252 inputs: vec![in_id],
10253 shape: Shape::from_dims(&[4]),
10254 dtype: DType::BF16,
10255 });
10256 let c2_id = g.push(Node {
10257 op: Op::Cast(DType::F32),
10258 inputs: vec![c1_id],
10259 shape: Shape::from_dims(&[4]),
10260 dtype: DType::F32,
10261 });
10262 g.set_target_backend(c1_id, BackendId::Cpu);
10263 g.set_target_backend(c2_id, BackendId::Cpu);
10264 (in_id, c1_id, c2_id)
10265 };
10266 let _ = c1_id;
10267 let mut inputs = StorageCache::new();
10268 inputs.insert(in_id, Arc::new(RwLock::new(storage)));
10269
10270 let (result_arc, _) = PipelinedExecutor::realize(graph, c2_id, inputs).expect("realize");
10271 let guard = result_arc.read().unwrap();
10272 assert_eq!(guard.dtype, DType::F32);
10273 let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
10274 panic!("expected Cpu storage");
10275 };
10276 assert_eq!(c.as_slice::<f32>().unwrap(), &[1.0_f32, 2.0, -3.0, 0.5]);
10277 }
10278
10279 #[test]
10285 fn pipelined_realize_slice_is_metadata_only() {
10286 let storage = fuel_memory::from_slice_cpu(&[10.0_f32, 20.0, 30.0, 40.0, 50.0]);
10289 let graph = Arc::new(RwLock::new(Graph::new()));
10290 let (in_id, s_id) = {
10291 let mut g = graph.write().unwrap();
10292 let in_id = g.push(Node {
10293 op: Op::Const,
10294 inputs: vec![],
10295 shape: Shape::from_dims(&[5]),
10296 dtype: DType::F32,
10297 });
10298 let s_id = g.push(Node {
10299 op: Op::Slice {
10300 dim: 0,
10301 start: 1,
10302 len: 3,
10303 },
10304 inputs: vec![in_id],
10305 shape: Shape::from_dims(&[3]),
10306 dtype: DType::F32,
10307 });
10308 (in_id, s_id)
10309 };
10310 let mut inputs = StorageCache::new();
10311 let in_arc = Arc::new(RwLock::new(storage));
10312 inputs.insert(in_id, Arc::clone(&in_arc));
10313
10314 let (result_arc, result_layout) =
10315 PipelinedExecutor::realize(graph, s_id, inputs).expect("realize");
10316
10317 assert!(Arc::ptr_eq(&result_arc, &in_arc));
10319 assert_eq!(result_layout.shape().dims(), &[3]);
10320 assert_eq!(result_layout.start_offset(), 1);
10324 assert_eq!(result_layout.stride(), &[1]);
10325 }
10326
10327 #[test]
10332 fn pipelined_realize_slice_then_sum_all() {
10333 let storage = fuel_memory::from_slice_cpu(&[10.0_f32, 20.0, 30.0, 40.0, 50.0]);
10334 let graph = Arc::new(RwLock::new(Graph::new()));
10335 let (in_id, s_id, sum_id) = {
10336 let mut g = graph.write().unwrap();
10337 let in_id = g.push(Node {
10338 op: Op::Const,
10339 inputs: vec![],
10340 shape: Shape::from_dims(&[5]),
10341 dtype: DType::F32,
10342 });
10343 let s_id = g.push(Node {
10344 op: Op::Slice {
10345 dim: 0,
10346 start: 1,
10347 len: 3,
10348 },
10349 inputs: vec![in_id],
10350 shape: Shape::from_dims(&[3]),
10351 dtype: DType::F32,
10352 });
10353 let sum_id = g.push(Node {
10354 op: Op::SumAll,
10355 inputs: vec![s_id],
10356 shape: Shape::from_dims(&[]),
10357 dtype: DType::F32,
10358 });
10359 g.set_target_backend(sum_id, BackendId::Cpu);
10360 (in_id, s_id, sum_id)
10361 };
10362 let _ = s_id;
10363 let mut inputs = StorageCache::new();
10364 inputs.insert(in_id, Arc::new(RwLock::new(storage)));
10365
10366 let (result_arc, _) = PipelinedExecutor::realize(graph, sum_id, inputs).expect("realize");
10367 let guard = result_arc.read().unwrap();
10368 let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
10369 panic!("expected Cpu storage");
10370 };
10371 assert_eq!(c.as_slice::<f32>().unwrap(), &[90.0]);
10372 }
10373
10374 #[test]
10380 fn pipelined_realize_flip_then_relu_materializes_in_reverse() {
10381 let storage = fuel_memory::from_slice_cpu(&[1.0_f32, 2.0, 3.0, 4.0]);
10382 let graph = Arc::new(RwLock::new(Graph::new()));
10383 let (in_id, _f_id, r_id) = {
10384 let mut g = graph.write().unwrap();
10385 let in_id = g.push(Node {
10386 op: Op::Const,
10387 inputs: vec![],
10388 shape: Shape::from_dims(&[4]),
10389 dtype: DType::F32,
10390 });
10391 let f_id = g.push(Node {
10392 op: Op::Flip { dim: 0 },
10393 inputs: vec![in_id],
10394 shape: Shape::from_dims(&[4]),
10395 dtype: DType::F32,
10396 });
10397 let r_id = g.push(Node {
10398 op: Op::Relu,
10399 inputs: vec![f_id],
10400 shape: Shape::from_dims(&[4]),
10401 dtype: DType::F32,
10402 });
10403 g.set_target_backend(r_id, BackendId::Cpu);
10404 (in_id, f_id, r_id)
10405 };
10406 let mut inputs = StorageCache::new();
10407 inputs.insert(in_id, Arc::new(RwLock::new(storage)));
10408
10409 let (result_arc, result_layout) =
10410 PipelinedExecutor::realize(graph, r_id, inputs).expect("realize");
10411 let guard = result_arc.read().unwrap();
10412 let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
10413 panic!("expected Cpu storage");
10414 };
10415 assert_eq!(c.as_slice::<f32>().unwrap(), &[4.0, 3.0, 2.0, 1.0]);
10416 assert_eq!(result_layout.shape().dims(), &[4]);
10417 assert!(result_layout.is_contiguous(), "relu output is contiguous");
10418 }
10419
10420 #[test]
10424 fn pipelined_realize_broadcast_is_metadata_only() {
10425 let storage = fuel_memory::from_slice_cpu(&[10.0_f32, 20.0, 30.0]);
10427 let graph = Arc::new(RwLock::new(Graph::new()));
10428 let (in_id, b_id) = {
10429 let mut g = graph.write().unwrap();
10430 let in_id = g.push(Node {
10431 op: Op::Const,
10432 inputs: vec![],
10433 shape: Shape::from_dims(&[3]),
10434 dtype: DType::F32,
10435 });
10436 let b_id = g.push(Node {
10437 op: Op::BroadcastTo(Shape::from_dims(&[4, 3])),
10438 inputs: vec![in_id],
10439 shape: Shape::from_dims(&[4, 3]),
10440 dtype: DType::F32,
10441 });
10442 (in_id, b_id)
10443 };
10444 let mut inputs = StorageCache::new();
10445 let in_arc = Arc::new(RwLock::new(storage));
10446 inputs.insert(in_id, Arc::clone(&in_arc));
10447
10448 let (result_arc, result_layout) =
10449 PipelinedExecutor::realize(graph, b_id, inputs).expect("realize");
10450
10451 assert!(Arc::ptr_eq(&result_arc, &in_arc));
10452 assert_eq!(result_layout.shape().dims(), &[4, 3]);
10453 assert_eq!(result_layout.stride(), &[0, 1]);
10455 }
10456
10457 #[test]
10462 fn pipelined_realize_matmul_2x3_times_3x2() {
10463 let lhs_storage = fuel_memory::from_slice_cpu(&[1.0_f32, 2.0, 3.0, 4.0, 5.0, 6.0]);
10466 let rhs_storage = fuel_memory::from_slice_cpu(&[7.0_f32, 8.0, 9.0, 10.0, 11.0, 12.0]);
10467
10468 let graph = Arc::new(RwLock::new(Graph::new()));
10469 let (lhs_id, rhs_id, mm_id) = {
10470 let mut g = graph.write().unwrap();
10471 let lhs = g.push(Node {
10472 op: Op::Const,
10473 inputs: vec![],
10474 shape: Shape::from_dims(&[2, 3]),
10475 dtype: DType::F32,
10476 });
10477 let rhs = g.push(Node {
10478 op: Op::Const,
10479 inputs: vec![],
10480 shape: Shape::from_dims(&[3, 2]),
10481 dtype: DType::F32,
10482 });
10483 let mm = g.push(Node {
10484 op: Op::MatMul,
10485 inputs: vec![lhs, rhs],
10486 shape: Shape::from_dims(&[2, 2]),
10487 dtype: DType::F32,
10488 });
10489 g.set_target_backend(mm, BackendId::Cpu);
10490 (lhs, rhs, mm)
10491 };
10492 let mut inputs = StorageCache::new();
10493 inputs.insert(lhs_id, Arc::new(RwLock::new(lhs_storage)));
10494 inputs.insert(rhs_id, Arc::new(RwLock::new(rhs_storage)));
10495
10496 let (result_arc, result_layout) =
10497 PipelinedExecutor::realize(graph, mm_id, inputs).expect("realize");
10498 assert_eq!(result_layout.shape().dims(), &[2, 2]);
10499 assert!(result_layout.is_contiguous());
10500
10501 let guard = result_arc.read().unwrap();
10502 let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
10503 panic!("expected Cpu storage");
10504 };
10505 assert_eq!(c.as_slice::<f32>().unwrap(), &[58.0, 64.0, 139.0, 154.0]);
10506 }
10507
10508 #[test]
10512 fn pipelined_realize_matmul_batched_2x_2x2_times_2x2() {
10513 let lhs_storage = fuel_memory::from_slice_cpu(&[
10514 1.0_f32, 2.0, 3.0, 4.0, 1.0, 0.0, 0.0, 1.0, ]);
10517 let rhs_storage = fuel_memory::from_slice_cpu(&[
10518 5.0_f32, 6.0, 7.0, 8.0, 10.0, 20.0, 30.0, 40.0, ]);
10521
10522 let graph = Arc::new(RwLock::new(Graph::new()));
10523 let (lhs_id, rhs_id, mm_id) = {
10524 let mut g = graph.write().unwrap();
10525 let lhs = g.push(Node {
10526 op: Op::Const,
10527 inputs: vec![],
10528 shape: Shape::from_dims(&[2, 2, 2]),
10529 dtype: DType::F32,
10530 });
10531 let rhs = g.push(Node {
10532 op: Op::Const,
10533 inputs: vec![],
10534 shape: Shape::from_dims(&[2, 2, 2]),
10535 dtype: DType::F32,
10536 });
10537 let mm = g.push(Node {
10538 op: Op::MatMul,
10539 inputs: vec![lhs, rhs],
10540 shape: Shape::from_dims(&[2, 2, 2]),
10541 dtype: DType::F32,
10542 });
10543 g.set_target_backend(mm, BackendId::Cpu);
10544 (lhs, rhs, mm)
10545 };
10546 let mut inputs = StorageCache::new();
10547 inputs.insert(lhs_id, Arc::new(RwLock::new(lhs_storage)));
10548 inputs.insert(rhs_id, Arc::new(RwLock::new(rhs_storage)));
10549
10550 let (result_arc, result_layout) =
10551 PipelinedExecutor::realize(graph, mm_id, inputs).expect("realize");
10552 assert_eq!(result_layout.shape().dims(), &[2, 2, 2]);
10553 assert!(result_layout.is_contiguous());
10554
10555 let guard = result_arc.read().unwrap();
10556 let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
10557 panic!("expected Cpu storage");
10558 };
10559 assert_eq!(
10562 c.as_slice::<f32>().unwrap(),
10563 &[19.0, 22.0, 43.0, 50.0, 10.0, 20.0, 30.0, 40.0]
10564 );
10565 }
10566
10567 #[test]
10571 fn pipelined_realize_add_f64() {
10572 let lhs = fuel_memory::from_slice_cpu(&[1.0_f64, 2.0, 3.0]);
10573 let rhs = fuel_memory::from_slice_cpu(&[10.0_f64, 20.0, 30.0]);
10574 let graph = Arc::new(RwLock::new(Graph::new()));
10575 let (l_id, r_id, op_id) = {
10576 let mut g = graph.write().unwrap();
10577 let l = g.push(Node {
10578 op: Op::Const,
10579 inputs: vec![],
10580 shape: Shape::from_dims(&[3]),
10581 dtype: DType::F64,
10582 });
10583 let r = g.push(Node {
10584 op: Op::Const,
10585 inputs: vec![],
10586 shape: Shape::from_dims(&[3]),
10587 dtype: DType::F64,
10588 });
10589 let op = g.push(Node {
10590 op: Op::Add,
10591 inputs: vec![l, r],
10592 shape: Shape::from_dims(&[3]),
10593 dtype: DType::F64,
10594 });
10595 g.set_target_backend(op, BackendId::Cpu);
10596 (l, r, op)
10597 };
10598 let mut inputs = StorageCache::new();
10599 inputs.insert(l_id, Arc::new(RwLock::new(lhs)));
10600 inputs.insert(r_id, Arc::new(RwLock::new(rhs)));
10601 let (result_arc, _) = PipelinedExecutor::realize(graph, op_id, inputs).expect("realize");
10602 let guard = result_arc.read().unwrap();
10603 assert_eq!(guard.dtype, DType::F64);
10604 let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
10605 panic!("expected Cpu storage");
10606 };
10607 assert_eq!(c.as_slice::<f64>().unwrap(), &[11.0_f64, 22.0, 33.0]);
10608 }
10609
10610 #[test]
10617 fn pipelined_realize_eq_f32_to_bool_mask() {
10618 let lhs = fuel_memory::from_slice_cpu(&[1.0_f32, 2.0, 3.0, f32::NAN, 0.0]);
10619 let rhs = fuel_memory::from_slice_cpu(&[1.0_f32, 5.0, 3.0, f32::NAN, -0.0]);
10620 let graph = Arc::new(RwLock::new(Graph::new()));
10621 let (l_id, r_id, eq_id) = {
10622 let mut g = graph.write().unwrap();
10623 let l = g.push(Node {
10624 op: Op::Const,
10625 inputs: vec![],
10626 shape: Shape::from_dims(&[5]),
10627 dtype: DType::F32,
10628 });
10629 let r = g.push(Node {
10630 op: Op::Const,
10631 inputs: vec![],
10632 shape: Shape::from_dims(&[5]),
10633 dtype: DType::F32,
10634 });
10635 let eq = g.push(Node {
10636 op: Op::Equal,
10637 inputs: vec![l, r],
10638 shape: Shape::from_dims(&[5]),
10639 dtype: DType::Bool,
10640 });
10641 g.set_target_backend(eq, BackendId::Cpu);
10642 (l, r, eq)
10643 };
10644 let mut inputs = StorageCache::new();
10645 inputs.insert(l_id, Arc::new(RwLock::new(lhs)));
10646 inputs.insert(r_id, Arc::new(RwLock::new(rhs)));
10647 let (result_arc, _) = PipelinedExecutor::realize(graph, eq_id, inputs).expect("realize");
10648 let guard = result_arc.read().unwrap();
10649 assert_eq!(guard.dtype, DType::Bool);
10650 let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
10651 panic!("expected Cpu storage");
10652 };
10653 let mask: &[u8] = c.as_slice().expect("u8 view");
10654 assert_eq!(mask, &[1, 0, 1, 0, 1]);
10660 }
10661
10662 #[test]
10666 fn pipelined_realize_ne_f32_to_bool_mask() {
10667 let lhs = fuel_memory::from_slice_cpu(&[1.0_f32, 2.0, 3.0, f32::NAN, 0.0]);
10668 let rhs = fuel_memory::from_slice_cpu(&[1.0_f32, 5.0, 3.0, f32::NAN, -0.0]);
10669 let graph = Arc::new(RwLock::new(Graph::new()));
10670 let (l_id, r_id, ne_id) = {
10671 let mut g = graph.write().unwrap();
10672 let l = g.push(Node {
10673 op: Op::Const,
10674 inputs: vec![],
10675 shape: Shape::from_dims(&[5]),
10676 dtype: DType::F32,
10677 });
10678 let r = g.push(Node {
10679 op: Op::Const,
10680 inputs: vec![],
10681 shape: Shape::from_dims(&[5]),
10682 dtype: DType::F32,
10683 });
10684 let ne = g.push(Node {
10685 op: Op::Ne,
10686 inputs: vec![l, r],
10687 shape: Shape::from_dims(&[5]),
10688 dtype: DType::Bool,
10689 });
10690 g.set_target_backend(ne, BackendId::Cpu);
10691 (l, r, ne)
10692 };
10693 let mut inputs = StorageCache::new();
10694 inputs.insert(l_id, Arc::new(RwLock::new(lhs)));
10695 inputs.insert(r_id, Arc::new(RwLock::new(rhs)));
10696 let (result_arc, _) = PipelinedExecutor::realize(graph, ne_id, inputs).expect("realize");
10697 let guard = result_arc.read().unwrap();
10698 assert_eq!(guard.dtype, DType::Bool);
10699 let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
10700 panic!("expected Cpu storage");
10701 };
10702 let mask: &[u8] = c.as_slice().expect("u8 view");
10703 assert_eq!(mask, &[0, 1, 0, 1, 0]);
10707 }
10708
10709 #[test]
10714 fn pipelined_realize_lt_f32_to_bool_mask() {
10715 let lhs = fuel_memory::from_slice_cpu(&[1.0_f32, 5.0, 3.0, f32::NAN, -1.0]);
10716 let rhs = fuel_memory::from_slice_cpu(&[2.0_f32, 5.0, 3.0, 0.0, 0.0]);
10717 let graph = Arc::new(RwLock::new(Graph::new()));
10718 let (l_id, r_id, lt_id) = {
10719 let mut g = graph.write().unwrap();
10720 let l = g.push(Node {
10721 op: Op::Const,
10722 inputs: vec![],
10723 shape: Shape::from_dims(&[5]),
10724 dtype: DType::F32,
10725 });
10726 let r = g.push(Node {
10727 op: Op::Const,
10728 inputs: vec![],
10729 shape: Shape::from_dims(&[5]),
10730 dtype: DType::F32,
10731 });
10732 let lt = g.push(Node {
10733 op: Op::Lt,
10734 inputs: vec![l, r],
10735 shape: Shape::from_dims(&[5]),
10736 dtype: DType::Bool,
10737 });
10738 g.set_target_backend(lt, BackendId::Cpu);
10739 (l, r, lt)
10740 };
10741 let mut inputs = StorageCache::new();
10742 inputs.insert(l_id, Arc::new(RwLock::new(lhs)));
10743 inputs.insert(r_id, Arc::new(RwLock::new(rhs)));
10744 let (result_arc, _) = PipelinedExecutor::realize(graph, lt_id, inputs).expect("realize");
10745 let guard = result_arc.read().unwrap();
10746 assert_eq!(guard.dtype, DType::Bool);
10747 let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
10748 panic!("expected Cpu storage");
10749 };
10750 let mask: &[u8] = c.as_slice().expect("u8 view");
10751 assert_eq!(mask, &[1, 0, 0, 0, 1]);
10754 }
10755
10756 #[test]
10759 fn pipelined_realize_le_f32_to_bool_mask() {
10760 let lhs = fuel_memory::from_slice_cpu(&[1.0_f32, 5.0, 3.0, f32::NAN, -1.0]);
10761 let rhs = fuel_memory::from_slice_cpu(&[2.0_f32, 5.0, 2.0, 0.0, 0.0]);
10762 let graph = Arc::new(RwLock::new(Graph::new()));
10763 let (l_id, r_id, le_id) = {
10764 let mut g = graph.write().unwrap();
10765 let l = g.push(Node {
10766 op: Op::Const,
10767 inputs: vec![],
10768 shape: Shape::from_dims(&[5]),
10769 dtype: DType::F32,
10770 });
10771 let r = g.push(Node {
10772 op: Op::Const,
10773 inputs: vec![],
10774 shape: Shape::from_dims(&[5]),
10775 dtype: DType::F32,
10776 });
10777 let le = g.push(Node {
10778 op: Op::Le,
10779 inputs: vec![l, r],
10780 shape: Shape::from_dims(&[5]),
10781 dtype: DType::Bool,
10782 });
10783 g.set_target_backend(le, BackendId::Cpu);
10784 (l, r, le)
10785 };
10786 let mut inputs = StorageCache::new();
10787 inputs.insert(l_id, Arc::new(RwLock::new(lhs)));
10788 inputs.insert(r_id, Arc::new(RwLock::new(rhs)));
10789 let (result_arc, _) = PipelinedExecutor::realize(graph, le_id, inputs).expect("realize");
10790 let guard = result_arc.read().unwrap();
10791 assert_eq!(guard.dtype, DType::Bool);
10792 let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
10793 panic!("expected Cpu storage");
10794 };
10795 let mask: &[u8] = c.as_slice().expect("u8 view");
10796 assert_eq!(mask, &[1, 1, 0, 0, 1]);
10799 }
10800
10801 #[test]
10804 fn pipelined_realize_gt_f32_to_bool_mask() {
10805 let lhs = fuel_memory::from_slice_cpu(&[3.0_f32, 5.0, 2.0, f32::NAN, 1.0]);
10806 let rhs = fuel_memory::from_slice_cpu(&[2.0_f32, 5.0, 3.0, 0.0, 0.0]);
10807 let graph = Arc::new(RwLock::new(Graph::new()));
10808 let (l_id, r_id, gt_id) = {
10809 let mut g = graph.write().unwrap();
10810 let l = g.push(Node {
10811 op: Op::Const,
10812 inputs: vec![],
10813 shape: Shape::from_dims(&[5]),
10814 dtype: DType::F32,
10815 });
10816 let r = g.push(Node {
10817 op: Op::Const,
10818 inputs: vec![],
10819 shape: Shape::from_dims(&[5]),
10820 dtype: DType::F32,
10821 });
10822 let gt = g.push(Node {
10823 op: Op::Gt,
10824 inputs: vec![l, r],
10825 shape: Shape::from_dims(&[5]),
10826 dtype: DType::Bool,
10827 });
10828 g.set_target_backend(gt, BackendId::Cpu);
10829 (l, r, gt)
10830 };
10831 let mut inputs = StorageCache::new();
10832 inputs.insert(l_id, Arc::new(RwLock::new(lhs)));
10833 inputs.insert(r_id, Arc::new(RwLock::new(rhs)));
10834 let (result_arc, _) = PipelinedExecutor::realize(graph, gt_id, inputs).expect("realize");
10835 let guard = result_arc.read().unwrap();
10836 assert_eq!(guard.dtype, DType::Bool);
10837 let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
10838 panic!("expected Cpu storage");
10839 };
10840 let mask: &[u8] = c.as_slice().expect("u8 view");
10841 assert_eq!(mask, &[1, 0, 0, 0, 1]);
10844 }
10845
10846 #[test]
10851 fn pipelined_realize_ge_f32_to_bool_mask() {
10852 let lhs = fuel_memory::from_slice_cpu(&[3.0_f32, 5.0, 2.0, f32::NAN, 0.0]);
10853 let rhs = fuel_memory::from_slice_cpu(&[2.0_f32, 5.0, 3.0, 0.0, 0.0]);
10854 let graph = Arc::new(RwLock::new(Graph::new()));
10855 let (l_id, r_id, ge_id) = {
10856 let mut g = graph.write().unwrap();
10857 let l = g.push(Node {
10858 op: Op::Const,
10859 inputs: vec![],
10860 shape: Shape::from_dims(&[5]),
10861 dtype: DType::F32,
10862 });
10863 let r = g.push(Node {
10864 op: Op::Const,
10865 inputs: vec![],
10866 shape: Shape::from_dims(&[5]),
10867 dtype: DType::F32,
10868 });
10869 let ge = g.push(Node {
10870 op: Op::Ge,
10871 inputs: vec![l, r],
10872 shape: Shape::from_dims(&[5]),
10873 dtype: DType::Bool,
10874 });
10875 g.set_target_backend(ge, BackendId::Cpu);
10876 (l, r, ge)
10877 };
10878 let mut inputs = StorageCache::new();
10879 inputs.insert(l_id, Arc::new(RwLock::new(lhs)));
10880 inputs.insert(r_id, Arc::new(RwLock::new(rhs)));
10881 let (result_arc, _) = PipelinedExecutor::realize(graph, ge_id, inputs).expect("realize");
10882 let guard = result_arc.read().unwrap();
10883 assert_eq!(guard.dtype, DType::Bool);
10884 let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
10885 panic!("expected Cpu storage");
10886 };
10887 let mask: &[u8] = c.as_slice().expect("u8 view");
10888 assert_eq!(mask, &[1, 1, 0, 0, 1]);
10891 }
10892
10893 #[test]
10897 fn pipelined_realize_eq_f64_to_bool_mask() {
10898 let lhs = fuel_memory::from_slice_cpu(&[1.0_f64, 2.0, 3.0]);
10899 let rhs = fuel_memory::from_slice_cpu(&[1.0_f64, 2.0, 4.0]);
10900 let graph = Arc::new(RwLock::new(Graph::new()));
10901 let (l_id, r_id, eq_id) = {
10902 let mut g = graph.write().unwrap();
10903 let l = g.push(Node {
10904 op: Op::Const,
10905 inputs: vec![],
10906 shape: Shape::from_dims(&[3]),
10907 dtype: DType::F64,
10908 });
10909 let r = g.push(Node {
10910 op: Op::Const,
10911 inputs: vec![],
10912 shape: Shape::from_dims(&[3]),
10913 dtype: DType::F64,
10914 });
10915 let eq = g.push(Node {
10916 op: Op::Equal,
10917 inputs: vec![l, r],
10918 shape: Shape::from_dims(&[3]),
10919 dtype: DType::Bool,
10920 });
10921 g.set_target_backend(eq, BackendId::Cpu);
10922 (l, r, eq)
10923 };
10924 let mut inputs = StorageCache::new();
10925 inputs.insert(l_id, Arc::new(RwLock::new(lhs)));
10926 inputs.insert(r_id, Arc::new(RwLock::new(rhs)));
10927 let (result_arc, _) = PipelinedExecutor::realize(graph, eq_id, inputs).expect("realize");
10928 let guard = result_arc.read().unwrap();
10929 assert_eq!(guard.dtype, DType::Bool);
10930 let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
10931 panic!("expected Cpu storage");
10932 };
10933 let mask: &[u8] = c.as_slice().expect("u8 view");
10934 assert_eq!(mask, &[1, 1, 0]);
10935 }
10936
10937 #[test]
10946 fn pipelined_realize_where_f32_picks_per_slot_from_bool_mask() {
10947 let mut cond_storage = fuel_memory::from_slice_cpu(&[1u8, 0, 1, 0, 1]);
10950 cond_storage.dtype = DType::Bool; let a_storage = fuel_memory::from_slice_cpu(&[1.0_f32, 2.0, 3.0, 4.0, 5.0]);
10952 let b_storage = fuel_memory::from_slice_cpu(&[10.0_f32, 20.0, 30.0, 40.0, 50.0]);
10953 let graph = Arc::new(RwLock::new(Graph::new()));
10954 let (cond_id, a_id, b_id, where_id) = {
10955 let mut g = graph.write().unwrap();
10956 let cond = g.push(Node {
10957 op: Op::Const,
10958 inputs: vec![],
10959 shape: Shape::from_dims(&[5]),
10960 dtype: DType::Bool,
10961 });
10962 let a = g.push(Node {
10963 op: Op::Const,
10964 inputs: vec![],
10965 shape: Shape::from_dims(&[5]),
10966 dtype: DType::F32,
10967 });
10968 let b = g.push(Node {
10969 op: Op::Const,
10970 inputs: vec![],
10971 shape: Shape::from_dims(&[5]),
10972 dtype: DType::F32,
10973 });
10974 let w = g.push(Node {
10975 op: Op::Where,
10976 inputs: vec![cond, a, b],
10977 shape: Shape::from_dims(&[5]),
10978 dtype: DType::F32,
10979 });
10980 g.set_target_backend(w, BackendId::Cpu);
10981 (cond, a, b, w)
10982 };
10983 let mut inputs = StorageCache::new();
10984 inputs.insert(cond_id, Arc::new(RwLock::new(cond_storage)));
10985 inputs.insert(a_id, Arc::new(RwLock::new(a_storage)));
10986 inputs.insert(b_id, Arc::new(RwLock::new(b_storage)));
10987 let (result_arc, _) = PipelinedExecutor::realize(graph, where_id, inputs).expect("realize");
10988 let guard = result_arc.read().unwrap();
10989 assert_eq!(guard.dtype, DType::F32);
10990 let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
10991 panic!("expected Cpu storage");
10992 };
10993 let out: &[f32] = c.as_slice().expect("f32 view");
10994 assert_eq!(out, &[1.0, 20.0, 3.0, 40.0, 5.0]);
10995 }
10996
10997 #[test]
11002 fn pipelined_realize_eq_then_where_full_chain() {
11003 let a_storage = fuel_memory::from_slice_cpu(&[1.0_f32, 2.0, 3.0]);
11007 let b_storage = fuel_memory::from_slice_cpu(&[1.0_f32, 5.0, 3.0]);
11008 let pick_storage = fuel_memory::from_slice_cpu(&[10.0_f32, 20.0, 30.0]);
11009 let fb_storage = fuel_memory::from_slice_cpu(&[99.0_f32, 99.0, 99.0]);
11010 let graph = Arc::new(RwLock::new(Graph::new()));
11011 let (a_id, b_id, pick_id, fb_id, where_id) = {
11012 let mut g = graph.write().unwrap();
11013 let a = g.push(Node {
11014 op: Op::Const,
11015 inputs: vec![],
11016 shape: Shape::from_dims(&[3]),
11017 dtype: DType::F32,
11018 });
11019 let b = g.push(Node {
11020 op: Op::Const,
11021 inputs: vec![],
11022 shape: Shape::from_dims(&[3]),
11023 dtype: DType::F32,
11024 });
11025 let pick = g.push(Node {
11026 op: Op::Const,
11027 inputs: vec![],
11028 shape: Shape::from_dims(&[3]),
11029 dtype: DType::F32,
11030 });
11031 let fb = g.push(Node {
11032 op: Op::Const,
11033 inputs: vec![],
11034 shape: Shape::from_dims(&[3]),
11035 dtype: DType::F32,
11036 });
11037 let eq = g.push(Node {
11038 op: Op::Equal,
11039 inputs: vec![a, b],
11040 shape: Shape::from_dims(&[3]),
11041 dtype: DType::Bool,
11042 });
11043 let w = g.push(Node {
11044 op: Op::Where,
11045 inputs: vec![eq, pick, fb],
11046 shape: Shape::from_dims(&[3]),
11047 dtype: DType::F32,
11048 });
11049 g.set_target_backend(eq, BackendId::Cpu);
11050 g.set_target_backend(w, BackendId::Cpu);
11051 (a, b, pick, fb, w)
11052 };
11053 let mut inputs = StorageCache::new();
11054 inputs.insert(a_id, Arc::new(RwLock::new(a_storage)));
11055 inputs.insert(b_id, Arc::new(RwLock::new(b_storage)));
11056 inputs.insert(pick_id, Arc::new(RwLock::new(pick_storage)));
11057 inputs.insert(fb_id, Arc::new(RwLock::new(fb_storage)));
11058 let (result_arc, _) = PipelinedExecutor::realize(graph, where_id, inputs).expect("realize");
11059 let guard = result_arc.read().unwrap();
11060 let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
11061 panic!("expected Cpu storage");
11062 };
11063 let out: &[f32] = c.as_slice().expect("f32 view");
11064 assert_eq!(out, &[10.0, 99.0, 30.0]);
11065 }
11066
11067 #[test]
11074 fn pipelined_realize_qmatmul_q4_0_unit_weight_sums_activations() {
11075 use fuel_graph::QuantType;
11076 use half::f16;
11077 let block_size = std::mem::size_of::<fuel_quantized::BlockQ4_0>();
11078 let mut w_bytes = vec![0u8; 2 * block_size];
11079 for block_idx in 0..2 {
11080 let off = block_idx * block_size;
11081 let d_bytes = f16::from_f32(1.0).to_le_bytes();
11082 w_bytes[off..off + 2].copy_from_slice(&d_bytes);
11083 for i in 0..16 {
11084 w_bytes[off + 2 + i] = 0x99;
11085 }
11086 }
11087 let w_storage = fuel_memory::Storage::new(
11089 fuel_memory::BackendStorage::Cpu(fuel_cpu_backend::CpuStorageBytes::from_bytes(
11090 &w_bytes,
11091 )),
11092 DType::U32,
11093 );
11094
11095 let act_vec: Vec<f32> = (1..=32).map(|x| x as f32).collect();
11096 let act_storage = fuel_memory::from_slice_cpu(&act_vec);
11097
11098 let graph = Arc::new(RwLock::new(Graph::new()));
11099 let (act_id, w_id, mm_id) = {
11100 let mut g = graph.write().unwrap();
11101 let act = g.push(Node {
11102 op: Op::Const,
11103 inputs: vec![],
11104 shape: Shape::from_dims(&[1, 32]),
11105 dtype: DType::F32,
11106 });
11107 let w = g.push(Node {
11108 op: Op::Const,
11109 inputs: vec![],
11110 shape: Shape::from_dims(&[w_bytes.len() / 4]),
11111 dtype: DType::U32,
11112 });
11113 let mm = g.push(Node {
11114 op: Op::Fused(
11115 fuel_graph::registry::FusedOps::QMATMUL,
11116 fuel_graph::registry::FusedOpParams::QMatMul {
11117 quant_type: QuantType::Q4_0,
11118 k: 32,
11119 n: 2,
11120 },
11121 ),
11122 inputs: vec![act, w],
11123 shape: Shape::from_dims(&[1, 2]),
11124 dtype: DType::F32,
11125 });
11126 g.set_target_backend(mm, BackendId::Cpu);
11127 (act, w, mm)
11128 };
11129 let mut inputs = StorageCache::new();
11130 inputs.insert(act_id, Arc::new(RwLock::new(act_storage)));
11131 inputs.insert(w_id, Arc::new(RwLock::new(w_storage)));
11132 let (result_arc, _) = PipelinedExecutor::realize(graph, mm_id, inputs).expect("realize");
11133 let guard = result_arc.read().unwrap();
11134 assert_eq!(guard.dtype, DType::F32);
11135 let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
11136 panic!("expected Cpu storage");
11137 };
11138 let r: &[f32] = c.as_slice().unwrap();
11139 assert!((r[0] - 528.0).abs() < 0.5, "got {}, want 528", r[0]);
11142 assert!((r[1] - 528.0).abs() < 0.5, "got {}, want 528", r[1]);
11143 }
11144
11145 #[test]
11151 fn pipelined_realize_qmatmul_q5_0_against_reference() {
11152 use fuel_graph::QuantType;
11153 use fuel_quantized::{BlockQ5_0, GgmlType};
11154 let n = 2;
11155 let k = 64; let w_f32 = vec![1.0_f32; n * k];
11158 let blocks_per_row = k / BlockQ5_0::BLCK_SIZE;
11159 let mut w_blocks = vec![BlockQ5_0::zeros(); n * blocks_per_row];
11160 BlockQ5_0::from_float(&w_f32, &mut w_blocks);
11161 let bytes_per_block = std::mem::size_of::<BlockQ5_0>();
11163 let w_bytes: Vec<u8> = unsafe {
11164 std::slice::from_raw_parts(
11165 w_blocks.as_ptr() as *const u8,
11166 w_blocks.len() * bytes_per_block,
11167 )
11168 }
11169 .to_vec();
11170 let w_storage = fuel_memory::Storage::new(
11171 fuel_memory::BackendStorage::Cpu(fuel_cpu_backend::CpuStorageBytes::from_bytes(
11172 &w_bytes,
11173 )),
11174 DType::U32,
11175 );
11176 let act_vec: Vec<f32> = (1..=k).map(|x| x as f32).collect();
11177 let act_storage = fuel_memory::from_slice_cpu(&act_vec);
11178 let mut ref_out = vec![0.0_f32; n];
11180 fuel_quantized::matmul::<BlockQ5_0>((1, k, n), &act_vec, &w_blocks, &mut ref_out)
11181 .expect("ref matmul");
11182
11183 let graph = Arc::new(RwLock::new(Graph::new()));
11184 let (act_id, w_id, mm_id) = {
11185 let mut g = graph.write().unwrap();
11186 let act = g.push(Node {
11187 op: Op::Const,
11188 inputs: vec![],
11189 shape: Shape::from_dims(&[1, k]),
11190 dtype: DType::F32,
11191 });
11192 let w = g.push(Node {
11193 op: Op::Const,
11194 inputs: vec![],
11195 shape: Shape::from_dims(&[w_bytes.len() / 4]),
11196 dtype: DType::U32,
11197 });
11198 let mm = g.push(Node {
11199 op: Op::Fused(
11200 fuel_graph::registry::FusedOps::QMATMUL,
11201 fuel_graph::registry::FusedOpParams::QMatMul {
11202 quant_type: QuantType::Q5_0,
11203 k,
11204 n,
11205 },
11206 ),
11207 inputs: vec![act, w],
11208 shape: Shape::from_dims(&[1, n]),
11209 dtype: DType::F32,
11210 });
11211 g.set_target_backend(mm, BackendId::Cpu);
11212 (act, w, mm)
11213 };
11214 let mut inputs = StorageCache::new();
11215 inputs.insert(act_id, Arc::new(RwLock::new(act_storage)));
11216 inputs.insert(w_id, Arc::new(RwLock::new(w_storage)));
11217 let (result_arc, _) = PipelinedExecutor::realize(graph, mm_id, inputs).expect("realize");
11218 let guard = result_arc.read().unwrap();
11219 let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
11220 panic!("expected Cpu storage");
11221 };
11222 let r: &[f32] = c.as_slice().unwrap();
11223 assert_eq!(
11226 r,
11227 ref_out.as_slice(),
11228 "pipelined Q5_0 differs from reference: got {r:?}, want {ref_out:?}"
11229 );
11230 }
11231
11232 #[test]
11237 fn pipelined_realize_qmatmul_q6k_against_reference() {
11238 use fuel_graph::QuantType;
11239 use fuel_quantized::{BlockQ6K, GgmlType};
11240 let n = 2;
11241 let k = 256; let w_f32 = vec![1.0_f32; n * k];
11243 let blocks_per_row = k / BlockQ6K::BLCK_SIZE;
11244 let mut w_blocks = vec![BlockQ6K::zeros(); n * blocks_per_row];
11245 BlockQ6K::from_float(&w_f32, &mut w_blocks);
11246 let bytes_per_block = std::mem::size_of::<BlockQ6K>();
11247 let w_bytes: Vec<u8> = unsafe {
11248 std::slice::from_raw_parts(
11249 w_blocks.as_ptr() as *const u8,
11250 w_blocks.len() * bytes_per_block,
11251 )
11252 }
11253 .to_vec();
11254 let w_storage = fuel_memory::Storage::new(
11255 fuel_memory::BackendStorage::Cpu(fuel_cpu_backend::CpuStorageBytes::from_bytes(
11256 &w_bytes,
11257 )),
11258 DType::U32,
11259 );
11260 let act_vec: Vec<f32> = (1..=k).map(|x| x as f32 / 100.0).collect();
11261 let act_storage = fuel_memory::from_slice_cpu(&act_vec);
11262 let mut ref_out = vec![0.0_f32; n];
11263 fuel_quantized::matmul::<BlockQ6K>((1, k, n), &act_vec, &w_blocks, &mut ref_out)
11264 .expect("ref matmul");
11265
11266 let graph = Arc::new(RwLock::new(Graph::new()));
11267 let (act_id, w_id, mm_id) = {
11268 let mut g = graph.write().unwrap();
11269 let act = g.push(Node {
11270 op: Op::Const,
11271 inputs: vec![],
11272 shape: Shape::from_dims(&[1, k]),
11273 dtype: DType::F32,
11274 });
11275 let w = g.push(Node {
11276 op: Op::Const,
11277 inputs: vec![],
11278 shape: Shape::from_dims(&[w_bytes.len() / 4]),
11279 dtype: DType::U32,
11280 });
11281 let mm = g.push(Node {
11282 op: Op::Fused(
11283 fuel_graph::registry::FusedOps::QMATMUL,
11284 fuel_graph::registry::FusedOpParams::QMatMul {
11285 quant_type: QuantType::Q6K,
11286 k,
11287 n,
11288 },
11289 ),
11290 inputs: vec![act, w],
11291 shape: Shape::from_dims(&[1, n]),
11292 dtype: DType::F32,
11293 });
11294 g.set_target_backend(mm, BackendId::Cpu);
11295 (act, w, mm)
11296 };
11297 let mut inputs = StorageCache::new();
11298 inputs.insert(act_id, Arc::new(RwLock::new(act_storage)));
11299 inputs.insert(w_id, Arc::new(RwLock::new(w_storage)));
11300 let (result_arc, _) = PipelinedExecutor::realize(graph, mm_id, inputs).expect("realize");
11301 let guard = result_arc.read().unwrap();
11302 let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
11303 panic!("expected Cpu storage");
11304 };
11305 let r: &[f32] = c.as_slice().unwrap();
11306 assert_eq!(
11307 r,
11308 ref_out.as_slice(),
11309 "pipelined Q6K differs from reference: got {r:?}, want {ref_out:?}"
11310 );
11311 }
11312
11313 #[test]
11318 fn pipelined_realize_rms_norm_bf16() {
11319 let v: Vec<half::bf16> = [3.0_f32, 4.0]
11320 .iter()
11321 .map(|&x| half::bf16::from_f32(x))
11322 .collect();
11323 let storage = fuel_memory::from_slice_cpu(&v);
11324 let graph = Arc::new(RwLock::new(Graph::new()));
11325 let (in_id, op_id) = {
11326 let mut g = graph.write().unwrap();
11327 let in_id = g.push(Node {
11328 op: Op::Const,
11329 inputs: vec![],
11330 shape: Shape::from_dims(&[1, 2]),
11331 dtype: DType::BF16,
11332 });
11333 let op_id = g.push(Node {
11334 op: Op::Fused(
11335 fuel_graph::registry::FusedOps::RMS_NORM_LAST_DIM,
11336 fuel_graph::registry::FusedOpParams::RmsNormLastDim { eps: 0.0 },
11337 ),
11338 inputs: vec![in_id],
11339 shape: Shape::from_dims(&[1, 2]),
11340 dtype: DType::BF16,
11341 });
11342 g.set_target_backend(op_id, BackendId::Cpu);
11343 (in_id, op_id)
11344 };
11345 let mut inputs = StorageCache::new();
11346 inputs.insert(in_id, Arc::new(RwLock::new(storage)));
11347 let (result_arc, _) = PipelinedExecutor::realize(graph, op_id, inputs).expect("realize");
11348 let guard = result_arc.read().unwrap();
11349 assert_eq!(guard.dtype, DType::BF16);
11350 let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
11351 panic!("expected Cpu storage");
11352 };
11353 let r: &[half::bf16] = c.as_slice().unwrap();
11354 let rms = (12.5_f32).sqrt();
11355 assert!((r[0].to_f32() - 3.0 / rms).abs() < 0.05);
11357 assert!((r[1].to_f32() - 4.0 / rms).abs() < 0.05);
11358 }
11359
11360 #[test]
11365 fn pipelined_realize_matmul_bf16_identity() {
11366 let lhs_v: Vec<half::bf16> = [1.0_f32, 2.0, 3.0, 4.0]
11367 .iter()
11368 .map(|&x| half::bf16::from_f32(x))
11369 .collect();
11370 let rhs_v: Vec<half::bf16> = [1.0_f32, 0.0, 0.0, 1.0]
11371 .iter()
11372 .map(|&x| half::bf16::from_f32(x))
11373 .collect();
11374 let lhs = fuel_memory::from_slice_cpu(&lhs_v);
11375 let rhs = fuel_memory::from_slice_cpu(&rhs_v);
11376 let graph = Arc::new(RwLock::new(Graph::new()));
11377 let (l_id, r_id, mm_id) = {
11378 let mut g = graph.write().unwrap();
11379 let l = g.push(Node {
11380 op: Op::Const,
11381 inputs: vec![],
11382 shape: Shape::from_dims(&[2, 2]),
11383 dtype: DType::BF16,
11384 });
11385 let r = g.push(Node {
11386 op: Op::Const,
11387 inputs: vec![],
11388 shape: Shape::from_dims(&[2, 2]),
11389 dtype: DType::BF16,
11390 });
11391 let mm = g.push(Node {
11392 op: Op::MatMul,
11393 inputs: vec![l, r],
11394 shape: Shape::from_dims(&[2, 2]),
11395 dtype: DType::BF16,
11396 });
11397 g.set_target_backend(mm, BackendId::Cpu);
11398 (l, r, mm)
11399 };
11400 let mut inputs = StorageCache::new();
11401 inputs.insert(l_id, Arc::new(RwLock::new(lhs)));
11402 inputs.insert(r_id, Arc::new(RwLock::new(rhs)));
11403 let (result_arc, _) = PipelinedExecutor::realize(graph, mm_id, inputs).expect("realize");
11404 let guard = result_arc.read().unwrap();
11405 assert_eq!(guard.dtype, DType::BF16);
11406 let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
11407 panic!("expected Cpu storage");
11408 };
11409 let r: &[half::bf16] = c.as_slice().unwrap();
11410 let result_f32: Vec<f32> = r.iter().map(|x| x.to_f32()).collect();
11411 assert_eq!(result_f32, vec![1.0, 2.0, 3.0, 4.0]);
11412 }
11413
11414 #[test]
11417 fn pipelined_realize_sum_dim_f16() {
11418 let v: Vec<half::f16> = [1.0_f32, 2.0, 3.0, 4.0, 5.0, 6.0]
11419 .iter()
11420 .map(|&x| half::f16::from_f32(x))
11421 .collect();
11422 let storage = fuel_memory::from_slice_cpu(&v);
11423 let graph = Arc::new(RwLock::new(Graph::new()));
11424 let (in_id, sum_id) = {
11425 let mut g = graph.write().unwrap();
11426 let in_id = g.push(Node {
11427 op: Op::Const,
11428 inputs: vec![],
11429 shape: Shape::from_dims(&[2, 3]),
11430 dtype: DType::F16,
11431 });
11432 let sum_id = g.push(Node {
11433 op: Op::SumDim(1),
11434 inputs: vec![in_id],
11435 shape: Shape::from_dims(&[2]),
11436 dtype: DType::F16,
11437 });
11438 g.set_target_backend(sum_id, BackendId::Cpu);
11439 (in_id, sum_id)
11440 };
11441 let mut inputs = StorageCache::new();
11442 inputs.insert(in_id, Arc::new(RwLock::new(storage)));
11443 let (result_arc, _) = PipelinedExecutor::realize(graph, sum_id, inputs).expect("realize");
11444 let guard = result_arc.read().unwrap();
11445 assert_eq!(guard.dtype, DType::F16);
11446 let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
11447 panic!("expected Cpu storage");
11448 };
11449 let r: &[half::f16] = c.as_slice().unwrap();
11450 let result_f32: Vec<f32> = r.iter().map(|x| x.to_f32()).collect();
11451 assert_eq!(result_f32, vec![6.0, 15.0]);
11452 }
11453
11454 #[test]
11456 fn pipelined_realize_add_bf16() {
11457 let lhs_vec: Vec<half::bf16> = [1.0_f32, 2.0, 3.0]
11458 .iter()
11459 .map(|&x| half::bf16::from_f32(x))
11460 .collect();
11461 let rhs_vec: Vec<half::bf16> = [10.0_f32, 20.0, 30.0]
11462 .iter()
11463 .map(|&x| half::bf16::from_f32(x))
11464 .collect();
11465 let lhs = fuel_memory::from_slice_cpu(&lhs_vec);
11466 let rhs = fuel_memory::from_slice_cpu(&rhs_vec);
11467 let graph = Arc::new(RwLock::new(Graph::new()));
11468 let (l_id, r_id, op_id) = {
11469 let mut g = graph.write().unwrap();
11470 let l = g.push(Node {
11471 op: Op::Const,
11472 inputs: vec![],
11473 shape: Shape::from_dims(&[3]),
11474 dtype: DType::BF16,
11475 });
11476 let r = g.push(Node {
11477 op: Op::Const,
11478 inputs: vec![],
11479 shape: Shape::from_dims(&[3]),
11480 dtype: DType::BF16,
11481 });
11482 let op = g.push(Node {
11483 op: Op::Add,
11484 inputs: vec![l, r],
11485 shape: Shape::from_dims(&[3]),
11486 dtype: DType::BF16,
11487 });
11488 g.set_target_backend(op, BackendId::Cpu);
11489 (l, r, op)
11490 };
11491 let mut inputs = StorageCache::new();
11492 inputs.insert(l_id, Arc::new(RwLock::new(lhs)));
11493 inputs.insert(r_id, Arc::new(RwLock::new(rhs)));
11494 let (result_arc, _) = PipelinedExecutor::realize(graph, op_id, inputs).expect("realize");
11495 let guard = result_arc.read().unwrap();
11496 assert_eq!(guard.dtype, DType::BF16);
11497 let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
11498 panic!("expected Cpu storage");
11499 };
11500 let r: &[half::bf16] = c.as_slice().unwrap();
11501 let result_f32: Vec<f32> = r.iter().map(|x| x.to_f32()).collect();
11502 assert_eq!(result_f32, vec![11.0, 22.0, 33.0]);
11503 }
11504
11505 #[test]
11509 fn pipelined_realize_sqr_then_sqrt_f16() {
11510 let v: Vec<half::f16> = [1.0_f32, 4.0, 9.0]
11511 .iter()
11512 .map(|&x| half::f16::from_f32(x))
11513 .collect();
11514 let storage = fuel_memory::from_slice_cpu(&v);
11515 let graph = Arc::new(RwLock::new(Graph::new()));
11516 let (in_id, sqrt_id) = {
11517 let mut g = graph.write().unwrap();
11518 let in_id = g.push(Node {
11519 op: Op::Const,
11520 inputs: vec![],
11521 shape: Shape::from_dims(&[3]),
11522 dtype: DType::F16,
11523 });
11524 let sqr_id = g.push(Node {
11525 op: Op::Sqr,
11526 inputs: vec![in_id],
11527 shape: Shape::from_dims(&[3]),
11528 dtype: DType::F16,
11529 });
11530 let sqrt_id = g.push(Node {
11531 op: Op::Sqrt,
11532 inputs: vec![sqr_id],
11533 shape: Shape::from_dims(&[3]),
11534 dtype: DType::F16,
11535 });
11536 g.set_target_backend(sqr_id, BackendId::Cpu);
11537 g.set_target_backend(sqrt_id, BackendId::Cpu);
11538 (in_id, sqrt_id)
11539 };
11540 let mut inputs = StorageCache::new();
11541 inputs.insert(in_id, Arc::new(RwLock::new(storage)));
11542 let (result_arc, _) = PipelinedExecutor::realize(graph, sqrt_id, inputs).expect("realize");
11543 let guard = result_arc.read().unwrap();
11544 assert_eq!(guard.dtype, DType::F16);
11545 let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
11546 panic!("expected Cpu storage");
11547 };
11548 let r: &[half::f16] = c.as_slice().unwrap();
11549 let result_f32: Vec<f32> = r.iter().map(|x| x.to_f32()).collect();
11550 for (got, want) in result_f32.iter().zip(&[1.0_f32, 4.0, 9.0]) {
11552 assert!((got - want).abs() < 0.05, "got {got}, want {want}");
11553 }
11554 }
11555
11556 #[test]
11559 fn pipelined_realize_sum_dim_f64() {
11560 let storage = fuel_memory::from_slice_cpu(&[1.0_f64, 2.0, 3.0, 4.0, 5.0, 6.0]);
11561 let graph = Arc::new(RwLock::new(Graph::new()));
11562 let (in_id, sum_id) = {
11563 let mut g = graph.write().unwrap();
11564 let in_id = g.push(Node {
11565 op: Op::Const,
11566 inputs: vec![],
11567 shape: Shape::from_dims(&[2, 3]),
11568 dtype: DType::F64,
11569 });
11570 let sum_id = g.push(Node {
11571 op: Op::SumDim(1),
11572 inputs: vec![in_id],
11573 shape: Shape::from_dims(&[2]),
11574 dtype: DType::F64,
11575 });
11576 g.set_target_backend(sum_id, BackendId::Cpu);
11577 (in_id, sum_id)
11578 };
11579 let mut inputs = StorageCache::new();
11580 inputs.insert(in_id, Arc::new(RwLock::new(storage)));
11581 let (result_arc, _) = PipelinedExecutor::realize(graph, sum_id, inputs).expect("realize");
11582 let guard = result_arc.read().unwrap();
11583 assert_eq!(guard.dtype, DType::F64);
11584 let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
11585 panic!("expected Cpu storage");
11586 };
11587 assert_eq!(c.as_slice::<f64>().unwrap(), &[6.0_f64, 15.0]);
11588 }
11589
11590 #[test]
11592 fn pipelined_realize_matmul_2x3_times_3x2_f64() {
11593 let lhs = fuel_memory::from_slice_cpu(&[1.0_f64, 2.0, 3.0, 4.0, 5.0, 6.0]);
11594 let rhs = fuel_memory::from_slice_cpu(&[7.0_f64, 8.0, 9.0, 10.0, 11.0, 12.0]);
11595 let graph = Arc::new(RwLock::new(Graph::new()));
11596 let (l_id, r_id, mm_id) = {
11597 let mut g = graph.write().unwrap();
11598 let l = g.push(Node {
11599 op: Op::Const,
11600 inputs: vec![],
11601 shape: Shape::from_dims(&[2, 3]),
11602 dtype: DType::F64,
11603 });
11604 let r = g.push(Node {
11605 op: Op::Const,
11606 inputs: vec![],
11607 shape: Shape::from_dims(&[3, 2]),
11608 dtype: DType::F64,
11609 });
11610 let mm = g.push(Node {
11611 op: Op::MatMul,
11612 inputs: vec![l, r],
11613 shape: Shape::from_dims(&[2, 2]),
11614 dtype: DType::F64,
11615 });
11616 g.set_target_backend(mm, BackendId::Cpu);
11617 (l, r, mm)
11618 };
11619 let mut inputs = StorageCache::new();
11620 inputs.insert(l_id, Arc::new(RwLock::new(lhs)));
11621 inputs.insert(r_id, Arc::new(RwLock::new(rhs)));
11622 let (result_arc, _) = PipelinedExecutor::realize(graph, mm_id, inputs).expect("realize");
11623 let guard = result_arc.read().unwrap();
11624 assert_eq!(guard.dtype, DType::F64);
11625 let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
11626 panic!("expected Cpu storage");
11627 };
11628 assert_eq!(
11629 c.as_slice::<f64>().unwrap(),
11630 &[58.0_f64, 64.0, 139.0, 154.0]
11631 );
11632 }
11633
11634 #[test]
11638 fn pipelined_realize_sqr_then_sqrt_f64() {
11639 let storage = fuel_memory::from_slice_cpu(&[1.0_f64, 4.0, 9.0]);
11640 let graph = Arc::new(RwLock::new(Graph::new()));
11641 let (in_id, sqrt_id) = {
11642 let mut g = graph.write().unwrap();
11643 let in_id = g.push(Node {
11644 op: Op::Const,
11645 inputs: vec![],
11646 shape: Shape::from_dims(&[3]),
11647 dtype: DType::F64,
11648 });
11649 let sqr_id = g.push(Node {
11650 op: Op::Sqr,
11651 inputs: vec![in_id],
11652 shape: Shape::from_dims(&[3]),
11653 dtype: DType::F64,
11654 });
11655 let sqrt_id = g.push(Node {
11656 op: Op::Sqrt,
11657 inputs: vec![sqr_id],
11658 shape: Shape::from_dims(&[3]),
11659 dtype: DType::F64,
11660 });
11661 g.set_target_backend(sqr_id, BackendId::Cpu);
11662 g.set_target_backend(sqrt_id, BackendId::Cpu);
11663 (in_id, sqrt_id)
11664 };
11665 let mut inputs = StorageCache::new();
11666 inputs.insert(in_id, Arc::new(RwLock::new(storage)));
11667 let (result_arc, _) = PipelinedExecutor::realize(graph, sqrt_id, inputs).expect("realize");
11668 let guard = result_arc.read().unwrap();
11669 assert_eq!(guard.dtype, DType::F64);
11670 let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
11671 panic!("expected Cpu storage");
11672 };
11673 assert_eq!(c.as_slice::<f64>().unwrap(), &[1.0_f64, 4.0, 9.0]);
11674 }
11675
11676 #[test]
11678 fn pipelined_realize_argmax_dim() {
11679 let storage = fuel_memory::from_slice_cpu(&[1.0_f32, 5.0, 2.0, 9.0, 0.0, 4.0]);
11682 let graph = Arc::new(RwLock::new(Graph::new()));
11683 let (in_id, op_id) = {
11684 let mut g = graph.write().unwrap();
11685 let in_id = g.push(Node {
11686 op: Op::Const,
11687 inputs: vec![],
11688 shape: Shape::from_dims(&[2, 3]),
11689 dtype: DType::F32,
11690 });
11691 let op_id = g.push(Node {
11692 op: Op::ArgMaxDim(1),
11693 inputs: vec![in_id],
11694 shape: Shape::from_dims(&[2]),
11695 dtype: DType::U32,
11696 });
11697 g.set_target_backend(op_id, BackendId::Cpu);
11698 (in_id, op_id)
11699 };
11700 let mut inputs = StorageCache::new();
11701 inputs.insert(in_id, Arc::new(RwLock::new(storage)));
11702 let (result_arc, _) = PipelinedExecutor::realize(graph, op_id, inputs).expect("realize");
11703 let guard = result_arc.read().unwrap();
11704 assert_eq!(guard.dtype, DType::U32);
11705 let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
11706 panic!("expected Cpu storage");
11707 };
11708 assert_eq!(c.as_slice::<u32>().unwrap(), &[1u32, 0]);
11709 }
11710
11711 #[test]
11714 fn pipelined_realize_index_add_simple() {
11715 let base = fuel_memory::from_slice_cpu(&[10.0_f32, 20.0, 30.0]);
11716 let indices = fuel_memory::from_slice_cpu(&[0u32, 0]);
11717 let src = fuel_memory::from_slice_cpu(&[1.0_f32, 2.0]);
11718 let graph = Arc::new(RwLock::new(Graph::new()));
11719 let (b_id, i_id, s_id, op_id) = {
11720 let mut g = graph.write().unwrap();
11721 let b = g.push(Node {
11722 op: Op::Const,
11723 inputs: vec![],
11724 shape: Shape::from_dims(&[3]),
11725 dtype: DType::F32,
11726 });
11727 let i = g.push(Node {
11728 op: Op::Const,
11729 inputs: vec![],
11730 shape: Shape::from_dims(&[2]),
11731 dtype: DType::U32,
11732 });
11733 let s = g.push(Node {
11734 op: Op::Const,
11735 inputs: vec![],
11736 shape: Shape::from_dims(&[2]),
11737 dtype: DType::F32,
11738 });
11739 let op = g.push(Node {
11740 op: Op::IndexAdd { dim: 0 },
11741 inputs: vec![b, i, s],
11742 shape: Shape::from_dims(&[3]),
11743 dtype: DType::F32,
11744 });
11745 g.set_target_backend(op, BackendId::Cpu);
11746 (b, i, s, op)
11747 };
11748 let mut inputs = StorageCache::new();
11749 inputs.insert(b_id, Arc::new(RwLock::new(base)));
11750 inputs.insert(i_id, Arc::new(RwLock::new(indices)));
11751 inputs.insert(s_id, Arc::new(RwLock::new(src)));
11752 let (result_arc, _) = PipelinedExecutor::realize(graph, op_id, inputs).expect("realize");
11753 let guard = result_arc.read().unwrap();
11754 let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
11755 panic!("expected Cpu storage");
11756 };
11757 assert_eq!(c.as_slice::<f32>().unwrap(), &[13.0, 20.0, 30.0]);
11759 }
11760
11761 #[test]
11764 fn pipelined_realize_scatter_add_outer_dim() {
11765 let base = fuel_memory::from_slice_cpu(&[0.0_f32; 6]);
11769 let indices = fuel_memory::from_slice_cpu(&[0u32, 1, 2, 0]);
11770 let src = fuel_memory::from_slice_cpu(&[1.0_f32, 2.0, 3.0, 4.0]);
11771 let graph = Arc::new(RwLock::new(Graph::new()));
11772 let (b_id, i_id, s_id, op_id) = {
11773 let mut g = graph.write().unwrap();
11774 let b = g.push(Node {
11775 op: Op::Const,
11776 inputs: vec![],
11777 shape: Shape::from_dims(&[3, 2]),
11778 dtype: DType::F32,
11779 });
11780 let i = g.push(Node {
11781 op: Op::Const,
11782 inputs: vec![],
11783 shape: Shape::from_dims(&[2, 2]),
11784 dtype: DType::U32,
11785 });
11786 let s = g.push(Node {
11787 op: Op::Const,
11788 inputs: vec![],
11789 shape: Shape::from_dims(&[2, 2]),
11790 dtype: DType::F32,
11791 });
11792 let op = g.push(Node {
11793 op: Op::ScatterAdd { dim: 0 },
11794 inputs: vec![b, i, s],
11795 shape: Shape::from_dims(&[3, 2]),
11796 dtype: DType::F32,
11797 });
11798 g.set_target_backend(op, BackendId::Cpu);
11799 (b, i, s, op)
11800 };
11801 let mut inputs = StorageCache::new();
11802 inputs.insert(b_id, Arc::new(RwLock::new(base)));
11803 inputs.insert(i_id, Arc::new(RwLock::new(indices)));
11804 inputs.insert(s_id, Arc::new(RwLock::new(src)));
11805 let (result_arc, _) = PipelinedExecutor::realize(graph, op_id, inputs).expect("realize");
11806 let guard = result_arc.read().unwrap();
11807 let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
11808 panic!("expected Cpu storage");
11809 };
11810 assert_eq!(
11811 c.as_slice::<f32>().unwrap(),
11812 &[1.0, 4.0, 0.0, 2.0, 3.0, 0.0]
11813 );
11814 }
11815
11816 #[test]
11820 fn pipelined_realize_rope_pi_over_two() {
11821 let x = fuel_memory::from_slice_cpu(&[1.0_f32, 2.0, 3.0, 4.0]);
11824 let cos = fuel_memory::from_slice_cpu(&[0.0_f32, 0.0, 0.0, 0.0]);
11825 let sin = fuel_memory::from_slice_cpu(&[1.0_f32, 1.0, 1.0, 1.0]);
11826 let graph = Arc::new(RwLock::new(Graph::new()));
11827 let (x_id, cos_id, sin_id, r_id) = {
11828 let mut g = graph.write().unwrap();
11829 let x = g.push(Node {
11830 op: Op::Const,
11831 inputs: vec![],
11832 shape: Shape::from_dims(&[1, 1, 4]),
11833 dtype: DType::F32,
11834 });
11835 let c = g.push(Node {
11836 op: Op::Const,
11837 inputs: vec![],
11838 shape: Shape::from_dims(&[1, 4]),
11839 dtype: DType::F32,
11840 });
11841 let s = g.push(Node {
11842 op: Op::Const,
11843 inputs: vec![],
11844 shape: Shape::from_dims(&[1, 4]),
11845 dtype: DType::F32,
11846 });
11847 let r = g.push(Node {
11848 op: Op::Fused(
11849 fuel_graph::registry::FusedOps::ROPE,
11850 fuel_graph::registry::FusedOpParams::Rope,
11851 ),
11852 inputs: vec![x, c, s],
11853 shape: Shape::from_dims(&[1, 1, 4]),
11854 dtype: DType::F32,
11855 });
11856 g.set_target_backend(r, BackendId::Cpu);
11857 (x, c, s, r)
11858 };
11859 let mut inputs = StorageCache::new();
11860 inputs.insert(x_id, Arc::new(RwLock::new(x)));
11861 inputs.insert(cos_id, Arc::new(RwLock::new(cos)));
11862 inputs.insert(sin_id, Arc::new(RwLock::new(sin)));
11863 let (result_arc, _) = PipelinedExecutor::realize(graph, r_id, inputs).expect("realize");
11864 let guard = result_arc.read().unwrap();
11865 let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
11866 panic!("expected Cpu storage");
11867 };
11868 assert_eq!(c.as_slice::<f32>().unwrap(), &[-3.0, -4.0, 1.0, 2.0]);
11869 }
11870
11871 #[test]
11874 fn pipelined_realize_gather_inner_dim() {
11875 let source =
11876 fuel_memory::from_slice_cpu(&[10.0_f32, 20.0, 30.0, 40.0, 50.0, 60.0, 70.0, 80.0]);
11877 let indices = fuel_memory::from_slice_cpu(&[0u32, 2, 1, 3, 0, 0]);
11878 let graph = Arc::new(RwLock::new(Graph::new()));
11879 let (src_id, idx_id, g_id) = {
11880 let mut g = graph.write().unwrap();
11881 let s = g.push(Node {
11882 op: Op::Const,
11883 inputs: vec![],
11884 shape: Shape::from_dims(&[2, 4]),
11885 dtype: DType::F32,
11886 });
11887 let i = g.push(Node {
11888 op: Op::Const,
11889 inputs: vec![],
11890 shape: Shape::from_dims(&[2, 3]),
11891 dtype: DType::U32,
11892 });
11893 let g_id = g.push(Node {
11894 op: Op::Gather { dim: 1 },
11895 inputs: vec![s, i],
11896 shape: Shape::from_dims(&[2, 3]),
11897 dtype: DType::F32,
11898 });
11899 g.set_target_backend(g_id, BackendId::Cpu);
11900 (s, i, g_id)
11901 };
11902 let mut inputs = StorageCache::new();
11903 inputs.insert(src_id, Arc::new(RwLock::new(source)));
11904 inputs.insert(idx_id, Arc::new(RwLock::new(indices)));
11905 let (result_arc, _) = PipelinedExecutor::realize(graph, g_id, inputs).expect("realize");
11906 let guard = result_arc.read().unwrap();
11907 let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
11908 panic!("expected Cpu storage");
11909 };
11910 assert_eq!(
11911 c.as_slice::<f32>().unwrap(),
11912 &[10.0, 30.0, 20.0, 80.0, 50.0, 50.0]
11913 );
11914 }
11915
11916 #[test]
11920 fn pipelined_realize_index_select_embedding_lookup() {
11921 let table = fuel_memory::from_slice_cpu(&[
11922 10.0_f32, 11.0, 12.0, 20.0, 21.0, 22.0, 30.0, 31.0, 32.0, 40.0, 41.0, 42.0, ]);
11927 let indices = fuel_memory::from_slice_cpu(&[2u32, 0, 2]);
11928 let graph = Arc::new(RwLock::new(Graph::new()));
11929 let (table_id, idx_id, sel_id) = {
11930 let mut g = graph.write().unwrap();
11931 let t = g.push(Node {
11932 op: Op::Const,
11933 inputs: vec![],
11934 shape: Shape::from_dims(&[4, 3]),
11935 dtype: DType::F32,
11936 });
11937 let i = g.push(Node {
11938 op: Op::Const,
11939 inputs: vec![],
11940 shape: Shape::from_dims(&[3]),
11941 dtype: DType::U32,
11942 });
11943 let s = g.push(Node {
11944 op: Op::IndexSelect { dim: 0 },
11945 inputs: vec![t, i],
11946 shape: Shape::from_dims(&[3, 3]),
11947 dtype: DType::F32,
11948 });
11949 g.set_target_backend(s, BackendId::Cpu);
11950 (t, i, s)
11951 };
11952 let mut inputs = StorageCache::new();
11953 inputs.insert(table_id, Arc::new(RwLock::new(table)));
11954 inputs.insert(idx_id, Arc::new(RwLock::new(indices)));
11955 let (result_arc, result_layout) =
11956 PipelinedExecutor::realize(graph, sel_id, inputs).expect("realize");
11957 assert_eq!(result_layout.shape().dims(), &[3, 3]);
11958 let guard = result_arc.read().unwrap();
11959 let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
11960 panic!("expected Cpu storage");
11961 };
11962 assert_eq!(
11963 c.as_slice::<f32>().unwrap(),
11964 &[30.0, 31.0, 32.0, 10.0, 11.0, 12.0, 30.0, 31.0, 32.0]
11965 );
11966 }
11967
11968 #[test]
11971 fn pipelined_realize_rms_norm_last_dim() {
11972 let storage = fuel_memory::from_slice_cpu(&[
11973 3.0_f32, 4.0, 6.0, 8.0, ]);
11976 let graph = Arc::new(RwLock::new(Graph::new()));
11977 let (in_id, op_id) = {
11978 let mut g = graph.write().unwrap();
11979 let in_id = g.push(Node {
11980 op: Op::Const,
11981 inputs: vec![],
11982 shape: Shape::from_dims(&[2, 2]),
11983 dtype: DType::F32,
11984 });
11985 let op_id = g.push(Node {
11986 op: Op::Fused(
11987 fuel_graph::registry::FusedOps::RMS_NORM_LAST_DIM,
11988 fuel_graph::registry::FusedOpParams::RmsNormLastDim { eps: 0.0 },
11989 ),
11990 inputs: vec![in_id],
11991 shape: Shape::from_dims(&[2, 2]),
11992 dtype: DType::F32,
11993 });
11994 g.set_target_backend(op_id, BackendId::Cpu);
11995 (in_id, op_id)
11996 };
11997 let mut inputs = StorageCache::new();
11998 inputs.insert(in_id, Arc::new(RwLock::new(storage)));
11999 let (result_arc, _) = PipelinedExecutor::realize(graph, op_id, inputs).expect("realize");
12000 let guard = result_arc.read().unwrap();
12001 let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
12002 panic!("expected Cpu storage");
12003 };
12004 let result: &[f32] = c.as_slice().unwrap();
12005 let rms0 = (12.5_f32).sqrt();
12007 assert!((result[0] - 3.0 / rms0).abs() < 1e-6);
12008 assert!((result[1] - 4.0 / rms0).abs() < 1e-6);
12009 let rms1 = (50.0_f32).sqrt();
12011 assert!((result[2] - 6.0 / rms1).abs() < 1e-6);
12012 assert!((result[3] - 8.0 / rms1).abs() < 1e-6);
12013 }
12014
12015 #[test]
12018 fn pipelined_realize_layer_norm_last_dim() {
12019 let storage = fuel_memory::from_slice_cpu(&[1.0_f32, 2.0, 3.0, 10.0, 20.0, 30.0]);
12020 let graph = Arc::new(RwLock::new(Graph::new()));
12021 let (in_id, op_id) = {
12022 let mut g = graph.write().unwrap();
12023 let in_id = g.push(Node {
12024 op: Op::Const,
12025 inputs: vec![],
12026 shape: Shape::from_dims(&[2, 3]),
12027 dtype: DType::F32,
12028 });
12029 let op_id = g.push(Node {
12030 op: Op::Fused(
12031 fuel_graph::registry::FusedOps::LAYER_NORM_LAST_DIM,
12032 fuel_graph::registry::FusedOpParams::LayerNormLastDim { eps: 0.0 },
12033 ),
12034 inputs: vec![in_id],
12035 shape: Shape::from_dims(&[2, 3]),
12036 dtype: DType::F32,
12037 });
12038 g.set_target_backend(op_id, BackendId::Cpu);
12039 (in_id, op_id)
12040 };
12041 let mut inputs = StorageCache::new();
12042 inputs.insert(in_id, Arc::new(RwLock::new(storage)));
12043 let (result_arc, _) = PipelinedExecutor::realize(graph, op_id, inputs).expect("realize");
12044 let guard = result_arc.read().unwrap();
12045 let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
12046 panic!("expected Cpu storage");
12047 };
12048 let result: &[f32] = c.as_slice().unwrap();
12049 for row in 0..2 {
12051 let off = row * 3;
12052 let sum: f32 = result[off..off + 3].iter().sum();
12053 let mean = sum / 3.0;
12054 assert!(mean.abs() < 1e-6, "row {row} mean should be 0, got {mean}");
12055 let var: f32 = result[off..off + 3]
12056 .iter()
12057 .map(|v| (v - mean).powi(2))
12058 .sum::<f32>()
12059 / 3.0;
12060 assert!(
12061 (var - 1.0).abs() < 1e-6,
12062 "row {row} var should be 1, got {var}"
12063 );
12064 }
12065 }
12066
12067 #[test]
12070 fn pipelined_realize_softmax_last_dim() {
12071 let storage = fuel_memory::from_slice_cpu(&[1.0_f32, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 100.0]);
12074 let graph = Arc::new(RwLock::new(Graph::new()));
12075 let (in_id, op_id) = {
12076 let mut g = graph.write().unwrap();
12077 let in_id = g.push(Node {
12078 op: Op::Const,
12079 inputs: vec![],
12080 shape: Shape::from_dims(&[2, 4]),
12081 dtype: DType::F32,
12082 });
12083 let op_id = g.push(Node {
12084 op: Op::Fused(
12085 fuel_graph::registry::FusedOps::SOFTMAX_LAST_DIM,
12086 fuel_graph::registry::FusedOpParams::SoftmaxLastDim,
12087 ),
12088 inputs: vec![in_id],
12089 shape: Shape::from_dims(&[2, 4]),
12090 dtype: DType::F32,
12091 });
12092 g.set_target_backend(op_id, BackendId::Cpu);
12093 (in_id, op_id)
12094 };
12095 let mut inputs = StorageCache::new();
12096 inputs.insert(in_id, Arc::new(RwLock::new(storage)));
12097 let (result_arc, _) = PipelinedExecutor::realize(graph, op_id, inputs).expect("realize");
12098 let guard = result_arc.read().unwrap();
12099 let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
12100 panic!("expected Cpu storage");
12101 };
12102 let result: &[f32] = c.as_slice().unwrap();
12103
12104 for v in &result[..4] {
12106 assert!((v - 0.25).abs() < 1e-7);
12107 }
12108 for v in &result[4..7] {
12111 assert!(
12112 *v < 1e-30,
12113 "row-1 leading positions should be near 0, got {v}"
12114 );
12115 }
12116 assert!(
12117 result[7] > 0.999,
12118 "row-1 last position should dominate, got {}",
12119 result[7]
12120 );
12121 let row0_sum: f32 = result[..4].iter().sum();
12123 let row1_sum: f32 = result[4..].iter().sum();
12124 assert!((row0_sum - 1.0).abs() < 1e-6);
12125 assert!((row1_sum - 1.0).abs() < 1e-6);
12126 }
12127
12128 #[test]
12130 fn pipelined_realize_concat_inner_dim() {
12131 let a = fuel_memory::from_slice_cpu(&[1.0_f32, 2.0, 3.0, 4.0, 5.0, 6.0]);
12132 let b = fuel_memory::from_slice_cpu(&[7.0_f32, 8.0, 9.0, 10.0, 11.0, 12.0]);
12133 let graph = Arc::new(RwLock::new(Graph::new()));
12134 let (a_id, b_id, c_id) = {
12135 let mut g = graph.write().unwrap();
12136 let a = g.push(Node {
12137 op: Op::Const,
12138 inputs: vec![],
12139 shape: Shape::from_dims(&[2, 3]),
12140 dtype: DType::F32,
12141 });
12142 let b = g.push(Node {
12143 op: Op::Const,
12144 inputs: vec![],
12145 shape: Shape::from_dims(&[2, 3]),
12146 dtype: DType::F32,
12147 });
12148 let c = g.push(Node {
12149 op: Op::Concat { dim: 1 },
12150 inputs: vec![a, b],
12151 shape: Shape::from_dims(&[2, 6]),
12152 dtype: DType::F32,
12153 });
12154 g.set_target_backend(c, BackendId::Cpu);
12155 (a, b, c)
12156 };
12157 let mut inputs = StorageCache::new();
12158 inputs.insert(a_id, Arc::new(RwLock::new(a)));
12159 inputs.insert(b_id, Arc::new(RwLock::new(b)));
12160 let (result_arc, result_layout) =
12161 PipelinedExecutor::realize(graph, c_id, inputs).expect("realize");
12162 assert_eq!(result_layout.shape().dims(), &[2, 6]);
12163 let guard = result_arc.read().unwrap();
12164 let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
12165 panic!("expected Cpu storage");
12166 };
12167 assert_eq!(
12168 c.as_slice::<f32>().unwrap(),
12169 &[
12170 1.0, 2.0, 3.0, 7.0, 8.0, 9.0, 4.0, 5.0, 6.0, 10.0, 11.0, 12.0
12171 ]
12172 );
12173 }
12174
12175 #[test]
12178 fn pipelined_realize_concat_three_inputs_outer() {
12179 let a = fuel_memory::from_slice_cpu(&[1.0_f32, 2.0]);
12180 let b = fuel_memory::from_slice_cpu(&[3.0_f32, 4.0]);
12181 let c = fuel_memory::from_slice_cpu(&[5.0_f32, 6.0]);
12182 let graph = Arc::new(RwLock::new(Graph::new()));
12183 let (a_id, b_id, c_id, cat_id) = {
12184 let mut g = graph.write().unwrap();
12185 let a = g.push(Node {
12186 op: Op::Const,
12187 inputs: vec![],
12188 shape: Shape::from_dims(&[2]),
12189 dtype: DType::F32,
12190 });
12191 let b = g.push(Node {
12192 op: Op::Const,
12193 inputs: vec![],
12194 shape: Shape::from_dims(&[2]),
12195 dtype: DType::F32,
12196 });
12197 let c = g.push(Node {
12198 op: Op::Const,
12199 inputs: vec![],
12200 shape: Shape::from_dims(&[2]),
12201 dtype: DType::F32,
12202 });
12203 let cat = g.push(Node {
12204 op: Op::Concat { dim: 0 },
12205 inputs: vec![a, b, c],
12206 shape: Shape::from_dims(&[6]),
12207 dtype: DType::F32,
12208 });
12209 g.set_target_backend(cat, BackendId::Cpu);
12210 (a, b, c, cat)
12211 };
12212 let mut inputs = StorageCache::new();
12213 inputs.insert(a_id, Arc::new(RwLock::new(a)));
12214 inputs.insert(b_id, Arc::new(RwLock::new(b)));
12215 inputs.insert(c_id, Arc::new(RwLock::new(c)));
12216 let (result_arc, _) = PipelinedExecutor::realize(graph, cat_id, inputs).expect("realize");
12217 let guard = result_arc.read().unwrap();
12218 let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
12219 panic!("expected Cpu storage");
12220 };
12221 assert_eq!(
12222 c.as_slice::<f32>().unwrap(),
12223 &[1.0, 2.0, 3.0, 4.0, 5.0, 6.0]
12224 );
12225 }
12226
12227 #[test]
12230 fn pipelined_realize_add_scalar() {
12231 let storage = fuel_memory::from_slice_cpu(&[1.0_f32, 2.0, 3.0]);
12232 let graph = Arc::new(RwLock::new(Graph::new()));
12233 let (in_id, op_id) = {
12234 let mut g = graph.write().unwrap();
12235 let in_id = g.push(Node {
12236 op: Op::Const,
12237 inputs: vec![],
12238 shape: Shape::from_dims(&[3]),
12239 dtype: DType::F32,
12240 });
12241 let op_id = g.push(Node {
12242 op: Op::AddScalar(10.0),
12243 inputs: vec![in_id],
12244 shape: Shape::from_dims(&[3]),
12245 dtype: DType::F32,
12246 });
12247 g.set_target_backend(op_id, BackendId::Cpu);
12248 (in_id, op_id)
12249 };
12250 let mut inputs = StorageCache::new();
12251 inputs.insert(in_id, Arc::new(RwLock::new(storage)));
12252 let (result_arc, _) = PipelinedExecutor::realize(graph, op_id, inputs).expect("realize");
12253 let guard = result_arc.read().unwrap();
12254 let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
12255 panic!("expected Cpu storage");
12256 };
12257 assert_eq!(c.as_slice::<f32>().unwrap(), &[11.0, 12.0, 13.0]);
12258 }
12259
12260 #[test]
12262 fn pipelined_realize_clamp() {
12263 let storage = fuel_memory::from_slice_cpu(&[-5.0_f32, 0.5, 100.0]);
12264 let graph = Arc::new(RwLock::new(Graph::new()));
12265 let (in_id, op_id) = {
12266 let mut g = graph.write().unwrap();
12267 let in_id = g.push(Node {
12268 op: Op::Const,
12269 inputs: vec![],
12270 shape: Shape::from_dims(&[3]),
12271 dtype: DType::F32,
12272 });
12273 let op_id = g.push(Node {
12274 op: Op::Clamp {
12275 min: -2.0,
12276 max: 2.0,
12277 },
12278 inputs: vec![in_id],
12279 shape: Shape::from_dims(&[3]),
12280 dtype: DType::F32,
12281 });
12282 g.set_target_backend(op_id, BackendId::Cpu);
12283 (in_id, op_id)
12284 };
12285 let mut inputs = StorageCache::new();
12286 inputs.insert(in_id, Arc::new(RwLock::new(storage)));
12287 let (result_arc, _) = PipelinedExecutor::realize(graph, op_id, inputs).expect("realize");
12288 let guard = result_arc.read().unwrap();
12289 let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
12290 panic!("expected Cpu storage");
12291 };
12292 assert_eq!(c.as_slice::<f32>().unwrap(), &[-2.0, 0.5, 2.0]);
12293 }
12294
12295 #[test]
12297 fn pipelined_realize_maximum_elementwise() {
12298 let lhs_storage = fuel_memory::from_slice_cpu(&[1.0_f32, 5.0, -3.0]);
12299 let rhs_storage = fuel_memory::from_slice_cpu(&[2.0_f32, 1.0, -1.0]);
12300 let graph = Arc::new(RwLock::new(Graph::new()));
12301 let (lhs_id, rhs_id, op_id) = {
12302 let mut g = graph.write().unwrap();
12303 let lhs = g.push(Node {
12304 op: Op::Const,
12305 inputs: vec![],
12306 shape: Shape::from_dims(&[3]),
12307 dtype: DType::F32,
12308 });
12309 let rhs = g.push(Node {
12310 op: Op::Const,
12311 inputs: vec![],
12312 shape: Shape::from_dims(&[3]),
12313 dtype: DType::F32,
12314 });
12315 let op = g.push(Node {
12316 op: Op::Maximum,
12317 inputs: vec![lhs, rhs],
12318 shape: Shape::from_dims(&[3]),
12319 dtype: DType::F32,
12320 });
12321 g.set_target_backend(op, BackendId::Cpu);
12322 (lhs, rhs, op)
12323 };
12324 let mut inputs = StorageCache::new();
12325 inputs.insert(lhs_id, Arc::new(RwLock::new(lhs_storage)));
12326 inputs.insert(rhs_id, Arc::new(RwLock::new(rhs_storage)));
12327 let (result_arc, _) = PipelinedExecutor::realize(graph, op_id, inputs).expect("realize");
12328 let guard = result_arc.read().unwrap();
12329 let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
12330 panic!("expected Cpu storage");
12331 };
12332 assert_eq!(c.as_slice::<f32>().unwrap(), &[2.0, 5.0, -1.0]);
12333 }
12334
12335 #[test]
12338 fn pipelined_realize_conv2d_2x2_sum_kernel() {
12339 let x_storage =
12343 fuel_memory::from_slice_cpu(&[1.0_f32, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]);
12344 let w_storage = fuel_memory::from_slice_cpu(&[1.0_f32, 1.0, 1.0, 1.0]);
12345
12346 let graph = Arc::new(RwLock::new(Graph::new()));
12347 let (x_id, w_id, c_id) = {
12348 let mut g = graph.write().unwrap();
12349 let x = g.push(Node {
12350 op: Op::Const,
12351 inputs: vec![],
12352 shape: Shape::from_dims(&[1, 1, 3, 3]),
12353 dtype: DType::F32,
12354 });
12355 let w = g.push(Node {
12356 op: Op::Const,
12357 inputs: vec![],
12358 shape: Shape::from_dims(&[1, 1, 2, 2]),
12359 dtype: DType::F32,
12360 });
12361 let c = g.push(Node {
12362 op: Op::Fused(
12363 fuel_graph::registry::FusedOps::CONV2D,
12364 fuel_graph::registry::FusedOpParams::Conv2D {
12365 stride: (1, 1),
12366 padding: (0, 0),
12367 groups: 1,
12368 },
12369 ),
12370 inputs: vec![x, w],
12371 shape: Shape::from_dims(&[1, 1, 2, 2]),
12372 dtype: DType::F32,
12373 });
12374 g.set_target_backend(c, BackendId::Cpu);
12375 (x, w, c)
12376 };
12377 let mut inputs = StorageCache::new();
12378 inputs.insert(x_id, Arc::new(RwLock::new(x_storage)));
12379 inputs.insert(w_id, Arc::new(RwLock::new(w_storage)));
12380
12381 let (result_arc, result_layout) =
12382 PipelinedExecutor::realize(graph, c_id, inputs).expect("realize");
12383 assert_eq!(result_layout.shape().dims(), &[1, 1, 2, 2]);
12384
12385 let guard = result_arc.read().unwrap();
12386 let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
12387 panic!("expected Cpu storage");
12388 };
12389 assert_eq!(c.as_slice::<f32>().unwrap(), &[12.0, 16.0, 24.0, 28.0]);
12390 }
12391
12392 #[test]
12394 fn pipelined_realize_conv2d_with_bias() {
12395 let x_storage =
12396 fuel_memory::from_slice_cpu(&[1.0_f32, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]);
12397 let w_storage = fuel_memory::from_slice_cpu(&[1.0_f32, 1.0, 1.0, 1.0]);
12398 let bias_storage = fuel_memory::from_slice_cpu(&[100.0_f32]);
12399
12400 let graph = Arc::new(RwLock::new(Graph::new()));
12401 let (x_id, w_id, b_id, c_id) = {
12402 let mut g = graph.write().unwrap();
12403 let x = g.push(Node {
12404 op: Op::Const,
12405 inputs: vec![],
12406 shape: Shape::from_dims(&[1, 1, 3, 3]),
12407 dtype: DType::F32,
12408 });
12409 let w = g.push(Node {
12410 op: Op::Const,
12411 inputs: vec![],
12412 shape: Shape::from_dims(&[1, 1, 2, 2]),
12413 dtype: DType::F32,
12414 });
12415 let b = g.push(Node {
12416 op: Op::Const,
12417 inputs: vec![],
12418 shape: Shape::from_dims(&[1]),
12419 dtype: DType::F32,
12420 });
12421 let c = g.push(Node {
12422 op: Op::Fused(
12423 fuel_graph::registry::FusedOps::CONV2D,
12424 fuel_graph::registry::FusedOpParams::Conv2D {
12425 stride: (1, 1),
12426 padding: (0, 0),
12427 groups: 1,
12428 },
12429 ),
12430 inputs: vec![x, w, b],
12431 shape: Shape::from_dims(&[1, 1, 2, 2]),
12432 dtype: DType::F32,
12433 });
12434 g.set_target_backend(c, BackendId::Cpu);
12435 (x, w, b, c)
12436 };
12437 let mut inputs = StorageCache::new();
12438 inputs.insert(x_id, Arc::new(RwLock::new(x_storage)));
12439 inputs.insert(w_id, Arc::new(RwLock::new(w_storage)));
12440 inputs.insert(b_id, Arc::new(RwLock::new(bias_storage)));
12441
12442 let (result_arc, _) = PipelinedExecutor::realize(graph, c_id, inputs).expect("realize");
12443 let guard = result_arc.read().unwrap();
12444 let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
12445 panic!("expected Cpu storage");
12446 };
12447 assert_eq!(c.as_slice::<f32>().unwrap(), &[112.0, 116.0, 124.0, 128.0]);
12448 }
12449
12450 #[test]
12452 fn pipelined_realize_conv2d_f64() {
12453 let x_storage =
12454 fuel_memory::from_slice_cpu(&[1.0_f64, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]);
12455 let w_storage = fuel_memory::from_slice_cpu(&[1.0_f64, 1.0, 1.0, 1.0]);
12456
12457 let graph = Arc::new(RwLock::new(Graph::new()));
12458 let (x_id, w_id, c_id) = {
12459 let mut g = graph.write().unwrap();
12460 let x = g.push(Node {
12461 op: Op::Const,
12462 inputs: vec![],
12463 shape: Shape::from_dims(&[1, 1, 3, 3]),
12464 dtype: DType::F64,
12465 });
12466 let w = g.push(Node {
12467 op: Op::Const,
12468 inputs: vec![],
12469 shape: Shape::from_dims(&[1, 1, 2, 2]),
12470 dtype: DType::F64,
12471 });
12472 let c = g.push(Node {
12473 op: Op::Fused(
12474 fuel_graph::registry::FusedOps::CONV2D,
12475 fuel_graph::registry::FusedOpParams::Conv2D {
12476 stride: (1, 1),
12477 padding: (0, 0),
12478 groups: 1,
12479 },
12480 ),
12481 inputs: vec![x, w],
12482 shape: Shape::from_dims(&[1, 1, 2, 2]),
12483 dtype: DType::F64,
12484 });
12485 g.set_target_backend(c, BackendId::Cpu);
12486 (x, w, c)
12487 };
12488 let mut inputs = StorageCache::new();
12489 inputs.insert(x_id, Arc::new(RwLock::new(x_storage)));
12490 inputs.insert(w_id, Arc::new(RwLock::new(w_storage)));
12491 let (result_arc, _) = PipelinedExecutor::realize(graph, c_id, inputs).expect("realize");
12492 let guard = result_arc.read().unwrap();
12493 let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
12494 panic!("expected Cpu storage");
12495 };
12496 assert_eq!(c.as_slice::<f64>().unwrap(), &[12.0, 16.0, 24.0, 28.0]);
12497 }
12498
12499 #[test]
12501 fn pipelined_realize_conv2d_bf16() {
12502 let x_data: Vec<half::bf16> = [1.0_f32, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]
12503 .iter()
12504 .map(|v| half::bf16::from_f32(*v))
12505 .collect();
12506 let w_data: Vec<half::bf16> = [1.0_f32, 1.0, 1.0, 1.0]
12507 .iter()
12508 .map(|v| half::bf16::from_f32(*v))
12509 .collect();
12510 let x_storage = fuel_memory::from_slice_cpu(&x_data);
12511 let w_storage = fuel_memory::from_slice_cpu(&w_data);
12512
12513 let graph = Arc::new(RwLock::new(Graph::new()));
12514 let (x_id, w_id, c_id) = {
12515 let mut g = graph.write().unwrap();
12516 let x = g.push(Node {
12517 op: Op::Const,
12518 inputs: vec![],
12519 shape: Shape::from_dims(&[1, 1, 3, 3]),
12520 dtype: DType::BF16,
12521 });
12522 let w = g.push(Node {
12523 op: Op::Const,
12524 inputs: vec![],
12525 shape: Shape::from_dims(&[1, 1, 2, 2]),
12526 dtype: DType::BF16,
12527 });
12528 let c = g.push(Node {
12529 op: Op::Fused(
12530 fuel_graph::registry::FusedOps::CONV2D,
12531 fuel_graph::registry::FusedOpParams::Conv2D {
12532 stride: (1, 1),
12533 padding: (0, 0),
12534 groups: 1,
12535 },
12536 ),
12537 inputs: vec![x, w],
12538 shape: Shape::from_dims(&[1, 1, 2, 2]),
12539 dtype: DType::BF16,
12540 });
12541 g.set_target_backend(c, BackendId::Cpu);
12542 (x, w, c)
12543 };
12544 let mut inputs = StorageCache::new();
12545 inputs.insert(x_id, Arc::new(RwLock::new(x_storage)));
12546 inputs.insert(w_id, Arc::new(RwLock::new(w_storage)));
12547 let (result_arc, _) = PipelinedExecutor::realize(graph, c_id, inputs).expect("realize");
12548 let guard = result_arc.read().unwrap();
12549 let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
12550 panic!("expected Cpu storage");
12551 };
12552 let got: Vec<f32> = c
12553 .as_slice::<half::bf16>()
12554 .unwrap()
12555 .iter()
12556 .map(|v| v.to_f32())
12557 .collect();
12558 let want = [12.0_f32, 16.0, 24.0, 28.0];
12559 for (g, w) in got.iter().zip(want.iter()) {
12560 assert!((g - w).abs() < 0.5, "got {got:?} want {want:?}");
12561 }
12562 }
12563
12564 #[test]
12566 fn pipelined_realize_conv2d_f16() {
12567 let x_data: Vec<half::f16> = [1.0_f32, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]
12568 .iter()
12569 .map(|v| half::f16::from_f32(*v))
12570 .collect();
12571 let w_data: Vec<half::f16> = [1.0_f32, 1.0, 1.0, 1.0]
12572 .iter()
12573 .map(|v| half::f16::from_f32(*v))
12574 .collect();
12575 let x_storage = fuel_memory::from_slice_cpu(&x_data);
12576 let w_storage = fuel_memory::from_slice_cpu(&w_data);
12577
12578 let graph = Arc::new(RwLock::new(Graph::new()));
12579 let (x_id, w_id, c_id) = {
12580 let mut g = graph.write().unwrap();
12581 let x = g.push(Node {
12582 op: Op::Const,
12583 inputs: vec![],
12584 shape: Shape::from_dims(&[1, 1, 3, 3]),
12585 dtype: DType::F16,
12586 });
12587 let w = g.push(Node {
12588 op: Op::Const,
12589 inputs: vec![],
12590 shape: Shape::from_dims(&[1, 1, 2, 2]),
12591 dtype: DType::F16,
12592 });
12593 let c = g.push(Node {
12594 op: Op::Fused(
12595 fuel_graph::registry::FusedOps::CONV2D,
12596 fuel_graph::registry::FusedOpParams::Conv2D {
12597 stride: (1, 1),
12598 padding: (0, 0),
12599 groups: 1,
12600 },
12601 ),
12602 inputs: vec![x, w],
12603 shape: Shape::from_dims(&[1, 1, 2, 2]),
12604 dtype: DType::F16,
12605 });
12606 g.set_target_backend(c, BackendId::Cpu);
12607 (x, w, c)
12608 };
12609 let mut inputs = StorageCache::new();
12610 inputs.insert(x_id, Arc::new(RwLock::new(x_storage)));
12611 inputs.insert(w_id, Arc::new(RwLock::new(w_storage)));
12612 let (result_arc, _) = PipelinedExecutor::realize(graph, c_id, inputs).expect("realize");
12613 let guard = result_arc.read().unwrap();
12614 let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
12615 panic!("expected Cpu storage");
12616 };
12617 let got: Vec<f32> = c
12618 .as_slice::<half::f16>()
12619 .unwrap()
12620 .iter()
12621 .map(|v| v.to_f32())
12622 .collect();
12623 let want = [12.0_f32, 16.0, 24.0, 28.0];
12624 for (g, w) in got.iter().zip(want.iter()) {
12625 assert!((g - w).abs() < 0.05, "got {got:?} want {want:?}");
12626 }
12627 }
12628
12629 #[test]
12644 fn pipelined_realize_paged_attn_f32() {
12645 let q = fuel_memory::from_slice_cpu(&[2.0_f32, 0.0]);
12646 let k_cache = fuel_memory::from_slice_cpu(&[1.0_f32, 0.0, 0.0, 1.0]);
12647 let v_cache = fuel_memory::from_slice_cpu(&[10.0_f32, 0.0, 0.0, 10.0]);
12648 let block_table_u32 = fuel_memory::Storage::new(
12649 fuel_memory::BackendStorage::Cpu(fuel_cpu_backend::CpuStorageBytes::from_slice(&[
12650 0_u32,
12651 ])),
12652 DType::U32,
12653 );
12654 let context_lens_u32 = fuel_memory::Storage::new(
12655 fuel_memory::BackendStorage::Cpu(fuel_cpu_backend::CpuStorageBytes::from_slice(&[
12656 2_u32,
12657 ])),
12658 DType::U32,
12659 );
12660 let graph = Arc::new(RwLock::new(Graph::new()));
12661 let (q_id, kc_id, vc_id, bt_id, cl_id, op_id) = {
12662 let mut g = graph.write().unwrap();
12663 let q = g.push(Node {
12664 op: Op::Const,
12665 inputs: vec![],
12666 shape: Shape::from_dims(&[1, 1, 1, 2]),
12667 dtype: DType::F32,
12668 });
12669 let kc = g.push(Node {
12670 op: Op::Const,
12671 inputs: vec![],
12672 shape: Shape::from_dims(&[1, 2, 1, 2]),
12673 dtype: DType::F32,
12674 });
12675 let vc = g.push(Node {
12676 op: Op::Const,
12677 inputs: vec![],
12678 shape: Shape::from_dims(&[1, 2, 1, 2]),
12679 dtype: DType::F32,
12680 });
12681 let bt = g.push(Node {
12682 op: Op::Const,
12683 inputs: vec![],
12684 shape: Shape::from_dims(&[1, 1]),
12685 dtype: DType::U32,
12686 });
12687 let cl = g.push(Node {
12688 op: Op::Const,
12689 inputs: vec![],
12690 shape: Shape::from_dims(&[1]),
12691 dtype: DType::U32,
12692 });
12693 let op = g.push(Node {
12694 op: Op::Fused(
12695 fuel_graph::registry::FusedOps::PAGED_ATTN,
12696 fuel_graph::registry::FusedOpParams::PagedAttn {
12697 softmax_scale: 1.0,
12698 block_size: 2,
12699 softcap: None,
12700 },
12701 ),
12702 inputs: vec![q, kc, vc, bt, cl],
12703 shape: Shape::from_dims(&[1, 1, 1, 2]),
12704 dtype: DType::F32,
12705 });
12706 g.set_target_backend(op, BackendId::Cpu);
12707 (q, kc, vc, bt, cl, op)
12708 };
12709 let mut inputs = StorageCache::new();
12710 inputs.insert(q_id, Arc::new(RwLock::new(q)));
12711 inputs.insert(kc_id, Arc::new(RwLock::new(k_cache)));
12712 inputs.insert(vc_id, Arc::new(RwLock::new(v_cache)));
12713 inputs.insert(bt_id, Arc::new(RwLock::new(block_table_u32)));
12714 inputs.insert(cl_id, Arc::new(RwLock::new(context_lens_u32)));
12715 let (result_arc, _) = PipelinedExecutor::realize(graph, op_id, inputs).expect("realize");
12716 let guard = result_arc.read().unwrap();
12717 let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
12718 panic!("expected Cpu storage");
12719 };
12720 let r = c.as_slice::<f32>().unwrap();
12721 let expected_p0 = 2.0_f32.exp() / (2.0_f32.exp() + 1.0);
12722 let expected_p1 = 1.0_f32 / (2.0_f32.exp() + 1.0);
12723 assert!(
12724 (r[0] - 10.0 * expected_p0).abs() < 1e-5,
12725 "row[0]: got {} expected {}",
12726 r[0],
12727 10.0 * expected_p0
12728 );
12729 assert!(
12730 (r[1] - 10.0 * expected_p1).abs() < 1e-5,
12731 "row[1]: got {} expected {}",
12732 r[1],
12733 10.0 * expected_p1
12734 );
12735 }
12736
12737 #[test]
12739 fn pipelined_realize_paged_attn_bf16() {
12740 let q_v: Vec<half::bf16> = [2.0_f32, 0.0]
12741 .iter()
12742 .map(|x| half::bf16::from_f32(*x))
12743 .collect();
12744 let kc_v: Vec<half::bf16> = [1.0_f32, 0.0, 0.0, 1.0]
12745 .iter()
12746 .map(|x| half::bf16::from_f32(*x))
12747 .collect();
12748 let vc_v: Vec<half::bf16> = [10.0_f32, 0.0, 0.0, 10.0]
12749 .iter()
12750 .map(|x| half::bf16::from_f32(*x))
12751 .collect();
12752 let q = fuel_memory::from_slice_cpu(&q_v);
12753 let k_cache = fuel_memory::from_slice_cpu(&kc_v);
12754 let v_cache = fuel_memory::from_slice_cpu(&vc_v);
12755 let bt = fuel_memory::Storage::new(
12756 fuel_memory::BackendStorage::Cpu(fuel_cpu_backend::CpuStorageBytes::from_slice(&[
12757 0_u32,
12758 ])),
12759 DType::U32,
12760 );
12761 let cl = fuel_memory::Storage::new(
12762 fuel_memory::BackendStorage::Cpu(fuel_cpu_backend::CpuStorageBytes::from_slice(&[
12763 2_u32,
12764 ])),
12765 DType::U32,
12766 );
12767 let graph = Arc::new(RwLock::new(Graph::new()));
12768 let (q_id, kc_id, vc_id, bt_id, cl_id, op_id) = {
12769 let mut g = graph.write().unwrap();
12770 let q = g.push(Node {
12771 op: Op::Const,
12772 inputs: vec![],
12773 shape: Shape::from_dims(&[1, 1, 1, 2]),
12774 dtype: DType::BF16,
12775 });
12776 let kc = g.push(Node {
12777 op: Op::Const,
12778 inputs: vec![],
12779 shape: Shape::from_dims(&[1, 2, 1, 2]),
12780 dtype: DType::BF16,
12781 });
12782 let vc = g.push(Node {
12783 op: Op::Const,
12784 inputs: vec![],
12785 shape: Shape::from_dims(&[1, 2, 1, 2]),
12786 dtype: DType::BF16,
12787 });
12788 let bt = g.push(Node {
12789 op: Op::Const,
12790 inputs: vec![],
12791 shape: Shape::from_dims(&[1, 1]),
12792 dtype: DType::U32,
12793 });
12794 let cl = g.push(Node {
12795 op: Op::Const,
12796 inputs: vec![],
12797 shape: Shape::from_dims(&[1]),
12798 dtype: DType::U32,
12799 });
12800 let op = g.push(Node {
12801 op: Op::Fused(
12802 fuel_graph::registry::FusedOps::PAGED_ATTN,
12803 fuel_graph::registry::FusedOpParams::PagedAttn {
12804 softmax_scale: 1.0,
12805 block_size: 2,
12806 softcap: None,
12807 },
12808 ),
12809 inputs: vec![q, kc, vc, bt, cl],
12810 shape: Shape::from_dims(&[1, 1, 1, 2]),
12811 dtype: DType::BF16,
12812 });
12813 g.set_target_backend(op, BackendId::Cpu);
12814 (q, kc, vc, bt, cl, op)
12815 };
12816 let mut inputs = StorageCache::new();
12817 inputs.insert(q_id, Arc::new(RwLock::new(q)));
12818 inputs.insert(kc_id, Arc::new(RwLock::new(k_cache)));
12819 inputs.insert(vc_id, Arc::new(RwLock::new(v_cache)));
12820 inputs.insert(bt_id, Arc::new(RwLock::new(bt)));
12821 inputs.insert(cl_id, Arc::new(RwLock::new(cl)));
12822 let (result_arc, _) = PipelinedExecutor::realize(graph, op_id, inputs).expect("realize");
12823 let guard = result_arc.read().unwrap();
12824 let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
12825 panic!("expected Cpu storage");
12826 };
12827 let got: Vec<f32> = c
12828 .as_slice::<half::bf16>()
12829 .unwrap()
12830 .iter()
12831 .map(|v| v.to_f32())
12832 .collect();
12833 let expected_p0 = 2.0_f32.exp() / (2.0_f32.exp() + 1.0);
12834 let expected_p1 = 1.0_f32 / (2.0_f32.exp() + 1.0);
12835 let want = [10.0 * expected_p0, 10.0 * expected_p1];
12836 for (g, w) in got.iter().zip(want.iter()) {
12837 assert!((g - w).abs() < 0.5, "got {got:?} want {want:?}");
12838 }
12839 }
12840
12841 #[test]
12848 fn pipelined_realize_flash_attn_f32() {
12849 let q = fuel_memory::from_slice_cpu(&[2.0_f32, 0.0]);
12851 let k = fuel_memory::from_slice_cpu(&[1.0_f32, 0.0, 0.0, 1.0]);
12852 let v = fuel_memory::from_slice_cpu(&[10.0_f32, 0.0, 0.0, 10.0]);
12853 let graph = Arc::new(RwLock::new(Graph::new()));
12854 let (q_id, k_id, v_id, op_id) = {
12855 let mut g = graph.write().unwrap();
12856 let q = g.push(Node {
12857 op: Op::Const,
12858 inputs: vec![],
12859 shape: Shape::from_dims(&[1, 1, 1, 2]),
12860 dtype: DType::F32,
12861 });
12862 let k = g.push(Node {
12863 op: Op::Const,
12864 inputs: vec![],
12865 shape: Shape::from_dims(&[1, 1, 2, 2]),
12866 dtype: DType::F32,
12867 });
12868 let v = g.push(Node {
12869 op: Op::Const,
12870 inputs: vec![],
12871 shape: Shape::from_dims(&[1, 1, 2, 2]),
12872 dtype: DType::F32,
12873 });
12874 let op = g.push(Node {
12875 op: Op::Fused(
12876 fuel_graph::registry::FusedOps::FLASH_ATTN,
12877 fuel_graph::registry::FusedOpParams::FlashAttn {
12878 softmax_scale: 1.0,
12879 causal: false,
12880 window_size_left: None,
12881 window_size_right: None,
12882 softcap: None,
12883 k_len: None,
12884 },
12885 ),
12886 inputs: vec![q, k, v],
12887 shape: Shape::from_dims(&[1, 1, 1, 2]),
12888 dtype: DType::F32,
12889 });
12890 g.set_target_backend(op, BackendId::Cpu);
12891 (q, k, v, op)
12892 };
12893 let mut inputs = StorageCache::new();
12894 inputs.insert(q_id, Arc::new(RwLock::new(q)));
12895 inputs.insert(k_id, Arc::new(RwLock::new(k)));
12896 inputs.insert(v_id, Arc::new(RwLock::new(v)));
12897 let (result_arc, _) = PipelinedExecutor::realize(graph, op_id, inputs).expect("realize");
12898 let guard = result_arc.read().unwrap();
12899 let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
12900 panic!("expected Cpu storage");
12901 };
12902 let r = c.as_slice::<f32>().unwrap();
12903 let expected_p0 = 2.0_f32.exp() / (2.0_f32.exp() + 1.0);
12904 let expected_p1 = 1.0_f32 / (2.0_f32.exp() + 1.0);
12905 assert!(
12906 (r[0] - 10.0 * expected_p0).abs() < 1e-5,
12907 "row[0]: got {} expected {}",
12908 r[0],
12909 10.0 * expected_p0
12910 );
12911 assert!(
12912 (r[1] - 10.0 * expected_p1).abs() < 1e-5,
12913 "row[1]: got {} expected {}",
12914 r[1],
12915 10.0 * expected_p1
12916 );
12917 }
12918
12919 #[test]
12923 fn pipelined_realize_flash_attn_causal_f32() {
12924 let q = fuel_memory::from_slice_cpu(&[1.0_f32, 0.0, 0.0, 1.0]);
12933 let k = fuel_memory::from_slice_cpu(&[1.0_f32, 0.0, 0.0, 1.0]);
12934 let v = fuel_memory::from_slice_cpu(&[5.0_f32, 6.0, 7.0, 8.0]);
12935 let graph = Arc::new(RwLock::new(Graph::new()));
12936 let (q_id, k_id, v_id, op_id) = {
12937 let mut g = graph.write().unwrap();
12938 let q = g.push(Node {
12939 op: Op::Const,
12940 inputs: vec![],
12941 shape: Shape::from_dims(&[1, 1, 2, 2]),
12942 dtype: DType::F32,
12943 });
12944 let k = g.push(Node {
12945 op: Op::Const,
12946 inputs: vec![],
12947 shape: Shape::from_dims(&[1, 1, 2, 2]),
12948 dtype: DType::F32,
12949 });
12950 let v = g.push(Node {
12951 op: Op::Const,
12952 inputs: vec![],
12953 shape: Shape::from_dims(&[1, 1, 2, 2]),
12954 dtype: DType::F32,
12955 });
12956 let op = g.push(Node {
12957 op: Op::Fused(
12958 fuel_graph::registry::FusedOps::FLASH_ATTN,
12959 fuel_graph::registry::FusedOpParams::FlashAttn {
12960 softmax_scale: 1.0,
12961 causal: true,
12962 window_size_left: None,
12963 window_size_right: None,
12964 softcap: None,
12965 k_len: None,
12966 },
12967 ),
12968 inputs: vec![q, k, v],
12969 shape: Shape::from_dims(&[1, 1, 2, 2]),
12970 dtype: DType::F32,
12971 });
12972 g.set_target_backend(op, BackendId::Cpu);
12973 (q, k, v, op)
12974 };
12975 let mut inputs = StorageCache::new();
12976 inputs.insert(q_id, Arc::new(RwLock::new(q)));
12977 inputs.insert(k_id, Arc::new(RwLock::new(k)));
12978 inputs.insert(v_id, Arc::new(RwLock::new(v)));
12979 let (result_arc, _) = PipelinedExecutor::realize(graph, op_id, inputs).expect("realize");
12980 let guard = result_arc.read().unwrap();
12981 let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
12982 panic!("expected Cpu storage");
12983 };
12984 let r = c.as_slice::<f32>().unwrap();
12985 assert!((r[0] - 5.0).abs() < 1e-5, "got {}", r[0]);
12987 assert!((r[1] - 6.0).abs() < 1e-5, "got {}", r[1]);
12988 let denom = (1.0_f32).exp() + 1.0;
12990 let expected_a = 5.0 / denom + 7.0 * (1.0_f32).exp() / denom;
12991 let expected_b = 6.0 / denom + 8.0 * (1.0_f32).exp() / denom;
12992 assert!(
12993 (r[2] - expected_a).abs() < 1e-5,
12994 "row1[0]: got {} expected {}",
12995 r[2],
12996 expected_a
12997 );
12998 assert!(
12999 (r[3] - expected_b).abs() < 1e-5,
13000 "row1[1]: got {} expected {}",
13001 r[3],
13002 expected_b
13003 );
13004 }
13005
13006 #[test]
13014 fn pipelined_realize_flash_attn_dynamic_k_len() {
13015 use fuel_ir::{DynScalar, SymId};
13016 let q = fuel_memory::from_slice_cpu(&[1.0_f32, 0.0, 0.0, 1.0]);
13019 let k = fuel_memory::from_slice_cpu(&[
13020 1.0_f32, 0.0, 0.0, 1.0, 1.0, 1.0, 100.0,
13021 100.0, ]);
13023 let v = fuel_memory::from_slice_cpu(&[
13024 5.0_f32, 6.0, 7.0, 8.0, 9.0, 10.0, 999.0,
13025 999.0, ]);
13027 let sym = SymId(0);
13028 let graph = Arc::new(RwLock::new(Graph::new()));
13029 let (q_id, k_id, v_id, op_id) = {
13030 let mut g = graph.write().unwrap();
13031 let q = g.push(Node {
13032 op: Op::Const,
13033 inputs: vec![],
13034 shape: Shape::from_dims(&[1, 1, 2, 2]),
13035 dtype: DType::F32,
13036 });
13037 let k = g.push(Node {
13038 op: Op::Const,
13039 inputs: vec![],
13040 shape: Shape::from_dims(&[1, 1, 4, 2]),
13041 dtype: DType::F32,
13042 });
13043 let v = g.push(Node {
13044 op: Op::Const,
13045 inputs: vec![],
13046 shape: Shape::from_dims(&[1, 1, 4, 2]),
13047 dtype: DType::F32,
13048 });
13049 let op = g.push(Node {
13050 op: Op::Fused(
13051 fuel_graph::registry::FusedOps::FLASH_ATTN,
13052 fuel_graph::registry::FusedOpParams::FlashAttn {
13053 softmax_scale: 1.0,
13054 causal: true,
13055 window_size_left: None,
13056 window_size_right: None,
13057 softcap: None,
13058 k_len: Some(DynScalar::Sym(sym)),
13059 },
13060 ),
13061 inputs: vec![q, k, v],
13062 shape: Shape::from_dims(&[1, 1, 2, 2]),
13063 dtype: DType::F32,
13064 });
13065 g.set_target_backend(op, BackendId::Cpu);
13066 (q, k, v, op)
13067 };
13068 let mut inputs = StorageCache::new();
13069 inputs.insert(q_id, Arc::new(RwLock::new(q)));
13070 inputs.insert(k_id, Arc::new(RwLock::new(k)));
13071 inputs.insert(v_id, Arc::new(RwLock::new(v)));
13072 let mut env = SymEnv::new();
13073 env.bind(sym, 3).unwrap();
13074 let (result_arc, _) = PipelinedExecutor::realize_with_env(graph, op_id, inputs, env)
13075 .expect("realize_with_env");
13076 let guard = result_arc.read().unwrap();
13077 let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
13078 panic!("expected Cpu storage");
13079 };
13080 let r = c.as_slice::<f32>().unwrap();
13081 let e = 1.0_f32.exp();
13082 let d0 = e + 1.0;
13085 let e0x = (5.0 * e + 7.0) / d0;
13086 let e0y = (6.0 * e + 8.0) / d0;
13087 let d1 = 1.0 + 2.0 * e;
13089 let e1x = (5.0 + 7.0 * e + 9.0 * e) / d1;
13090 let e1y = (6.0 + 8.0 * e + 10.0 * e) / d1;
13091 for (got, want) in r.iter().zip([e0x, e0y, e1x, e1y].iter()) {
13092 assert!(
13093 (got - want).abs() < 1e-4,
13094 "flash dyn k_len: got {got}, want {want} (full output {r:?}); \
13095 a poison-contaminated value means the capacity rows past k_len leaked in",
13096 );
13097 }
13098 }
13099
13100 #[test]
13103 fn pipelined_realize_flash_attn_dynamic_k_len_unbound_errors() {
13104 use fuel_ir::{DynScalar, SymId};
13105 let q = fuel_memory::from_slice_cpu(&[1.0_f32, 0.0, 0.0, 1.0]);
13106 let k = fuel_memory::from_slice_cpu(&[0.0_f32; 16]);
13107 let v = fuel_memory::from_slice_cpu(&[0.0_f32; 16]);
13108 let graph = Arc::new(RwLock::new(Graph::new()));
13109 let (q_id, k_id, v_id, op_id) = {
13110 let mut g = graph.write().unwrap();
13111 let q = g.push(Node {
13112 op: Op::Const,
13113 inputs: vec![],
13114 shape: Shape::from_dims(&[1, 1, 2, 2]),
13115 dtype: DType::F32,
13116 });
13117 let k = g.push(Node {
13118 op: Op::Const,
13119 inputs: vec![],
13120 shape: Shape::from_dims(&[1, 1, 4, 2]),
13121 dtype: DType::F32,
13122 });
13123 let v = g.push(Node {
13124 op: Op::Const,
13125 inputs: vec![],
13126 shape: Shape::from_dims(&[1, 1, 4, 2]),
13127 dtype: DType::F32,
13128 });
13129 let op = g.push(Node {
13130 op: Op::Fused(
13131 fuel_graph::registry::FusedOps::FLASH_ATTN,
13132 fuel_graph::registry::FusedOpParams::FlashAttn {
13133 softmax_scale: 1.0,
13134 causal: true,
13135 window_size_left: None,
13136 window_size_right: None,
13137 softcap: None,
13138 k_len: Some(DynScalar::Sym(SymId(0))),
13139 },
13140 ),
13141 inputs: vec![q, k, v],
13142 shape: Shape::from_dims(&[1, 1, 2, 2]),
13143 dtype: DType::F32,
13144 });
13145 g.set_target_backend(op, BackendId::Cpu);
13146 (q, k, v, op)
13147 };
13148 let mut inputs = StorageCache::new();
13149 inputs.insert(q_id, Arc::new(RwLock::new(q)));
13150 inputs.insert(k_id, Arc::new(RwLock::new(k)));
13151 inputs.insert(v_id, Arc::new(RwLock::new(v)));
13152 let result = PipelinedExecutor::realize_with_env(graph, op_id, inputs, SymEnv::new());
13154 assert!(
13155 result.is_err(),
13156 "unbound flash k_len symbol must surface a typed error"
13157 );
13158 }
13159
13160 #[test]
13162 fn pipelined_realize_flash_attn_bf16() {
13163 let q_v: Vec<half::bf16> = [2.0_f32, 0.0]
13164 .iter()
13165 .map(|x| half::bf16::from_f32(*x))
13166 .collect();
13167 let k_v: Vec<half::bf16> = [1.0_f32, 0.0, 0.0, 1.0]
13168 .iter()
13169 .map(|x| half::bf16::from_f32(*x))
13170 .collect();
13171 let v_v: Vec<half::bf16> = [10.0_f32, 0.0, 0.0, 10.0]
13172 .iter()
13173 .map(|x| half::bf16::from_f32(*x))
13174 .collect();
13175 let q = fuel_memory::from_slice_cpu(&q_v);
13176 let k = fuel_memory::from_slice_cpu(&k_v);
13177 let v = fuel_memory::from_slice_cpu(&v_v);
13178 let graph = Arc::new(RwLock::new(Graph::new()));
13179 let (q_id, k_id, v_id, op_id) = {
13180 let mut g = graph.write().unwrap();
13181 let q = g.push(Node {
13182 op: Op::Const,
13183 inputs: vec![],
13184 shape: Shape::from_dims(&[1, 1, 1, 2]),
13185 dtype: DType::BF16,
13186 });
13187 let k = g.push(Node {
13188 op: Op::Const,
13189 inputs: vec![],
13190 shape: Shape::from_dims(&[1, 1, 2, 2]),
13191 dtype: DType::BF16,
13192 });
13193 let v = g.push(Node {
13194 op: Op::Const,
13195 inputs: vec![],
13196 shape: Shape::from_dims(&[1, 1, 2, 2]),
13197 dtype: DType::BF16,
13198 });
13199 let op = g.push(Node {
13200 op: Op::Fused(
13201 fuel_graph::registry::FusedOps::FLASH_ATTN,
13202 fuel_graph::registry::FusedOpParams::FlashAttn {
13203 softmax_scale: 1.0,
13204 causal: false,
13205 window_size_left: None,
13206 window_size_right: None,
13207 softcap: None,
13208 k_len: None,
13209 },
13210 ),
13211 inputs: vec![q, k, v],
13212 shape: Shape::from_dims(&[1, 1, 1, 2]),
13213 dtype: DType::BF16,
13214 });
13215 g.set_target_backend(op, BackendId::Cpu);
13216 (q, k, v, op)
13217 };
13218 let mut inputs = StorageCache::new();
13219 inputs.insert(q_id, Arc::new(RwLock::new(q)));
13220 inputs.insert(k_id, Arc::new(RwLock::new(k)));
13221 inputs.insert(v_id, Arc::new(RwLock::new(v)));
13222 let (result_arc, _) = PipelinedExecutor::realize(graph, op_id, inputs).expect("realize");
13223 let guard = result_arc.read().unwrap();
13224 let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
13225 panic!("expected Cpu storage");
13226 };
13227 let got: Vec<f32> = c
13228 .as_slice::<half::bf16>()
13229 .unwrap()
13230 .iter()
13231 .map(|v| v.to_f32())
13232 .collect();
13233 let expected_p0 = 2.0_f32.exp() / (2.0_f32.exp() + 1.0);
13234 let expected_p1 = 1.0_f32 / (2.0_f32.exp() + 1.0);
13235 let want = [10.0 * expected_p0, 10.0 * expected_p1];
13236 for (g, w) in got.iter().zip(want.iter()) {
13237 assert!((g - w).abs() < 0.5, "got {got:?} want {want:?}");
13238 }
13239 }
13240
13241 #[test]
13246 fn pipelined_realize_fused_linear_f32() {
13247 let a = fuel_memory::from_slice_cpu(&[1.0_f32, 2.0, 3.0, 4.0, 5.0, 6.0]);
13248 let b = fuel_memory::from_slice_cpu(&[1.0_f32, 0.0, 0.0, 1.0, 1.0, 1.0]);
13249 let bias = fuel_memory::from_slice_cpu(&[10.0_f32, 20.0]);
13250 let graph = Arc::new(RwLock::new(Graph::new()));
13251 let (a_id, b_id, bias_id, op_id) = {
13252 let mut g = graph.write().unwrap();
13253 let a = g.push(Node {
13254 op: Op::Const,
13255 inputs: vec![],
13256 shape: Shape::from_dims(&[1, 2, 3]),
13257 dtype: DType::F32,
13258 });
13259 let b = g.push(Node {
13260 op: Op::Const,
13261 inputs: vec![],
13262 shape: Shape::from_dims(&[1, 3, 2]),
13263 dtype: DType::F32,
13264 });
13265 let bias = g.push(Node {
13266 op: Op::Const,
13267 inputs: vec![],
13268 shape: Shape::from_dims(&[2]),
13269 dtype: DType::F32,
13270 });
13271 let op = g.push(Node {
13272 op: Op::Fused(
13273 fuel_graph::registry::FusedOps::FUSED_LINEAR,
13274 fuel_graph::registry::FusedOpParams::FusedLinear,
13275 ),
13276 inputs: vec![a, b, bias],
13277 shape: Shape::from_dims(&[1, 2, 2]),
13278 dtype: DType::F32,
13279 });
13280 g.set_target_backend(op, BackendId::Cpu);
13281 (a, b, bias, op)
13282 };
13283 let mut inputs = StorageCache::new();
13284 inputs.insert(a_id, Arc::new(RwLock::new(a)));
13285 inputs.insert(b_id, Arc::new(RwLock::new(b)));
13286 inputs.insert(bias_id, Arc::new(RwLock::new(bias)));
13287 let (result_arc, _) = PipelinedExecutor::realize(graph, op_id, inputs).expect("realize");
13288 let guard = result_arc.read().unwrap();
13289 let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
13290 panic!("expected Cpu storage");
13291 };
13292 assert_eq!(c.as_slice::<f32>().unwrap(), &[14.0, 25.0, 20.0, 31.0]);
13293 }
13294
13295 #[test]
13297 fn pipelined_realize_fused_linear_f64() {
13298 let a = fuel_memory::from_slice_cpu(&[1.0_f64, 2.0, 3.0, 4.0, 5.0, 6.0]);
13299 let b = fuel_memory::from_slice_cpu(&[1.0_f64, 0.0, 0.0, 1.0, 1.0, 1.0]);
13300 let bias = fuel_memory::from_slice_cpu(&[10.0_f64, 20.0]);
13301 let graph = Arc::new(RwLock::new(Graph::new()));
13302 let (a_id, b_id, bias_id, op_id) = {
13303 let mut g = graph.write().unwrap();
13304 let a = g.push(Node {
13305 op: Op::Const,
13306 inputs: vec![],
13307 shape: Shape::from_dims(&[1, 2, 3]),
13308 dtype: DType::F64,
13309 });
13310 let b = g.push(Node {
13311 op: Op::Const,
13312 inputs: vec![],
13313 shape: Shape::from_dims(&[1, 3, 2]),
13314 dtype: DType::F64,
13315 });
13316 let bias = g.push(Node {
13317 op: Op::Const,
13318 inputs: vec![],
13319 shape: Shape::from_dims(&[2]),
13320 dtype: DType::F64,
13321 });
13322 let op = g.push(Node {
13323 op: Op::Fused(
13324 fuel_graph::registry::FusedOps::FUSED_LINEAR,
13325 fuel_graph::registry::FusedOpParams::FusedLinear,
13326 ),
13327 inputs: vec![a, b, bias],
13328 shape: Shape::from_dims(&[1, 2, 2]),
13329 dtype: DType::F64,
13330 });
13331 g.set_target_backend(op, BackendId::Cpu);
13332 (a, b, bias, op)
13333 };
13334 let mut inputs = StorageCache::new();
13335 inputs.insert(a_id, Arc::new(RwLock::new(a)));
13336 inputs.insert(b_id, Arc::new(RwLock::new(b)));
13337 inputs.insert(bias_id, Arc::new(RwLock::new(bias)));
13338 let (result_arc, _) = PipelinedExecutor::realize(graph, op_id, inputs).expect("realize");
13339 let guard = result_arc.read().unwrap();
13340 let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
13341 panic!("expected Cpu storage");
13342 };
13343 assert_eq!(c.as_slice::<f64>().unwrap(), &[14.0, 25.0, 20.0, 31.0]);
13344 }
13345
13346 #[test]
13348 fn pipelined_realize_fused_linear_bf16() {
13349 let a_v: Vec<half::bf16> = [1.0_f32, 2.0, 3.0, 4.0, 5.0, 6.0]
13350 .iter()
13351 .map(|x| half::bf16::from_f32(*x))
13352 .collect();
13353 let b_v: Vec<half::bf16> = [1.0_f32, 0.0, 0.0, 1.0, 1.0, 1.0]
13354 .iter()
13355 .map(|x| half::bf16::from_f32(*x))
13356 .collect();
13357 let bias_v: Vec<half::bf16> = [10.0_f32, 20.0]
13358 .iter()
13359 .map(|x| half::bf16::from_f32(*x))
13360 .collect();
13361 let a = fuel_memory::from_slice_cpu(&a_v);
13362 let b = fuel_memory::from_slice_cpu(&b_v);
13363 let bias = fuel_memory::from_slice_cpu(&bias_v);
13364 let graph = Arc::new(RwLock::new(Graph::new()));
13365 let (a_id, b_id, bias_id, op_id) = {
13366 let mut g = graph.write().unwrap();
13367 let a = g.push(Node {
13368 op: Op::Const,
13369 inputs: vec![],
13370 shape: Shape::from_dims(&[1, 2, 3]),
13371 dtype: DType::BF16,
13372 });
13373 let b = g.push(Node {
13374 op: Op::Const,
13375 inputs: vec![],
13376 shape: Shape::from_dims(&[1, 3, 2]),
13377 dtype: DType::BF16,
13378 });
13379 let bias = g.push(Node {
13380 op: Op::Const,
13381 inputs: vec![],
13382 shape: Shape::from_dims(&[2]),
13383 dtype: DType::BF16,
13384 });
13385 let op = g.push(Node {
13386 op: Op::Fused(
13387 fuel_graph::registry::FusedOps::FUSED_LINEAR,
13388 fuel_graph::registry::FusedOpParams::FusedLinear,
13389 ),
13390 inputs: vec![a, b, bias],
13391 shape: Shape::from_dims(&[1, 2, 2]),
13392 dtype: DType::BF16,
13393 });
13394 g.set_target_backend(op, BackendId::Cpu);
13395 (a, b, bias, op)
13396 };
13397 let mut inputs = StorageCache::new();
13398 inputs.insert(a_id, Arc::new(RwLock::new(a)));
13399 inputs.insert(b_id, Arc::new(RwLock::new(b)));
13400 inputs.insert(bias_id, Arc::new(RwLock::new(bias)));
13401 let (result_arc, _) = PipelinedExecutor::realize(graph, op_id, inputs).expect("realize");
13402 let guard = result_arc.read().unwrap();
13403 let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
13404 panic!("expected Cpu storage");
13405 };
13406 let got: Vec<f32> = c
13407 .as_slice::<half::bf16>()
13408 .unwrap()
13409 .iter()
13410 .map(|v| v.to_f32())
13411 .collect();
13412 let want = [14.0_f32, 25.0, 20.0, 31.0];
13413 for (g, w) in got.iter().zip(want.iter()) {
13414 assert!((g - w).abs() < 0.5, "got {got:?} want {want:?}");
13415 }
13416 }
13417
13418 #[test]
13421 fn pipelined_realize_reduce_sum_to_f32_drops_leading_axis() {
13422 let v = fuel_memory::from_slice_cpu(&[1.0_f32, 2.0, 3.0, 4.0, 5.0, 6.0]);
13423 let graph = Arc::new(RwLock::new(Graph::new()));
13424 let (in_id, op_id) = {
13425 let mut g = graph.write().unwrap();
13426 let in_id = g.push(Node {
13427 op: Op::Const,
13428 inputs: vec![],
13429 shape: Shape::from_dims(&[2, 3]),
13430 dtype: DType::F32,
13431 });
13432 let op_id = g.push(Node {
13433 op: Op::ReduceSumTo(Shape::from_dims(&[3])),
13434 inputs: vec![in_id],
13435 shape: Shape::from_dims(&[3]),
13436 dtype: DType::F32,
13437 });
13438 g.set_target_backend(op_id, BackendId::Cpu);
13439 (in_id, op_id)
13440 };
13441 let mut inputs = StorageCache::new();
13442 inputs.insert(in_id, Arc::new(RwLock::new(v)));
13443 let (result_arc, _) = PipelinedExecutor::realize(graph, op_id, inputs).expect("realize");
13444 let guard = result_arc.read().unwrap();
13445 let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
13446 panic!("expected Cpu storage");
13447 };
13448 assert_eq!(c.as_slice::<f32>().unwrap(), &[5.0, 7.0, 9.0]);
13449 }
13450
13451 #[test]
13454 fn pipelined_realize_reduce_sum_to_f32_keepdim_middle() {
13455 let mut v: Vec<f32> = (1..=24).map(|x| x as f32).collect();
13458 let _ = &mut v;
13459 let s = fuel_memory::from_slice_cpu(&v);
13460 let graph = Arc::new(RwLock::new(Graph::new()));
13461 let (in_id, op_id) = {
13462 let mut g = graph.write().unwrap();
13463 let in_id = g.push(Node {
13464 op: Op::Const,
13465 inputs: vec![],
13466 shape: Shape::from_dims(&[2, 3, 4]),
13467 dtype: DType::F32,
13468 });
13469 let op_id = g.push(Node {
13470 op: Op::ReduceSumTo(Shape::from_dims(&[2, 1, 4])),
13471 inputs: vec![in_id],
13472 shape: Shape::from_dims(&[2, 1, 4]),
13473 dtype: DType::F32,
13474 });
13475 g.set_target_backend(op_id, BackendId::Cpu);
13476 (in_id, op_id)
13477 };
13478 let mut inputs = StorageCache::new();
13479 inputs.insert(in_id, Arc::new(RwLock::new(s)));
13480 let (result_arc, _) = PipelinedExecutor::realize(graph, op_id, inputs).expect("realize");
13481 let guard = result_arc.read().unwrap();
13482 let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
13483 panic!("expected Cpu storage");
13484 };
13485 assert_eq!(
13488 c.as_slice::<f32>().unwrap(),
13489 &[15.0, 18.0, 21.0, 24.0, 51.0, 54.0, 57.0, 60.0],
13490 );
13491 }
13492
13493 #[test]
13495 fn pipelined_realize_reduce_sum_to_f64() {
13496 let v = fuel_memory::from_slice_cpu(&[1.0_f64, 2.0, 3.0, 4.0, 5.0, 6.0]);
13497 let graph = Arc::new(RwLock::new(Graph::new()));
13498 let (in_id, op_id) = {
13499 let mut g = graph.write().unwrap();
13500 let in_id = g.push(Node {
13501 op: Op::Const,
13502 inputs: vec![],
13503 shape: Shape::from_dims(&[2, 3]),
13504 dtype: DType::F64,
13505 });
13506 let op_id = g.push(Node {
13507 op: Op::ReduceSumTo(Shape::from_dims(&[3])),
13508 inputs: vec![in_id],
13509 shape: Shape::from_dims(&[3]),
13510 dtype: DType::F64,
13511 });
13512 g.set_target_backend(op_id, BackendId::Cpu);
13513 (in_id, op_id)
13514 };
13515 let mut inputs = StorageCache::new();
13516 inputs.insert(in_id, Arc::new(RwLock::new(v)));
13517 let (result_arc, _) = PipelinedExecutor::realize(graph, op_id, inputs).expect("realize");
13518 let guard = result_arc.read().unwrap();
13519 let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
13520 panic!("expected Cpu storage");
13521 };
13522 assert_eq!(c.as_slice::<f64>().unwrap(), &[5.0, 7.0, 9.0]);
13523 }
13524
13525 #[test]
13527 fn pipelined_realize_reduce_max_to_f32_drops_leading_axis() {
13528 let v = fuel_memory::from_slice_cpu(&[1.0_f32, 7.0, 3.0, 4.0, 2.0, 6.0]);
13531 let graph = Arc::new(RwLock::new(Graph::new()));
13532 let (in_id, op_id) = {
13533 let mut g = graph.write().unwrap();
13534 let in_id = g.push(Node {
13535 op: Op::Const,
13536 inputs: vec![],
13537 shape: Shape::from_dims(&[2, 3]),
13538 dtype: DType::F32,
13539 });
13540 let op_id = g.push(Node {
13541 op: Op::ReduceMaxTo(Shape::from_dims(&[3])),
13542 inputs: vec![in_id],
13543 shape: Shape::from_dims(&[3]),
13544 dtype: DType::F32,
13545 });
13546 g.set_target_backend(op_id, BackendId::Cpu);
13547 (in_id, op_id)
13548 };
13549 let mut inputs = StorageCache::new();
13550 inputs.insert(in_id, Arc::new(RwLock::new(v)));
13551 let (result_arc, _) = PipelinedExecutor::realize(graph, op_id, inputs).expect("realize");
13552 let guard = result_arc.read().unwrap();
13553 let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
13554 panic!("expected Cpu storage");
13555 };
13556 assert_eq!(c.as_slice::<f32>().unwrap(), &[4.0, 7.0, 6.0]);
13557 }
13558
13559 #[test]
13563 fn pipelined_realize_reduce_max_to_f32_keepdim_trailing() {
13564 let v = fuel_memory::from_slice_cpu(&[1.0_f32, 2.0, 3.0, 4.0, 5.0, 6.0]);
13566 let graph = Arc::new(RwLock::new(Graph::new()));
13567 let (in_id, op_id) = {
13568 let mut g = graph.write().unwrap();
13569 let in_id = g.push(Node {
13570 op: Op::Const,
13571 inputs: vec![],
13572 shape: Shape::from_dims(&[2, 3]),
13573 dtype: DType::F32,
13574 });
13575 let op_id = g.push(Node {
13576 op: Op::ReduceMaxTo(Shape::from_dims(&[2, 1])),
13577 inputs: vec![in_id],
13578 shape: Shape::from_dims(&[2, 1]),
13579 dtype: DType::F32,
13580 });
13581 g.set_target_backend(op_id, BackendId::Cpu);
13582 (in_id, op_id)
13583 };
13584 let mut inputs = StorageCache::new();
13585 inputs.insert(in_id, Arc::new(RwLock::new(v)));
13586 let (result_arc, _) = PipelinedExecutor::realize(graph, op_id, inputs).expect("realize");
13587 let guard = result_arc.read().unwrap();
13588 let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
13589 panic!("expected Cpu storage");
13590 };
13591 assert_eq!(c.as_slice::<f32>().unwrap(), &[3.0, 6.0]);
13592 }
13593
13594 #[test]
13596 fn pipelined_realize_reduce_sum_to_bf16() {
13597 let v: Vec<half::bf16> = [1.0_f32, 2.0, 3.0, 4.0, 5.0, 6.0]
13598 .iter()
13599 .map(|x| half::bf16::from_f32(*x))
13600 .collect();
13601 let s = fuel_memory::from_slice_cpu(&v);
13602 let graph = Arc::new(RwLock::new(Graph::new()));
13603 let (in_id, op_id) = {
13604 let mut g = graph.write().unwrap();
13605 let in_id = g.push(Node {
13606 op: Op::Const,
13607 inputs: vec![],
13608 shape: Shape::from_dims(&[2, 3]),
13609 dtype: DType::BF16,
13610 });
13611 let op_id = g.push(Node {
13612 op: Op::ReduceSumTo(Shape::from_dims(&[3])),
13613 inputs: vec![in_id],
13614 shape: Shape::from_dims(&[3]),
13615 dtype: DType::BF16,
13616 });
13617 g.set_target_backend(op_id, BackendId::Cpu);
13618 (in_id, op_id)
13619 };
13620 let mut inputs = StorageCache::new();
13621 inputs.insert(in_id, Arc::new(RwLock::new(s)));
13622 let (result_arc, _) = PipelinedExecutor::realize(graph, op_id, inputs).expect("realize");
13623 let guard = result_arc.read().unwrap();
13624 let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
13625 panic!("expected Cpu storage");
13626 };
13627 let got: Vec<f32> = c
13628 .as_slice::<half::bf16>()
13629 .unwrap()
13630 .iter()
13631 .map(|v| v.to_f32())
13632 .collect();
13633 let want = [5.0_f32, 7.0, 9.0];
13634 for (g, w) in got.iter().zip(want.iter()) {
13635 assert!((g - w).abs() < 0.5, "got {got:?} want {want:?}");
13636 }
13637 }
13638
13639 #[test]
13647 fn pipelined_realize_conv_transpose2d_f32() {
13648 let x_storage = fuel_memory::from_slice_cpu(&[1.0_f32, 2.0, 3.0, 4.0]);
13649 let w_storage = fuel_memory::from_slice_cpu(&[1.0_f32, 1.0, 1.0, 1.0]);
13650 let graph = Arc::new(RwLock::new(Graph::new()));
13651 let (x_id, w_id, c_id) = {
13652 let mut g = graph.write().unwrap();
13653 let x = g.push(Node {
13654 op: Op::Const,
13655 inputs: vec![],
13656 shape: Shape::from_dims(&[1, 1, 2, 2]),
13657 dtype: DType::F32,
13658 });
13659 let w = g.push(Node {
13660 op: Op::Const,
13661 inputs: vec![],
13662 shape: Shape::from_dims(&[1, 1, 2, 2]),
13663 dtype: DType::F32,
13664 });
13665 let c = g.push(Node {
13666 op: Op::Fused(
13667 fuel_graph::registry::FusedOps::CONV_TRANSPOSE2D,
13668 fuel_graph::registry::FusedOpParams::ConvTranspose2D {
13669 stride: (1, 1),
13670 padding: (0, 0),
13671 output_padding: (0, 0),
13672 dilation: (1, 1),
13673 groups: 1,
13674 },
13675 ),
13676 inputs: vec![x, w],
13677 shape: Shape::from_dims(&[1, 1, 3, 3]),
13678 dtype: DType::F32,
13679 });
13680 g.set_target_backend(c, BackendId::Cpu);
13681 (x, w, c)
13682 };
13683 let mut inputs = StorageCache::new();
13684 inputs.insert(x_id, Arc::new(RwLock::new(x_storage)));
13685 inputs.insert(w_id, Arc::new(RwLock::new(w_storage)));
13686 let (result_arc, _) = PipelinedExecutor::realize(graph, c_id, inputs).expect("realize");
13687 let guard = result_arc.read().unwrap();
13688 let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
13689 panic!("expected Cpu storage");
13690 };
13691 assert_eq!(
13692 c.as_slice::<f32>().unwrap(),
13693 &[1.0, 3.0, 2.0, 4.0, 10.0, 6.0, 3.0, 7.0, 4.0],
13694 );
13695 }
13696
13697 #[test]
13699 fn pipelined_realize_conv_transpose2d_f64() {
13700 let x_storage = fuel_memory::from_slice_cpu(&[1.0_f64, 2.0, 3.0, 4.0]);
13701 let w_storage = fuel_memory::from_slice_cpu(&[1.0_f64, 1.0, 1.0, 1.0]);
13702 let graph = Arc::new(RwLock::new(Graph::new()));
13703 let (x_id, w_id, c_id) = {
13704 let mut g = graph.write().unwrap();
13705 let x = g.push(Node {
13706 op: Op::Const,
13707 inputs: vec![],
13708 shape: Shape::from_dims(&[1, 1, 2, 2]),
13709 dtype: DType::F64,
13710 });
13711 let w = g.push(Node {
13712 op: Op::Const,
13713 inputs: vec![],
13714 shape: Shape::from_dims(&[1, 1, 2, 2]),
13715 dtype: DType::F64,
13716 });
13717 let c = g.push(Node {
13718 op: Op::Fused(
13719 fuel_graph::registry::FusedOps::CONV_TRANSPOSE2D,
13720 fuel_graph::registry::FusedOpParams::ConvTranspose2D {
13721 stride: (1, 1),
13722 padding: (0, 0),
13723 output_padding: (0, 0),
13724 dilation: (1, 1),
13725 groups: 1,
13726 },
13727 ),
13728 inputs: vec![x, w],
13729 shape: Shape::from_dims(&[1, 1, 3, 3]),
13730 dtype: DType::F64,
13731 });
13732 g.set_target_backend(c, BackendId::Cpu);
13733 (x, w, c)
13734 };
13735 let mut inputs = StorageCache::new();
13736 inputs.insert(x_id, Arc::new(RwLock::new(x_storage)));
13737 inputs.insert(w_id, Arc::new(RwLock::new(w_storage)));
13738 let (result_arc, _) = PipelinedExecutor::realize(graph, c_id, inputs).expect("realize");
13739 let guard = result_arc.read().unwrap();
13740 let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
13741 panic!("expected Cpu storage");
13742 };
13743 assert_eq!(
13744 c.as_slice::<f64>().unwrap(),
13745 &[1.0, 3.0, 2.0, 4.0, 10.0, 6.0, 3.0, 7.0, 4.0],
13746 );
13747 }
13748
13749 #[test]
13751 fn pipelined_realize_conv_transpose2d_bf16() {
13752 let x: Vec<half::bf16> = [1.0_f32, 2.0, 3.0, 4.0]
13753 .iter()
13754 .map(|v| half::bf16::from_f32(*v))
13755 .collect();
13756 let w: Vec<half::bf16> = [1.0_f32, 1.0, 1.0, 1.0]
13757 .iter()
13758 .map(|v| half::bf16::from_f32(*v))
13759 .collect();
13760 let x_storage = fuel_memory::from_slice_cpu(&x);
13761 let w_storage = fuel_memory::from_slice_cpu(&w);
13762 let graph = Arc::new(RwLock::new(Graph::new()));
13763 let (x_id, w_id, c_id) = {
13764 let mut g = graph.write().unwrap();
13765 let x = g.push(Node {
13766 op: Op::Const,
13767 inputs: vec![],
13768 shape: Shape::from_dims(&[1, 1, 2, 2]),
13769 dtype: DType::BF16,
13770 });
13771 let w = g.push(Node {
13772 op: Op::Const,
13773 inputs: vec![],
13774 shape: Shape::from_dims(&[1, 1, 2, 2]),
13775 dtype: DType::BF16,
13776 });
13777 let c = g.push(Node {
13778 op: Op::Fused(
13779 fuel_graph::registry::FusedOps::CONV_TRANSPOSE2D,
13780 fuel_graph::registry::FusedOpParams::ConvTranspose2D {
13781 stride: (1, 1),
13782 padding: (0, 0),
13783 output_padding: (0, 0),
13784 dilation: (1, 1),
13785 groups: 1,
13786 },
13787 ),
13788 inputs: vec![x, w],
13789 shape: Shape::from_dims(&[1, 1, 3, 3]),
13790 dtype: DType::BF16,
13791 });
13792 g.set_target_backend(c, BackendId::Cpu);
13793 (x, w, c)
13794 };
13795 let mut inputs = StorageCache::new();
13796 inputs.insert(x_id, Arc::new(RwLock::new(x_storage)));
13797 inputs.insert(w_id, Arc::new(RwLock::new(w_storage)));
13798 let (result_arc, _) = PipelinedExecutor::realize(graph, c_id, inputs).expect("realize");
13799 let guard = result_arc.read().unwrap();
13800 let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
13801 panic!("expected Cpu storage");
13802 };
13803 let got: Vec<f32> = c
13804 .as_slice::<half::bf16>()
13805 .unwrap()
13806 .iter()
13807 .map(|v| v.to_f32())
13808 .collect();
13809 let want = [1.0_f32, 3.0, 2.0, 4.0, 10.0, 6.0, 3.0, 7.0, 4.0];
13810 for (g, w) in got.iter().zip(want.iter()) {
13811 assert!((g - w).abs() < 0.5, "got {got:?} want {want:?}");
13812 }
13813 }
13814
13815 #[test]
13819 fn pipelined_realize_matmul_gqa() {
13820 let lhs_storage =
13824 fuel_memory::from_slice_cpu(&[1.0_f32, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]);
13825 let rhs_storage = fuel_memory::from_slice_cpu(&[1.0_f32, 0.0, 0.0, 1.0]);
13826
13827 let graph = Arc::new(RwLock::new(Graph::new()));
13828 let (lhs_id, rhs_id, mm_id) = {
13829 let mut g = graph.write().unwrap();
13830 let lhs = g.push(Node {
13831 op: Op::Const,
13832 inputs: vec![],
13833 shape: Shape::from_dims(&[4, 1, 2]),
13834 dtype: DType::F32,
13835 });
13836 let rhs = g.push(Node {
13837 op: Op::Const,
13838 inputs: vec![],
13839 shape: Shape::from_dims(&[2, 2, 1]),
13840 dtype: DType::F32,
13841 });
13842 let mm = g.push(Node {
13843 op: Op::MatMul,
13844 inputs: vec![lhs, rhs],
13845 shape: Shape::from_dims(&[4, 1, 1]),
13846 dtype: DType::F32,
13847 });
13848 g.set_target_backend(mm, BackendId::Cpu);
13849 (lhs, rhs, mm)
13850 };
13851 let mut inputs = StorageCache::new();
13852 inputs.insert(lhs_id, Arc::new(RwLock::new(lhs_storage)));
13853 inputs.insert(rhs_id, Arc::new(RwLock::new(rhs_storage)));
13854
13855 let (result_arc, result_layout) =
13856 PipelinedExecutor::realize(graph, mm_id, inputs).expect("realize");
13857 assert_eq!(result_layout.shape().dims(), &[4, 1, 1]);
13858
13859 let guard = result_arc.read().unwrap();
13860 let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
13861 panic!("expected Cpu storage");
13862 };
13863 assert_eq!(c.as_slice::<f32>().unwrap(), &[1.0, 3.0, 6.0, 8.0]);
13864 }
13865
13866 #[test]
13871 fn pipelined_realize_matmul_with_transposed_rhs() {
13872 let lhs_storage = fuel_memory::from_slice_cpu(&[1.0_f32, 2.0, 3.0, 4.0]);
13877 let rhs_storage = fuel_memory::from_slice_cpu(&[5.0_f32, 6.0, 7.0, 8.0]);
13878
13879 let graph = Arc::new(RwLock::new(Graph::new()));
13880 let (lhs_id, rhs_id, t_id, mm_id) = {
13881 let mut g = graph.write().unwrap();
13882 let lhs = g.push(Node {
13883 op: Op::Const,
13884 inputs: vec![],
13885 shape: Shape::from_dims(&[2, 2]),
13886 dtype: DType::F32,
13887 });
13888 let rhs = g.push(Node {
13889 op: Op::Const,
13890 inputs: vec![],
13891 shape: Shape::from_dims(&[2, 2]),
13892 dtype: DType::F32,
13893 });
13894 let t = g.push(Node {
13895 op: Op::Transpose,
13896 inputs: vec![rhs],
13897 shape: Shape::from_dims(&[2, 2]),
13898 dtype: DType::F32,
13899 });
13900 let mm = g.push(Node {
13901 op: Op::MatMul,
13902 inputs: vec![lhs, t],
13903 shape: Shape::from_dims(&[2, 2]),
13904 dtype: DType::F32,
13905 });
13906 g.set_target_backend(mm, BackendId::Cpu);
13907 (lhs, rhs, t, mm)
13908 };
13909 let _ = t_id;
13910 let mut inputs = StorageCache::new();
13911 inputs.insert(lhs_id, Arc::new(RwLock::new(lhs_storage)));
13912 inputs.insert(rhs_id, Arc::new(RwLock::new(rhs_storage)));
13913
13914 let (result_arc, _) = PipelinedExecutor::realize(graph, mm_id, inputs).expect("realize");
13915 let guard = result_arc.read().unwrap();
13916 let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
13917 panic!("expected Cpu storage");
13918 };
13919 assert_eq!(c.as_slice::<f32>().unwrap(), &[17.0, 23.0, 39.0, 53.0]);
13920 }
13921
13922 #[test]
13926 fn pipelined_realize_reshape_zero_copy_when_contiguous() {
13927 let storage = fuel_memory::from_slice_cpu(&[1.0_f32, 2.0, 3.0, 4.0, 5.0, 6.0]);
13928 let graph = Arc::new(RwLock::new(Graph::new()));
13929 let (in_id, r_id) = {
13930 let mut g = graph.write().unwrap();
13931 let in_id = g.push(Node {
13932 op: Op::Const,
13933 inputs: vec![],
13934 shape: Shape::from_dims(&[2, 3]),
13935 dtype: DType::F32,
13936 });
13937 let r_id = g.push(Node {
13938 op: Op::Reshape(Shape::from_dims(&[3, 2])),
13939 inputs: vec![in_id],
13940 shape: Shape::from_dims(&[3, 2]),
13941 dtype: DType::F32,
13942 });
13943 (in_id, r_id)
13944 };
13945 let mut inputs = StorageCache::new();
13946 let in_arc = Arc::new(RwLock::new(storage));
13947 inputs.insert(in_id, Arc::clone(&in_arc));
13948
13949 let (result_arc, result_layout) =
13950 PipelinedExecutor::realize(graph, r_id, inputs).expect("realize");
13951
13952 assert!(
13954 Arc::ptr_eq(&result_arc, &in_arc),
13955 "contiguous reshape must zero-copy"
13956 );
13957 assert_eq!(result_layout.shape().dims(), &[3, 2]);
13958 assert!(result_layout.is_contiguous());
13959
13960 let guard = result_arc.read().unwrap();
13962 let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
13963 panic!("expected Cpu storage");
13964 };
13965 assert_eq!(
13966 c.as_slice::<f32>().unwrap(),
13967 &[1.0, 2.0, 3.0, 4.0, 5.0, 6.0]
13968 );
13969 }
13970
13971 #[test]
13976 fn pipelined_realize_reshape_materializes_when_strided() {
13977 let storage = fuel_memory::from_slice_cpu(&[1.0_f32, 2.0, 3.0, 4.0, 5.0, 6.0]);
13981 let graph = Arc::new(RwLock::new(Graph::new()));
13982 let (in_id, t_id, r_id) = {
13983 let mut g = graph.write().unwrap();
13984 let in_id = g.push(Node {
13985 op: Op::Const,
13986 inputs: vec![],
13987 shape: Shape::from_dims(&[2, 3]),
13988 dtype: DType::F32,
13989 });
13990 let t_id = g.push(Node {
13991 op: Op::Transpose,
13992 inputs: vec![in_id],
13993 shape: Shape::from_dims(&[3, 2]),
13994 dtype: DType::F32,
13995 });
13996 let r_id = g.push(Node {
13997 op: Op::Reshape(Shape::from_dims(&[6])),
13998 inputs: vec![t_id],
13999 shape: Shape::from_dims(&[6]),
14000 dtype: DType::F32,
14001 });
14002 (in_id, t_id, r_id)
14003 };
14004 let _ = t_id;
14005 let mut inputs = StorageCache::new();
14006 let in_arc = Arc::new(RwLock::new(storage));
14007 inputs.insert(in_id, Arc::clone(&in_arc));
14008
14009 let (result_arc, result_layout) =
14010 PipelinedExecutor::realize(graph, r_id, inputs).expect("realize");
14011
14012 assert!(!Arc::ptr_eq(&result_arc, &in_arc));
14014 assert_eq!(result_layout.shape().dims(), &[6]);
14015 assert!(result_layout.is_contiguous());
14016
14017 let guard = result_arc.read().unwrap();
14019 let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
14020 panic!("expected Cpu storage");
14021 };
14022 assert_eq!(
14023 c.as_slice::<f32>().unwrap(),
14024 &[1.0, 4.0, 2.0, 5.0, 3.0, 6.0]
14025 );
14026 }
14027
14028 #[test]
14036 fn pipelined_realize_transpose_then_sum_dim_e2e() {
14037 let storage = fuel_memory::from_slice_cpu(&[1.0_f32, 2.0, 3.0, 4.0, 5.0, 6.0]);
14041 let graph = Arc::new(RwLock::new(Graph::new()));
14042 let (in_id, t_id, sum_id) = {
14043 let mut g = graph.write().unwrap();
14044 let in_id = g.push(Node {
14045 op: Op::Const,
14046 inputs: vec![],
14047 shape: Shape::from_dims(&[2, 3]),
14048 dtype: DType::F32,
14049 });
14050 let t_id = g.push(Node {
14051 op: Op::Transpose,
14052 inputs: vec![in_id],
14053 shape: Shape::from_dims(&[3, 2]),
14054 dtype: DType::F32,
14055 });
14056 let sum_id = g.push(Node {
14057 op: Op::SumDim(1),
14058 inputs: vec![t_id],
14059 shape: Shape::from_dims(&[3]),
14060 dtype: DType::F32,
14061 });
14062 g.set_target_backend(sum_id, BackendId::Cpu);
14065 (in_id, t_id, sum_id)
14066 };
14067 let _ = t_id;
14068 let mut inputs = StorageCache::new();
14069 inputs.insert(in_id, Arc::new(RwLock::new(storage)));
14070
14071 let (result_arc, result_layout) =
14072 PipelinedExecutor::realize(graph, sum_id, inputs).expect("realize");
14073
14074 assert_eq!(result_layout.shape().dims(), &[3]);
14076 assert!(result_layout.is_contiguous());
14077
14078 let guard = result_arc.read().unwrap();
14079 let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
14080 panic!("expected Cpu storage");
14081 };
14082 let typed: &[f32] = c.as_slice().unwrap();
14083 assert_eq!(typed, &[5.0, 7.0, 9.0]);
14084 }
14085
14086 #[test]
14090 fn pipelined_realize_broadcast_then_add_e2e() {
14091 let bc_input = fuel_memory::from_slice_cpu(&[10.0_f32, 20.0, 30.0]);
14096 let plus_input = fuel_memory::from_slice_cpu(&[1.0_f32, 2.0, 3.0, 4.0, 5.0, 6.0]);
14097 let graph = Arc::new(RwLock::new(Graph::new()));
14098 let (bc_in_id, plus_in_id, b_id, add_id) = {
14099 let mut g = graph.write().unwrap();
14100 let bc_in = g.push(Node {
14101 op: Op::Const,
14102 inputs: vec![],
14103 shape: Shape::from_dims(&[3]),
14104 dtype: DType::F32,
14105 });
14106 let plus_in = g.push(Node {
14107 op: Op::Const,
14108 inputs: vec![],
14109 shape: Shape::from_dims(&[2, 3]),
14110 dtype: DType::F32,
14111 });
14112 let b = g.push(Node {
14113 op: Op::BroadcastTo(Shape::from_dims(&[2, 3])),
14114 inputs: vec![bc_in],
14115 shape: Shape::from_dims(&[2, 3]),
14116 dtype: DType::F32,
14117 });
14118 let add = g.push(Node {
14119 op: Op::Add,
14120 inputs: vec![b, plus_in],
14121 shape: Shape::from_dims(&[2, 3]),
14122 dtype: DType::F32,
14123 });
14124 g.set_target_backend(add, BackendId::Cpu);
14125 (bc_in, plus_in, b, add)
14126 };
14127 let _ = b_id;
14128 let mut inputs = StorageCache::new();
14129 inputs.insert(bc_in_id, Arc::new(RwLock::new(bc_input)));
14130 inputs.insert(plus_in_id, Arc::new(RwLock::new(plus_input)));
14131
14132 let (result_arc, _result_layout) =
14133 PipelinedExecutor::realize(graph, add_id, inputs).expect("realize");
14134 let guard = result_arc.read().unwrap();
14135 let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
14136 panic!("expected Cpu storage");
14137 };
14138 let typed: &[f32] = c.as_slice().unwrap();
14139 assert_eq!(typed, &[11.0, 22.0, 33.0, 14.0, 25.0, 36.0]);
14140 }
14141
14142 #[test]
14146 fn pipelined_realize_sum_dim() {
14147 let storage = fuel_memory::from_slice_cpu(&[1.0_f32, 2.0, 3.0, 4.0, 5.0, 6.0]);
14149 let graph = Arc::new(RwLock::new(Graph::new()));
14150 let (in_id, sum_id) = {
14151 let mut g = graph.write().unwrap();
14152 let in_id = g.push(Node {
14153 op: Op::Const,
14154 inputs: vec![],
14155 shape: Shape::from_dims(&[2, 3]),
14156 dtype: DType::F32,
14157 });
14158 let sum_id = g.push(Node {
14159 op: Op::SumDim(1),
14160 inputs: vec![in_id],
14161 shape: Shape::from_dims(&[2]),
14162 dtype: DType::F32,
14163 });
14164 g.set_target_backend(sum_id, BackendId::Cpu);
14165 (in_id, sum_id)
14166 };
14167 let mut inputs = StorageCache::new();
14168 inputs.insert(in_id, Arc::new(RwLock::new(storage)));
14169
14170 let (result_arc, _) = PipelinedExecutor::realize(graph, sum_id, inputs).expect("realize");
14171 let guard = result_arc.read().unwrap();
14172 if let fuel_memory::BackendStorage::Cpu(c) = &guard.inner {
14173 let typed: &[f32] = c.as_slice().unwrap();
14174 assert_eq!(typed, &[6.0, 15.0]);
14175 }
14176 }
14177
14178 #[test]
14181 fn pipelined_realize_sum_all_rank3() {
14182 let data: Vec<f32> = (1..=24).map(|x| x as f32).collect();
14183 let storage = fuel_memory::from_slice_cpu(&data);
14184 let graph = Arc::new(RwLock::new(Graph::new()));
14185 let (in_id, sum_id) = {
14186 let mut g = graph.write().unwrap();
14187 let in_id = g.push(Node {
14188 op: Op::Const,
14189 inputs: vec![],
14190 shape: Shape::from_dims(&[2, 3, 4]),
14191 dtype: DType::F32,
14192 });
14193 let sum_id = g.push(Node {
14194 op: Op::SumAll,
14195 inputs: vec![in_id],
14196 shape: Shape::from_dims(&[]),
14197 dtype: DType::F32,
14198 });
14199 g.set_target_backend(sum_id, BackendId::Cpu);
14200 (in_id, sum_id)
14201 };
14202 let mut inputs = StorageCache::new();
14203 inputs.insert(in_id, Arc::new(RwLock::new(storage)));
14204
14205 let (result_arc, _) = PipelinedExecutor::realize(graph, sum_id, inputs).expect("realize");
14206 let guard = result_arc.read().unwrap();
14207 if let fuel_memory::BackendStorage::Cpu(c) = &guard.inner {
14208 let typed: &[f32] = c.as_slice().unwrap();
14209 assert_eq!(typed, &[300.0]);
14211 }
14212 }
14213
14214 #[test]
14217 fn pipelined_realize_max_then_mean() {
14218 let storage = fuel_memory::from_slice_cpu(&[1.0_f32, 9.0, 3.0, 4.0, 2.0, 8.0]);
14220 let graph = Arc::new(RwLock::new(Graph::new()));
14221 let (in_id, mean_id) = {
14222 let mut g = graph.write().unwrap();
14223 let in_id = g.push(Node {
14224 op: Op::Const,
14225 inputs: vec![],
14226 shape: Shape::from_dims(&[2, 3]),
14227 dtype: DType::F32,
14228 });
14229 let max_id = g.push(Node {
14230 op: Op::MaxDim(1),
14231 inputs: vec![in_id],
14232 shape: Shape::from_dims(&[2]),
14233 dtype: DType::F32,
14234 });
14235 let mean_id = g.push(Node {
14236 op: Op::MeanDim(0),
14237 inputs: vec![max_id],
14238 shape: Shape::from_dims(&[]),
14239 dtype: DType::F32,
14240 });
14241 g.set_target_backend(max_id, BackendId::Cpu);
14242 g.set_target_backend(mean_id, BackendId::Cpu);
14243 (in_id, mean_id)
14244 };
14245 let mut inputs = StorageCache::new();
14246 inputs.insert(in_id, Arc::new(RwLock::new(storage)));
14247
14248 let (result_arc, _) = PipelinedExecutor::realize(graph, mean_id, inputs).expect("realize");
14249 let guard = result_arc.read().unwrap();
14250 if let fuel_memory::BackendStorage::Cpu(c) = &guard.inner {
14251 let typed: &[f32] = c.as_slice().unwrap();
14252 assert_eq!(typed, &[8.5]);
14254 }
14255 }
14256
14257 #[test]
14262 fn pipelined_realize_sigmoid_then_silu() {
14263 let storage = fuel_memory::from_slice_cpu(&[0.0_f32, 1.0, -1.0]);
14264 let graph = Arc::new(RwLock::new(Graph::new()));
14265 let (in_id, sig_id, silu_id) = {
14266 let mut g = graph.write().unwrap();
14267 let in_id = g.push(Node {
14268 op: Op::Const,
14269 inputs: vec![],
14270 shape: Shape::from_dims(&[3]),
14271 dtype: DType::F32,
14272 });
14273 let sig_id = g.push(Node {
14274 op: Op::Sigmoid,
14275 inputs: vec![in_id],
14276 shape: Shape::from_dims(&[3]),
14277 dtype: DType::F32,
14278 });
14279 let silu_id = g.push(Node {
14281 op: Op::Silu,
14282 inputs: vec![sig_id],
14283 shape: Shape::from_dims(&[3]),
14284 dtype: DType::F32,
14285 });
14286 g.set_target_backend(sig_id, BackendId::Cpu);
14287 g.set_target_backend(silu_id, BackendId::Cpu);
14288 (in_id, sig_id, silu_id)
14289 };
14290 let _ = sig_id;
14291 let mut inputs = StorageCache::new();
14292 inputs.insert(in_id, Arc::new(RwLock::new(storage)));
14293
14294 let (result_arc, _) = PipelinedExecutor::realize(graph, silu_id, inputs).expect("realize");
14295 let guard = result_arc.read().unwrap();
14296 if let fuel_memory::BackendStorage::Cpu(c) = &guard.inner {
14297 let typed: &[f32] = c.as_slice().unwrap();
14298 assert!((typed[0] - 0.5 * (1.0 / (1.0 + (-0.5_f32).exp()))).abs() < 1e-6);
14300 }
14301 }
14302
14303 #[test]
14306 fn pipelined_realize_chained_unary_sqr_then_sqrt() {
14307 let storage = fuel_memory::from_slice_cpu(&[1.0_f32, 4.0, 9.0]);
14308 let graph = Arc::new(RwLock::new(Graph::new()));
14309 let (in_id, sqrt_id) = {
14310 let mut g = graph.write().unwrap();
14311 let in_id = g.push(Node {
14312 op: Op::Const,
14313 inputs: vec![],
14314 shape: Shape::from_dims(&[3]),
14315 dtype: DType::F32,
14316 });
14317 let sqr_id = g.push(Node {
14318 op: Op::Sqr,
14319 inputs: vec![in_id],
14320 shape: Shape::from_dims(&[3]),
14321 dtype: DType::F32,
14322 });
14323 let sqrt_id = g.push(Node {
14324 op: Op::Sqrt,
14325 inputs: vec![sqr_id],
14326 shape: Shape::from_dims(&[3]),
14327 dtype: DType::F32,
14328 });
14329 g.set_target_backend(sqr_id, BackendId::Cpu);
14330 g.set_target_backend(sqrt_id, BackendId::Cpu);
14331 (in_id, sqrt_id)
14332 };
14333 let mut inputs = StorageCache::new();
14334 inputs.insert(in_id, Arc::new(RwLock::new(storage)));
14335
14336 let (result_arc, _) = PipelinedExecutor::realize(graph, sqrt_id, inputs).expect("realize");
14337 let guard = result_arc.read().unwrap();
14338 if let fuel_memory::BackendStorage::Cpu(c) = &guard.inner {
14339 let typed: &[f32] = c.as_slice().unwrap();
14340 assert_eq!(typed, &[1.0, 4.0, 9.0]);
14342 }
14343 }
14344
14345 #[test]
14349 fn pipelined_realize_chained_adds() {
14350 let a_storage = fuel_memory::from_slice_cpu(&[1.0_f32, 2.0]);
14351 let b_storage = fuel_memory::from_slice_cpu(&[10.0_f32, 20.0]);
14352 let c_storage = fuel_memory::from_slice_cpu(&[100.0_f32, 200.0]);
14353
14354 let graph = Arc::new(RwLock::new(Graph::new()));
14355 let (a_id, b_id, c_id, ab_id, abc_id) = {
14356 let mut g = graph.write().unwrap();
14357 let a = g.push(Node {
14358 op: Op::Const,
14359 inputs: vec![],
14360 shape: Shape::from_dims(&[2]),
14361 dtype: DType::F32,
14362 });
14363 let b = g.push(Node {
14364 op: Op::Const,
14365 inputs: vec![],
14366 shape: Shape::from_dims(&[2]),
14367 dtype: DType::F32,
14368 });
14369 let c = g.push(Node {
14370 op: Op::Const,
14371 inputs: vec![],
14372 shape: Shape::from_dims(&[2]),
14373 dtype: DType::F32,
14374 });
14375 let ab = g.push(Node {
14376 op: Op::Add,
14377 inputs: vec![a, b],
14378 shape: Shape::from_dims(&[2]),
14379 dtype: DType::F32,
14380 });
14381 let abc = g.push(Node {
14382 op: Op::Add,
14383 inputs: vec![ab, c],
14384 shape: Shape::from_dims(&[2]),
14385 dtype: DType::F32,
14386 });
14387 g.set_target_backend(ab, BackendId::Cpu);
14388 g.set_target_backend(abc, BackendId::Cpu);
14389 (a, b, c, ab, abc)
14390 };
14391
14392 let mut inputs = StorageCache::new();
14393 inputs.insert(a_id, Arc::new(RwLock::new(a_storage)));
14394 inputs.insert(b_id, Arc::new(RwLock::new(b_storage)));
14395 inputs.insert(c_id, Arc::new(RwLock::new(c_storage)));
14396
14397 let (result_arc, _) = PipelinedExecutor::realize(graph, abc_id, inputs).expect("realize");
14398 let _ = ab_id;
14400
14401 let guard = result_arc.read().unwrap();
14402 if let fuel_memory::BackendStorage::Cpu(c) = &guard.inner {
14403 let typed: &[f32] = c.as_slice().unwrap();
14404 assert_eq!(typed, &[111.0, 222.0]);
14406 }
14407 }
14408
14409 #[test]
14413 fn pipelined_realize_triu_3x3_diag0() {
14414 let storage =
14415 fuel_memory::from_slice_cpu(&[1.0_f32, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]);
14416 let graph = Arc::new(RwLock::new(Graph::new()));
14417 let (in_id, out_id) = {
14418 let mut g = graph.write().unwrap();
14419 let in_id = g.push(Node {
14420 op: Op::Const,
14421 inputs: vec![],
14422 shape: Shape::from_dims(&[3, 3]),
14423 dtype: DType::F32,
14424 });
14425 let out_id = g.push(Node {
14426 op: Op::Triu { diagonal: 0 },
14427 inputs: vec![in_id],
14428 shape: Shape::from_dims(&[3, 3]),
14429 dtype: DType::F32,
14430 });
14431 g.set_target_backend(out_id, BackendId::Cpu);
14432 (in_id, out_id)
14433 };
14434 let mut inputs = StorageCache::new();
14435 inputs.insert(in_id, Arc::new(RwLock::new(storage)));
14436 let (result_arc, _) = PipelinedExecutor::realize(graph, out_id, inputs).expect("realize");
14437 let guard = result_arc.read().unwrap();
14438 let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
14439 panic!("expected Cpu storage");
14440 };
14441 let out: &[f32] = c.as_slice().unwrap();
14442 assert_eq!(out, &[1.0, 2.0, 3.0, 0.0, 5.0, 6.0, 0.0, 0.0, 9.0,]);
14443 }
14444
14445 #[test]
14448 fn pipelined_realize_tril_3x3_diag0_causal_mask() {
14449 let storage =
14450 fuel_memory::from_slice_cpu(&[1.0_f32, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]);
14451 let graph = Arc::new(RwLock::new(Graph::new()));
14452 let (in_id, out_id) = {
14453 let mut g = graph.write().unwrap();
14454 let in_id = g.push(Node {
14455 op: Op::Const,
14456 inputs: vec![],
14457 shape: Shape::from_dims(&[3, 3]),
14458 dtype: DType::F32,
14459 });
14460 let out_id = g.push(Node {
14461 op: Op::Tril { diagonal: 0 },
14462 inputs: vec![in_id],
14463 shape: Shape::from_dims(&[3, 3]),
14464 dtype: DType::F32,
14465 });
14466 g.set_target_backend(out_id, BackendId::Cpu);
14467 (in_id, out_id)
14468 };
14469 let mut inputs = StorageCache::new();
14470 inputs.insert(in_id, Arc::new(RwLock::new(storage)));
14471 let (result_arc, _) = PipelinedExecutor::realize(graph, out_id, inputs).expect("realize");
14472 let guard = result_arc.read().unwrap();
14473 let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
14474 panic!("expected Cpu storage");
14475 };
14476 let out: &[f32] = c.as_slice().unwrap();
14477 assert_eq!(out, &[1.0, 0.0, 0.0, 4.0, 5.0, 0.0, 7.0, 8.0, 9.0,]);
14478 }
14479
14480 #[test]
14487 fn pipelined_realize_log_softmax_last_dim() {
14488 let storage = fuel_memory::from_slice_cpu(&[1.0_f32, 2.0, 3.0, 3.0, 2.0, 1.0]);
14489 let graph = Arc::new(RwLock::new(Graph::new()));
14490 let (in_id, out_id) = {
14491 let mut g = graph.write().unwrap();
14492 let in_id = g.push(Node {
14493 op: Op::Const,
14494 inputs: vec![],
14495 shape: Shape::from_dims(&[2, 3]),
14496 dtype: DType::F32,
14497 });
14498 let out_id = g.push(Node {
14499 op: Op::LogSoftmaxLastDim,
14500 inputs: vec![in_id],
14501 shape: Shape::from_dims(&[2, 3]),
14502 dtype: DType::F32,
14503 });
14504 g.set_target_backend(out_id, BackendId::Cpu);
14505 (in_id, out_id)
14506 };
14507 let mut inputs = StorageCache::new();
14508 inputs.insert(in_id, Arc::new(RwLock::new(storage)));
14509 let (result_arc, _) = PipelinedExecutor::realize(graph, out_id, inputs).expect("realize");
14510 let guard = result_arc.read().unwrap();
14511 let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
14512 panic!("expected Cpu storage");
14513 };
14514 let out: &[f32] = c.as_slice().unwrap();
14515 let expected = [
14516 -2.407_606,
14517 -1.4076059,
14518 -0.40760595_f32,
14519 -0.40760595,
14520 -1.4076059,
14521 -2.407_606,
14522 ];
14523 for (a, b) in out.iter().zip(expected.iter()) {
14524 assert!((a - b).abs() < 1e-5, "log_softmax mismatch: {a} vs {b}");
14525 }
14526 for row in out.chunks(3) {
14528 let sum: f32 = row.iter().map(|x| x.exp()).sum();
14529 assert!(
14530 (sum - 1.0).abs() < 1e-5,
14531 "softmax(log_softmax) row sum != 1: {sum}"
14532 );
14533 }
14534 }
14535
14536 #[test]
14550 fn pipelined_realize_merges_op_release_side_effect_root() {
14551 let a = fuel_memory::from_slice_cpu(&[1.0_f32, 2.0]);
14552 let b = fuel_memory::from_slice_cpu(&[10.0_f32, 20.0]);
14553 let c = fuel_memory::from_slice_cpu(&[100.0_f32, 200.0]);
14554 let graph = Arc::new(RwLock::new(Graph::new()));
14555 let (a_id, b_id, c_id, add_id, release_id) = {
14556 let mut g = graph.write().unwrap();
14557 let a = g.push(Node {
14558 op: Op::Const,
14559 inputs: vec![],
14560 shape: Shape::from_dims(&[2]),
14561 dtype: DType::F32,
14562 });
14563 let b = g.push(Node {
14564 op: Op::Const,
14565 inputs: vec![],
14566 shape: Shape::from_dims(&[2]),
14567 dtype: DType::F32,
14568 });
14569 let c = g.push(Node {
14572 op: Op::Const,
14573 inputs: vec![],
14574 shape: Shape::from_dims(&[2]),
14575 dtype: DType::F32,
14576 });
14577 let add = g.push(Node {
14578 op: Op::Add,
14579 inputs: vec![a, b],
14580 shape: Shape::from_dims(&[2]),
14581 dtype: DType::F32,
14582 });
14583 let release = g.push(Node {
14587 op: Op::Release,
14588 inputs: vec![c],
14589 shape: Shape::from_dims(&[0]),
14590 dtype: DType::F32,
14591 });
14592 g.set_target_backend(add, BackendId::Cpu);
14593 g.set_target_backend(release, BackendId::Cpu);
14594 g.add_side_effect_root(release);
14595 (a, b, c, add, release)
14596 };
14597 let mut inputs = StorageCache::new();
14598 inputs.insert(a_id, Arc::new(RwLock::new(a)));
14599 inputs.insert(b_id, Arc::new(RwLock::new(b)));
14600 let c_arc = Arc::new(RwLock::new(c));
14601 let c_arc_external = Arc::clone(&c_arc);
14602 inputs.insert(c_id, c_arc);
14603
14604 let out = PipelinedExecutor::realize_many(graph, &[add_id], inputs).expect("realize_many");
14607 assert_eq!(out.len(), 1);
14608 let add_guard = out[0].0.read().unwrap();
14609 let fuel_memory::BackendStorage::Cpu(arr) = &add_guard.inner else {
14610 panic!()
14611 };
14612 assert_eq!(arr.as_slice::<f32>().unwrap(), &[11.0, 22.0]);
14613
14614 assert_eq!(
14619 Arc::strong_count(&c_arc_external),
14620 1,
14621 "Op::Release should evict the cache's Arc to its source; \
14622 only the test's external clone should remain",
14623 );
14624 let _ = release_id;
14625 }
14626
14627 #[test]
14631 fn compile_one_emits_release_marker_with_destructive_input() {
14632 let graph_rc = Arc::new(RwLock::new(Graph::new()));
14633 let (src_id, release_id) = {
14634 let mut g = graph_rc.write().unwrap();
14635 let src = g.push(Node {
14636 op: Op::Const,
14637 inputs: vec![],
14638 shape: Shape::from_dims(&[2]),
14639 dtype: DType::F32,
14640 });
14641 let release = g.push(Node {
14642 op: Op::Release,
14643 inputs: vec![src],
14644 shape: Shape::from_dims(&[0]),
14645 dtype: DType::F32,
14646 });
14647 g.set_target_backend(release, BackendId::Cpu);
14648 (src, release)
14649 };
14650 let g = graph_rc.read().unwrap();
14651 let bindings = crate::dispatch::global_bindings();
14652 let mut layout_cache: HashMap<NodeId, Layout> = HashMap::new();
14653 let item = compile_one(
14654 &g,
14655 release_id,
14656 &mut layout_cache,
14657 &bindings,
14658 &SymEnv::default(),
14659 )
14660 .expect("compile_one Op::Release");
14661 assert!(matches!(item.kind, WorkItemKind::ReleaseMarker));
14662 assert_eq!(item.destructive_input, Some(0));
14663 assert_eq!(item.inputs, vec![src_id]);
14664 assert_eq!(
14665 item.elem_count, 0,
14666 "Release output is the zero-element marker"
14667 );
14668 }
14669
14670 #[test]
14677 fn compile_one_emits_move_with_destructive_input() {
14678 let graph_rc = Arc::new(RwLock::new(Graph::new()));
14679 let (src_id, move_id) = {
14680 let mut g = graph_rc.write().unwrap();
14681 let src = g.push(Node {
14682 op: Op::Const,
14683 inputs: vec![],
14684 shape: Shape::from_dims(&[2]),
14685 dtype: DType::F32,
14686 });
14687 let mv = g.push(Node {
14688 op: Op::Move {
14689 target: DeviceLocation::Cpu,
14690 },
14691 inputs: vec![src],
14692 shape: Shape::from_dims(&[2]),
14693 dtype: DType::F32,
14694 });
14695 g.set_target_backend(mv, BackendId::Cpu);
14696 (src, mv)
14697 };
14698 let g = graph_rc.read().unwrap();
14699 let bindings = crate::dispatch::global_bindings();
14700 let mut layout_cache: HashMap<NodeId, Layout> = HashMap::new();
14701 let item = compile_one(
14702 &g,
14703 move_id,
14704 &mut layout_cache,
14705 &bindings,
14706 &SymEnv::default(),
14707 )
14708 .expect("compile_one Op::Move");
14709 assert!(matches!(
14710 item.kind,
14711 WorkItemKind::Move {
14712 target_location: DeviceLocation::Cpu
14713 },
14714 ));
14715 assert_eq!(item.destructive_input, Some(0), "Move destroys its source");
14716 assert_eq!(item.inputs, vec![src_id]);
14717 assert!(
14718 item.compiled.is_some(),
14719 "Move resolves a transfer kernel at (OpKind::Copy, [dt, dt], source backend)",
14720 );
14721 }
14722
14723 #[test]
14731 fn compile_one_resolves_runtime_fused_from_the_sidecar() {
14732 fn sentinel_kernel(
14733 _inputs: &[Arc<RwLock<fuel_memory::Storage>>],
14734 _outputs: &mut [Arc<RwLock<fuel_memory::Storage>>],
14735 _layouts: &[Layout],
14736 _params: &crate::kernel::OpParams,
14737 ) -> Result<()> {
14738 Ok(())
14739 }
14740 let region = fuel_graph::jit::PatternNode::Op {
14743 op: fuel_graph::jit::OpTag::Exp,
14744 attrs: fuel_graph::jit::OpAttrs::default(),
14745 operands: vec![fuel_graph::jit::PatternNode::Op {
14746 op: fuel_graph::jit::OpTag::Sub,
14747 attrs: fuel_graph::jit::OpAttrs::default(),
14748 operands: vec![
14749 fuel_graph::jit::PatternNode::Bind { index: 0 },
14750 fuel_graph::jit::PatternNode::Bind { index: 1 },
14751 ],
14752 }],
14753 };
14754 let rid = crate::runtime_fused_kernels::adopt_runtime_fused(
14755 "test::compile_one::exp_sub",
14756 region,
14757 sentinel_kernel as crate::kernel::KernelRef,
14758 vec![DType::F32, DType::F32, DType::F32],
14759 BackendId::Cpu,
14760 )
14761 .expect("registrable region");
14762
14763 let graph_rc = Arc::new(RwLock::new(Graph::new()));
14764 let fused_id = {
14765 let mut g = graph_rc.write().unwrap();
14766 let s = Shape::from_dims(&[4]);
14767 let a = g.push(Node {
14768 op: Op::Const,
14769 inputs: vec![],
14770 shape: s.clone(),
14771 dtype: DType::F32,
14772 });
14773 let b = g.push(Node {
14774 op: Op::Const,
14775 inputs: vec![],
14776 shape: s.clone(),
14777 dtype: DType::F32,
14778 });
14779 let fused = g.push(Node {
14780 op: Op::Fused(
14781 rid,
14782 fuel_graph::registry::FusedOpParams::Runtime {
14783 scalars: Vec::new(),
14784 },
14785 ),
14786 inputs: vec![a, b],
14787 shape: s,
14788 dtype: DType::F32,
14789 });
14790 g.set_target_backend(fused, BackendId::Cpu);
14792 fused
14793 };
14794 let g = graph_rc.read().unwrap();
14795 let bindings = crate::dispatch::global_bindings();
14796 let mut layout_cache: HashMap<NodeId, Layout> = HashMap::new();
14797 let item = compile_one(
14798 &g,
14799 fused_id,
14800 &mut layout_cache,
14801 &bindings,
14802 &SymEnv::default(),
14803 )
14804 .expect("compile_one runtime fused op");
14805 assert!(matches!(item.kind, WorkItemKind::Kernel));
14806 let compiled = item.compiled.expect("resolved a kernel");
14807 assert_eq!(compiled.op, fuel_ir::dispatch::OpKind::RuntimeFused);
14808 assert_eq!(compiled.backend, BackendId::Cpu);
14809 assert!(
14810 std::ptr::fn_addr_eq(compiled.kernel, sentinel_kernel as crate::kernel::KernelRef),
14811 "the kernel pointer is exactly the adopted one",
14812 );
14813 assert!(
14814 layout_cache.contains_key(&fused_id),
14815 "output layout cached for consumers"
14816 );
14817 }
14818
14819 #[test]
14823 fn compile_one_rejects_runtime_fused_dtype_mismatch() {
14824 fn noop_kernel(
14825 _inputs: &[Arc<RwLock<fuel_memory::Storage>>],
14826 _outputs: &mut [Arc<RwLock<fuel_memory::Storage>>],
14827 _layouts: &[Layout],
14828 _params: &crate::kernel::OpParams,
14829 ) -> Result<()> {
14830 Ok(())
14831 }
14832 let region = fuel_graph::jit::PatternNode::Op {
14833 op: fuel_graph::jit::OpTag::Log,
14834 attrs: fuel_graph::jit::OpAttrs::default(),
14835 operands: vec![fuel_graph::jit::PatternNode::Op {
14836 op: fuel_graph::jit::OpTag::Maximum,
14837 attrs: fuel_graph::jit::OpAttrs::default(),
14838 operands: vec![
14839 fuel_graph::jit::PatternNode::Bind { index: 0 },
14840 fuel_graph::jit::PatternNode::Bind { index: 1 },
14841 ],
14842 }],
14843 };
14844 let rid = crate::runtime_fused_kernels::adopt_runtime_fused(
14846 "test::compile_one::log_maximum",
14847 region,
14848 noop_kernel as crate::kernel::KernelRef,
14849 vec![DType::F32, DType::F32, DType::F32],
14850 BackendId::Cpu,
14851 )
14852 .expect("registrable region");
14853
14854 let graph_rc = Arc::new(RwLock::new(Graph::new()));
14855 let fused_id = {
14856 let mut g = graph_rc.write().unwrap();
14857 let s = Shape::from_dims(&[4]);
14858 let a = g.push(Node {
14860 op: Op::Const,
14861 inputs: vec![],
14862 shape: s.clone(),
14863 dtype: DType::F64,
14864 });
14865 let b = g.push(Node {
14866 op: Op::Const,
14867 inputs: vec![],
14868 shape: s.clone(),
14869 dtype: DType::F64,
14870 });
14871 let fused = g.push(Node {
14872 op: Op::Fused(
14873 rid,
14874 fuel_graph::registry::FusedOpParams::Runtime {
14875 scalars: Vec::new(),
14876 },
14877 ),
14878 inputs: vec![a, b],
14879 shape: s,
14880 dtype: DType::F64,
14881 });
14882 g.set_target_backend(fused, BackendId::Cpu);
14883 fused
14884 };
14885 let g = graph_rc.read().unwrap();
14886 let bindings = crate::dispatch::global_bindings();
14887 let mut layout_cache: HashMap<NodeId, Layout> = HashMap::new();
14888 let err = match compile_one(
14889 &g,
14890 fused_id,
14891 &mut layout_cache,
14892 &bindings,
14893 &SymEnv::default(),
14894 ) {
14895 Ok(_) => panic!("dtype mismatch must be a typed error"),
14896 Err(e) => e,
14897 };
14898 assert!(
14902 matches!(
14903 err,
14904 fuel_ir::Error::NoBackendForOp {
14905 op: fuel_ir::dispatch::OpKind::RuntimeFused,
14906 ..
14907 }
14908 ),
14909 "a dtype mismatch is an honest binding miss: {err}",
14910 );
14911 }
14912
14913 #[test]
14918 fn compile_one_maps_runtime_scalars_to_jit_scalars() {
14919 fn noop_kernel(
14920 _inputs: &[Arc<RwLock<fuel_memory::Storage>>],
14921 _outputs: &mut [Arc<RwLock<fuel_memory::Storage>>],
14922 _layouts: &[Layout],
14923 _params: &crate::kernel::OpParams,
14924 ) -> Result<()> {
14925 Ok(())
14926 }
14927 let region = fuel_graph::jit::PatternNode::Op {
14929 op: fuel_graph::jit::OpTag::Floor,
14930 attrs: fuel_graph::jit::OpAttrs::default(),
14931 operands: vec![fuel_graph::jit::PatternNode::Op {
14932 op: fuel_graph::jit::OpTag::MulScalar,
14933 attrs: fuel_graph::jit::OpAttrs::default(), operands: vec![fuel_graph::jit::PatternNode::Bind { index: 0 }],
14935 }],
14936 };
14937 let rid = crate::runtime_fused_kernels::adopt_runtime_fused(
14938 "test::compile_one::floor_mul_scalar",
14939 region,
14940 noop_kernel as crate::kernel::KernelRef,
14941 vec![DType::F32, DType::F32],
14942 BackendId::Cpu,
14943 )
14944 .expect("slot template registrable");
14945
14946 let graph_rc = Arc::new(RwLock::new(Graph::new()));
14947 let fused_id = {
14948 let mut g = graph_rc.write().unwrap();
14949 let s = Shape::from_dims(&[4]);
14950 let x = g.push(Node {
14951 op: Op::Const,
14952 inputs: vec![],
14953 shape: s.clone(),
14954 dtype: DType::F32,
14955 });
14956 let fused = g.push(Node {
14957 op: Op::Fused(
14958 rid,
14959 fuel_graph::registry::FusedOpParams::Runtime {
14960 scalars: vec![3.5, -1.25],
14961 },
14962 ),
14963 inputs: vec![x],
14964 shape: s,
14965 dtype: DType::F32,
14966 });
14967 g.set_target_backend(fused, BackendId::Cpu);
14968 fused
14969 };
14970 let g = graph_rc.read().unwrap();
14971 let bindings = crate::dispatch::global_bindings();
14972 let mut layout_cache: HashMap<NodeId, Layout> = HashMap::new();
14973 let item = compile_one(
14974 &g,
14975 fused_id,
14976 &mut layout_cache,
14977 &bindings,
14978 &SymEnv::default(),
14979 )
14980 .expect("compile_one runtime fused op with scalars");
14981 let compiled = item.compiled.expect("resolved a kernel");
14982 assert!(
14983 matches!(
14984 &compiled.op_params,
14985 crate::kernel::OpParams::JitScalars { scalars } if scalars == &vec![3.5, -1.25]
14986 ),
14987 "Runtime scalars ride into OpParams::JitScalars, got {:?}",
14988 compiled.op_params,
14989 );
14990 }
14991
14992 #[test]
14999 fn pipelined_op_move_same_device_is_plain_copy() {
15000 let src = fuel_memory::from_slice_cpu(&[1.0_f32, 2.0, 3.0, 4.0]);
15001 let graph = Arc::new(RwLock::new(Graph::new()));
15002 let (src_id, move_id) = {
15003 let mut g = graph.write().unwrap();
15004 let src = g.push(Node {
15005 op: Op::Const,
15006 inputs: vec![],
15007 shape: Shape::from_dims(&[4]),
15008 dtype: DType::F32,
15009 });
15010 let mv = g.push(Node {
15011 op: Op::Move {
15012 target: DeviceLocation::Cpu,
15013 },
15014 inputs: vec![src],
15015 shape: Shape::from_dims(&[4]),
15016 dtype: DType::F32,
15017 });
15018 g.set_target_backend(mv, BackendId::Cpu);
15019 (src, mv)
15020 };
15021 let src_arc = Arc::new(RwLock::new(src));
15022 let src_arc_external = Arc::clone(&src_arc);
15023 let mut inputs = StorageCache::new();
15024 inputs.insert(src_id, src_arc);
15025
15026 let (out, layout) =
15027 PipelinedExecutor::realize(graph, move_id, inputs).expect("realize Op::Move");
15028 let guard = out.read().unwrap();
15029 let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
15030 panic!()
15031 };
15032 assert_eq!(
15033 c.as_slice::<f32>().unwrap(),
15034 &[1.0, 2.0, 3.0, 4.0],
15035 "Op::Move output should carry input's data to target",
15036 );
15037 assert!(layout.is_contiguous());
15038 drop(guard);
15039 assert!(
15040 !Arc::ptr_eq(&out, &src_arc_external),
15041 "same-device Move is a plain copy — fresh storage, not an alias",
15042 );
15043 assert_eq!(
15047 Arc::strong_count(&src_arc_external),
15048 1,
15049 "Op::Move should evict the cache's Arc to its source",
15050 );
15051 }
15052
15053 #[test]
15066 fn pipelined_op_move_multi_consumer_source_not_stranded() {
15067 let a = fuel_memory::from_slice_cpu(&[1.0_f32, 2.0, 3.0, 4.0]);
15068 let graph = Arc::new(RwLock::new(Graph::new()));
15069 let (a_id, out_id) = {
15070 let mut g = graph.write().unwrap();
15071 let a = g.push(Node {
15072 op: Op::Const,
15073 inputs: vec![],
15074 shape: Shape::from_dims(&[4]),
15075 dtype: DType::F32,
15076 });
15077 let b = g.push(Node {
15078 op: Op::Relu,
15079 inputs: vec![a],
15080 shape: Shape::from_dims(&[4]),
15081 dtype: DType::F32,
15082 });
15083 let m = g.push(Node {
15084 op: Op::Move {
15085 target: DeviceLocation::Cpu,
15086 },
15087 inputs: vec![a],
15088 shape: Shape::from_dims(&[4]),
15089 dtype: DType::F32,
15090 });
15091 let out = g.push(Node {
15092 op: Op::Add,
15093 inputs: vec![b, m],
15094 shape: Shape::from_dims(&[4]),
15095 dtype: DType::F32,
15096 });
15097 g.set_target_backend(b, BackendId::Cpu);
15098 g.set_target_backend(m, BackendId::Cpu);
15099 g.set_target_backend(out, BackendId::Cpu);
15100 (a, out)
15101 };
15102 let mut inputs = StorageCache::new();
15103 inputs.insert(a_id, Arc::new(RwLock::new(a)));
15104
15105 let (out, _) = PipelinedExecutor::realize(graph, out_id, inputs)
15106 .expect("realize add(relu(a), move(a))");
15107 let guard = out.read().unwrap();
15108 let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
15109 panic!()
15110 };
15111 assert_eq!(
15112 c.as_slice::<f32>().unwrap(),
15113 &[2.0, 4.0, 6.0, 8.0],
15114 "relu(a) must run before move(a) evicts a",
15115 );
15116 }
15117
15118 #[test]
15123 fn pipelined_op_move_source_in_target_set_not_evicted() {
15124 let a = fuel_memory::from_slice_cpu(&[5.0_f32, 6.0]);
15125 let graph = Arc::new(RwLock::new(Graph::new()));
15126 let (a_id, move_id) = {
15127 let mut g = graph.write().unwrap();
15128 let a = g.push(Node {
15129 op: Op::Const,
15130 inputs: vec![],
15131 shape: Shape::from_dims(&[2]),
15132 dtype: DType::F32,
15133 });
15134 let mv = g.push(Node {
15135 op: Op::Move {
15136 target: DeviceLocation::Cpu,
15137 },
15138 inputs: vec![a],
15139 shape: Shape::from_dims(&[2]),
15140 dtype: DType::F32,
15141 });
15142 g.set_target_backend(mv, BackendId::Cpu);
15143 (a, mv)
15144 };
15145 let a_arc = Arc::new(RwLock::new(a));
15146 let a_arc_external = Arc::clone(&a_arc);
15147 let mut inputs = StorageCache::new();
15148 inputs.insert(a_id, a_arc);
15149
15150 let out = PipelinedExecutor::realize_many(graph, &[move_id, a_id], inputs)
15151 .expect("realize_many [move, source]");
15152 assert_eq!(out.len(), 2);
15153 let mv_guard = out[0].0.read().unwrap();
15154 let fuel_memory::BackendStorage::Cpu(c) = &mv_guard.inner else {
15155 panic!()
15156 };
15157 assert_eq!(c.as_slice::<f32>().unwrap(), &[5.0, 6.0]);
15158 assert!(
15159 Arc::ptr_eq(&out[1].0, &a_arc_external),
15160 "requested source must survive the Move's destructive cleanup",
15161 );
15162 }
15163
15164 #[test]
15169 fn pipelined_realize_many_empty_targets() {
15170 let graph = Arc::new(RwLock::new(Graph::new()));
15171 let inputs = StorageCache::new();
15172 let out = PipelinedExecutor::realize_many(graph, &[], inputs).expect("realize_many");
15173 assert!(out.is_empty());
15174 }
15175
15176 #[test]
15180 fn pipelined_realize_many_two_independent_targets() {
15181 let lhs = fuel_memory::from_slice_cpu(&[1.0_f32, 2.0, 3.0]);
15182 let rhs = fuel_memory::from_slice_cpu(&[10.0_f32, 20.0, 30.0]);
15183 let graph = Arc::new(RwLock::new(Graph::new()));
15184 let (lhs_id, rhs_id, add_id, mul_id) = {
15185 let mut g = graph.write().unwrap();
15186 let lhs = g.push(Node {
15187 op: Op::Const,
15188 inputs: vec![],
15189 shape: Shape::from_dims(&[3]),
15190 dtype: DType::F32,
15191 });
15192 let rhs = g.push(Node {
15193 op: Op::Const,
15194 inputs: vec![],
15195 shape: Shape::from_dims(&[3]),
15196 dtype: DType::F32,
15197 });
15198 let add = g.push(Node {
15199 op: Op::Add,
15200 inputs: vec![lhs, rhs],
15201 shape: Shape::from_dims(&[3]),
15202 dtype: DType::F32,
15203 });
15204 let mul = g.push(Node {
15205 op: Op::Mul,
15206 inputs: vec![lhs, rhs],
15207 shape: Shape::from_dims(&[3]),
15208 dtype: DType::F32,
15209 });
15210 g.set_target_backend(add, BackendId::Cpu);
15211 g.set_target_backend(mul, BackendId::Cpu);
15212 (lhs, rhs, add, mul)
15213 };
15214 let mut inputs = StorageCache::new();
15215 inputs.insert(lhs_id, Arc::new(RwLock::new(lhs)));
15216 inputs.insert(rhs_id, Arc::new(RwLock::new(rhs)));
15217
15218 let out = PipelinedExecutor::realize_many(graph, &[mul_id, add_id], inputs)
15221 .expect("realize_many");
15222 assert_eq!(out.len(), 2);
15223
15224 let mul_guard = out[0].0.read().unwrap();
15225 let fuel_memory::BackendStorage::Cpu(c) = &mul_guard.inner else {
15226 panic!()
15227 };
15228 assert_eq!(c.as_slice::<f32>().unwrap(), &[10.0, 40.0, 90.0]);
15229
15230 let add_guard = out[1].0.read().unwrap();
15231 let fuel_memory::BackendStorage::Cpu(c) = &add_guard.inner else {
15232 panic!()
15233 };
15234 assert_eq!(c.as_slice::<f32>().unwrap(), &[11.0, 22.0, 33.0]);
15235 }
15236
15237 #[test]
15240 fn pipelined_realize_many_duplicate_target_returns_same_arc() {
15241 let storage = fuel_memory::from_slice_cpu(&[1.0_f32, 2.0, 3.0]);
15242 let graph = Arc::new(RwLock::new(Graph::new()));
15243 let (in_id, neg_id) = {
15244 let mut g = graph.write().unwrap();
15245 let in_id = g.push(Node {
15246 op: Op::Const,
15247 inputs: vec![],
15248 shape: Shape::from_dims(&[3]),
15249 dtype: DType::F32,
15250 });
15251 let neg = g.push(Node {
15252 op: Op::Neg,
15253 inputs: vec![in_id],
15254 shape: Shape::from_dims(&[3]),
15255 dtype: DType::F32,
15256 });
15257 g.set_target_backend(neg, BackendId::Cpu);
15258 (in_id, neg)
15259 };
15260 let mut inputs = StorageCache::new();
15261 inputs.insert(in_id, Arc::new(RwLock::new(storage)));
15262
15263 let out = PipelinedExecutor::realize_many(graph, &[neg_id, neg_id], inputs)
15264 .expect("realize_many");
15265 assert_eq!(out.len(), 2);
15266 assert!(
15267 Arc::ptr_eq(&out[0].0, &out[1].0),
15268 "duplicate target should share the same storage Arc",
15269 );
15270 }
15271
15272 #[test]
15276 fn pipelined_realize_many_shared_subgraph_evaluates_once() {
15277 let a_storage = fuel_memory::from_slice_cpu(&[1.0_f32, 2.0, 3.0]);
15278 let b_storage = fuel_memory::from_slice_cpu(&[10.0_f32, 20.0, 30.0]);
15279 let graph = Arc::new(RwLock::new(Graph::new()));
15280 let (a_id, b_id, add_id, mul_id) = {
15281 let mut g = graph.write().unwrap();
15282 let a = g.push(Node {
15283 op: Op::Const,
15284 inputs: vec![],
15285 shape: Shape::from_dims(&[3]),
15286 dtype: DType::F32,
15287 });
15288 let b = g.push(Node {
15289 op: Op::Const,
15290 inputs: vec![],
15291 shape: Shape::from_dims(&[3]),
15292 dtype: DType::F32,
15293 });
15294 let add = g.push(Node {
15295 op: Op::Add,
15296 inputs: vec![a, b],
15297 shape: Shape::from_dims(&[3]),
15298 dtype: DType::F32,
15299 });
15300 let mul = g.push(Node {
15301 op: Op::Mul,
15302 inputs: vec![add, a],
15303 shape: Shape::from_dims(&[3]),
15304 dtype: DType::F32,
15305 });
15306 g.set_target_backend(add, BackendId::Cpu);
15307 g.set_target_backend(mul, BackendId::Cpu);
15308 (a, b, add, mul)
15309 };
15310 let mut inputs = StorageCache::new();
15311 inputs.insert(a_id, Arc::new(RwLock::new(a_storage)));
15312 inputs.insert(b_id, Arc::new(RwLock::new(b_storage)));
15313
15314 let out = PipelinedExecutor::realize_many(graph, &[add_id, mul_id], inputs)
15315 .expect("realize_many");
15316 assert_eq!(out.len(), 2);
15317
15318 let add_guard = out[0].0.read().unwrap();
15319 let fuel_memory::BackendStorage::Cpu(c) = &add_guard.inner else {
15320 panic!()
15321 };
15322 assert_eq!(c.as_slice::<f32>().unwrap(), &[11.0, 22.0, 33.0]);
15323
15324 let mul_guard = out[1].0.read().unwrap();
15325 let fuel_memory::BackendStorage::Cpu(c) = &mul_guard.inner else {
15326 panic!()
15327 };
15328 assert_eq!(c.as_slice::<f32>().unwrap(), &[11.0, 44.0, 99.0]);
15330 }
15331
15332 #[test]
15336 fn pipelined_realize_masked_fill_attention_pattern() {
15337 let x_storage = fuel_memory::from_slice_cpu(&[1.0_f32, 2.0, 3.0, 4.0]);
15338 let mut mask_storage = fuel_memory::from_slice_cpu(&[0u8, 1, 0, 1]);
15339 mask_storage.dtype = DType::Bool; let graph = Arc::new(RwLock::new(Graph::new()));
15341 let (x_id, mask_id, out_id) = {
15342 let mut g = graph.write().unwrap();
15343 let x = g.push(Node {
15344 op: Op::Const,
15345 inputs: vec![],
15346 shape: Shape::from_dims(&[4]),
15347 dtype: DType::F32,
15348 });
15349 let mask = g.push(Node {
15350 op: Op::Const,
15351 inputs: vec![],
15352 shape: Shape::from_dims(&[4]),
15353 dtype: DType::Bool,
15354 });
15355 let out = g.push(Node {
15356 op: Op::MaskedFill {
15357 value: fuel_ir::Scalar::F32(-1000.0),
15358 },
15359 inputs: vec![x, mask],
15360 shape: Shape::from_dims(&[4]),
15361 dtype: DType::F32,
15362 });
15363 g.set_target_backend(out, BackendId::Cpu);
15364 (x, mask, out)
15365 };
15366 let mut inputs = StorageCache::new();
15367 inputs.insert(x_id, Arc::new(RwLock::new(x_storage)));
15368 inputs.insert(mask_id, Arc::new(RwLock::new(mask_storage)));
15369 let (result_arc, _) = PipelinedExecutor::realize(graph, out_id, inputs).expect("realize");
15370 let guard = result_arc.read().unwrap();
15371 let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
15372 panic!("expected Cpu storage");
15373 };
15374 let out: &[f32] = c.as_slice().unwrap();
15375 assert_eq!(out, &[1.0, -1000.0, 3.0, -1000.0]);
15376 }
15377
15378 #[test]
15384 fn pipelined_realize_write_slice_kv_cache_append() {
15385 let dest_storage = fuel_memory::from_slice_cpu(&[0.0_f32; 24]);
15386 let src_storage = fuel_memory::from_slice_cpu(&[1.0_f32, 2.0, 3.0, 4.0, 5.0, 6.0]);
15387 let graph = Arc::new(RwLock::new(Graph::new()));
15388 let (dest_id, src_id, ws_id) = {
15389 let mut g = graph.write().unwrap();
15390 let dest = g.push(Node {
15391 op: Op::Const,
15392 inputs: vec![],
15393 shape: Shape::from_dims(&[4, 3, 2]),
15394 dtype: DType::F32,
15395 });
15396 let src = g.push(Node {
15397 op: Op::Const,
15398 inputs: vec![],
15399 shape: Shape::from_dims(&[1, 3, 2]),
15400 dtype: DType::F32,
15401 });
15402 let ws = g.push(Node {
15403 op: Op::WriteSlice {
15404 ranges: vec![(2, 3), (0, 3), (0, 2)],
15405 dyn_offset: None,
15406 },
15407 inputs: vec![dest, src],
15408 shape: Shape::from_dims(&[4, 3, 2]), dtype: DType::F32,
15410 });
15411 g.set_target_backend(ws, BackendId::Cpu);
15412 (dest, src, ws)
15413 };
15414 let mut inputs = StorageCache::new();
15415 inputs.insert(dest_id, Arc::new(RwLock::new(dest_storage)));
15416 inputs.insert(src_id, Arc::new(RwLock::new(src_storage)));
15417 let (result_arc, _) = PipelinedExecutor::realize(graph, ws_id, inputs).expect("realize");
15418 let guard = result_arc.read().unwrap();
15419 let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
15420 panic!("expected Cpu storage");
15421 };
15422 let out: &[f32] = c.as_slice().unwrap();
15423 let expected = [
15424 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 2.0, 3.0, 4.0, 5.0,
15425 6.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
15427 ];
15428 assert_eq!(out, &expected);
15429 }
15430
15431 #[test]
15436 fn pipelined_realize_write_slice_aliases_dest_storage() {
15437 let dest_storage = fuel_memory::from_slice_cpu(&[0.0_f32; 6]);
15438 let src_storage = fuel_memory::from_slice_cpu(&[10.0_f32, 20.0]);
15439 let graph = Arc::new(RwLock::new(Graph::new()));
15440 let (dest_id, src_id, ws_id, dest_arc_external) = {
15441 let mut g = graph.write().unwrap();
15442 let dest = g.push(Node {
15443 op: Op::Const,
15444 inputs: vec![],
15445 shape: Shape::from_dims(&[3, 2]),
15446 dtype: DType::F32,
15447 });
15448 let src = g.push(Node {
15449 op: Op::Const,
15450 inputs: vec![],
15451 shape: Shape::from_dims(&[1, 2]),
15452 dtype: DType::F32,
15453 });
15454 let ws = g.push(Node {
15455 op: Op::WriteSlice {
15456 ranges: vec![(1, 2), (0, 2)],
15457 dyn_offset: None,
15458 },
15459 inputs: vec![dest, src],
15460 shape: Shape::from_dims(&[3, 2]),
15461 dtype: DType::F32,
15462 });
15463 g.set_target_backend(ws, BackendId::Cpu);
15464 let arc = Arc::new(RwLock::new(dest_storage));
15465 (dest, src, ws, arc)
15466 };
15467 let mut inputs = StorageCache::new();
15468 inputs.insert(dest_id, Arc::clone(&dest_arc_external));
15469 inputs.insert(src_id, Arc::new(RwLock::new(src_storage)));
15470 let (result_arc, _) = PipelinedExecutor::realize(graph, ws_id, inputs).expect("realize");
15471 assert!(
15475 Arc::ptr_eq(&result_arc, &dest_arc_external),
15476 "WriteSlice output Arc must be the same Arc as the destination's"
15477 );
15478 let guard = dest_arc_external.read().unwrap();
15479 let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
15480 panic!("expected Cpu storage");
15481 };
15482 let out: &[f32] = c.as_slice().unwrap();
15483 assert_eq!(out, &[0.0, 0.0, 10.0, 20.0, 0.0, 0.0]);
15484 }
15485
15486 #[test]
15494 fn pipelined_realize_write_slice_dynamic_offset() {
15495 use fuel_ir::{DynScalar, SymId};
15496 let dest_storage = fuel_memory::from_slice_cpu(&[0.0_f32; 6]);
15497 let src_storage = fuel_memory::from_slice_cpu(&[7.0_f32, 8.0]);
15498 let graph = Arc::new(RwLock::new(Graph::new()));
15499 let sym = SymId(0);
15500 let (dest_id, src_id, ws_id) = {
15501 let mut g = graph.write().unwrap();
15502 let dest = g.push(Node {
15503 op: Op::Const,
15504 inputs: vec![],
15505 shape: Shape::from_dims(&[6]),
15506 dtype: DType::F32,
15507 });
15508 let src = g.push(Node {
15509 op: Op::Const,
15510 inputs: vec![],
15511 shape: Shape::from_dims(&[2]),
15512 dtype: DType::F32,
15513 });
15514 let ws = g.push(Node {
15515 op: Op::WriteSlice {
15518 ranges: vec![(0, 2)],
15519 dyn_offset: Some((0, DynScalar::Sym(sym))),
15520 },
15521 inputs: vec![dest, src],
15522 shape: Shape::from_dims(&[6]),
15523 dtype: DType::F32,
15524 });
15525 g.set_target_backend(ws, BackendId::Cpu);
15526 (dest, src, ws)
15527 };
15528 let mut inputs = StorageCache::new();
15529 inputs.insert(dest_id, Arc::new(RwLock::new(dest_storage)));
15530 inputs.insert(src_id, Arc::new(RwLock::new(src_storage)));
15531
15532 let mut env = SymEnv::new();
15534 env.bind(sym, 3).unwrap();
15535
15536 let (result_arc, _) = PipelinedExecutor::realize_with_env(graph, ws_id, inputs, env)
15537 .expect("realize_with_env");
15538 let guard = result_arc.read().unwrap();
15539 let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
15540 panic!("expected Cpu storage");
15541 };
15542 let out: &[f32] = c.as_slice().unwrap();
15543 assert_eq!(
15544 out,
15545 &[0.0, 0.0, 0.0, 7.0, 8.0, 0.0],
15546 "slab must land at the SymEnv-bound offset 3, not the static placeholder 0",
15547 );
15548 }
15549
15550 #[test]
15557 fn pipelined_realize_nonzero_indices_publishes_count() {
15558 use fuel_ir::SymId;
15559 use fuel_ir::storage::{OutputViewSpec, compose_bundle};
15560 let x_storage = fuel_memory::from_slice_cpu(&[0.0_f32, 1.0, 0.0, 1.0, 1.0, 0.0]);
15562 let graph = Arc::new(RwLock::new(Graph::new()));
15563 let count_sym = SymId(0);
15564 let (x_id, nzi_id) = {
15565 let mut g = graph.write().unwrap();
15566 let x = g.push(Node {
15567 op: Op::Const,
15568 inputs: vec![],
15569 shape: Shape::from_dims(&[2, 3]),
15570 dtype: DType::F32,
15571 });
15572 let capacity = 6usize;
15573 let idx_shape = Shape::from_dims(&[capacity]);
15574 let cnt_shape = Shape::from_dims(&[1]);
15575 let specs = vec![
15576 OutputViewSpec {
15577 dtype: DType::U32,
15578 shape: idx_shape.clone(),
15579 layout: Layout::contiguous(idx_shape.clone()),
15580 name: Some("indices"),
15581 },
15582 OutputViewSpec {
15583 dtype: DType::U32,
15584 shape: cnt_shape.clone(),
15585 layout: Layout::contiguous(cnt_shape),
15586 name: Some("count"),
15587 },
15588 ];
15589 let (_bytes, views) = compose_bundle(&specs).unwrap();
15590 let nzi = g.push(Node {
15591 op: Op::NonZeroIndices { count_sym },
15592 inputs: vec![x],
15593 shape: idx_shape,
15594 dtype: DType::U32,
15595 });
15596 g.set_output_views(nzi, Arc::from(views.into_boxed_slice()))
15597 .unwrap();
15598 g.set_target_backend(nzi, BackendId::Cpu);
15599 (x, nzi)
15600 };
15601 let mut inputs = StorageCache::new();
15602 inputs.insert(x_id, Arc::new(RwLock::new(x_storage)));
15603
15604 let (_arc, _layout, produced) =
15605 PipelinedExecutor::realize_with_env_producing(graph, nzi_id, inputs, SymEnv::new())
15606 .expect("realize_with_env_producing");
15607
15608 assert_eq!(
15609 produced.get(count_sym),
15610 Some(3),
15611 "the executor must publish the data-determined nonzero count into the SymEnv",
15612 );
15613 }
15614
15615 #[test]
15618 fn pipelined_realize_write_slice_dynamic_offset_unbound_errors() {
15619 use fuel_ir::{DynScalar, SymId};
15620 let dest_storage = fuel_memory::from_slice_cpu(&[0.0_f32; 6]);
15621 let src_storage = fuel_memory::from_slice_cpu(&[7.0_f32, 8.0]);
15622 let graph = Arc::new(RwLock::new(Graph::new()));
15623 let (dest_id, src_id, ws_id) = {
15624 let mut g = graph.write().unwrap();
15625 let dest = g.push(Node {
15626 op: Op::Const,
15627 inputs: vec![],
15628 shape: Shape::from_dims(&[6]),
15629 dtype: DType::F32,
15630 });
15631 let src = g.push(Node {
15632 op: Op::Const,
15633 inputs: vec![],
15634 shape: Shape::from_dims(&[2]),
15635 dtype: DType::F32,
15636 });
15637 let ws = g.push(Node {
15638 op: Op::WriteSlice {
15639 ranges: vec![(0, 2)],
15640 dyn_offset: Some((0, DynScalar::Sym(SymId(0)))),
15641 },
15642 inputs: vec![dest, src],
15643 shape: Shape::from_dims(&[6]),
15644 dtype: DType::F32,
15645 });
15646 g.set_target_backend(ws, BackendId::Cpu);
15647 (dest, src, ws)
15648 };
15649 let mut inputs = StorageCache::new();
15650 inputs.insert(dest_id, Arc::new(RwLock::new(dest_storage)));
15651 inputs.insert(src_id, Arc::new(RwLock::new(src_storage)));
15652 let result = PipelinedExecutor::realize_with_env(graph, ws_id, inputs, SymEnv::new());
15654 assert!(
15655 result.is_err(),
15656 "unbound dyn_offset symbol must surface a typed error, not a panic",
15657 );
15658 }
15659
15660 #[test]
15663 fn pipelined_realize_write_slice_dynamic_offset_out_of_capacity_errors() {
15664 use fuel_ir::{DynScalar, SymId};
15665 let dest_storage = fuel_memory::from_slice_cpu(&[0.0_f32; 6]);
15666 let src_storage = fuel_memory::from_slice_cpu(&[7.0_f32, 8.0]);
15667 let graph = Arc::new(RwLock::new(Graph::new()));
15668 let sym = SymId(0);
15669 let (dest_id, src_id, ws_id) = {
15670 let mut g = graph.write().unwrap();
15671 let dest = g.push(Node {
15672 op: Op::Const,
15673 inputs: vec![],
15674 shape: Shape::from_dims(&[6]),
15675 dtype: DType::F32,
15676 });
15677 let src = g.push(Node {
15678 op: Op::Const,
15679 inputs: vec![],
15680 shape: Shape::from_dims(&[2]),
15681 dtype: DType::F32,
15682 });
15683 let ws = g.push(Node {
15684 op: Op::WriteSlice {
15685 ranges: vec![(0, 2)],
15686 dyn_offset: Some((0, DynScalar::Sym(sym))),
15687 },
15688 inputs: vec![dest, src],
15689 shape: Shape::from_dims(&[6]),
15690 dtype: DType::F32,
15691 });
15692 g.set_target_backend(ws, BackendId::Cpu);
15693 (dest, src, ws)
15694 };
15695 let mut inputs = StorageCache::new();
15696 inputs.insert(dest_id, Arc::new(RwLock::new(dest_storage)));
15697 inputs.insert(src_id, Arc::new(RwLock::new(src_storage)));
15698 let mut env = SymEnv::new();
15700 env.bind(sym, 5).unwrap();
15701 let result = PipelinedExecutor::realize_with_env(graph, ws_id, inputs, env);
15702 assert!(
15703 result.is_err(),
15704 "resolved slab past destination capacity must surface a typed error",
15705 );
15706 }
15707
15708 #[test]
15724 fn pipelined_inplace_with_multiple_readers_orders_correctly() {
15725 use fuel_ir::Shape;
15726 let data = [1.0_f32, -2.0, 3.0, -4.0];
15727 let src_storage = fuel_memory::from_slice_cpu(&data);
15728
15729 let graph = Arc::new(RwLock::new(Graph::new()));
15730 let (x_id, step_id, sig_id) = {
15731 let mut g = graph.write().unwrap();
15732 let x = g.push(Node {
15733 op: Op::Const,
15734 inputs: vec![],
15735 shape: Shape::from_dims(&[4]),
15736 dtype: DType::F32,
15737 });
15738 let step = g.push(Node {
15739 op: Op::Step,
15740 inputs: vec![x],
15741 shape: Shape::from_dims(&[4]),
15742 dtype: DType::F32,
15743 });
15744 let sig = g.push(Node {
15745 op: Op::SigmoidInplace,
15746 inputs: vec![x],
15747 shape: Shape::from_dims(&[4]),
15748 dtype: DType::F32,
15749 });
15750 g.set_target_backend(step, BackendId::Cpu);
15751 g.set_target_backend(sig, BackendId::Cpu);
15752 (x, step, sig)
15753 };
15754
15755 let mut cache = StorageCache::new();
15756 cache.insert(x_id, Arc::new(RwLock::new(src_storage)));
15757
15758 let results = PipelinedExecutor::realize_many(graph, &[step_id, sig_id], cache)
15759 .expect("realize_many");
15760
15761 let step_guard = results[0].0.read().unwrap();
15765 let fuel_memory::BackendStorage::Cpu(c) = &step_guard.inner else {
15766 panic!("expected Cpu storage for step output");
15767 };
15768 let step_out: &[f32] = c.as_slice().expect("f32 cast");
15769 assert_eq!(
15770 step_out,
15771 &[1.0_f32, 0.0, 1.0, 0.0],
15772 "Step must run before SigmoidInplace; got post-mutation bytes"
15773 );
15774 drop(step_guard);
15775
15776 let sig_guard = results[1].0.read().unwrap();
15779 let fuel_memory::BackendStorage::Cpu(c) = &sig_guard.inner else {
15780 panic!("expected Cpu storage for sigmoid output");
15781 };
15782 let sig_out: &[f32] = c.as_slice().expect("f32 cast");
15783 let sig_ref = [
15784 1.0_f32 / (1.0 + (-1.0_f32).exp()),
15785 1.0_f32 / (1.0 + (2.0_f32).exp()),
15786 1.0_f32 / (1.0 + (-3.0_f32).exp()),
15787 1.0_f32 / (1.0 + (4.0_f32).exp()),
15788 ];
15789 for (got, want) in sig_out.iter().zip(sig_ref.iter()) {
15790 assert!((got - want).abs() < 1e-6, "sigmoid: got {got}, want {want}");
15791 }
15792 }
15793
15794 #[test]
15809 fn pipelined_residual_connection_inserts_safety_copy() {
15810 use fuel_ir::Shape;
15811 let data = [1.0_f32, -2.0, 3.0, -4.0];
15812 let src_storage = fuel_memory::from_slice_cpu(&data);
15813
15814 let graph = Arc::new(RwLock::new(Graph::new()));
15815 let (x_id, _y_id, z_id) = {
15816 let mut g = graph.write().unwrap();
15817 let x = g.push(Node {
15818 op: Op::Const,
15819 inputs: vec![],
15820 shape: Shape::from_dims(&[4]),
15821 dtype: DType::F32,
15822 });
15823 let y = g.push(Node {
15824 op: Op::ReluInplace,
15825 inputs: vec![x],
15826 shape: Shape::from_dims(&[4]),
15827 dtype: DType::F32,
15828 });
15829 let z = g.push(Node {
15830 op: Op::Add,
15831 inputs: vec![y, x],
15832 shape: Shape::from_dims(&[4]),
15833 dtype: DType::F32,
15834 });
15835 g.set_target_backend(y, BackendId::Cpu);
15836 g.set_target_backend(z, BackendId::Cpu);
15837 (x, y, z)
15838 };
15839
15840 let mut cache = StorageCache::new();
15841 cache.insert(x_id, Arc::new(RwLock::new(src_storage)));
15842
15843 let (result_arc, _) = PipelinedExecutor::realize(graph, z_id, cache)
15844 .expect("realize z — insert_safety_copies must break the cycle");
15845
15846 let guard = result_arc.read().unwrap();
15847 let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
15848 panic!("expected Cpu storage");
15849 };
15850 let got: &[f32] = c.as_slice().expect("f32 cast");
15851 assert_eq!(
15852 got,
15853 &[2.0_f32, -2.0, 6.0, -4.0],
15854 "Add must read pre-mutation x via the inserted safety copy"
15855 );
15856 }
15857
15858 fn cpu_const_f32(
15866 graph: &Arc<RwLock<Graph>>,
15867 cache: &mut StorageCache,
15868 data: &[f32],
15869 dims: &[usize],
15870 ) -> NodeId {
15871 let id = {
15872 let mut g = graph.write().unwrap();
15873 g.push(Node {
15874 op: Op::Const,
15875 inputs: vec![],
15876 shape: Shape::from_dims(dims),
15877 dtype: DType::F32,
15878 })
15879 };
15880 cache.insert(id, Arc::new(RwLock::new(fuel_memory::from_slice_cpu(data))));
15881 id
15882 }
15883
15884 #[test]
15889 fn pipelined_realize_selective_scan_bundled_produces_y_and_last_state() {
15890 let graph: Arc<RwLock<Graph>> = Arc::new(RwLock::new(Graph::new()));
15891 let mut cache = StorageCache::new();
15892
15893 let u_id = cpu_const_f32(&graph, &mut cache, &[3.0], &[1, 1, 1]);
15894 let delta_id = cpu_const_f32(&graph, &mut cache, &[1.0], &[1, 1, 1]);
15895 let a_id = cpu_const_f32(&graph, &mut cache, &[-1.0], &[1, 1]);
15896 let b_id = cpu_const_f32(&graph, &mut cache, &[2.0], &[1, 1, 1]);
15897 let c_id = cpu_const_f32(&graph, &mut cache, &[0.5], &[1, 1, 1]);
15898
15899 let (y_id, last_state_id) = {
15900 let u_t = fuel_graph::NodeHandle::from_existing(Arc::clone(&graph), u_id);
15901 let delta_t = fuel_graph::NodeHandle::from_existing(Arc::clone(&graph), delta_id);
15902 let a_t = fuel_graph::NodeHandle::from_existing(Arc::clone(&graph), a_id);
15903 let b_t = fuel_graph::NodeHandle::from_existing(Arc::clone(&graph), b_id);
15904 let c_t = fuel_graph::NodeHandle::from_existing(Arc::clone(&graph), c_id);
15905 let (y, last_state) = u_t
15906 .selective_scan_bundled(&delta_t, &a_t, &b_t, &c_t, false)
15907 .expect("selective_scan_bundled");
15908 (y.id(), last_state.id())
15909 };
15910 {
15912 let mut g = graph.write().unwrap();
15913 let producer = g.node(y_id).inputs[0];
15914 g.set_target_backend(producer, BackendId::Cpu);
15915 }
15916
15917 let outputs =
15918 PipelinedExecutor::realize_many(Arc::clone(&graph), &[y_id, last_state_id], cache)
15919 .expect("realize_many");
15920 let (y_arc, y_layout) = outputs[0].clone();
15921 let (last_state_arc, last_state_layout) = outputs[1].clone();
15922
15923 assert!(
15925 Arc::ptr_eq(&y_arc, &last_state_arc),
15926 "y and last_state Views project the SAME bundled producer Storage",
15927 );
15928
15929 let guard = y_arc.read().unwrap();
15934 let fuel_memory::BackendStorage::Cpu(c_bytes) = &guard.inner else {
15935 panic!("expected Cpu storage");
15936 };
15937 let typed: &[f32] = c_bytes.as_slice().expect("f32 cast");
15938 assert_eq!(y_layout.start_offset(), 0);
15939 let y_value = typed[y_layout.start_offset()];
15940 assert!((y_value - 3.0).abs() < 1e-5, "y expected 3.0 got {y_value}");
15941 assert_eq!(
15942 last_state_layout.start_offset(),
15943 1,
15944 "slot 1 byte_offset 4 / sizeof(f32) = 1",
15945 );
15946 let ls_value = typed[last_state_layout.start_offset()];
15947 assert!(
15948 (ls_value - 6.0).abs() < 1e-5,
15949 "last_state expected 6.0 got {ls_value}"
15950 );
15951 }
15952
15953 #[test]
15964 fn pipelined_write_slice_round2_plain_slice_reader_not_stranded() {
15965 let graph: Arc<RwLock<Graph>> = Arc::new(RwLock::new(Graph::new()));
15966 let mut cache = StorageCache::new();
15967
15968 let b_id = cpu_const_f32(&graph, &mut cache, &[0.0, 0.0, 0.0, 0.0], &[4]);
15969 let src1_id = cpu_const_f32(&graph, &mut cache, &[1.0, 2.0], &[2]);
15970 let src2_id = cpu_const_f32(&graph, &mut cache, &[99.0, 98.0], &[2]);
15971
15972 let (k_id, y_id) = {
15973 let mut g = graph.write().unwrap();
15974 let x_id = g.push(Node {
15975 op: Op::WriteSlice {
15976 ranges: vec![(0, 2)],
15977 dyn_offset: None,
15978 },
15979 inputs: vec![b_id, src1_id],
15980 shape: Shape::from_dims(&[4]),
15981 dtype: DType::F32,
15982 });
15983 let s_id = g.push(Node {
15984 op: Op::Slice {
15985 dim: 0,
15986 start: 0,
15987 len: 2,
15988 },
15989 inputs: vec![x_id],
15990 shape: Shape::from_dims(&[2]),
15991 dtype: DType::F32,
15992 });
15993 let k_id = g.push(Node {
15994 op: Op::Relu,
15995 inputs: vec![s_id],
15996 shape: Shape::from_dims(&[2]),
15997 dtype: DType::F32,
15998 });
15999 let y_id = g.push(Node {
16002 op: Op::WriteSlice {
16003 ranges: vec![(0, 2)],
16004 dyn_offset: None,
16005 },
16006 inputs: vec![x_id, src2_id],
16007 shape: Shape::from_dims(&[4]),
16008 dtype: DType::F32,
16009 });
16010 g.set_target_backend(x_id, BackendId::Cpu);
16011 g.set_target_backend(k_id, BackendId::Cpu);
16012 g.set_target_backend(y_id, BackendId::Cpu);
16013 (k_id, y_id)
16014 };
16015
16016 let plan = fuel_graph::opt::execution_plan(&graph.read().unwrap(), &[k_id, y_id]);
16017 eprintln!("[diagnostic plain-slice] execution_plan = {:?}", plan,);
16018
16019 let outputs = PipelinedExecutor::realize_many(Arc::clone(&graph), &[k_id, y_id], cache)
16020 .expect("realize_many: plain slice reader must not error");
16021 let (k_arc, _) = &outputs[0];
16022 let guard = k_arc.read().unwrap();
16023 let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
16024 panic!("expected Cpu storage");
16025 };
16026 let out: &[f32] = c.as_slice().unwrap();
16027 assert_eq!(
16028 out,
16029 &[1.0, 2.0],
16030 "K = relu(slice(X,0,2)) must read PRE-round-2 bytes; got {:?}",
16031 out,
16032 );
16033 }
16034
16035 #[test]
16052 fn pipelined_write_slice_reshape_adopted_arc_reader_reads_pre_write_bytes() {
16053 let graph: Arc<RwLock<Graph>> = Arc::new(RwLock::new(Graph::new()));
16054 let mut cache = StorageCache::new();
16055
16056 let b_id = cpu_const_f32(&graph, &mut cache, &[0.0, 0.0, 0.0, 0.0], &[4]);
16057 let src1_id = cpu_const_f32(&graph, &mut cache, &[1.0, 2.0], &[2]);
16058 let src2_id = cpu_const_f32(&graph, &mut cache, &[99.0, 98.0], &[2]);
16059
16060 let (x_id, k_id, y_id) = {
16061 let mut g = graph.write().unwrap();
16062 let x_id = g.push(Node {
16063 op: Op::WriteSlice {
16064 ranges: vec![(0, 2)],
16065 dyn_offset: None,
16066 },
16067 inputs: vec![b_id, src1_id],
16068 shape: Shape::from_dims(&[4]),
16069 dtype: DType::F32,
16070 });
16071 let s_id = g.push(Node {
16072 op: Op::Slice {
16075 dim: 0,
16076 start: 0,
16077 len: 2,
16078 },
16079 inputs: vec![x_id],
16080 shape: Shape::from_dims(&[2]),
16081 dtype: DType::F32,
16082 });
16083 let r_id = g.push(Node {
16084 op: Op::Reshape(Shape::from_dims(&[2])),
16085 inputs: vec![s_id],
16086 shape: Shape::from_dims(&[2]),
16087 dtype: DType::F32,
16088 });
16089 let k_id = g.push(Node {
16090 op: Op::Relu,
16091 inputs: vec![r_id],
16092 shape: Shape::from_dims(&[2]),
16093 dtype: DType::F32,
16094 });
16095 let y_id = g.push(Node {
16100 op: Op::WriteSlice {
16101 ranges: vec![(0, 2)],
16102 dyn_offset: None,
16103 },
16104 inputs: vec![x_id, src2_id],
16105 shape: Shape::from_dims(&[4]),
16106 dtype: DType::F32,
16107 });
16108 g.set_target_backend(x_id, BackendId::Cpu);
16109 g.set_target_backend(k_id, BackendId::Cpu);
16110 g.set_target_backend(y_id, BackendId::Cpu);
16111 (x_id, k_id, y_id)
16112 };
16113 let _ = x_id;
16114
16115 let plan = fuel_graph::opt::execution_plan(&graph.read().unwrap(), &[k_id, y_id]);
16116 eprintln!(
16117 "[diagnostic reshape-adopted-arc] execution_plan = {:?}",
16118 plan,
16119 );
16120
16121 let outputs = PipelinedExecutor::realize_many(Arc::clone(&graph), &[k_id, y_id], cache)
16122 .expect("realize_many");
16123 let (k_arc, _) = &outputs[0];
16124 let guard = k_arc.read().unwrap();
16125 let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
16126 panic!("expected Cpu storage");
16127 };
16128 let out: &[f32] = c.as_slice().unwrap();
16129 assert_eq!(
16134 out,
16135 &[1.0, 2.0],
16136 "K must read PRE-round-2 bytes through the adopted Arc; got {:?} \
16137 (post-round-2 corruption if this fails)",
16138 out,
16139 );
16140 }
16141
16142 #[test]
16153 fn pipelined_write_slice_mla_two_layer_round2_reshape_reader_not_stranded() {
16154 let graph: Arc<RwLock<Graph>> = Arc::new(RwLock::new(Graph::new()));
16155 let mut cache = StorageCache::new();
16156
16157 let b_l_id = cpu_const_f32(&graph, &mut cache, &[0.0, 0.0, 0.0, 0.0], &[4]);
16158 let src_l_n_id = cpu_const_f32(&graph, &mut cache, &[1.0, 2.0], &[2]);
16159 let b_l1_id = cpu_const_f32(&graph, &mut cache, &[0.0, 0.0, 0.0, 0.0], &[4]);
16160 let src_l_n1_id = cpu_const_f32(&graph, &mut cache, &[99.0, 98.0], &[2]);
16161
16162 let (x_l_n1_id, x_l1_n_id) = {
16163 let mut g = graph.write().unwrap();
16164 let x_l_n = g.push(Node {
16166 op: Op::WriteSlice {
16167 ranges: vec![(0, 2)],
16168 dyn_offset: None,
16169 },
16170 inputs: vec![b_l_id, src_l_n_id],
16171 shape: Shape::from_dims(&[4]),
16172 dtype: DType::F32,
16173 });
16174 let s_l_n = g.push(Node {
16177 op: Op::Slice {
16178 dim: 0,
16179 start: 0,
16180 len: 2,
16181 },
16182 inputs: vec![x_l_n],
16183 shape: Shape::from_dims(&[2]),
16184 dtype: DType::F32,
16185 });
16186 let rs_l_n = g.push(Node {
16187 op: Op::Reshape(Shape::from_dims(&[2])),
16188 inputs: vec![s_l_n],
16189 shape: Shape::from_dims(&[2]),
16190 dtype: DType::F32,
16191 });
16192 let r_l_n = g.push(Node {
16193 op: Op::Relu,
16194 inputs: vec![rs_l_n],
16195 shape: Shape::from_dims(&[2]),
16196 dtype: DType::F32,
16197 });
16198 let x_l1_n = g.push(Node {
16200 op: Op::WriteSlice {
16201 ranges: vec![(0, 2)],
16202 dyn_offset: None,
16203 },
16204 inputs: vec![b_l1_id, r_l_n],
16205 shape: Shape::from_dims(&[4]),
16206 dtype: DType::F32,
16207 });
16208 let x_l_n1 = g.push(Node {
16212 op: Op::WriteSlice {
16213 ranges: vec![(0, 2)],
16214 dyn_offset: None,
16215 },
16216 inputs: vec![x_l_n, src_l_n1_id],
16217 shape: Shape::from_dims(&[4]),
16218 dtype: DType::F32,
16219 });
16220 g.set_target_backend(x_l_n, BackendId::Cpu);
16221 g.set_target_backend(r_l_n, BackendId::Cpu);
16222 g.set_target_backend(x_l1_n, BackendId::Cpu);
16223 g.set_target_backend(x_l_n1, BackendId::Cpu);
16224 (x_l_n1, x_l1_n)
16225 };
16226
16227 let plan = fuel_graph::opt::execution_plan(&graph.read().unwrap(), &[x_l1_n_id, x_l_n1_id]);
16228 eprintln!("[diagnostic mla-2-layer] execution_plan = {:?}", plan);
16229
16230 let outputs =
16231 PipelinedExecutor::realize_many(Arc::clone(&graph), &[x_l1_n_id, x_l_n1_id], cache)
16232 .expect("realize_many");
16233 let (x_l1_n_arc, _) = &outputs[0];
16236 let guard = x_l1_n_arc.read().unwrap();
16237 let fuel_memory::BackendStorage::Cpu(c) = &guard.inner else {
16238 panic!("expected Cpu storage");
16239 };
16240 let out: &[f32] = c.as_slice().unwrap();
16241 assert_eq!(
16242 out,
16243 &[1.0, 2.0, 0.0, 0.0],
16244 "layer L+1's round-N append must reflect PRE-round-(N+1) attention \
16245 math output; got {:?} (post-round-(N+1) corruption if this fails)",
16246 out,
16247 );
16248 }
16249}