ai_compiler_tutorial_v2

第6章:并行化与调度策略

在现代AI系统中,并行化不再是一种优化手段,而是达到实用性能的必要条件。无论是自动驾驶汽车需要在毫秒级响应时间内处理多路传感器数据,还是具身智能机器人需要同时执行感知、规划和控制任务,高效的并行化策略都是系统成功的关键。本章将深入探讨AI编译器如何实现各种并行化策略,以及如何在满足实时约束的同时最大化硬件利用率。

6.1 并行化的层次与维度

在深入具体技术之前,我们需要理解AI系统中并行化的多个层次。现代AI系统的并行化不是单一维度的优化问题,而是涉及从硬件到应用的多层次协同设计。每一层都有其独特的并行化机会,同时也面临特定的约束和挑战。

应用层并行(多任务/多模型)
    ↓
模型层并行  ←→  批处理并行
    ↓              ↓
算子层并行  ←→  数据流并行
    ↓              ↓
硬件层并行(SIMD/多核/多卡/集群)

这个层次结构反映了并行化的复杂性。应用层可能同时运行目标检测、语义分割和轨迹预测等多个任务;模型层需要将大模型拆分到多个设备;算子层要考虑矩阵运算的分块和向量化;而硬件层则直接映射到物理计算资源。AI编译器的核心任务是在这些层次间协调优化,找到全局最优的并行策略。

更重要的是,这些层次之间存在复杂的相互作用。例如,批处理并行可以提高硬件利用率,但会影响应用层的延迟;模型并行减少了单设备内存需求,但增加了设备间通信。理解这些相互作用是设计高效并行系统的关键。

6.1.1 并行化的基本权衡

并行化设计中存在几个基本的权衡,这些权衡决定了系统的性能边界:

  1. 通信vs计算:这是并行计算的根本权衡。增加并行度通常会增加通信开销,而通信往往成为性能瓶颈。理想的并行化策略应该使得: \(\text{加速比} = \frac{T_{\text{串行}}}{T_{\text{并行}} + T_{\text{通信}}} \approx P\) 其中$P$是并行处理单元数。但实际上,由于Amdahl定律的限制,存在串行部分$s$: \(\text{实际加速比} = \frac{1}{s + \frac{1-s}{P}}\)

    当$P \to \infty$时,加速比的上限是$1/s$。这意味着即使有无限的并行资源,串行部分也会限制整体性能。

  2. 延迟vs吞吐量:这是系统设计的经典权衡。批处理可以显著提高吞吐量,因为它能更好地利用硬件的并行能力,分摊固定开销。但批处理需要等待足够的请求,这会增加单个请求的延迟。对于自动驾驶系统,这个权衡尤其关键——我们需要在保证实时响应的前提下最大化处理能力。

    数学上,如果单个请求的处理时间是$t_1$,批处理$B$个请求的时间是$t_B$,那么:

    • 延迟:$L = t_B + t_{\text{wait}}$(等待形成批次的时间)
    • 吞吐量:$T = B / t_B$

    理想情况下,$t_B < B \cdot t_1$,这就是批处理的效率来源。

  3. 负载均衡vs调度开销:细粒度的任务划分有助于负载均衡,但会增加调度开销。假设有$N$个任务分配给$P$个处理器,如果任务粒度太粗($N \approx P$),容易出现负载不均;如果太细($N » P$),调度开销可能超过并行收益。

    最优的任务粒度通常满足: \(\text{任务粒度} \approx \frac{\text{总工作量}}{P \times \text{调度开销系数}}\)

  4. 空间vs时间:这涉及内存使用和计算时间的权衡。例如,保存中间结果可以避免重复计算(空间换时间),而重新计算可以减少内存占用(时间换空间)。在GPU等内存受限的设备上,这个权衡尤为重要。

  5. 精度vs性能:混合精度计算可以提升性能,但可能影响数值稳定性。量化可以减少内存带宽需求,但需要careful calibration to maintain accuracy。

6.1.2 并行化的可扩展性分析

并行系统的可扩展性(scalability)是衡量其设计质量的关键指标。可扩展性分为两个维度:

强扩展性(Strong Scaling):固定问题规模,增加处理器数量。理想情况下,执行时间应该线性减少。但实际上,由于通信开销和串行部分的存在,强扩展性通常会在某个点后饱和。

弱扩展性(Weak Scaling):按比例增加问题规模和处理器数量。理想情况下,执行时间应该保持恒定。弱扩展性通常比强扩展性更容易实现,因为增大的问题规模可以分摊通信开销。

对于AI工作负载,我们需要考虑特殊的扩展性特征:

6.1.3 并行化的性能模型

为了定量分析并行策略,我们需要建立性能模型。一个实用的性能模型应该包含:

计算时间模型: \(T_{\text{comp}} = \frac{\text{FLOPs}}{\text{峰值性能} \times \text{利用率}}\)

通信时间模型: \(T_{\text{comm}} = \alpha + \beta \times \text{消息大小}\) 其中$\alpha$是延迟,$\beta$是带宽的倒数。

内存访问模型: \(T_{\text{mem}} = \frac{\text{数据量}}{\text{带宽}} + \text{缓存未命中代价}\)

