<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en"><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://xuan7zhang.github.io/feed.xml" rel="self" type="application/atom+xml"/><link href="https://xuan7zhang.github.io/" rel="alternate" type="text/html" hreflang="en"/><updated>2026-11-01T18:47:30+00:00</updated><id>https://xuan7zhang.github.io/feed.xml</id><subtitle>Personal website of Xuanqi Zhang, a Ph.D. student at UBC working on efficient machine learning, RL post-training, and vision-related research. </subtitle><entry><title type="html">On Policy Distillation 迁移到 Trainium2（Trn2/Neuron）—— Generate() 问题、排查与解决</title><link href="https://xuan7zhang.github.io/posts/2026/05/27/distill.html" rel="alternate" type="text/html" title="On Policy Distillation 迁移到 Trainium2（Trn2/Neuron）—— Generate() 问题、排查与解决"/><published>2026-05-27T00:00:00+00:00</published><updated>2026-05-27T00:00:00+00:00</updated><id>https://xuan7zhang.github.io/posts/2026/05/27/distill</id><content type="html" xml:base="https://xuan7zhang.github.io/posts/2026/05/27/distill.html"><![CDATA[<h2 id="bug-summary">Bug Summary</h2> <p>把原本在 4x L40S/CUDA 上运行的 OPSD OGLS-SD 训练脚本迁移到 AWS Trn2/Trainium2 后，问题的根源不是单个参数错位，而是 CUDA 训练栈（CUDA/NCCL/DeepSpeed/FlashAttention + Transformers 的 GPU 采样实现）与 Neuron/XLA 编译栈之间的不兼容。</p> <p>原脚本假定一套在 GPU 上可用且可编译的算子（例如全词表排序、<code class="language-plaintext highlighter-rouge">torch.isin</code>、top-p sampling、NCCL 通信等）。在 Trn2 上，这些要么不可用、要么会触发 Neuron 编译器产生不被支持的 HLO op，从而导致编译失败或运行时错误。</p> <h2 id="主要症状">主要症状</h2> <p>最大的坑：Transformers 的 <code class="language-plaintext highlighter-rouge">generate()</code> 在 Trn2 上触发不被支持的 <code class="language-plaintext highlighter-rouge">sort</code> 操作：</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Operation sort is not supported on trn2
%sort = ... sort(... vocab_size ...)
</code></pre></div></div> <p>一开始怀疑是 EOS stopping criteria 中的 <code class="language-plaintext highlighter-rouge">torch.isin</code>，随后又发现 top-p / sampling 的路径也会触发对全词表的排序。经过多次尝试，继续修补 <code class="language-plaintext highlighter-rouge">generate()</code> 不是稳定方案，最终选择在 Neuron 环境下完全绕过 <code class="language-plaintext highlighter-rouge">model.generate()</code>，改用更小且可编译的生成路径。</p> <h2 id="为什么-generate-会在-trn2-上炸">为什么 <code class="language-plaintext highlighter-rouge">generate()</code> 会在 Trn2 上炸</h2> <p><code class="language-plaintext highlighter-rouge">generate()</code> 最大的问题可以拆成一句话：<strong>它不是一个简单的 forward loop，而是一个很复杂的 decoding 系统；里面很多在 GPU 上很正常的操作，在 Trn2/Neuron/XLA 上会被编译成不支持的 HLO op。</strong></p> <p>很多人直觉上以为 generate 只是这样：</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">outputs</span> <span class="o">=</span> <span class="nf">model</span><span class="p">(</span><span class="n">input_ids</span><span class="p">)</span>
<span class="n">next_token</span> <span class="o">=</span> <span class="n">outputs</span><span class="p">.</span><span class="n">logits</span><span class="p">[:,</span> <span class="o">-</span><span class="mi">1</span><span class="p">,</span> <span class="p">:].</span><span class="nf">argmax</span><span class="p">()</span>
<span class="n">input_ids</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">cat</span><span class="p">([</span><span class="n">input_ids</span><span class="p">,</span> <span class="n">next_token</span><span class="p">])</span>
</code></pre></div></div> <p>也就是：</p> <div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>模型 forward → 取最后一个 token → 拼回去 → 重复
</code></pre></div></div> <p>如果只是 greedy decoding，确实差不多。但 Transformers 的 <code class="language-plaintext highlighter-rouge">generate()</code> 实际上要支持很多功能：</p> <div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>greedy search
sampling
top-k sampling
top-p sampling
temperature
beam search
EOS stopping
repetition penalty
bad words filtering
min length / max length
logits processor
logits warper
cache management
attention mask 更新
多 batch 不同长度处理
</code></pre></div></div> <p>所以它不是一个简单 loop，而是一个大而全的 generation framework。</p> <p>CUDA/GPU 之所以常常没事，是因为它更偏 eager-friendly，很多 PyTorch op 可以直接丢给 GPU kernel 跑。但 Trn2 走的是：</p> <div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>PyTorch code
→ torch-xla 捕获计算图
→ 转成 XLA HLO
→ neuronx-cc 编译
→ Trainium 执行
</code></pre></div></div> <p>也就是说，Trn2 更像是：<strong>你写的 PyTorch 逻辑必须能被编译器理解，并 lowering 成 Neuron 支持的算子。</strong> 如果中间出现一个 Neuron 不支持的 HLO，比如 <code class="language-plaintext highlighter-rouge">sort</code>、<code class="language-plaintext highlighter-rouge">isin</code>、某些 dynamic shape 操作、某些 int64 dot、某些复杂 scatter/gather、某些 multinomial/sampling 操作，整个编译就会失败。</p> <p>这也解释了为什么你看到的 <code class="language-plaintext highlighter-rouge">sort not supported on trn2</code> 这么典型。这个报错通常来自 top-p sampling。top-p 的逻辑通常是先对整个 vocab 的概率排序，再累加到阈值 <code class="language-plaintext highlighter-rouge">top_p</code> 为止，所以它天然依赖 <code class="language-plaintext highlighter-rouge">sort</code>：</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">logits</span> <span class="o">=</span> <span class="n">outputs</span><span class="p">.</span><span class="n">logits</span><span class="p">[:,</span> <span class="o">-</span><span class="mi">1</span><span class="p">,</span> <span class="p">:]</span>
<span class="n">probs</span> <span class="o">=</span> <span class="nf">softmax</span><span class="p">(</span><span class="n">logits</span><span class="p">)</span>

