想象一间深夜食堂的后厨。墙上贴着一本菜谱,四口炒锅前站着四位厨子。老板一声“开火”,四位厨子读同一本菜谱、按同样的步骤下料,但锅里分别是不同的食材:一号锅是牛肉、二号锅是豆腐、三号锅是鱼、四号锅是青菜。程序还是那个程序,数据各不相同——这就是“单程序、多数据”(SPMD,Single Program Multiple Data)的直觉。在深度学习里,GPU/TPU集群就像是这间后厨:一份训练脚本被复制到所有设备上,每台设备只拿到一批数据的分片,各自前向、反向、算梯度,再通过通信原语把结果汇总。JAX 框架的核心本领,就是把“开几个灶、每个灶分多少菜、菜怎么传”这套编排逻辑,用简洁的声明式 API 表达出来。

岗位关联

对 AI 从业者而言,掌握 SPMD 与 JAX 的 sharding 机制,意味着你能用一份代码把训练从单卡平滑扩展到上千卡,这是大模型时代搭建分布式训练系统必备的核心能力。

目录
  1. 可视化演示
  2. SPMD核心思想
  3. JAX并行API
  4. Sharding分片机制
  5. 分布式执行原理
  6. 数据并行策略
  7. 模型并行策略
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

动态演示SPMD编程模型与JAX分布的核心机制,通过源头/管道/节点/终端四种模式展示关键概念与交互流程。

本节配有交互动效(进入视口后自动加载,约 3KB)。

SPMD编程模型与JAX分布:SPMD SPMD编程模型核心思想 pjit与SPMD的实现机制 核心机制可视化 SPMD编程模型与JAX分布整体架构与关键组件

SPMD核心思想

深夜食堂后厨的比喻再往前推一步:主厨发现客人太多、一口锅忙不过来,于是把同一本菜谱复印四份,四口锅同时开火。每个厨子的动作完全一致,但案板上的食材不同——厨子一处理牛肉,厨子二处理豆腐,互不干扰。这正是 SPMD 的图景:一份程序(菜谱),多份数据(食材),多个执行单元(灶台)。

在并行计算的历史里,SPMD 是形态最朴素、也最实用的一种并行范式。它不同于 SIMD——SIMD 是处理器内部的“单指令多数据”,所有计算单元被同一个时钟驱动,严格同步;SPMD 则是进程/设备级别的并行,每台设备有自己的指令流、自己的内存,只是执行同一份源码。它也不同于“多程序多数据”(MPMD)——后者让不同设备跑不同程序,而 SPMD 的团队协作方式高度统一,代码只有一份,大幅降低了维护和调试成本。

SPMD 给分布式训练带来了一个关键优势:可扩展性。只要数据量足够大,把数据切成 4 份分给 4 台设备,理想情况下训练时间能缩短近 4 倍。但现实世界总有无法并行的“串行尾巴”——数据加载、日志上报、设备同步等。Amdahl 定律定量刻画了这种上限:

回到厨房:炒菜本身可以并行,但“切菜备料”如果只有主厨一个人做,那么即便灶台翻倍,整体出菜速度也会被切菜拖住。这也是为什么真实的分布式训练系统要尽量把数据加载、预处理等也并行化,让 \(f\) 尽可能接近 1。

S=1(1−f)+fN\displaystyle S = \frac{1}{(1 - f) + \dfrac{f}{N}}
其中 \(f\) 是程序中可以并行执行的比例,\(N\) 是设备数量,\(S\) 是加速比。即使 90% 的代码可以并行,100 台设备的最大加速也只有 10 倍左右。
  1. 串行占比假设程序中不可并行的部分占比为 \(1 - f\),这部分只能由单台设备完成,是加速比的“天花板”来源。
    1−f\displaystyle 1 - f
  2. 并行均摊可并行部分占比为 \(f\),在理想情况下可以平均分摊到 \(N\) 台设备上。
    fN\displaystyle \frac{f}{N}
  3. 整体加速加速比等于总用时除以优化后用时,即 1 除以串行时间与并行分摊时间之和,得到 Amdahl 定律。
    S=1(1−f)+fN\displaystyle S = \frac{1}{(1 - f) + \frac{f}{N}}