总执行时间: \(T_{\text{total}} = \max(T_{\text{comp}}, T_{\text{mem}}) + T_{\text{comm}}\)

这个模型告诉我们,当计算密集度(compute intensity)$\text{FLOPs}/\text{数据量}$较低时,系统是内存带宽受限的;当通信量大时,系统是通信受限的。AI编译器需要根据这些特征选择合适的并行策略。

6.2 数据并行vs模型并行

6.2.1 数据并行的原理与实现

数据并行是最直观也是应用最广泛的并行化策略。其核心思想简单而优雅:将输入数据分片,每个处理单元运行相同的模型副本,然后聚合结果。这种策略的美妙之处在于它保持了模型的完整性,不需要修改模型架构,因此实现相对简单,且能够很好地扩展到大规模系统。

基本流程与数学原理:

数据并行的数学基础是梯度下降的线性可加性。对于损失函数$L$和参数$\theta$,有: \(\nabla_\theta L(B) = \frac{1}{|B|} \sum_{b \in B} \nabla_\theta L(b)\)

这意味着我们可以在不同设备上独立计算子批次的梯度,然后求平均:

输入批次 B = [b₁, b₂, ..., bₙ]
         ↓ 分片
   GPU₁: b₁...bₖ → 计算梯度g₁
   GPU₂: bₖ₊₁...b₂ₖ → 计算梯度g₂
   ...
   GPUₚ: b₍ₚ₋₁₎ₖ₊₁...bₙ → 计算梯度gₚ
         ↓ 梯度聚合
   g_avg = (g₁ + g₂ + ... + gₚ) / P
         ↓ 参数更新
   θ = θ - η × g_avg (所有设备同步更新)

通信模式的深入分析:

数据并行的性能瓶颈通常在于梯度聚合的通信开销。让我们详细分析不同的通信模式:

  1. 参数服务器模式(Parameter Server):

    这是最早期的设计,有专门的参数服务器节点负责梯度聚合和参数更新:

    • 通信复杂度:每个worker需要发送$M$字节梯度,接收$M$字节更新后的参数
    • 总通信量:$2M \times (P-1)$
    • 通信步骤:2轮(push梯度 + pull参数)
    • 瓶颈分析:参数服务器成为带宽瓶颈,尤其当worker数量增加时

    参数服务器可以通过分片(sharding)来缓解瓶颈:

    PS₁: 负责参数[0, M/3)
    PS₂: 负责参数[M/3, 2M/3)
    PS₃: 负责参数[2M/3, M)
    
  2. Ring-AllReduce模式:

    这是目前主流的通信模式,将设备组织成环形拓扑:

    • 第一阶段(Scatter-Reduce):每个设备负责聚合1/P的参数
    • 第二阶段(All-Gather):每个设备广播自己负责的参数片段
    • 通信量:每个设备发送/接收$2M \times \frac{P-1}{P}$字节
    • 通信步骤:$2(P-1)$轮
    • 优势:无单点瓶颈,负载完全均衡,带宽利用率接近理论上限

    Ring-AllReduce的带宽效率分析: \(\text{有效带宽} = \frac{M}{\text{通信时间}} = \frac{M}{2M(P-1)/(P \times B)} = \frac{P \times B}{2(P-1)} \approx \frac{B}{2}\) 其中$B$是单链路带宽。这说明Ring-AllReduce能达到理论带宽的50%。

  3. 树形AllReduce:

    适用于高延迟网络,通过构建平衡树来减少通信轮数:

    • 通信步骤:$2\log_2(P)$轮
    • 适用场景:跨地域分布式训练,网络延迟dominates通信时间
  4. 分层AllReduce(Hierarchical AllReduce):

    针对多级网络拓扑优化(如机器内vs机器间):

    第一层:机器内GPU间AllReduce(NVLink,600GB/s)
    第二层:机器间代表GPU的AllReduce(InfiniBand,200Gbps)
    第三层:结果广播回各机器内GPU
    

自动驾驶场景的实际应用:

让我们以一个真实的自动驾驶感知系统为例,深入分析数据并行的应用:

系统配置:

硬件配置:
- 4个NVIDIA Drive AGX Orin(每个512 CUDA cores)
- 8个摄像头:3前视 + 2侧视 + 1后视 + 2环视
- 1个128线激光雷达
- 处理频率:30Hz(33ms周期)

模型架构:
- 骨干网络:ResNet-101 (44M参数)
- 检测头:FPN + YOLO (20M参数)
- 分割头:DeepLab (15M参数)
- 融合网络:Transformer (30M参数)
总计:约109M参数 ≈ 436MB (FP32)

