import inspect
import os
import time
import types
import unittest
from functools import wraps
import torch
import torch.distributed as dist
import torch_npu
from torch.distributed.pipelining import Schedule1F1B, SplitPoint
from torch_npu.distributed.pipelining import pipeline
from torch_npu.testing.common_distributed import skipIfUnsupportMultiNPU
from torch_npu.testing.testcase import run_tests, TestCase
d_hid = 64
batch_size = 32
chunks = 4
device_type = "npu"
total_steps = 15
warmup_steps = 5
join_timeout = 120
class TinyTransformerBlock(torch.nn.Module):
def __init__(self, dim: int) -> None:
super().__init__()
self.attention_norm = torch.nn.LayerNorm(dim)
self.attention = torch.nn.Linear(dim, dim)
self.ffn_norm = torch.nn.LayerNorm(dim)
self.feed_forward = torch.nn.Sequential(
torch.nn.Linear(dim, dim * 2),
torch.nn.GELU(),
torch.nn.Linear(dim * 2, dim),
)
def forward(self, x):
x = x + self.attention(self.attention_norm(x))
x = x + self.feed_forward(self.ffn_norm(x))
return x
class TinyTransformer(torch.nn.Module):
def __init__(self, dim: int = d_hid, n_layers: int = 2) -> None:
super().__init__()
self.layers = torch.nn.ModuleList(
[TinyTransformerBlock(dim) for _ in range(n_layers)]
)
self.norm = torch.nn.LayerNorm(dim)
self.output = torch.nn.Linear(dim, dim)
self.split_spec = {
f"layers.{i}": SplitPoint.BEGINNING for i in range(1, n_layers)
}
def forward(self, x):
for layer in self.layers:
x = layer(x)
return self.output(self.norm(x))
class NoneMaskBlock(torch.nn.Module):
def __init__(self) -> None:
super().__init__()
self.linear = torch.nn.Linear(d_hid, d_hid)
def forward(self, x, mask=None):
assert mask is None
return self.linear(x)
class NoneInputPipelineModel(torch.nn.Module):
def __init__(self) -> None:
super().__init__()
self.block0 = NoneMaskBlock()
self.block1 = NoneMaskBlock()
self.split_spec = {"block1": SplitPoint.BEGINNING}
def forward(self, x, mask=None):
x = self.block0(x, mask)
return self.block1(x, mask)
class FXTracerPublicApiTest(TestCase):
def test_public_pipeline_is_patched(self):
import torch.distributed.pipelining as pipelining
self.assertIsNot(pipeline, pipelining.pipeline)
self.assertTrue(getattr(pipeline, "_torch_npu_pipelining_patch_applied", False))
signature = inspect.signature(pipeline)
self.assertIn("mode", signature.parameters)
self.assertIn("atomic_units", signature.parameters)
self.assertEqual(pipeline.__module__, "torch_npu.distributed.pipelining")
class FXTracerScheduleTest(TestCase):
MAIN_PROCESS_RANK = -1
world_size = 2
def join_or_run(self, fn):
@wraps(fn)
def wrapper(self):
if self.rank == self.MAIN_PROCESS_RANK:
for process in self.processes:
process.join(join_timeout)
if process.is_alive():
process.terminate()
process.join()
self.assertEqual(process.exitcode, 0)
else:
fn()
return types.MethodType(wrapper, self)
def __init__(self, method_name: str = "runTest") -> None:
super().__init__(method_name)
fn = getattr(self, method_name)
setattr(self, method_name, self.join_or_run(fn))
def setUp(self):
super(TestCase, self).setUp()
if (
not torch.npu.is_available()
or torch.npu.device_count() < self.world_size
):
raise unittest.SkipTest("Multi-NPU 2 condition not satisfied")
os.environ["MASTER_ADDR"] = "127.0.0.1"
os.environ["MASTER_PORT"] = "29589"
os.environ["BACKEND"] = dist.Backend.HCCL
self.processes = []
self.rank = self.MAIN_PROCESS_RANK
proc = torch.multiprocessing.get_context("spawn").Process
for rank in range(self.world_size):
process = proc(
target=self.__class__._run,
name="process " + str(rank),
args=(rank, self._current_test_name()),
)
process.start()
self.processes.append(process)
def tearDown(self):
super().tearDown()
for process in self.processes:
process.terminate()
self.processes = []
def _current_test_name(self) -> str:
return self.id().split(".")[-1]
@classmethod
def _run(cls, rank: int, test_name: str) -> None:
os.environ["LOCAL_RANK"] = str(rank)
os.environ["RANK"] = str(rank)
os.environ["WORLD_SIZE"] = str(cls.world_size)
self = cls(test_name)
self.rank = rank
getattr(self, test_name)()
def dist_init(self):
torch.npu.set_device(self.rank)
dist.init_process_group(
backend="hccl",
rank=self.rank,
world_size=self.world_size,
)
@property
def device(self) -> torch.device:
return torch.device(device_type, self.rank % torch.npu.device_count())
def _seed_all(self, seed: int) -> None:
torch.manual_seed(seed)
torch.npu.manual_seed_all(seed)
def _sync_device(self) -> None:
torch.npu.synchronize()
def _set_device(self) -> None:
torch.npu.set_device(self.device)
def _make_model_and_input(self):
self._seed_all(2026)
self._set_device()
model = TinyTransformer(n_layers=self.world_size).to(self.device)
x = torch.randn(batch_size, d_hid, device=self.device)
return model, x
def _build_pipe_and_stage(self, mode: str, atomic_units=None):
model, x = self._make_model_and_input()
x_mb = x.chunk(chunks)[0]
kwargs = {
"module": model,
"mb_args": (x_mb,),
"split_spec": model.split_spec,
"mode": mode,
}
if atomic_units is not None:
kwargs["atomic_units"] = atomic_units
pipe = pipeline(**kwargs)
stage = pipe.build_stage(self.rank, self.device)
return model, pipe, stage, x
def _build_none_input_pipe_and_stage(self):
self._seed_all(2028)
self._set_device()
model = NoneInputPipelineModel().to(self.device)
x = torch.randn(batch_size, d_hid, device=self.device)
x_mb = x.chunk(chunks)[0]
pipe = pipeline(
module=model,
mb_args=(x_mb, None),
split_spec=model.split_spec,
mode="fx",
atomic_units=["block0", "block1"],
)
stage = pipe.build_stage(self.rank, self.device)
return pipe, stage, x
def _schedule_step(self, schedule, *args, **kwargs):
original_fork_rng = torch.random.fork_rng
def npu_fork_rng(*fork_args, **fork_kwargs):
if fork_kwargs.get("device_type", "cuda") == "cuda":
fork_kwargs["device_type"] = "npu"
return original_fork_rng(*fork_args, **fork_kwargs)
torch.random.fork_rng = npu_fork_rng
try:
return schedule.step(*args, **kwargs)
finally:
torch.random.fork_rng = original_fork_rng
def _run_1f1b(self, mode: str, atomic_units=None):
model, pipe, stage, _ = self._build_pipe_and_stage(mode, atomic_units)
self._set_device()
self._seed_all(2027)
inputs = [
torch.randn(batch_size, d_hid, device=self.device)
for _ in range(total_steps)
]
targets = [
torch.randn(batch_size, d_hid, device=self.device)
for _ in range(total_steps)
]
loss_fn = torch.nn.MSELoss(reduction="sum")
schedule = Schedule1F1B(stage, chunks, loss_fn=loss_fn, scale_grads=False)
stage_module = pipe.get_stage_module(self.rank)
dist.barrier()
self._sync_device()
total_elapsed = 0.0
step_losses = []
final_out = None
for step_idx in range(total_steps):
stage_module.zero_grad(set_to_none=True)
self._set_device()
self._sync_device()
start_time = time.perf_counter()
losses = []
out = self._schedule_step(
schedule,
inputs[step_idx], target=targets[step_idx], losses=losses
)
self._sync_device()
if step_idx >= warmup_steps:
total_elapsed += time.perf_counter() - start_time
if self.rank == self.world_size - 1:
final_out = out.detach()
step_losses.append(sum(losses).detach())
dist.barrier()
if self.rank == self.world_size - 1:
return model, pipe, final_out, step_losses, total_elapsed
return model, pipe, None, None, total_elapsed
def _assert_fx_close_to_export(self, atomic_units=None):
_, _, export_out, export_losses, export_elapsed = self._run_1f1b("export")
_, _, fx_out, fx_losses, fx_elapsed = self._run_1f1b(
"fx",
atomic_units=atomic_units,
)
if self.rank == self.world_size - 1:
torch.testing.assert_close(fx_out, export_out, rtol=1e-3, atol=1e-3)
self.assertEqual(len(fx_losses), total_steps)
self.assertEqual(len(export_losses), total_steps)
for fx_loss, export_loss in zip(fx_losses, export_losses):
torch.testing.assert_close(
fx_loss,
export_loss,
rtol=1e-3,
atol=1e-3,
)
self.assertLessEqual(fx_elapsed, export_elapsed * 1.2)
@skipIfUnsupportMultiNPU(2)
def test_fx_schedule_accepts_none_input(self):
self.dist_init()
pipe, stage, x = self._build_none_input_pipe_and_stage()
self.assertEqual(pipe.num_stages, self.world_size)
for node in pipe.split_gm.graph.nodes:
if node.op != "call_module":
continue
stage_module = pipe.split_gm.get_submodule(node.target)
placeholders = [
stage_node
for stage_node in stage_module.graph.nodes
if stage_node.op == "placeholder"
]
self.assertEqual(len(node.args), len(placeholders))
self.assertTrue(
all(
placeholder.meta.get("val", "__missing__") is not None
for placeholder in placeholders
)
)
loss_fn = torch.nn.MSELoss(reduction="sum")
schedule = Schedule1F1B(stage, chunks, loss_fn=loss_fn, scale_grads=False)
target = torch.randn(batch_size, d_hid, device=self.device)
losses = []
self._schedule_step(schedule, x, target=target, losses=losses)
@skipIfUnsupportMultiNPU(2)
def test_fx_tracer_builds_pipe_and_stage(self):
self.dist_init()
_, pipe, stage, _ = self._build_pipe_and_stage("fx")
self.assertEqual(pipe.num_stages, self.world_size)
self.assertIsNotNone(stage.submod)
self.assertEqual(stage.stage_index, self.rank)
@skipIfUnsupportMultiNPU(2)
def test_fx_tracer_matches_export_with_1f1b(self):
self.dist_init()
self._assert_fx_close_to_export()
@skipIfUnsupportMultiNPU(2)
def test_fx_tracer_atomic_units_match_export_with_1f1b(self):
self.dist_init()
self._assert_fx_close_to_export(atomic_units=["layers.0"])
if __name__ == "__main__":
run_tests()