JAX并行API

主厨既想多开灶,又不想让每位厨子分头研究菜谱如何改编,于是雇佣了一位“分厨长”。分厨长精通整本菜谱,负责把菜谱翻译成每口灶台上可直接执行的分步骤指令。在 JAX 的世界里,这位分厨长就是并行 API:pmap、pjit,以及如今更统一的 jax.jit 加 in_shardings 组合。

pmap 是 JAX 最早的并行原语之一:它把一个函数编译成单程序多数据的形态,并且自动把输入张量的第 0 维均匀切分到所有设备上。它的学习曲线平缓,但有局限——如果你想切分第二维、第三维,或者想表达更复杂的模型并行,pmap 就力不从心了。

于是 pjit(parallel jit)出现了:它允许用户通过 PartitionSpec 指定“张量的哪个维度映射到逻辑网格的哪个轴”,从而同时覆盖数据并行和模型并行。随着 JAX 演进,pjit 的能力被并入了 jax.jit——只需传入 in_shardings 和 out_shardings,编译器就会在编译期为每个设备生成对应的 SPMD 程序。这种统一的接口,让并行策略变成了函数签名的一部分:

分厨长的好处是,厨师们不需要自己分配食材——只需在门口写清楚“第几位厨子拿第几筐菜”,实际的分菜动作由 JAX 在编译和运行时自动完成。同理,使用 jax.jit 声明分片后,开发者不需要手写设备间的数据搬运代码,程序会自己生成通信指令。

xi=x[ i⋅Blocal:(i+1)⋅Blocal ],Blocal=⌈BN⌉\displaystyle x_i = x\left[\, i \cdot B_{\text{local}} : (i+1) \cdot B_{\text{local}} \,\right], \quad B_{\text{local}} = \left\lceil \frac{B}{N} \right\rceil
意义:总批次大小为 \(B\) 的数据,被切成 \(N\) 份,第 \(i\) 台设备拿到第 \(i\) 个连续片段,每份大小约为 \(B/N\)。
  1. 批次总大小假设一次训练迭代的输入数据有 \(B\) 条样本,形状为 \(B \times D\)。
    B×D\displaystyle B \times D
  2. 设备均分把样本维度平均切成 \(N\) 份,每份大小 \(B_{\text{local}} = \lceil B / N \rceil\),不足的部分由最后一批补足。
    Blocal=⌈BN⌉\displaystyle B_{\text{local}} = \left\lceil \frac{B}{N} \right\rceil
  3. 索引区间第 \(i\) 台设备拿到的区间是左闭右开的 \( [\,i \cdot B_{\text{local}}, (i+1) \cdot B_{\text{local}}\,) \)。
    xi=x[ i⋅Blocal:(i+1)⋅Blocal ]\displaystyle x_i = x\left[\, i \cdot B_{\text{local}} : (i+1) \cdot B_{\text{local}} \,\right]

Sharding分片机制

分厨长在发食材前,要先在案板上画好切线:哪几根萝卜归一号灶,哪几根藕归二号灶。这张“分切图纸”在 JAX 中称为 Sharding,它描述了一个张量如何沿各个维度被切分并映射到设备网格上。

具体的“图纸语言”由三件套构成:Mesh 定义逻辑网格与物理设备的绑定,PartitionSpec 描述张量维度与网格轴的对应关系,NamedSharding 把两者合成为一个完整的 sharding 声明。例如 PartitionSpec('data', None) 表示:张量的第 0 维沿名为 'data' 的网格轴切分,第 1 维不切分、在每个设备上完整保留。

更厉害的是,sharding 信息不是孤立的:JAX 编译器会根据输入的分片规则,在函数内部自动“传播”分片——某个中间结果该用哪种分片方式,编译器会基于计算图推断出来,用户只需在函数边界(输入和输出)说清楚即可。

