项目文件夹

文件
2026-07-13 13:18:33 +08:00

297 行
13 KiB
Python

# Copyright (c) Microsoft Corporation.
# SPDX-License-Identifier: Apache-2.0
# DeepSpeed Team
from copy import deepcopy
import os
import random
import numpy as np
import torch
import deepspeed
import deepspeed.comm as dist
from deepspeed.accelerator import get_accelerator
from deepspeed.runtime.zero import GatheredParameters
from unit.simple_model import SimpleModel
from unit.common import allclose_on_all_ranks
def compare_loss(self, config, dtype, iteration=5, hidden_dim_override=None):
hidden_dim = hidden_dim_override if hidden_dim_override is not None else 10
# the default tolerances of torch.testing.assert_close are too small
RTOL = 5e-1
ATOL = 1e-2
# Use a fixed seed for determinism. We don't use the @enable_determinism decorator
# because it also sets torch.use_deterministic_algorithms(True), which seems
# incompatible with torch.compile() in test environments.
# Might be related to https://github.com/pytorch/pytorch/issues/159855
local_rank = int(os.getenv("LOCAL_RANK", "0"))
seed = 123 + local_rank
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
get_accelerator().manual_seed(seed)
get_accelerator().manual_seed_all(seed)
device = torch.device(get_accelerator().current_device_name())
model = SimpleModel(hidden_dim)
i = get_accelerator().current_device()
baseline_model = deepcopy(model)
baseline_config = deepcopy(config)
baseline_config["zero_optimization"]["stage"] = 0
baseline_config["zero_optimization"]["offload_optimizer"] = {}
baseline_engine, baseline_optimizer, _, _ = deepspeed.initialize(config=baseline_config,
model=baseline_model,
model_parameters=baseline_model.parameters())
if config["zero_optimization"]["stage"] == 3:
with deepspeed.zero.Init(config_dict_or_path=config):
target_model = SimpleModel(hidden_dim)
with GatheredParameters(target_model.parameters(), modifier_rank=0):
for p1, p2 in zip(target_model.parameters(), model.parameters()):
p1.data.copy_(p2.data)
else:
target_model = deepcopy(model)
target_engine, target_optimizer, _, _ = deepspeed.initialize(config=config,
model=target_model,
model_parameters=target_model.parameters())
target_engine.compile()
train_batch_size = config["train_micro_batch_size_per_gpu"]
xs = [torch.randn(train_batch_size, hidden_dim, device=device, dtype=dtype) for _ in range(iteration)]
ys = [torch.randn_like(x) for x in xs]
for x, y in zip(xs, ys):
baseline_loss = baseline_engine(x, y)
target_loss = target_engine(x, y)
allclose_on_all_ranks(baseline_loss, target_loss, "Loss values are not close.", rtol=RTOL, atol=ATOL)
baseline_engine.backward(baseline_loss)
target_engine.backward(target_loss)
baseline_engine.step()
target_engine.step()
with GatheredParameters(target_engine.parameters()):
for p1, p2 in zip(baseline_engine.parameters(), target_engine.parameters()):
allclose_on_all_ranks(p1, p2, "Parameters are not equal.", rtol=RTOL, atol=ATOL)
baseline_engine.destroy()
target_engine.destroy()
def compare_sp_loss(self, config, sp_size, iterations=3):
"""
Compare AutoSP compiled model loss against a compiled Ulysses SP model (ground truth).
Both engines are trained in lockstep. After all training steps the final-step
losses are compared.
"""
import torch.nn.functional as F
from transformers import AutoModelForCausalLM, AutoConfig
from transformers.modeling_utils import ALL_ATTENTION_FUNCTIONS
from deepspeed.compile import constants as autosp_constants
from deepspeed.compile.custom_ops.sp_dp_registry import populate_registry, get_group
from deepspeed.sequence.layer import DistributedAttention
RTOL, ATOL = 0.1, 0.01
model_name = 'hf-internal-testing/tiny-random-LlamaForCausalLM'
seq_length = 64
torch.manual_seed(42)
get_accelerator().manual_seed_all(42)
device = torch.device(get_accelerator().current_device_name())
model_config = AutoConfig.from_pretrained(model_name)
model_config._attn_implementation = "sdpa"
base_model = AutoModelForCausalLM.from_pretrained(model_name, config=model_config)
vocab_size = model_config.vocab_size
# Set up SP/DP process groups (shared by both Ulysses and AutoSP).
dp_size = dist.get_world_size() // sp_size
populate_registry(sp_size, dp_size)
# The DP-rank index selects which SP group the current rank belongs to.
sp_group = get_group(dist.get_rank() // sp_size)
sp_rank = dist.get_rank() % sp_size
chunk = seq_length // sp_size
# Build a DistributedAttention wrapper that mirrors distributed_attention.py.
# Registered under a unique key so the model's "sdpa" slot stays untouched —
# AutoSP's graph pass can therefore find F.scaled_dot_product_attention nodes.
def _sdpa_inner(q, k, v, attn_mask=None, dropout_p=0.0, is_causal=True, scale=None):
# DistributedAttention delivers tensors in [b, s, n, h]; SDPA wants [b, n, s, h].
out = F.scaled_dot_product_attention(q.permute(0, 2, 1, 3),
k.permute(0, 2, 1, 3),
v.permute(0, 2, 1, 3),
dropout_p=dropout_p,
is_causal=is_causal,
scale=scale)
return out.permute(0, 2, 1, 3)
_dist_attn = DistributedAttention(_sdpa_inner, sp_group, scatter_idx=2, gather_idx=1)
def _ulysses_attn_forward(module,
query_states,
key_states,
value_states,
attention_mask,
scaling=None,
dropout=0.0,
is_causal=False,
**kwargs):
q = query_states.transpose(1, 2).contiguous()
k = key_states.transpose(1, 2).contiguous()
v = value_states.transpose(1, 2).contiguous()
out = _dist_attn(q, k, v, batch_dim_idx=0, dropout_p=dropout, is_causal=is_causal, scale=scaling)
return out, None
ALL_ATTENTION_FUNCTIONS["ulyssess"] = _ulysses_attn_forward
# Ulysses baseline: regular torch.compile, no deepcompile or autosp pass.
ulysses_config = deepcopy(config)
ulysses_config.pop("compile", None)
ulysses_model = deepcopy(base_model)
ulysses_model.config._attn_implementation = "ulyssess"
ulysses_engine, _, _, _ = deepspeed.initialize(config=ulysses_config,
model=ulysses_model,
model_parameters=ulysses_model.parameters())
ulysses_engine.compile()
# AutoSP model: sdpa so the autosp pass can find F.scaled_dot_product_attention.
# dynamic=True ensures all shape dimensions are treated symbolically so the autosp
# pass can correctly shard the sequence dimension for all dtypes including fp16/bf16.
autosp_model = deepcopy(base_model)
autosp_engine, _, _, _ = deepspeed.initialize(config=config,
model=autosp_model,
model_parameters=autosp_model.parameters())
autosp_engine.compile(compile_kwargs={"dynamic": True})
# Train both engines in lockstep; compare the losses at the final step.
ul_loss = autosp_loss = None
for i in range(iterations):
torch.manual_seed(42 + i)
full_ids = torch.randint(0, vocab_size, (1, seq_length), device=device)
# Ulysses: each rank processes its own shard.
shard_ids = full_ids[:, sp_rank * chunk:(sp_rank + 1) * chunk]
shard_pos = torch.arange(sp_rank * chunk, (sp_rank + 1) * chunk, device=device).unsqueeze(0)
shard_mask = torch.ones(1, chunk, device=device, dtype=torch.long)
ul_out = ulysses_engine(input_ids=shard_ids,
labels=shard_ids,
position_ids=shard_pos,
attention_mask=shard_mask)
# Average per-shard losses across SP ranks to get the full-sequence loss.
ul_loss = ul_out.loss.clone()
dist.all_reduce(ul_loss, group=sp_group)
ul_loss = ul_loss / sp_size
# AutoSP: full sequence. dynamic=True makes all shapes symbolic, so mark_dynamic
# is not needed; only the tag attributes that the autosp pass uses are set here.
autosp_ids = full_ids.clone()
autosp_lbl = autosp_ids.clone()
autosp_pos = torch.arange(seq_length, device=device).unsqueeze(0)
autosp_msk = torch.ones(1, seq_length, device=device, dtype=torch.long)
autosp_ids.tag = autosp_constants.AUTOSP_INPUT_ID_KEY
autosp_lbl.tag = autosp_constants.AUTOSP_LABEL_ID_KEY
autosp_pos.tag = autosp_constants.AUTOSP_POSITION_ID_KEY
autosp_out = autosp_engine(input_ids=autosp_ids,
labels=autosp_lbl,
position_ids=autosp_pos,
attention_mask=autosp_msk)
autosp_loss = autosp_out.loss
ulysses_engine.backward(ul_out.loss)
ulysses_engine.step()
autosp_engine.backward(autosp_loss)
autosp_engine.step()
allclose_on_all_ranks(autosp_loss, ul_loss, "AutoSP and Ulysses losses are not close.", rtol=RTOL, atol=ATOL)
ulysses_engine.destroy()
del ALL_ATTENTION_FUNCTIONS["ulyssess"]
autosp_engine.destroy()
def create_gm_nodes(batch_size: int = 1, seq_len: int = 16):
"""
Load a tiny LlamaForCausalLM, tag inputs with AutoSP keys, mark the sequence
dimension dynamic, and capture the torch-fx GraphModule via a custom
torch.compile backend.
The returned gm is identical to what the autosp pass receives during training:
placeholder nodes carry tensor_dict tags and meta['val'] shapes are symbolic
(SymInt) in the sequence dimension.
Returns:
gm – GraphModule with fully populated node metadata
inputs – (input_ids, labels, position_ids) used for tracing
"""
from transformers import AutoModelForCausalLM, AutoConfig
from deepspeed.compile import constants
# Each call needs a clean dynamo state; without this, the recompile_limit
# (default 8) is exhausted across tests and the backend is never invoked.
torch._dynamo.reset()
model_name = 'hf-internal-testing/tiny-random-LlamaForCausalLM'
model_config = AutoConfig.from_pretrained(model_name)
model_config._attn_implementation = "sdpa"
model = AutoModelForCausalLM.from_pretrained(model_name, config=model_config)
model.eval()
vocab_size = model_config.vocab_size
input_ids = torch.randint(0, vocab_size, (batch_size, seq_len))
labels = torch.randint(0, vocab_size, (batch_size, seq_len))
position_ids = torch.arange(seq_len).unsqueeze(0)
# dynamo propagates Python tensor attributes into node.meta['tensor_dict'];
# find_node_by_tag relies on this to identify the AutoSP input nodes.
input_ids.tag = constants.AUTOSP_INPUT_ID_KEY
labels.tag = constants.AUTOSP_LABEL_ID_KEY
position_ids.tag = constants.AUTOSP_POSITION_ID_KEY
# Marking the sequence dim dynamic causes dynamo to emit a SymInt placeholder
# node and store symbolic shapes in node.meta['val'], which shard_tensor_node
# needs to locate the sequence-length symbol in the graph.
torch._dynamo.decorators.mark_dynamic(input_ids, 1)
torch._dynamo.decorators.mark_dynamic(labels, 1)
torch._dynamo.decorators.mark_dynamic(position_ids, 1)
captured_gm = [None]
def _capture_backend(gm, example_inputs):
if captured_gm[0] is None:
captured_gm[0] = gm
return gm
compiled = torch.compile(model, backend=_capture_backend, dynamic=True)
with torch.no_grad():
compiled(input_ids=input_ids, labels=labels, position_ids=position_ids)
assert captured_gm[0] is not None, "Capture backend was never invoked — graph capture failed"
return captured_gm[0], (input_ids, labels, position_ids)
def find_sym_seq_node(gm):
"""
Return the SymInt placeholder node for the sequence-length dimension of
input_ids, or None if it cannot be found.
"""
from deepspeed.compile.util import get_input_id_node
from deepspeed.compile.fx import get_node_shape_meta, find_node_by_name
input_ids_node = get_input_id_node(gm)
val = get_node_shape_meta(input_ids_node)
seq_symint = val.shape[1]
return find_node_by_name(gm, str(seq_symint))