数据并行策略设计:

  1. 摄像头分组策略:
    Orin-1: 前视广角 + 前视标准 → 主要负责前方障碍物
    Orin-2: 前视长焦 + 右侧 → 负责远距离和右侧盲区
    Orin-3: 左侧 + 后视 → 负责左侧和后方
    Orin-4: 环视×2 + 激光雷达 → 负责360°感知和点云处理
    

    这种分组考虑了:

    • 视野重叠最小化(减少融合通信)
    • 计算负载均衡(每个Orin处理相近的数据量)
    • 故障隔离(单个Orin故障不会完全失去某个方向的感知)
  2. 流水线与数据并行结合:
    时刻t: 数据采集(5ms) → Orin1-4并行处理(20ms) → 融合(5ms) → 输出(3ms)
    时刻t+33ms: 新一轮处理开始
       
    关键优化:
    - 异步数据传输:在t时刻处理时,预取t+33ms的数据
    - 增量式融合:只传输和融合变化的特征
    - 优先级调度:紧急制动相关的检测优先处理
    
  3. 通信优化实践:

    梯度压缩策略:

    # 伪代码展示梯度压缩逻辑
    def compress_gradient(grad, compression_ratio=0.01):
        # Top-K稀疏化
        threshold = torch.quantile(grad.abs(), 1-compression_ratio)
        mask = grad.abs() > threshold
        compressed = grad * mask
           
        # 误差反馈
        error_feedback[param] += grad - compressed
           
        # 量化到INT8
        scale = compressed.abs().max() / 127
        quantized = (compressed / scale).round().to(torch.int8)
           
        return quantized, scale, mask
    

    通过这种压缩,436MB的梯度可以压缩到:

    • 稀疏化后:4.36MB(1%的非零元素)
    • 量化后:1.09MB(INT8)
    • 加上元数据:约2MB

    通信时间从436MB/(10Gbps) ≈ 350ms降低到2MB/(10Gbps) ≈ 1.6ms。

  4. 容错与动态负载均衡:

    当检测到某个摄像头故障时的动态调整:

    故障检测(通过心跳和校验)
         ↓
    负载重分配决策
         ↓
    迁移相关计算到邻近Orin
         ↓
    调整融合权重补偿缺失视角
    

    这个过程需要在100ms内完成,以保持系统的实时性。

6.2.2 模型并行的策略

当模型规模超过单个设备的内存容量时,模型并行成为必然选择。随着大语言模型参数量从数十亿增长到数千亿甚至万亿,模型并行技术变得愈发重要。与数据并行不同,模型并行需要将模型本身拆分到多个设备上,这带来了全新的挑战:如何划分模型以最小化通信开销?如何保持设备间的负载均衡?如何处理复杂的数据依赖关系?

层间并行(Pipeline Parallelism)的深入分析:

层间并行是最直观的模型并行方式,将模型的不同层分配到不同设备:

设备分配示意:
设备1: [Embedding, Layer1-10]  → 2GB参数
设备2: [Layer11-20]           → 2GB参数
设备3: [Layer21-30]           → 2GB参数
设备4: [Layer31-40, Output]   → 2GB参数

前向传播流程:
时刻1: 设备1处理输入 → 产生激活A₁
时刻2: 设备2处理A₁ → 产生激活A₂ (设备1空闲)
时刻3: 设备3处理A₂ → 产生激活A₃ (设备1,2空闲)
时刻4: 设备4处理A₃ → 产生输出    (设备1,2,3空闲)

这种朴素的实现存在严重的效率问题:

数学分析:假设每个阶段的计算时间为$t$,批大小为$B$,流水线深度为$P$:

张量并行(Tensor Parallelism)的实现细节:

张量并行在单个算子级别进行并行化,特别适合大型矩阵运算:

  1. 列并行(Column Parallel):
    输入X: [B, H]
    权重W: [H, O] → 切分为 W = [W₁|W₂|...|Wₚ], 每个Wᵢ: [H, O/P]
       
    设备i计算:
    Yᵢ = XWᵢ  # 结果shape: [B, O/P]
       
    最终结果:
    Y = Concat(Y₁, Y₂, ..., Yₚ, dim=1)  # shape: [B, O]
    

    通信分析:

    • 前向:无需通信(输入X广播到所有设备)
    • 反向:需要AllReduce梯度∇X
  2. 行并行(Row Parallel):
    输入X: [B, H] → 切分为 X = [X₁; X₂; ...; Xₚ], 每个Xᵢ: [B, H/P]
    权重W: [H, O] → 切分为 W = [W₁; W₂; ...; Wₚ], 每个Wᵢ: [H/P, O]
       
    设备i计算:
    Yᵢ = XᵢWᵢ  # 部分结果
       
    最终结果:
    Y = AllReduce(Y₁ + Y₂ + ... + Yₚ)
    

    通信分析:

    • 前向:需要AllReduce输出
    • 反向:输入梯度自然分片
  3. Transformer的高效张量并行:

    对于Multi-Head Attention,自然的并行维度是注意力头: \(\text{MHA}(Q, K, V) = \text{Concat}(\text{head}_1, ..., \text{head}_h)W^O\)

    每个设备处理$h/P$个注意力头:

    设备i负责head_{(i-1)h/P+1}到head_{ih/P}
       
    计算流程:
    1. Q, K, V投影(列并行)
    2. 注意力计算(完全并行,无通信)
    3. 输出投影(行并行)
    

    对于FFN层:

    FFN(x) = GELU(xW₁)W₂
       
    设备分配:
    - W₁: [d_model, 4*d_model] 列切分
    - W₂: [4*d_model, d_model] 行切分
       
    通信模式:
    前向:x → 广播 → xW₁(并行) → GELU → W₂(并行) → AllReduce
    反向:相反方向传播梯度
    
  4. 2D和3D张量并行:

    对于超大模型,可以在多个维度上并行:

    2D并行(Summa算法):
    将P个设备组织成√P × √P网格
    权重W按行列同时切分
       
    通信复杂度:O(√P)而非O(P)
    