shard(X,d)=X[ld:rd,:],ld=⌊d⋅D0N⌋,  rd=⌊(d+1)⋅D0N⌋\displaystyle \text{shard}(X, d) = X\left[ l_d : r_d, : \right], \quad l_d = \left\lfloor \frac{d \cdot D_0}{N} \right\rfloor, \; r_d = \left\lfloor \frac{(d+1) \cdot D_0}{N} \right\rfloor

1. 分片区间:每台设备拿到的不是完整张量,而是从行号 \(l_d\) 到 \(r_d\) 的一个切片。

X[ld:rd,:]\displaystyle X[l_d : r_d, :]

2. 起始行号:对设备 \(d\),起始行是总行数乘以设备序号再除以设备总数后向下取整。

ld=⌊d⋅D0N⌋\displaystyle l_d = \left\lfloor \frac{d \cdot D_0}{N} \right\rfloor

3. 结束行号:结束行是下一个设备起始行的位置,从而保证所有设备的切片连续且不重叠。

rd=⌊(d+1)⋅D0N⌋\displaystyle r_d = \left\lfloor \frac{(d+1) \cdot D_0}{N} \right\rfloor

画切线越清楚,后厨的分工就越顺畅。同理,分片声明写得越细致,JAX 越能精准地生成通信方案,避免不必要的设备间数据搬运。这也是为什么在 JAX 中调分布式性能,本质上是在调 sharding 策略。

分布式执行原理

厨房不是一堆灶台随意摆放,而是被分成了备菜区、炒菜区、传菜口;每位厨子在哪口锅、旁边是哪位厨子,都事先排定。JAX 把这种物理空间的编排抽象为“设备网格”(Mesh)。

设备网格的构建方式很直白:把当前可见的设备排列成逻辑形状,并给每个维度命名。例如 8 台设备可以排成 Mesh(devices, ('data', 'model')),形成 4×2 的逻辑网格。之后 PartitionSpec('data', None) 中的轴名,会被网格解析成物理设备的编号。执行时,XLA 编译器拿到完整的计算图和分片约束,会把图“切”成与设备数相同的若干子图,每个子图跑在对应设备上;图与图之间需要交换数据的地方,自动插入 all-reduce、all-gather、scatter 等集合通信原语。

分布式执行的性能可以用一个朴素的时间模型理解:

T并行≈T串行N+T通信\displaystyle T_{\text{并行}} \approx \frac{T_{\text{串行}}}{N} + T_{\text{通信}}

1. 计算分摊:把一份工作在 \(N\) 台设备上均分,理想情况下每台设备只需付出原来的 \(1/N\)。

T串行N\displaystyle \frac{T_{\text{串行}}}{N}

2. 通信代价:设备间同步梯度、交换数据需要时间,记作 \(T_{\text{通信}}\)。

T通信\displaystyle T_{\text{通信}}

3. 并行总耗时:真正的耗时是计算与通信之和,这解释了为什么小规模并行容易见效、大规模并行收益递减。

T并行≈T串行N+T通信\displaystyle T_{\text{并行}} \approx \frac{T_{\text{串行}}}{N} + T_{\text{通信}}

在真实的厨房里,传菜通道只有那么宽——菜做得再快,传不过来也是白搭。分布式系统同理:设备越多,通信拓扑越复杂,通信与计算的比值(computation-to-communication ratio)就成了衡量扩展效率的核心指标。

数据并行策略

最经典的大锅饭做法是:食材按桌数平均分配到每口锅,每个厨子手里都有一份完整菜谱,炒完后把各自调好的“酱汁”汇到一起拌匀。这就是数据并行:每台设备保存一份完整的模型参数,各自基于本地的 batch 分片计算梯度,最后通过 AllReduce 通信把梯度汇总、求平均,再同步更新参数。

数据并行的优势在于实现简单。在 JAX 中,只需要把 batch 维度映射到 'data' 轴,模型参数用 PartitionSpec(None, None) 保持完整复制即可。训练开始后,每台设备独立完成前向和反向计算,得到局部梯度:

gˉ=1N∑i=1Ngi,θ←θ−η⋅gˉ\displaystyle \bar{g} = \frac{1}{N} \sum_{i=1}^{N} g_i, \qquad \theta \leftarrow \theta - \eta \cdot \bar{g}

1. 局部梯度:第 \(i\) 台设备在自己的数据分片上反向传播,得到局部梯度 \(g_i\)。

gi=∇θLi(θ)\displaystyle g_i = \nabla_{\theta} L_i(\theta)

2. 梯度平均:所有设备通过 AllReduce 通信把各自梯度求和再除以设备数,得到全局梯度。

gˉ=1N∑i=1Ngi\displaystyle \bar{g} = \frac{1}{N} \sum_{i=1}^{N} g_i

3. 参数更新:每台设备用相同的全局梯度更新本地参数副本,保证模型一致。

θ←θ−η⋅gˉ\displaystyle \theta \leftarrow \theta - \eta \cdot \bar{g}

大锅饭的缺点也很明显:如果菜谱太大,一口锅装不下所有配料,数据并行就无能为力——因为每台设备都必须持有完整模型副本。这也是为什么当模型规模超过单卡显存时,人们开始转向模型并行。

模型并行策略

当“一口锅装不下整份菜谱”时,后厨会换一种协作方式:不再每口锅复制同样的食材,而是把整桌宴席拆成几道大菜,不同灶台各负责一道。在分布式训练里,这就是模型并行:把模型本身切碎,分配到不同设备上。

常见的模型并行有三类:参数分片(如 ZeRO,把优化器状态、梯度、参数分布到各设备)、张量并行(一个 Transformer 层的矩阵乘被切成多块,不同设备各算一部分)、流水线并行(不同层放在不同设备,前向传播像流水线一样按层传递)。三者可以组合,构成二维甚至三维的并行策略。张量并行最简单直观的例子是切分权重矩阵:

W=[ W1∣W2 ],y=xW=[ xW1,  xW2 ]\displaystyle W = [\,W_1 \mid W_2\,], \qquad y = xW = [\,xW_1,\; xW_2\,]

1. 按列切分权重:参数矩阵 \(W\) 的列被分成两部分 \(W_1\) 和 \(W_2\),属于张量并行。

W=[ W1∣W2 ]\displaystyle W = [\,W_1 \mid W_2\,]

2. 分布计算:设备 1 计算 \(xW_1\),设备 2 计算 \(xW_2\),输入 \(x\) 可以复制或分片。

xW1,xW2\displaystyle xW_1, \quad xW_2

3. 拼接结果:把两个部分结果沿特征维拼接,就得到与原始矩阵乘相同的结果。

y=[ xW1,  xW2 ]\displaystyle y = [\,xW_1,\; xW_2\,]

这就像把一道复杂的佛跳墙拆成“各自炖汤、最后合盅”。模型并行让超大模型的训练成为可能,但每一层都涉及设备间的通信——切得越细,通信越频繁,因此实际中常把张量并行与现代通信技术(如 NVLink、ICI)配合使用。

AI 应用场景

大规模语言模型训练(TPU Pod)

阶段:模型训练与并行策略设计

解决的问题:千亿参数模型的权重、梯度和优化器状态远超单卡显存,传统手写多机多卡训练代码需要人工处理张量切分、梯度聚合和容错,开发成本极高。JAX 的 SPMD 将分片逻辑收敛为 Mesh + PartitionSpec 声明,编译期自动生成 AllReduce 等通信指令,开发者的注意力从“通信细节”回归“模型结构”。

具体案例:某团队在 64 个 TPU(8×8 逻辑网格)上训练 170B 参数生成模型,数据沿 'data' 轴并行、参数沿 'model' 轴分片,同时叠加张量并行与流水线并行。最终硬件利用率(MFU)达到 55% 以上,训练脚本与单卡版本保持高度一致。

