407 lines
16 KiB
Python
407 lines
16 KiB
Python
# Copyright (c) DeepSpeed Team.
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
|
|
# DeepSpeed Team
|
|
|
|
import operator
|
|
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
import torch
|
|
from torch.fx import Graph, GraphModule
|
|
|
|
import deepspeed.compile.util as compile_util
|
|
from deepspeed.compile import backend as backend_mod
|
|
from deepspeed.compile import inductor as inductor_mod
|
|
from deepspeed.compile import list_schedule as schedule_mod
|
|
from deepspeed.compile.passes import prefetch as prefetch_mod
|
|
from deepspeed.compile.passes import selective_gather as selective_gather_mod
|
|
from deepspeed.compile.profilers import ProfilingResult
|
|
from deepspeed.compile.profilers.graph_profile import _backfill_missing_profile_metadata, is_profile_incomplete
|
|
|
|
_DC_LIBRARIES = []
|
|
|
|
|
|
def _define_dc_ops():
|
|
try:
|
|
torch.ops.dc.allgather_param.default
|
|
torch.ops.dc.wait_allgather.default
|
|
torch.ops.dc.release_param.default
|
|
torch.ops.dc.reduce_grad.default
|
|
return
|
|
except AttributeError:
|
|
pass
|
|
|
|
lib = torch.library.Library("dc", "DEF")
|
|
for schema in (
|
|
"allgather_param(Tensor a, int graph_id, int id, ScalarType? dtype = None) -> Tensor",
|
|
"wait_allgather(Tensor(a) a, int graph_id, int id) -> Tensor(a)",
|
|
"release_param(Tensor(a) a, int graph_id, int id, int n_users) -> Tensor(a)",
|
|
"reduce_grad(Tensor a, int graph_id, int id) -> Tensor",
|
|
"free_tensors(Tensor[] tensors) -> ()",
|
|
"end_backward(Tensor[] tensors, int graph_id, bool release_reduce_buckets = True) -> ()",
|
|
):
|
|
try:
|
|
lib.define(schema)
|
|
except RuntimeError as exc:
|
|
if "already been registered" not in str(exc):
|
|
raise
|
|
_DC_LIBRARIES.append(lib)
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def stub_deepcompile_ops(monkeypatch):
|
|
_define_dc_ops()
|
|
no_copy_ops = {torch.ops.dc.wait_allgather.default}
|
|
monkeypatch.setattr(compile_util, "get_no_copy_ops", lambda: no_copy_ops)
|
|
|
|
|
|
def _with_meta(node, tensor_size=0, device_time=0):
|
|
node.meta["tensor_size"] = tensor_size
|
|
if device_time is not None:
|
|
node.meta["device_time"] = device_time
|
|
return node
|
|
|
|
|
|
def _placeholder(graph, name):
|
|
return _with_meta(graph.placeholder(name))
|
|
|
|
|
|
def test_sync_memory_profile_complete_noops_without_distributed(monkeypatch):
|
|
monkeypatch.setattr(backend_mod.dist, "is_initialized", lambda: False)
|
|
|
|
def fail_all_reduce(*args, **kwargs):
|
|
raise AssertionError("all_reduce should not run without distributed init")
|
|
|
|
monkeypatch.setattr(backend_mod.dist, "all_reduce", fail_all_reduce)
|
|
|
|
assert backend_mod._sync_memory_profile_complete(True)
|
|
assert not backend_mod._sync_memory_profile_complete(False)
|
|
|
|
|
|
def test_sync_memory_profile_complete_reduces_asymmetric_failure(monkeypatch):
|
|
monkeypatch.setattr(backend_mod.dist, "is_initialized", lambda: True)
|
|
monkeypatch.setattr(backend_mod, "get_accelerator", lambda: SimpleNamespace(current_device=lambda: "cpu"))
|
|
|
|
def mark_any_rank_failed(tensor, op):
|
|
assert op == backend_mod.dist.ReduceOp.MIN
|
|
tensor[0] = 0
|
|
|
|
monkeypatch.setattr(backend_mod.dist, "all_reduce", mark_any_rank_failed)
|
|
|
|
assert not backend_mod._sync_memory_profile_complete(True)
|
|
|
|
|
|
def _allgather(graph, arg, ds_id, name, tensor_size=1, device_time=1):
|
|
return _with_meta(
|
|
graph.call_function(torch.ops.dc.allgather_param.default, (arg, 0, ds_id), {"dtype": torch.float16},
|
|
name=f"allgather_ds_param_{name}_{ds_id}"),
|
|
tensor_size=tensor_size,
|
|
device_time=device_time,
|
|
)
|
|
|
|
|
|
def _wait(graph, arg, ds_id, name):
|
|
return _with_meta(
|
|
graph.call_function(torch.ops.dc.wait_allgather.default, (arg, 0, ds_id),
|
|
name=f"wait_allgather_ds_param_{name}_{ds_id}"))
|
|
|
|
|
|
def _neg(graph, arg, name, device_time=0):
|
|
return _with_meta(graph.call_function(operator.neg, (arg, ), name=name), device_time=device_time)
|
|
|
|
|
|
def _add(graph, lhs, rhs, name, device_time=0):
|
|
return _with_meta(graph.call_function(operator.add, (lhs, rhs), name=name), device_time=device_time)
|
|
|
|
|
|
def _release(graph, arg, ds_id, name):
|
|
return _with_meta(
|
|
graph.call_function(torch.ops.dc.release_param.default, (arg, 0, ds_id, 1),
|
|
name=f"release_ds_param_{name}_{ds_id}"))
|
|
|
|
|
|
def _scheduled_names(graph):
|
|
return [node.name for node in schedule_mod.fast_free_schedule(graph, 0, 0, debug_log=True).nodes]
|
|
|
|
|
|
def test_fast_free_schedule_keeps_zero_free_acc_filter():
|
|
graph = Graph()
|
|
|
|
safe_param = _placeholder(graph, "safe_param")
|
|
safe_pre_param = _placeholder(graph, "safe_pre_param")
|
|
unsafe_param = _placeholder(graph, "unsafe_param")
|
|
unsafe_extra_param = _placeholder(graph, "unsafe_extra_param")
|
|
|
|
safe_pre_ag = _allgather(graph, safe_pre_param, 10, "safe_pre")
|
|
safe_pre_wait = _wait(graph, safe_pre_ag, 10, "safe_pre")
|
|
safe_pre_use = _neg(graph, safe_pre_wait, "safe_pre_use")
|
|
safe_ag = _allgather(graph, _add(graph, safe_param, safe_pre_use, "safe_param_dep"), 11, "safe")
|
|
safe_wait = _wait(graph, safe_ag, 11, "safe")
|
|
safe_use = _neg(graph, safe_wait, "safe_use", device_time=100)
|
|
safe_release = _release(graph, safe_use, 11, "safe")
|
|
|
|
unsafe_ag = _allgather(graph, unsafe_param, 20, "unsafe")
|
|
unsafe_wait = _wait(graph, unsafe_ag, 20, "unsafe")
|
|
unsafe_extra_ag = _allgather(graph, unsafe_extra_param, 21, "unsafe_extra")
|
|
unsafe_extra_wait = _wait(graph, unsafe_extra_ag, 21, "unsafe_extra")
|
|
unsafe_use = _add(graph, unsafe_wait, unsafe_extra_wait, "unsafe_use", device_time=1)
|
|
unsafe_release = _release(graph, unsafe_use, 20, "unsafe")
|
|
|
|
graph.output((safe_release, unsafe_release))
|
|
graph.lint()
|
|
|
|
names = _scheduled_names(graph)
|
|
|
|
assert names.index(safe_release.name) < names.index(unsafe_ag.name)
|
|
assert names.index(safe_release.name) < names.index(unsafe_extra_ag.name)
|
|
|
|
|
|
def test_fast_free_schedule_prefers_lower_allgather_pressure_in_zero_free_acc_bucket():
|
|
graph = Graph()
|
|
|
|
high_param = _placeholder(graph, "high_param")
|
|
high_pre_param = _placeholder(graph, "high_pre_param")
|
|
low_param = _placeholder(graph, "low_param")
|
|
low_pre_param = _placeholder(graph, "low_pre_param")
|
|
|
|
high_pre_ag = _allgather(graph, high_pre_param, 30, "high_pre", tensor_size=100)
|
|
high_pre_wait = _wait(graph, high_pre_ag, 30, "high_pre")
|
|
high_ag = _allgather(graph, _add(graph, high_param, high_pre_wait, "high_param_dep"), 31, "high")
|
|
high_wait = _wait(graph, high_ag, 31, "high")
|
|
high_use = _neg(graph, high_wait, "high_use", device_time=1)
|
|
high_release = _release(graph, high_use, 31, "high")
|
|
|
|
low_pre_ag = _allgather(graph, low_pre_param, 40, "low_pre", tensor_size=1)
|
|
low_pre_wait = _wait(graph, low_pre_ag, 40, "low_pre")
|
|
low_ag = _allgather(graph, _add(graph, low_param, low_pre_wait, "low_param_dep"), 41, "low")
|
|
low_wait = _wait(graph, low_ag, 41, "low")
|
|
low_use = _neg(graph, low_wait, "low_use", device_time=100)
|
|
low_release = _release(graph, low_use, 41, "low")
|
|
|
|
graph.output((high_release, low_release))
|
|
graph.lint()
|
|
|
|
names = _scheduled_names(graph)
|
|
|
|
assert names.index(low_release.name) < names.index(high_ag.name)
|
|
|
|
|
|
def test_fast_free_schedule_uses_pressure_tiebreaker_in_fallback_bucket():
|
|
graph = Graph()
|
|
|
|
high_param = _placeholder(graph, "fallback_high_param")
|
|
high_extra_param = _placeholder(graph, "fallback_high_extra_param")
|
|
low_param = _placeholder(graph, "fallback_low_param")
|
|
low_extra_param = _placeholder(graph, "fallback_low_extra_param")
|
|
|
|
high_ag = _allgather(graph, high_param, 50, "fallback_high", tensor_size=100)
|
|
high_wait = _wait(graph, high_ag, 50, "fallback_high")
|
|
high_extra_ag = _allgather(graph, high_extra_param, 51, "fallback_high_extra", tensor_size=10)
|
|
high_extra_wait = _wait(graph, high_extra_ag, 51, "fallback_high_extra")
|
|
high_use = _add(graph, high_wait, high_extra_wait, "fallback_high_use", device_time=1)
|
|
high_release = _release(graph, high_use, 50, "fallback_high")
|
|
|
|
low_ag = _allgather(graph, low_param, 60, "fallback_low", tensor_size=1)
|
|
low_wait = _wait(graph, low_ag, 60, "fallback_low")
|
|
low_extra_ag = _allgather(graph, low_extra_param, 61, "fallback_low_extra", tensor_size=10)
|
|
low_extra_wait = _wait(graph, low_extra_ag, 61, "fallback_low_extra")
|
|
low_use = _add(graph, low_wait, low_extra_wait, "fallback_low_use", device_time=100)
|
|
low_release = _release(graph, low_use, 60, "fallback_low")
|
|
|
|
graph.output((high_release, low_release))
|
|
graph.lint()
|
|
|
|
names = _scheduled_names(graph)
|
|
|
|
assert names.index(low_ag.name) < names.index(high_ag.name)
|
|
|
|
|
|
def test_fast_free_schedule_keeps_single_allgather_release_order():
|
|
graph = Graph()
|
|
|
|
param = _placeholder(graph, "param")
|
|
ag = _allgather(graph, param, 70, "single")
|
|
wait = _wait(graph, ag, 70, "single")
|
|
use = _neg(graph, wait, "single_use")
|
|
release = _release(graph, use, 70, "single")
|
|
|
|
graph.output((release, ))
|
|
graph.lint()
|
|
|
|
names = _scheduled_names(graph)
|
|
|
|
assert names.index(ag.name) < names.index(wait.name)
|
|
assert names.index(wait.name) < names.index(use.name)
|
|
assert names.index(use.name) < names.index(release.name)
|
|
|
|
|
|
def test_profile_backfill_makes_partial_profile_safe_for_profile_dependent_passes(monkeypatch):
|
|
graph = Graph()
|
|
|
|
param = _placeholder(graph, "partial_profile_param")
|
|
ag = _allgather(graph, param, 90, "partial_profile", device_time=None)
|
|
wait = _wait(graph, ag, 90, "partial_profile")
|
|
use = _neg(graph, wait, "partial_profile_use", device_time=None)
|
|
release = _release(graph, use, 90, "partial_profile")
|
|
|
|
ag.meta.pop("tensor_size", None)
|
|
for node in (ag, use):
|
|
node.meta.pop("wall_time", None)
|
|
node.meta.pop("alloc_mem", None)
|
|
node.meta.pop("max_mem", None)
|
|
|
|
graph.output((release, ))
|
|
graph.lint()
|
|
|
|
_backfill_missing_profile_metadata(graph)
|
|
assert is_profile_incomplete(graph)
|
|
|
|
for node in graph.nodes:
|
|
if node in (ag, use):
|
|
assert node.meta["device_time"] == 0.0
|
|
else:
|
|
assert "device_time" in node.meta
|
|
assert "wall_time" in node.meta
|
|
assert "tensor_size" in node.meta
|
|
assert "alloc_mem" in node.meta
|
|
assert "max_mem" in node.meta
|
|
assert ag.meta["tensor_size"] == 0
|
|
|
|
names = _scheduled_names(graph)
|
|
assert names.index(ag.name) < names.index(wait.name)
|
|
assert names.index(wait.name) < names.index(use.name)
|
|
assert names.index(use.name) < names.index(release.name)
|
|
|
|
class FakeAccelerator:
|
|
|
|
def current_device(self):
|
|
return "cpu"
|
|
|
|
def total_memory(self):
|
|
return 1024
|
|
|
|
def available_memory(self):
|
|
return 1024
|
|
|
|
fake_ds_param = SimpleNamespace(numel=7,
|
|
dtype=torch.float16,
|
|
param=SimpleNamespace(ds_persist=False, ds_shape=(1, )))
|
|
fake_param_manager = {
|
|
0: SimpleNamespace(params={"partial_profile_param": fake_ds_param}, ds_ids={"partial_profile_param": 90})
|
|
}
|
|
profiling_results = {
|
|
0: ProfilingResult(fwd_graph=graph, bwd_graph=None, fwd_mem=[("profiled_before_abort", 0, 0, 0)])
|
|
}
|
|
gm = GraphModule(torch.nn.Module(), graph)
|
|
logs = []
|
|
prefetch_logs = []
|
|
persisted = []
|
|
|
|
monkeypatch.setattr(prefetch_mod, "print_rank_0", lambda message: prefetch_logs.append(message))
|
|
assert prefetch_mod.schedule_prefetch(gm,
|
|
graph_id=0,
|
|
graph_order=[(0, True)],
|
|
profiling_results=profiling_results,
|
|
create_inputs_fn=lambda: (),
|
|
mem_budget=0,
|
|
param_manager=fake_param_manager,
|
|
bwd=False) is gm
|
|
assert any("incomplete profiling data" in message for message in prefetch_logs)
|
|
|
|
monkeypatch.setattr(selective_gather_mod, "print_rank_0", lambda message: logs.append(message))
|
|
monkeypatch.setattr(selective_gather_mod, "get_accelerator", lambda: FakeAccelerator())
|
|
monkeypatch.setattr(selective_gather_mod, "get_deepcompile_handle",
|
|
lambda: SimpleNamespace(set_persistent=persisted.append))
|
|
monkeypatch.setattr(selective_gather_mod.dist, "all_reduce", lambda *args, **kwargs: None)
|
|
|
|
selective_gather_mod.selective_gather(gm,
|
|
graph_id=0,
|
|
graph_order=[(0, True)],
|
|
profiling_results=profiling_results,
|
|
create_inputs_fn=lambda: (),
|
|
mem_budget=0,
|
|
param_manager=fake_param_manager,
|
|
bwd=True)
|
|
assert persisted == []
|
|
assert any("incomplete profiling data" in message for message in logs)
|
|
|
|
|
|
def test_schedule_prefetch_skips_when_memory_profile_incomplete(monkeypatch):
|
|
graph = Graph()
|
|
|
|
param = _placeholder(graph, "mem_incomplete_param")
|
|
ag = _allgather(graph, param, 91, "mem_incomplete")
|
|
wait = _wait(graph, ag, 91, "mem_incomplete")
|
|
use = _neg(graph, wait, "mem_incomplete_use")
|
|
release = _release(graph, use, 91, "mem_incomplete")
|
|
|
|
graph.output((release, ))
|
|
graph.lint()
|
|
|
|
profiling_results = {
|
|
0:
|
|
ProfilingResult(fwd_graph=graph,
|
|
bwd_graph=None,
|
|
fwd_mem=[("profiled_before_abort", 0, 0, 0)],
|
|
fwd_mem_complete=False)
|
|
}
|
|
gm = GraphModule(torch.nn.Module(), graph)
|
|
logs = []
|
|
|
|
monkeypatch.setattr(prefetch_mod, "print_rank_0", lambda message: logs.append(message))
|
|
|
|
assert prefetch_mod.schedule_prefetch(gm,
|
|
graph_id=0,
|
|
graph_order=[(0, False)],
|
|
profiling_results=profiling_results,
|
|
create_inputs_fn=lambda: (),
|
|
mem_budget=0,
|
|
param_manager={},
|
|
bwd=False) is gm
|
|
assert gm.graph is graph
|
|
assert any("incomplete profiling data" in message for message in logs)
|
|
|
|
|
|
def test_graphsafe_rng_state_outputs_are_registered_no_reuse():
|
|
graphsafe_run_with_rng_state = inductor_mod._get_graphsafe_run_with_rng_state()
|
|
if graphsafe_run_with_rng_state is None:
|
|
pytest.skip("graphsafe_run_with_rng_state is unavailable in this torch build")
|
|
|
|
calls = []
|
|
|
|
def fake_register(op_overload, **kwargs):
|
|
calls.append((op_overload, kwargs))
|
|
|
|
assert inductor_mod._register_graphsafe_rng_state_no_reuse(fake_register)
|
|
assert calls == [(graphsafe_run_with_rng_state, {"never_reuse_output": True})]
|
|
|
|
|
|
def test_register_custom_ops_includes_graphsafe_rng_state_no_reuse(monkeypatch):
|
|
graphsafe_run_with_rng_state = inductor_mod._get_graphsafe_run_with_rng_state()
|
|
if graphsafe_run_with_rng_state is None:
|
|
pytest.skip("graphsafe_run_with_rng_state is unavailable in this torch build")
|
|
|
|
_define_dc_ops()
|
|
registered_ops = []
|
|
|
|
def fake_add_needs_realized_inputs(_op_overload):
|
|
return None
|
|
|
|
def fake_register_lowering(op_overload, **_kwargs):
|
|
|
|
def record_handler(handler):
|
|
registered_ops.append(op_overload)
|
|
return handler
|
|
|
|
return record_handler
|
|
|
|
monkeypatch.setattr(inductor_mod, "add_needs_realized_inputs", fake_add_needs_realized_inputs)
|
|
monkeypatch.setattr(inductor_mod, "register_lowering", fake_register_lowering)
|
|
monkeypatch.setattr(inductor_mod, "fallbacks", set())
|
|
monkeypatch.setattr(inductor_mod.Scheduler, "is_dc_patched", True, raising=False)
|
|
|
|
inductor_mod.register_custom_ops()
|
|
|
|
assert graphsafe_run_with_rng_state in registered_ops
|