混合并行策略的设计原则:

现代大模型训练通常结合多种并行策略,以GPT-3(175B参数)为例:

模型配置:
- 96层Transformer
- 隐藏维度:12,288
- 注意力头:96
- FFN维度:49,152

并行策略(使用1024个GPU):
- 数据并行度:64
- 张量并行度:8
- 流水线并行度:2

设备分组:
- 8个GPU为一组做张量并行(同一节点,NVLink连接)
- 2组做流水线并行(跨节点)
- 64路数据并行(跨机架)

内存分析:
- 每个张量并行组:175B/8 = 22B参数
- 每个流水线阶段:22B/2 = 11B参数
- 加上优化器状态和激活:约60GB/GPU

选择并行策略的决策树:

if 模型fits单GPU:
    使用纯数据并行
elif 模型fits单节点:
    节点内张量并行 + 跨节点数据并行
else:
    节点内张量并行 + 跨节点流水线并行 + 数据并行

6.2.3 通信优化技术

通信是分布式训练的主要瓶颈,优化通信对提升整体性能至关重要。

梯度压缩的理论与实践:

  1. 量化压缩:

    理论基础:神经网络训练对梯度精度的要求相对较低,可以容忍一定的量化误差。

    量化函数:
    Q(x) = s × round(clip(x/s, -2^(b-1), 2^(b-1)-1))
    其中s是缩放因子,b是位宽
       
    动态量化:
    s = max(|x|) / (2^(b-1) - 1)
       
    随机量化(保证无偏):
    Q(x) = s × (floor(x/s) + Bernoulli((x/s - floor(x/s))))
    

    实验数据:

    • FP32→FP16:几乎无精度损失,2×压缩
    • FP32→INT8:0.1-0.5%精度损失,4×压缩
    • FP32→INT4:1-2%精度损失,8×压缩
  2. 稀疏化压缩:

    Top-K稀疏化:只传输最大的K%梯度

    def top_k_sparsification(grad, sparsity=0.99):
        k = int(grad.numel() * (1 - sparsity))
        values, indices = torch.topk(grad.abs().view(-1), k)
        mask = torch.zeros_like(grad).view(-1)
        mask[indices] = 1
        return grad * mask.view(grad.shape)
    

    理论分析(收敛性保证): 设原始梯度为$g$,稀疏化后为$\tilde{g}$,如果满足: \(E[\tilde{g}] = g \text{ 且 } E[\|\tilde{g} - g\|^2] \leq \sigma^2\) 则SGD依然收敛,只是收敛速度变慢。

  3. 误差反馈机制:

    累积压缩误差,避免信息永久丢失:

    error_feedback = {}
       
    def compress_with_ef(param_name, grad):
        # 加上历史误差
        grad_with_ef = grad + error_feedback.get(param_name, 0)
           
        # 压缩
        compressed = compress(grad_with_ef)
           
        # 记录新误差
        error_feedback[param_name] = grad_with_ef - compressed
           
        return compressed
    

通信与计算重叠(Communication-Computation Overlap):

关键思想:在计算第$i$层的同时,通信第$i-1$层的梯度。

优化的反向传播流程:
for i in reversed(range(num_layers)):
    # 计算第i层梯度
    grad[i] = compute_gradient(layer[i])
    
    # 立即启动异步通信
    handle[i] = all_reduce_async(grad[i])
    
    # 如果i+1层通信完成,更新参数
    if i < num_layers - 1:
        wait(handle[i+1])
        update_params(layer[i+1], grad[i+1])

时间节省分析:

NUMA感知的通信优化深入:

NUMA(Non-Uniform Memory Access)架构在现代服务器中普遍存在,理解其特性对优化至关重要:

典型的2-socket服务器NUMA拓扑:
Socket 0                    Socket 1
┌──────────────────┐       ┌──────────────────┐
│ CPU: 32 cores    │       │ CPU: 32 cores    │
│ Mem: 256GB       │ QPI   │ Mem: 256GB       │
│ PCIe: GPU0, GPU1 │←─────→│ PCIe: GPU2, GPU3 │
└──────────────────┘ 25GB/s└──────────────────┘
      ↓↑                          ↓↑
   本地内存                     本地内存
   200GB/s                      200GB/s

NUMA的性能影响:

优化策略实施:

  1. 内存分配策略:
    # 使用libnuma进行NUMA感知分配
    numa_node = get_gpu_numa_node(gpu_id)
    memory = numa_alloc_onnode(size, numa_node)
    
  2. 进程绑定策略:
    # 将进程绑定到对应NUMA节点
    numactl --cpunodebind=0 --membind=0 python train.py --gpu=0,1
    
  3. 通信拓扑优化:
    # 构建NUMA感知的通信环
    def build_numa_aware_ring(gpus):
        # 同NUMA节点的GPU相邻
        numa_groups = group_by_numa(gpus)
        ring = []
        for group in numa_groups:
            ring.extend(group)
        return ring
    
  4. 数据放置优化:
    # 数据分片考虑NUMA亲和性
    for gpu_id, data_shard in enumerate(data_shards):
        numa_node = gpu_to_numa[gpu_id]
        # 确保数据在对应NUMA节点
        place_data_on_numa(data_shard, numa_node)
    

