清晨六点,城市中心最大的中央厨房开始了忙碌的一天。后厨主管老张站在调度板前,面前是一份五百人宴会的订单。他有两种办法可以按时出菜:第一种,把五百份红烧肉分给五位厨师,每人负责一百份,大家用同样的菜谱、同样的工序,同时开火;第二种,把厨房拆成洗菜组、切菜组、炒菜组、摆盘组,每组的活儿不一样,但流水线一环扣一环。老张选了哪种?其实,现代计算机在处理海量数据时,面临的和老张一模一样的选择——这就是数据并行与任务并行。 这两个概念是并行计算的基石。假设你手里有一万张照片要加滤镜,一台电脑要跑一个小时;但如果把照片分成十份,分给十台电脑同时处理,六分钟就能搞定——这正是“把同样的事分给多人干”的数据并行。反过来,如果一家公司的数据处理流程包含抓取、清洗、分析、出报告四个环节,每个环节由不同的服务器专门负责,像流水线一样协同推进——这便是“不同的人干不同的事”的任务并行。

岗位关联

理解这两种并行模式,是设计高吞吐AI系统、分布式训练框架和多核加速方案的底层能力。无论你是做推荐系统、大模型训练还是实时推理服务,并行策略的选择直接决定了系统的吞吐量与成本。

目录
  1. 可视化演示
  2. 两种并行初探
  3. 工作拆分策略
  4. 任务调度分配
  5. 通信开销对比
  6. 负载均衡技巧
  7. 适用场景分析
  8. 混合并行设计
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

展示IEEE 754单精度与双精度浮点数的二进制表示,动态演示符号位、指数位、尾数位的编码过程与精度差异。

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

IEEE 754单精度浮点数表示 符号位(S):1位,0正1负 指数位(E):8位,偏移量127 尾数位(M):23位,隐含前导1 V = (-1)^S × 1.M × 2^(E-127)

两种并行初探

中央厨房的老张站在调度板前,心里默念两种解法。解法一叫“复制厨师”:五个厨师,同一本菜谱,每人面前摆一百份同样的五花肉,动作整齐划一。解法二叫“分工流水线”:一个人专门焯水,一个人专门炒糖色,一个人专门收汁——每道工序的人只做自己那一手绝活。前者的灵魂是“大家做同一件事,各管各的料”;后者的灵魂是“大家做不同的事,前后接力”。老张的直觉告诉他,有些菜适合复制厨师,有些菜适合流水线。

在计算机世界里,这两种直觉被命名为数据并行和任务并行。数据并行(Data Parallelism)将同一个操作施加到不同的数据切片上:比如对一万张图片做缩放,GPU 上上千个核心同时执行“缩放”这个操作,各自处理不同的图片区域。任务并行(Task Parallelism)则将不同的操作分配给不同的处理单元:比如一个推荐系统服务,用户请求先经过鉴权模块,再进特征工程模块,最后到模型推理模块,每个模块由独立的服务器或线程承担。

从硬件指令集的角度看,这两者分别对应 SIMD(单指令多数据流)和 MIMD(多指令多数据流)两种架构。SIMD 就像阅兵方阵,指挥官喊一声“向右转”,所有人同时执行同一个动作;MIMD 则像交响乐团,指挥挥一下棒,小提琴、铜管、打击乐各自演奏不同的声部。现代 CPU 的向量寄存器(如 AVX-512)是 SIMD 的典型代表,而多核 CPU 集群则是 MIMD 的舞台。

衡量并行效果的核心指标是加速比。假设单核处理耗时 $T_1$,使用 $p$ 个处理核后耗时为 $T_p$,加速比定义为:

理想情况下 $S_p = p$,但现实中由于通信、同步和负载不均,$S_p$ 通常小于 $p$。

老张的直觉在计算机里得到了精确的量化:如果五个厨师做五百份红烧肉,理想加速比是 5,但切肉时互相借案板、起锅时抢灶台,实际可能只有 4.2。这个“损耗”就是通信与同步开销——它将在后文中反复出现,成为并行系统设计的头号敌人。

Sp=T1TpS_p = \frac{T_1}{T_p}
  1. 单核耗时 $T_1$指整个任务在单个处理单元上串行执行所需的总时间,它是对比的基准线。
    T1=串行执行总耗时T_1 = \text{串行执行总耗时}
  2. 并行耗时 $T_p$指任务在使用 $p$ 个处理单元并行执行后的总耗时,包含计算时间和通信时间。
    Tp=Tcompute+TcommT_p = T_{\text{compute}} + T_{\text{comm}}
  3. 加速比 $S_p$单核耗时与并行耗时的比值,反映并行带来的收益倍数。
    Sp=T1TpS_p = \frac{T_1}{T_p}

