use std::collections::BTreeSet;
use crate::error::{GraphError, GraphResult};
use crate::graph::ComputeGraph;
use crate::node::{KernelConfig, NodeId, NodeKind};
#[derive(Debug, Clone, PartialEq)]
pub enum NodeParamUpdate {
Kernel {
function_name: Option<String>,
config: KernelConfig,
},
Memset {
value: u8,
},
}
impl NodeParamUpdate {
#[must_use]
pub fn target_tag(&self) -> &'static str {
match self {
Self::Kernel { .. } => "kernel",
Self::Memset { .. } => "memset",
}
}
}
#[derive(Debug, Clone)]
pub struct ExecGraph {
kinds: Vec<NodeKind>,
edges: BTreeSet<(u32, u32)>,
execution_order: Vec<NodeId>,
}
impl ExecGraph {
pub fn instantiate(graph: &ComputeGraph) -> GraphResult<Self> {
let execution_order = graph.topological_order()?;
let kinds: Vec<NodeKind> = graph.nodes().iter().map(|n| n.kind.clone()).collect();
let edges: BTreeSet<(u32, u32)> =
graph.edges().into_iter().map(|(a, b)| (a.0, b.0)).collect();
Ok(Self {
kinds,
edges,
execution_order,
})
}
#[must_use]
pub fn node_count(&self) -> usize {
self.kinds.len()
}
#[must_use]
pub fn edge_count(&self) -> usize {
self.edges.len()
}
#[must_use]
pub fn execution_order(&self) -> &[NodeId] {
&self.execution_order
}
pub fn node_kind(&self, id: NodeId) -> GraphResult<&NodeKind> {
self.kinds
.get(id.0 as usize)
.ok_or(GraphError::NodeNotFound(id))
}
#[must_use]
pub fn has_edge(&self, from: NodeId, to: NodeId) -> bool {
self.edges.contains(&(from.0, to.0))
}
pub fn update_node(&mut self, id: NodeId, update: NodeParamUpdate) -> GraphResult<()> {
let kind = self
.kinds
.get_mut(id.0 as usize)
.ok_or(GraphError::NodeNotFound(id))?;
match (kind, update) {
(
NodeKind::KernelLaunch {
function_name,
config,
..
},
NodeParamUpdate::Kernel {
function_name: new_name,
config: new_config,
},
) => {
if let Some(name) = new_name {
*function_name = name;
}
*config = new_config;
Ok(())
}
(NodeKind::Memset { value, .. }, NodeParamUpdate::Memset { value: new_value }) => {
*value = new_value;
Ok(())
}
(kind, update) => Err(GraphError::ValidationFailed(format!(
"node {id} is a '{}' node but update targets '{}'",
kind.tag(),
update.target_tag()
))),
}
}
pub fn update(&mut self, new_graph: &ComputeGraph) -> GraphResult<()> {
let diff = self.diff(new_graph)?;
if !diff.is_updatable() {
return Err(GraphError::ValidationFailed(diff.reject_reason()));
}
self.kinds = new_graph.nodes().iter().map(|n| n.kind.clone()).collect();
Ok(())
}
pub fn diff(&self, other: &ComputeGraph) -> GraphResult<ExecGraphDiff> {
if other.is_empty() {
return Err(GraphError::EmptyGraph);
}
if other.node_count() != self.kinds.len() {
return Ok(ExecGraphDiff {
node_count_changed: true,
topology_changed: true,
non_updatable_nodes: Vec::new(),
changed_params: Vec::new(),
});
}
let other_edges: BTreeSet<(u32, u32)> =
other.edges().into_iter().map(|(a, b)| (a.0, b.0)).collect();
let topology_changed = other_edges != self.edges;
let mut non_updatable_nodes = Vec::new();
let mut changed_params = Vec::new();
for (i, new_node) in other.nodes().iter().enumerate() {
let old = &self.kinds[i];
let new = &new_node.kind;
match classify_change(old, new) {
NodeChange::Same => {}
NodeChange::Param => changed_params.push(NodeId(i as u32)),
NodeChange::NonUpdatable => non_updatable_nodes.push(NodeId(i as u32)),
}
}
Ok(ExecGraphDiff {
node_count_changed: false,
topology_changed,
non_updatable_nodes,
changed_params,
})
}
}
enum NodeChange {
Same,
Param,
NonUpdatable,
}
fn classify_change(old: &NodeKind, new: &NodeKind) -> NodeChange {
match (old, new) {
(
NodeKind::KernelLaunch {
function_name: of,
config: oc,
fusible: ob,
},
NodeKind::KernelLaunch {
function_name: nf,
config: nc,
fusible: nb,
},
) => {
if ob != nb {
NodeChange::NonUpdatable
} else if of == nf && oc == nc {
NodeChange::Same
} else {
NodeChange::Param
}
}
(
NodeKind::Memset {
size_bytes: os,
value: ov,
},
NodeKind::Memset {
size_bytes: ns,
value: nv,
},
) => {
if os != ns {
NodeChange::NonUpdatable
} else if ov == nv {
NodeChange::Same
} else {
NodeChange::Param
}
}
(
NodeKind::Memcpy {
dir: od,
size_bytes: os,
},
NodeKind::Memcpy {
dir: nd,
size_bytes: ns,
},
) => {
if od == nd && os == ns {
NodeChange::Same
} else {
NodeChange::NonUpdatable
}
}
(a, b) if a == b => NodeChange::Same,
_ => NodeChange::NonUpdatable,
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ExecGraphDiff {
pub node_count_changed: bool,
pub topology_changed: bool,
pub non_updatable_nodes: Vec<NodeId>,
pub changed_params: Vec<NodeId>,
}
impl ExecGraphDiff {
#[must_use]
pub fn is_identical(&self) -> bool {
!self.node_count_changed
&& !self.topology_changed
&& self.non_updatable_nodes.is_empty()
&& self.changed_params.is_empty()
}
#[must_use]
pub fn is_updatable(&self) -> bool {
!self.node_count_changed && !self.topology_changed && self.non_updatable_nodes.is_empty()
}
#[must_use]
pub fn reject_reason(&self) -> String {
if self.node_count_changed {
"node count differs — topology must match for in-place update".to_owned()
} else if self.topology_changed {
"dependency edges differ — topology must match for in-place update".to_owned()
} else if !self.non_updatable_nodes.is_empty() {
format!(
"{} node(s) changed in a non-updatable way (kind / size / fusibility)",
self.non_updatable_nodes.len()
)
} else {
"update is possible".to_owned()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::builder::GraphBuilder;
use crate::node::MemcpyDir;
fn diamond() -> (ComputeGraph, [NodeId; 4]) {
let mut b = GraphBuilder::new().with_auto_infer_edges(false);
let a = b.add_kernel("a", 4, 256, 0).finish();
let l = b.add_kernel("l", 4, 256, 0).finish();
let r = b.add_kernel("r", 4, 256, 0).finish();
let d = b.add_kernel("d", 4, 256, 0).finish();
b.dep(a, l).dep(a, r).dep(l, d).dep(r, d);
(b.build().expect("diamond builds"), [a, l, r, d])
}
#[test]
fn instantiate_empty_errors() {
let g = ComputeGraph::new();
assert!(matches!(
ExecGraph::instantiate(&g),
Err(GraphError::EmptyGraph)
));
}
#[test]
fn instantiate_snapshots_topology() {
let (g, _) = diamond();
let ex = ExecGraph::instantiate(&g).expect("instantiate");
assert_eq!(ex.node_count(), 4);
assert_eq!(ex.edge_count(), 4);
assert_eq!(ex.execution_order().len(), 4);
}
#[test]
fn execution_order_respects_dependencies() {
let (g, [a, l, r, d]) = diamond();
let ex = ExecGraph::instantiate(&g).expect("instantiate");
let order = ex.execution_order();
let pos = |n: NodeId| order.iter().position(|&x| x == n).expect("present");
assert!(pos(a) < pos(l));
assert!(pos(a) < pos(r));
assert!(pos(l) < pos(d));
assert!(pos(r) < pos(d));
}
#[test]
fn clone_preserves_topology() {
let (g, [a, _l, _r, d]) = diamond();
let ex = ExecGraph::instantiate(&g).expect("instantiate");
let cloned = ex.clone();
assert_eq!(cloned.node_count(), ex.node_count());
assert_eq!(cloned.edge_count(), ex.edge_count());
assert!(cloned.has_edge(a, NodeId(1)));
assert_eq!(cloned.execution_order(), ex.execution_order());
assert!(!cloned.has_edge(d, a));
}
#[test]
fn update_node_kernel_config() {
let (g, [a, ..]) = diamond();
let mut ex = ExecGraph::instantiate(&g).expect("instantiate");
ex.update_node(
a,
NodeParamUpdate::Kernel {
function_name: Some("a_v2".into()),
config: KernelConfig::linear(8, 128, 512),
},
)
.expect("kernel update");
let k = ex.node_kind(a).expect("node a");
match k {
NodeKind::KernelLaunch {
function_name,
config,
..
} => {
assert_eq!(function_name, "a_v2");
assert_eq!(config.grid, (8, 1, 1));
assert_eq!(config.shared_mem_bytes, 512);
}
_ => panic!("expected kernel"),
}
}
#[test]
fn update_node_wrong_kind_rejected() {
let mut b = GraphBuilder::new().with_auto_infer_edges(false);
let z = b.add_memset("zero", 4096, 0);
let g = b.build().expect("builds");
let mut ex = ExecGraph::instantiate(&g).expect("instantiate");
let res = ex.update_node(
z,
NodeParamUpdate::Kernel {
function_name: None,
config: KernelConfig::linear(1, 1, 0),
},
);
assert!(matches!(res, Err(GraphError::ValidationFailed(_))));
}
#[test]
fn update_node_out_of_range() {
let (g, _) = diamond();
let mut ex = ExecGraph::instantiate(&g).expect("instantiate");
let res = ex.update_node(NodeId(99), NodeParamUpdate::Memset { value: 1 });
assert!(matches!(res, Err(GraphError::NodeNotFound(_))));
}
#[test]
fn whole_graph_update_param_only_succeeds() {
let (g0, _) = diamond();
let mut ex = ExecGraph::instantiate(&g0).expect("instantiate");
let mut b = GraphBuilder::new().with_auto_infer_edges(false);
let a = b.add_kernel("a", 16, 64, 0).finish(); let l = b.add_kernel("l", 4, 256, 0).finish();
let r = b.add_kernel("r", 4, 256, 0).finish();
let d = b.add_kernel("d", 4, 256, 0).finish();
b.dep(a, l).dep(a, r).dep(l, d).dep(r, d);
let g1 = b.build().expect("builds");
ex.update(&g1).expect("param-only update should succeed");
match ex.node_kind(a).expect("a") {
NodeKind::KernelLaunch { config, .. } => assert_eq!(config.grid, (16, 1, 1)),
_ => panic!("kernel"),
}
}
#[test]
fn whole_graph_update_topology_change_rejected() {
let (g0, _) = diamond();
let mut ex = ExecGraph::instantiate(&g0).expect("instantiate");
let mut b = GraphBuilder::new().with_auto_infer_edges(false);
let a = b.add_kernel("a", 4, 256, 0).finish();
let l = b.add_kernel("l", 4, 256, 0).finish();
let r = b.add_kernel("r", 4, 256, 0).finish();
let d = b.add_kernel("d", 4, 256, 0).finish();
b.dep(a, l).dep(a, r).dep(l, d); let g1 = b.build().expect("builds");
let res = ex.update(&g1);
assert!(matches!(res, Err(GraphError::ValidationFailed(_))));
}
#[test]
fn whole_graph_update_node_count_change_rejected() {
let (g0, _) = diamond();
let mut ex = ExecGraph::instantiate(&g0).expect("instantiate");
let mut b = GraphBuilder::new().with_auto_infer_edges(false);
let a = b.add_kernel("a", 4, 256, 0).finish();
let l = b.add_kernel("l", 4, 256, 0).finish();
b.dep(a, l);
let g1 = b.build().expect("builds");
let res = ex.update(&g1);
assert!(matches!(res, Err(GraphError::ValidationFailed(_))));
}
#[test]
fn diff_identical_graph() {
let (g, _) = diamond();
let ex = ExecGraph::instantiate(&g).expect("instantiate");
let d = ex.diff(&g).expect("diff");
assert!(d.is_identical());
assert!(d.is_updatable());
assert!(d.changed_params.is_empty());
}
#[test]
fn diff_reports_changed_params() {
let (g0, [a, ..]) = diamond();
let ex = ExecGraph::instantiate(&g0).expect("instantiate");
let mut b = GraphBuilder::new().with_auto_infer_edges(false);
let na = b.add_kernel("a", 99, 1, 0).finish(); let l = b.add_kernel("l", 4, 256, 0).finish();
let r = b.add_kernel("r", 4, 256, 0).finish();
let dd = b.add_kernel("d", 4, 256, 0).finish();
b.dep(na, l).dep(na, r).dep(l, dd).dep(r, dd);
let g1 = b.build().expect("builds");
let diff = ex.diff(&g1).expect("diff");
assert!(diff.is_updatable());
assert!(!diff.is_identical());
assert_eq!(diff.changed_params, vec![a]);
}
#[test]
fn diff_memcpy_size_change_non_updatable() {
let mut b0 = GraphBuilder::new().with_auto_infer_edges(false);
let up0 = b0.add_memcpy("up", MemcpyDir::HostToDevice, 1024);
let k0 = b0.add_kernel("k", 1, 32, 0).finish();
b0.dep(up0, k0);
let g0 = b0.build().expect("builds");
let ex = ExecGraph::instantiate(&g0).expect("instantiate");
let mut b1 = GraphBuilder::new().with_auto_infer_edges(false);
let up1 = b1.add_memcpy("up", MemcpyDir::HostToDevice, 2048); let k1 = b1.add_kernel("k", 1, 32, 0).finish();
b1.dep(up1, k1);
let g1 = b1.build().expect("builds");
let diff = ex.diff(&g1).expect("diff");
assert!(!diff.is_updatable());
assert_eq!(diff.non_updatable_nodes, vec![up0]);
assert!(matches!(ex.diff(&g1).map(|d| d.is_updatable()), Ok(false)));
}
#[test]
fn diff_kind_change_non_updatable() {
let mut b0 = GraphBuilder::new().with_auto_infer_edges(false);
let n0 = b0.add_kernel("k", 1, 32, 0).finish();
let g0 = b0.build().expect("builds");
let ex = ExecGraph::instantiate(&g0).expect("instantiate");
let mut b1 = GraphBuilder::new().with_auto_infer_edges(false);
let _n1 = b1.add_memset("z", 4096, 0); let g1 = b1.build().expect("builds");
let diff = ex.diff(&g1).expect("diff");
assert!(!diff.is_updatable());
assert_eq!(diff.non_updatable_nodes, vec![n0]);
}
#[test]
fn diff_empty_other_errors() {
let (g, _) = diamond();
let ex = ExecGraph::instantiate(&g).expect("instantiate");
let empty = ComputeGraph::new();
assert!(matches!(ex.diff(&empty), Err(GraphError::EmptyGraph)));
}
#[test]
fn param_update_target_tag() {
assert_eq!(NodeParamUpdate::Memset { value: 1 }.target_tag(), "memset");
assert_eq!(
NodeParamUpdate::Kernel {
function_name: None,
config: KernelConfig::linear(1, 1, 0)
}
.target_tag(),
"kernel"
);
}
#[test]
fn stress_large_graph_instantiates_and_diffs() {
const N: usize = 10_000;
let mut b = GraphBuilder::new().with_auto_infer_edges(false);
let ids: Vec<NodeId> = (0..N)
.map(|i| b.add_kernel(&format!("k{i}"), 1, 32, 0).finish())
.collect();
for i in 2..N {
b.dep(ids[i - 1], ids[i]);
b.dep(ids[i - 2], ids[i]);
}
let g = b.build().expect("large graph builds");
let ex = ExecGraph::instantiate(&g).expect("large graph instantiates");
assert_eq!(ex.node_count(), N);
assert_eq!(ex.execution_order().len(), N);
let diff = ex.diff(&g).expect("self-diff");
assert!(diff.is_identical());
}
#[test]
fn update_memset_value() {
let mut b = GraphBuilder::new().with_auto_infer_edges(false);
let z = b.add_memset("z", 4096, 0x00);
let g = b.build().expect("builds");
let mut ex = ExecGraph::instantiate(&g).expect("instantiate");
ex.update_node(z, NodeParamUpdate::Memset { value: 0xff })
.expect("memset update");
match ex.node_kind(z).expect("z") {
NodeKind::Memset { value, .. } => assert_eq!(*value, 0xff),
_ => panic!("memset"),
}
}
}