实际性能提升数据:

6.3 流水线并行化

流水线并行通过将模型分成多个阶段(stage),让不同的微批次(micro-batch)在不同阶段并行执行,从而提高设备利用率。

6.3.1 基础流水线原理

朴素流水线的问题:

时间 →
设备1: F₁(mb₁) → F₁(mb₂) → F₁(mb₃) → F₁(mb₄) → 空闲...
设备2: 等待 → F₂(mb₁) → F₂(mb₂) → F₂(mb₃) → F₂(mb₄) → 空闲...
设备3: 等待 → 等待 → F₃(mb₁) → F₃(mb₂) → F₃(mb₃) → F₃(mb₄)
设备4: 等待 → 等待 → 等待 → F₄(mb₁) → F₄(mb₂) → F₄(mb₃) → F₄(mb₄)

问题:流水线气泡(bubble)导致设备利用率低

6.3.2 GPipe策略

GPipe通过增加微批次数量来减少气泡比例:

设K = 微批次数,P = 流水线深度
气泡比例 = (P-1) / (K + P - 1)

当K >> P时,气泡比例 → 0

前向和反向传播的调度:

时间步: 1  2  3  4  5  6  7  8  9  10 11 12
设备1:  F₁ F₂ F₃ F₄ B₄ B₃ B₂ B₁
设备2:     F₁ F₂ F₃ F₄ B₄ B₃ B₂ B₁
设备3:        F₁ F₂ F₃ F₄ B₄ B₃ B₂ B₁
设备4:           F₁ F₂ F₃ F₄ B₄ B₃ B₂ B₁

F = 前向传播,B = 反向传播,数字 = 微批次编号

内存需求分析:

GPipe需要保存所有微批次的激活值直到反向传播:

6.3.3 PipeDream策略

PipeDream采用1F1B(One Forward One Backward)调度减少内存需求:

设备1: F₁¹ F₁² F₁³ F₁⁴ B₁¹ F₁⁵ B₁² F₁⁶ B₁³ F₁⁷ B₁⁴ ...
设备2:    F₂¹ F₂² F₂³ F₂⁴ B₂¹ F₂⁵ B₂² F₂⁶ B₂³ F₂⁷ ...
设备3:       F₃¹ F₃² F₃³ F₃⁴ B₃¹ F₃⁵ B₃² F₃⁶ B₃³ ...
设备4:          F₄¹ F₄² F₄³ F₄⁴ B₄¹ F₄⁵ B₄² F₄⁶ ...

上标表示微批次编号

优势:

挑战:

6.3.4 具身智能的流水线设计

在具身智能系统中,流水线不仅存在于模型内部,更体现在感知-规划-控制的系统级流水线:

传感器数据流:
摄像头 ──┐
激光雷达 ─┼→ 感知模块 → 场景理解 → 路径规划 → 运动控制 → 执行器
IMU ─────┘     (20ms)     (15ms)     (10ms)      (5ms)

流水线时序(50Hz主循环):
时刻t:   感知(t)   理解(t-1)  规划(t-2)  控制(t-3)
时刻t+1: 感知(t+1) 理解(t)    规划(t-1)  控制(t-2)

关键设计要点:

  1. 时间戳对齐:不同传感器的数据需要时间同步
  2. 预测补偿:使用运动模型预测流水线延迟期间的状态变化
  3. 优先级管理:紧急避障可以打断常规流水线

6.4 动态批处理

动态批处理是在保证延迟约束的前提下,动态调整批大小以提高吞吐量的技术。

6.4.1 批处理的效率分析

批处理之所以高效,主要因为:

  1. 分摊固定开销:核函数启动、内存分配等
  2. 提高并行度:更好地利用GPU的大规模并行架构
  3. 改善内存访问模式:连续内存访问,提高缓存命中率

效率模型: \(\text{效率}(B) = \frac{B \times \text{计算量}}{T_{\text{固定}} + B \times T_{\text{变动}}}\)

其中$B$是批大小,存在最优批大小$B^*$使效率最大化。

6.4.2 Padding与变长序列

问题描述:

在自然语言处理或可变目标数量的检测任务中,输入长度不一:

批次内序列长度:[128, 45, 89, 256, 34, 198, 67, 145]

朴素Padding方案:
所有序列填充到256 → 计算浪费 = (256×8 - 实际总长度) / (256×8) ≈ 60%

优化策略:

  1. 动态Padding:
    将批次按长度排序并分组:
    组1: [34, 45, 67]    → Pad到67
    组2: [89, 128, 145]  → Pad到145  
    组3: [198, 256]      → Pad到256
       
    计算浪费降至 ~20%
    
  2. 序列打包(Sequence Packing):
    原始: [seq1_____] [seq2________] [seq3___]
    打包: [seq1|seq2|seq3|seq4|seq5|...]
       
    需要额外维护位置信息和掩码
    
  3. 注意力掩码优化:

    对于Transformer模型,可以使用块稀疏注意力: \(\text{BlockSparseAttention} = \text{Softmax}(QK^T \odot M)V\) 其中$M$是块掩码矩阵。

