<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Training Schedule on Echo的技术博客</title><link>https://cybersecurityerial.github.io/echo_blog/series/training-schedule/</link><description>Recent content in Training Schedule on Echo的技术博客</description><generator>Hugo</generator><language>zh-cn</language><lastBuildDate>Mon, 08 Jun 2026 00:00:00 +0800</lastBuildDate><atom:link href="https://cybersecurityerial.github.io/echo_blog/series/training-schedule/index.xml" rel="self" type="application/rss+xml"/><item><title>LLM System: Training Schedule 01 - 训练框架中的 Schedule 算法</title><link>https://cybersecurityerial.github.io/echo_blog/posts/llm-system-schedule-01-training-framework-schedule/</link><pubDate>Mon, 08 Jun 2026 00:00:00 +0800</pubDate><guid>https://cybersecurityerial.github.io/echo_blog/posts/llm-system-schedule-01-training-framework-schedule/</guid><description>&lt;blockquote&gt;
&lt;p&gt;本篇目标：&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="问题背景"&gt;问题背景&lt;/h2&gt;
&lt;p&gt;什么是schedule，这个词含义很广但是在训练框架这里一般考虑的是f和b任务之间的编排。&lt;/p&gt;
&lt;h2 id="pp"&gt;PP&lt;/h2&gt;
&lt;p&gt;pp开几一般就是把所有layer除以几，然后每个就是一个stage的layer数量。一般按照layer切。&lt;/p&gt;
&lt;h2 id="gpipe"&gt;GPipe&lt;/h2&gt;
&lt;p&gt;有很多mb，每个mb要做很多stage（模型的layer或op，跨卡或跨机，这些都行），GPipe就是要等到所有的mb都做完他们自己的所有前向stage，然后开始反向stage。气泡比较多，此外因为前向和反向的layer是反过来的，所以对于一个mb来说，他做的这些stage里面做前向越早的那个stage，做反向越晚。也就是inflight越多。inflight越多就代表得保存中间的状态，占显存。所以2个肉眼可见的缺点一个是空泡另一个是inflight。&lt;/p&gt;
&lt;p&gt;&lt;a href="https://ui.perfetto.dev/#!/?url=https://CyberSecurityErial.github.io/echo_blog/traces/gpipe_trace.json"&gt;在 Perfetto 中打开 GPipe trace&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;如何实现一个GPipe呢？（底层组件假设已经分好了，我们只需要考虑怎么把任务排好发出来，底层组件的事情可以见后文如何实现一个调度器）
非常的简单，给每个stage执行的载体（GPU）从mb0下发到mbn就可以了。然后执行那个stage对应的layer的前向传播/反向传播。&lt;/p&gt;
&lt;h2 id="1f1b"&gt;1F1B&lt;/h2&gt;
&lt;p&gt;做F的预取，然后让F和B同时进行。中间的卡交替进行f和b。好处是inflight少，但是空泡不减。&lt;/p&gt;
&lt;p&gt;&lt;a href="https://ui.perfetto.dev/#!/?url=https://CyberSecurityErial.github.io/echo_blog/traces/1f1b_trace.json"&gt;在 Perfetto 中打开 1F1B trace&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;如何实现一个1F1B呢？也并非很难，假设我们的stage执行载体（GPU，虽然总是括号里写GPU但是某些场景不一定是GPU，目前为了便于理解先这么写）
有m个，那只需要给stage编号（这个编号代表第一次启动任务的顺序）为i的stage提前分配m-i+1个mb就行了，mb的序号是从0到m-i。
然后这样预填充完之后，只需要做简单的配对+交叉即可。因为是1f1b，所以只需要交替下发f和b任务，f和b任务对应的mb编号只需要匹配最近一次任务即可，如果是b则找最旧的未完成mb任务id让fb闭合，如果是f则找最新的未完成mb任务id+1。&lt;/p&gt;
&lt;h2 id="interleaved-1f1b"&gt;interleaved 1F1B&lt;/h2&gt;
&lt;p&gt;也叫vpp，把一个stage再划分为几个虚拟stage，用interleave的形式排到几张卡上。这个场景为什么能减少bubble在我第一次理解的时候其实不是很直观，因为我思考的是，就算切细了那三角形的空泡依然存在，为什么空泡会少。所以就计算了一下size。只算开始部分的三角形空泡（结束时候是对称的就不管了）不计算的很细的话，我们看三角形空泡里面最长的部分，也就是最底下的那条，长度正比于每个f/b的时间*（pp-1），但这里要注意一个很容易想当然的问题，这里的pp是物理pp数，也就是真实的stage，而不是虚拟的stage。因为我们真实的stage数量一般和gpu数量一样，所以就算很多虚拟stage，一次填充到流水线的阶段也最多只有物理个gpu数。那切完以后f/b的t就变小了，显然空泡就小了。&lt;/p&gt;
&lt;p&gt;这么解释不太直观，，最直观的其实是，让最底下的那个rank早启动。假设就是rank0到7，rank7得等好几个阶段才能启动，那就把阶段切细，然后启动的就快了。但是如果切得太细，跨rank（其实是stage）通信不能忽略，那就也不行。&lt;/p&gt;
&lt;p&gt;然后写这个还想一个问题就是stage到底跨卡还是跨机还是跨什么东西，问了下ai说具体情况具体分析（等于没说）然后翻了下之前（未发布）的训练框架学习笔记，原则上stage没有跨什么东西的限制，但是在机内有高速互联的情况下一般是跨节点的。因为高速互联要留给tp。优先级tp&amp;gt;dp&amp;gt;pp因为我们假设tp每一层都开一次，那么tp的通信量是&lt;/p&gt;
$$
seq\_len \times batch\_size \times layer \times hidden
$$&lt;p&gt;dp没有layer这个维度肯定要少点。pp一般都可以overlap了。如果节点内没有高速互联是需要开pp的。&lt;/p&gt;
&lt;h2 id="chimera"&gt;Chimera&lt;/h2&gt;
&lt;p&gt;最接近dualpipe的办法。&lt;/p&gt;
&lt;p&gt;初始流水线：s0f-s1f-s2f-s3f-s3b-s2b-s1b-s0b。&lt;/p&gt;
&lt;p&gt;Chimera主要减少了bubble，前面二者有bubble都是因为GPU来任务的时间难免有pipeline式的三角形空泡问题。但是三角形空泡来源一个先入为主的假设就是我们总假设只能gpu0开始做mb0stage0。如果让其他gpu也同时开始一个任务，三角形空洞就能补上很多。（拓展，Chimera只是同时走两段pipe，能不能更多的pipe，收益如何）&lt;/p&gt;
&lt;p&gt;其实就是排两个交叉的流水线。依然要vpp把stage加倍。&lt;/p&gt;
&lt;p&gt;假设原先4stage，vpp成8个。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;s0 0 7
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;s1 1 6
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;s2 2 5
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;s3 3 4
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;就大概这样的。&lt;/p&gt;
&lt;p&gt;如果纯做vpp的话是这样的：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;s0 0 4
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;s1 1 5
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;s2 2 6
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;s3 3 7
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;那为什么vpp的效果不如Chimera呢。可以观察一件事情，stage越“在时间上靠前”被下发做f的，在做b收口的时候越晚，占用的显存就越大。所以有一个直观的结论，不同stage的显存开销在时间上是不均匀的。最影响显存开销的就是f的第一stage-b的最后一个stage这一对。这一对fb启动最早释放最晚，所以我们如果多同时启动几个这样的f，就可以让显存开销在时间上更均匀，进而降低了显存需求量的峰值。我们做vpp的话很难让不同stage的显存分配量是均匀的，甚至还有可能让inflight叠加。&lt;/p&gt;</description></item></channel></rss>