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

358 lines
13 KiB
Python

# Copyright (c) The DeepSpeed Contributors
# SPDX-License-Identifier: Apache-2.0
# DeepSpeed Team
"""
Arctic Long Sequence Training (ALST) Tiled compute component tests
"""
from deepspeed.runtime.sequence_parallel.ulysses_sp import TiledMLP, sequence_tiled_compute, TiledFusedLogitsLoss
from deepspeed.utils import safe_get_full_grad
from torch.nn import Linear, Module
from unit.common import DistributedTest, preferred_dtype
from unit.util import torch_assert_equal, torch_assert_close, CaptureStderr
import deepspeed
import pytest
import torch
def get_grad(param, zero_stage):
return safe_get_full_grad(param)
# z1 now has contiguous_gradients enabled by default so `param.grad is None` even under z1
# if zero_stage == 1:
# return param.grad
# else:
# return safe_get_full_grad(param)
class SimpleMLP(Module):
def __init__(self, hidden_dim):
super().__init__()
self.up_proj = Linear(hidden_dim, hidden_dim * 2, bias=False)
self.down_proj = Linear(hidden_dim * 2, hidden_dim, bias=False)
self.act = torch.nn.ReLU()
def forward(self, x):
return self.down_proj(self.act(self.up_proj(x)))
# save the original implementation to pass through to the tiled computation wrapper
mlp_forward_orig = SimpleMLP.forward
class MyModel(Module):
def __init__(self, hidden_dim, vocab_size):
super().__init__()
self.vocab_size = vocab_size
# Critical - need to use a stack of at least 2 mlps to validate that the backward of the last mlp sends the correct gradients to the previous mlp in the stack
self.mlp1 = SimpleMLP(hidden_dim)
self.mlp2 = SimpleMLP(hidden_dim)
self.lm_head = torch.nn.Linear(hidden_dim, vocab_size, bias=False)
self.cross_entropy_loss = torch.nn.CrossEntropyLoss()
def forward(self, x, y):
x = self.mlp1(x)
x = self.mlp2(x)
logits = self.lm_head(x)
return self.cross_entropy_loss(logits.view(-1, self.vocab_size), y.view(-1))
def mlp_forward_tiled_mlp(self, x):
# this tests TiledMLP
compute_params = [self.down_proj.weight, self.up_proj.weight]
num_shards = 4
return TiledMLP.apply(
mlp_forward_orig,
self,
x,
num_shards,
compute_params,
)
def mlp_forward_sequence_tiled_compute(self, x):
# this tests: sequence_tiled_compute + SequenceTiledCompute - same as TiledMLP but a-non-MLP
# specific generic implementation of tiled compute
kwargs_to_shard = dict(x=x)
kwargs_to_pass = dict(self=self)
grad_requiring_tensor_key = "x"
compute_params = [self.down_proj.weight, self.up_proj.weight]
seqlen = x.shape[1]
num_shards = 4
return sequence_tiled_compute(
mlp_forward_orig,
seqlen,
num_shards,
kwargs_to_shard,
kwargs_to_pass,
grad_requiring_tensor_key,
compute_params,
output_unshard_dimension=1, # x
output_reduction=None,
)
@pytest.mark.parametrize("batch_size", [1, 2])
@pytest.mark.parametrize("zero_stage", [2, 3])
class TestTiledCompute(DistributedTest):
world_size = 1
def test_tiled_mlp(self, zero_stage, batch_size):
config_dict = {
"train_micro_batch_size_per_gpu": 1,
"zero_optimization": {
"stage": zero_stage
},
"optimizer": {
"type": "Adam",
"params": {
"lr": 1e-3
}
},
}
dtype = preferred_dtype()
if dtype == torch.bfloat16:
config_dict["bf16"] = {"enabled": True}
elif dtype == torch.float16:
config_dict["fp16"] = {"enabled": True, "loss_scale": 1.0}
# for debug
# torch.set_printoptions(precision=8, sci_mode=True)
vocab_size = 10
seed = 42
hidden_dim = 128
bs = batch_size
seqlen = 125 # use a non 2**n length to test varlen shards (last short)
torch.manual_seed(seed)
x = torch.rand((bs, seqlen, hidden_dim), dtype=dtype, requires_grad=True)
y = torch.empty((bs, seqlen), dtype=torch.long, requires_grad=False).random_(vocab_size)
# A. Baseline: model with normal MLP
torch.manual_seed(seed)
model_a = MyModel(hidden_dim=hidden_dim, vocab_size=vocab_size).to(dtype)
model_a, _, _, _ = deepspeed.initialize(config=config_dict,
model=model_a,
model_parameters=model_a.parameters())
x = x.to(model_a.device)
y = y.to(model_a.device)
x_a = x.clone().detach().requires_grad_(True)
y_a = y.clone().detach()
loss_a = model_a(x_a, y_a)
model_a.backward(loss_a)
param_grad_a1 = get_grad(model_a.module.mlp1.up_proj.weight, zero_stage)
param_grad_a2 = get_grad(model_a.module.mlp2.up_proj.weight, zero_stage)
x_grad_a = x_a.grad
assert param_grad_a1 is not None
assert param_grad_a2 is not None
assert x_grad_a is not None
# B. model with tiled MLP using TiledMLP
torch.manual_seed(seed)
SimpleMLP.forward = mlp_forward_tiled_mlp
model_b = MyModel(hidden_dim=hidden_dim, vocab_size=vocab_size).to(dtype)
model_b, _, _, _ = deepspeed.initialize(config=config_dict,
model=model_b,
model_parameters=model_b.parameters())
x_b = x.clone().detach().requires_grad_(True)
y_b = y.clone().detach()
loss_b = model_b(x_b, y_b)
with CaptureStderr() as cs:
model_b.backward(loss_b)
# see the explanation inside TiledMLP.backward
assert "grad and param do not obey the gradient layout contract" not in cs.err, f"stride issue: {cs.err}"
param_grad_b1 = get_grad(model_b.module.mlp1.up_proj.weight, zero_stage)
param_grad_b2 = get_grad(model_b.module.mlp2.up_proj.weight, zero_stage)
x_grad_b = x_b.grad
assert param_grad_b1 is not None
assert param_grad_b2 is not None
assert x_grad_b is not None
# print(f"{loss_a=}")
# print(f"{loss_b=}")
# print(f"{param_grad_a1=}")
# print(f"{param_grad_b1=}")
# print(f"{param_grad_a2=}")
# print(f"{param_grad_b2=}")
torch_assert_equal(loss_a, loss_b)
# Gradient will not be exactly the same, especially under half-precision. And bf16 is
# particularly lossy so need to lower tolerance a bit more than the default. Switch to
# dtype torch.float or even torch.double to see that the diff is tiny - so the math is
# correct, but accumulation error adds up. Alternatively making hidden_dim bigger makes the
# divergence much smaller as well.
torch_assert_close(param_grad_a1, param_grad_b1) #, rtol=1e-03, atol=1e-04)
torch_assert_close(param_grad_a2, param_grad_b2) #, rtol=1e-03, atol=1e-04)
torch_assert_close(x_grad_a, x_grad_b)
# C. model with tiled MLP using the generic version of the same via sequence_tiled_compute + SequenceTiledCompute
torch.manual_seed(seed)
SimpleMLP.forward = mlp_forward_sequence_tiled_compute
model_c = MyModel(hidden_dim=hidden_dim, vocab_size=vocab_size).to(dtype)
model_c, _, _, _ = deepspeed.initialize(config=config_dict,
model=model_c,
model_parameters=model_c.parameters())
x_c = x.clone().detach().requires_grad_(True)
y_c = y.clone().detach()
loss_c = model_c(x_c, y_c)
with CaptureStderr() as cs:
model_c.backward(loss_c)
assert "grad and param do not obey the gradient layout contract" not in cs.err, f"stride issue: {cs.err}"
param_grad_c1 = get_grad(model_c.module.mlp1.up_proj.weight, zero_stage)
param_grad_c2 = get_grad(model_c.module.mlp2.up_proj.weight, zero_stage)
x_grad_c = x_c.grad
assert param_grad_c1 is not None
assert param_grad_c2 is not None
assert x_grad_c is not None
# print(f"{loss_a=}")
# print(f"{loss_c=}")
# print(f"{param_grad_a1=}")
# print(f"{param_grad_c1=}")
# see notes for B
torch_assert_equal(loss_a, loss_c)
torch_assert_close(param_grad_a1, param_grad_c1) #, rtol=1e-03, atol=1e-04)
torch_assert_close(param_grad_a2, param_grad_c2) #, rtol=1e-03, atol=1e-04)
torch_assert_close(x_grad_a, x_grad_c)
@pytest.mark.parametrize("batch_size", [1, 2])
@pytest.mark.parametrize("zero_stage", [2, 3])
class TestTiledFusedLogitsLoss(DistributedTest):
world_size = 1
def test_tiled_fused_logits_loss(self, zero_stage, batch_size):
def tiled_forward(self, x, y):
x = self.mlp1(x)
x = self.mlp2(x)
def loss_fn(self, x, y):
logits = self.lm_head(x)
return self.cross_entropy_loss(logits.view(-1, self.vocab_size), y.view(-1))
mask = None
shards = 2
compute_params = [self.lm_head.weight]
output_reduction = "mean"
loss = TiledFusedLogitsLoss.apply(
loss_fn,
self,
x,
y,
mask,
shards,
compute_params,
output_reduction,
)
return loss
config_dict = {
"train_micro_batch_size_per_gpu": 1,
"zero_optimization": {
"stage": zero_stage
},
"optimizer": {
"type": "Adam",
"params": {
"lr": 1e-3
}
},
}
dtype = preferred_dtype()
#dtype = torch.float
if dtype == torch.bfloat16:
config_dict["bf16"] = {"enabled": True}
elif dtype == torch.float16:
config_dict["fp16"] = {"enabled": True, "loss_scale": 1.0}
# for debug
# torch.set_printoptions(precision=8, sci_mode=True)
vocab_size = 100
seed = 42
hidden_dim = 64
bs = batch_size
seqlen = 425 # use a non 2**n length to test varlen shards (last short)
torch.manual_seed(seed)
x = torch.rand((bs, seqlen, hidden_dim), dtype=dtype, requires_grad=True)
y = torch.empty((bs, seqlen), dtype=torch.long, requires_grad=False).random_(vocab_size)
# A. Baseline: model with normal loss
torch.manual_seed(seed)
model_a = MyModel(hidden_dim=hidden_dim, vocab_size=vocab_size).to(dtype)
model_a, _, _, _ = deepspeed.initialize(config=config_dict,
model=model_a,
model_parameters=model_a.parameters())
x = x.to(model_a.device)
y = y.to(model_a.device)
x_a = x.clone().detach().requires_grad_(True)
y_a = y.clone().detach()
loss_a = model_a(x_a, y_a)
model_a.backward(loss_a)
param_grad_a = get_grad(model_a.module.lm_head.weight, zero_stage)
x_grad_a = x_a.grad
assert param_grad_a is not None
assert x_grad_a is not None
# B. model with fused tiled logits loss
torch.manual_seed(seed)
MyModel.forward_orig = MyModel.forward
MyModel.forward = tiled_forward
model_b = MyModel(hidden_dim=hidden_dim, vocab_size=vocab_size).to(dtype)
model_b, _, _, _ = deepspeed.initialize(config=config_dict,
model=model_b,
model_parameters=model_b.parameters())
x_b = x.clone().detach().requires_grad_(True)
y_b = y.clone().detach()
loss_b = model_b(x_b, y_b)
with CaptureStderr() as cs:
model_b.backward(loss_b)
# see the explanation inside TiledMLP.backward
assert "grad and param do not obey the gradient layout contract" not in cs.err, f"stride issue: {cs.err}"
param_grad_b = get_grad(model_b.module.lm_head.weight, zero_stage)
x_grad_b = x_b.grad
assert param_grad_b is not None
assert x_grad_b is not None
# print(f"{loss_a=}")
# print(f"{loss_b=}")
# print(f"{x_grad_a=}")
# print(f"{x_grad_b=}")
# print(f"{param_grad_a=}")
# print(f"{param_grad_b=}")
# usually this is an exact match, but on cpu CI this fails.
torch_assert_close(loss_a, loss_b)
# Gradient will not be exactly the same, especially under half-precision. And bf16 is
# particularly lossy so need to lower tolerance a bit more than the default. Switch to
# dtype torch.float or even torch.double to see that the diff is tiny - so the math is
# correct, but accumulation error adds up. Alternatively making hidden_dim bigger makes the
# divergence much smaller as well.
torch_assert_close(x_grad_a, x_grad_b)
torch_assert_close(param_grad_a, param_grad_b) #, rtol=1e-03, atol=1e-04)
# restore
MyModel.forward = MyModel.forward_orig