6.4.3 机会批处理

机会批处理(Opportunistic Batching)在请求到达时动态形成批次:

等待策略:

if (队列中请求数 >= 最小批大小) {
    立即处理
} else if (最早请求等待时间 > 延迟阈值) {
    处理当前所有请求
} else {
    继续等待
}

自适应批大小调整:

基于Little’s Law:$L = \lambda W$

动态调整策略:

if (当前延迟 < 目标延迟 × 0.8) {
    增大批大小上限
} else if (当前延迟 > 目标延迟 × 0.95) {
    减小批大小上限
}

6.4.4 实时系统的批处理权衡

在自动驾驶等实时系统中,批处理策略需要特别谨慎:

分级批处理策略:

关键路径(10ms期限):
- 紧急制动检测:批大小=1,无等待
- 碰撞预警:批大小≤2,最大等待2ms

常规路径(50ms期限):
- 车道线检测:批大小≤8,最大等待10ms
- 交通标志识别:批大小≤16,最大等待20ms

后台任务(无严格期限):
- 地图更新:批大小32-64,机会批处理

6.5 实时系统的调度约束

6.5.1 实时性需求分类

硬实时(Hard Real-Time):

软实时(Soft Real-Time):

Firm实时:

6.5.2 调度算法

速率单调调度(Rate Monotonic Scheduling, RMS):

任务按周期排序,周期越短优先级越高:

任务集合:
任务A: 周期=10ms, 执行时间=2ms
任务B: 周期=20ms, 执行时间=5ms  
任务C: 周期=50ms, 执行时间=10ms

优先级: A > B > C

可调度性测试(Liu & Layland界限):
U = Σ(Cᵢ/Tᵢ) ≤ n(2^(1/n) - 1)
其中Cᵢ是执行时间,Tᵢ是周期,n是任务数

最早期限优先(Earliest Deadline First, EDF):

动态优先级,总是执行期限最近的任务:

时刻t的任务队列:
任务1: 期限 = t+5ms
任务2: 期限 = t+3ms  ← 优先执行
任务3: 期限 = t+8ms

EDF是最优的:如果任务集可调度,EDF一定能调度
利用率界限:U ≤ 1(100%利用率)

6.5.3 WCET分析

最坏情况执行时间分析对于硬实时系统至关重要:

静态分析方法:

基本块执行时间:
BB1: 10 cycles (内存访问)
BB2: 5 cycles  (计算)
BB3: 15 cycles (分支预测失败)

控制流路径:
Path1: BB1 → BB2 → BB3 = 30 cycles
Path2: BB1 → BB3 = 25 cycles

WCET = max(所有路径) = 30 cycles

缓存和内存的影响:

缓存分析:
- 必中(Must Hit):100% 在缓存中
- 必失(Must Miss):100% 不在缓存中  
- 可能(May):不确定

WCET计算:
WCET = WCET_base + Σ(可能失效 × 失效代价)

GPU上的WCET挑战:

GPU的WCET分析特别困难,因为:

  1. Warp调度的不确定性
  2. 缓存竞争
  3. 内存带宽争用

缓解策略:

6.5.4 自动驾驶的安全关键路径

安全关键路径设计:
                    ┌─────────────┐
传感器 → 预处理 → │ 主感知模型  │ → 决策
            ↓      └─────────────┘      ↓
        ┌─────────────┐            ┌─────────┐
        │ 简化备份   │ ─────────→ │ 仲裁器  │ → 执行
        └─────────────┘            └─────────┘
         (低延迟路径)               (安全检查)

时间预算分配(100ms总预算):
- 传感器采集:10ms
- 预处理:10ms
- 感知:40ms(主路径)/ 20ms(备份路径)
- 决策:20ms
- 执行验证:10ms
- 余量:10ms

优先级反转问题及解决:

问题场景:
高优先级任务(H) 等待低优先级任务(L)持有的资源
中优先级任务(M) 抢占L,导致H间接等待M

解决方案:
1. 优先级继承:L临时继承H的优先级
2. 优先级天花板:资源关联最高可能优先级
3. 无锁数据结构:避免锁竞争

6.6 高级调度优化

6.6.1 能量感知调度

在具身智能设备上,能量效率至关重要:

DVFS(动态电压频率调节):

能量模型:E = C × V² × f × t
功率模型:P = C × V² × f

策略:
- 空闲时降频:f_idle = 0.4 × f_max
- 批处理时升频:f_batch = f_max
- deadline宽松时降频运行更长时间

大小核调度(big.LITTLE):

任务分类:
- 延迟敏感 → 大核(高性能)
- 吞吐量导向 → 小核集群(高效率)
- 后台任务 → 小核(低功耗)

迁移策略:
if (任务负载 > 阈值 && 大核可用) {
    迁移到大核
} else if (任务负载 < 阈值 && 在大核上) {
    迁移到小核
}

6.6.2 协同调度

在多租户或多模型场景下,协同调度可以提高整体效率:

GPU时分复用:

时间片分配(10ms周期):
模型A(高优先级): 6ms
模型B(中优先级): 3ms
模型C(低优先级): 1ms

上下文切换开销:~0.5ms
有效利用率:95%

