☰
大模型训练之序列并行双雄:DeepSpeed Ulysses和Ring-Attention配置实战
2026/10/2 16:40:06 网站建设 项目流程

1. 长序列训练为什么绕不开序列并行

先把问题摆清楚。你手上有 8 张 A100 80G,想训一个 32K 甚至 128K 上下文的模型。单卡放不下怎么办?常规做法是数据并行、张量并行、流水并行三件套,但这三样都解决不了「单条样本序列太长」的问题——数据并行每张卡还是得吃完整序列,张量并行切的是 hidden 维度,序列长度一点没少。激活值显存里,Attention 那块是 O(N²) 增长的,序列翻倍,显存直接四倍,很快就爆。

序列并行(Sequence Parallelism)就是专门切序列维度的。它把一条长序列沿 token 方向切成 P 份,每张卡只持有 N/P 个 token 的 Q、K、V,Attention 计算通过卡间通信拼起来。这样单卡激活显存从 O(N²) 降到大约 O((N/P)²),序列长度就能往上顶了。

目前工程上最常被拿来对比的两套方案,就是 DeepSpeed Ulysses 和 Ring-Attention。它们都切 Q、K、V 的序列维度,但通信模式完全不同:Ulysses 用 All2All 做一次分布式转置,把序列切分变成 head 切分,然后每张卡本地跑标准 FlashAttention;Ring-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.1+,DeepSpeed 用 0.14.x 或更新的版本,老版本对 Ulysses 的 sp_size 支持不完整。

然后是依赖安装。DeepSpeed 本身要装带 CUDA 扩展的版本,别用纯 Python 的:

pip install torch==2.1.2 torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121 pip install deepspeed==0.14.0 pip install flash-attn==2.5.8 --no-build-isolation

flash-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 head(MHA),Ulysses 并行度能开到 32;如果是 GQA 模型 KV head 只有 8,那 Ulysses 的 sp_size 最多 8,再大就切不动了。

数据这边,长序列训练建议先用固定长度(比如统一 8192 或 16384)的 packed 数据跑通,别一上来就搞变长,变长会把 Ring-Attention 的负载均衡问题放大,排查起来很痛苦。

环境变量也要设一下,避免 NCCL 超时和显存碎片:

export NCCL_DEBUG=WARN export NCCL_TIMEOUT=1800 export PYTORCH_CUDA_ALLOC_CONF=max_split_size_mb:128

到这里,环境、依赖、权重、数据都齐了,可以进配置环节。

3. 可复制配置:DeepSpeed Ulysses 与 Ring-Attention 接入

这一节是核心,直接给能抄的配置。先说 Ulysses,因为它在 DeepSpeed 里是原生支持的,配置最省事。

Ulysses 的关键参数是sequence_parallel_size,它决定序列切几份。这个值必须能整除 attention head 数。比如 32 个 head,你可以设 8,也可以设 16,但不能设 5。下面是一个 8 卡、sp_size=8 的 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 是 1,sp_size=8 意味着这 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_dtype=torch.bfloat16, attn_implementation="flash_attention_2" ) ds_config = "ds_config_ulysses.json" model_engine, optimizer, _, _ = deepspeed.initialize( model=model, model_parameters=model.parameters(), config=ds_config )

DeepSpeed 会在内部把 Attention 的 Q、K、V 按序列维度切分,然后通过 All2All 转置成 head 维度切分,本地跑 FlashAttention,再 All2All 转回来。你不需要改模型代码,这是 Ulysses 最大的优势——对 Attention 计算零侵入。

再说 Ring-Attention。它没有 DeepSpeed 原生集成,需要你手动替换 Attention 实现。骨架大概是这样,先定义一个 ring attention 的 forward:

import torch from ring_flash_attn import ring_flash_attn_func def ring_attention_forward( q, k, v, sp_group, sp_size, sp_rank, causal=True, ): # 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, group=sp_group, dropout_p=0.0, causal=causal, 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_size

Ring-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/api,Key 在控制台生成,Model ID 按你实际要调的模型填。这三样缺一个都会报 401。

配置写完,下一步就是启动和验证。

4. 启动命令与显存吞吐验证

配置对不对,跑起来才知道。先给 Ulysses 的启动命令,用torchrun拉起 8 卡:

torchrun --nproc_per_node=8 --nnodes=1 --node_rank=0 \ --master_addr=127.0.0.1 --master_port=29500 \ train_ulysses.py \ --model_name meta-llama/Llama-2-7b-hf \ --seq_len 32768 \ --ds_config ds_config_ulysses.json

启动后先看日志里有没有sequence_parallel_size=8被正确解析,以及 All2All 通信组有没有建起来。如果卡在初始化超过 3 分钟,多半是 NCCL 建链问题,检查NCCL_SOCKET_IFNAME有没有指定正确的网卡。

Ring-Attention 的启动类似,但要在脚本里先初始化 sp_group:

torchrun --nproc_per_node=8 --nnodes=1 --node_rank=0 \ --master_addr=127.0.0.1 --master_port=29501 \ 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_size=8 之后,单卡激活应该降到 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(f"throughput={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_IFNAME=eth0。另外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、head=8 的场景下,相比纯 Ring 有 18% 到 31% 的训练和推理性能提升;head=2 时训练提升能到 54%。

要把这套跑起来,你需要一个稳定的 API 入口来调模型做验证和评测。API Key 在控制台生成,接入文档里有各语言的调用示例。如果你要长期做编码类 Agent 或者大规模训练任务,Coding Plan 更适合,额度和并发都更宽松。验证模型效果时,可以直接在模型对话页面试,不用写代码就能对比不同模型在长序列任务上的表现。

最后留一个实操建议:不管选哪个方案,先用小模型(1B 以下)和短序列(4K)把并行链路跑通,确认 All2All 或 P2P 通信正常、显存确实降下来了,再逐步加长序列和换大模型。一上来就 128K + 70B,报错会让你怀疑人生。序列并行的调试成本主要在通信,而通信问题在小规模下更容易定位。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询