尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

狂潮分布式训练系统六层原子化权责架构设计与实操

狂潮分布式训练系统六层原子化权责架构设计与实操 1. 为什么“狂潮”这类分布式训练系统需要六层权责架构做过大模型训练的人都有一个共同体会单卡跑通一个 demo 很容易真正把几十上百张卡组织起来稳定训练难点根本不在模型代码本身而在于“谁负责什么、谁不能碰什么”。狂潮分布式训练系统这套六层原子化权责架构本质上就是给一个复杂训练系统做“职责切分”和“权限上锁”让每个模块只干自己该干的事边界清晰到可以单独替换、单独测试、单独追责。我先说清楚这套架构解决的核心痛点。分布式训练系统通常包含通信、调度、数据加载、计算、容错、监控这几大块如果一开始不把权责划清楚后期会出现非常典型的问题调度器偷偷改了并行策略通信层却按旧拓扑建链数据加载模块为了提速越权访问了显存容错模块重启时把优化器状态搞丢。这些问题在单机时代不明显一旦上到多机多卡排查成本会指数级上升。六层原子化权责架构的价值就是把这些“越权”和“职责模糊”从设计层面直接堵死。所谓“原子化”指的是每一层的职责不可再分、不可交叉。你可以把它理解成一家公司的组织架构董事会定战略CTO 管技术财务管钱HR 管人每个角色有明确的权限边界不能互相代签。狂潮的六层大致可以对应为资源抽象层、通信拓扑层、并行策略层、执行调度层、状态容错层、观测治理层。这六层从上到下依次收敛权限从下到上依次暴露能力任何跨层调用都必须经过显式的权限规约。这套架构适合谁来参考如果你正在从单机训练往分布式迁移或者你已经在维护一个多机训练平台但被各种“玄学 bug”折磨又或者你是团队里的架构负责人需要给训练系统定规范那这套六层权责思路可以直接抄作业。它不绑定具体框架PyTorch、Megatron、DeepSpeed 都能套用核心是权责划分的思想而不是某个库的 API。提示权责架构不是写完文档就完事它必须落到代码的模块边界和接口签名上否则就是纸上谈兵。2. 六层原子化权责架构的逐层拆解与边界定义2.1 资源抽象层只负责“有什么”不负责“怎么用”资源抽象层是整个系统的地基它的唯一职责是回答“当前集群有哪些计算资源、显存多大、互联带宽多少”。这一层绝对不能碰任何训练逻辑也不能决定哪张卡跑哪个 rank。我见过太多项目把设备分配逻辑写进资源层结果换一个调度策略就要动地基牵一发动全身。这一层的权限规约很明确对外只暴露资源描述符包括设备列表、显存容量、互联拓扑、健康状态。它不提供“分配”能力分配是上层调度层的事。这样设计的好处是资源层可以独立做设备发现和健康探测哪怕调度层挂了资源层依然能给出准确的集群快照。实操中资源抽象层通常对应一个设备管理器模块。它启动时扫描所有可见设备生成一份不可变的资源清单。注意这里是“不可变”一旦生成就不允许训练过程中动态修改否则并行策略层会拿到过期信息。如果确实需要动态扩缩容应该走“重新生成清单 通知上层”的流程而不是让资源层自己改。2.2 通信拓扑层建链、维护、销毁三件事之外一律不管通信拓扑层负责根据资源清单和并行策略建立进程组和通信链路。它的边界是只管理“链路”不管理“数据”。也就是说这一层知道 rank0 和 rank1 之间有一条 NCCL 通道但它不知道这条通道上跑的是梯度还是参数。权限规约上通信拓扑层只接受来自并行策略层的拓扑描述不允许调度层直接干预建链顺序。为什么因为建链顺序直接影响通信死锁概率。如果调度层能随意插队建链很容易出现 A 等 B、B 等 C、C 等 A 的环形等待。把建链权收归拓扑层由它统一做拓扑排序是避免死锁最有效的手段。这一层还有一个容易被忽视的职责链路健康监测。当某条链路超时或断开时拓扑层应该主动上报给状态容错层而不是自己尝试重连。重连策略属于容错层的权限拓扑层越权重连会导致状态不一致。2.3 并行策略层决定“怎么切”但不决定“什么时候算”并行策略层是分布式训练的大脑之一它负责决定数据并行、张量并行、流水线并行、序列并行怎么组合每一层的切分维度是多少。这一层的输出是一份完整的并行计划包括每个 rank 负责哪部分计算、需要和哪些 rank 通信。边界关键在于并行策略层只产出“计划”不执行“计划”。它不直接调用通信库也不直接启动计算核。这样做的好处是并行策略可以离线计算和验证甚至可以在 CPU 上模拟推演确认没有显存溢出和通信死锁后再交给执行层。权限规约上并行策略层不允许访问运行时状态比如当前显存占用、当前 loss 值。它只能基于静态的资源清单和模型结构做决策。这是为了保证并行策略的可复现性——同样的输入必须产出同样的计划否则训练无法复现就是灾难。2.4 执行调度层唯一有权“按下启动键”的层执行调度层是六层里权限最集中的一层它负责把并行计划翻译成实际的 kernel 启动、通信调用、数据搬运。它是唯一有权操作计算流和通信流的层其他层只能给它下指令不能绕过它直接操作硬件。这一层的权责边界是按计划执行不修改计划。如果执行过程中发现计划不可行比如显存不够它应该上报给并行策略层重新规划而不是自己偷偷缩小 batch size。我踩过这个坑早期为了“智能”让调度层在显存不足时自动降 batch结果训练日志里的 batch size 和配置对不上复现实验时怎么都跑不出同样结果。执行调度层还需要维护一个“执行上下文”记录当前迭代到哪一步、哪些通信已完成、哪些计算已提交。这个上下文是状态容错层做 checkpoint 的依据所以调度层必须保证上下文的完整性和时序正确性。2.5 状态容错层管生管死但不管算得快不快状态容错层负责 checkpoint 保存与恢复、故障检测、进程重启、状态一致性校验。它的权限是“决定何时保存、何时重启、恢复到哪个状态”但它不参与任何计算逻辑也不修改并行计划。这一层和调度层的边界最容易模糊。我的经验是调度层负责“正常流程”容错层负责“异常流程”。正常迭代中调度层按计划跑一旦检测到故障控制权移交容错层由它决定是重启当前 rank 还是整个作业。重启完成后容错层把控制权交回调度层并附带恢复后的状态上下文。权限规约上容错层不允许直接操作通信链路重启后的建链必须重新走通信拓扑层。这是为了防止容错层用旧的链路信息恢复导致 rank 错位。2.6 观测治理层只读权限但拥有“一票否决”的告警权观测治理层是唯一一个对全系统有只读权限的层。它可以采集所有层的指标、日志、trace但不能修改任何层的状态。它的核心职责是让系统的运行状态可观测、可审计、可回溯。这一层有一个特殊权限当检测到严重异常时比如梯度爆炸、通信超时率飙升、显存泄漏它可以触发告警并建议容错层介入但不能直接杀进程。最终决策权仍在容错层。这样设计是为了避免监控系统误判导致训练中断。六层之间的权责关系可以用一张表来概括层级核心职责允许操作禁止操作资源抽象层设备发现与描述扫描设备、生成清单分配设备、修改清单通信拓扑层建链与链路维护建链、拓扑排序、健康上报重连、传输数据并行策略层切分方案制定生成并行计划访问运行时状态、执行计划执行调度层计划执行启动 kernel、通信调用修改计划、越权降 batch状态容错层故障恢复checkpoint、重启、状态校验操作通信链路、修改计划观测治理层监控审计采集指标、告警修改任何层状态3. 模块边界落地的实操要点与权限规约写法3.1 用接口签名把边界“焊死”权责架构最怕停留在文档层面。我的做法是每一层对外只暴露一个入口接口接口参数里不允许出现跨层对象。比如并行策略层的入口是plan(resource_desc, model_desc) - parallel_plan参数里只有资源描述和模型描述拿不到调度上下文也拿不到通信句柄。这样从类型系统层面就杜绝了越权。接口返回值也要做限制。并行策略层返回的parallel_plan应该是一个纯数据结构不包含任何可执行对象或回调。执行调度层拿到这个结构后自己决定怎么执行。如果计划里带了函数指针那就等于策略层把执行权也攥在手里了边界就破了。3.2 权限规约用“能力令牌”而不是布尔标志很多系统用is_allowed这种布尔值做权限控制粒度太粗。狂潮这套架构更适合用能力令牌每一层启动时拿到一组令牌令牌决定它能调用哪些下层接口。比如调度层拿到execute_token和comm_token但没有checkpoint_token那它就无法触发保存。能力令牌的好处是可审计。每次跨层调用都记录“谁用哪个令牌调了什么”出问题时能快速定位是哪一层越权。实测下来这套机制在排查“谁改了并行度”这类问题时特别高效日志一查就知道是策略层重新规划了还是调度层偷偷改了。3.3 边界测试要作为 CI 的强制项模块边界不是写完就稳的必须有测试兜底。我建议在 CI 里加三类边界测试第一类是“越权调用测试”模拟调度层尝试调用容错层接口必须报错第二类是“跨层数据污染测试”检查策略层返回的计划里是否混入了运行时对象第三类是“权限令牌失效测试”令牌过期后所有跨层调用必须被拒绝。这三类测试看起来简单但能挡住 80% 的架构腐化。我见过一个项目上线半年后调度层代码里出现了import checkpoint_manager一查是某个紧急修复时有人图省事直接调了容错层接口。如果没有边界测试这种腐化会越来越多最后六层架构名存实亡。注意边界测试的断言要写死具体异常类型不能只判断“是否抛异常”否则容易误判。4. 从零搭建六层架构的完整实操流程4.1 第一步定义资源描述符与设备发现先写资源抽象层。核心是一个ResourceDescriptor数据类包含devices、memory、topology三个字段。设备发现用框架自带的 APIPyTorch 用torch.cuda.device_count()同时读取每张卡的显存和互联信息。拓扑信息可以通过nvidia-smi topo -m解析或者用 NCCL 的拓扑探测接口。from dataclasses import dataclass from typing import List, Dict dataclass(frozenTrue) class DeviceInfo: index: int memory_total: int compute_capability: tuple dataclass(frozenTrue) class ResourceDescriptor: devices: List[DeviceInfo] topology: Dict[str, List[int]] timestamp: float注意frozenTrue这是保证资源清单不可变的关键。任何试图修改清单的操作都会在运行时抛异常从语言层面守住边界。4.2 第二步实现通信拓扑层的建链与排序通信拓扑层接收资源描述符和并行计划输出进程组。核心是拓扑排序根据并行计划里的通信关系生成一个无环的建链顺序。这里我用的是 Kahn 算法做拓扑排序确保不会出现环形等待。def build_process_groups(resource, plan): graph build_comm_graph(plan) order topological_sort(graph) groups [] for rank_group in order: pg dist.new_group(ranksrank_group) groups.append(pg) return groups建链完成后拓扑层要启动一个健康监测线程定期 ping 各条链路。监测结果只上报不处理处理权在容错层。4.3 第三步并行策略层的计划生成与校验策略层拿到资源描述和模型描述后先做显存估算再做切分决策。显存估算要考虑参数、梯度、优化器状态、激活值四部分。以 7B 模型为例FP16 参数约 14GB梯度 14GBAdam 优化器状态 56GB合计 84GB单卡 80GB 放不下必须切分。切分决策我通常按“先张量并行、再流水线并行、最后数据并行”的顺序。张量并行度受限于单机内 NVLink 数量流水线并行度受限于层数数据并行度受限于剩余资源。策略层输出一份ParallelPlan包含每个 rank 的模型分片、通信组、执行顺序。计划生成后必须做校验显存是否够、通信组是否覆盖所有 rank、流水线是否有空泡。校验不通过就报错不允许带着问题计划进入执行层。4.4 第四步执行调度层的计划翻译与启动调度层拿到ParallelPlan后翻译成实际的执行序列。这里的关键是“按计划执行”不允许任何动态调整。调度层维护一个ExecutionContext记录当前迭代、已完成的通信、已提交的计算。class ExecutionContext: def __init__(self, plan): self.plan plan self.iteration 0 self.comm_done set() self.compute_done set() def step(self): for op in self.plan.ops_for_iter(self.iteration): if op.type comm: self.execute_comm(op) self.comm_done.add(op.id) elif op.type compute: self.execute_compute(op) self.compute_done.add(op.id) self.iteration 1调度层不允许修改plan如果执行中发现计划不可行抛异常给策略层重新规划。4.5 第五步容错层的 checkpoint 与恢复容错层定期保存 checkpoint保存内容包括模型参数、优化器状态、调度层的ExecutionContext。保存时机由容错层决定通常是每 N 步或每 T 分钟。保存时必须暂停调度层保证状态一致。恢复时容错层先重建资源描述符和通信拓扑再把ExecutionContext恢复到保存点最后把控制权交回调度层。注意恢复后的建链必须重新走拓扑层不能用旧的进程组。4.6 第六步观测层的指标采集与告警观测层用只读方式采集各层指标。资源层采集设备利用率拓扑层采集链路带宽和延迟策略层采集切分方案调度层采集迭代耗时容错层采集 checkpoint 耗时和恢复次数。所有指标汇总到一个时序数据库配一套告警规则。告警规则里梯度范数超过阈值、通信超时率超过 1%、显存增长速率异常这三条是必须有的。告警触发后通知容错层由容错层决定是否介入。5. 常见问题与排查技巧实录5.1 训练卡死但无报错怎么定位是哪一层的问题这是分布式训练最经典的问题。我的排查顺序是先看观测层的链路延迟指标如果某条链路延迟飙升问题在通信拓扑层如果链路正常但迭代不推进看调度层的ExecutionContext确认是不是某个通信 op 没完成如果上下文正常但 loss 不更新看策略层的计划是否和实际执行一致。实操中我遇到过一次卡死最后定位到是拓扑层建链顺序和策略层计划不一致导致两个 rank 互相等待。根因是拓扑层用了缓存的旧拓扑没有根据新计划重新排序。修复方法是在拓扑层加一个计划版本号校验版本不匹配就重新建链。5.2 checkpoint 恢复后 loss 对不上问题出在哪loss 对不上通常是状态恢复不完整。检查三样东西优化器状态是否完整恢复、数据加载器的随机种子是否恢复、调度层的迭代计数是否恢复。我踩过的坑是数据加载器种子没恢复导致恢复后喂的数据和中断前不一样loss 自然对不上。解决方法是把数据加载器的状态也纳入 checkpoint包括当前 epoch、当前 batch index、随机种子。容错层保存时统一序列化恢复时统一反序列化。5.3 并行策略层报显存不足但实际显存还有余量这是显存估算过于保守导致的。策略层估算激活值时通常按最坏情况算但实际训练中激活值可以重计算显存占用远低于估算值。解决方法是给策略层加一个“激活重计算”开关开启后按重计算后的显存需求估算。另一个原因是碎片化。显存够但分配不出连续大块也会报 OOM。这时候可以在资源层加一个碎片整理接口但注意整理操作必须由调度层触发资源层不能自己整理。5.4 权限令牌误拦截了正常调用能力令牌粒度太细会导致误拦截。比如调度层需要读取资源描述符来做执行决策但资源描述符的读权限只给了策略层。这时候要么给调度层加读权限要么把资源描述符做成公开只读数据。我的经验是资源描述符和并行计划这类“纯数据”应该公开只读不需要令牌控制只有“操作类”接口才需要令牌。这样既保证安全又避免过度设计。5.5 六层架构下如何做性能优化性能优化最容易破坏边界。比如为了减少通信有人会让调度层直接合并通信 op这就越权了。正确做法是把优化需求反馈给策略层由策略层重新生成计划比如把两个小通信合并成一个大通信。我总结的原则是优化可以跨层提需求但不能跨层改实现。每一层的优化都在自己边界内做边界外的优化通过接口协商。这样架构不会因为优化而腐化。常见问题可能层级排查手段解决方向训练卡死拓扑层/调度层查链路延迟、执行上下文校验计划版本、重新建链loss 对不上容错层查 checkpoint 完整性补全数据加载器状态显存误报策略层查估算模型开启激活重计算令牌误拦截权限系统查调用日志纯数据公开只读优化破坏边界全层查跨层调用需求上提、实现内收6. 我在实际项目里踩过的坑和几条硬经验第一条经验六层架构不是一次设计出来的是迭代出来的。我第一个版本只分了四层把容错和调度混在一起结果故障恢复时调度状态经常丢。后来拆出独立的容错层问题才解决。所以如果你刚开始设计不用追求一次到位但每一层拆出来时一定要把权限规约写清楚。第二条经验边界测试比功能测试更重要。功能测试保证系统能跑边界测试保证系统不腐化。我现在的项目里边界测试用例数量是功能测试的两倍每次 PR 必须通过全部边界测试才能合并。第三条经验观测层的只读权限要严格执行。我见过观测层为了“智能告警”直接调容错层接口杀进程的结果误杀了一次正常训练。观测层就是眼睛眼睛不能动手。第四条经验并行策略层的计划要可序列化、可回放。这样出问题时可以把计划 dump 出来离线复现。我现在的做法是每次训练启动时把ParallelPlan存一份到日志目录排查问题时直接加载回放效率极高。第五条经验权限令牌要有过期机制。长期有效的令牌等于没有令牌。我给每个令牌设了生命周期跨层调用完成后令牌立即失效下次调用重新申请。这样即使令牌泄露影响范围也有限。这套六层原子化权责架构说到底就是一句话让每一层只做自己该做的事并且用代码和测试把这条规矩焊死。分布式训练系统的复杂度不会因为架构清晰而降低但会因为权责明确而变得可维护、可排查、可演进。
返回列表