工作拆分策略

老张的第二课,是怎么把五百份菜分下去。他有两种切法:第一种叫“按块切”,把五百份五花肉切成五堆,每堆一百份,整块整块地分给厨师,厨师拿到手的是一块完整的大肉;第二种叫“循环切”,把五百份肉切成薄片,按“你一片、我一片”的方式轮流分给五个人,保证每个人拿到的肉片肥瘦均匀。计算机的数据划分,恰好也有这两种经典方式——块划分与循环划分。

块划分(Block Partition)将长度为 $N$ 的数据按连续区间切成 $p$ 块,第 $i$ 个处理单元分到第 $i$ 块。它的优点是每个单元的数据在内存中连续,局部性好,适合需要频繁访问相邻数据的计算(如卷积操作)。循环划分(Cyclic Partition)则将数据按 $i \bmod p$ 的规则轮流分配给各单元,让每个单元拿到“均匀混合”的数据切片,在负载不均衡时能有效避免“一个人撑死、一个人饿死”的局面。

任务分解是另一条路线。与数据划分关注“数据怎么切”不同,任务分解关注“工序怎么拆”。以电商订单处理流程为例,它可以被分解为订单校验、库存扣减、支付处理、通知发送四个独立任务,形成一个流水线。在任务并行中,每个处理单元专职负责一个或多个任务,数据在任务之间单向流动。

拆分粒度(Granularity)是决定性能的关键权衡。粒度太粗,并行度不够,多核闲着没事干;粒度太细,任务切换与通信开销会淹没计算收益。假设总计算量为 $W$,切成 $n$ 个任务,每个任务的开销为 $O_{\text{task}}$,则并行效率受以下关系制约:

回到老张的厨房:如果他把五百份肉切成五千片再分给五人,每个人光数片就得花半天——这就是细粒度带来的调度开销。好的拆分策略,是在“够并行”和“不过碎”之间找到甜点区。这一权衡贯穿所有并行系统设计,从多核线程到分布式集群无一例外。

E=WW+n×OtaskE = \frac{W}{W + n \times O_{\text{task}}}
  1. 总计算量 $W$表示完成所有工作所需的纯计算时间,不含任何调度或通信开销。
    W=∑i=1nwiW = \sum_{i=1}^{n} w_i
  2. 任务数 $n$将工作拆分成的子任务数量,$n$ 越大并行潜力越高,但开销也随之增加。
    n=Wwavgn = \frac{W}{w_{\text{avg}}}
  3. 任务开销 $O_{\text{task}}$每个子任务在创建、调度、销毁过程中产生的额外时间成本。
    Otask=Ocreate+Oschedule+OdestroyO_{\text{task}} = O_{\text{create}} + O_{\text{schedule}} + O_{\text{destroy}}
  4. 并行效率 $E$有效计算时间占总时间的比例,$E$ 越接近 1,说明开销占比越低。
    E=WW+n×OtaskE = \frac{W}{W + n \times O_{\text{task}}}

任务调度分配

现在老张面临第三道难题:五位厨师的活儿怎么派?他可以在早晨开工前一小时就把每道菜分配给指定厨师,大家按部就班;也可以设一个“领菜窗口”,哪位厨师做完手里的菜,就自己来窗口领下一道。前一种方式在计算机里叫静态调度,后一种叫动态调度。

静态调度(Static Scheduling)在编译期或任务启动前就确定了每个任务与处理单元的映射关系。它的优点是调度开销几乎为零,运行时不需要任何决策;缺点是如果任务执行时间预估不准,就会造成严重的负载不均。静态调度适合任务执行时间稳定、可预测的场景,比如矩阵乘法中每个分块的计算量完全一样。

动态调度(Dynamic Scheduling)则把任务放入一个共享的工作池(Work Pool),每个处理单元完成任务后主动从池中领取新任务。这种“按需领取”的模式天然具备负载均衡能力——干得快的单元会自动领到更多任务。工作池的实现通常需要一个线程安全的队列,并配套条件变量实现同步。

调度策略的选择可以用一个简单的模型来理解。假设有 $p$ 个处理单元和 $n$ 个任务,静态调度将每个单元分配 $n/p$ 个任务;动态调度则让每个单元领取数量不等的任务。设任务 $j$ 的执行时间为 $t_j$,静态调度下单元 $i$ 的总耗时是:

静态调度就像老张提前把菜谱贴到每个灶台上,厨师们不需要思考,照做即可。但万一某位厨师手速慢,其他四位做完也只能等他——这就是静态调度的风险。而工作池模式如同“领菜窗口”,手快的厨师多做几道,手慢的少做几道,整体完工时间反而更短。动态调度牺牲了一点调度开销,换来了更优的负载均衡。

Tstatic(i)=∑j∈blockitjT_{\text{static}}^{(i)} = \sum_{j \in \text{block}_i} t_j
  1. 任务集合 $\text{block}_i$静态调度分配给第 $i$ 个处理单元的任务集合,由调度器预先划分。
    blocki={j∣i×np≤j<(i+1)×np}\text{block}_i = \{j \mid i \times \frac{n}{p} \le j < (i+1) \times \frac{n}{p}\}
  2. 单元耗时 $T_{\text{static}}^{(i)}$第 $i$ 个单元执行完自己分内所有任务所需的时间,等于其分得任务的时间总和。
    Tstatic(i)=∑j∈blockitjT_{\text{static}}^{(i)} = \sum_{j \in \text{block}_i} t_j
  3. 整体完成时间并行系统的总耗时取决于最慢的那个单元——木桶效应。
    Ttotal=max⁡iTstatic(i)T_{\text{total}} = \max_{i} T_{\text{static}}^{(i)}

通信开销对比

老张的厨房里最热闹的时刻不是炒菜,而是“传菜”。做红烧肉的五个厨师,如果每人负责一百份,那他们之间几乎不需要交流;但如果做的是“佛跳墙”——一道菜要经过五个人的手,每做完一步就要传给下一个人,那传菜的时间就变得至关重要。计算机里,数据并行和任务并行的通信模式也截然不同。

数据并行中的通信主要体现为同步屏障(Barrier)。每个处理单元完成自己那部分计算后,必须等待所有其他单元都完成,才能进入下一轮迭代。比如训练一个神经网络,每算完一个 batch 的梯度,所有 GPU 必须做一次 AllReduce 汇总,再统一更新参数。同步的频率越高,通信开销越大。设每轮计算时间为 $C$,通信时间为 $M$,则一轮迭代的总时间为:

任务并行中的通信则是流水线传递。每个单元的输出就是下一个单元的输入,数据在单元之间单向流动。与数据并行不同,任务并行的通信是点对点的,不需要全局同步。但流水线有一个天然短板:整条线的吞吐量由最慢的环节决定。设流水线有 $k$ 级,每级耗时 $s_i$,则处理一个数据项的总延迟为:

老张很快发现了一个残酷的规律:菜做得越快,传菜时间占比就越高。计算从 10 分钟优化到 1 分钟,但传菜还是 1 分钟,那总时间只从 11 分钟降到 2 分钟——加速比远达不到预期。这引出了并行系统设计中最重要的原则之一:通信与计算重叠。优秀的系统会让单元在等待数据时先做本地能做的计算,让通信的“等待”与计算的“忙碌”互相掩蔽,从而把 $M$ 从关键路径上“藏”起来。

Titer=C+MT_{\text{iter}} = C + M
L=∑i=1ksiL = \sum_{i=1}^{k} s_i
  1. 计算时间 $C$处理单元完成本地计算所需的时间,不包含任何数据交换。
    C=WpC = \frac{W}{p}
  2. 通信时间 $M$同步屏障或数据传输所花费的时间,与数据量和网络带宽有关。
    M=DBM = \frac{D}{B}
  3. 迭代耗时 $T_{\text{iter}}$数据并行中一轮完整迭代的总时间,是计算与通信的简单叠加。
    Titer=C+MT_{\text{iter}} = C + M
  4. 流水线延迟 $L$任务并行中一个数据项从进入流水线到离开的总时间,等于各级耗时之和。
    L=∑i=1ksiL = \sum_{i=1}^{k} s_i

负载均衡技巧

周五晚高峰,老张注意到一个奇怪的现象:负责糖醋排骨的张厨师忙得满头大汗,负责清蒸鱼的李厨师却闲得看手机。原因很简单——点糖醋排骨的客人多,点清蒸鱼的客人少。这是任务并行中最典型的负载不均衡问题。而在数据并行中,如果某些数据块的处理难度远高于其他块,同样会出现“一人拖垮全队”的长尾效应。