<span class="n">sorted_probs</span><span class="p">,</span> <span class="n">sorted_indices</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">sort</span><span class="p">(</span><span class="n">probs</span><span class="p">,</span> <span class="n">descending</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
<span class="n">cumulative_probs</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">cumsum</span><span class="p">(</span><span class="n">sorted_probs</span><span class="p">,</span> <span class="n">dim</span><span class="o">=-</span><span class="mi">1</span><span class="p">)</span>
<span class="n">remove_mask</span> <span class="o">=</span> <span class="n">cumulative_probs</span> <span class="o">&gt;</span> <span class="n">top_p</span>
</code></pre></div></div> <p>当 vocab 很大时，这就变成对整条词表维度做排序。GPU 上这很常见，但在 Trn2 上会被 lowering 成 XLA HLO 的 <code class="language-plaintext highlighter-rouge">sort</code>，然后 Neuron 编译器直接报不支持。</p> <p>一开始怀疑 <code class="language-plaintext highlighter-rouge">torch.isin</code> 也合理，因为 Transformers <code class="language-plaintext highlighter-rouge">generate()</code> 还要处理 EOS stopping：</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="n">next_token</span> <span class="ow">in</span> <span class="n">eos_token_ids</span><span class="p">:</span>
   <span class="n">stop</span>