多卡 GPU 微调视觉模型

阶段:迁移学习 / 模型微调

解决的问题:单卡微调 ViT 时 batch size 太小,BatchNorm 统计不稳定,模型收敛慢。数据并行可以扩大有效 batch,但用 PyTorch DDP 等多进程方案时,梯度通信、权重同步、分布式采样器都需要额外处理。JAX 只需在 jax.jit 的函数签名里声明 in_shardings,即可让同一份训练函数横跨多卡运行。

具体案例:算法工程师在 4 张 A100 上微调 ViT-Large,将 batch size 从 32 提升到 128,训练吞吐提高约 3.5 倍;代码变更仅为增加 mesh 定义和分片声明。

分布式强化学习采样

阶段:数据采集与策略训练

解决的问题:强化学习的瓶颈常在环境采样——单进程跑模拟器太慢。SPMD 可以让每个设备独立运行一组环境模拟器,各自产生经验数据并计算梯度,最后通过 AllReduce 聚合,从而把采样吞吐提升一个数量级。

具体案例:某机器人控制项目用 JAX 在 8 个 TPU core 上并行跑数百个环境实例,每步采样量提升约 8 倍,训练耗时减半。

Python 演示

python43 行
import jaximport jax.numpy as jnpfrom jax.sharding import Mesh, PartitionSpec, NamedSharding # 1. 查看当前可见的物理设备(如 8 张 GPU/TPU core)devices = jax.devices()print("物理设备数:", len(devices)) # 2. 建立逻辑设备网格:一维网格,轴名为 "data"mesh = Mesh(devices, ("data",)) # 3. 同一份程序:一个两层 MLPdef mlp(x, w1, b1, w2, b2):    return jnp.tanh(x @ w1 + b1) @ w2 + b2 # 4. 构造输入与参数x = jnp.ones((64, 128))w1 = jnp.ones((128, 64))b1 = jnp.zeros((64,))w2 = jnp.ones((64, 16))b2 = jnp.zeros((16,)) # 5. 声明分片:x 沿 data 轴切分,模型参数完整复制data_sharding = NamedSharding(mesh, PartitionSpec("data", None))model_sharding = NamedSharding(mesh, PartitionSpec(None, None)) # 6. 编译为 SPMD 程序mlp_spmd = jax.jit(    mlp,    in_shardings=(        data_sharding,        model_sharding,        model_sharding,        model_sharding,        model_sharding,    ),    out_shardings=data_sharding,) # 7. 执行:每个设备持有 64/设备数 行,参数在每台设备上完整保留y = mlp_spmd(x, w1, b1, w2, b2)print("输出形状:", y.shape)print("输出分片设备:", sorted({d.id for d in y.devices}))
目的:在 JAX 中演示最基础的 SPMD 分布式执行——同一份 MLP 程序被复制到多台设备,输入数据按 batch 维分片,模型参数完整复制,最终输出也按 batch 维分布在所有设备上。
  1. mesh = Mesh(devices, ("data",))定义逻辑网格,把物理设备绑定到名为 data 的逻辑轴上
  2. PartitionSpec("data", None)声明“第 0 维被切分,第 1 维不切分”
  3. NamedSharding(mesh, ...)把网格和分片规范合成一个完整的 sharding 对象
  4. x = jnp.ones((64, 128))初始张量在 host 上创建,传给带 sharding 约束的 jit 后会被自动分发
  5. in_shardings=(...)描述每个入参的分片方式,函数内部自动推导中间结果的分片
  6. out_shardings=data_sharding描述输出也是按 batch 维分片的,避免不必要的收集
  7. mlp_spmd(...)调用时 JAX 只编译一次,生成多设备上的 SPMD 可执行程序
  8. y.devices查看输出张量实际分布在哪些物理设备上,验证分片是否生效

常见误区

误区写了 SPMD 程序之后,每台设备应该都能看到所有数据,只是大家轮流算,不然怎么会叫“多数据”呢?