解决负载不均衡有两类策略:静态均衡与动态均衡。静态均衡在任务划分时就尽量让每个单元分到等量的工作,适合任务耗时均匀的场景。它的依据是预估——提前估算每个分块的计算量,然后按计算量比例分配。但预估总有误差,于是出现了动态均衡:在运行时根据各单元的实际完成进度动态调整任务分配。

动态均衡最著名的实现是工作窃取(Work Stealing)。每个处理单元维护一个本地任务队列,当自己的队列空了,就随机从其他繁忙单元的队列尾部“偷”一个任务过来执行。工作窃取算法的核心思想是:让忙的单元更忙,闲的单元主动找活干,从而在运行时自动收敛到均衡状态。

负载均衡的效果可以用不均衡度来量化。设 $p$ 个单元的实际耗时分别为 $T_1, T_2, \dots, T_p$,理想均衡时每个单元耗时应为 $\bar{T} = \frac{1}{p}\sum T_i$。不均衡度定义为:

老张最终为厨房引入了“动态领菜”制度:每道菜做好后统一放在出菜口,谁空闲谁就来取下一道菜。这样即使糖醋排骨点单量翻倍,其他厨师也能赶来支援。工作窃取与之异曲同工——它不预设谁该干什么,而是让空闲的处理器主动去“偷”别人的任务,在运行时自适应地消除不均衡。$\Phi$ 越低,系统的资源利用率越高,整体吞吐量也越接近理论峰值。

Φ=max⁡iTi−TˉTˉ\Phi = \frac{\max_i T_i - \bar{T}}{\bar{T}}
  1. 实际耗时 $T_i$第 $i$ 个处理单元完成分配任务的总耗时,包含计算与等待时间。
    Ti=tcompute(i)+tidle(i)T_i = t_{\text{compute}}^{(i)} + t_{\text{idle}}^{(i)}
  2. 平均耗时 $\bar{T}$所有单元耗时的平均值,代表理想的均衡完成时间。
    Tˉ=1p∑i=1pTi\bar{T} = \frac{1}{p}\sum_{i=1}^{p} T_i
  3. 不均衡度 $\Phi$最慢单元偏离平均水平的比例,$\Phi$ 越大,木桶效应越严重。
    Φ=max⁡iTi−TˉTˉ\Phi = \frac{\max_i T_i - \bar{T}}{\bar{T}}

适用场景分析

老张现在明白了两种模式的区别,但他更关心的是:什么菜适合复制厨师,什么菜适合流水线?他总结出一条经验:如果食材相同、工序相同,就适合复制厨师——五个灶台同时做红烧肉,效率翻五倍;如果食材不同、工序各异,就适合流水线——洗、切、炒、摆各司其职,谁也替代不了谁。计算机的选型逻辑如出一辙。

数据并行适合同构计算场景:操作是统一的,数据是可分割的。经典的例子包括图像处理(对每张图片做同样的滤镜)、矩阵运算(对每个矩阵块做同样的乘法)、神经网络训练(对每个 batch 做同样的前向反向传播)。这些场景的共同特征是“高计算密度、低通信频率”——每个数据块的计算量大,块与块之间不需要频繁交换信息。

任务并行适合异构流水线场景:流程由多个功能不同的阶段组成,各阶段之间存在明确的数据依赖。经典的例子包括 ETL 数据管道(抽取、转换、加载各由独立模块负责)、视频编码流水线(采集、编码、封装、推流)、Web 服务架构(网关、鉴权、业务逻辑、存储各自独立部署)。这些场景的共同特征是“阶段间耦合弱、阶段内计算量可独立扩展”。

选择准则可以用一个简单的决策问题来归纳:问自己一句——并行的是“数据”还是“流程”? 如果是同一份操作要跑在不同数据上,选数据并行;如果是一份数据要经过不同操作,选任务并行。但真实世界往往没有这么干净,复杂系统通常是两者交织。一个深度学习训练平台,节点间用数据并行切分训练数据,节点内用任务并行管理数据加载、预处理和梯度更新——这便引出了本文的最后一个主题:混合并行。

混合并行设计

老张的中央厨房最终没有二选一,而是把两种模式融合在了一起。工作日中午,他安排五位厨师同时做红烧肉套餐(数据并行),但套餐里的米饭、例汤、凉菜由专门的小吃组负责(任务并行)。两种模式各管一段,在时间和空间上巧妙叠加,最终实现了五百份套餐的准时出餐。

