use crate::graph::{Graph, NodeId};
use crate::{Result, TensorError};
use std::cmp::Reverse;
use std::collections::{BinaryHeap, HashMap};
use super::pass_support::{get_node_inputs, OptimizationPass};
pub struct OperationSchedulingPass {
pub(super) prefer_memory_locality: bool,
pub(super) enable_parallelization: bool,
}
impl OperationSchedulingPass {
pub fn new() -> Self {
Self {
prefer_memory_locality: true,
enable_parallelization: true,
}
}
pub fn with_config(prefer_memory_locality: bool, enable_parallelization: bool) -> Self {
Self {
prefer_memory_locality,
enable_parallelization,
}
}
pub(super) fn compute_dependencies(&self, graph: &Graph) -> HashMap<NodeId, Vec<NodeId>> {
let mut deps = HashMap::new();
for node in graph.nodes() {
let inputs = get_node_inputs(graph, node.id);
deps.insert(node.id, inputs);
}
deps
}
pub(super) fn find_reorderable_operations(
&self,
graph: &Graph,
dependencies: &HashMap<NodeId, Vec<NodeId>>,
) -> Vec<(NodeId, NodeId)> {
let mut reorderable = Vec::new();
let nodes: Vec<_> = graph.nodes().collect();
for i in 0..nodes.len() {
for j in (i + 1)..nodes.len() {
let node_a = nodes[i].id;
let node_b = nodes[j].id;
if self.can_reorder(node_a, node_b, dependencies) {
reorderable.push((node_a, node_b));
}
}
}
reorderable
}
pub(super) fn can_reorder(
&self,
node_a: NodeId,
node_b: NodeId,
dependencies: &HashMap<NodeId, Vec<NodeId>>,
) -> bool {
let deps_a = dependencies
.get(&node_a)
.map(|v| v.as_slice())
.unwrap_or(&[]);
let deps_b = dependencies
.get(&node_b)
.map(|v| v.as_slice())
.unwrap_or(&[]);
!deps_a.contains(&node_b) && !deps_b.contains(&node_a)
}
pub(super) fn schedule_policy(&self) -> SchedulePolicy {
if self.prefer_memory_locality {
SchedulePolicy::Locality
} else if self.enable_parallelization {
SchedulePolicy::Parallelism
} else {
SchedulePolicy::Lexical
}
}
pub(super) fn apply_scheduling_heuristics(&self, graph: &mut Graph) -> Result<bool> {
let optimized = self.compute_schedule(graph, self.schedule_policy())?;
let baseline = self.compute_schedule(graph, SchedulePolicy::Lexical)?;
let current = graph
.topological_order
.clone()
.unwrap_or_else(|| baseline.clone());
if optimized == current {
if graph.topological_order.is_none() {
graph.topological_order = Some(optimized);
}
Ok(false)
} else {
graph.topological_order = Some(optimized);
Ok(true)
}
}
pub(super) fn compute_schedule(
&self,
graph: &Graph,
policy: SchedulePolicy,
) -> Result<Vec<NodeId>> {
let mut in_degree: HashMap<NodeId, usize> = HashMap::new();
let mut adjacency: HashMap<NodeId, Vec<NodeId>> = HashMap::new();
for node in graph.nodes() {
in_degree.insert(node.id, 0);
adjacency.insert(node.id, Vec::new());
}
for edge in graph.edges() {
if edge.is_control {
continue;
}
if let Some(successors) = adjacency.get_mut(&edge.from_node) {
successors.push(edge.to_node);
}
if let Some(degree) = in_degree.get_mut(&edge.to_node) {
*degree += 1;
}
}
for successors in adjacency.values_mut() {
successors.sort_unstable();
}
let out_degree: HashMap<NodeId, usize> = adjacency
.iter()
.map(|(&id, successors)| (id, successors.len()))
.collect();
let node_count = graph.node_count();
let mut result: Vec<NodeId> = Vec::with_capacity(node_count);
match policy {
SchedulePolicy::Lexical => {
let mut ready: BinaryHeap<Reverse<NodeId>> = in_degree
.iter()
.filter(|&(_, °ree)| degree == 0)
.map(|(&id, _)| Reverse(id))
.collect();
while let Some(Reverse(node_id)) = ready.pop() {
result.push(node_id);
for successor in
self.release_ready_successors(node_id, &adjacency, &mut in_degree)
{
ready.push(Reverse(successor));
}
}
}
SchedulePolicy::Parallelism => {
let mut ready: BinaryHeap<(usize, Reverse<NodeId>)> = in_degree
.iter()
.filter(|&(_, °ree)| degree == 0)
.map(|(&id, _)| (*out_degree.get(&id).unwrap_or(&0), Reverse(id)))
.collect();
while let Some((_, Reverse(node_id))) = ready.pop() {
result.push(node_id);
for successor in
self.release_ready_successors(node_id, &adjacency, &mut in_degree)
{
let fan_out = *out_degree.get(&successor).unwrap_or(&0);
ready.push((fan_out, Reverse(successor)));
}
}
}
SchedulePolicy::Locality => {
let mut ready: Vec<NodeId> = in_degree
.iter()
.filter(|&(_, °ree)| degree == 0)
.map(|(&id, _)| id)
.collect();
ready.sort_unstable_by_key(|&id| Reverse(id));
while let Some(node_id) = ready.pop() {
result.push(node_id);
let mut newly =
self.release_ready_successors(node_id, &adjacency, &mut in_degree);
newly.sort_unstable_by_key(|&id| Reverse(id));
ready.extend(newly);
}
}
}
if result.len() != node_count {
return Err(TensorError::invalid_argument(
"Operation scheduling failed: the graph contains a cycle".to_string(),
));
}
Ok(result)
}
pub(super) fn release_ready_successors(
&self,
node_id: NodeId,
adjacency: &HashMap<NodeId, Vec<NodeId>>,
in_degree: &mut HashMap<NodeId, usize>,
) -> Vec<NodeId> {
let mut newly_ready = Vec::new();
if let Some(successors) = adjacency.get(&node_id) {
for &successor in successors {
if let Some(degree) = in_degree.get_mut(&successor) {
*degree -= 1;
if *degree == 0 {
newly_ready.push(successor);
}
}
}
}
newly_ready
}
}
impl OptimizationPass for OperationSchedulingPass {
fn apply(&self, graph: &mut Graph) -> Result<bool> {
let dependencies = self.compute_dependencies(graph);
let reorderable_ops = self.find_reorderable_operations(graph, &dependencies);
if reorderable_ops.is_empty() {
return Ok(false);
}
self.apply_scheduling_heuristics(graph)
}
fn name(&self) -> &str {
"OperationScheduling"
}
fn is_applicable(&self, graph: &Graph) -> bool {
graph.node_count() > 2
}
fn priority(&self) -> u32 {
120
}
}
impl Default for OperationSchedulingPass {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum SchedulePolicy {
Lexical,
Parallelism,
Locality,
}