正解SPMD 的关键恰恰相反——每台设备只持有数据的一个分片。就像四位厨子各管一口锅,一号锅厨子根本不需要碰牛肉以外的食材。设备只在自己本地的数据分片上运算,需要全局信息时才通过通信原语交换。这是它能省内存、提升吞吐的根本原因。

为何常见:很多初学者把“多数据”理解为“每台设备都有完整数据”,忽略了 SPMD 与数据并行共享的核心前提:分片并分布。

误区分片计算完了,我想在 host 端打印一下结果,直接 `.numpy()` 不就行了吗?反正数据已经在设备上了。

正解当一个张量按 SPMD 分布在多台设备上时,调用 .numpy()、.tolist() 或 .item() 会在后台触发一次全局数据收集(gather),把所有设备上的分片搬运到 host 并拼接成完整数组。这个操作涉及大量通信,代价与张量大小和设备数成正比,绝不是零成本的。

为何常见:单设备编程中 .numpy() 只是把显存拷回内存,人们习惯了它“很快”,忽略了多设备环境下它隐含的分布式通信。

误区用 4 台设备训练,batch size 必须是 4 的倍数,比如 64、128,否则程序一定会报错。

正解JAX 的 sharding 机制允许不均匀分片:当 batch 不能被设备数整除时,部分设备会分到略多或略少的样本,B_{\text{local}} = \lceil B / N \rceil,不整除的最后一批自然少一点。实际使用中,为了性能和数值稳定性,通常还是会选可整除的 batch,但“不整除就报错”是不成立的。

为何常见:许多框架的数据加载器按等分批次报错,让人们误以为 SPMD 也一样严格。

误区`vmap` 不是一次处理一批数据吗?它在内部把 batch 展开、并行计算,这不就是 SPMD 吗?

正解vmap 是向量化映射,它在单设备内把循环改为 SIMD/XLA 向量运算,不涉及多设备通信;SPMD 则是在多设备上复制程序并交换数据。二者解决的问题不同:vmap 消除 Python 循环开销,SPMD 消除“设备不够用”的瓶颈。它们也常被组合使用,但绝不能画等号。

为何常见:从命名上看,vmap 中的“多数据”和 SPMD 很像,初学者容易混淆单设备向量化与多设备并行的边界。

面试题

Q1. 关于SPMD的正确描述是?

单选题 · easy

  • A.多个程序并行运行,每个程序处理相同数据集
  • B.单个程序复制到多设备,各自处理数据分片← 正确答案
  • C.单设备同时执行多条指令流和多个数据流
  • D.所有设备执行相同代码且共享同一份内存数据

答案:B。SPMD 的“单程序”指同一份源码被复制到所有进程,“多数据”指每台设备只处理自己的数据分片。A 是 MPMD 的特征,C 更接近超标量/多线程,D 的“共享内存”违背了分布式内存的基本假设。

Q2. jax.sharding.Mesh 的作用是什么?

单选题 · easy

  • A.把逻辑轴名称和物理设备集合绑定在一起← 正确答案
  • B.替代AllReduce完成设备间梯度同步
  • C.自动把模型参数保存到磁盘进行检查点
  • D.负责计算JAX函数的自动微分

答案:A。Mesh 是 JAX 中连接“逻辑轴名”(如 data、model)与“物理设备”的中介。PartitionSpec 中的轴名通过 Mesh 解析为具体设备编号。B、C、D 分别属于通信、持久化、自动微分模块,与设备映射无关。

Q3. PartitionSpec('data', None) 表示什么?

单选题 · mid

  • A.第二维按data轴切分,而第一维不切分
  • B.两个维度都按data轴进行并行切分
  • C.第一维按data轴切分,第二维不切分← 正确答案
  • D.None表示两个维度都不进行切分

答案:C。PartitionSpec 中每个位置对应张量的一个维度,字符串表示该维映射到哪个网格轴,None 表示该维不切分。因此第 0 维(第一维)按 data 轴切,第 1 维(第二维)保持完整。

