<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>文章 | 木叶吟</title><link>https://yezhisheng.me/zh/post/</link><atom:link href="https://yezhisheng.me/zh/post/index.xml" rel="self" type="application/rss+xml"/><description>文章</description><generator>Wowchemy (https://wowchemy.com)</generator><language>zh-Hans</language><copyright> 又拍云提供CDN服务
京ICP备16021535号-1</copyright><lastBuildDate>Mon, 18 May 2026 12:00:00 +0800</lastBuildDate><image><url>https://yezhisheng.me/media/icon_hu585778a5d9441f07b7d64e1beae1be58_320895_512x512_fill_lanczos_center_3.png</url><title>文章</title><link>https://yezhisheng.me/zh/post/</link></image><item><title>Helix：用图调度自动化通信-计算重叠</title><link>https://yezhisheng.me/zh/post/helix/</link><pubDate>Mon, 18 May 2026 12:00:00 +0800</pubDate><guid>https://yezhisheng.me/zh/post/helix/</guid><description>&lt;p>大模型很少只靠一种干净的并行策略训练或推理。Tensor parallelism 切分矩阵计算，pipeline parallelism 切分模型层，sequence parallelism 把长上下文分散到多张卡上，expert parallelism 则把 token 路由到分布式 expert。真实部署里，这些维度往往会组合在一起。&lt;/p>
&lt;p>这种组合很强，但也带来一个熟悉的系统成本：通信空泡。
当 AllReduce、AllGather、ReduceScatter 或 All-to-All 落在关键路径上时，GPU 计算单元就在等待。在 dense tensor-parallel block 里，等待可能来自 sharded matrix result；在 long-context sequence-parallel block 里，等待可能来自 sequence chunk 交换；在 MoE layer 里，等待可能来自跨 expert 的 token routing。并行策略不同，但问题形状相同：计算和通信都存在，只是执行图没有暴露出足够多可以安全重叠的机会。&lt;/p>
&lt;p>Helix 的核心想法是：communication-computation overlap 应该是一个图调度问题，而不是每遇到一种新并行模式就重新手写一套 kernel trick。Helix 目前是一个从 2026 年初开始的 WIP research project，后续会继续更新。&lt;/p>
&lt;h2 id="为什么手写重叠难以扩展">为什么手写重叠难以扩展&lt;/h2>
&lt;p>最快的 overlap 技术通常会深入 kernel。它们把一个操作切成小块，提前发起通信，并把足够多的计算融合进去以隐藏通信延迟。对单一模式来说，这种方法可以非常有效。Ring-style attention 可以把 sequence exchange 和本地 attention block 重叠起来；tensor-parallel kernel 可以把 collective 和 partial matrix multiplication 做流水；MoE 系统也可以围绕 token dispatch 调度 expert computation。&lt;/p>
&lt;p>问题在于，这类优化往往把模型、collective、tiling shape 和同步协议的假设都写进了实现。一旦模型结构变化，或者部署同时组合 tensor、sequence、expert parallelism，优化就很难复用。更重要的是，局部技巧可能会错过全局机会：某个并行维度产生的通信，本来可以藏到另一个并行维度的计算下面，但 pattern-specific optimizer 未必看得见。&lt;/p>
&lt;p>Helix 把优化边界上移到编译后的执行图。&lt;code>torch.compile&lt;/code> 捕获 model-parallel program 后，Helix 可以在同一个 intermediate representation 里看到 compute operator、communication operator、wait 和 dependency edge。这个统一图抽象是关键。编译器不再需要为每一种并行策略写单独的 overlap recipe；只要节点在图里可见、依赖关系明确，就可以在同一套正确性规则下调度它们。&lt;/p>
&lt;h2 id="调度目标">调度目标&lt;/h2>
&lt;p>从高层看，Helix 把 model-parallel program 看成一张有向图。节点是 compute 或 communication operator，边是 precedence constraint：一个 operator 只有在依赖的数据准备好之后才能运行。&lt;/p>
&lt;p>优化目标很直接，但有约束：&lt;/p>
&lt;ul>
&lt;li>通过把通信藏到独立计算下面，降低图的 makespan；&lt;/li>
&lt;li>保留原始图中的所有数据依赖；&lt;/li>
&lt;li>把 peak memory 控制在设备可用预算内。&lt;/li>
&lt;/ul>
&lt;p>最后一点很重要。激进 overlap 并不是免费的。如果编译器过早 launch 太多 work，中间 activation 和 communication buffer 的生命周期会变长。一个时间线上看起来更快的 schedule，可能因为 peak memory 膨胀而无法运行。因此 Helix 同时优化时间和内存，并用一个轻量 graph simulator 来指导决策。&lt;/p>
&lt;p>系统主要包含三个 compiler pass：tiling、reordering 和 bucketing。&lt;/p>
&lt;h2 id="tiling制造重叠机会">Tiling：制造重叠机会&lt;/h2>
&lt;p>原始执行图通常太粗。一个大的 compute operator 可能必须等待一个大的 communication operator，即使这些工作本来可以拆成小块交错执行。Helix 的第一步是 graph tiling：把 operator 切分成多个 tile stream，同时保留每个 stream 内部的依赖结构。&lt;/p>
&lt;p>默认情况下，论文沿 batch dimension 做 tiling，因为它适用范围广，也更容易保证正确性。在某些 correctness 已经明确的图区域里，也可以沿 sequence length 等其他维度切分。&lt;/p>
&lt;p>Tiling 有两个收益。第一，它暴露 overlap。一个 tile stream 的通信可以在另一个 tile stream 的计算还在运行时提前发起，从而把一张刚性的图变成几条可以交织调度的小流。第二，它可以降低 activation memory。tile 变小后，同时存活的 input 和 intermediate tensor 也更小，只要生命周期控制得当，peak memory 就会下降。&lt;/p>
&lt;p>但 tiling 也有代价。更小的 compute kernel 可能损失效率，尤其是 normalization、softmax 和 pointwise function 这类 memory-bound operator。更小的通信消息也可能降低有效带宽。论文里的 profiling 显示，小 tiling factor 通常是更实际的选择；Helix 默认使用 &lt;code>K = 2&lt;/code>，因为它能暴露足够 overlap，同时避免过度碎片化。&lt;/p>
&lt;h2 id="reordering安全地实现重叠">Reordering：安全地实现重叠&lt;/h2>
&lt;p>Tiling 之后，编译器得到多条相对独立的 tile stream。接下来的问题是 launch order。&lt;/p>
&lt;p>朴素 schedule 会把这些 stream 逐条执行。这当然正确，但会留下通信空泡。过于激进的 schedule 则会提前 launch 大量 asynchronous operator，虽然可能增加 overlap，却也会让太多 tensor 同时存活，推高 peak memory。&lt;/p>
&lt;p>Helix 使用 Segmented Round-Robin Reordering，在两者之间取平衡。关键观察是，显式 wait operator 天然就是 segment boundary。在一条 tile stream 内，Helix 会把连续的 non-blocking compute 和 communication operator 组成一个 segment，直到遇到 wait。之后，它以 round-robin 的方式在多条 stream 之间调度这些 segment。这样，一个 stream 的通信可以被插入另一个 stream 的 compute-heavy 区间，而 wait 仍然保证原始数据依赖不被破坏。&lt;/p>
&lt;p>Segment 级粒度很关键。它比逐 operator eager scheduling 更粗，因此不会让大量中间结果同时挂起；segment 在同步边界被 flush 后，其中间 tensor 可以及时释放。它又比完全串行执行更细，因此能把通信提前到原始图中无法利用的位置。&lt;/p>
&lt;p>这也是 Helix 泛化性的重要来源。调度器不需要关心某个节点来自 tensor parallelism、sequence parallelism 还是 expert parallelism。只要节点在图中可见，依赖关系是显式的，reordering pass 就可以对它做统一推理。&lt;/p>
&lt;h2 id="bucketing把-kernel-效率拿回来">Bucketing：把 kernel 效率拿回来&lt;/h2>
&lt;p>Tiling 创造了灵活性，但过度碎片化会伤害硬件效率。Bucketing pass 负责有选择地修复这种损伤。&lt;/p>
&lt;p>它的想法是，在确实能提升端到端性能时，把不同 tile stream 中兼容的 operator 重新合并成更大的 bucket。这听起来简单，但里面有 trade-off。Bucketing 可以降低 kernel launch overhead，也可以改善计算或通信效率；但它也可能重新引入同步、减少调度自由度，并因为把某些 work 提前而延长 tensor 生命周期。&lt;/p>
&lt;p>Helix 把 bucketing 看成一个 constrained search。对一个 candidate merge，graph simulator 会估计两个量：新的 makespan 和新的 peak memory。只有当节省的时间值得额外内存成本，并且不会破坏 tiling 和 reordering 已经创造出的关键 overlap 时，这个 merge 才有价值。实现上，系统用 dynamic programming 在 candidate bucket 中选择一组最优 merge，使 schedule 在 memory budget 内获得更好性能。&lt;/p>
&lt;p>这个 pass 让 Helix 不只是“全部切碎然后祈祷”。它先主动制造 overlap 粒度，再把不该碎着运行的部分融合回来。&lt;/p>
&lt;h2 id="simulator-是控制回路">Simulator 是控制回路&lt;/h2>
&lt;p>Graph simulator 很轻量，但很核心。它在编译期运行，用来估计候选 schedule 的 runtime 和 peak memory。对 compute 和 communication cost，它结合图中可见的 operator semantics、tensor shape、analytical modeling 和自动 benchmark。对 memory，它模拟执行顺序，并追踪 tensor 和 communication buffer 的生命周期。&lt;/p>
&lt;p>Simulator 不需要完美才有用。它只需要足够准确地给 scheduling choice 排序，让编译器避开明显糟糕的 trade-off。论文报告显示，在 GPT-3、LLaMA3 和 Qwen3-MoE 的配置上，估计结果和真实 trace 比较接近。例如在一个 GPT-3 Curie 配置中，estimated runtime 是 6.80 秒，真实 measured runtime 是 6.41 秒；estimated peak memory 是 65.9 GiB，真实值是 66.0 GiB。&lt;/p>
&lt;p>这种 fidelity 很重要，因为 optimizer 必须在真实运行前做决定。没有 simulator，编译器要么需要昂贵的 trial execution，要么只能依赖脆弱的 heuristic。&lt;/p>
&lt;h2 id="helix-带来了什么">Helix 带来了什么&lt;/h2>
&lt;p>在 GPT-3、LLaMA3 和 Qwen3-MoE workload 上，Helix 展现出一致模式：一旦通信被暴露给图调度，bubble 会缩小，有效 GPU work 会增加。端到端训练吞吐在单节点内提升 4% 到 9%；当通信跨节点时，收益扩大到 12% 到 30%。在 layer level，communication bubble 通常减少超过 60%，这直接说明 scheduler 确实在隐藏通信，而不是简单把开销挪到别处。&lt;/p>
&lt;p>内存收益同样重要。在 long-context inference 中，Helix 最多减少 30% activation memory，在 measured trace 中把 peak memory 从 23.5 GiB 降到 21.4 GiB。这和性能收益来自同一个设计原则：graph scheduler 控制 tile 什么时候变成 live、其中间结果什么时候可以释放，而不是让 overlap 意外拉长 memory lifetime。&lt;/p>
&lt;p>和手写 tensor-parallel overlap 的对比也很有意思。在大规模 GPT 和 LLaMA 训练中，Helix 相对 baseline 达到 17% 和 16% speedup，而 AsyncTP 在同一对比中是 12% 和 13%。这并不是说 compiler scheduling 会让 specialized kernel 过时；更重要的是，graph-level optimizer 可以在同一个地方同时处理跨维度 overlap、正确性、同步、kernel efficiency 和 memory lifetime。&lt;/p>
&lt;p>Helix 的技术核心就是这件事：让通信在图里可见，让依赖关系显式化，然后让编译器去调度那些原本需要为每个 workload 手工重新发现的 overlap。&lt;/p></description></item><item><title>GPU 任务的暂停、恢复与迁移：调度器一直缺的那块拼图</title><link>https://yezhisheng.me/zh/post/gpu-pause-resume-migration/</link><pubDate>Sun, 17 May 2026 15:00:00 +0800</pubDate><guid>https://yezhisheng.me/zh/post/gpu-pause-resume-migration/</guid><description>&lt;p>如果正在运行的 GPU 任务能像普通 CPU 进程一样行动，GPU 集群调度会容易很多。暂停它，移动它，在别处恢复它。当更高优先级任务到来时回收设备。通过迁移修复碎片化，而不杀掉用户工作。&lt;/p>
&lt;p>现实中，GPU 调度恰恰卡在这里。&lt;/p>
&lt;p>CPU 进程可以通过保存 address space、file descriptor 和 kernel-visible state 来 checkpoint。GPU 任务则有另一半生命在普通进程抽象之外：CUDA context、device allocation、stream、event、library handle、正在运行的 kernel，以及驻留在 GPU memory 中的数据。操作系统并不天然知道如何 serialize 这些状态。调度器可以停止 host process，但这不等于拥有一个正确、可迁移的 GPU computation checkpoint。&lt;/p>
&lt;p>&lt;a href="https://yezhisheng.me/publication/flowgpu/">FlowGPU&lt;/a> 的目标就是把 GPU checkpoint/restore 变成系统原语。在 FlowGPU 成为完整系统之前，我写过 &lt;a href="https://github.com/yzs981130/cudaw" target="_blank" rel="noopener">cudaw&lt;/a> 作为代码库的最早版本：一个 CUDA wrapper prototype，用来 interpose runtime call、跟踪 GPU object、翻译 application-visible address，并让 pause/resume/migration 在不修改 CUDA application 的情况下变得可能。&lt;/p>
&lt;h2 id="为什么调度器需要这个原语">为什么调度器需要这个原语&lt;/h2>
&lt;p>Pause/resume 和 migration 会改变调度器能做什么。&lt;/p>
&lt;p>没有 GPU checkpoint/restore 时，抢占往往很粗暴。调度器可以杀掉任务，要求 framework 在预定义 training boundary checkpoint，或者等用户代码主动配合。这对某些 training loop 可以接受，但和集群事件并不匹配。高优先级任务可能现在就到达，GPU 可能现在就故障，碎片化 placement 也可能现在就需要修复。Framework-level checkpoint 通常服务于应用便利性，而不是调度器控制。&lt;/p>
&lt;p>有了透明 GPU checkpoint，scheduler 就拥有了更强的操作：&lt;/p>
&lt;ul>
&lt;li>暂停任务并释放 GPU memory；&lt;/li>
&lt;li>稍后在同一张 GPU 上恢复；&lt;/li>
&lt;li>迁移到另一张 GPU 或另一个节点；&lt;/li>
&lt;li>为 fault tolerance 做周期性 checkpoint；&lt;/li>
&lt;li>通过迁移任务修复集群碎片化；&lt;/li>
&lt;li>用更少用户代码介入支持 elastic scaling 和 priority scheduling。&lt;/li>
&lt;/ul>
&lt;p>这是调度策略和 GPU execution 之间缺失的连接。调度器也许知道正确决策是什么，但如果没有安全 migration primitive，它就无法低成本执行这个决策。&lt;/p>
&lt;h2 id="cuda-wrapper-视角">CUDA Wrapper 视角&lt;/h2>
&lt;p>我的 &lt;code>cudaw&lt;/code> prototype 的基本想法，是在应用和 CUDA runtime 之间放一个 wrapper。应用不再直接和 &lt;code>libcudart&lt;/code> 交互，而是由 wrapper 拦截 allocation、memory copy、kernel launch 等 CUDA call。从调度器的角度看，这会形成一份 execution log，以及 GPU state 的 shadow view。&lt;/p>
&lt;p>这个 wrapper layer 可以记录哪些 device memory region 存在、哪些 host-side pointer 与之对应、数据如何在 CPU 和 GPU 之间移动、哪些 kernel 带着哪些参数被 launch。它也可以维护 virtual GPU address：应用看到稳定的 logical address，而 wrapper 在底层把它们映射到真实 CUDA allocation。这层 indirection 让 restore 和 migration 变得可能，因为恢复后的任务在目标设备上可能拿到不同的 physical GPU address。&lt;/p>
&lt;p>在一个简化的 checkpoint flow 中，wrapper 到达 safe point，同步 GPU work，把 live GPU memory 拷贝到 checkpoint image，保存足以重建 CUDA state 的 metadata，然后释放设备。Restore 则反向执行：在目标 GPU 上分配 memory，重建 mapping，把数据拷贝回来，replay 必要的 CUDA setup call，并继续执行。&lt;/p>
&lt;p>这个早期 prototype 捕捉到的核心直觉，后来也影响了 FlowGPU。GPU migration 不是魔法，而是状态重建。难点在于，让重建后的世界和原来的世界无法区分。&lt;/p>
&lt;h2 id="纯-wrapper-设计难在哪里">纯 Wrapper 设计难在哪里&lt;/h2>
&lt;p>Wrapper 思路很有力量，但边界情况非常残酷。&lt;/p>
&lt;p>第一，CUDA state 远不止 &lt;code>cudaMalloc&lt;/code> 和 &lt;code>cudaMemcpy&lt;/code>。真实应用会使用 stream、event、cuBLAS、cuDNN、NCCL、memory pool、unified memory、graph execution 和 framework allocator。许多对象是 opaque 的：CUDA 暴露的是 handle，而不是可序列化的内部状态。Checkpoint system 必须记录并 replay 创建或修改它们的操作。&lt;/p>
&lt;p>第二，address identity 很重要。Pointer value 可能存储在应用数据结构、kernel argument、framework metadata 或 library state 里。如果 restore 后程序看到不同的 GPU virtual address，即使 bytes 被正确拷贝，应用也可能出现非常隐蔽的错误。&lt;/p>
&lt;p>第三，深度学习框架会隐藏 memory behavior。PyTorch 和 TensorFlow 通常会预留大块 GPU memory 并复用它们。在某个时刻，其中很多 reserved memory 可能并不活跃。Naive checkpoint 如果保存 runtime 分配过的一切，就会产生巨大的 checkpoint image，即使真正有用的 live state 小得多。&lt;/p>
&lt;p>第四，distributed training 是一个同步问题。多 GPU 任务的一致 checkpoint 需要安全暂停所有参与 rank。遇到 NCCL communication 时，如果暂停 blocking send/receive 的一侧，而另一侧还在等待，checkpoint protocol 本身就可能 deadlock。&lt;/p>
&lt;p>这些正是 FlowGPU 试图系统化解决的问题。&lt;/p>
&lt;h2 id="flowgpu-的核心动作">FlowGPU 的核心动作&lt;/h2>
&lt;p>FlowGPU 的关键 insight 是，以前的 system-level GPU checkpoint/restore 设计经常把 C/R 和 API forwarding 绑在一起。在 API forwarding 中，所有 GPU operation 都经过一个 privileged central process。这让 interception 和 state separation 更容易，但会引入 runtime overhead，在 sharing 场景下产生 GPU address conflict，并阻碍部分 GPU feature。&lt;/p>
&lt;p>FlowGPU 把 checkpoint/restore 从 virtualization 中解耦出来。&lt;/p>
&lt;p>正常执行时，每个任务使用 per-task intercept library。GPU operation 仍然属于该任务，并直接访问 GPU，避免 central forwarding process 带来的 IPC overhead。需要 checkpoint 时，FlowGPU 创建一个 ghost process。Ghost process 临时接管 GPU state，而原 process 变成一个传统 CPU process，可以用 CRIU checkpoint。GPU state 和 CPU state 并行保存，再在 restore 时重新组合。&lt;/p>
&lt;p>这个设计保留了 interception 有用的部分，同时避免正常执行期间所有 GPU operation 都绕过 virtualization server。&lt;/p>
&lt;h2 id="让-checkpoint-更小也更可靠">让 Checkpoint 更小也更可靠&lt;/h2>
&lt;p>FlowGPU 增加了几项对深度学习任务尤其重要的机制。&lt;/p>
&lt;p>Active memory identification 避免保存整个 framework-reserved memory pool。FlowGPU 在稳定的 DL framework backend allocation/free interface 上插入 memory stub，跟踪真正活跃的 memory region。它也可以在 checkpoint 前短暂等待 active memory 降到 training iteration 中较低的位置。原因是 training 中 active memory 可能在 iteration 末尾和 forward/backward 中 activation 最重的阶段之间剧烈波动。&lt;/p>
&lt;p>Virtual memory management 用于保留 GPU address identity。FlowGPU 拦截 GPU allocation，并使用 &lt;code>cuMemAddressReserve&lt;/code>、&lt;code>cuMemCreate&lt;/code>、&lt;code>cuMemMap&lt;/code> 等 CUDA VMM API，在 restore 时保留并 remap 相同的 virtual address。这移除了 pointer-rich GPU application 中一类主要 correctness bug。&lt;/p>
&lt;p>Record/replay 用来处理 opaque runtime object。CUDA stream、event、context 和 library handle 无法简单读成 bytes，因此 FlowGPU 记录创建或修改它们的操作，并在恢复期间 replay。&lt;/p>
&lt;p>Pause mechanism 也针对 distributed task 做了细化。FlowGPU 会协调多个 rank 的暂停，但为了避免一种已知 NCCL deadlock pattern，如果完整 pause 无法达成，它会在 timeout 后恢复所有 instance。这个细节很小，后果很大：checkpointing 不能引入一个比原问题更糟的 failure mode。&lt;/p>
&lt;p>对于 multi-GPU task，FlowGPU 还做了细粒度 deduplication。Replicated model parameter 可能出现在多张 GPU 上，但 runtime memory block 很少完全一致。FlowGPU 对固定大小 region 做去重，降低分布式任务的 checkpoint image size。&lt;/p>
&lt;h2 id="这对调度意味着什么">这对调度意味着什么&lt;/h2>
&lt;p>一旦 GPU pause/resume 变得实用，很多调度策略就更现实了。&lt;/p>
&lt;p>Priority scheduling 可以抢占低优先级 GPU 任务，而不丢掉它的全部进度。Fairness scheduling 可以用更低扰动在时间上重新分配 service。Fragmentation-aware scheduler 可以迁移任务，重建 gang-scheduled workload 需要的连续 placement。Fault-tolerance system 可以按调度器控制的间隔 checkpoint，而不只依赖 framework checkpoint。Elastic scheduler 可以更清晰地 shrink、expand 或 relocate 任务。&lt;/p>
&lt;p>这个原语也改变了 GPU sharing 的经济性。如果一个任务可以快速 pause 和 restore，集群就能在 bursty demand 下采取更激进的动作。Online inference、training 和 HPO workload 不必完全生活在彼此隔离的资源孤岛里；当优先级变化时，调度器有了更好的移动工作方式。&lt;/p>
&lt;p>FlowGPU 的评估展示了细节为什么重要。论文报告称，因为任务可以不经过 API forwarding 直接访问 GPU，它在正常 single-GPU execution 中没有 runtime overhead。对于 DL task，相比 POS，FlowGPU 将 checkpoint pause time 降低 6.2x 到 15x；相比 Singularity，最多降低 10.4x。Restore time 相比 POS 降低 12x 到 18x，相比 Singularity 最多降低 4.1x。Migration 方面，FlowGPU 最多比 Singularity 快 2.1x，比 PyTorch framework-level checkpointing 快 1.7x 到 4.5x。&lt;/p>
&lt;p>这些数字不只是 checkpointing 结果，也是调度能力被释放出来的结果。慢 checkpoint 是调度器不敢频繁使用的 policy；快而透明的 checkpoint 才会变成真正的 control knob。&lt;/p>
&lt;h2 id="小结">小结&lt;/h2>
&lt;p>GPU 调度经常被讨论成算法问题：fairness metric、placement heuristic、bin packing、elastic allocation 和 priority queue。但调度器的能力上限取决于下面的 execution primitive。&lt;/p>
&lt;p>&lt;code>cudaw&lt;/code> 是我对 wrapper-level 直觉的第一版实现：interpose CUDA，virtualize application 看到的东西，并在需要时重建 GPU state。FlowGPU 把这个直觉推进成更完整的系统设计：per-task interception 保证低开销，ghost process 实现 state separation，active-memory tracking 缩小 image，VMM 保证 address correctness，distributed pause logic 支撑 multi-GPU workload。&lt;/p>
&lt;p>最终结果是 policy 和 mechanism 之间更清晰的边界。调度器决定一个任务什么时候应该 pause、resume 或 move；checkpoint/restore layer 让这个决策足够安全，可以真正执行。&lt;/p>
&lt;p>Paper: &lt;a href="https://yezhisheng.me/publication/flowgpu/">FlowGPU: Transparent and Efficient GPU Checkpointing and Restore&lt;/a>&lt;br>
Early codebase: &lt;a href="https://github.com/yzs981130/cudaw" target="_blank" rel="noopener">yzs981130/cudaw&lt;/a>&lt;/p></description></item><item><title>GPU 集群调度：深度学习任务该如何排队、放置与共享</title><link>https://yezhisheng.me/zh/post/gpu-cluster-scheduling/</link><pubDate>Sun, 17 May 2026 14:30:00 +0800</pubDate><guid>https://yezhisheng.me/zh/post/gpu-cluster-scheduling/</guid><description>&lt;p>GPU 集群调度很容易被低估。乍看起来，它像一个熟悉的资源分配问题：任务到达，GPU 有空闲也有忙碌，调度器决定谁先运行。&lt;/p>
&lt;p>深度学习打破了这种简单性。&lt;/p>
&lt;p>训练任务可能运行好几天，需要成组 GPU，并且对 placement topology 非常敏感。推理服务是在线服务，对 latency 敏感，如果不做 batching 或 colocation，往往又难以充分利用 GPU。超参数搜索会启动大量相似 trial，其中大多数注定会被丢弃。LLM workload 还会带来 model parallelism、巨大的 memory footprint、long context，以及开发过程中的 bursty pattern。&lt;/p>
&lt;p>我们的 survey，&lt;a href="https://yezhisheng.me/publication/survey/">Deep Learning Workload Scheduling in GPU Datacenters&lt;/a>，试图整理这个复杂的设计空间。理解这个领域最有用的方式，不是把调度器列成清单，而是看它们面对的一组张力：速度与成本、利用率与隔离、公平性与效率、在线 latency 与集群整体吞吐。&lt;/p>
&lt;h2 id="为什么深度学习调度不一样">为什么深度学习调度不一样&lt;/h2>
&lt;p>传统 HPC 和大数据调度器提供了有用起点，但深度学习任务有自己的物理规律。&lt;/p>
&lt;p>训练任务往往需要 gang scheduling。一个分布式任务必须同时拿到所有请求的 GPU，因此 GPU 不像 CPU slot 那样容易切分。Placement 很重要，因为通信密集型任务如果被放在同一节点内或通过 NVLink 连接，可能比散落在弱链路上快得多。抢占很昂贵，因为模型和优化器状态都很大。同时，训练又具有迭代性，所以少量 profiled iteration 往往能暴露 throughput、memory behavior 和 placement sensitivity。&lt;/p>
&lt;p>推理的压力几乎相反。每个请求相比训练任务很小，但服务有 latency SLO。Batching 可以提高 GPU utilization，但等待组 batch 会增加用户可见 latency。Colocation 可以提升 throughput，但 interference 可能打破 tail latency。调度器必须在平均效率和最坏情况下的用户体验之间做取舍。&lt;/p>
&lt;p>这就是为什么 GPU 集群调度不是一个单一问题。它是一组相关问题，正确答案取决于工作负载。&lt;/p>
&lt;h2 id="训练效率公平性deadline">训练：效率、公平性、Deadline&lt;/h2>
&lt;p>对于训练任务，survey 把调度目标分成三大类。&lt;/p>
&lt;p>第一类是效率。有些调度器通过 priority rule 降低任务完成时间，比如 least attained service 或 progress-aware variant。另一些调度器使用 profiling 或 learning-based method 预测任务时长、速度、placement sensitivity 或未来资源需求。Placement 是效率的核心部分：一个调度器可能在总量上有足够 GPU，却因为集群碎片化而无法满足 locality，导致性能很差。&lt;/p>
&lt;p>第二类是公平性。公平性很微妙，因为在常见 gang-scheduling 场景中 GPU 不可分割，而异构 GPU 对不同任务的价值也不一样。Finish-time fairness、long-term GPU-time fairness 和 heterogeneity-aware fairness 都在回答同一个问题的不同版本：这个任务或租户应得多少 service，实际又获得了多少？&lt;/p>
&lt;p>第三类是 deadline guarantee。Deadline-aware training 研究相对少，但对生产流程很重要。Best-effort 任务可以容忍等待；SLO 任务不行。这类系统需要预测某个任务在不同 placement 和 resource allocation 下能否按 deadline 完成，再决定如何混合 deadline 任务和普通任务。&lt;/p>
&lt;h2 id="训练gpu-如何被使用">训练：GPU 如何被使用&lt;/h2>
&lt;p>目标只是 taxonomy 的一半，另一半是调度器如何使用资源。&lt;/p>
&lt;p>Heterogeneous resource scheduling 认识到“一张 GPU”并不是一个统一单位。不同 model architecture 对新一代 GPU、CPU allocation、memory、network bandwidth 和 storage 的收益不同。一个 cost-effective 调度器应该把任务放到和其 bottleneck 匹配的硬件上，而不是盲目把所有任务都送到最新设备。&lt;/p>
&lt;p>GPU sharing 试图解决 underutilization 问题。许多训练任务无法吃满现代 GPU。通过 MPS、MIG、virtualization、time sharing 或 framework-level co-execution，把多个任务打包到同一设备上可以提高利用率。风险是 interference：调度器必须知道什么时候 sharing 有收益，什么时候它只是悄悄拖慢所有任务。&lt;/p>
&lt;p>Elastic training 会随时间改变分配给任务的 GPU 数量。在需求波动时，它可以减少排队并提升利用率。但 elasticity 不是免费的。资源变化可能需要 checkpoint、reinitialization 或 batch-size adaptation。如果 batch size 的变化影响 convergence，调度器可能提升了系统 throughput，却悄悄改变了模型行为。&lt;/p>
&lt;p>一个大趋势是，训练调度器越来越需要和训练框架协同设计。调度器想要细粒度控制，但框架才知道一个任务是否能安全 pause、resize、share 或改变 batch size。&lt;/p>
&lt;h2 id="推理latency成本throughput">推理：Latency、成本、Throughput&lt;/h2>
&lt;p>推理调度由另一组三角关系塑造：latency、cost 和 accuracy。&lt;/p>
&lt;p>Latency 通常是一等约束。Model serving system 可以通过 batching 提升 throughput，但请求在队列里等待本身就是用户可见 latency。实际调度器往往使用 dynamic batching：服务健康时增大 batch size；latency 接近 SLO 时缩小 batch。&lt;/p>
&lt;p>Cost 来自 cloud instance selection、autoscaling 和 heterogeneous hardware。有些工作负载在 CPU 上更便宜，有些需要 GPU，还有些只有在 batch 足够大时才划算。调度器不仅要决定模型放在哪里，还要决定需要多少 replica、哪些 instance type 值得付费。&lt;/p>
&lt;p>Accuracy 又引入了一个维度。有些系统会在 model variant、ensemble 或 modality 之间选择。小模型便宜快速但准确率较低；大模型更慢但效果更好。这让推理调度变成 policy problem：在给定 latency 或 cost budget 下，可以接受多大 accuracy loss？&lt;/p>
&lt;p>Throughput 技术包括 batching、caching、model residency 和 colocation。但推理 colocation 比训练 colocation 更危险，因为 SLO violation 是即时可见的。调度器需要 interference model、isolation mechanism 或 hardware partitioning，才能让 sharing 安全。&lt;/p>
&lt;h2 id="训练和推理之外">训练和推理之外&lt;/h2>
&lt;p>有些工作负载值得单独分类。&lt;/p>
&lt;p>Hyperparameter optimization 技术上属于训练，但在操作上很不一样。它会启动许多相似 trial，提前剪枝较弱的 trial，并把资源转向更有前途的 configuration。这种结构带来了 early stopping、elastic trial allocation、trial packing、model fusion 和 surrogate-based tuning 的机会。我们的 Hydro 工作就是一个例子：它用 model scaling、trial fusion 和 cluster-level interleaving 让 HPO 少一点 brute force。&lt;/p>
&lt;p>混合训练和推理工作负载是另一个前沿。推理集群往往为了应对 burst 而过度配置，在低流量期间留下 idle GPU。如果系统能在推理需求回来时快速 preempt 或 resize 训练任务，训练就可以借用这部分容量。挑战是，在回收空闲资源的同时仍然尊重在线 SLO。&lt;/p>
&lt;p>这些例子指向一个更大的趋势：未来调度器会越来越 workload-aware。面对深度学习开发的多样性，一个泛泛的 GPU queue 已经太粗糙。&lt;/p>
&lt;h2 id="这个领域正在走向哪里">这个领域正在走向哪里&lt;/h2>
&lt;p>Survey 最后总结了三个至今仍然重要的研究方向。&lt;/p>
&lt;p>第一，emerging workload 会继续改变调度器设计。LLM pretraining、fine-tuning、serving、agentic inference 和 HPO 都暴露出不同瓶颈。调度器需要理解的不只是 GPU 数量，还包括 memory pressure、communication structure、context length、trial similarity 和 elasticity。&lt;/p>
&lt;p>第二，调度决策需要更好的智能。Heuristic 鲁棒且容易部署，mathematical optimization 更有原则但可能很慢，ML/RL-based scheduler 能捕捉复杂 pattern 但难以信任和 benchmark。实际调度器可能会结合三者：fast path 用 heuristic，profiling 用于校准，复杂决策再交给 optimization 或 learning。&lt;/p>
&lt;p>第三，hardware heterogeneity 已经不可避免。生产集群可能包含多代 GPU、专用 interconnect、CPU、storage tier 和 accelerator。异构性带来更好的 cost-performance 机会，但也让公平性更复杂。给一个任务分配老 GPU 和新 GPU，即使用时相同，也很少代表相同服务。&lt;/p>
&lt;p>最简单的总结是：GPU 调度已经不再只是填满空 slot。它是在用户可见目标之下，把工作负载结构匹配到硬件结构。&lt;/p>
&lt;p>这也是这个方向有意思的地方。最好的调度器不只是队列最短的那个，而是理解眼前的深度学习任务是什么、真正需要什么资源，以及集群愿意做出什么 trade-off 的那个。&lt;/p>
&lt;p>Paper: &lt;a href="https://yezhisheng.me/publication/survey/">Deep Learning Workload Scheduling in GPU Datacenters: A Survey&lt;/a>&lt;br>
Project: &lt;a href="https://github.com/S-Lab-System-Group/Awesome-DL-Scheduling-Papers" target="_blank" rel="noopener">Awesome DL Scheduling Papers&lt;/a>&lt;/p></description></item><item><title>ResiHP：大模型训练故障下的动态混合并行</title><link>https://yezhisheng.me/zh/post/resihp/</link><pubDate>Sun, 17 May 2026 14:00:00 +0800</pubDate><guid>https://yezhisheng.me/zh/post/resihp/</guid><description>&lt;p>Reference reading: &lt;a href="https://zhuanlan.zhihu.com/p/2036192731547035544" target="_blank" rel="noopener">大模型训练遇到 GPU 故障怎么办？我们的做法是动态调整 3D 并行&lt;/a>.&lt;/p>
&lt;p>大规模 LLM 训练不是一个单独的分布式系统问题，而是好几个问题叠在一起。&lt;/p>
&lt;p>当训练规模达到数百甚至数千张 GPU 时，故障不再是罕见事件。有些设备会直接消失；另一些设备还活着，但变慢了。后者尤其麻烦：fail-slow GPU 不会让训练任务崩溃，却会拖慢整个同步训练迭代。在 hybrid parallel training 中，这个延迟会沿着 tensor parallelism、pipeline parallelism 和 data parallelism 传播，最后让一张变慢的设备悄悄决定整个任务的速度。&lt;/p>
&lt;p>&lt;a href="https://yezhisheng.me/publication/resihp/">ResiHP&lt;/a> 就是为这个场景设计的。它的核心思想是让 hybrid parallelism 变成动态结构。ResiHP 不把 3D parallel layout 当作启动后固定不变的配置，而是在检测到异常设备后，围绕剩余资源重新组织训练计划。&lt;/p>
&lt;h2 id="为什么故障检测很难">为什么故障检测很难&lt;/h2>
&lt;p>最直观的信号是 iteration time。如果某一次 iteration 变慢了，也许就有设备故障。&lt;/p>
&lt;p>这个逻辑对 LLM 训练来说太脆弱。&lt;/p>
&lt;p>现代 LLM workload 经常使用 variable-length sequences。即使用 sequence packing 控制 token budget，真实 attention cost 仍然取决于每个 micro-batch 内部的 sequence length。一个包含许多长序列的 packed batch，自然会比短序列更多的 packed batch 更慢。Pipeline scheduling 又增加了一层噪声：观测到的 iteration time 不只是某个 micro-batch cost，而是多个 pipeline stage 上 forward、backward 和 weight-update chunk 共同形成的 critical path。&lt;/p>
&lt;p>这也是知乎文章强调的点：detector 不能盯着 raw iteration time，把每个 spike 都判成故障。它首先要估计，如果所有设备都健康，这个 iteration 本来应该花多少时间。&lt;/p>
&lt;h2 id="用-flops-归一化检测信号">用 FLOPs 归一化检测信号&lt;/h2>
&lt;p>ResiHP 的 Detector 会用期望计算量对 iteration time 做归一化。&lt;/p>
&lt;p>在 micro-batch 层面，它根据 packed sequence structure 估计工作量。Attention cost 并不是 sequence length 的线性函数，所以模型不仅数 token，还会考虑 quadratic attention cost。在 pipeline 层面，ResiHP 模拟 forward、backward 和 weight-update chunk 的 schedule，预测健康 iteration 的 critical path。&lt;/p>
&lt;p>只有在完成这种归一化之后，ResiHP 才比较 observed time 和 expected time。如果二者之间的 gap 仍然异常，系统才把它当作 fail-slow signal，而不是普通的 sequence-length variation。Fail-stop 则通过 missing heartbeat 另行处理。&lt;/p>
&lt;p>这个区分很重要，因为 false positive 代价很高。如果一个 resilient training system 经常把正常工作负载偏斜误判成硬件故障，它就会毫无必要地反复重组训练任务。ResiHP 试图让检测足够轻量，能够在线使用；同时也足够准确，让 adaptation 只在真正出问题时发生。&lt;/p>
&lt;h2 id="为什么混合并行让恢复更棘手">为什么混合并行让恢复更棘手&lt;/h2>
&lt;p>一旦某个设备被识别为不健康，最简单的反应是把它移除。&lt;/p>
&lt;p>但这通常不够。&lt;/p>
&lt;p>在纯 data parallelism 中，少一个 worker 主要意味着 replica 数量下降。在 hybrid parallelism 中，一个设备参与的是结构。它可能同时是 tensor-parallel group 里的一个 rank、pipeline 的一个 stage，以及 data-parallel replica 的成员。如果一个 tensor-parallel rank 故障，整个 TP group 都受影响。如果一个 pipeline stage 变慢，上游和下游 stage 都会等待。如果一个 data-parallel replica 落后，同步也会被拖慢。&lt;/p>
&lt;p>故障是局部的，但性能损伤是全局的。&lt;/p>
&lt;p>因此 ResiHP 不采用单一 workaround，而是在多个层面适配。它会改变 parallelism group size，重新划分 pipeline stage 上的 model layer，调整工作调度，并在 replica 之间重新分配任务。&lt;/p>
&lt;h2 id="动态重组-3d-并行">动态重组 3D 并行&lt;/h2>
&lt;p>ResiHP 的 Scheduler 负责把检测结果转化为新的训练计划。&lt;/p>
&lt;p>对于 tensor parallelism，ResiHP 可以围绕健康设备收缩或重组 TP group。目标不是简单丢弃受影响 group 里的所有设备，因为那可能浪费太多健康 GPU。调度器会搜索更合适的 group size 和 membership，在避开慢 rank 或故障 rank 的同时保留尽可能多的有效计算。&lt;/p>
&lt;p>对于 pipeline parallelism，ResiHP 可以重新平衡 model partitioning。慢 stage 不应该继续承担和健康 stage 一样多的 layer。如果某个 stage 变慢，调度器可以给它分配更少 layer，并把工作转移给更健康的 stage，从而降低 pipeline bottleneck。&lt;/p>
&lt;p>对于 data parallelism，ResiHP 使用 workload migration。如果某个 replica 落后，而另一个 replica 还有余量，调度器可以迁移一部分工作，让整体进度更平衡。这一点尤其有用，因为 data-parallel replica 在逻辑上对称，但在设备故障或性能退化之后，实际速度可能分化。&lt;/p>
&lt;p>关键工程点在于，这些 adaptation 是协调发生的。只调 TP 可能制造 pipeline imbalance；只调 PP 可能让健康 GPU 利用不足；只调 DP 可能没有消除原始瓶颈。ResiHP 把 layout 当作一个相互连接的 3D object 来处理。&lt;/p>
&lt;h2 id="executor-如何兜住重配置">Executor 如何兜住重配置&lt;/h2>
&lt;p>新的 plan 只有在 runtime 能执行时才有意义，而且 recovery 本身不能变成第二次故障。&lt;/p>
&lt;p>ResiHP 的 Executor 负责动态重配置的具体机制。它在新的 parallel layout 下重建 model 和 optimizer state，更新通信策略，并支持针对新 group 的高效 data movement。也正是在这里，系统从调度策略进入真正的 fault-tolerant training。&lt;/p>
&lt;p>Executor 对 fail-stop recovery 也很重要。如果 GPU 消失了，系统必须在重新分布受影响 model shard 和工作负载的同时保持训练连续性。如果 GPU 只是变慢了，系统又必须避免过度反应，同时降低它对全局 critical path 的影响。&lt;/p>
&lt;h2 id="resihp-带来了什么">ResiHP 带来了什么&lt;/h2>
&lt;p>ResiHP 在 256-GPU 集群上、多个故障场景中进行了评估。论文报告称，它达到接近最优的故障检测准确率，并相比已有 resilient training system 将训练吞吐提升 1.13x 到 2.22x。&lt;/p>
&lt;p>更大的经验是：LLM 训练的 resilience 不能只是一个 checkpoint-and-restart loop。Hybrid parallelism 本来就是大规模训练能够成立的结构，因此 resilience 也必须理解这个结构。ResiHP 把问题拆成三个层次：&lt;/p>
&lt;ul>
&lt;li>这次 slowdown 是真实故障，还是 sequence-length variation？&lt;/li>
&lt;li>3D parallel layout 中真正受损的是哪一部分？&lt;/li>
&lt;li>TP、PP 和 DP 应该如何一起改变，才能让训练任务持续前进？&lt;/li>
&lt;/ul>
&lt;p>我喜欢 ResiHP 的地方就在这里：它把故障处理看成一个 dynamic parallelism 问题，而不只是 device replacement 问题。&lt;/p>
&lt;p>Paper: &lt;a href="https://yezhisheng.me/publication/resihp/">ResiHP: Taming LLM Training Failures with Dynamic Hybrid Parallelism&lt;/a>&lt;br>
Preprint: &lt;a href="https://arxiv.org/abs/2605.06374" target="_blank" rel="noopener">arXiv:2605.06374&lt;/a>&lt;/p></description></item><item><title>CONCUR：让 Agent 批量推理避开中期拥塞</title><link>https://yezhisheng.me/zh/post/concur/</link><pubDate>Sun, 17 May 2026 13:10:00 +0800</pubDate><guid>https://yezhisheng.me/zh/post/concur/</guid><description>&lt;p>过去的 LLM 批量推理主要围绕请求组织。一个请求到达后，服务端调度 prefill 和 decode，为这条序列维护 KV cache，请求结束后再释放相关状态。&lt;/p>
&lt;p>Agentic workload 不一样。一个 agent 不是一次请求，而是一个长时间运行的循环：planning、tool call、observation，然后继续 generation。许多 agent 可以同时存活，并逐步累积自己的 KV state。服务端看到的仍然是一个个请求，但真正制造资源压力的是 agent 的生命周期。&lt;/p>
&lt;p>&lt;a href="https://yezhisheng.me/publication/concur/">CONCUR&lt;/a> 关注的就是这种场景下出现的一类病理现象：mid-phase thrashing。&lt;/p>
&lt;h2 id="什么是中期抖动">什么是中期抖动&lt;/h2>
&lt;p>一批长时间运行的 agent 并不会一开始就失败。刚开始时，大多数 agent 的上下文还很短，KV cache 需求有限，吞吐看起来很健康。接近结束时，很多 agent 已经完成，压力也会下降。&lt;/p>
&lt;p>真正困难的是中间阶段。&lt;/p>
&lt;p>真正困难的是中期：大量 agent 仍然活跃，而且它们的上下文已经明显增长。整体 KV cache footprint 变得很大，但 GPU memory 可能还没有被完全耗尽。这让问题很隐蔽：单看容量检查，系统似乎还能运行；但从 cache efficiency 看，它已经接近失控。&lt;/p>
&lt;p>当 KV pressure 跨过某个阈值后，请求级 cache 管理会开始和自己打架。Serving system 可能会为了给新内容腾空间而 evict 旧的 KV blocks。但 agentic workload 很快又会回到这些被 evict 的历史，因为同一个 agent 会继续生成、调用工具、再继续推理。于是服务端不得不重新计算或重新加载上下文，消耗 GPU 时间，并带来更多 cache churn。更多 churn 导致更多 eviction，更多 eviction 又导致更多 recomputation。内存容量还没正式耗尽，吞吐已经先崩了。&lt;/p>
&lt;p>这就是 mid-phase thrashing。&lt;/p>
&lt;h2 id="为什么只管请求已经太晚">为什么只管请求已经太晚&lt;/h2>
&lt;p>根因是控制粒度错位。Serving runtime 管的是单个请求，但压力来源是活跃 agent 的数量。&lt;/p>
&lt;p>如果一开始放进来的 agent 太多，每个 agent 都会继续增长自己的上下文。Reactive cache policy 只能在 KV cache 已经拥塞之后再反应。LRU 这类 eviction 对单个请求流可能局部合理，但对 agentic workload 来说不是一个好的全局信号。它不知道某个被 evict 的 block 属于一个仍然存活的 agent，而这个 agent 很可能很快又需要它。&lt;/p>
&lt;p>换句话说，系统并不只是“内存不够了”。它是把太多长生命周期的 state machine 同时放进了一个共享 cache。&lt;/p>
&lt;p>CONCUR 把问题从“现在该 evict 哪个 KV block”改写成“同一时间应该允许多少 agent 保持活跃”。&lt;/p>
&lt;h2 id="在-agent-层做准入控制">在 Agent 层做准入控制&lt;/h2>
&lt;p>CONCUR 在 LLM serving engine 上方加入了一个轻量控制层。它不替换后端 cache manager，而是调节 agent admission，让活跃 agent 的整体压力停留在 cache efficiency 崩溃点之前。&lt;/p>
&lt;p>这个设计借鉴了 congestion control 的思路。KV cache 被视为共享瓶颈资源，并发活跃 agent 数量就是 control window。当运行时 cache signal 表明系统健康时，CONCUR 增加并发，进一步利用容量；当 signal 显示系统开始拥塞时，它会在抖动失控之前退让。&lt;/p>
&lt;p>这更接近 AIMD-style control，而不是静态 batching。Additive increase 让系统谨慎探索更多并行度，multiplicative decrease 则在 cache pressure 变危险时快速反应。关键细节是，控制单位是 agent，而不是请求。暂停接纳新 agent 可以保留已经进入系统的 agent 的执行连续性，也避免反复 evict 它们很快还会复用的历史。&lt;/p>
&lt;p>这种主动控制还保持了兼容性。已有 LLM serving system 仍然可以在内部管理请求调度和 KV placement。CONCUR 只根据 cache-aware feedback 决定 active set 中允许存在多少 agent。&lt;/p>
&lt;h2 id="为什么有效">为什么有效&lt;/h2>
&lt;p>Mid-phase thrashing 的根源是累积状态压力，所以解决方案必须在 cache 进入 thrashing regime 之前介入。通过约束活跃 agent 数量，CONCUR 减少了同一时间争抢 KV cache 的长生命周期上下文。系统可能同时跑更少 agent，但每个活跃 agent 经历的 eviction 和 recomputation 更少，所以有效 generation throughput 反而提升。&lt;/p>
&lt;p>论文报告显示，CONCUR 可以在大模型和真实 agent workload 上避免 mid-phase thrashing，并将批量推理吞吐在 Qwen3-32B 上最多提升 4.09x，在 DeepSeek-V3 上提升 1.9x。&lt;/p>
&lt;p>这里的经验很简单，但很容易忽略：对于 agentic inference，正确的调度对象不一定总是请求。有时候请求只是表象。真正累积状态、持续消耗 cache、并一遍遍回到同一段历史的实体是 agent。CONCUR 让 serving system 看见了这个实体。&lt;/p>
&lt;p>Paper: &lt;a href="https://yezhisheng.me/publication/concur/">CONCUR: High-Throughput Agentic Batch Inference of LLM via Congestion-Based Concurrency Control&lt;/a>&lt;br>
Preprint: &lt;a href="https://arxiv.org/abs/2601.22705" target="_blank" rel="noopener">arXiv:2601.22705&lt;/a>&lt;/p></description></item><item><title>ASTRAEA：GPU 集群里的公平，不只是分到几张卡</title><link>https://yezhisheng.me/zh/post/astraea/</link><pubDate>Sun, 17 May 2026 13:00:00 +0800</pubDate><guid>https://yezhisheng.me/zh/post/astraea/</guid><description>&lt;p>公平性听起来很简单，直到一个 GPU 集群真的开始承载各种深度学习任务。&lt;/p>
&lt;p>在共享的科研或生产集群里，不同租户提交的任务形态差异很大。有些任务只需要一张 GPU，跑几分钟做调试；有些训练需要很多张 GPU，并且连续运行好几天。只追求利用率的调度器，可能让长任务长期占据集群；过度偏向短任务的调度器，又可能让大型训练一直排队。两类用户都可以很合理地说：这个系统不公平。&lt;/p>
&lt;p>&lt;a href="https://yezhisheng.me/publication/astraea/">ASTRAEA&lt;/a> 关注的正是这个问题：多租户 GPU 集群如何在不浪费昂贵加速器的前提下，把公平性真正落到调度决策里？&lt;/p>
&lt;h2 id="为什么已有公平性会失效">为什么已有公平性会失效&lt;/h2>
&lt;p>传统集群调度器经常从“瞬时资源公平”的角度思考问题。比如两个用户共享一个集群，那么在某个时刻，每个人都应该拿到自己的公平份额。对很多大数据任务来说，这个思路很自然，因为任务更容易切分、迁移和重新平衡。&lt;/p>
&lt;p>深度学习训练没有这么灵活。训练任务通常需要 gang scheduling：请求的所有 GPU 必须同时分配到位。通信密集型任务对 GPU 拓扑很敏感。抢占也很贵，因为模型状态和优化器状态需要 checkpoint、移动和恢复。如果调度器为了追求公平而频繁重排 GPU，反而可能破坏原本想保护的性能。&lt;/p>
&lt;p>另一类方法是 finish-time fairness，也就是判断一个任务是否不晚于它在私有 fair-share cluster 里的完成时间。这个目标有用，但不完整。它强调时间，却容易忽略公平性的空间维度：一个请求更多 GPU 的任务，在单位时间内消耗了更多集群容量。只用完成时间看待 1-GPU 任务和 8-GPU 任务，可能鼓励用户多报资源。&lt;/p>
&lt;p>ASTRAEA 的核心想法是直接度量集群真正付出的东西：GPU-time。&lt;/p>
&lt;h2 id="长期-gpu-time-公平性">长期 GPU-Time 公平性&lt;/h2>
&lt;p>ASTRAEA 提出了 Long-Term GPU-Time Fairness，简称 LTGF。它不只问“这个租户现在有几张 GPU”，也不只问“这个任务什么时候完成”，而是问：在一段时间内，某个租户或任务实际获得了多少 GPU service，相比它应得的份额是否公平。&lt;/p>
&lt;p>这个指标同时捕捉了分配的两个维度：&lt;/p>
&lt;ul>
&lt;li>时间维度：任务运行了多久；&lt;/li>
&lt;li>空间维度：任务运行时占用了多少 GPU。&lt;/li>
&lt;/ul>
&lt;p>在租户层面，LTGF 按照预算或 quota 这类权重分配 GPU-time。在任务层面，它在同一个租户内的并发任务之间公平分配 GPU-time。这个两级视角很重要，因为公平集群既要保护组织层面的共享契约，也要照顾每个租户队列里正在等待的具体任务。&lt;/p>
&lt;p>这个指标还避免了对剩余时间预测的过度依赖。真实集群里，用户会取消任务，任务会失败，训练吞吐也会随 placement 变化。ASTRAEA 可以根据历史分配记录评估公平性，再用这个信号决定下一步该服务谁。&lt;/p>
&lt;h2 id="astraea-如何调度">ASTRAEA 如何调度&lt;/h2>
&lt;p>ASTRAEA 使用两阶段调度算法。&lt;/p>
&lt;p>第一阶段，它选择 tenant-level fairness index 最低的租户。直白地说，调度器会找到那个相对自己应得份额而言，过去获得 GPU-time 最少的租户。如果这个租户有等待中的任务，并且集群可以放下其中某个任务，ASTRAEA 就会把资源分给它。&lt;/p>
&lt;p>第二阶段，ASTRAEA 在该租户内部用 job-level fairness index 选择具体任务。这样即使一个租户整体上被公平对待，它内部的任务队列也不会变得很不公平。任务级策略仍然可以加入实际优先级，但需要受到公平性信号约束。&lt;/p>
&lt;p>ASTRAEA 采用 lease-based 调度。它不会在公平性发生变化时立刻抢占，而是给运行中的任务一个租约周期。在租约边界，调度器可以重新安排执行顺序来修复公平性。这是一个务实折中：短租约能更快响应公平性变化，但太短又会增加抢占开销，拉长任务完成时间。ASTRAEA 会为深度学习训练选择一个平衡这些因素的租约长度。&lt;/p>
&lt;h2 id="它带来了什么">它带来了什么&lt;/h2>
&lt;p>ASTRAEA 在真实 GPU 集群 trace 上做了大规模仿真评估，包括 SenseTime 的 Venus trace 和 Microsoft 的 Philly trace。论文报告称，相比已有先进调度器，ASTRAEA 将租户级公平性最多提升 9.42x，将任务级公平性最多提升 10.3x，同时没有牺牲平均任务完成时间。&lt;/p>
&lt;p>这里重要的经验是：GPU 集群里的公平性不只是政策偏好，它首先是一个测量问题。如果指标忽略 GPU 数量，用户就可能 overclaim；如果指标忽略时间，长任务就可能被饿死；如果指标忽略租户，集群就会违反共享契约；如果指标忽略任务，个体用户仍然会感受到不公平。&lt;/p>
&lt;p>ASTRAEA 的贡献，是把深度学习集群中最关键的资源服务单位“长期 GPU-time”变成了可测量、可调度的公平性目标。&lt;/p>
&lt;p>Paper: &lt;a href="https://yezhisheng.me/publication/astraea/">ASTRAEA: A Fair Deep Learning Scheduler for Multi-tenant GPU Clusters&lt;/a>&lt;br>
Code: &lt;a href="https://github.com/yzs981130/Astraea_Artifacts/" target="_blank" rel="noopener">Astraea Artifacts&lt;/a>&lt;/p></description></item><item><title>Hydro：把超参数搜索放进流水线空泡</title><link>https://yezhisheng.me/zh/post/hydro/</link><pubDate>Sun, 17 May 2026 12:00:00 +0800</pubDate><guid>https://yezhisheng.me/zh/post/hydro/</guid><description>&lt;p>超参数搜索曾经像是一种可以接受的成本。训练很多个模型，扫几组 learning rate 和 batch size，然后留下最好的那个。它很贵，但仍然属于正常工程节奏的一部分。&lt;/p>
&lt;p>后来模型大到一定程度，这个心智模型悄悄失效了。&lt;/p>
&lt;p>如果训练一个模型本身就已经占用 GPU 集群的大块资源，那么传统 hyperparameter sweep 几乎显得荒唐：系统要求我们训练很多个高度相似的模型，而其中大多数只是为了被丢弃。更糟的是，已有 tuning framework 通常只看自己被分配到的资源。它们并不知道周围的集群里可能存在空闲 GPU 碎片、异构加速器，或者带有周期性空泡的长时间 pipeline-parallel 训练任务。&lt;/p>
&lt;p>&lt;a href="https://yezhisheng.me/publication/hydro/">Hydro&lt;/a> 从一个简单问题出发：能不能让 hyperparameter tuning 少一点 brute force，多一点 systems thinking？&lt;/p>
&lt;p>Hydro 有两面。在任务层面，它通过调优更小的 surrogate model 让每个 trial 变便宜。在集群层面，它问了一个更有意思的问题：数据中心能不能用当前被浪费的 GPU 时间来运行这些便宜 trial？&lt;/p>
&lt;h2 id="先让-trial-变小">先让 Trial 变小&lt;/h2>
&lt;p>Hydro 的第一步，是尽可能避免直接调优 target model。它缩小模型，调优小模型，再把找到的 hyperparameter 转移回原模型。风险在于，朴素缩小会改变训练动态。一个适合窄模型的 learning rate，可能在更宽的模型上完全失效，因此 cheap search 可能给出误导性答案。&lt;/p>
&lt;p>Hydro 通过 parametrization 让这个想法变得可行，具体来说，是对 maximal update parametrization 的系统化适配。它不只是改变 layer width，还会逐层调整 initialization 和 optimizer behavior，让不同宽度模型在训练中保持可比较的 update scale。更工程化地说，Hydro 希望 surrogate 和 target model 对“哪些 hyperparameter configuration 更好”这件事有一致判断。&lt;/p>
&lt;p>实现上，Hydro 采用服务化设计。Model Shrinker 用 &lt;code>torch.fx&lt;/code> trace PyTorch model，缩放符合条件的 layer，应用 parametrization rules，并在调优任务继续之前做轻量 correctness check。Trial Binder 则通过 grouped &lt;code>hydro.nn&lt;/code> module，把许多小 surrogate trial 融合成一个批量执行单元。这很重要，因为单个 surrogate trial 可能太小，无法喂饱一张 A100；fusion 把很多 tiny trial 变成了形状更适合 GPU 的工作负载。&lt;/p>
&lt;p>这些组件都重要，但这篇文章想重点看最有 datacenter 味道的部分：Bubble Squeezer。&lt;/p>
&lt;h2 id="集群也是调优系统的一部分">集群也是调优系统的一部分&lt;/h2>
&lt;p>大多数 tuning framework 把集群调度器当作资源售货机。Hydro 则把集群本身纳入优化空间。&lt;/p>
&lt;p>Hydro Coordinator 加入了这种集群视角。它最有辨识度的组件是 Bubble Squeezer，目标是长时间运行的 pipeline-parallel 训练任务。Pipeline parallelism 常用于大模型，因为模型会被切成多个 stage，放到多张 GPU 或多个节点上。在常见的 1F1B schedule 中，每个 worker 交替执行 forward 和 backward microbatch，但 schedule 并不是完全致密的。某个 stage 可能完成了一个 microbatch 的 forward pass，然后等待另一个 stage 产生对应的 backward work。这个等待区间就是 pipeline bubble，也就是这里说的流水线空泡。&lt;/p>
&lt;p>对大型训练任务来说，bubble 很尴尬。它们很短，反复出现，并且和通信交织在一起。在 bubble 期间，唯一活跃的 kernel 可能只是 NCCL communication，所以即使 GPU 名义上已经分配给训练任务，SM activity 也可能非常低。对普通训练任务来说，这点空间不足以安全运行新任务；但对 Hydro 来说，这是一个入口。&lt;/p>
&lt;p>HydroTrial 特别适合放进 bubble 里运行，原因有三点。第一，它们对吞吐波动不敏感：某个 candidate trial 慢一点没关系，只要整个调优任务在前进。第二，它们经过 profiling：Hydro 在把 trial 放到大模型旁边之前，已经知道每个 fused trial 的 memory 和 compute footprint。第三，它们是 elastic 的：fusion count 可以调整，让 trial bundle 适配某个 bubble 剩余的 memory 和 time budget。&lt;/p>
&lt;h2 id="bubble-squeezer-如何挤出空泡">Bubble Squeezer 如何挤出空泡&lt;/h2>
&lt;p>Bubble Squeezer 把 pipeline bubble 变成短暂可用的资源。当一个 pipeline-parallel 大模型训练任务正在运行时，Hydro 会和数据中心调度器协调，获取这些临时机会，并把相应 GPU 标记为只有在 bubble 期间可用。目标很窄也很明确：在不拖慢主训练任务的前提下运行调优工作。&lt;/p>
&lt;p>控制循环分成两边。&lt;/p>
&lt;p>在大模型侧，Hydro 修改了基于 DeepSpeed 的执行路径，用来报告 pipeline progress 和 resource consumption。这告诉 Hydro 某个 worker 什么时候进入 bubble，以及还有多少 memory 可用。Hydro 还会观察 NCCL kernel 的 CUDA stream 状态，以区分 communication-heavy waiting time 和 compute time。这个区分非常关键：直接 colocation 会让两个工作负载盲目竞争，论文报告 direct colocation 会给大模型带来约 12% slowdown。&lt;/p>
&lt;p>在 tuning 侧，Hydro 已经把 surrogate model 规范化成 &lt;code>hydro.nn&lt;/code> module。Bubble Squeezer 在这些 module 上注册 hook，让 HydroTrial 可以以细粒度 pause 和 resume，包括 forward 和 backward pass 内部。在 bubble 开始时，Hydro resume 一组 fused surrogate trial；在 bubble 结束时，在大模型重新需要 GPU 前把它们 pause。实现上使用 Linux signal 做 pause/resume 控制，调度决策则由 profiled trial footprint 和当前可用 memory 共同决定。&lt;/p>
&lt;p>
&lt;figure >
&lt;div class="d-flex justify-content-center">
&lt;div class="w-100" >&lt;img alt="Bubble Squeezer 将 HydroTrial 插入流水线并行训练的空闲区间。" srcset="
/zh/post/hydro/interleaved_pp_hu6e9fefa0070975ce8c017300ac4eba55_220216_5a468d91b6980b7f8873d18eae1338ce.png 400w,
/zh/post/hydro/interleaved_pp_hu6e9fefa0070975ce8c017300ac4eba55_220216_26fcb76d8f3d3a25cb965e3fc401fff8.png 760w,
/zh/post/hydro/interleaved_pp_hu6e9fefa0070975ce8c017300ac4eba55_220216_1200x1200_fit_lanczos_3.png 1200w"
src="https://yezhisheng.me/zh/post/hydro/interleaved_pp_hu6e9fefa0070975ce8c017300ac4eba55_220216_5a468d91b6980b7f8873d18eae1338ce.png"
width="760"
height="678"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;/figure>
&lt;/p>
&lt;p>Fusion count 不是固定的。如果某个 pipeline stage 有更多 spare memory，Hydro 可以运行更大的 fused HydroTrial；如果某个 stage 更紧张，就降低 fusion count 或跳过这个 bubble。这正是 Hydro 任务层和集群层两部分之间的小而关键的连接：surrogate scaling 让每个 trial 变小，trial fusion 塑造工作负载，Bubble Squeezer 决定这种工作负载能在某个具体 bubble 里放进去多少。&lt;/p>
&lt;p>理想场景是长时间运行的 pipeline-parallel foundation-model 训练任务，它跨多个 stage 和多台服务器。Stage 越多，通常意味着 bubble 越多，也意味着临时执行机会越多。Multi-fidelity tuning 也很适合这种模式，因为许多不 promising 的 trial 可以用 bubble resource 先推进或淘汰，最强的 trial 再获得 exclusive resource。&lt;/p>
&lt;h2 id="空泡能换来什么">空泡能换来什么&lt;/h2>
&lt;p>评估给出了一个很直观的尺度。Hydro 将 ResNet-18 HydroTrial 插入到一个运行在 32 张 A100、4 个 pipeline stage 上的大型 GPT 训练任务中。在原始 GPT training trace 里，bubble 内 SM activity 大约只有 2%。使用 Bubble Squeezer 后，Hydro 把 bubble 期间的 SM utilization 提升到约 50%，同时没有观察到对 GPT 任务的明显 slowdown。&lt;/p>
&lt;p>
&lt;figure >
&lt;div class="d-flex justify-content-center">
&lt;div class="w-100" >&lt;img alt="插入 HydroTrial 后，bubble 内 GPU 活动显著增加，同时大模型训练时间线保持稳定。" srcset="
/zh/post/hydro/interleaved_perf_hu47cb388a726aabddff78cd36644fa70f_150041_e424e404c79e7e0efb666dca8b197915.png 400w,
/zh/post/hydro/interleaved_perf_hu47cb388a726aabddff78cd36644fa70f_150041_b5deec769b48c93bf3fa88d7cbafbbea.png 760w,
/zh/post/hydro/interleaved_perf_hu47cb388a726aabddff78cd36644fa70f_150041_1200x1200_fit_lanczos_3.png 1200w"
src="https://yezhisheng.me/zh/post/hydro/interleaved_perf_hu47cb388a726aabddff78cd36644fa70f_150041_e424e404c79e7e0efb666dca8b197915.png"
width="760"
height="422"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;/figure>
&lt;/p>
&lt;p>这些调优工作不会像独占 GPU 时那样快，这是预期内的。在实验中，一个 fusion count 为 16 的 HydroTrial 在 bubble 中获得了大约 15% 的 exclusive throughput。但这部分资源基本来自原本闲置的时间片。在一个端到端模拟场景中，当调优任务只有 1 张独占 GPU、而大模型占据大部分集群时，Bubble Squeezer 将 tuning makespan 降低了 2.7x。&lt;/p>
&lt;p>这是 Hydro 最有意思的地方。调度器通常会把已经分配给大训练任务的 GPU 看作不可用。Bubble Squeezer 则往这个 allocation 内部看，找到可重复、边界清晰、低干扰的窗口，让小型、已 profile、可 pause 的工作向前推进。&lt;/p>
&lt;p>完整 Hydro 系统仍然很重要：surrogate scaling 让 trial 变便宜，fusion 把 trial 组合成高效 bundle，Bubble Squeezer 再把这些 bundle 放进 pipeline bubble。它们合起来，把 HPO 从 brute-force outer loop 变成了 datacenter-aware service。&lt;/p>
&lt;p>当然也有限制。Parametrization 最适合控制 initialization 和 training dynamics 的 hyperparameter，比如 learning rate、batch size、learning-rate scheduler 和 momentum。Dropout、weight decay 这类 regularization 相关选择更难，因为它们更直接依赖模型和数据规模。一些 architecture 也可能需要定制分析。Hydro 并不声称所有 hyperparameter 都可以在所有模型之间转移。&lt;/p>
&lt;p>但核心经验是持久的：一旦模型训练进入数据中心规模，超参数搜索就必须同时理解模型、运行时和集群。Hydro 是让这整个 stack 可见的一次尝试。&lt;/p>
&lt;p>Paper: &lt;a href="https://yezhisheng.me/publication/hydro/">Hydro: Surrogate-Based Hyperparameter Tuning Service in Datacenters&lt;/a>&lt;br>
Code: &lt;a href="https://github.com/S-Lab-System-Group/Hydro" target="_blank" rel="noopener">S-Lab-System-Group/Hydro&lt;/a>&lt;/p></description></item><item><title>本站优化与服务</title><link>https://yezhisheng.me/zh/post/optimizations-and-services/</link><pubDate>Mon, 30 Aug 2021 12:10:58 +0800</pubDate><guid>https://yezhisheng.me/zh/post/optimizations-and-services/</guid><description>&lt;h2 id="本站做过的优化">本站做过的优化&lt;/h2>
&lt;h3 id="构建流程">构建流程&lt;/h3>
&lt;p>整个站点由 Hugo 生成，本质上是一组静态页面。作为一个 Golang 用户，我很难完全客观地评价 Hugo 用 Go modules 管理主题这件事是否优雅，尤其是它还要求构建环境里有合适的 Hugo binary 和 Go 环境。不过至少从结果看，构建速度确实非常快。&lt;/p>
&lt;p>和很多 Hugo 站点类似，&lt;code>yezhisheng.me&lt;/code> 的构建流程由 GitHub Actions 在仓库 push 后触发，生成的静态内容再通过 CDN 分发给最终访问者。&lt;/p>
&lt;h3 id="cdn">CDN&lt;/h3>
&lt;ul>
&lt;li>&lt;a href="https://pages.cloudflare.com/" target="_blank" rel="noopener">Cloudflare Pages&lt;/a> 用于全球访问。&lt;/li>
&lt;li>&lt;a href="https://www.upyun.com" target="_blank" rel="noopener">Upyun&lt;/a> 用于服务 &lt;a href="https://yezhisheng.com.cn" target="_blank" rel="noopener">https://yezhisheng.com.cn&lt;/a>。&lt;/li>
&lt;/ul>
&lt;p>在比较过 &lt;a href="https://vercel.com/dashboard" target="_blank" rel="noopener">Vercel&lt;/a> 和 &lt;a href="https://www.cloudflare.com" target="_blank" rel="noopener">Cloudflare&lt;/a> 之后，我最终选择了 Cloudflare。主要原因是它对 IPv6 的支持比较完整，在中国大陆 CERNET 环境里的响应速度也更好。不过 Vercel 在构建流水线方面更灵活，对中国大陆其他 ISP 的质量也可能更稳定，同时还提供基于 LetsEncrypt 的自动 SSL 证书续期，对配置 CAA 记录也很方便。&lt;/p>
&lt;p>Cloudflare 对我来说比较熟悉，毕竟已经持续用了五年多。它的 CNAME 加速也很容易通过简单点击启用。另一个值得一提的点是，Cloudflare Pages 可以直接镜像 GitHub Pages 原始仓库里的 HTML 文件，这样回源开销基本可以摊薄到亚毫秒级别。&lt;/p>
&lt;p>中国大陆的情况稍微不同。无论是 OSS 还是带 HTTPS 的 CDN，主流云厂商几乎都很难长期免费提供，包括阿里云、腾讯云、七牛云，更不用说 AWS China 和 Azure China。最后我选择了又拍云，主要是因为它提供免费的 HTTPS CDN 和 HTTP/3 支持，代价是节点数量较少，也没有 HTTPS rewrite。源站则直接设置为 GitHub Pages。&lt;/p>
&lt;h3 id="主题相关优化">主题相关优化&lt;/h3>
&lt;ul>
&lt;li>&lt;a href="https://wowchemy.com/docs/guide/offline-site/" target="_blank" rel="noopener">静态资源本地化&lt;/a>&lt;/li>
&lt;li>&lt;a href="http://instantclick.io/" target="_blank" rel="noopener">InstantClick&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="当前服务">当前服务&lt;/h2>
&lt;p>得益于 Cloudflare 的开发体验和易用性，一些小服务逐渐以几乎零额外成本的方式跑了起来，背后主要依赖 Cloudflare CDN 和 Cloudflare Workers。&lt;/p>
&lt;h3 id="反向代理">反向代理&lt;/h3>
&lt;ul>
&lt;li>&lt;a href="https://ao3.yezhisheng.me/" target="_blank" rel="noopener">AO3&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://g.yezhisheng.me/" target="_blank" rel="noopener">Google search&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://proxy.yezhisheng.me/-----https://www.baidu.com/" target="_blank" rel="noopener">Proxy&lt;/a>&lt;/li>
&lt;/ul>
&lt;h3 id="其他">其他&lt;/h3>
&lt;ul>
&lt;li>&lt;a href="https://pkg.yezhisheng.me/" target="_blank" rel="noopener">Personal Go module&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://system-ddls.yezhisheng.me/" target="_blank" rel="noopener">System conference deadlines&lt;/a>&lt;/li>
&lt;/ul></description></item></channel></rss>