在计算机系统中,这种融合被称为混合并行。最经典的实践是 MPI + OpenMP 分层模型:在分布式集群的多个节点之间使用 MPI(Message Passing Interface)实现数据并行——每个节点处理一份数据分片,节点间通过网络通信同步;而在单个节点内部,使用 OpenMP 实现任务并行——将节点的多个 CPU 核心组织成流水线或任务池,处理节点分到的数据块。这种分层设计兼顾了分布式系统的横向扩展能力和多核处理器的纵向计算密度。

混合并行的设计核心是资源建模与权衡。设集群有 $P$ 个节点,每个节点有 $C$ 个核心,总核心数为 $P \times C$。一个自然的拆分策略是:将数据分成 $P$ 份做节点间数据并行,每个节点内将计算分解为 $C$ 个任务做节点内任务并行。选择何种组合,取决于通信层次的特征——节点间网络延迟高(毫秒级),节点内共享内存延迟低(纳秒级),因此节点间应尽量减少通信,节点内可以频繁同步。

设计混合并行时,通常用以下代价模型来指导决策。设节点间通信开销为 $O_{\text{inter}}$,节点内通信开销为 $O_{\text{intra}}$,则每轮迭代的总开销为:

老张最后在厨房墙上贴了一张便签:“复制厨师管量,流水线管序,两者叠加管全局。” 混合并行的工程本质就是这样一种权衡艺术——把通信最频繁的操作放在共享内存层(节点内),把需要海量数据的计算放在分布式层(节点间),让每一级硬件都做自己最擅长的事。没有放之四海而皆准的最优配置,只有针对具体负载特征的精雕细琢。

---

Ttotal=Tcompute+Ointer+OintraT_{\text{total}} = T_{\text{compute}} + O_{\text{inter}} + O_{\text{intra}}
  1. 计算时间 $T_{\text{compute}}$所有核心并行计算所花费的纯计算时间,与数据量和每核心计算速度有关。
    Tcompute=WtotalP×C×vT_{\text{compute}} = \frac{W_{\text{total}}}{P \times C \times v}
  2. 节点间通信 $O_{\text{inter}}$跨网络的数据传输与同步开销,通常比节点内通信高一到两个数量级。
    Ointer=DinterBnetworkO_{\text{inter}} = \frac{D_{\text{inter}}}{B_{\text{network}}}
  3. 节点内通信 $O_{\text{intra}}$共享内存或线程间同步的开销,延迟低但频率高。
    Ointra=k×tlockO_{\text{intra}} = k \times t_{\text{lock}}
  4. 总开销 $T_{\text{total}}$混合并行每轮迭代的整体耗时,设计目标是最小化三者之和。
    Ttotal=Tcompute+Ointer+OintraT_{\text{total}} = T_{\text{compute}} + O_{\text{inter}} + O_{\text{intra}}

AI 应用场景

大模型分布式训练中的混合并行

阶段:模型训练

解决的问题:训练千亿参数大模型时,单块 GPU 显存无法容纳模型和数据,需要将模型参数与训练数据分布到数千块 GPU 上。纯数据并行会导致每块 GPU 都要存一份完整模型副本,显存爆炸;纯任务并行又会因层间依赖导致通信过于频繁。混合并行将模型的不同层分配到不同 GPU(任务并行),同层的参数更新采用数据并行,显存与通信取得平衡。

具体案例:Meta 训练 LLaMA-65B 时,在 2048 块 A100 GPU 上采用了“数据并行 × 流水线并行 × 张量并行”的三维混合并行策略。模型层间用流水线并行(任务并行),层内矩阵乘法用张量并行,多个数据副本间用数据并行。训练吞吐量达到 380 TFLOPs/GPU,相比单一并行模式提升了 3-5 倍。

推荐系统特征管道中的双模式并行

阶段:特征工程与推理服务

解决的问题:推荐系统的特征管道包含日志解析、特征抽取、特征拼接、模型打分多个阶段,且每天要处理数十亿条用户日志。传统串行管道处理一条日志需要 200ms,完全无法满足实时推荐的需求。通过任务并行将各阶段部署为独立微服务构成流水线,同时每个阶段内部用数据并行将日志分片到多实例并行处理,端到端延迟降至 30ms。

具体案例:某短视频平台的实时推荐系统采用 Flink 构建特征流水线(任务并行),每个算子的并行度设为 32(数据并行),结合 Kafka 消息队列实现削峰填谷。在晚高峰每秒 80 万请求的压力下,P99 延迟稳定在 45ms 以内。