空间复用(MPS/MIG):

选择策略:

if (模型间无干扰 && 总显存 < GPU容量) {
    使用MPS共享
} else if (需要性能隔离) {
    使用MIG分区
} else {
    时分复用
}

6.7 本章小结

本章深入探讨了AI编译器中的并行化与调度策略,涵盖了从基础的数据/模型并行到复杂的实时调度约束。核心要点包括:

关键概念:

  1. 并行化层次:从硬件级到应用级的多层次并行机会
  2. 通信模式:Ring-AllReduce、参数服务器等通信优化
  3. 流水线策略:GPipe和PipeDream的权衡
  4. 批处理优化:动态批处理与延迟的平衡
  5. 实时约束:硬实时系统的WCET分析和调度保证

关键公式:

实践指导:

  1. 数据并行适合批处理场景,模型并行适合大模型
  2. 流水线并行需要平衡气泡开销和内存使用
  3. 实时系统需要预留安全余量,避免过度优化
  4. NUMA架构下优先本地通信,减少跨节点开销
  5. 动态批处理需要根据负载自适应调整策略

练习题

🟢 基础题

练习6.1:Ring-AllReduce通信分析

在4个GPU的数据并行训练中,模型参数量为100MB,网络带宽为10GB/s。计算Ring-AllReduce的通信时间。

💡 提示:Ring-AllReduce分为scatter-reduce和all-gather两个阶段,每个阶段传输$(P-1)/P$的数据量。

📝 参考答案 Ring-AllReduce的通信过程: 1. Scatter-reduce阶段:每个GPU发送$(P-1)/P \times M = 3/4 \times 100MB = 75MB$ 2. All-gather阶段:同样发送75MB 3. 总通信量:$2 \times 75MB = 150MB$每GPU 4. 通信时间:$150MB / 10GB/s = 15ms$ 相比参数服务器模式(需要传输$2 \times 100MB = 200MB$),Ring-AllReduce减少了25%的通信量。

练习6.2:流水线气泡计算

一个模型被分成4个阶段进行流水线并行,使用8个微批次。计算流水线的气泡比例和设备利用率。

💡 提示:使用公式:气泡比例 = $(P-1)/(K+P-1)$

📝 参考答案 给定:$P = 4$(流水线深度),$K = 8$(微批次数) 气泡比例 = $(P-1)/(K+P-1) = 3/(8+4-1) = 3/11 \approx 27.3\%$ 设备利用率 = $1 - 0.273 = 72.7\%$ 如果增加微批次数到16: 气泡比例 = $3/(16+3) = 3/19 \approx 15.8\%$ 利用率提升到84.2%

练习6.3:RMS可调度性判断

三个周期性任务:

判断这个任务集在单核处理器上是否可以用RMS调度。

💡 提示:计算CPU利用率并与Liu-Layland界限比较。

📝 参考答案 CPU利用率计算: $U = 3/10 + 4/20 + 8/40 = 0.3 + 0.2 + 0.2 = 0.7$ Liu-Layland界限(n=3): $U_{LL} = 3(2^{1/3} - 1) \approx 3 \times 0.26 = 0.78$ 因为$U = 0.7 < 0.78$,所以该任务集可以用RMS调度。 验证:在最坏情况下(所有任务同时到达),高优先级任务A总能满足期限,B和C也能在各自周期内完成。

🟡 进阶题

练习6.4:动态批处理优化

一个在线推理服务,请求到达率服从泊松分布($\lambda = 100$ req/s),单个请求处理时间1ms,批处理8个请求需要3ms。设计一个动态批处理策略,使得99%的请求延迟小于10ms。

💡 提示:考虑等待时间阈值和批大小的关系,使用排队论分析。

📝 参考答案 策略设计: 1. 批处理效率分析: - 单独处理:8个请求需要8ms - 批处理:8个请求需要3ms - 加速比:8/3 ≈ 2.67 2. 等待策略: - 最大等待时间:7ms(留3ms处理时间) - 最小批大小:4(保证50%效率提升) - 最大批大小:8 3. 自适应算法: ``` if (队列长度 >= 8) { 立即处理8个请求 } else if (最早请求等待 > 7ms) { 处理所有等待请求 } else if (队列长度 >= 4 && 最早等待 > 3ms) { 处理min(8, 队列长度)个请求 } ``` 4. 性能分析: - 平均到达间隔:10ms - 平均形成8批次时间:80ms - 实际平均等待:约4ms - P99延迟:约9ms(满足要求)

练习6.5:NUMA感知的并行策略

在2个NUMA节点的系统上,每个节点有4个GPU。设计一个混合并行策略,模型大小16GB,每个GPU显存32GB,批大小64。节点内带宽200GB/s,跨节点50GB/s。

💡 提示:考虑数据并行和模型并行的通信模式差异。