</code></pre></div></div> <p>如果 EOS token 有多个，内部可能会用类似 <code class="language-plaintext highlighter-rouge">torch.isin(next_tokens, eos_token_ids)</code> 的 tensor membership check。这个在 GPU 上也很普通，但在 XLA/Neuron 里也可能展开成不太友好的 graph。只是后来你已经确认更大的雷是 sampling / top-p / logits warper 路径，所以继续 patch <code class="language-plaintext highlighter-rouge">generate()</code> 基本是在打地鼠。</p> <p>这也是为什么 <code class="language-plaintext highlighter-rouge">do_sample=True</code> 很危险。它会把 decoding 推进到 sampling path：</p> <div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>logits → temperature scaling → logits processors → logits warpers → top-k / top-p filtering → softmax → multinomial sample → append next token
</code></pre></div></div> <p>里面最危险的点就是 top-p 依赖全词表 sort，<code class="language-plaintext highlighter-rouge">multinomial</code> 自身也可能不好编译，EOS stopping / logits warper 也可能触发复杂 mask / scatter / dynamic checks。所以在 Trn2 上，先把 <code class="language-plaintext highlighter-rouge">OPSD_GENERATION_DO_SAMPLE=0</code>、<code class="language-plaintext highlighter-rouge">TOP_P=1.0</code> 这些参数压住，是为了尽量避开复杂路径；但最终最稳的做法仍然是直接绕开 <code class="language-plaintext highlighter-rouge">generate()</code>。</p> <h2 id="为什么自定义-greedy-decode-更稳">为什么自定义 greedy decode 更稳</h2> <p>自定义版本只做：</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">for</span> <span class="n">_</span> <span class="ow">in</span> <span class="nf">range</span><span class="p">(</span><span class="n">max_new_tokens</span><span class="p">):</span>
   <span class="n">outputs</span> <span class="o">=</span> <span class="nf">model</span><span class="p">(</span>
      <span class="n">input_ids</span><span class="o">=</span><span class="n">next_input_ids</span><span class="p">,</span>
      <span class="n">attention_mask</span><span class="o">=</span><span class="n">current_attention_mask</span><span class="p">,</span>
      <span class="n">past_key_values</span><span class="o">=</span><span class="n">past_key_values</span><span class="p">,</span>
      <span class="n">use_cache</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span>
      <span class="n">return_dict</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span>
   <span class="p">)</span>
   <span class="n">next_token</span> <span class="o">=</span> <span class="n">outputs</span><span class="p">.</span><span class="n">logits</span><span class="p">[:,</span> <span class="o">-</span><span class="mi">1</span><span class="p">,</span> <span class="p">:].</span><span class="nf">argmax</span><span class="p">(</span><span class="n">dim</span><span class="o">=-</span><span class="mi">1</span><span class="p">,</span> <span class="n">keepdim</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
   <span class="n">generated</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">cat</span><span class="p">([</span><span class="n">generated</span><span class="p">,</span> <span class="n">next_token</span><span class="p">],</span> <span class="n">dim</span><span class="o">=</span><span class="mi">1</span><span class="p">)</span>
   <span class="n">current_attention_mask</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">cat</span><span class="p">(</span>
      <span class="p">[</span><span class="n">current_attention_mask</span><span class="p">,</span> <span class="n">torch</span><span class="p">.</span><span class="nf">ones_like</span><span class="p">(</span><span class="n">next_token</span><span class="p">)],</span> <span class="n">dim</span><span class="o">=</span><span class="mi">1</span>
   <span class="p">)</span>
   <span class="n">next_input_ids</span> <span class="o">=</span> <span class="n">next_token</span>
   <span class="n">past_key_values</span> <span class="o">=</span> <span class="n">outputs</span><span class="p">.</span><span class="n">past_key_values</span>
</code></pre></div></div> <p>这个路径很干净，只有 <code class="language-plaintext highlighter-rouge">forward</code>、<code class="language-plaintext highlighter-rouge">argmax</code>、<code class="language-plaintext highlighter-rouge">cat</code>、<code class="language-plaintext highlighter-rouge">attention_mask</code> 更新和 KV cache 更新，没有 <code class="language-plaintext highlighter-rouge">sort</code>、<code class="language-plaintext highlighter-rouge">top-p</code>、<code class="language-plaintext highlighter-rouge">multinomial</code>、beam search、复杂 EOS stopping、复杂 logits processor，所以更容易通过 Neuron 编译。这就是为什么最终解决方案不是继续 patch <code class="language-plaintext highlighter-rouge">generate()</code>，而是<strong>完全绕开 Transformers 的 <code class="language-plaintext highlighter-rouge">generate()</code></strong>。</p> <p>对 OPSD 来说，这一点尤其关键。普通 SFT 只需要 <code class="language-plaintext highlighter-rouge">forward/backward</code>，但 OPSD / OPD / GRPO / on-policy distillation 这类训练，核心还包括 rollout generation：</p> <div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>student 先生成 rollout
→ teacher/student 对 rollout 算 logprob 或 KL
→ 再训练 student
</code></pre></div></div> <p>也就是说，生成阶段不是一个可有可无的 eval，而是训练主路径的一部分。CUDA 上它只是普通推理步骤，但在 Trn2 上，generation 也必须能被 XLA/Neuron 编译。因此更准确地说：<strong>SFT forward/backward 能跑，不代表 OPSD 的 rollout generation 能跑。</strong></p> <p>所以这个 bug 的关键不是“<code class="language-plaintext highlighter-rouge">generate()</code> 参数没设对”，而是：<strong>Transformers 的 <code class="language-plaintext highlighter-rouge">model.generate()</code> 不是一个简单 forward loop，而是一个很复杂的 decoding 系统；它里面很多 GPU 上很正常的操作，在 Trn2/Neuron/XLA 上会被编译成不支持的 HLO op。</strong> 你遇到的 <code class="language-plaintext highlighter-rouge">sort not supported on trn2</code> 就是最典型的例子。</p> <h2 id="最终解决方案核心">最终解决方案（核心）</h2> <p>在 Neuron/Trn2 上不使用 Transformers 的 <code class="language-plaintext highlighter-rouge">model.generate()</code>，改用自定义的 greedy decode（仅使用 <code class="language-plaintext highlighter-rouge">argmax</code>，避免排序、top-p、采样等）：</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">for</span> <span class="n">_</span> <span class="ow">in</span> <span class="nf">range</span><span class="p">(</span><span class="n">max_new_tokens</span><span class="p">):</span>
   <span class="n">outputs</span> <span class="o">=</span> <span class="nf">model</span><span class="p">(</span>
      <span class="n">input_ids</span><span class="o">=</span><span class="n">next_input_ids</span><span class="p">,</span>
      <span class="n">attention_mask</span><span class="o">=</span><span class="n">current_attention_mask</span><span class="p">,</span>
      <span class="n">past_key_values</span><span class="o">=</span><span class="n">past_key_values</span><span class="p">,</span>
      <span class="n">use_cache</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span>
      <span class="n">return_dict</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span>
   <span class="p">)</span>
   <span class="n">next_token</span> <span class="o">=</span> <span class="n">outputs</span><span class="p">.</span><span class="n">logits</span><span class="p">[:,</span> <span class="o">-</span><span class="mi">1</span><span class="p">,</span> <span class="p">:].</span><span class="nf">argmax</span><span class="p">(</span><span class="n">dim</span><span class="o">=-</span><span class="mi">1</span><span class="p">,</span> <span class="n">keepdim</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
   <span class="n">generated</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">cat</span><span class="p">([</span><span class="n">generated</span><span class="p">,</span> <span class="n">next_token</span><span class="p">],</span> <span class="n">dim</span><span class="o">=</span><span class="mi">1</span><span class="p">)</span>
   <span class="n">current_attention_mask</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">cat</span><span class="p">(</span>
      <span class="p">[</span><span class="n">current_attention_mask</span><span class="p">,</span> <span class="n">torch</span><span class="p">.</span><span class="nf">ones_like</span><span class="p">(</span><span class="n">next_token</span><span class="p">)],</span> <span class="n">dim</span><span class="o">=</span><span class="mi">1</span>
   <span class="p">)</span>
   <span class="n">next_input_ids</span> <span class="o">=</span> <span class="n">next_token</span>
   <span class="n">past_key_values</span> <span class="o">=</span> <span class="n">outputs</span><span class="p">.</span><span class="n">past_key_values</span>