计算机视觉中的数据并行推理加速

阶段:模型推理

解决的问题:工业质检场景需要对生产线上的每一帧图像实时检测缺陷,单 GPU 推理速度仅能处理 30 FPS,而产线要求 120 FPS。利用数据并行将连续的视频帧分发给 4 块 GPU 组成推理集群,每块 GPU 负责不同的帧,并通过负载均衡策略应对帧检测难度的波动。

具体案例:某工厂的质检系统将 4 块 NVIDIA T4 GPU 组成推理集群,使用 Nginx 将视频流按帧平均分发,配合工作窃取策略动态调整分配,最终实现 150 FPS 的检测吞吐量,漏检率低于 0.1%。

Python 演示

python68 行
import multiprocessing as mpimport timeimport random def process_chunk(data_chunk):    """数据并行: 每个worker对一块数据做相同操作"""    result = 0    for x in data_chunk:        result += x * x        time.sleep(0.0001)  # 模拟计算耗时    return result def pipeline_stage(data, stage_id, queue_out):    """任务并行: 流水线中的一个处理阶段"""    processed = []    for item in data:        if stage_id == 0:            item = item * 2          # 阶段1: 放大        elif stage_id == 1:            item = item + 1          # 阶段2: 偏移        elif stage_id == 2:            item = item - 5          # 阶段3: 修正        processed.append(item)    queue_out.put(processed) def data_parallel_demo(data, num_workers=4):    """演示数据并行: 数据切块 -> 并行处理 -> 汇总"""    chunk_size = len(data) // num_workers    chunks = [data[i*chunk_size:(i+1)*chunk_size] for i in range(num_workers)]    with mp.Pool(num_workers) as pool:        results = pool.map(process_chunk, chunks)    return sum(results) def task_parallel_demo(data):    """演示任务并行: 流水线三个阶段各自处理"""    queues = [mp.Queue() for _ in range(3)]    processes = []    prev_data = data    for stage in range(3):        p = mp.Process(target=pipeline_stage, args=(prev_data, stage, queues[stage]))        processes.append(p)        p.start()    for p in processes:        p.join() def main():    random.seed(42)    data = [random.randint(1, 100) for _ in range(2000)]     start = time.time()    serial_sum = sum(x * x for x in data)    serial_time = time.time() - start     start = time.time()    parallel_sum = data_parallel_demo(data)    data_parallel_time = time.time() - start     start = time.time()    task_parallel_demo(data)    task_parallel_time = time.time() - start     assert serial_sum == parallel_sum, "结果不一致!"    print(f"串行计算耗时: {serial_time:.4f}s")    print(f"数据并行耗时: {data_parallel_time:.4f}s (加速比 {serial_time/data_parallel_time:.2f}x)")    print(f"任务并行耗时: {task_parallel_time:.4f}s") if __name__ == "__main__":    main()
目的:演示数据并行(Pool 对数据分块做相同操作)与任务并行(多进程流水线分阶段处理)的完整实现。
  1. chunks = [data[i*chunk_size:(i+1)*chunk_size] for i in range(num_workers)]将 2000 个数据切成 4 块,每块 500 个,对应数据并行的块划分策略。
  2. pool.map(process_chunk, chunks)线程池将每块数据分配给一个进程执行相同的平方求和操作,体现“同操作、异数据”。
  3. def pipeline_stage(data, stage_id, queue_out)定义流水线中的一个阶段,三个阶段的变换函数各不相同,对应任务并行“异操作”的本质。
  4. mp.Process(target=pipeline_stage, args=(prev_data, stage, queues[stage]))每个阶段创建独立进程,数据通过队列在阶段间传递,实现流水线单向数据流。
  5. assert serial_sum == parallel_sum验证数据并行结果与串行一致,说明并行只是加速,不改变计算逻辑。

常见误区

误区我以为数据并行就是把一个大数组切成 N 块,分给 N 个线程,剩下的就不用管了。只要切了数据,并行就自动发生了。

正解切分数据只是第一步。数据并行真正困难的地方在于:每个分块计算完后,结果如何合并?如果后续计算需要用到其他分块的结果,就必须引入同步屏障——这会产生通信开销。例如神经网络训练中,每个 GPU 算出梯度后必须 AllReduce 汇总,这个通信过程往往比计算本身更耗时。数据并行的本质是“分而治之 + 合而统一”,“合”的过程才是性能瓶颈所在。