📝 参考答案 优化策略: 1. **并行方案分析**: - 纯数据并行:每次AllReduce需要同步16GB参数 - 纯模型并行:频繁的激活值传输 - 混合方案:结合两者优势 2. **推荐方案**: - NUMA节点内:4路数据并行 - NUMA节点间:2路模型并行(按层划分) 3. **通信分析**: - 节点内数据并行:16GB × 3/4 = 12GB,耗时60ms - 节点间模型并行:仅传输激活值(约100MB),耗时2ms - 总通信时间:62ms(相比纯数据并行的320ms大幅降低) 4. **内存使用**: - 每个GPU:模型8GB + 激活16GB + 优化器状态8GB = 32GB - 刚好充分利用显存 5. **批次分配**: - 每个NUMA节点处理32个样本 - 节点内每个GPU处理8个样本

🔴 挑战题

练习6.6:端到端延迟优化

设计一个自动驾驶感知系统的并行调度方案。系统包含:

💡 提示:考虑传感器融合时序、模型依赖关系、GPU负载均衡。

📝 参考答案 综合调度方案: 1. **传感器同步策略**(0-10ms): - 使用最近的激光雷达帧作为主时钟(100ms周期) - 摄像头数据缓冲和插值对齐 - 时间戳误差容忍度:±15ms 2. **GPU任务分配**: - GPU0:前向摄像头目标检测(20ms) - GPU1:侧向/后向摄像头检测(20ms) - GPU2:激光雷达处理+点云分割(25ms) - GPU3:融合+轨迹预测(15ms) 3. **流水线设计**: ``` T=0ms: 传感器采集 T=10ms: GPU0/1/2并行处理 T=35ms: 特征融合(GPU3) T=50ms: 轨迹预测(GPU3) T=65ms: 决策规划 T=80ms: 控制输出 T=90ms: 安全验证 T=100ms: 执行 ``` 4. **优化技术**: - 多流并发:每个GPU使用2个CUDA流 - 内存池:预分配避免动态分配开销 - 零拷贝:使用统一内存避免H2D拷贝 - 模型量化:INT8推理减少延迟 5. **容错设计**: - 降级模式:单传感器失效时的备用方案 - 时间监控:超时自动切换到安全模式 - 冗余计算:关键检测在两个GPU上并行验证

练习6.7:投机执行支持

为支持大语言模型的投机解码(speculative decoding),设计编译器的调度支持。小模型在GPU上投机生成k个token,大模型批量验证。

💡 提示:考虑投机成功率、批处理效率、内存管理。

📝 参考答案 投机执行调度设计: 1. **基础架构**: ``` 小模型(draft): 1B参数,单token延迟5ms 大模型(target): 70B参数,单token延迟50ms 投机长度k: 4-8(自适应) ``` 2. **调度策略**: - **并行模式**: ``` GPU0: 运行小模型,连续生成k个token GPU1-3: 运行大模型分片,批量验证 ``` - **时序安排**: ``` 小模型生成: 5ms × k = 20-40ms 大模型验证: 50ms(批量处理k个位置) 有效加速: k × α(α为接受率) ``` 3. **内存管理**: - KV缓存复用:验证失败时回滚到分叉点 - 投机树管理:维护多个投机分支 - 内存预分配:避免动态扩展开销 4. **自适应优化**: ``` if (接受率 > 0.8) { k = min(k + 1, 8) // 增加投机长度 } else if (接受率 < 0.5) { k = max(k - 1, 2) // 减少投机长度 } ``` 5. **编译器支持**: - 条件执行:生成验证成功/失败两条路径 - 寄存器分配:为投机状态预留寄存器 - 分支预测提示:标记高概率分支 6. **性能分析**: - 理想加速比:$k/(1 + (1-α)k)$ - 当α=0.7, k=4时,加速比≈2.17 - 实际考虑开销后约1.8-2.0倍加速

常见陷阱与错误 (Gotchas)

1. 通信被低估

错误:只考虑计算时间,忽略通信开销

// 错误假设
加速比 = 并行度P

// 实际情况  
加速比 = P / (1 + 通信时间/计算时间)

正确做法:通信和计算重叠,使用异步通信

2. 负载不均衡

错误:简单均分任务,忽略任务异构性

// 问题场景
GPU0: 处理简单图像 → 10ms完成 → 等待
GPU1: 处理复杂图像 → 30ms完成

正确做法:动态负载均衡,任务窃取

3. 流水线气泡过大

错误:微批次数量过少

// 4阶段流水线,4个微批次
气泡率 = 3/7 = 43% (太高)

正确做法:增加微批次数,但需权衡内存开销

4. 忽视NUMA效应

错误:随机分配内存和线程

// 性能可能相差2-3倍
跨NUMA访问延迟:~300ns
本地NUMA访问:~100ns

正确做法:NUMA感知的内存分配和线程绑定

5. 过度批处理

错误:盲目增大批次追求吞吐量

批大小256 → 延迟500ms → 用户体验差

正确做法:根据SLA要求动态调整批大小

6. 同步点过多

错误:频繁的全局同步

for epoch in epochs:
    for batch in batches:
        forward()
        backward()  
        sync()  # 每个batch都同步

正确做法:延迟同步,累积多个batch的梯度

7. 忽略实时约束

错误:平均性能好但最坏情况差

平均延迟:20ms ✓
P99延迟:200ms ✗ (超过100ms要求)

正确做法:关注尾延迟,预留安全余量

8. 上下文切换开销

错误:过度细粒度的任务划分

任务粒度1ms + 切换开销0.5ms = 33%开销

正确做法:合并小任务,减少调度开销