Q4. 数据并行训练中,AllReduce 解决什么问题?

单选题 · mid

  • A.将模型参数广播到所有设备数据副本
  • B.聚合各设备局部梯度为全局梯度← 正确答案
  • C.将数据集划分为各设备的数据分片
  • D.将输出结果切分成若干小分片

答案:B。数据并行中每台设备算出局部梯度后,需要 AllReduce 把所有梯度求和或求平均,得到全局梯度后再同步更新参数。A 是初始化阶段的操作,C 是数据加载阶段的划分,D 是输出分片,都不属于 AllReduce 的核心职责。

Q5. 以下哪些 API 可以用来表达 SPMD 分片?

多选题 · mid

  • A.pmap← 正确答案
  • B.jax.jit 加 in_shardings 参数← 正确答案
  • C.jax.vmap 向量化映射
  • D.jax.random 密钥生成

答案:AB。pmap 是 JAX 早期并行原语,自动切分第 0 维;jax.jit(..., in_shardings=...) 是当前推荐的 SPMD 声明方式。vmap 是单设备向量化,random 是随机数模块,都与“跨设备分片”无关。

Q6. 在 JAX 中,把分片张量转回完整数组的常见操作是什么?

单选题 · hard

  • A.调用 numpy() 或 tolist() 触发全局收集← 正确答案
  • B.pmap 返回后自动隐藏所有分片细节
  • C.reshape 即可完成跨设备的复制合并
  • D.jit 编译时可隐式合并所有分片数据

答案:A。分片张量在 host 端是不可见的,调用 .numpy() 会触发跨设备的全量收集(all-gather),这是有通信代价的操作。reshape 只能改变单设备上的张量形状,pmap 和 jit 都不会自动做全局合并。

Q7. 关于 SPMD 程序与单设备程序的关系,正确的有?

多选题 · mid

  • A.用户维护一份源码,编译后多设备执行← 正确答案
  • B.分片信息在函数边界声明,内部自动推导← 正确答案
  • C.必须手写循环把每台设备的结果合并
  • D.必须为每台设备维护独立的源码分支

答案:AB。SPMD 的核心价值就是“一份程序、多处运行”,用户只需在 jit 的边界声明分片,JAX/XLA 自动完成推导和通信生成。C 和 D 描述的正是用户想避免的重复劳动。

Q8. 当模型参数巨大、单设备装不下时,更适合采用哪种策略?

单选题 · mid

  • A.纯数据并行复制完整模型到每台设备
  • B.模型分片或张量并行来拆分参数← 正确答案
  • C.增大 batch size 直到内存刚好装下
  • D.减少设备数量来节省显存空间

答案:B。模型并行(参数分片、张量并行、流水线并行)的核心动机就是突破单卡显存上限。数据并行要求每台设备有完整模型副本,增大 batch 只会占用更多显存,减少设备数量反而降低并行度。

Q9. Amdahl 定律说明了什么?

单选题 · easy

  • A.加速比受串行比例和设备数量共同制约← 正确答案
  • B.设备越多加速比提升幅度无限增大
  • C.并行程序的速度只受到内存带宽影响
  • D.串行代码越多加速比反而越高

答案:A。Amdahl 定律 \(S = 1 / ((1-f) + f/N)\) 说明,即使设备数趋于无穷,加速比也受限于 \(1/(1-f)\)。因此串行比例越低、设备越多,加速比越大,但存在天花板。

Q10. 以下哪些思路有助于提高 SPMD 的可扩展性?

多选题 · hard

  • A.增大每设备 batch 摊薄通信开销← 正确答案
  • B.减少 AllReduce 次数或用梯度累积← 正确答案
  • C.让每设备的计算尽量本地化← 正确答案
  • D.完全避免跨设备通信依赖

答案:ABC。增大 batch 可以提高计算/通信比,梯度累积能降低通信频率,计算本地化能减少不必要的数据搬运。D 绝对化——分布式训练必然包含通信,完全避免不现实也不必要。