为何常见:初学时看到“数据并行 = 数据分片”的简单定义,忽略了并行系统必须处理的数据交换与同步环节。

误区我觉得任务并行听起来更复杂、更灵活,应该比数据并行先进。数据并行只是切数据,任务并行才是真正的并行艺术。

正解两种模式没有高低之分,只有适配之别。数据并行适合 SIMD 架构与同构计算,代码简单、可扩展性强,GPU 上绝大多数计算都是数据并行;任务并行适合流水线场景,但扩展性受限于流程阶段的个数——你的流程只有 3 个阶段,最多只能并行到 3 个单元。在实际工程中,数据并行是绝对的主流,任务并行往往作为补充手段出现。

为何常见:对“任务”一词的直觉——觉得任务并行是更宏观的调度,而数据并行只是简单重复。实际上数据并行在 AI 和科学计算中的统治地位远超任务并行。

误区既然并行能加速,那我加机器就好了。从 4 核加到 8 核,速度应该翻倍;从 8 核加到 16 核,继续翻倍。

正解Amdahl 定律告诉我们,串行部分的存在决定了加速比的上限。如果程序有 10% 的串行部分,无论加多少核,加速比都不可能超过 10 倍。此外,随着处理单元增多,通信开销也在增长。当通信时间超过计算时间的节省时,增加核心反而会让总耗时上升。这就是为什么 64 核跑出的性能可能不如 32 核。

为何常见:直觉上认为“资源翻倍、收益翻倍”,忽略了 Amdahl 定律和通信开销这两个物理约束。

误区只要让每个线程处理的任务数量一样,负载就均衡了。把 100 个任务平分给 4 个线程,每个 25 个,肯定公平。

正解任务数量相等不代表计算量相等。有的任务 1 毫秒完成,有的任务需要 1 秒——按数量平分会导致严重的木桶效应。真正的负载均衡要按“完成时间”均衡,而非“任务个数”均衡。这正是动态调度和工作窃取存在的意义:运行时根据实际完成进度调整分配,让每个核的总工作时间尽量接近。

为何常见:用日常生活中的“分东西要平均”来类比并行计算,忽略了计算任务的耗时差异。

面试题

Q1. 数据并行的核心特征是( )

单选题 · easy

  • A.不同处理单元执行不同操作
  • B.不同处理单元对数据的不同分区执行相同操作← 正确答案
  • C.每个处理单元执行完整的独立程序
  • D.将任务按优先级排序后依次执行

答案:B。数据并行强调“同操作、异数据”——所有处理单元执行相同的指令流,各自处理不同的数据分区。选项 A 描述的是任务并行;选项 C 是程序级并行;选项 D 是任务调度而非并行模式。类比中央厨房:五位厨师用同一本菜谱做各自的五花肉。

Q2. 以下哪些属于任务并行的典型特征?( )

多选题 · mid

  • A.不同阶段由不同处理单元负责← 正确答案
  • B.数据在处理单元之间单向流动← 正确答案
  • C.所有单元执行同一条指令
  • D.适合流水线式的处理流程← 正确答案

答案:ABD。任务并行将流程分解为多个功能不同的阶段,每个阶段由独立单元负责,数据像流水线一样在单元间传递,因此 A、B、D 均正确。C 描述的是 SIMD 的特征,属于数据并行的硬件基础。类比后厨:洗菜、切菜、炒菜由不同厨师负责,食材在工位间传递。

Q3. 块划分(Block Partition)相比循环划分(Cyclic Partition)的主要优势是( )

单选题 · easy

  • A.负载自动均衡
  • B.每个单元的数据在内存中连续,局部性好← 正确答案
  • C.通信开销更少
  • D.不需要同步

答案:B。块划分将连续区间分给同一处理单元,数据在内存中连续存放,缓存命中率高,局部性好。循环划分的优势在于负载更均匀;通信开销取决于同步频率而非划分方式;无论哪种划分,需要同步时都必须同步。类比切肉:整块分给一个厨师便于连续操作,切片轮分则更平均但每个厨师碰到的肉更碎。

Q4. 工作窃取(Work Stealing)算法中,空闲的处理单元会( )

单选题 · mid

  • A.阻塞等待新任务
  • B.从繁忙单元的任务队列尾部偷取任务← 正确答案
  • C.向调度器重新申请任务
  • D.停止工作直到下一轮调度

