Files
2026-07-13 13:18:33 +08:00

442 lines
16 KiB
Python

# Copyright (c) Microsoft Corporation.
# SPDX-License-Identifier: Apache-2.0
# DeepSpeed Team
from collections import defaultdict
from typing import List, Dict
from copy import copy
from dataclasses import dataclass
import torch
from torch.fx import Graph, Node
from torch.fx.node import map_arg
try:
from torch.utils._pytree import tree_iter
except ImportError:
pass
from .util import get_last_uses, is_release_node
from .fx import get_output_node
def make_graph_from_schedule(scheduled: List[Node]):
new_graph = Graph()
env = {}
for node in scheduled:
new_node = new_graph.node_copy(node, lambda n: env[n.name])
env[node.name] = new_node
return new_graph
def get_original_args_num(node: Node):
if node.name.startswith("allgather_ds_param") \
or node.name.startswith("release_ds_param") \
or node.name.startswith("wait_allgather_ds_param") \
or node.name.startswith("reduce_ds_param"):
return 1
return len(node.args)
def flat_nodes_in_args(args: List[Node]):
return [a for a in tree_iter(args) if isinstance(a, Node)]
def filter_args(node: Node):
args = node.args[:get_original_args_num(node)]
return flat_nodes_in_args(args)
def init_schedule(graph: Graph):
mem_table = create_mem_table(graph)
remaining_users = defaultdict(set)
user_to_producer = {}
scheduled = []
unscheduled = []
edges = defaultdict(list)
for node in graph.nodes:
filtered_args = filter_args(node)
# print(f"Node: {node} args: {node.args}")
if len(filtered_args) == 0:
scheduled.append(node)
remaining_users[node] = set(node.users.keys())
for user in node.users.keys():
user_to_producer[user] = node
else:
unscheduled.append(node)
for a in filtered_args:
for elem_a in tree_iter(a):
if isinstance(elem_a, Node):
if node not in edges[elem_a]:
edges[elem_a].append(node)
return scheduled, unscheduled, edges, mem_table, remaining_users, user_to_producer
def get_runnable_nodes(scheduled: List[Node], unscheduled: List[Node]):
scheduled = set(scheduled)
return [node for node in unscheduled if all(arg in scheduled for arg in filter_args(node))]
def choose_next_node(scheduled: List[Node], unscheduled: List[Node], mem_table: Dict[str, int]):
runnable_nodes = get_runnable_nodes(scheduled, unscheduled)
# sort by memory usage
runnable_nodes = sorted(runnable_nodes, key=lambda n: mem_table[n.name])
return runnable_nodes[0]
def create_mem_table(graph: Graph) -> Dict[str, int]:
mem_table = {}
for node in graph.nodes:
if node.name.startswith("allgather_ds_param"):
mem_table[node.name] = node.meta["tensor_size"]
elif node.name.startswith("release_ds_param") or node.name.startswith("reduce_ds_param"):
mem_table[node.name] = -node.meta["tensor_size"]
else:
mem_table[node.name] = 0
return mem_table
def list_schedule(graph: Graph) -> Graph:
scheduled, unscheduled, mem_table = init_schedule(graph)
while len(unscheduled) > 0:
next_node = choose_next_node(scheduled, unscheduled, mem_table)
scheduled.append(next_node)
unscheduled.remove(next_node)
return make_graph_from_schedule(scheduled)
###############################
def get_new_runnable_nodes_with(scheduled: List[Node], edges: Dict[Node, List[Node]], new_scheduled: Node):
scheduled = set(scheduled)
new_runnables = []
for node in edges[new_scheduled]:
if all(arg in scheduled for arg in filter_args(node) if arg != new_scheduled):
new_runnables.append(node)
return new_runnables
def _do_schedule_without_allgather(scheduled: List[Node], unscheduled: List[Node], edges: Dict[Node, List[Node]],
non_ag_runnable: List[Node]):
while len(non_ag_runnable) > 0:
next_node = non_ag_runnable.pop()
new_runnables = get_new_runnable_nodes_with(scheduled, edges, next_node)
non_ag_runnable += [n for n in new_runnables if not n.name.startswith("allgather_ds_param")]
scheduled.append(next_node)
unscheduled.remove(next_node)
return scheduled, unscheduled
def schedule_without_allgather(scheduled: List[Node], unscheduled: List[Node], edges: Dict[Node, List[Node]]):
runnable = get_runnable_nodes(scheduled, unscheduled)
non_ag_runnable = [n for n in runnable if not n.name.startswith("allgather_ds_param")]
tmp_scheduled = copy(scheduled)
tmp_unscheduled = copy(unscheduled)
return _do_schedule_without_allgather(tmp_scheduled, tmp_unscheduled, edges, non_ag_runnable)
def try_schedule_with_new_allgather(scheduled: List[Node], unscheduled: List[Node], edges: Dict[Node, List[Node]],
new_scheduled: Node):
new_runnables = get_new_runnable_nodes_with(scheduled, edges, new_scheduled)
non_ag_runnable = [n for n in new_runnables if not n.name.startswith("allgather_ds_param")]
tmp_scheduled = copy(scheduled)
tmp_unscheduled = copy(unscheduled)
tmp_scheduled.append(new_scheduled)
tmp_unscheduled.remove(new_scheduled)
return _do_schedule_without_allgather(tmp_scheduled, tmp_unscheduled, edges, non_ag_runnable)
def simple_prefetch(graph: Graph, available_mem: int, output_size: int, debug_log: bool) -> Graph:
scheduled, unscheduled, edges, mem_table, remaining_users, user_to_producer = init_schedule(graph)
tmp_scheduled, tmp_unscheduled = schedule_without_allgather(scheduled, unscheduled, edges)
while len(tmp_unscheduled) > 0:
runnable = get_runnable_nodes(tmp_scheduled, tmp_unscheduled)
ag_with_unblock_time = []
for ag_node in runnable:
ag_scheduled, ag_unscheduled = try_schedule_with_new_allgather(tmp_scheduled, tmp_unscheduled, edges,
ag_node)
unblock_time = sum(n.meta["device_time"] for n in ag_scheduled[len(tmp_scheduled) + 1:])
ag_with_unblock_time.append((ag_node, unblock_time, ag_scheduled, ag_unscheduled))
ag_with_unblock_time = sorted(ag_with_unblock_time, key=lambda x: x[1], reverse=True)
best_ag_node = ag_with_unblock_time[0][0]
best_ag_scheduled = ag_with_unblock_time[0][2]
no_ag_runnables = tmp_scheduled[len(scheduled):]
after_ag_runnables = best_ag_scheduled[len(tmp_scheduled) + 1:]
scheduled.append(best_ag_node)
unscheduled.remove(best_ag_node)
for n in no_ag_runnables:
scheduled.append(n)
unscheduled.remove(n)
tmp_scheduled = copy(scheduled)
tmp_unscheduled = copy(unscheduled)
for n in after_ag_runnables:
tmp_scheduled.append(n)
tmp_unscheduled.remove(n)
return make_graph_from_schedule(tmp_scheduled)
###############################
def init_schedule_with_placeholders(graph: Graph):
mem_table = create_mem_table(graph)
remaining_users = defaultdict(set)
user_to_producer = {}
scheduled = []
unscheduled = []
edges = defaultdict(list)
for node in graph.nodes:
if node.op == 'placeholder':
scheduled.append(node)
remaining_users[node] = set(node.users.keys())
for user in node.users.keys():
user_to_producer[user] = node
else:
unscheduled.append(node)
return scheduled, unscheduled, edges, mem_table, remaining_users, user_to_producer
def get_node_requirements(target_node: Node, scheduled: List[Node]):
scheduled = set(scheduled)
visited = set()
ordered_nodes = []
def dfs(node: Node):
if node in scheduled:
return
if node in visited:
return
visited.add(node)
args = []
def register_arg(n: Node):
args.append(n)
map_arg(node.args, register_arg)
for arg in args:
dfs(arg)
ordered_nodes.append(node)
dfs(target_node)
return ordered_nodes
@dataclass
class AllgatherTask:
node: Node
allgather_cost: float
free_cost: float
allgathered_mem: int
allgather_acc_mem: int
free_acc_mem: int
last_use: Node
n_scheduled_ags: int
schedule_until_ag: List[Node]
schedule_until_free: List[Node]
def _free_path_allgather_key(task: AllgatherTask):
return (task.n_scheduled_ags, task.allgather_acc_mem, task.free_cost, task.node.name)
def _fallback_allgather_key(task: AllgatherTask):
return (task.free_acc_mem, task.n_scheduled_ags, task.allgather_acc_mem, task.free_cost, task.node.name)
def fast_free_schedule(graph: Graph, available_mem: int, output_size: int, debug_log: bool) -> Graph:
node_to_last_use, user_to_last_uses = get_last_uses(graph)
# check tensor size
for node in graph.nodes:
if "tensor_size" not in node.meta:
# Our profiler may not visit all nodes because of the control flow.
node.meta["tensor_size"] = 0
scheduled, unscheduled, edges, mem_table, remaining_users, user_to_producer = init_schedule_with_placeholders(
graph)
unscheduled_ags = [n for n in unscheduled if n.target == torch.ops.dc.allgather_param.default]
release_nodes = defaultdict(list)
for n in unscheduled:
if is_release_node(n):
release_nodes[n.args[2]].append(n)
ag_nodes_in_path = {}
for ag_node in unscheduled_ags:
last_use = node_to_last_use[ag_node]
required_nodes = get_node_requirements(last_use, scheduled)
ag_nodes_in_path[ag_node] = set(n for n in required_nodes if n.target == torch.ops.dc.allgather_param.default)
reduce_nodes = [n for n in unscheduled if n.target == torch.ops.dc.reduce_grad.default]
ag_nodes_in_path_to_reduce_nodes = {}
for reduce_node in reduce_nodes:
ag_nodes_in_path_to_reduce_nodes[reduce_node] = set(n for n in get_node_requirements(reduce_node, scheduled)
if n.target == torch.ops.dc.allgather_param.default)
output_nodes = [
n for n in get_output_node(graph).args[0]
if isinstance(n, Node) and n.target != torch.ops.dc.reduce_grad.default
]
ag_nodes_in_path_to_output_nodes = {}
for output_node in output_nodes:
ag_nodes_in_path_to_output_nodes[output_node] = set(n for n in get_node_requirements(output_node, scheduled)
if n.target == torch.ops.dc.allgather_param.default)
while len(unscheduled_ags) > 0:
ag_nodes_count = {ag_node: len(nodes) for ag_node, nodes in ag_nodes_in_path.items()}
count_list = sorted(set(ag_nodes_count.values()))
runnable_ags = []
for ag_count in count_list:
target_unscheduled_ags = [ag for ag in unscheduled_ags if ag_nodes_count[ag] == ag_count]
for node in target_unscheduled_ags:
ds_id = node.args[2]
schedule_until_ag = get_node_requirements(node, scheduled)
if schedule_until_ag is None:
continue
last_use = node_to_last_use[node]
diff_required_nodes = get_node_requirements(last_use, scheduled + schedule_until_ag)
allgather_cost = sum(n.meta["device_time"] for n in schedule_until_ag)
free_cost = sum(n.meta["device_time"] for n in diff_required_nodes)
allgathered_mem = node.meta["tensor_size"]
allgather_acc_mem = sum(n.meta["tensor_size"] for n in schedule_until_ag
if n.target == torch.ops.dc.allgather_param.default)
free_acc_mem = sum(n.meta["tensor_size"] for n in diff_required_nodes
if n.target == torch.ops.dc.allgather_param.default)
schedule_until_free = schedule_until_ag + diff_required_nodes
for release_node in release_nodes[ds_id]:
for release_dep_node in get_node_requirements(release_node, scheduled + schedule_until_free):
if release_dep_node not in schedule_until_free:
schedule_until_free.append(release_dep_node)
n_scheduled_ags = len(
[n for n in schedule_until_free if n.target == torch.ops.dc.allgather_param.default])
task = AllgatherTask(node, allgather_cost, free_cost, allgathered_mem, allgather_acc_mem, free_acc_mem,
last_use, n_scheduled_ags, schedule_until_ag, schedule_until_free)
# print(f" ag_count {ag_count} allgather runnable {i}: {node} last_use: {node_to_last_use[node]} t: {t2-t1:.2f}")
runnable_ags.append(task)
if len(runnable_ags) > 0:
break
assert len(runnable_ags) > 0, "No runnable allgather nodes"
# Criteria of the choice:
# We want to choose allgather that does not require additional allgather until releasing the param.
# When we can find such a node, free_acc_mem will be zero. In that case, we choose the one with the smallest cost until free to minimize the period of occupying memory for the gathered param.
# If there is no such node, we choose the one with the smallest free_cost to minimize the period of occupying memory for the gathered param.
ags_with_no_additional_ag = [ag for ag in runnable_ags if ag.free_acc_mem == 0]
if len(ags_with_no_additional_ag) > 0:
sorted_ags = sorted(ags_with_no_additional_ag, key=_free_path_allgather_key)
next_ag = sorted_ags[0]
assert not debug_log or next_ag.free_acc_mem == 0
nodes_to_schedule = next_ag.schedule_until_free
else:
# sorted_ags = sorted(runnable_ags, key=lambda x: x.allgathered_mem)
sorted_ags = sorted(runnable_ags, key=_fallback_allgather_key)
next_ag = sorted_ags[0]
nodes_to_schedule = next_ag.schedule_until_ag
# print(f" next_ag {next_ag}")
for n in nodes_to_schedule:
scheduled.append(n)
unscheduled.remove(n)
unscheduled_ags.remove(next_ag.node)
ag_nodes_in_path.pop(next_ag.node)
for ag_node, nodes in ag_nodes_in_path.items():
if next_ag.node in nodes:
nodes.remove(next_ag.node)
# Schedule reduce nodes when possible to free memory earlier
reduces_to_schedule = []
for reduce_node in reduce_nodes:
if next_ag.node in ag_nodes_in_path_to_reduce_nodes[reduce_node]:
ag_nodes_in_path_to_reduce_nodes[reduce_node].remove(next_ag.node)
if len(ag_nodes_in_path_to_reduce_nodes[reduce_node]) == 0:
reduces_to_schedule.append(reduce_node)
for n in reduces_to_schedule:
need_to_schedule = get_node_requirements(n, scheduled)
for nn in need_to_schedule:
scheduled.append(nn)
unscheduled.remove(nn)
# Do the same for output nodes
outputs_to_schedule = []
for output_node in output_nodes:
if next_ag.node in ag_nodes_in_path_to_output_nodes[output_node]:
ag_nodes_in_path_to_output_nodes[output_node].remove(next_ag.node)
if len(ag_nodes_in_path_to_output_nodes[output_node]) == 0:
outputs_to_schedule.append(output_node)
for n in outputs_to_schedule:
need_to_schedule = get_node_requirements(n, scheduled)
for nn in need_to_schedule:
scheduled.append(nn)
unscheduled.remove(nn)
# print(f"After ag scheduled: scheduled: {scheduled}")
scheduled_set = set(scheduled)
for node in graph.nodes:
if node in scheduled_set:
continue
scheduled.append(node)
unscheduled.remove(node)
assert len(unscheduled) == 0, f"There are unscheduled nodes: {unscheduled}"
ret_graph = make_graph_from_schedule(scheduled)
ret_graph.lint()
return ret_graph