1use crate::analysis::{liveness_analyse, topo_analyse};
13use crate::error::{GraphError, GraphResult};
14use crate::graph::ComputeGraph;
15use crate::node::{KernelConfig, MemcpyDir, NodeId, NodeKind, StreamId};
16use crate::optimizer::{fusion_analyse, memory_analyse, stream_analyse};
17
18#[derive(Debug, Clone, PartialEq)]
24pub enum PlanStep {
25 KernelLaunch {
27 nodes: Vec<NodeId>,
29 function_name: String,
31 config: KernelConfig,
33 stream: StreamId,
35 },
36
37 Memcpy {
39 node: NodeId,
40 dir: MemcpyDir,
41 size_bytes: usize,
42 stream: StreamId,
43 },
44
45 Memset {
47 node: NodeId,
48 size_bytes: usize,
49 value: u8,
50 stream: StreamId,
51 },
52
53 EventRecord {
55 event_id: usize,
57 stream: StreamId,
58 },
59
60 EventWait { event_id: usize, stream: StreamId },
62
63 HostCallback {
65 node: NodeId,
66 label: String,
67 stream: StreamId,
68 },
69
70 Barrier { node: NodeId, stream: StreamId },
72}
73
74impl PlanStep {
75 pub fn stream(&self) -> StreamId {
77 match self {
78 Self::KernelLaunch { stream, .. } => *stream,
79 Self::Memcpy { stream, .. } => *stream,
80 Self::Memset { stream, .. } => *stream,
81 Self::EventRecord { stream, .. } => *stream,
82 Self::EventWait { stream, .. } => *stream,
83 Self::HostCallback { stream, .. } => *stream,
84 Self::Barrier { stream, .. } => *stream,
85 }
86 }
87
88 pub fn tag(&self) -> &'static str {
90 match self {
91 Self::KernelLaunch { .. } => "kernel",
92 Self::Memcpy { .. } => "memcpy",
93 Self::Memset { .. } => "memset",
94 Self::EventRecord { .. } => "event_record",
95 Self::EventWait { .. } => "event_wait",
96 Self::HostCallback { .. } => "host_cb",
97 Self::Barrier { .. } => "barrier",
98 }
99 }
100
101 pub fn is_compute(&self) -> bool {
103 matches!(self, Self::KernelLaunch { .. })
104 }
105}
106
107impl std::fmt::Display for PlanStep {
108 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
109 match self {
110 Self::KernelLaunch {
111 function_name,
112 config,
113 stream,
114 nodes,
115 } => {
116 write!(
117 f,
118 "{} kernel {function_name} [{config}] @{stream} (nodes: {})",
119 self.tag(),
120 nodes
121 .iter()
122 .map(|n| n.to_string())
123 .collect::<Vec<_>>()
124 .join(",")
125 )
126 }
127 Self::Memcpy {
128 dir,
129 size_bytes,
130 stream,
131 ..
132 } => {
133 write!(f, " memcpy {dir} {size_bytes}B @{stream}")
134 }
135 Self::Memset {
136 size_bytes,
137 value,
138 stream,
139 ..
140 } => {
141 write!(f, " memset {size_bytes}B=0x{value:02x} @{stream}")
142 }
143 Self::EventRecord { event_id, stream } => {
144 write!(f, " event_record ev{event_id} @{stream}")
145 }
146 Self::EventWait { event_id, stream } => {
147 write!(f, " event_wait ev{event_id} @{stream}")
148 }
149 Self::HostCallback { label, stream, .. } => {
150 write!(f, " host_cb '{label}' @{stream}")
151 }
152 Self::Barrier { stream, .. } => {
153 write!(f, " barrier @{stream}")
154 }
155 }
156 }
157}
158
159#[derive(Debug, Clone)]
168pub struct ExecutionPlan {
169 pub steps: Vec<PlanStep>,
171 pub num_streams: usize,
173 pub pool_bytes: usize,
175 pub kernel_count_original: usize,
177 pub kernel_count_fused: usize,
179 pub event_count: usize,
181}
182
183impl ExecutionPlan {
184 pub fn build(graph: &ComputeGraph, max_streams: usize) -> GraphResult<Self> {
198 if graph.is_empty() {
199 return Err(GraphError::EmptyGraph);
200 }
201
202 let topo = topo_analyse(graph)?;
204 let _liveness = liveness_analyse(graph)?;
205 let fusion_plan = fusion_analyse(graph)?;
206 let memory_plan = memory_analyse(graph)?;
207 let stream_plan = stream_analyse(graph, max_streams)?;
208
209 let pool_bytes = memory_plan.total_bytes;
210 let num_streams = stream_plan.num_streams;
211 let kernel_count_original = graph.kernel_nodes().len();
212
213 let mut event_counter = 0usize;
215 let mut edge_events: std::collections::HashMap<(NodeId, NodeId), usize> =
217 std::collections::HashMap::new();
218 for sp in &stream_plan.sync_points {
219 let key = (sp.from, sp.to);
220 edge_events.entry(key).or_insert_with(|| {
221 let eid = event_counter;
222 event_counter += 1;
223 eid
224 });
225 }
226
227 let mut event_waits: std::collections::HashMap<NodeId, Vec<usize>> =
230 std::collections::HashMap::new();
231 let mut event_records: std::collections::HashMap<NodeId, Vec<usize>> =
233 std::collections::HashMap::new();
234
235 for sp in &stream_plan.sync_points {
236 let eid = edge_events[&(sp.from, sp.to)];
237 event_records.entry(sp.from).or_default().push(eid);
238 event_waits.entry(sp.to).or_default().push(eid);
239 }
240
241 let fused_name_of = |node_id: NodeId| -> String {
244 let group = fusion_plan.group_of(node_id);
245 match group {
246 Some(g) if !g.is_trivial() => {
247 let member_names: Vec<String> = g
249 .members
250 .iter()
251 .filter_map(|&m| {
252 graph
253 .node(m)
254 .ok()
255 .and_then(|n| n.kind.function_name().map(|s| s.to_owned()))
256 })
257 .collect();
258 format!("fused_{}", member_names.join("_"))
259 }
260 _ => graph
261 .node(node_id)
262 .ok()
263 .and_then(|n| n.kind.function_name())
264 .unwrap_or("unknown")
265 .to_owned(),
266 }
267 };
268
269 let mut emitted_groups: std::collections::HashSet<usize> = std::collections::HashSet::new();
272
273 let mut steps: Vec<PlanStep> = Vec::new();
274 let mut kernel_count_fused = 0;
275
276 for &node_id in &topo.order {
277 let node = graph.node(node_id)?;
278 let stream = stream_plan.stream_of(node_id);
279
280 if let Some(waits) = event_waits.get(&node_id) {
282 for &eid in waits {
283 steps.push(PlanStep::EventWait {
284 event_id: eid,
285 stream,
286 });
287 }
288 }
289
290 match &node.kind {
292 NodeKind::KernelLaunch {
293 function_name,
294 config,
295 ..
296 } => {
297 let group_id = fusion_plan.node_to_group.get(&node_id).copied();
299 let group = group_id.and_then(|gid| fusion_plan.groups.get(gid));
300
301 match group {
302 Some(g) if !g.is_trivial() => {
303 if !emitted_groups.contains(&g.id) {
305 emitted_groups.insert(g.id);
306 let fused_name = fused_name_of(node_id);
307 steps.push(PlanStep::KernelLaunch {
308 nodes: g.members.clone(),
309 function_name: fused_name,
310 config: g.config,
311 stream,
312 });
313 kernel_count_fused += 1;
314 }
315 }
317 _ => {
318 steps.push(PlanStep::KernelLaunch {
320 nodes: vec![node_id],
321 function_name: function_name.clone(),
322 config: *config,
323 stream,
324 });
325 kernel_count_fused += 1;
326 }
327 }
328 }
329 NodeKind::Memcpy { dir, size_bytes } => {
330 steps.push(PlanStep::Memcpy {
331 node: node_id,
332 dir: *dir,
333 size_bytes: *size_bytes,
334 stream,
335 });
336 }
337 NodeKind::Memset { size_bytes, value } => {
338 steps.push(PlanStep::Memset {
339 node: node_id,
340 size_bytes: *size_bytes,
341 value: *value,
342 stream,
343 });
344 }
345 NodeKind::HostCallback { label } => {
346 steps.push(PlanStep::HostCallback {
347 node: node_id,
348 label: label.clone(),
349 stream,
350 });
351 }
352 NodeKind::EventRecord => {
353 steps.push(PlanStep::EventRecord {
355 event_id: event_counter,
356 stream,
357 });
358 event_counter += 1;
359 }
360 NodeKind::EventWait => {
361 steps.push(PlanStep::EventWait {
362 event_id: event_counter,
363 stream,
364 });
365 event_counter += 1;
366 }
367 NodeKind::Barrier | NodeKind::Conditional { .. } => {
368 steps.push(PlanStep::Barrier {
369 node: node_id,
370 stream,
371 });
372 }
373 }
374
375 if let Some(records) = event_records.get(&node_id) {
377 for &eid in records {
378 steps.push(PlanStep::EventRecord {
379 event_id: eid,
380 stream,
381 });
382 }
383 }
384 }
385
386 Ok(ExecutionPlan {
387 steps,
388 num_streams,
389 pool_bytes,
390 kernel_count_original,
391 kernel_count_fused,
392 event_count: event_counter,
393 })
394 }
395
396 pub fn steps_on(&self, stream: StreamId) -> Vec<&PlanStep> {
398 self.steps.iter().filter(|s| s.stream() == stream).collect()
399 }
400
401 pub fn total_steps(&self) -> usize {
403 self.steps.len()
404 }
405
406 pub fn compute_steps(&self) -> usize {
408 self.steps.iter().filter(|s| s.is_compute()).count()
409 }
410}
411
412impl std::fmt::Display for ExecutionPlan {
413 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
414 writeln!(
415 f,
416 "ExecutionPlan: {} steps, {} streams, {} bytes pool, {} kernels (→{} fused), {} events",
417 self.steps.len(),
418 self.num_streams,
419 self.pool_bytes,
420 self.kernel_count_original,
421 self.kernel_count_fused,
422 self.event_count
423 )?;
424 for (i, step) in self.steps.iter().enumerate() {
425 writeln!(f, " [{i:3}] {step}")?;
426 }
427 Ok(())
428 }
429}
430
431#[cfg(test)]
436mod tests {
437 use super::*;
438 use crate::builder::GraphBuilder;
439 use crate::node::MemcpyDir;
440
441 fn build_simple_inference_graph() -> ComputeGraph {
442 let mut b = GraphBuilder::new().with_auto_infer_edges(false);
443 let inp = b.alloc_buffer("input", 4096);
444 let out = b.alloc_buffer("output", 4096);
445 let upload = b.add_memcpy("upload", MemcpyDir::HostToDevice, 4096);
446 let k0 = b
447 .add_kernel("relu", 4, 256, 0)
448 .fusible(true)
449 .inputs([inp])
450 .outputs([out])
451 .finish();
452 let k1 = b
453 .add_kernel("scale", 4, 256, 0)
454 .fusible(true)
455 .inputs([out])
456 .outputs([inp])
457 .finish();
458 let download = b.add_memcpy("download", MemcpyDir::DeviceToHost, 4096);
459 b.chain(&[upload, k0, k1, download]);
460 b.set_outputs(upload, [inp]);
461 b.set_inputs(download, [inp]);
462 b.build().expect("test graph builds successfully")
463 }
464
465 #[test]
466 fn plan_empty_graph_error() {
467 let g = ComputeGraph::new();
468 assert!(matches!(
469 ExecutionPlan::build(&g, 4),
470 Err(GraphError::EmptyGraph)
471 ));
472 }
473
474 #[test]
475 fn plan_builds_without_error() {
476 let g = build_simple_inference_graph();
477 let plan = ExecutionPlan::build(&g, 4).expect("execution plan builds from valid graph");
478 assert!(plan.total_steps() > 0);
479 }
480
481 #[test]
482 fn plan_covers_all_nodes() {
483 let g = build_simple_inference_graph();
484 let plan = ExecutionPlan::build(&g, 4).expect("execution plan builds from valid graph");
485 assert!(plan.total_steps() >= 2); }
489
490 #[test]
491 fn plan_fused_kernels_reduce_compute_steps() {
492 let g = build_simple_inference_graph();
493 let plan = ExecutionPlan::build(&g, 4).expect("execution plan builds from valid graph");
494 assert!(plan.compute_steps() <= 2);
496 assert!(plan.kernel_count_fused <= plan.kernel_count_original);
498 }
499
500 #[test]
501 fn plan_single_stream_no_events() {
502 let mut b = GraphBuilder::new().with_auto_infer_edges(false);
503 let a = b.add_barrier("a");
504 let bnode = b.add_barrier("b");
505 b.dep(a, bnode);
506 let g = b.build().expect("test graph builds successfully");
507 let plan = ExecutionPlan::build(&g, 1).expect("execution plan builds from valid graph");
508 let event_steps: Vec<_> = plan
510 .steps
511 .iter()
512 .filter(|s| matches!(s, PlanStep::EventRecord { .. } | PlanStep::EventWait { .. }))
513 .collect();
514 assert_eq!(event_steps.len(), 0);
517 }
518
519 #[test]
520 fn plan_steps_on_stream() {
521 let mut b = GraphBuilder::new().with_auto_infer_edges(false);
522 b.add_barrier("n");
523 let g = b.build().expect("test graph builds successfully");
524 let plan = ExecutionPlan::build(&g, 1).expect("execution plan builds from valid graph");
525 let on_s0 = plan.steps_on(StreamId(0));
526 assert!(!on_s0.is_empty());
527 }
528
529 #[test]
530 fn plan_display_output() {
531 let g = build_simple_inference_graph();
532 let plan = ExecutionPlan::build(&g, 4).expect("execution plan builds from valid graph");
533 let s = plan.to_string();
534 assert!(s.contains("ExecutionPlan"));
535 assert!(s.contains("streams"));
536 }
537
538 #[test]
539 fn plan_memcpy_and_memset_steps() {
540 let mut b = GraphBuilder::new().with_auto_infer_edges(false);
541 let up = b.add_memcpy("up", MemcpyDir::HostToDevice, 1024);
542 let ms = b.add_memset("zero", 4096, 0x00);
543 b.dep(up, ms);
544 let g = b.build().expect("test graph builds successfully");
545 let plan = ExecutionPlan::build(&g, 1).expect("execution plan builds from valid graph");
546 let has_memcpy = plan
547 .steps
548 .iter()
549 .any(|s| matches!(s, PlanStep::Memcpy { .. }));
550 let has_memset = plan
551 .steps
552 .iter()
553 .any(|s| matches!(s, PlanStep::Memset { .. }));
554 assert!(has_memcpy);
555 assert!(has_memset);
556 }
557
558 #[test]
559 fn plan_pool_bytes_reported() {
560 let g = build_simple_inference_graph();
561 let plan = ExecutionPlan::build(&g, 4).expect("execution plan builds from valid graph");
562 let _ = plan.pool_bytes;
566 }
567
568 #[test]
569 fn plan_kernel_count_fields_valid() {
570 let g = build_simple_inference_graph();
571 let plan = ExecutionPlan::build(&g, 4).expect("execution plan builds from valid graph");
572 assert_eq!(plan.kernel_count_original, 2); assert!(plan.kernel_count_fused <= plan.kernel_count_original);
574 }
575}