</code></pre></div></div> <p>通过环境变量控制行为：</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">export </span><span class="nv">PJRT_DEVICE</span><span class="o">=</span>NEURON
<span class="nb">export </span><span class="nv">OPSD_NEURON_CUSTOM_GREEDY</span><span class="o">=</span>1
<span class="nb">export </span><span class="nv">OPSD_GENERATION_DO_SAMPLE</span><span class="o">=</span>0
<span class="nb">export </span><span class="nv">TOP_P</span><span class="o">=</span>1.0
</code></pre></div></div> <p>在该模式下，生成阶段只走最小可编译路径（greedy + argmax），避免触发 Neuron 编译器不支持的 HLO。</p> <h2 id="takeaway迁移经验">Takeaway（迁移经验）</h2> <p>Trn2/Trainium 的迁移不是把 CUDA 改成 Neuron 就完成。训练的前向/反向（forward/backward）路径虽然多数情况下能通过编译，但在 on-policy RL、self-distillation 或任何在训练中调用模型生成（<code class="language-plaintext highlighter-rouge">generate()</code>）的任务里，生成逻辑里常见的 GPU 优化（如全词表排序、top-p、sampling warper、<code class="language-plaintext highlighter-rouge">torch.isin</code>）极易踩到 XLA/Neuron 的编译限制。</p> <p>稳妥的迁移策略：</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>先用最小可编译的 generation path 跑通（greedy + argmax + no top-p + no sampling），
再逐步恢复复杂采样功能并验证每一步是否可编译。
</code></pre></div></div> <h2 id="项目侧实际改动摘要">项目侧实际改动（摘要）</h2> <ul> <li>新增 <code class="language-plaintext highlighter-rouge">accelerate_trn2_ogls.yaml</code></li> <li>新增 <code class="language-plaintext highlighter-rouge">requirements_trn2.txt</code></li> <li>launcher 改为通过 Neuron/XLA 的环境变量启动</li> <li>将 <code class="language-plaintext highlighter-rouge">opsd_train.py</code> 改为包含 <code class="language-plaintext highlighter-rouge">main()</code> 并在 Neuron 下默认使用 <code class="language-plaintext highlighter-rouge">sdpa</code></li> <li><code class="language-plaintext highlighter-rouge">opsd_trainer.py</code> 在 Neuron 下绕开 <code class="language-plaintext highlighter-rouge">transformers.generate()</code>，使用自定义 greedy decode</li> <li>WandB 支持 online/offline 的显式控制（环境变量控制）</li> </ul> <hr/>]]></content><author><name></name></author><category term="posts"/><category term="migration"/><category term="trn2"/><category term="neuron"/><category term="trainium"/><category term="deep-learning"/><category term="distill"/><summary type="html"><![CDATA[将基于 CUDA 的 OPSD OGLS-SD 训练迁移到 AWS Trn2/Trainium2 时遇到的主要问题、排查过程与最终解决方案（distill 风格）。]]></summary></entry></feed>