答案:B。工作窃取的核心是空闲单元主动从其他繁忙单元的本地队列尾部“偷”任务执行,减少调度器瓶颈并实现动态均衡。选项 A 是传统工作池模型的阻塞行为;选项 C 描述的是集中式调度;选项 D 是静态调度中单元空闲时的状态。类比后厨:空闲厨师直接从忙厨师的台面拿菜来做。

Q5. 关于 Amdahl 定律,下列说法正确的有( )

多选题 · mid

  • A.串行部分占比越高,加速比上限越低← 正确答案
  • B.处理单元无限增加时,加速比趋近于串行时间的倒数← 正确答案
  • C.通信开销不影响加速比
  • D.它揭示了并行系统加速比的物理上限← 正确答案

答案:ABD。Amdahl 定律指出加速比上限为 $1/(s + (1-s)/p)$,其中 $s$ 为串行比例。当 $p \to \infty$ 时,加速比趋近 $1/s$,因此 A、B、D 正确。C 错误——通信开销会增加等效串行时间,进一步降低实际加速比,Amdahl 定律是最理想情况下的上限,实际系统只会更差。

Q6. 数据并行中,每轮迭代的总时间 $T_{\text{iter}} = C + M$ 中,$M$ 代表( )

单选题 · mid

  • A.内存分配时间
  • B.通信与同步时间← 正确答案
  • C.计算时间
  • D.任务创建时间

答案:B。在数据并行中,$M$ 是每轮迭代中所有处理单元完成计算后的通信与同步时间,典型如 AllReduce 梯度汇总。它是制约扩展性的核心因素。类比厨房:厨师们炒完菜后必须等所有人到齐再统一开下一桌——这个等待时间就是 $M$。

Q7. 关于混合并行(MPI + OpenMP),正确的设计思路有( )

多选题 · hard

  • A.节点间用 MPI 做数据并行,减少跨网络通信← 正确答案
  • B.节点内用 OpenMP 做任务并行,利用共享内存低延迟← 正确答案
  • C.节点间通信频繁、数据量大时,应增大节点内并行度
  • D.计算密集且数据可分时,节点间采用数据并行更合适← 正确答案

答案:ABD。混合并行的分层思路是:跨节点通信开销大,应用 MPI 数据并行将通信次数降到最低;节点内共享内存延迟低,用 OpenMP 任务并行利用多核。A、B 正确。C 错误——节点间通信频繁时应减少跨节点并行度、增加节点内并行,将更多交互约束在共享内存内。D 符合数据并行“同构计算”的适用条件,正确。

Q8. 流水线任务并行的吞吐量由什么决定?( )

单选题 · hard

  • A.最快阶段的耗时
  • B.最慢阶段的耗时← 正确答案
  • C.所有阶段的平均耗时
  • D.阶段总数

答案:B。流水线的吞吐量受最慢阶段限制——每个数据项都必须经过最慢阶段,因此整体吞吐量等于最慢阶段的吞吐量。这与木桶效应一致。类比厨房流水线:如果切菜需要 2 分钟、炒菜需要 5 分钟、摆盘 1 分钟,那每道菜至少 5 分钟才能完成,瓶颈在炒菜。

Q9. 不均衡度 $\Phi = (\max_i T_i - \bar{T}) / \bar{T}$ 越接近 0 说明( )

单选题 · easy

  • A.通信开销越小
  • B.各处理单元的完成时间越接近← 正确答案
  • C.任务数量越少
  • D.计算速度越快

答案:B。不均衡度衡量各处理单元完成时间的离散程度,越接近 0 说明最慢单元与平均水平的差距越小,负载越均衡。它不直接反映通信开销、任务数量或计算速度。类比厨房:五位厨师同时收工,说明工作量分配合理;有人提前下班有人加班,则 $\Phi$ 偏大。

Q10. 下列场景中,适合采用任务并行模式的有( )

多选题 · hard

  • A.对 10 万张图片做相同的缩放操作
  • B.一个包含数据抓取、清洗、分析、出报告的 ETL 流程← 正确答案
  • C.视频编码中的采集、编码、封装、推流流水线← 正确答案
  • D.对一个大矩阵做分块乘法

答案:BC。任务并行适合“流程分阶段、各阶段功能不同”的场景。B 的 ETL 流程和 C 的视频流水线都具备清晰的阶段划分,适合任务并行。A 和 D 是典型的同构计算——所有操作相同、数据可分割,更适合数据并行。这是选择两种模式的经典判据:“同操作分数据 vs 异操作分工序”。