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

297 lines
13 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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))