首页 / 资讯中心 / 文章详情

大模型训练之序列并行双雄:DeepSpeed Ulysses和Ring-Attention配置实战

大模型训练之序列并行双雄:DeepSpeed Ulysses和Ring-Attention配置实战 ★ FEATURED ARTICLE
1. 长序列训练为什么绕不开序列并行先把问题摆清楚。你手上有 8 张 A100 80G想训一个 32K 甚至 128K 上下文的模型。单卡放不下怎么办常规做法是数据并行、张量并行、流水并行三件套但这三样都解决不了「单条样本序列太长」的问题——数据并行每张卡还是得吃完整序列张量并行切的是 hidden 维度序列长度一点没少。激活值显存里Attention 那块是 O(N²) 增长的序列翻倍显存直接四倍很快就爆。序列并行Sequence Parallelism就是专门切序列维度的。它把一条长序列沿 token 方向切成 P 份每张卡只持有 N/P 个 token 的 Q、K、VAttention 计算通过卡间通信拼起来。这样单卡激活显存从 O(N²) 降到大约 O((N/P)²)序列长度就能往上顶了。目前工程上最常被拿来对比的两套方案就是 DeepSpeed Ulysses 和 Ring-Attention。它们都切 Q、K、V 的序列维度但通信模式完全不同Ulysses 用 All2All 做一次分布式转置把序列切分变成 head 切分然后每张卡本地跑标准 FlashAttentionRing-Attention 则是把 FlashAttention 的双循环搬到多卡上K、V 块在卡间环形传递用 online softmax 增量更新结果通信和计算重叠。选哪个不是拍脑袋。Ulysses 通信量是 O(N×d)和 GPU 数无关扩展性好但对 head 数量敏感——GQA/MQA 场景下 KV head 很少并行度上不去。Ring-Attention 对网络拓扑要求低P2P 通信更鲁棒模型结构泛化性好但通信量随序列长度平方增长强依赖计算通信重叠变长序列处理也麻烦。这篇就按「能跑通」的标准来给你可复制的 DeepSpeed 配置片段、Ring-Attention 接入骨架、启动命令以及显存和吞吐怎么验证。跑通之后你再决定选哪个。2. 前置准备环境、依赖与模型权重获取在动手配并行之前有几件事必须先落地否则后面报错你都不知道是并行配错了还是环境本身有问题。首先是硬件和驱动。序列并行对卡间带宽敏感Ulysses 的 All2All 在 NVLink 域内表现最好跨机走 InfiniBand 也能跑但带宽会掉。Ring-Attention 用 P2P对拓扑要求低一些PCIe 也能凑合。建议至少 4 卡起步8 卡是常见验证规模。CUDA 版本建议 12.1 以上PyTorch 2.1DeepSpeed 用 0.14.x 或更新的版本老版本对 Ulysses 的 sp_size 支持不完整。然后是依赖安装。DeepSpeed 本身要装带 CUDA 扩展的版本别用纯 Python 的pip install torch2.1.2 torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121 pip install deepspeed0.14.0 pip install flash-attn2.5.8 --no-build-isolationflash-attn 编译比较吃时间--no-build-isolation能省掉重复装依赖。如果编译报 nvcc 版本不匹配先export CUDA_HOME/usr/local/cuda-12.1再装。Ring-Attention 这边社区里用得比较多的是 ring-flash-attention 这个实现它把原始 Ring 方案做了不少工程优化比如负载均衡和变长处理。装法pip install ring-flash-attention如果你要自己改 Attention 逻辑建议直接把它的ring_flash_attn模块源码拉下来放进项目里方便打断点。模型权重方面验证阶段不用上大模型用一个 1B 到 7B 的模型就够看出并行效果。HuggingFace 上拉一个 Llama 结构的小模型即可。注意一点Ulysses 对 head 数敏感选模型时看清楚num_attention_heads和num_key_value_heads。比如 Llama-2-7B 是 32 个 attention head、32 个 KV headMHAUlysses 并行度能开到 32如果是 GQA 模型 KV head 只有 8那 Ulysses 的 sp_size 最多 8再大就切不动了。数据这边长序列训练建议先用固定长度比如统一 8192 或 16384的 packed 数据跑通别一上来就搞变长变长会把 Ring-Attention 的负载均衡问题放大排查起来很痛苦。环境变量也要设一下避免 NCCL 超时和显存碎片export NCCL_DEBUGWARN export NCCL_TIMEOUT1800 export PYTORCH_CUDA_ALLOC_CONFmax_split_size_mb:128到这里环境、依赖、权重、数据都齐了可以进配置环节。3. 可复制配置DeepSpeed Ulysses 与 Ring-Attention 接入这一节是核心直接给能抄的配置。先说 Ulysses因为它在 DeepSpeed 里是原生支持的配置最省事。Ulysses 的关键参数是sequence_parallel_size它决定序列切几份。这个值必须能整除 attention head 数。比如 32 个 head你可以设 8也可以设 16但不能设 5。下面是一个 8 卡、sp_size8 的 DeepSpeed 配置片段存成ds_config_ulysses.json{ train_batch_size: 8, train_micro_batch_size_per_gpu: 1, gradient_accumulation_steps: 1, sequence_parallel_size: 8, zero_optimization: { stage: 1, offload_optimizer: { device: none } }, bf16: { enabled: true }, optimizer: { type: AdamW, params: { lr: 1e-5, betas: [0.9, 0.95], eps: 1e-8, weight_decay: 0.1 } }, gradient_clipping: 1.0, steps_per_print: 10, wall_clock_breakdown: false }注意sequence_parallel_size和train_batch_size的关系全局 batch 是 8每卡 micro batch 是 1sp_size8 意味着这 8 张卡协同处理同一条序列的不同片段。也就是说一条 32K 的序列被切成 8 段每卡 4K token。如果你想让每卡处理不同样本那 sp_size 和数据并行是正交的要分开算。模型侧要开启 Ulysses需要在初始化时把sequence_parallel_size传进去。用 DeepSpeed 的initialize接口import deepspeed import torch from transformers import AutoModelForCausalLM, AutoConfig model AutoModelForCausalLM.from_pretrained( meta-llama/Llama-2-7b-hf, torch_dtypetorch.bfloat16, attn_implementationflash_attention_2 ) ds_config ds_config_ulysses.json model_engine, optimizer, _, _ deepspeed.initialize( modelmodel, model_parametersmodel.parameters(), configds_config )DeepSpeed 会在内部把 Attention 的 Q、K、V 按序列维度切分然后通过 All2All 转置成 head 维度切分本地跑 FlashAttention再 All2All 转回来。你不需要改模型代码这是 Ulysses 最大的优势——对 Attention 计算零侵入。再说 Ring-Attention。它没有 DeepSpeed 原生集成需要你手动替换 Attention 实现。骨架大概是这样先定义一个 ring attention 的 forwardimport torch from ring_flash_attn import ring_flash_attn_func def ring_attention_forward( q, k, v, sp_group, sp_size, sp_rank, causalTrue, ): # q, k, v shape: [batch, seq_len_local, num_heads, head_dim] # seq_len_local full_seq_len / sp_size out ring_flash_attn_func( q, k, v, groupsp_group, dropout_p0.0, causalcausal, window_size(-1, -1), ) return out然后在模型的 Attention 模块里把原来的scaled_dot_product_attention或flash_attn_func替换成上面这个。关键是sp_group要正确初始化用torch.distributed.new_group建一个序列并行的通信组import torch.distributed as dist def init_sp_group(sp_size): world_size dist.get_world_size() rank dist.get_rank() assert world_size % sp_size 0 num_sp_groups world_size // sp_size sp_groups [] for i in range(num_sp_groups): ranks list(range(i * sp_size, (i 1) * sp_size)) group dist.new_group(ranks) sp_groups.append(group) my_group_idx rank // sp_size return sp_groups[my_group_idx], sp_size, rank % sp_sizeRing-Attention 的分块大小c是个可调参数它决定每次 P2P 传多少 K、V。块太小通信次数多块太大显存吃紧。经验值是让c落在 1024 到 4096 之间具体看你的 head_dim 和显存。ring-flash-attention 里一般通过chunk_size参数控制。如果你用的是 Cline MCP 或者 Codex 这类工具做代码辅助配置里要写全三件套Base URL、Key、Model ID。比如在~/.codex/auth.json里{ base_url: https://taotoken.net/api, api_key: sk-你的key, model: claude-sonnet-4-20250514 }Base URL 用https://taotoken.net/apiKey 在控制台生成Model ID 按你实际要调的模型填。这三样缺一个都会报 401。配置写完下一步就是启动和验证。4. 启动命令与显存吞吐验证配置对不对跑起来才知道。先给 Ulysses 的启动命令用torchrun拉起 8 卡torchrun --nproc_per_node8 --nnodes1 --node_rank0 \ --master_addr127.0.0.1 --master_port29500 \ train_ulysses.py \ --model_name meta-llama/Llama-2-7b-hf \ --seq_len 32768 \ --ds_config ds_config_ulysses.json启动后先看日志里有没有sequence_parallel_size8被正确解析以及 All2All 通信组有没有建起来。如果卡在初始化超过 3 分钟多半是 NCCL 建链问题检查NCCL_SOCKET_IFNAME有没有指定正确的网卡。Ring-Attention 的启动类似但要在脚本里先初始化 sp_grouptorchrun --nproc_per_node8 --nnodes1 --node_rank0 \ --master_addr127.0.0.1 --master_port29501 \ train_ring.py \ --model_name meta-llama/Llama-2-7b-hf \ --seq_len 32768 \ --sp_size 8 \ --chunk_size 2048跑起来之后验证两件事显存和吞吐。显存验证用torch.cuda.max_memory_allocated()在 forward 和 backward 之后各打一次import torch def print_memory(step): allocated torch.cuda.max_memory_allocated() / 1024**3 reserved torch.cuda.max_memory_reserved() / 1024**3 print(f[step {step}] max_allocated{allocated:.2f}GB reserved{reserved:.2f}GB)对比基准不开序列并行时32K 序列在 7B 模型上单卡激活大概要 40GB 以上8 卡 Ulysses sp_size8 之后单卡激活应该降到 8GB 上下。如果没降检查sequence_parallel_size是不是真的生效了有时候配置文件路径写错DeepSpeed 会静默用默认值。吞吐验证看 tokens/s。在训练循环里累计处理的 token 数除以耗时import time start time.time() for step, batch in enumerate(dataloader): loss model_engine(batch).loss model_engine.backward(loss) model_engine.step() if step % 10 0: elapsed time.time() - start tokens (step 1) * global_batch_size * seq_len print(fthroughput{tokens / elapsed:.0f} tokens/s)Ulysses 在 NVLink 8 卡、32K 序列下7B 模型大概能跑到 3000 到 5000 tokens/s具体看你的 batch 和梯度累积。Ring-Attention 因为通信和计算重叠理论上吞吐接近但如果 chunk_size 没调好会掉到一半以下。一个实用的对比动作固定序列长度和 batch分别跑 Ulysses 和 Ring-Attention记录显存峰值和 tokens/s做成表格。这样选型就有数据支撑而不是凭感觉。5. 常见报错排查401、local proxy failed、reading choices、OAuth跑并行训练报错五花八门这里挑几个高频的对照真实报错说怎么修。401 Unauthorized。这个一般不是并行本身的问题而是你在训练脚本里调了外部 API比如用模型做数据标注或评测Key 没配对。检查auth.json或环境变量里的base_url和api_key。Base URL 应该是https://taotoken.net/api别多加斜杠或者写成别的路径。Key 过期也会 401去控制台重新生成一个。如果你用的是 Codex 的auth.json确认三个字段都在base_url、api_key、model缺一个都会认证失败。local proxy failed。这个报错通常出现在你本地起了代理但训练进程没走对网络路径。序列并行训练本身不需要代理NCCL 走的是内网。如果你看到local proxy failed同时伴随 NCCL 建链超时先检查NCCL_SOCKET_IFNAME有没有指向正确的内网网卡比如export NCCL_SOCKET_IFNAMEeth0。另外http_proxy和https_proxy环境变量在训练进程里最好清掉避免 NCCL 误走 HTTP 代理。reading choices 报错。完整报错一般是TypeError: NoneType object is not subscriptable或者reading choices这多半是你调模型对话接口时返回体结构没对上。比如你期望response[choices][0][message][content]但实际返回的是流式 chunk 或者错误体。排查方法先把原始 response 打出来看结构。如果是流式要按 SSE 逐块解析如果是错误体里面通常有error.message告诉你具体原因。在训练脚本里调模型做评测时建议加一层重试和结构校验。OAuth 相关报错。如果你用 Claude Code 或者类似的 coding agent 工具可能会碰到 OAuth token 过期。报错一般是OAuth token expired或invalid_grant。这时候需要重新走一遍授权流程或者换成 API Key 方式认证。在auth.json里把api_key填上base_url用https://taotoken.net/api就能绕过 OAuth 直接走 Key 认证。注意别把 OAuth 的 token 和 API Key 混用两者认证头不一样。还有一个 Ulysses 特有的坑AssertionError: sequence_parallel_size must divide num_attention_heads。这就是前面说的 head 数限制。解决办法要么调小 sp_size要么换 MHA 模型要么改用 Ring-Attention。GQA 模型下 Ulysses 的并行度上限就是 KV head 数这个绕不过去。Ring-Attention 这边常见的坑是RuntimeError: NCCL timeout因为 P2P 通信如果某张卡算得慢其他卡会等它。排查方法是看每张卡的负载是否均衡causal mask 下三角计算会导致前面的卡算得多、后面的卡算得少。ring-flash-attention 里有负载均衡的优化确认你用的版本包含这个特性。另外 chunk_size 设太小会导致通信次数暴增也容易超时试着调到 2048 或 4096。6. 选型建议与后续接入路径跑通之后选型其实就看你的约束条件。如果你的模型是 MHA、head 数够多、卡间是 NVLink 或高速 IB优先 Ulysses。它配置简单、对 Attention 零侵入、通信量和 GPU 数无关扩展性最好。DeepSpeed 原生支持改个 JSON 就能上维护成本低。如果你的模型是 GQA/MQA、head 数少、或者网络拓扑一般PCIe 甚至跨机Ring-Attention 更合适。它对模型结构不敏感P2P 通信对网络要求低但你要接受它实现复杂、需要调 chunk_size、变长序列处理麻烦这些代价。实际生产中两者可以混合用在 NVLink 域内用 Ulysses 做 All2All跨机用 Ring 做 P2P这样既突破 head 数限制又避免跨机 All2All 的拥塞。这个混合方案在 8 卡 A100 NVLink、head8 的场景下相比纯 Ring 有 18% 到 31% 的训练和推理性能提升head2 时训练提升能到 54%。要把这套跑起来你需要一个稳定的 API 入口来调模型做验证和评测。API Key 在控制台生成接入文档里有各语言的调用示例。如果你要长期做编码类 Agent 或者大规模训练任务Coding Plan 更适合额度和并发都更宽松。验证模型效果时可以直接在模型对话页面试不用写代码就能对比不同模型在长序列任务上的表现。最后留一个实操建议不管选哪个方案先用小模型1B 以下和短序列4K把并行链路跑通确认 All2All 或 P2P 通信正常、显存确实降下来了再逐步加长序列和换大模型。一上来就 128K 70B报错会让你怀疑人生。序列并行的调试成本主要在通信而通信问题在小规模下更容易定位。
阅读完成 · 觉得有帮助?
咨询建站