<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Junyi's Lab</title><link>https://www.junyi.dev/</link><description>Recent blog posts on Junyi's Lab</description><generator>Hugo (https://gohugo.io)</generator><language>zh-cn</language><managingEditor>junyi.h@comp.nus.edu.sg (Junyi Hou)</managingEditor><webMaster>junyi.h@comp.nus.edu.sg (Junyi Hou)</webMaster><lastBuildDate>Thu, 15 Oct 2026 00:00:00 +0800</lastBuildDate><atom:link href="https://www.junyi.dev/posts/index.xml" rel="self" type="application/rss+xml"/><item><title>deciax：用 JAX 和 Pallas 在 TPU 上运行 Decision Model</title><link>https://www.junyi.dev/posts/deciax/</link><pubDate>Thu, 15 Oct 2026 00:00:00 +0800</pubDate><author>junyi.h@comp.nus.edu.sg (Junyi Hou)</author><description>
Laya 是一个 Decision Model：用一次前向计算处理选择、评分和是非问题。我把它的推理路径移植到了 JAX，做成了 deciax，让这类模型可以在 TPU 上运行，再用 Pallas 优化其中的计算。
这次测试里1，单芯片 TPU v6e 的英文模型单问题调用 p50 是 2.18 ms，多语言模型是 1.55 ms。作为对比，A100 SXM4、H100 PCIe、H200 NVL 的最快实测结果分别是 6.4 / 4.0 ms、4.8 / 3.2 ms、5.8 / 4.1 ms（英文 / 多语言）。
每次 50 个问题时，v6e Pallas 的 p50 是 33.69 / 17.83 ms，相较普通 JAX 实现提速约 2.3 倍；A100、H100、H200 的最快实测结果分别是 68.8 / 36.2 ms、44.0 / 23.2 ms、39.8 / 21.2 ms。</description><content:encoded>&lt;p&gt;&lt;a href="https://github.com/NandhaKishorM/laya" target="_blank" rel="noopener noreferrer"&gt;Laya&lt;/a&gt; 是一个 Decision Model：用一次前向计算处理选择、评分和是非问题。我把它的推理路径移植到了 JAX，做成了 &lt;strong&gt;&lt;a href="https://pypi.org/project/deciax/" target="_blank" rel="noopener noreferrer"&gt;deciax&lt;/a&gt;&lt;/strong&gt;，让这类模型可以在 TPU 上运行，再用 Pallas 优化其中的计算。&lt;/p&gt;
&lt;p&gt;这次测试里&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt;，单芯片 TPU v6e 的英文模型单问题调用 p50 是 &lt;strong&gt;2.18 ms&lt;/strong&gt;，多语言模型是 &lt;strong&gt;1.55 ms&lt;/strong&gt;。作为对比，A100 SXM4、H100 PCIe、H200 NVL 的最快实测结果分别是 &lt;strong&gt;6.4 / 4.0 ms、4.8 / 3.2 ms、5.8 / 4.1 ms&lt;/strong&gt;（英文 / 多语言）。&lt;/p&gt;
&lt;p&gt;每次 50 个问题时，v6e Pallas 的 p50 是 &lt;strong&gt;33.69 / 17.83 ms&lt;/strong&gt;，相较普通 JAX 实现提速约 &lt;strong&gt;2.3 倍&lt;/strong&gt;；A100、H100、H200 的最快实测结果分别是 &lt;strong&gt;68.8 / 36.2 ms、44.0 / 23.2 ms、39.8 / 21.2 ms&lt;/strong&gt;。&lt;/p&gt;
&lt;h2 id="从一个例子开始" &gt;
&lt;div&gt;
&lt;a href="#%e4%bb%8e%e4%b8%80%e4%b8%aa%e4%be%8b%e5%ad%90%e5%bc%80%e5%a7%8b"&gt;
#
&lt;/a&gt;
从一个例子开始
&lt;/div&gt;
&lt;/h2&gt;
&lt;p&gt;deciax 接收输入和一组问题，返回结构化结果。以一条重复扣款的工单为例：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff6ac1"&gt;import&lt;/span&gt; deciax
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;decider &lt;span style="color:#ff6ac1"&gt;=&lt;/span&gt; deciax&lt;span style="color:#ff6ac1"&gt;.&lt;/span&gt;load(&lt;span style="color:#5af78e"&gt;&amp;#34;laya&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;result &lt;span style="color:#ff6ac1"&gt;=&lt;/span&gt; decider&lt;span style="color:#ff6ac1"&gt;.&lt;/span&gt;decide(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {&lt;span style="color:#5af78e"&gt;&amp;#34;user&amp;#34;&lt;/span&gt;: &lt;span style="color:#5af78e"&gt;&amp;#34;I was charged twice this month. Please refund the duplicate.&amp;#34;&lt;/span&gt;},
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#5af78e"&gt;&amp;#34;department&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#5af78e"&gt;&amp;#34;type&amp;#34;&lt;/span&gt;: &lt;span style="color:#5af78e"&gt;&amp;#34;choice&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#5af78e"&gt;&amp;#34;instructions&amp;#34;&lt;/span&gt;: &lt;span style="color:#5af78e"&gt;&amp;#34;Which department should handle this?&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#5af78e"&gt;&amp;#34;criteria&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#5af78e"&gt;&amp;#34;billing&amp;#34;&lt;/span&gt;: &lt;span style="color:#5af78e"&gt;&amp;#34;payments, invoices, refunds&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#5af78e"&gt;&amp;#34;technical&amp;#34;&lt;/span&gt;: &lt;span style="color:#5af78e"&gt;&amp;#34;bugs, outages, system errors&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#5af78e"&gt;&amp;#34;other&amp;#34;&lt;/span&gt;: &lt;span style="color:#5af78e"&gt;&amp;#34;everything else&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#5af78e"&gt;&amp;#34;urgency&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#5af78e"&gt;&amp;#34;type&amp;#34;&lt;/span&gt;: &lt;span style="color:#5af78e"&gt;&amp;#34;score&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#5af78e"&gt;&amp;#34;instructions&amp;#34;&lt;/span&gt;: &lt;span style="color:#5af78e"&gt;&amp;#34;How urgent is this?&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#5af78e"&gt;&amp;#34;criteria&amp;#34;&lt;/span&gt;: [&lt;span style="color:#5af78e"&gt;&amp;#34;not urgent&amp;#34;&lt;/span&gt;, &lt;span style="color:#5af78e"&gt;&amp;#34;soon&amp;#34;&lt;/span&gt;, &lt;span style="color:#5af78e"&gt;&amp;#34;blocking&amp;#34;&lt;/span&gt;],
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#5af78e"&gt;&amp;#34;refund_requested&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#5af78e"&gt;&amp;#34;type&amp;#34;&lt;/span&gt;: &lt;span style="color:#5af78e"&gt;&amp;#34;noul&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#5af78e"&gt;&amp;#34;instructions&amp;#34;&lt;/span&gt;: &lt;span style="color:#5af78e"&gt;&amp;#34;Does the user ask for a refund?&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff5c57"&gt;print&lt;/span&gt;(result)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="我们先用-jax-实现-laya" &gt;
&lt;div&gt;
&lt;a href="#%e6%88%91%e4%bb%ac%e5%85%88%e7%94%a8-jax-%e5%ae%9e%e7%8e%b0-laya"&gt;
#
&lt;/a&gt;
我们先用 JAX 实现 Laya
&lt;/div&gt;
&lt;/h2&gt;
&lt;p&gt;我们先用 JAX 重新实现 Laya 的推理计算：由 ModernBERT encoder 处理输入，再由决策 head 输出各个问题的结果。输入构造保持不变，checkpoint 和 tokenizer 也沿用原模型。这样就得到了一条可以在 TPU 上运行的普通 JAX 推理路径，作为下面性能测试和后续 Pallas 优化的基线。&lt;/p&gt;
&lt;figure&gt;
&lt;img src="https://www.junyi.dev/posts/deciax/figures/laya-architecture.svg" alt="Laya 模型结构：逐问题构造输入，经过 ModernBERT、Transformer head 层、选项评分器和 action head。" loading="lazy"&gt;
&lt;figcaption&gt;deciax 中的 Laya 推理流程。每个问题构成独立的一行输入，经过编码和 head 层后，得到选项分数与 action probability。&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h3 id="jax-实现的性能如何" &gt;
&lt;div&gt;
&lt;a href="#jax-%e5%ae%9e%e7%8e%b0%e7%9a%84%e6%80%a7%e8%83%bd%e5%a6%82%e4%bd%95"&gt;
##
&lt;/a&gt;
JAX 实现的性能如何？
&lt;/div&gt;
&lt;/h3&gt;
&lt;p&gt;测试在 Oct 12, 2026 进行，覆盖 RTX 3090、A100 SXM4、RTX A6000、H100 PCIe 和 H200 NVL，以及各用一颗芯片的 TPU v5e、v6e。&lt;/p&gt;
&lt;p&gt;延迟图展示&lt;strong&gt;进程内调用的稳态 p50&lt;/strong&gt;&lt;sup id="fnref:2"&gt;&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref"&gt;2&lt;/a&gt;&lt;/sup&gt;，包含输入编码、推理和结果处理。实心柱是 p50，虚线标记 p95。&lt;/p&gt;
&lt;p&gt;先只看 GPU 的 PyTorch eager 和 TPU 的普通 JAX 实现：&lt;/p&gt;
&lt;figure&gt;
&lt;img src="https://www.junyi.dev/posts/deciax/figures/baseline-latency.png" alt="未优化基线：PyTorch eager 与普通 JAX 在英文和多语言模型上每次 1 个及 50 个问题的 p50、p95 延迟" loading="lazy"&gt;
&lt;figcaption&gt;未优化基线，单位 ms，越低越好。实心柱表示 p50，虚线表示 p95，标注为 p50 / p95。星号表示共享主机。&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;英文模型单问题调用，GPU eager 的 p50 是 13.3–25.8 ms，v6e JAX 是 2.27 ms。但每次 50 个问题时，v6e JAX 需要 76.79 ms，GPU eager 的最低值是 H200 NVL 的 57.5 ms。多语言模型也有类似变化：v6e JAX 从单问题的 1.61 ms 增长到 50 问题的 41.24 ms，H200 eager 的 50 问题结果是 34.0 ms。&lt;/p&gt;
&lt;h3 id="耗时分解" &gt;
&lt;div&gt;
&lt;a href="#%e8%80%97%e6%97%b6%e5%88%86%e8%a7%a3"&gt;
##
&lt;/a&gt;
耗时分解
&lt;/div&gt;
&lt;/h3&gt;
&lt;p&gt;接着拆开基线调用的耗时，以英文模型为例&lt;sup id="fnref:3"&gt;&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref"&gt;3&lt;/a&gt;&lt;/sup&gt;：&lt;/p&gt;
&lt;figure&gt;
&lt;img src="https://www.junyi.dev/posts/deciax/figures/baseline-breakdown.png" alt="英文模型基线耗时拆解：单问题时调度与其余处理占比较大，50 问题时设备计算占比上升" loading="lazy"&gt;
&lt;figcaption&gt;蓝色为 profiler 记录的设备忙碌时间，斜线部分为总延迟减去设备时间的余量，标注是独立计时的 p50。两部分来自不同测量序列，是近似拆分；余量包含调度、Python、输入输出处理及设备空闲等开销，不是精确 CPU 耗时。&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;单问题的 GPU eager 调用包含大量小 kernel：英文模型一次调用就有 &lt;strong&gt;1090–1213 次 &lt;code&gt;cudaLaunchKernel&lt;/code&gt;&lt;/strong&gt;。设备执行之外的余量很大，优化需要同时考虑 kernel 融合和调用开销。&lt;/p&gt;
&lt;p&gt;每次 50 个问题时，计算本身开始占主要时间。v6e JAX 的英文模型总延迟为 76.79 ms，其中单独测量的 forward-only 是 71.27 ms，输入编码约 3 ms。XProf 中，&lt;code&gt;dot_general&lt;/code&gt; 和 &lt;code&gt;sub&lt;/code&gt; 分别占这组设备时间的约 38% 和 21%，提示我们关注矩阵计算和逐元素操作的数据搬运。&lt;/p&gt;
&lt;h2 id="如果让它更快呢" &gt;
&lt;div&gt;
&lt;a href="#%e5%a6%82%e6%9e%9c%e8%ae%a9%e5%ae%83%e6%9b%b4%e5%bf%ab%e5%91%a2"&gt;
#
&lt;/a&gt;
如果让它更快呢？
&lt;/div&gt;
&lt;/h2&gt;
&lt;p&gt;我们使用 Pallas 对 TPU 上的 Laya 进行 kernel fusion 优化。当序列长度不超过 512 token 时，每个 encoder 层和 Transformer head 层都由一个 kernel 完成，将归一化、投影、attention 和 MLP 融合起来，encoder 中还包括 RoPE&lt;sup id="fnref:4"&gt;&lt;a href="#fn:4" class="footnote-ref" role="doc-noteref"&gt;4&lt;/a&gt;&lt;/sup&gt;。中间结果留在片上 VMEM 中，减少反复读写 HBM；每个 kernel 的权重也驻留在 VMEM 中，供不同的行块复用。此外，我们调整了 attention 的矩阵布局，采用转置形式的计算来更好地适配 TPU 矩阵单元。&lt;a href="https://github.com/Junyi-99/deciax/pull/1" target="_blank" rel="noopener noreferrer"&gt;PR #1&lt;/a&gt; 记录了这些优化，以及长序列使用的三 kernel 路径。&lt;/p&gt;
&lt;p&gt;为了尽可能公平，我们也测试了使用 TileLang、CUDA Graph 和 &lt;code&gt;torch.compile&lt;/code&gt; 优化的 GPU 版 Laya。对于每台主机、每个模型和问题数，我们选取&lt;strong&gt;实测最快的实现&lt;/strong&gt;，与 TPU v6e 上的 Pallas 版本进行比较。&lt;/p&gt;
&lt;figure&gt;
&lt;img src="https://www.junyi.dev/posts/deciax/figures/latency-1q.png" alt="优化后的单问题调用：各 GPU 的实测最快实现与 v6e Pallas 对比，实心柱为 p50、虚线为 p95" loading="lazy"&gt;
&lt;figcaption&gt;每次一个问题，单位 ms，越低越好。实心柱表示 p50，虚线表示 p95，标注为 p50 / p95。星号表示共享主机。GPU 测试覆盖 TileLang、CUDA Graph 和 torch.compile，选择各卡实测最快的实现，以尽可能保证公平。&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;单问题调用，v6e Pallas 的英文模型 p50 是 &lt;strong&gt;2.18 ms&lt;/strong&gt;，多语言模型是 &lt;strong&gt;1.55 ms&lt;/strong&gt;。GPU 的英文模型最低值是 H100 PCIe 上 torch.compile with cudagraph 的 4.8 ms，多语言模型最低值是 3.2 ms。这个形状下，Pallas 相比普通 JAX 的改善很小。&lt;/p&gt;
&lt;figure&gt;
&lt;img src="https://www.junyi.dev/posts/deciax/figures/latency-50q.png" alt="优化后的 50 问题调用：各 GPU 的实测最快实现与 v6e Pallas 对比，英文模型 Pallas p50 为 33.69 ms、多语言为 17.83 ms" loading="lazy"&gt;
&lt;figcaption&gt;每次 50 个问题，实心柱为 p50，虚线为 p95，标注为 p50 / p95。所有 GPU 项的最快实现均为 torch.compile with cudagraph；v6e Pallas 的 p50 是 33.69 ms / 17.83 ms，最快 GPU 是 39.8 ms / 21.2 ms（英文 / 多语言）。&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;每次 50 个问题，v6e 的英文模型从 JAX 的 76.79 ms 降到 Pallas 的 &lt;strong&gt;33.69 ms&lt;/strong&gt;，多语言模型从 41.24 ms 降到 &lt;strong&gt;17.83 ms&lt;/strong&gt;，分别提速 &lt;strong&gt;2.28 倍、2.31 倍&lt;/strong&gt;。英文模型的 forward-only 从 71.27 ms 降到 28.25 ms，多语言模型从 36.54 ms 降到 13.36 ms。&lt;/p&gt;
&lt;h2 id="不同-batch-size-下的性能" &gt;
&lt;div&gt;
&lt;a href="#%e4%b8%8d%e5%90%8c-batch-size-%e4%b8%8b%e7%9a%84%e6%80%a7%e8%83%bd"&gt;
#
&lt;/a&gt;
不同 batch size 下的性能
&lt;/div&gt;
&lt;/h2&gt;
&lt;p&gt;Batch size 会影响硬件利用率，因为它决定了硬件能同时处理多少计算。小 batch 可能无法充分利用计算单元，调度等固定开销在单次调用中的占比也更高。增大 batch 可以提供更多并行工作，让权重在更多输入之间复用，并将固定开销分摊到更多决策上。但它也会增加计算量和内存需求，因此吞吐量提高的同时，单次调用的延迟也可能变长。在小 batch 下表现好的实现，换到大 batch 后不一定仍有同样的优势。&lt;/p&gt;
&lt;p&gt;Laya 会把每个问题构造成独立的一行输入，因此这里针对同一条输入，每次分别提交 1、5、10、50 个问题，改变逻辑 batch size。TPU 实现会将这些行补齐到支持的 batch 桶，所以问题数代表有效输入行数，不一定等于实际执行张量的 batch 维度。下图对比 H100、H200 与 v6e Pallas 在这些 batch size 下的单次调用延迟，观察处理更多决策时耗时如何增长：&lt;/p&gt;
&lt;figure&gt;
&lt;img src="https://www.junyi.dev/posts/deciax/figures/latency-curve.png" alt="延迟曲线：每次 1、5、10、50 个问题下，H100 PCIe、H200 NVL 与 v6e Pallas 的 p50" loading="lazy"&gt;
&lt;figcaption&gt;横轴为每次调用的问题数，纵轴为 p50 ms，两轴均为对数刻度。分别展示 H100 PCIe、H200 NVL 各问题数下实测最快的实现，以及 v6e Pallas。每条 GPU 曲线固定对应同一张卡。&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id="部署模型服务" &gt;
&lt;div&gt;
&lt;a href="#%e9%83%a8%e7%bd%b2%e6%a8%a1%e5%9e%8b%e6%9c%8d%e5%8a%a1"&gt;
#
&lt;/a&gt;
部署模型服务
&lt;/div&gt;
&lt;/h2&gt;
&lt;h3 id="http-服务" &gt;
&lt;div&gt;
&lt;a href="#http-%e6%9c%8d%e5%8a%a1"&gt;
##
&lt;/a&gt;
HTTP 服务
&lt;/div&gt;
&lt;/h3&gt;
&lt;p&gt;为了让其他应用调用 Laya，我们通过 &lt;code&gt;deciax.serve&lt;/code&gt; 将同样的决策接口暴露为 HTTP 服务：向 &lt;code&gt;POST /v1/systemone&lt;/code&gt; 发送输入和问题，服务以 JSON 返回结构化答案。模型在服务启动时加载，并在后续请求之间复用。&lt;/p&gt;
&lt;p&gt;在已安装 deciax 的 TPU 主机上，运行以下命令，使用 Pallas 实现在 8000 端口启动 HTTP server：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;python -m deciax.serve --model laya --impl pallas --port &lt;span style="color:#ff9f43"&gt;8000&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;启动和预热完成后，在另一个终端发送请求：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;curl http://127.0.0.1:8000/v1/systemone &lt;span style="color:#5af78e"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -H &lt;span style="color:#5af78e"&gt;&amp;#39;Content-Type: application/json&amp;#39;&lt;/span&gt; &lt;span style="color:#5af78e"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -d &lt;span style="color:#5af78e"&gt;&amp;#39;{
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#5af78e"&gt; &amp;#34;state&amp;#34;: &amp;#34;I was charged twice. Please refund the duplicate charge.&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#5af78e"&gt; &amp;#34;questions&amp;#34;: {
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#5af78e"&gt; &amp;#34;refund&amp;#34;: {
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#5af78e"&gt; &amp;#34;type&amp;#34;: &amp;#34;noul&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#5af78e"&gt; &amp;#34;instructions&amp;#34;: &amp;#34;Does the user ask for a refund?&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#5af78e"&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#5af78e"&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#5af78e"&gt; }&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;返回结果中的 &lt;code&gt;answers.refund.noul&lt;/code&gt; 表示答案为真的概率，同时还包含置信度、action probability 和 token 用量。服务也提供 &lt;code&gt;GET /health&lt;/code&gt; 健康检查和 &lt;code&gt;POST /v1/systemone/batch&lt;/code&gt; 批量接口。&lt;/p&gt;
&lt;p&gt;在 loopback、keep-alive 条件下，我们测得 v6e Pallas 的单问题 HTTP p50 为英文 &lt;strong&gt;3.0 ms&lt;/strong&gt;、多语言 &lt;strong&gt;2.4 ms&lt;/strong&gt;。&lt;/p&gt;
&lt;h3 id="costmillion-model-input-tokens-per-us-dollar" &gt;
&lt;div&gt;
&lt;a href="#costmillion-model-input-tokens-per-us-dollar"&gt;
##
&lt;/a&gt;
Cost：Million model input tokens per US dollar
&lt;/div&gt;
&lt;/h3&gt;
&lt;p&gt;成本只比较 &lt;strong&gt;English model 的优化版本&lt;/strong&gt;：GPU 取前面的实测最快实现，TPU 只放 v6e Pallas。分别按 on-demand 与 preemptible / spot 两种小时价格计算。&lt;/p&gt;
&lt;p&gt;性能数据和价格数据采集于 Oct 12, 2026。&lt;/p&gt;
&lt;p&gt;价格单位：&lt;strong&gt;USD / GPU-hour 或 TPU chip-hour&lt;/strong&gt;。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;硬件&lt;/th&gt;
&lt;th&gt;供应商&lt;/th&gt;
&lt;th style="text-align: right"&gt;On-demand&lt;/th&gt;
&lt;th style="text-align: right"&gt;Preemptible / Spot&lt;sup id="fnref:5"&gt;&lt;a href="#fn:5" class="footnote-ref" role="doc-noteref"&gt;5&lt;/a&gt;&lt;/sup&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A100 SXM4 80GB&lt;/td&gt;
&lt;td&gt;&lt;a href="https://vast.ai/pricing" target="_blank" rel="noopener noreferrer"&gt;Vast.ai&lt;/a&gt;&lt;/td&gt;
&lt;td style="text-align: right"&gt;$1.176&lt;/td&gt;
&lt;td style="text-align: right"&gt;$0.727&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;H100（SXM 报价）&lt;/td&gt;
&lt;td&gt;&lt;a href="https://nebius.com/prices" target="_blank" rel="noopener noreferrer"&gt;Nebius&lt;/a&gt;&lt;/td&gt;
&lt;td style="text-align: right"&gt;$4.500&lt;/td&gt;
&lt;td style="text-align: right"&gt;$0.790&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;H200（SXM 报价）&lt;/td&gt;
&lt;td&gt;&lt;a href="https://nebius.com/prices" target="_blank" rel="noopener noreferrer"&gt;Nebius&lt;/a&gt;&lt;/td&gt;
&lt;td style="text-align: right"&gt;$5.400&lt;/td&gt;
&lt;td style="text-align: right"&gt;$0.790&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TPU v6e&lt;/td&gt;
&lt;td&gt;&lt;a href="https://cloud.google.com/tpu/pricing" target="_blank" rel="noopener noreferrer"&gt;Google Cloud&lt;/a&gt;&lt;/td&gt;
&lt;td style="text-align: right"&gt;$2.700&lt;/td&gt;
&lt;td style="text-align: right"&gt;$0.970&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;计算公式是&lt;sup id="fnref:6"&gt;&lt;a href="#fn:6" class="footnote-ref" role="doc-noteref"&gt;6&lt;/a&gt;&lt;/sup&gt;：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-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;Million model input tokens per US dollar
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; = model input tokens per second × 3600 / hourly price / 1,000,000
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这里的 model input tokens 包含各问题重复的工单文本。&lt;/p&gt;
&lt;figure&gt;
&lt;img src="https://www.junyi.dev/posts/deciax/figures/cost-english.png" alt="English model 优化版本的 Million model input tokens per US dollar：A100、H100 与 TPU v6e Pallas，分别采用 on-demand 和 preemptible/spot 价格" loading="lazy"&gt;
&lt;figcaption&gt;每美元可处理的百万 model input tokens，越高越好。仅 English model，对比 A100、H100 的实测最快实现与 TPU v6e Pallas。上图每次一个问题，下图每次 50 个问题。价格、日期和供应商见上文；preemptible / spot 沿用报告的价格假设。&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id="总结" &gt;
&lt;div&gt;
&lt;a href="#%e6%80%bb%e7%bb%93"&gt;
#
&lt;/a&gt;
总结
&lt;/div&gt;
&lt;/h2&gt;
&lt;p&gt;deciax 把 Laya 的结构化决策能力带到了 TPU 上。这次实验中，普通 JAX 已经能把单问题调用做到约 2 ms，Pallas 在这个形状下的额外收益很小；当每次调用增加到 50 个问题，融合后的 Pallas 路径相较普通 JAX 提速约 &lt;strong&gt;2.3 倍&lt;/strong&gt;。问题数改变了主要开销，也改变了优化的收益。&lt;/p&gt;
&lt;p&gt;在本文测试的输入、模型和实现范围内，v6e Pallas 的单问题和 50 问题延迟都低于参与对比的 GPU 实现。&lt;/p&gt;
&lt;p&gt;如果你想试试 deciax，可以在 &lt;a href="https://colab.research.google.com/drive/1sY_4BeRq_em4tzePW3poPyl-lGXyxO1g?usp=sharing" target="_blank" rel="noopener noreferrer"&gt;Colab&lt;/a&gt; 中体验，也可以到 &lt;a href="https://github.com/Junyi-99/deciax" target="_blank" rel="noopener noreferrer"&gt;GitHub 仓库&lt;/a&gt;查看代码。&lt;/p&gt;
&lt;h2 id="致谢" &gt;
&lt;div&gt;
&lt;a href="#%e8%87%b4%e8%b0%a2"&gt;
#
&lt;/a&gt;
致谢
&lt;/div&gt;
&lt;/h2&gt;
&lt;p&gt;感谢 2026 TPU Builders Program 和 &lt;a href="https://sites.research.google/trc/about/" target="_blank" rel="noopener noreferrer"&gt;TPU Research Cloud（TRC）&lt;/a&gt; 为本文工作提供 TPU 计算资源。&lt;/p&gt;
&lt;div class="footnotes" role="doc-endnotes"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;本文性能数据来自开发版本 &lt;a href="https://github.com/Junyi-99/deciax/commit/6f3a930" target="_blank" rel="noopener noreferrer"&gt;6f3a930&lt;/a&gt;。&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:2"&gt;
&lt;p&gt;输入采用上游的 &lt;em&gt;Payout failing&lt;/em&gt; 工单，问题交替使用 &lt;code&gt;noul&lt;/code&gt; 和 &lt;code&gt;choice&lt;/code&gt;。每组先预热 20 次，再计时 20 次，并等待设备计算完成。&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:3"&gt;
&lt;p&gt;耗时拆解图中的 GPU p50 来自 profiling 配套的独立计时，与前一张基线图的测试序列不同。&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:4"&gt;
&lt;p&gt;本次报告中的 Pallas 路径融合了 norm、QKV 投影、RoPE、attention、输出投影和 MLP。对于序列长度不超过 512 的路径，每层使用一次 &lt;code&gt;pallas_call&lt;/code&gt;。&amp;#160;&lt;a href="#fnref:4" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:5"&gt;
&lt;p&gt;Nebius Spot 使用起始价；TPU preemptible 使用报告快照。&amp;#160;&lt;a href="#fnref:5" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:6"&gt;
&lt;p&gt;English model 每次单问题调用有 194 个 model input tokens，每次 50 问题有 9975 个。吞吐采用报告记录的 input tokens/s，由输入 token 数除以原始 p50 延迟得到。&amp;#160;&lt;a href="#fnref:6" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</content:encoded><category>TPU</category><category>JAX</category><category>Pallas</category><category>Inference</category><category>Tech</category><guid isPermaLink="true">https://www.junyi.dev/posts/deciax/</guid></item><item><title>HLO Visualizer：把 XLA HLO 变成看得懂的图</title><link>https://www.junyi.dev/posts/hlo-visualizer/</link><pubDate>Sat, 10 Oct 2026 14:00:00 +0800</pubDate><author>junyi.h@comp.nus.edu.sg (Junyi Hou)</author><description>
调 JAX / TPU 程序的时候，迟早会碰到 HLO。想知道一个计算被编译成了什么、为什么多出几次 copy、一个 fusion 里到底装了哪些操作，最后都要打开编译器吐出来的那份文本。
指令里挤着 shape、layout、tiling、memory space 和 computation 引用，光认出 bf16 和 fusion 还远远不够。
所以我做了 HLO Visualizer：把 HLO 文本变成可以探索的图，点一下节点，就能把这条指令一块一块地读明白。</description><content:encoded>&lt;p&gt;调 JAX / TPU 程序的时候，迟早会碰到 HLO。想知道一个计算被编译成了什么、为什么多出几次 copy、一个 fusion 里到底装了哪些操作，最后都要打开编译器吐出来的那份文本。&lt;/p&gt;
&lt;p&gt;指令里挤着 shape、layout、tiling、memory space 和 computation 引用，光认出 &lt;code&gt;bf16&lt;/code&gt; 和 &lt;code&gt;fusion&lt;/code&gt; 还远远不够。&lt;/p&gt;
&lt;p&gt;所以我做了 &lt;strong&gt;&lt;a href="https://www.junyi.dev/hlo-visualizer/" target="_blank" rel="noopener noreferrer"&gt;HLO Visualizer&lt;/a&gt;&lt;/strong&gt;：把 HLO 文本变成可以探索的图，点一下节点，就能把这条指令一块一块地读明白。&lt;/p&gt;
&lt;h2 id="从画出计算图到读懂这条指令" &gt;
&lt;div&gt;
&lt;a href="#%e4%bb%8e%e7%94%bb%e5%87%ba%e8%ae%a1%e7%ae%97%e5%9b%be%e5%88%b0%e8%af%bb%e6%87%82%e8%bf%99%e6%9d%a1%e6%8c%87%e4%bb%a4"&gt;
#
&lt;/a&gt;
从「画出计算图」到「读懂这条指令」
&lt;/div&gt;
&lt;/h2&gt;
&lt;p&gt;官方已经有 HLO 可视化工具了：&lt;strong&gt;XProf 的 Graph Viewer&lt;/strong&gt;。它能围绕某条操作查看依赖关系、展开或折叠 fusion，并把图与 profiling 数据关联起来。定位到耗时操作后，再跳进图里看它的上下游，这个工作流很有用。&lt;a href="https://openxla.org/xprof/graph_viewer" target="_blank" rel="noopener noreferrer"&gt;官方文档&lt;/a&gt;也介绍了这些功能。&lt;/p&gt;
&lt;figure&gt;
&lt;img src="https://www.junyi.dev/posts/hlo-visualizer/xprof-graph-viewer.png" alt="XProf 官方 Graph Viewer，展示 reduce.111 周围的 HLO 指令与依赖关系" loading="lazy"&gt;
&lt;figcaption&gt;XProf Graph Viewer。图片来自 &lt;a href="https://openxla.org/xprof/graph_viewer"&gt;OpenXLA 官方文档&lt;/a&gt;，按 &lt;a href="https://creativecommons.org/licenses/by/4.0/"&gt;CC BY 4.0&lt;/a&gt; 署名使用。&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;HLO Visualizer 把图和指令解释放在同一个界面里。图上的节点按输入、计算、数据搬运、控制流等类别区分；选中节点后，可以追踪上下游，查看原始 HLO，再进入它引用的 computation。读图和读指令之间不需要一直切换窗口。&lt;/p&gt;
&lt;figure&gt;
&lt;img src="https://www.junyi.dev/posts/hlo-visualizer/attention-graph.jpg" alt="HLO Visualizer 展示 TPU 上编译后的 attention 入口计算，包含输入、HBM 到 VMEM 的搬运和 fusion 节点" loading="lazy"&gt;
&lt;figcaption&gt;HLO Visualizer 的 attention 示例：先看清输入、数据搬运和 fusion 之间的关系，再点击节点看细节。与上面的官方截图使用不同程序，展示的是界面与阅读方式。&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;XProf 的强项是把 HLO 与真实运行表现联系起来。我的工具更侧重 &lt;strong&gt;拿到一份 HLO 文本，就开始理解它&lt;/strong&gt;。&lt;/p&gt;
&lt;h2 id="把教程放到你正在读的指令旁边" &gt;
&lt;div&gt;
&lt;a href="#%e6%8a%8a%e6%95%99%e7%a8%8b%e6%94%be%e5%88%b0%e4%bd%a0%e6%ad%a3%e5%9c%a8%e8%af%bb%e7%9a%84%e6%8c%87%e4%bb%a4%e6%97%81%e8%be%b9"&gt;
#
&lt;/a&gt;
把教程放到你正在读的指令旁边
&lt;/div&gt;
&lt;/h2&gt;
&lt;p&gt;Google DeepMind 的 &lt;em&gt;How to Scale Your Model&lt;/em&gt; 中，&lt;a href="https://jax-ml.github.io/scaling-book/profiling/#how-to-read-an-xla-op" target="_blank" rel="noopener noreferrer"&gt;How to read an XLA op&lt;/a&gt; 是很好的入门材料：它用一条 fusion 说明名称、类型、布局、内存位置和输入该怎么读。OpenXLA 的 &lt;a href="https://openxla.org/xla/operation_semantics" target="_blank" rel="noopener noreferrer"&gt;Operation semantics&lt;/a&gt; 则适合查操作的准确语义。&lt;/p&gt;
&lt;p&gt;读完教程，再面对自己的 HLO，还是需要把这些知识逐项对应回去。&lt;/p&gt;
&lt;p&gt;比如下面是一条简化的指令：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-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;%x = bf16[128,128]{1,0:T(8,128)(2,1)S(1)} parameter(0)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;它同时包含几层信息：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;%x&lt;/code&gt; 是这条指令结果的名字。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;bf16[128,128]&lt;/code&gt; 是逻辑类型和 shape。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;1,0&lt;/code&gt; 是布局的 minor-to-major 维度顺序。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;T(8,128)(2,1)&lt;/code&gt; 表示两层物理 tiling。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;S(1)&lt;/code&gt; 在这里的 TPU 示例中表示 VMEM。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;parameter(0)&lt;/code&gt; 表示当前 computation 的第 0 个输入。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Inspector 会把原始指令分成对应的彩色片段，解释名称、操作、输入和属性。类型还可以继续展开，分别看 shape、维度顺序、tiling 和内存空间；tuple 里的元素也能逐层查看。&lt;/p&gt;
&lt;figure&gt;
&lt;img src="https://www.junyi.dev/posts/hlo-visualizer/decomposition.png" alt="Inspector 把 while 指令拆成结果类型、名称、操作、输入、循环条件和循环体，并分别解释" loading="lazy" style="max-width: 480px; width: 100%; display: block; margin: auto;"&gt;
&lt;figcaption&gt;一条 while 指令拆开以后，循环状态、condition 和 body 就有了各自的解释。&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id="tpu-的-layout直接画出来" &gt;
&lt;div&gt;
&lt;a href="#tpu-%e7%9a%84-layout%e7%9b%b4%e6%8e%a5%e7%94%bb%e5%87%ba%e6%9d%a5"&gt;
#
&lt;/a&gt;
TPU 的 layout，直接画出来
&lt;/div&gt;
&lt;/h2&gt;
&lt;p&gt;HLO 里一个很重要的部分是 layout。&lt;strong&gt;逻辑 shape 和物理存储布局是两回事&lt;/strong&gt;：一个 &lt;code&gt;128 × 128&lt;/code&gt; 的数组，还可以按 tile 分块，再在 tile 内继续分块。OpenXLA 的 &lt;a href="https://openxla.org/xla/shapes" target="_blank" rel="noopener noreferrer"&gt;Shapes and layout&lt;/a&gt; 与 &lt;a href="https://openxla.org/xla/tiled_layout" target="_blank" rel="noopener noreferrer"&gt;Tiled layout&lt;/a&gt; 给出了正式说明。&lt;/p&gt;
&lt;p&gt;对于 TPU 示例里常见的 &lt;code&gt;T(8,128)(2,1)&lt;/code&gt;，HLO Visualizer 会画出 tile 的一个局部，标明元素在 tile 内的物理偏移和内层 tile 的边界。这样就能把字符串里的两组数字和实际排列对应起来。&lt;/p&gt;
&lt;figure&gt;
&lt;img src="https://www.junyi.dev/posts/hlo-visualizer/array-layout.png" alt="bf16 数组的 T(8,128)(2,1) 布局图，数字标记 tile 内的物理偏移，红框标记 2×1 内层 tile" loading="lazy" style="max-width: 480px; width: 100%; display: block; margin: auto;"&gt;
&lt;figcaption&gt;8×128 外层 tile 的左上角；红框是 2×1 内层 tile，格子里的数字是 tile 内的物理偏移。&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;工具也会按 TPU 的内存空间解释 HBM、VMEM、SMEM 等位置，帮助理解数据搬运指令。这里的空间编号与后端有关，文章和截图里的解释以项目提供的 TPU v6e 示例为背景。&lt;/p&gt;
&lt;h2 id="用一个小程序开始" &gt;
&lt;div&gt;
&lt;a href="#%e7%94%a8%e4%b8%80%e4%b8%aa%e5%b0%8f%e7%a8%8b%e5%ba%8f%e5%bc%80%e5%a7%8b"&gt;
#
&lt;/a&gt;
用一个小程序开始
&lt;/div&gt;
&lt;/h2&gt;
&lt;p&gt;内置示例来自 &lt;strong&gt;21 个在 TPU v6e 上编译的 JAX 小程序&lt;/strong&gt;，每个都提供优化前和优化后的 HLO。你可以从 &lt;a href="https://www.junyi.dev/hlo-visualizer/?example=attention.after" target="_blank" rel="noopener noreferrer"&gt;attention&lt;/a&gt; 看输入和 fusion，也可以从 &lt;a href="https://www.junyi.dev/hlo-visualizer/?example=fori_matmul.after" target="_blank" rel="noopener noreferrer"&gt;fori_matmul&lt;/a&gt; 看循环，或者从 &lt;a href="https://www.junyi.dev/hlo-visualizer/?example=gather_scatter.after" target="_blank" rel="noopener noreferrer"&gt;gather_scatter&lt;/a&gt; 看索引操作。&lt;/p&gt;
&lt;p&gt;一个简单的学习顺序是：打开示例，从左侧进入入口 computation，点一个节点看输入和结果类型，再进入 fusion 或 while 引用的 computation。然后在 Examples 中切换同一个程序的 Before / After，观察编译前后图的变化。&lt;/p&gt;
&lt;p&gt;如果想看自己的代码，可以按照 &lt;a href="https://docs.jax.dev/en/latest/aot.html" target="_blank" rel="noopener noreferrer"&gt;JAX 的 lowering / compilation 接口&lt;/a&gt; 导出 HLO：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff6ac1"&gt;import&lt;/span&gt; jax
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff6ac1"&gt;import&lt;/span&gt; jax.numpy &lt;span style="color:#ff6ac1"&gt;as&lt;/span&gt; jnp
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff6ac1"&gt;def&lt;/span&gt; &lt;span style="color:#57c7ff"&gt;f&lt;/span&gt;(x, w):
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;return&lt;/span&gt; jax&lt;span style="color:#ff6ac1"&gt;.&lt;/span&gt;nn&lt;span style="color:#ff6ac1"&gt;.&lt;/span&gt;relu(x &lt;span style="color:#ff6ac1"&gt;@&lt;/span&gt; w)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;x &lt;span style="color:#ff6ac1"&gt;=&lt;/span&gt; jnp&lt;span style="color:#ff6ac1"&gt;.&lt;/span&gt;ones((&lt;span style="color:#ff9f43"&gt;128&lt;/span&gt;, &lt;span style="color:#ff9f43"&gt;128&lt;/span&gt;), dtype&lt;span style="color:#ff6ac1"&gt;=&lt;/span&gt;jnp&lt;span style="color:#ff6ac1"&gt;.&lt;/span&gt;bfloat16)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;w &lt;span style="color:#ff6ac1"&gt;=&lt;/span&gt; jnp&lt;span style="color:#ff6ac1"&gt;.&lt;/span&gt;ones((&lt;span style="color:#ff9f43"&gt;128&lt;/span&gt;, &lt;span style="color:#ff9f43"&gt;128&lt;/span&gt;), dtype&lt;span style="color:#ff6ac1"&gt;=&lt;/span&gt;jnp&lt;span style="color:#ff6ac1"&gt;.&lt;/span&gt;bfloat16)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;lowered &lt;span style="color:#ff6ac1"&gt;=&lt;/span&gt; jax&lt;span style="color:#ff6ac1"&gt;.&lt;/span&gt;jit(f)&lt;span style="color:#ff6ac1"&gt;.&lt;/span&gt;lower(x, w)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#78787e"&gt;# 优化前的 HLO&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff5c57"&gt;print&lt;/span&gt;(lowered&lt;span style="color:#ff6ac1"&gt;.&lt;/span&gt;as_text(dialect&lt;span style="color:#ff6ac1"&gt;=&lt;/span&gt;&lt;span style="color:#5af78e"&gt;&amp;#34;hlo&amp;#34;&lt;/span&gt;))
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#78787e"&gt;# 编译后的 HLO；具体布局与内存空间取决于后端&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff5c57"&gt;print&lt;/span&gt;(lowered&lt;span style="color:#ff6ac1"&gt;.&lt;/span&gt;compile()&lt;span style="color:#ff6ac1"&gt;.&lt;/span&gt;as_text())
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;复制其中一份输出，点击 &lt;strong&gt;Open HLO&lt;/strong&gt; 粘贴进去即可。XLA 通过 &lt;code&gt;--xla_dump_to&lt;/code&gt; 导出的 HLO 文本也可以打开。&lt;/p&gt;
&lt;p&gt;我希望第一次学习 HLO 的人，也能从一个小程序开始，把抽象的语法和眼前的计算对应起来。&lt;strong&gt;打开图，点一条指令，顺着数据往下看。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.junyi.dev/hlo-visualizer/" target="_blank" rel="noopener noreferrer"&gt;试试 HLO Visualizer&lt;/a&gt;&lt;/strong&gt;。如果它让你更容易读懂自己的 HLO，欢迎给 &lt;a href="https://github.com/Junyi-99/hlo-visualizer" target="_blank" rel="noopener noreferrer"&gt;GitHub 仓库&lt;/a&gt; 点个 star；遇到看不懂或解析不对的指令，也欢迎提 issue。&lt;/p&gt;</content:encoded><category>TPU</category><category>JAX</category><category>XLA</category><category>HLO</category><category>Tech</category><guid isPermaLink="true">https://www.junyi.dev/posts/hlo-visualizer/</guid></item><item><title>LLM Inference on TPU v6e-4: Small Dense, Large MoE, and Large Dense Models</title><link>https://www.junyi.dev/posts/tpu-v6e/</link><pubDate>Thu, 11 Jun 2026 00:00:00 Z</pubDate><author>junyi.h@comp.nus.edu.sg (Junyi Hou)</author><description>
# Abstract We benchmark LLM inference on one Google TPU v6e-4 host (four chips, one VM). We use vLLM 0.20.0 with the tpu-inference backend and an fp8 KV cache.
We test three Qwen3 models:
Model Type Params Parallelism Chips Qwen3.5-4B dense 4B active tp1 1 Qwen3-30B-A3B MoE 30B total / 3B active tp4 4 Qwen3-32B dense 32B active tp4 4 We measure three parts of inference: prefill, decode, and end-to-end online serving.</description><content:encoded>&lt;h2 id="abstract" &gt;
&lt;div&gt;
&lt;a href="#abstract"&gt;
#
&lt;/a&gt;
Abstract
&lt;/div&gt;
&lt;/h2&gt;
&lt;p&gt;We benchmark LLM inference on one Google &lt;code&gt;TPU v6e-4&lt;/code&gt; host (four chips, one VM). We use &lt;code&gt;vLLM 0.20.0&lt;/code&gt; with the &lt;code&gt;tpu-inference&lt;/code&gt; backend and an &lt;code&gt;fp8&lt;/code&gt; KV cache.&lt;/p&gt;
&lt;p&gt;We test three Qwen3 models:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th style="text-align: right"&gt;Params&lt;/th&gt;
&lt;th style="text-align: right"&gt;Parallelism&lt;/th&gt;
&lt;th style="text-align: right"&gt;Chips&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Qwen3.5-4B&lt;/td&gt;
&lt;td&gt;dense&lt;/td&gt;
&lt;td style="text-align: right"&gt;4B active&lt;/td&gt;
&lt;td style="text-align: right"&gt;tp1&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen3-30B-A3B&lt;/td&gt;
&lt;td&gt;MoE&lt;/td&gt;
&lt;td style="text-align: right"&gt;30B total / 3B active&lt;/td&gt;
&lt;td style="text-align: right"&gt;tp4&lt;/td&gt;
&lt;td style="text-align: right"&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen3-32B&lt;/td&gt;
&lt;td&gt;dense&lt;/td&gt;
&lt;td style="text-align: right"&gt;32B active&lt;/td&gt;
&lt;td style="text-align: right"&gt;tp4&lt;/td&gt;
&lt;td style="text-align: right"&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;We measure three parts of inference: prefill, decode, and end-to-end online serving.&lt;/p&gt;
&lt;p&gt;The MoE model is fastest in all three tests. It reaches 26,063 tok/s in prefill, compared with 19,714 tok/s for the dense 32B model. At batch size 1, its decode latency is 7.0 ms per token, compared with 17.9 ms for the dense 32B model. In online serving, it reaches 1.27 req/s and 1,303 output tok/s, compared with 0.88 req/s and 901 output tok/s for the dense 32B model.&lt;/p&gt;
&lt;p&gt;The 4B model has good latency at low load, but it does not handle high concurrency well on one chip. Its stable serving capacity is about 0.45 req/s.&lt;/p&gt;
&lt;p&gt;The goal is to report TPU v6e performance using the same vLLM tools that users would run in practice.&lt;/p&gt;
&lt;h2 id="1-motivation" &gt;
&lt;div&gt;
&lt;a href="#1-motivation"&gt;
#
&lt;/a&gt;
1. Motivation
&lt;/div&gt;
&lt;/h2&gt;
&lt;p&gt;Public TPU data for LLM inference is limited. The only TPU submission Google has made to MLPerf Inference is SDXL image generation, not an LLM. The TPU LLM numbers are measured on Google&amp;rsquo;s JAX/JetStream stack rather than the vLLM path most practitioners deploy.&lt;/p&gt;
&lt;p&gt;We measured the performance ourselves on one TPU v6e-4 host. The goal is to give simple, reproducible numbers for three common model types:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;a small dense model,&lt;/li&gt;
&lt;li&gt;a large MoE model,&lt;/li&gt;
&lt;li&gt;a large dense model.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="2-experimental-setup" &gt;
&lt;div&gt;
&lt;a href="#2-experimental-setup"&gt;
#
&lt;/a&gt;
2. Experimental Setup
&lt;/div&gt;
&lt;/h2&gt;
&lt;p&gt;All measurements use one TPU v6e-4 host with four chips in one VM. The 4B model uses one chip. The 30B-A3B and 32B models use all four chips. For per-chip numbers, we divide the four-chip throughput by four.&lt;/p&gt;
&lt;p&gt;We use &lt;code&gt;vLLM 0.20.0&lt;/code&gt;, &lt;code&gt;JAX 0.10.0&lt;/code&gt;, &lt;code&gt;libtpu 0.0.40&lt;/code&gt;, &lt;code&gt;Python 3.11&lt;/code&gt; with the &lt;code&gt;tpu-inference&lt;/code&gt; backend. The KV cache uses &lt;code&gt;fp8_e5m2&lt;/code&gt; in all runs. We report exact versions because TPU performance can change quickly across releases.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th style="text-align: right"&gt;Total / active params&lt;/th&gt;
&lt;th style="text-align: right"&gt;Parallelism&lt;/th&gt;
&lt;th style="text-align: right"&gt;Chips&lt;/th&gt;
&lt;th&gt;Config&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Qwen3.5-4B&lt;/td&gt;
&lt;td&gt;dense&lt;/td&gt;
&lt;td style="text-align: right"&gt;4B / 4B&lt;/td&gt;
&lt;td style="text-align: right"&gt;tp1&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;td&gt;GDN, MBT2048&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen3-30B-A3B&lt;/td&gt;
&lt;td&gt;MoE&lt;/td&gt;
&lt;td style="text-align: right"&gt;30B / 3B&lt;/td&gt;
&lt;td style="text-align: right"&gt;tp4&lt;/td&gt;
&lt;td style="text-align: right"&gt;4&lt;/td&gt;
&lt;td&gt;GMU0.8, MBT8192&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen3-32B&lt;/td&gt;
&lt;td&gt;dense&lt;/td&gt;
&lt;td style="text-align: right"&gt;32B / 32B&lt;/td&gt;
&lt;td style="text-align: right"&gt;tp4&lt;/td&gt;
&lt;td style="text-align: right"&gt;4&lt;/td&gt;
&lt;td&gt;GMU0.8, MBT8192&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;figcaption&gt;Table 1. Model configurations used in all experiments. GDN = guided decoding off, GMU = GPU memory utilization, MBT = max-num-batched-tokens.&lt;/figcaption&gt;
&lt;p&gt;The key comparison is between Qwen3-30B-A3B and Qwen3-32B. They have similar total size, but the MoE model only uses 3B parameters per token, while the dense 32B model uses all 32B.&lt;/p&gt;
&lt;p&gt;We run three tests.&lt;/p&gt;
&lt;p&gt;For prefill, we use concurrency 1 and output length 1. We vary context length over 512, 1024, 2048, 4096, and 8192 tokens. This measures how fast the model processes the input prompt.&lt;/p&gt;
&lt;p&gt;For decode, we fix the output length at 128 tokens and vary batch size over 1, 4, 16, and 64. We mainly report context length 1024, with context length 4096 as a longer-KV case. This measures the cost of generating tokens after prefill.&lt;/p&gt;
&lt;p&gt;For online serving, we use &lt;code&gt;vllm bench serve&lt;/code&gt; against the OpenAI-compatible endpoint. Input and output lengths are both 1024 tokens. We increase the request rate until the server saturates.&lt;/p&gt;
&lt;p&gt;Metrics:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;TTFT: time to first token. This mainly measures prefill.&lt;/li&gt;
&lt;li&gt;TPOT: time per output token. This mainly measures decode.&lt;/li&gt;
&lt;li&gt;E2E latency: full request time.&lt;/li&gt;
&lt;li&gt;Prefill throughput: context length divided by p50 TTFT.&lt;/li&gt;
&lt;li&gt;Decode throughput: batch size × 1000 divided by p50 TPOT.&lt;/li&gt;
&lt;li&gt;Output throughput: generated tokens per second.&lt;/li&gt;
&lt;li&gt;Request throughput: completed requests per second.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We report p50 unless stated otherwise.&lt;/p&gt;
&lt;h2 id="3-results" &gt;
&lt;div&gt;
&lt;a href="#3-results"&gt;
#
&lt;/a&gt;
3. Results
&lt;/div&gt;
&lt;/h2&gt;
&lt;h3 id="31-prefill" &gt;
&lt;div&gt;
&lt;a href="#31-prefill"&gt;
##
&lt;/a&gt;
3.1 Prefill
&lt;/div&gt;
&lt;/h3&gt;
&lt;figure&gt;
&lt;img style="border-radius: 8px" src="https://www.junyi.dev/posts/tpu-v6e/figures/fig1_prefill.png" alt="Prefill throughput versus context length on TPU v6e-4"&gt;
&lt;figcaption&gt;Figure 1. Prefill throughput versus context length. Higher is better. Throughput is aggregate over the chips each model uses — &lt;b&gt;the 30B-A3B and 32B use four chips&lt;/b&gt;, the 4B uses one.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;The MoE model has the highest prefill throughput.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th style="text-align: right"&gt;Best prefill throughput&lt;/th&gt;
&lt;th style="text-align: right"&gt;Per-chip peak&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Qwen3-30B-A3B&lt;/td&gt;
&lt;td style="text-align: right"&gt;26,063 tok/s&lt;/td&gt;
&lt;td style="text-align: right"&gt;~6.5k tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen3-32B&lt;/td&gt;
&lt;td style="text-align: right"&gt;19,714 tok/s&lt;/td&gt;
&lt;td style="text-align: right"&gt;~4.9k tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen3.5-4B&lt;/td&gt;
&lt;td style="text-align: right"&gt;4,177 tok/s&lt;/td&gt;
&lt;td style="text-align: right"&gt;~4.2k tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;figcaption&gt;Table 2. Peak prefill throughput and per-chip peak for each model.&lt;/figcaption&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th style="text-align: right"&gt;ctx 512&lt;/th&gt;
&lt;th style="text-align: right"&gt;ctx 1024&lt;/th&gt;
&lt;th style="text-align: right"&gt;ctx 2048&lt;/th&gt;
&lt;th style="text-align: right"&gt;ctx 4096&lt;/th&gt;
&lt;th style="text-align: right"&gt;ctx 8192&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Qwen3-30B-A3B&lt;/td&gt;
&lt;td style="text-align: right"&gt;35 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;51 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;84 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;157 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;318 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen3-32B&lt;/td&gt;
&lt;td style="text-align: right"&gt;38 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;58 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;104 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;208 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;461 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen3.5-4B&lt;/td&gt;
&lt;td style="text-align: right"&gt;432 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;452 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;495 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;983 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;1,961 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;figcaption&gt;Table 3. p50 TTFT at each context length (ms).&lt;/figcaption&gt;
&lt;p&gt;The 4B result is limited by its single-chip setup and by &lt;code&gt;max-num-batched-tokens=2048&lt;/code&gt;. For contexts above 2048, prefill is split into chunks, so the curve flattens.&lt;/p&gt;
&lt;h3 id="32-decode" &gt;
&lt;div&gt;
&lt;a href="#32-decode"&gt;
##
&lt;/a&gt;
3.2 Decode
&lt;/div&gt;
&lt;/h3&gt;
&lt;figure&gt;
&lt;img style="border-radius: 8px" src="https://www.junyi.dev/posts/tpu-v6e/figures/fig2_decode_tpot.png" alt="Decode p50 TPOT versus batch size at context 1024 and 4096"&gt;
&lt;figcaption&gt;Figure 2. Decode p50 TPOT versus batch size. Solid lines: context 1024. Dashed lines: context 4096. Lower is better.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure&gt;
&lt;img style="border-radius: 8px" src="https://www.junyi.dev/posts/tpu-v6e/figures/fig3_decode_throughput.png" alt="Aggregate decode throughput versus batch size at context 1024 and 4096"&gt;
&lt;figcaption&gt;Figure 3. Aggregate decode throughput versus batch size. Solid lines: context 1024. Dashed lines: context 4096. Higher is better. Throughput is aggregate over the chips each model uses — the 30B-A3B and 32B use four chips, the 4B uses one.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th style="text-align: right"&gt;BS&lt;/th&gt;
&lt;th style="text-align: right"&gt;p50 TPOT&lt;/th&gt;
&lt;th style="text-align: right"&gt;p99 TPOT&lt;/th&gt;
&lt;th style="text-align: right"&gt;throughput&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Qwen3-30B-A3B&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;td style="text-align: right"&gt;7.0 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;7.1 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;143 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen3-30B-A3B&lt;/td&gt;
&lt;td style="text-align: right"&gt;4&lt;/td&gt;
&lt;td style="text-align: right"&gt;8.8 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;9.9 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;456 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen3-30B-A3B&lt;/td&gt;
&lt;td style="text-align: right"&gt;16&lt;/td&gt;
&lt;td style="text-align: right"&gt;14.9 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;17.5 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;1,071 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen3-30B-A3B&lt;/td&gt;
&lt;td style="text-align: right"&gt;64&lt;/td&gt;
&lt;td style="text-align: right"&gt;48.1 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;152.7 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;1,331 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen3-32B&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;td style="text-align: right"&gt;17.9 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;17.9 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;56 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen3-32B&lt;/td&gt;
&lt;td style="text-align: right"&gt;4&lt;/td&gt;
&lt;td style="text-align: right"&gt;18.2 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;19.4 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;220 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen3-32B&lt;/td&gt;
&lt;td style="text-align: right"&gt;16&lt;/td&gt;
&lt;td style="text-align: right"&gt;22.4 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;27.4 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;715 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen3-32B&lt;/td&gt;
&lt;td style="text-align: right"&gt;64&lt;/td&gt;
&lt;td style="text-align: right"&gt;58.4 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;65.3 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;1,096 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen3.5-4B&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;td style="text-align: right"&gt;10.7 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;10.7 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;93 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen3.5-4B&lt;/td&gt;
&lt;td style="text-align: right"&gt;4&lt;/td&gt;
&lt;td style="text-align: right"&gt;16.2 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;18.3 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;247 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen3.5-4B&lt;/td&gt;
&lt;td style="text-align: right"&gt;16&lt;/td&gt;
&lt;td style="text-align: right"&gt;41.4 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;647.3 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;387 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen3.5-4B&lt;/td&gt;
&lt;td style="text-align: right"&gt;64&lt;/td&gt;
&lt;td style="text-align: right"&gt;80.5 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;97.2 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;795 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;figcaption&gt;Table 4. Decode TPOT at context length 1024 (p50, p99, and aggregate throughput).&lt;/figcaption&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th style="text-align: right"&gt;BS&lt;/th&gt;
&lt;th style="text-align: right"&gt;p50 TPOT&lt;/th&gt;
&lt;th style="text-align: right"&gt;p99 TPOT&lt;/th&gt;
&lt;th style="text-align: right"&gt;throughput&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Qwen3-30B-A3B&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;td style="text-align: right"&gt;7.0 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;7.1 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;143 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen3-30B-A3B&lt;/td&gt;
&lt;td style="text-align: right"&gt;4&lt;/td&gt;
&lt;td style="text-align: right"&gt;9.7 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;12.9 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;411 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen3-30B-A3B&lt;/td&gt;
&lt;td style="text-align: right"&gt;16&lt;/td&gt;
&lt;td style="text-align: right"&gt;22.3 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;28.5 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;719 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen3-30B-A3B&lt;/td&gt;
&lt;td style="text-align: right"&gt;64&lt;/td&gt;
&lt;td style="text-align: right"&gt;96.8 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;213.9 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;661 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen3-32B&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;td style="text-align: right"&gt;19.1 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;19.3 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;53 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen3-32B&lt;/td&gt;
&lt;td style="text-align: right"&gt;4&lt;/td&gt;
&lt;td style="text-align: right"&gt;21.4 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;25.2 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;187 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen3-32B&lt;/td&gt;
&lt;td style="text-align: right"&gt;16&lt;/td&gt;
&lt;td style="text-align: right"&gt;37.7 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;130.6 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;424 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen3-32B&lt;/td&gt;
&lt;td style="text-align: right"&gt;64&lt;/td&gt;
&lt;td style="text-align: right"&gt;82.5 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;92.8 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;775 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen3.5-4B&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;td style="text-align: right"&gt;10.8 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;10.8 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;93 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen3.5-4B&lt;/td&gt;
&lt;td style="text-align: right"&gt;4&lt;/td&gt;
&lt;td style="text-align: right"&gt;25.3 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;34.1 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;158 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen3.5-4B&lt;/td&gt;
&lt;td style="text-align: right"&gt;16&lt;/td&gt;
&lt;td style="text-align: right"&gt;85.0 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;89.2 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;188 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen3.5-4B&lt;/td&gt;
&lt;td style="text-align: right"&gt;64&lt;/td&gt;
&lt;td style="text-align: right"&gt;85.0 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;89.2 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;753 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;figcaption&gt;Table 5. Decode TPOT at context length 4096 (p50, p99, and aggregate throughput).&lt;/figcaption&gt;
&lt;p&gt;At batch size 1, the MoE model is 2.5× faster than the dense 32B model: 7.0 ms versus 17.9 ms at context 1024. This follows active parameter count. Decode is memory-bandwidth-bound. The MoE model reads 3B active parameters per token; the dense 32B model reads all 32B.&lt;/p&gt;
&lt;p&gt;Tail latency — the p99 columns of Tables 4 and 5 — stays tight for the four-chip models until BS64 (the 30B-A3B reaches p99 152.7 ms against a 48.1 ms p50 at ctx 1024). The 4B model on one chip is the outlier: at BS16 ctx 1024 its p99 TPOT reaches 647.3 ms against a 41.4 ms p50. These decode runs are driven at an unlimited request rate, so on the single chip the prefill chunks of newly admitted requests repeatedly preempt in-flight decode — the median inter-token latency stays near 12 ms while a heavy minority of requests stall. This marks the 4B&amp;rsquo;s single-chip concurrency limit, not steady-state decode.&lt;/p&gt;
&lt;p&gt;At context 4096, longer KV sequences reduce throughput across all models. The 30B-A3B at BS64 drops from 1,331 tok/s (ctx 1024) to 661 tok/s (ctx 4096). The dense 32B drops less in relative terms (1,096 to 775 tok/s) because its compute is already the bottleneck at ctx 1024. The 4B model saturates at BS16 for ctx 4096: p50 TPOT at BS64 equals BS16 (85.0 ms both), meaning additional concurrency gives no throughput gain on one chip.&lt;/p&gt;
&lt;h3 id="33-online-serving" &gt;
&lt;div&gt;
&lt;a href="#33-online-serving"&gt;
##
&lt;/a&gt;
3.3 Online serving
&lt;/div&gt;
&lt;/h3&gt;
&lt;figure&gt;
&lt;img style="border-radius: 8px" src="https://www.junyi.dev/posts/tpu-v6e/figures/fig4_e2e_pareto.png" alt="Online-serving throughput-latency curve"&gt;
&lt;figcaption&gt;Figure 4. Online serving throughput and latency. The x-axis is output throughput. The y-axis is p50 TPOT. Lower-right is better. Throughput is aggregate over the chips each model uses — the 30B-A3B and 32B use four chips, the 4B uses one.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;In online serving, higher request rate gives higher throughput, but also higher latency. Each model reaches a point where adding more requests no longer helps.&lt;/p&gt;
&lt;figure&gt;
&lt;img style="border-radius: 8px" src="https://www.junyi.dev/posts/tpu-v6e/figures/fig7_e2e_sweep.png" alt="End-to-end TTFT and TPOT versus output throughput"&gt;
&lt;figcaption&gt;Figure 5. End-to-end latency sweep. Left: p50 TTFT versus output throughput. Right: p50 TPOT versus output throughput. The × marker on the 4B line marks the concurrency-collapse point under unlimited load. Throughput is aggregate over the chips each model uses — the 30B-A3B and 32B use four chips, the 4B uses one.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style="text-align: right"&gt;Req rate&lt;/th&gt;
&lt;th style="text-align: right"&gt;Actual req/s&lt;/th&gt;
&lt;th style="text-align: right"&gt;Output tok/s&lt;/th&gt;
&lt;th style="text-align: right"&gt;TTFT p50&lt;/th&gt;
&lt;th style="text-align: right"&gt;TTFT p99&lt;/th&gt;
&lt;th style="text-align: right"&gt;TPOT p50&lt;/th&gt;
&lt;th style="text-align: right"&gt;TPOT p99&lt;/th&gt;
&lt;th style="text-align: right"&gt;E2E p50&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style="text-align: right"&gt;0.2&lt;/td&gt;
&lt;td style="text-align: right"&gt;0.19&lt;/td&gt;
&lt;td style="text-align: right"&gt;197&lt;/td&gt;
&lt;td style="text-align: right"&gt;90 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;2,682 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;7.9 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;9.0 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;8.2 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: right"&gt;0.4&lt;/td&gt;
&lt;td style="text-align: right"&gt;0.36&lt;/td&gt;
&lt;td style="text-align: right"&gt;366&lt;/td&gt;
&lt;td style="text-align: right"&gt;92 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;190 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;9.2 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;10.1 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;9.5 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: right"&gt;0.6&lt;/td&gt;
&lt;td style="text-align: right"&gt;0.52&lt;/td&gt;
&lt;td style="text-align: right"&gt;537&lt;/td&gt;
&lt;td style="text-align: right"&gt;94 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;1,331 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;11.2 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;13.8 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;11.6 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: right"&gt;0.8&lt;/td&gt;
&lt;td style="text-align: right"&gt;0.62&lt;/td&gt;
&lt;td style="text-align: right"&gt;635&lt;/td&gt;
&lt;td style="text-align: right"&gt;116 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;6,335 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;18.8 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;28.7 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;24.6 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: right"&gt;∞&lt;/td&gt;
&lt;td style="text-align: right"&gt;1.27&lt;/td&gt;
&lt;td style="text-align: right"&gt;1,303&lt;/td&gt;
&lt;td style="text-align: right"&gt;712 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;1,092 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;22.3 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;22.8 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;23.6 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;figcaption&gt;Table 6. Qwen3-30B-A3B online serving results (ISL = OSL = 1024 tokens).&lt;/figcaption&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style="text-align: right"&gt;Req rate&lt;/th&gt;
&lt;th style="text-align: right"&gt;Actual req/s&lt;/th&gt;
&lt;th style="text-align: right"&gt;Output tok/s&lt;/th&gt;
&lt;th style="text-align: right"&gt;TTFT p50&lt;/th&gt;
&lt;th style="text-align: right"&gt;TTFT p99&lt;/th&gt;
&lt;th style="text-align: right"&gt;TPOT p50&lt;/th&gt;
&lt;th style="text-align: right"&gt;TPOT p99&lt;/th&gt;
&lt;th style="text-align: right"&gt;E2E p50&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style="text-align: right"&gt;0.15&lt;/td&gt;
&lt;td style="text-align: right"&gt;0.14&lt;/td&gt;
&lt;td style="text-align: right"&gt;144&lt;/td&gt;
&lt;td style="text-align: right"&gt;115 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;2,243 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;18.0 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;18.3 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;18.5 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: right"&gt;0.3&lt;/td&gt;
&lt;td style="text-align: right"&gt;0.26&lt;/td&gt;
&lt;td style="text-align: right"&gt;271&lt;/td&gt;
&lt;td style="text-align: right"&gt;115 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;1,042 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;18.9 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;20.1 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;19.5 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: right"&gt;0.45&lt;/td&gt;
&lt;td style="text-align: right"&gt;0.37&lt;/td&gt;
&lt;td style="text-align: right"&gt;380&lt;/td&gt;
&lt;td style="text-align: right"&gt;116 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;295 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;19.5 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;20.9 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;20.2 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: right"&gt;0.6&lt;/td&gt;
&lt;td style="text-align: right"&gt;0.45&lt;/td&gt;
&lt;td style="text-align: right"&gt;466&lt;/td&gt;
&lt;td style="text-align: right"&gt;123 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;3,983 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;21.0 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;28.1 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;21.7 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: right"&gt;∞&lt;/td&gt;
&lt;td style="text-align: right"&gt;0.88&lt;/td&gt;
&lt;td style="text-align: right"&gt;901&lt;/td&gt;
&lt;td style="text-align: right"&gt;1,190 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;9,249 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;43.2 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;44.0 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;45.4 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;figcaption&gt;Table 7. Qwen3-32B online serving results (ISL = OSL = 1024 tokens).&lt;/figcaption&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style="text-align: right"&gt;Req rate&lt;/th&gt;
&lt;th style="text-align: right"&gt;Actual req/s&lt;/th&gt;
&lt;th style="text-align: right"&gt;Output tok/s&lt;/th&gt;
&lt;th style="text-align: right"&gt;TTFT p50&lt;/th&gt;
&lt;th style="text-align: right"&gt;TTFT p99&lt;/th&gt;
&lt;th style="text-align: right"&gt;TPOT p50&lt;/th&gt;
&lt;th style="text-align: right"&gt;TPOT p99&lt;/th&gt;
&lt;th style="text-align: right"&gt;E2E p50&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style="text-align: right"&gt;0.2&lt;/td&gt;
&lt;td style="text-align: right"&gt;0.19&lt;/td&gt;
&lt;td style="text-align: right"&gt;194&lt;/td&gt;
&lt;td style="text-align: right"&gt;509 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;33,147 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;12.3 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;15.4 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;13.1 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: right"&gt;0.3&lt;/td&gt;
&lt;td style="text-align: right"&gt;0.28&lt;/td&gt;
&lt;td style="text-align: right"&gt;283&lt;/td&gt;
&lt;td style="text-align: right"&gt;507 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;1,185 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;12.8 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;14.3 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;13.7 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: right"&gt;0.4&lt;/td&gt;
&lt;td style="text-align: right"&gt;0.36&lt;/td&gt;
&lt;td style="text-align: right"&gt;367&lt;/td&gt;
&lt;td style="text-align: right"&gt;506 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;1,099 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;13.4 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;15.7 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;14.4 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: right"&gt;0.45&lt;/td&gt;
&lt;td style="text-align: right"&gt;0.40&lt;/td&gt;
&lt;td style="text-align: right"&gt;407&lt;/td&gt;
&lt;td style="text-align: right"&gt;507 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;1,141 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;13.8 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;15.9 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;14.6 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: right"&gt;∞&lt;/td&gt;
&lt;td style="text-align: right"&gt;0.14&lt;/td&gt;
&lt;td style="text-align: right"&gt;143&lt;/td&gt;
&lt;td style="text-align: right"&gt;6,762 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;219,855 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;133.8 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;237.2 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;206.8 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;figcaption&gt;Table 8. Qwen3.5-4B online serving results (ISL = OSL = 1024 tokens).&lt;/figcaption&gt;
&lt;p&gt;The MoE model (30B-A3B) has a stable working zone up to about 0.6 req/s, where p50 TPOT stays below 12 ms and p99 TTFT stays below 1.4 seconds. At 0.8 req/s the server is already queuing: p99 TTFT jumps to 6.3 seconds while p50 TPOT jumps from 11 ms to 19 ms. Saturation at ∞ rate gives tight p99/p50 TPOT (22.8 ms versus 22.3 ms), meaning the server is consistently loaded with no idle cycles.&lt;/p&gt;
&lt;p&gt;The dense 32B model’s TPOT p50 is nearly flat from 0.15 to 0.6 req/s (18–21 ms), then doubles at saturation (43.2 ms). Its TTFT p99 is noisy across the sweep, suggesting occasional prefill bursts even at low load.&lt;/p&gt;
&lt;p&gt;The 4B model holds stable from 0.2 to 0.45 req/s. At 0.45 req/s it sustains 0.40 req/s and 407 output tok/s, with TPOT p50 of 13.8 ms and E2E p50 of 14.6 seconds. Under unlimited load it collapses entirely: output throughput drops from 407 tok/s to 143 tok/s, p50 TPOT rises from 13.8 ms to 133.8 ms, and p50 E2E latency reaches 206.8 seconds. The p99 TTFT at unlimited load is 219,855 ms — 220 seconds. This is not a degraded state; it is a broken one. We treat the unlimited-load point as a single-chip concurrency limit, not useful serving capacity.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th style="text-align: right"&gt;p50 TTFT&lt;/th&gt;
&lt;th style="text-align: right"&gt;p50 E2E latency&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Qwen3-30B-A3B&lt;/td&gt;
&lt;td style="text-align: right"&gt;712 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;23.6 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen3-32B&lt;/td&gt;
&lt;td style="text-align: right"&gt;1,190 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;45.4 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen3.5-4B&lt;/td&gt;
&lt;td style="text-align: right"&gt;507 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;14.6 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;figcaption&gt;Table 9. p50 TTFT and p50 E2E latency at the saturation or stable operating point.&lt;/figcaption&gt;
&lt;figure&gt;
&lt;img style="border-radius: 8px" src="https://www.junyi.dev/posts/tpu-v6e/figures/fig5_summary.png" alt="MoE-versus-dense efficiency summary"&gt;
&lt;figcaption&gt;Figure 6. Summary relative to the dense Qwen3-32B baseline. Higher is better.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Across all three tests, the MoE model beats the dense 32B model at similar total parameter count:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th style="text-align: right"&gt;Qwen3-30B-A3B&lt;/th&gt;
&lt;th style="text-align: right"&gt;Qwen3-32B&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Best prefill throughput&lt;/td&gt;
&lt;td style="text-align: right"&gt;26,063 tok/s&lt;/td&gt;
&lt;td style="text-align: right"&gt;19,714 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BS1 decode TPOT&lt;/td&gt;
&lt;td style="text-align: right"&gt;7.0 ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;17.9 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Serving capacity&lt;/td&gt;
&lt;td style="text-align: right"&gt;1.27 req/s&lt;/td&gt;
&lt;td style="text-align: right"&gt;0.88 req/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output throughput at saturation&lt;/td&gt;
&lt;td style="text-align: right"&gt;1,303 tok/s&lt;/td&gt;
&lt;td style="text-align: right"&gt;901 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;figcaption&gt;Table 10. Summary comparison of Qwen3-30B-A3B versus Qwen3-32B across all three tests.&lt;/figcaption&gt;
&lt;p&gt;The MoE model gives about 2.5× lower single-stream decode latency, about 1.4× higher serving capacity, and about 1.3× higher prefill throughput.&lt;/p&gt;
&lt;h2 id="4-discussion" &gt;
&lt;div&gt;
&lt;a href="#4-discussion"&gt;
#
&lt;/a&gt;
4. Discussion
&lt;/div&gt;
&lt;/h2&gt;
&lt;p&gt;The large MoE and dense models have similar total parameter counts but different active sizes. The lower active parameter count helps explain the MoE model&amp;rsquo;s decode advantage.&lt;/p&gt;
&lt;p&gt;The small 4B model is limited by its single-chip setup. To serve more traffic, the better path is to add chips or replicas, rather than increasing concurrency on one chip.&lt;/p&gt;
&lt;h2 id="5-conclusion" &gt;
&lt;div&gt;
&lt;a href="#5-conclusion"&gt;
#
&lt;/a&gt;
5. Conclusion
&lt;/div&gt;
&lt;/h2&gt;
&lt;p&gt;In these tests, Qwen3-30B-A3B has the highest prefill throughput, the lowest batch-size-1 decode latency, and the highest serving capacity. The 4B dense model has good low-load latency, but its single-chip setup limits concurrency.&lt;/p&gt;
&lt;h2 id="limitations" &gt;
&lt;div&gt;
&lt;a href="#limitations"&gt;
#
&lt;/a&gt;
Limitations
&lt;/div&gt;
&lt;/h2&gt;
&lt;p&gt;These results are for one host only: four TPU v6e chips in one VM. We did not test multi-host scaling.&lt;/p&gt;
&lt;p&gt;We did not run a GPU baseline, so this study makes no TPU-versus-GPU claim.&lt;/p&gt;
&lt;p&gt;The online serving test uses one input/output shape: 1024 input tokens and 1024 output tokens. Other workloads may shift the balance between prefill and decode.&lt;/p&gt;
&lt;p&gt;The results are version-specific. TPU support in vLLM changes quickly, so the numbers here apply to &lt;code&gt;vLLM 0.20.0&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;All runs use an &lt;code&gt;fp8_e5m2&lt;/code&gt; KV cache. We did not test other KV cache formats.&lt;/p&gt;
&lt;h2 id="reproducibility" &gt;
&lt;div&gt;
&lt;a href="#reproducibility"&gt;
#
&lt;/a&gt;
Reproducibility
&lt;/div&gt;
&lt;/h2&gt;
&lt;p&gt;The main numbers are &lt;a href="https://www.junyi.dev/posts/tpu-v6e/data.json" target="_blank" rel="noopener noreferrer"&gt;./data.json&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Figures are generated by &lt;a href="https://www.junyi.dev/posts/tpu-v6e/figures/plot.py" target="_blank" rel="noopener noreferrer"&gt;./figures/plot.py&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Per-model results are at &lt;a href="https://www.junyi.dev/posts/tpu-v6e/rawdata.zip" target="_blank" rel="noopener noreferrer"&gt;./rawdata.zip&lt;/a&gt;&lt;/p&gt;</content:encoded><category>TPU</category><guid isPermaLink="true">https://www.junyi.dev/posts/tpu-v6e/</guid></item><item><title>Claude Code 在 tmux 里要求重新登录？八成是 macOS Keychain 的锅</title><link>https://www.junyi.dev/posts/tmux-keychain/</link><pubDate>Fri, 03 Apr 2026 00:00:00 +0800</pubDate><author>junyi.h@comp.nus.edu.sg (Junyi Hou)</author><description>
终端里 claude 好好的，Team Account 也认得，进了 tmux 就让我重新登录。折腾了一会儿才发现是 macOS Security Session 的老问题。</description><content:encoded>&lt;p&gt;终端里 &lt;code&gt;claude&lt;/code&gt; 好好的，Team Account 也认得，进了 tmux 就让我重新登录。折腾了一会儿才发现是 macOS Security Session 的老问题。&lt;/p&gt;
&lt;h2 id="现象" &gt;
&lt;div&gt;
&lt;a href="#%e7%8e%b0%e8%b1%a1"&gt;
#
&lt;/a&gt;
现象
&lt;/div&gt;
&lt;/h2&gt;
&lt;p&gt;Ghostty 里直接跑 &lt;code&gt;claude&lt;/code&gt;，没问题。tmux 里跑，要求登录。&lt;/p&gt;
&lt;details&gt;
&lt;summary&gt;排查过程（点击展开）&lt;/summary&gt;
&lt;h3 id="环境变量" &gt;
&lt;div&gt;
&lt;a href="#%e7%8e%af%e5%a2%83%e5%8f%98%e9%87%8f"&gt;
##
&lt;/a&gt;
环境变量？
&lt;/div&gt;
&lt;/h3&gt;
&lt;p&gt;第一反应是 tmux 里环境变量不一样。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;env | grep -iE &lt;span style="color:#5af78e"&gt;&amp;#39;claude|anthropic&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;两边都没有。Claude Code 认证不存环境变量。&lt;/p&gt;
&lt;h3 id="配置文件" &gt;
&lt;div&gt;
&lt;a href="#%e9%85%8d%e7%bd%ae%e6%96%87%e4%bb%b6"&gt;
##
&lt;/a&gt;
配置文件？
&lt;/div&gt;
&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;cat ~/.claude/.credentials.json 2&amp;gt;/dev/null &lt;span style="color:#ff6ac1"&gt;||&lt;/span&gt; &lt;span style="color:#ff5c57"&gt;echo&lt;/span&gt; &lt;span style="color:#5af78e"&gt;&amp;#34;no credentials file&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;也没有。不在文件系统里。&lt;/p&gt;
&lt;h3 id="keychain" &gt;
&lt;div&gt;
&lt;a href="#keychain"&gt;
##
&lt;/a&gt;
Keychain
&lt;/div&gt;
&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;security dump-keychain 2&amp;gt;&amp;amp;&lt;span style="color:#ff9f43"&gt;1&lt;/span&gt; | grep -i -A3 &lt;span style="color:#5af78e"&gt;&amp;#39;claude\|anthropic&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;找到了，存在 macOS Keychain 里，service name 是 &lt;code&gt;Claude Code-credentials&lt;/code&gt;：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;0x00000007 &amp;lt;blob&amp;gt;=&amp;#34;Claude Code-credentials&amp;#34;
&amp;#34;acct&amp;#34;&amp;lt;blob&amp;gt;=&amp;#34;junyi&amp;#34;
&amp;#34;svce&amp;#34;&amp;lt;blob&amp;gt;=&amp;#34;Claude Code-credentials&amp;#34;
keychain: &amp;#34;/Users/junyi/Library/Keychains/login.keychain-db&amp;#34;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;然后试了下：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;security find-generic-password -s &lt;span style="color:#5af78e"&gt;&amp;#34;Claude Code-credentials&amp;#34;&lt;/span&gt; -a &lt;span style="color:#5af78e"&gt;&amp;#34;junyi&amp;#34;&lt;/span&gt; -w
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;终端里能输出 token，tmux 里报错。问题在这。&lt;/p&gt;
&lt;/details&gt;
&lt;h2 id="为什么" &gt;
&lt;div&gt;
&lt;a href="#%e4%b8%ba%e4%bb%80%e4%b9%88"&gt;
#
&lt;/a&gt;
为什么
&lt;/div&gt;
&lt;/h2&gt;
&lt;p&gt;macOS 的 Keychain 访问绑在 &lt;strong&gt;Security Session&lt;/strong&gt; 上，底层对应的是 Bootstrap Namespace。你开终端窗口的时候，进程会挂到当前用户的 &lt;strong&gt;Aqua session&lt;/strong&gt;，Keychain 就是通过这个 session 解锁的。&lt;/p&gt;
&lt;p&gt;但 &lt;code&gt;tmux server&lt;/code&gt; 是个常驻后台进程，第一次 &lt;code&gt;tmux new&lt;/code&gt; 的时候启动，之后就一直活着。你后面 &lt;code&gt;tmux attach&lt;/code&gt; 的时候，tmux server fork 出来的 shell 继承的是 &lt;strong&gt;server 启动时的那个旧 session&lt;/strong&gt;，不是你当前的 Aqua session。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;正常终端:
Ghostty → fork shell → 继承 Aqua session → ✅ 能读 Keychain
tmux:
Ghostty → attach → tmux server (旧进程)
→ fork shell → 继承旧 session
→ ❌ 读不了 Keychain
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;pbcopy&lt;/code&gt;/&lt;code&gt;pbpaste&lt;/code&gt; 在 tmux 里挂掉也是这个原因。&lt;/p&gt;
&lt;h2 id="修复" &gt;
&lt;div&gt;
&lt;a href="#%e4%bf%ae%e5%a4%8d"&gt;
#
&lt;/a&gt;
修复
&lt;/div&gt;
&lt;/h2&gt;
&lt;p&gt;装 &lt;code&gt;reattach-to-user-namespace&lt;/code&gt;，让 tmux 里的进程重新接入当前用户的 Aqua session：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;brew install reattach-to-user-namespace
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;~/.tmux.conf&lt;/code&gt; 加一行：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;set-option -g default-command &lt;span style="color:#5af78e"&gt;&amp;#34;reattach-to-user-namespace -l &lt;/span&gt;&lt;span style="color:#5af78e"&gt;${&lt;/span&gt;&lt;span style="color:#ff5c57"&gt;SHELL&lt;/span&gt;&lt;span style="color:#5af78e"&gt;}&lt;/span&gt;&lt;span style="color:#5af78e"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;然后&lt;strong&gt;杀掉整个 tmux server 重开&lt;/strong&gt;。kill-session 没用，新 window 也没用，都还是从旧 server fork 的：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;tmux kill-server
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;tmux new -s main
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;验证：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;security find-generic-password -s &lt;span style="color:#5af78e"&gt;&amp;#34;Claude Code-credentials&amp;#34;&lt;/span&gt; -a &lt;span style="color:#5af78e"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#ff5c57"&gt;$USER&lt;/span&gt;&lt;span style="color:#5af78e"&gt;&amp;#34;&lt;/span&gt; -w 2&amp;gt;&amp;amp;&lt;span style="color:#ff9f43"&gt;1&lt;/span&gt; | head -c &lt;span style="color:#ff9f43"&gt;50&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;能输出 token 就说明通了。&lt;/p&gt;
&lt;h2 id="顺带一提" &gt;
&lt;div&gt;
&lt;a href="#%e9%a1%ba%e5%b8%a6%e4%b8%80%e6%8f%90"&gt;
#
&lt;/a&gt;
顺带一提
&lt;/div&gt;
&lt;/h2&gt;
&lt;p&gt;tmux 里这些问题多半也是同一个原因：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pbcopy&lt;/code&gt;/&lt;code&gt;pbpaste&lt;/code&gt; 不工作&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ssh-agent&lt;/code&gt; 访问不了&lt;/li&gt;
&lt;li&gt;&lt;code&gt;osascript&lt;/code&gt; 执行失败&lt;/li&gt;
&lt;li&gt;&lt;code&gt;gh auth&lt;/code&gt;、&lt;code&gt;op&lt;/code&gt; 等 CLI 工具认证挂了&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;都是 &lt;code&gt;reattach-to-user-namespace&lt;/code&gt; 一把搞定。&lt;/p&gt;</content:encoded><category>Linux</category><category>tmux</category><category>AI Systems</category><category>Keychain</category><category>Security Session</category><guid isPermaLink="true">https://www.junyi.dev/posts/tmux-keychain/</guid></item><item><title>Agent Mesh：聊聊我对多 AI 协同工作流的一些想法</title><link>https://www.junyi.dev/posts/agent-mesh/</link><pubDate>Wed, 11 Mar 2026 04:29:00 +0800</pubDate><author>junyi.h@comp.nus.edu.sg (Junyi Hou)</author><description>
现在 AI 编程助手越来越多，从聊天框一路卷到了跑在命令行里的 Agent。用了一圈下来，我最大的感受是：没有哪个 Agent 能在所有方面都做到最好。
我日常主要在用 Claude Code、Google Gemini CLI 和 Moonshot Kimi Code。一开始也是把它们当独立工具用，后来发现这样太浪费了，因为它们各有各的长板，如果让它们根据各自的优势互相配合、互相委派任务，效果会好很多。我把这个思路叫做 Agent Mesh。</description><content:encoded>&lt;p&gt;现在 AI 编程助手越来越多，从聊天框一路卷到了跑在命令行里的 Agent。用了一圈下来，我最大的感受是：&lt;strong&gt;没有哪个 Agent 能在所有方面都做到最好。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我日常主要在用 &lt;strong&gt;Claude Code&lt;/strong&gt;、&lt;strong&gt;Google Gemini CLI&lt;/strong&gt; 和 &lt;strong&gt;Moonshot Kimi Code&lt;/strong&gt;。一开始也是把它们当独立工具用，后来发现这样太浪费了，因为它们各有各的长板，如果让它们根据各自的优势互相配合、互相委派任务，效果会好很多。我把这个思路叫做 &lt;strong&gt;Agent Mesh&lt;/strong&gt;。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="核心角色我的观察和真实体验" &gt;
&lt;div&gt;
&lt;a href="#%e6%a0%b8%e5%bf%83%e8%a7%92%e8%89%b2%e6%88%91%e7%9a%84%e8%a7%82%e5%af%9f%e5%92%8c%e7%9c%9f%e5%ae%9e%e4%bd%93%e9%aa%8c"&gt;
#
&lt;/a&gt;
核心角色：我的观察和真实体验
&lt;/div&gt;
&lt;/h2&gt;
&lt;h3 id="claude-code规划与执行" &gt;
&lt;div&gt;
&lt;a href="#claude-code%e8%a7%84%e5%88%92%e4%b8%8e%e6%89%a7%e8%a1%8c"&gt;
##
&lt;/a&gt;
Claude Code：规划与执行
&lt;/div&gt;
&lt;/h3&gt;
&lt;p&gt;我主要用 Claude Code 创建项目和重构代码。它能遍历文件、运行 bash、修复编译错误，接着执行下一步。我已经在所有服务器上安装了它。短板是幻觉，尤其是在生成大段文档、或者引用它没吃透的大型系统时，它会编东西出来。&lt;/p&gt;
&lt;h3 id="gemini-cli大数据吞吐者与幻觉检查员" &gt;
&lt;div&gt;
&lt;a href="#gemini-cli%e5%a4%a7%e6%95%b0%e6%8d%ae%e5%90%9e%e5%90%90%e8%80%85%e4%b8%8e%e5%b9%bb%e8%a7%89%e6%a3%80%e6%9f%a5%e5%91%98"&gt;
##
&lt;/a&gt;
Gemini CLI：大数据吞吐者与“幻觉检查员”
&lt;/div&gt;
&lt;/h3&gt;
&lt;p&gt;Gemini 最大的优势是那个 200 万 Token 的上下文窗口。实测下来，它真的能一口气吞掉整个仓库、很长很长的错误日志，我试过几百 K 的那种。正因为它能把整个宏观上下文都装进脑子里，它天然就是比较好的审计者。当 Claude Code 生成了一个出色但可能存在幻觉的代码方案时，可以调用 Gemini 进行大规模跨文件验证。&lt;/p&gt;
&lt;h3 id="kimi-code深度推理者与数据挖掘者待评估" &gt;
&lt;div&gt;
&lt;a href="#kimi-code%e6%b7%b1%e5%ba%a6%e6%8e%a8%e7%90%86%e8%80%85%e4%b8%8e%e6%95%b0%e6%8d%ae%e6%8c%96%e6%8e%98%e8%80%85%e5%be%85%e8%af%84%e4%bc%b0"&gt;
##
&lt;/a&gt;
Kimi Code：深度推理者与数据挖掘者，待评估
&lt;/div&gt;
&lt;/h3&gt;
&lt;p&gt;Kimi Code 走的是一条完全不同的路。它底层是 K2.5 的&amp;quot;长思考&amp;quot;模型 k0，走显式思维链，路子类似 OpenAI o1，再加上&amp;quot;Agentic Swarm&amp;quot;的玩法，理论上可以拉起一堆子 Agent 去网上多跳检索文档，或者死磕那种深度嵌套的算法难题。不过我得打个问号：Claude 和 Gemini 我是每天在用的，它们的能力边界我心里有数，但 Kimi 在&amp;quot;数据挖掘&amp;quot;和&amp;quot;深度推理&amp;quot;上到底比其他两个强多少，说实话我目前主要是看它架构上的宣称，还没做过严格的对比评估。所以目前它在这块只能说占了一个理论上的生态位。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="我理想中的-agent-mesh-长什么样" &gt;
&lt;div&gt;
&lt;a href="#%e6%88%91%e7%90%86%e6%83%b3%e4%b8%ad%e7%9a%84-agent-mesh-%e9%95%bf%e4%bb%80%e4%b9%88%e6%a0%b7"&gt;
#
&lt;/a&gt;
我理想中的 Agent Mesh 长什么样
&lt;/div&gt;
&lt;/h2&gt;
&lt;p&gt;&lt;img src="https://www.junyi.dev/posts/agent-mesh/diagram.svg" alt="Agent Mesh 工作流示意图"&gt;&lt;/p&gt;
&lt;p&gt;大部分时候 Claude Code 是干活的主力，代码基本都是它在写。但它遇到吃不下的大上下文时，比如我要重构一个模块但不知道会影响那 50 万行老代码的哪些地方，就丢给 Gemini，让它把整个仓库读一遍，告诉我影响范围。&lt;/p&gt;
&lt;p&gt;反过来，Claude 执行完一份重构计划，我也会让 Gemini 拿着去跟实际项目文件交叉比对，看看有没有幻觉或坏掉的依赖。如果 Gemini 在审计过程中碰到特别硬的算法问题，还可以转手给 Kimi Code 去长考。&lt;/p&gt;
&lt;p&gt;Kimi 想完之后也不直接改文件，而是把结论交回给 Claude Code，让 Claude 去落地执行。整个流程就是这样一个环：各自干各自最擅长的，结果互相流转。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="隐形因素agent-架构-vs-基座模型" &gt;
&lt;div&gt;
&lt;a href="#%e9%9a%90%e5%bd%a2%e5%9b%a0%e7%b4%a0agent-%e6%9e%b6%e6%9e%84-vs-%e5%9f%ba%e5%ba%a7%e6%a8%a1%e5%9e%8b"&gt;
#
&lt;/a&gt;
隐形因素：Agent 架构 vs 基座模型
&lt;/div&gt;
&lt;/h2&gt;
&lt;p&gt;组合这些 Agent 时，我会同时看&lt;strong&gt;基座模型和工具的执行流程&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;例如，Anthropic 的 Claude 3.5 Sonnet 可以在多个环境中使用：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;作为 &lt;strong&gt;VS Code&lt;/strong&gt; 里的标准聊天助手。&lt;/li&gt;
&lt;li&gt;作为 &lt;strong&gt;Cursor IDE&lt;/strong&gt; 内部的驱动引擎。&lt;/li&gt;
&lt;li&gt;通过 &lt;strong&gt;OpenHands / Open Code&lt;/strong&gt; 这样的开源 Agent 框架。&lt;/li&gt;
&lt;li&gt;原生通过 &lt;strong&gt;Claude Code CLI&lt;/strong&gt; 使用。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;在我用过的工程工作流里，即使使用相同模型，不同工具的执行体验也有差别。Claude Code 的文件操作、命令执行和错误恢复配合起来更顺手。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="社区里的类似探索" &gt;
&lt;div&gt;
&lt;a href="#%e7%a4%be%e5%8c%ba%e9%87%8c%e7%9a%84%e7%b1%bb%e4%bc%bc%e6%8e%a2%e7%b4%a2"&gt;
#
&lt;/a&gt;
社区里的类似探索
&lt;/div&gt;
&lt;/h2&gt;
&lt;p&gt;写完这篇之后，我发现社区里已经有人在做类似的事情。&lt;strong&gt;&lt;a href="https://github.com/humania-org/humanize" target="_blank" rel="noopener noreferrer"&gt;Humanize&lt;/a&gt;&lt;/strong&gt; 是一个 Claude Code 插件，它实现了一个叫 &lt;strong&gt;RLCR&lt;/strong&gt; 的工作流，全称 Ralph-Loop with Codex Review：Claude 负责写代码，Codex 独立审查，发现问题就打回去重做，循环往复直到通过为止。核心思路和 Agent Mesh 一样，让架构上不同的 AI 各司其职，通过 work-feedback loop 不断迭代，而不是指望一个模型一次搞定所有事。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="写在最后" &gt;
&lt;div&gt;
&lt;a href="#%e5%86%99%e5%9c%a8%e6%9c%80%e5%90%8e"&gt;
#
&lt;/a&gt;
写在最后
&lt;/div&gt;
&lt;/h2&gt;
&lt;p&gt;除了给同一个模型分配不同角色，我也会考虑模型本身和 Agent 工作方式的差异。&lt;strong&gt;&lt;a href="https://github.com/Xtra-Computing/LLM-DNA" target="_blank" rel="noopener noreferrer"&gt;LLM-DNA&lt;/a&gt;&lt;/strong&gt; 是一个分析语言模型之间进化关系和功能差异的研究框架，已在 &lt;a href="https://openreview.net/forum?id=UIxHaAqFqQ" target="_blank" rel="noopener noreferrer"&gt;ICLR'26&lt;/a&gt; 以 Oral 形式发表，可以用来观察模型之间的差异。&lt;/p&gt;
&lt;p&gt;在我的项目里，&lt;strong&gt;Claude Code 和 Gemini CLI 配合起来最顺手&lt;/strong&gt;：Claude 负责写代码和执行，Gemini 负责结合整个仓库检查改动。&lt;/p&gt;</content:encoded><category>AI Systems</category><category>LLM Inference</category><category>Tech</category><guid isPermaLink="true">https://www.junyi.dev/posts/agent-mesh/</guid></item><item><title>用 nerdctl + Tailscale Sidecar 让容器流量走 Exit Node：踩坑全记录</title><link>https://www.junyi.dev/posts/nerdctl-tailscale-sidecar-pitfalls/</link><pubDate>Fri, 06 Mar 2026 00:00:00 +0800</pubDate><author>junyi.h@comp.nus.edu.sg (Junyi Hou)</author><description>
在容器化部署中，有时候我们需要让容器的所有出站流量通过特定的网络出口。Tailscale 的 sidecar 模式可以做到这一点：用一个 Tailscale 容器作为 sidecar，其他容器共享它的网络命名空间，流量通过 WireGuard 隧道经由远端 exit node 出去。
这个方案在 Docker Compose 下很成熟，但迁移到 nerdctl（containerd）时，我踩了一连串的坑。记录下来，希望能帮后来人少走弯路。</description><content:encoded>&lt;p&gt;在容器化部署中，有时候我们需要让容器的所有出站流量通过特定的网络出口。Tailscale 的 sidecar 模式可以做到这一点：用一个 Tailscale 容器作为 sidecar，其他容器共享它的网络命名空间，流量通过 WireGuard 隧道经由远端 exit node 出去。&lt;/p&gt;
&lt;p&gt;这个方案在 Docker Compose 下很成熟，但迁移到 nerdctl（containerd）时，我踩了一连串的坑。记录下来，希望能帮后来人少走弯路。&lt;/p&gt;
&lt;h2 id="最终目标" &gt;
&lt;div&gt;
&lt;a href="#%e6%9c%80%e7%bb%88%e7%9b%ae%e6%a0%87"&gt;
#
&lt;/a&gt;
最终目标
&lt;/div&gt;
&lt;/h2&gt;
&lt;p&gt;让任意容器通过 Tailscale sidecar 接入 tailnet，所有出站流量经由远程 exit node 转发。架构如下：&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.junyi.dev/posts/nerdctl-tailscale-sidecar-pitfalls/architecture.svg" alt="nerdctl &amp;#43; Tailscale Sidecar 架构图"&gt;&lt;/p&gt;
&lt;p&gt;为了方便验证网络是否通畅，我用 &lt;code&gt;nicolaka/netshoot&lt;/code&gt; 作为测试容器——它自带 curl、dig、tcpdump、iperf 等一整套网络排查工具，比临时装工具方便得多。&lt;/p&gt;
&lt;h2 id="坑-1nerdctl-compose-不支持-network_mode-servicexxx" &gt;
&lt;div&gt;
&lt;a href="#%e5%9d%91-1nerdctl-compose-%e4%b8%8d%e6%94%af%e6%8c%81-network_mode-servicexxx"&gt;
#
&lt;/a&gt;
坑 1：nerdctl compose 不支持 &lt;code&gt;network_mode: service:xxx&lt;/code&gt;
&lt;/div&gt;
&lt;/h2&gt;
&lt;p&gt;Docker Compose 支持 &lt;code&gt;network_mode: service:&amp;lt;service_name&amp;gt;&lt;/code&gt; 语法，它会自动把 service 名解析为对应容器的网络命名空间。但 nerdctl compose 没有实现这个翻译层，直接报错：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;unsupported network_mode: service:tailscale-sidecar
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;解法：&lt;/strong&gt; 改用 &lt;code&gt;network_mode: container:&amp;lt;container_name&amp;gt;&lt;/code&gt;，并通过 &lt;code&gt;container_name&lt;/code&gt; 字段固定 sidecar 容器的名称。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff6ac1"&gt;services&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;tailscale-sidecar&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;container_name&lt;/span&gt;: tailscale-sidecar &lt;span style="color:#78787e"&gt;# 固定名称&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#78787e"&gt;# ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;netshoot&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;network_mode&lt;/span&gt;: container:tailscale-sidecar &lt;span style="color:#78787e"&gt;# 用容器名而非服务名&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;如果不固定 &lt;code&gt;container_name&lt;/code&gt;，nerdctl compose 会自动生成类似 &lt;code&gt;blockchain-tailscale-sidecar-1&lt;/code&gt; 的名称，导致引用不匹配。&lt;/p&gt;
&lt;h2 id="坑-2tailscale-acl-中未定义-tag" &gt;
&lt;div&gt;
&lt;a href="#%e5%9d%91-2tailscale-acl-%e4%b8%ad%e6%9c%aa%e5%ae%9a%e4%b9%89-tag"&gt;
#
&lt;/a&gt;
坑 2：Tailscale ACL 中未定义 tag
&lt;/div&gt;
&lt;/h2&gt;
&lt;p&gt;启动后 Tailscale 容器立即退出，日志显示：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;Received error: requested tags [tag:container] are invalid or not permitted
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;解法：&lt;/strong&gt; 在 Tailscale 管理后台的 ACL 策略中添加 tag 定义，并在生成 auth key 时勾选对应 tag：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#5af78e"&gt;&amp;#34;tagOwners&amp;#34;&lt;/span&gt;&lt;span style="color:#ff5c57"&gt;:&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;&amp;#34;tag:container&amp;#34;&lt;/span&gt;: [&lt;span style="color:#5af78e"&gt;&amp;#34;autogroup:admin&amp;#34;&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="坑-3缺少-sys_module-capability" &gt;
&lt;div&gt;
&lt;a href="#%e5%9d%91-3%e7%bc%ba%e5%b0%91-sys_module-capability"&gt;
#
&lt;/a&gt;
坑 3：缺少 &lt;code&gt;SYS_MODULE&lt;/code&gt; capability
&lt;/div&gt;
&lt;/h2&gt;
&lt;p&gt;这个坑最隐蔽。容器能正常加入 tailnet，&lt;code&gt;tailscale ping&lt;/code&gt; 也通，但开启 exit node 后 TCP 流量完全不通，只有 ICMP 能过。&lt;/p&gt;
&lt;p&gt;原因是内核模式（&lt;code&gt;TS_USERSPACE=false&lt;/code&gt;）需要加载 &lt;code&gt;xt_mark&lt;/code&gt;、&lt;code&gt;nf_nat&lt;/code&gt; 等内核模块来设置 iptables 转发规则。缺少 &lt;code&gt;SYS_MODULE&lt;/code&gt; capability 时，这些模块无法加载，iptables 规则写入但不生效，表现为 ICMP 正常而 TCP 断流。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解法：&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff6ac1"&gt;cap_add&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - net_admin
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - SYS_MODULE &lt;span style="color:#78787e"&gt;# 必须加上&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="坑-4exit-node-模式下-dns-失败" &gt;
&lt;div&gt;
&lt;a href="#%e5%9d%91-4exit-node-%e6%a8%a1%e5%bc%8f%e4%b8%8b-dns-%e5%a4%b1%e8%b4%a5"&gt;
#
&lt;/a&gt;
坑 4：Exit Node 模式下 DNS 失败
&lt;/div&gt;
&lt;/h2&gt;
&lt;p&gt;开启 exit node 后，容器所有出站流量都走 WireGuard 隧道到远端 exit node 再出去。但 &lt;code&gt;/etc/resolv.conf&lt;/code&gt; 里的 DNS 服务器仍然是本地地址（如 &lt;code&gt;10.0.2.3&lt;/code&gt;），这些地址从 exit node 那端是不可达的，DNS 查询自然失败。&lt;/p&gt;
&lt;p&gt;症状是 &lt;code&gt;curl ifconfig.me&lt;/code&gt; 报 &lt;code&gt;Could not resolve host&lt;/code&gt;，而直接用 IP 访问则卡住或返回异常。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解法：&lt;/strong&gt; 让 Tailscale 接管 DNS，查询也走隧道：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff6ac1"&gt;environment&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - TS_ACCEPT_DNS=true
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="最终可用的-composeyaml" &gt;
&lt;div&gt;
&lt;a href="#%e6%9c%80%e7%bb%88%e5%8f%af%e7%94%a8%e7%9a%84-composeyaml"&gt;
#
&lt;/a&gt;
最终可用的 compose.yaml
&lt;/div&gt;
&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff6ac1"&gt;services&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;tailscale-sidecar&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;image&lt;/span&gt;: tailscale/tailscale:latest
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;container_name&lt;/span&gt;: tailscale-sidecar
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;hostname&lt;/span&gt;: tailscale-sidecar
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;environment&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - TS_AUTHKEY=${TS_AUTHKEY}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - TS_EXTRA_ARGS=--advertise-tags=tag:container --exit-node=${TS_EXITNODE}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - TS_STATE_DIR=/var/lib/tailscale
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - TS_USERSPACE=false
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - TS_ACCEPT_DNS=true
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;volumes&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - ${PWD}/tailscale-sidecar/state:/var/lib/tailscale
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;devices&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - /dev/net/tun:/dev/net/tun
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;cap_add&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - net_admin
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - SYS_MODULE
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;restart&lt;/span&gt;: unless-stopped
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;netshoot&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;image&lt;/span&gt;: nicolaka/netshoot
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;depends_on&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - tailscale-sidecar
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;network_mode&lt;/span&gt;: container:tailscale-sidecar
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;command&lt;/span&gt;: sleep infinity
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;需要在同目录下准备 &lt;code&gt;.env&lt;/code&gt; 文件：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;TS_AUTHKEY=tskey-auth-xxxxx
TS_EXITNODE=100.x.x.x
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="验证方法" &gt;
&lt;div&gt;
&lt;a href="#%e9%aa%8c%e8%af%81%e6%96%b9%e6%b3%95"&gt;
#
&lt;/a&gt;
验证方法
&lt;/div&gt;
&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;nicolaka/netshoot&lt;/code&gt; 自带丰富的网络工具，非常适合验证：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#78787e"&gt;# 检查出口 IP，确认流量是否走了 exit node&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;nerdctl &lt;span style="color:#ff5c57"&gt;exec&lt;/span&gt; -it netshoot curl ifconfig.me
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#78787e"&gt;# DNS 解析测试&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;nerdctl &lt;span style="color:#ff5c57"&gt;exec&lt;/span&gt; -it netshoot dig google.com
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#78787e"&gt;# 查看路由表&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;nerdctl &lt;span style="color:#ff5c57"&gt;exec&lt;/span&gt; -it netshoot ip route show table all
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#78787e"&gt;# 抓包排查&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;nerdctl &lt;span style="color:#ff5c57"&gt;exec&lt;/span&gt; -it netshoot tcpdump -i any -n
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#78787e"&gt;# Tailscale 侧的状态检查&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;nerdctl &lt;span style="color:#ff5c57"&gt;exec&lt;/span&gt; -it tailscale-sidecar tailscale status
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;nerdctl &lt;span style="color:#ff5c57"&gt;exec&lt;/span&gt; -it tailscale-sidecar tailscale exit-node status
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;nerdctl &lt;span style="color:#ff5c57"&gt;exec&lt;/span&gt; -it tailscale-sidecar tailscale ping &amp;lt;exit-node-ip&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="排查思路总结" &gt;
&lt;div&gt;
&lt;a href="#%e6%8e%92%e6%9f%a5%e6%80%9d%e8%b7%af%e6%80%bb%e7%bb%93"&gt;
#
&lt;/a&gt;
排查思路总结
&lt;/div&gt;
&lt;/h2&gt;
&lt;p&gt;这次排查的过程本身也值得回顾。面对&amp;quot;exit node 不工作&amp;quot;这样的复合问题，有效的方法是逐层剥离：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;先确认隧道连通性：&lt;/strong&gt; &lt;code&gt;tailscale ping&lt;/code&gt; 验证 WireGuard 隧道本身是否通。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;对比实验：&lt;/strong&gt; 关掉 exit node 测试，确认问题出在 exit node 还是基础网络。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;区分协议：&lt;/strong&gt; ICMP 通但 TCP 不通，指向 iptables/内核模块问题。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;区分 DNS 和连通性：&lt;/strong&gt; 用 IP 直连绕过 DNS，分别定位。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;对比已知可用的配置：&lt;/strong&gt; 发现之前能用的命令多了 &lt;code&gt;SYS_MODULE&lt;/code&gt;，一下锁定问题。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;每一步缩小范围，避免在错误的方向上浪费时间。&lt;/p&gt;
&lt;h2 id="写在最后" &gt;
&lt;div&gt;
&lt;a href="#%e5%86%99%e5%9c%a8%e6%9c%80%e5%90%8e"&gt;
#
&lt;/a&gt;
写在最后
&lt;/div&gt;
&lt;/h2&gt;
&lt;p&gt;nerdctl compose 作为 Docker Compose 的替代方案已经很成熟了，但在细节兼容性上仍有差距。&lt;code&gt;network_mode: service:&lt;/code&gt; 的缺失、容器名的自动生成规则不同，这些都是迁移时容易踩到的坑。而 Tailscale 在容器中使用 exit node 时，&lt;code&gt;SYS_MODULE&lt;/code&gt; 和 &lt;code&gt;TS_ACCEPT_DNS&lt;/code&gt; 是两个容易被忽略但又不可或缺的配置。希望这篇记录能帮到遇到同样问题的人。&lt;/p&gt;</content:encoded><category>Containers</category><category>Networking</category><category>Tech</category><guid isPermaLink="true">https://www.junyi.dev/posts/nerdctl-tailscale-sidecar-pitfalls/</guid></item><item><title>从像素到笔触：跨越十年的凝视</title><link>https://www.junyi.dev/posts/into-the-modern/</link><pubDate>Wed, 07 Jan 2026 15:16:00 +0800</pubDate><author>junyi.h@comp.nus.edu.sg (Junyi Hou)</author><description>
INTO THE MODERN 2026 年初，我在新加坡 “INTO THE MODERN” 印象派画展中，第一次站在莫奈Monet的原作前。</description><content:encoded>&lt;div style="display: flex; gap: 1rem; flex-wrap: wrap; justify-content: center;"&gt;
&lt;figure style="flex: 1; min-width: 280px; max-width: 280px; margin: 0;"&gt;
&lt;img src="https://www.junyi.dev/posts/into-the-modern/IMG_1797.jpg" alt="into-the-modern" style="width: 280px; border-radius: 10px;" /&gt;
&lt;figcaption style="text-align: center;"&gt;INTO THE MODERN&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/div&gt;
&lt;p&gt;2026 年初，我在新加坡 “INTO THE MODERN” 印象派画展中，第一次站在&lt;ruby&gt;莫奈&lt;rt&gt;Monet&lt;/rt&gt;&lt;/ruby&gt;的原作前。&lt;/p&gt;
&lt;p&gt;颜料层层堆叠，笔触清晰可见，仿佛凝固了百年前某个晨光熹微或日暮西沉的瞬间。那一刻，我想起整整十年前（2016 年）的一个寂静的周日，我坐在电脑前，对着屏幕一页页翻找图片，为美术课挑选自己要介绍的画，主题是《从传统走向现代》。那时的我，连“&lt;ruby&gt;印象派&lt;rt&gt;Impressionism&lt;/rt&gt;&lt;/ruby&gt;”三个字都还陌生，更无法想象未来某天，竟能与这些曾只存在于像素中的杰作面对面。&lt;/p&gt;
&lt;p&gt;展览虽未展出莫奈最著名的作品，却意外地完整：&lt;ruby&gt;莫奈&lt;rt&gt;Monet&lt;/rt&gt;&lt;/ruby&gt;、&lt;ruby&gt;雷诺阿&lt;rt&gt;Renoir&lt;/rt&gt;&lt;/ruby&gt;、&lt;ruby&gt;西斯莱&lt;rt&gt;Sisley&lt;/rt&gt;&lt;/ruby&gt;、&lt;ruby&gt;塞尚&lt;rt&gt;Cézanne&lt;/rt&gt;&lt;/ruby&gt;。这四位曾同窗于&lt;ruby&gt;格莱尔画室&lt;rt&gt;Gleyre&amp;rsquo;s Studio&lt;/rt&gt;&lt;/ruby&gt;的青年，如今以画作重聚。莫奈的画，颜料厚重如浮雕，可以清楚地看到笔触堆叠成厚厚的一团。雷诺阿用柔光晕染人脸，明亮却朦胧。西斯莱的风景静谧而克制。而塞尚，我对他没有太多的印象&amp;hellip;&lt;/p&gt;
&lt;div style="display: flex; gap: 1rem; flex-wrap: wrap;"&gt;
&lt;figure style="flex: 1; min-width: 280px; margin: 0;"&gt;
&lt;img src="https://www.junyi.dev/posts/into-the-modern/IMG_1785.jpg" alt="haystacks" style="width: 280px; border-radius: 10px;" /&gt;
&lt;figcaption&gt;&lt;em&gt;Haystacks&lt;/em&gt; 干草堆, 1890 - Claude Monet&lt;br/&gt;（颜料厚重如浮雕，可以清楚地看到笔触堆叠成厚厚的一团）&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/div&gt;
&lt;p&gt;这次展览最令我惊喜的是&lt;ruby&gt;保罗·西涅克&lt;rt&gt;Paul Signac&lt;/rt&gt;&lt;/ruby&gt;，他的画，色彩上保留了印象派的味道，但技法上却截然不同。画布上没有挥洒的笔触，取而代之的是无数细密的色点。后来我才知道，保罗·西涅克是“&lt;a href="https://en.wikipedia.org/wiki/Pointillism" target="_blank" rel="noopener noreferrer"&gt;点彩派&lt;/a&gt;”的代表画家。&lt;/p&gt;
&lt;div style="display: flex; gap: 1rem; flex-wrap: wrap;"&gt;
&lt;figure style="flex: 1; min-width: 280px; margin: 0;"&gt;
&lt;img src="https://www.junyi.dev/posts/into-the-modern/IMG_1763.jpg" alt="View of the Seine at Herblay 1889, Paul Signac" style="width: 280px; border-radius: 10px;" /&gt;
&lt;figcaption&gt;&lt;em&gt;View of the Seine at Herblay&lt;/em&gt; 埃尔布莱的塞纳河景, 1889 - Paul Signac&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure style="flex: 1; min-width: 280px; margin: 0;"&gt;
&lt;img src="https://www.junyi.dev/posts/into-the-modern/IMG_1759.jpg" alt="IMG_1759" style="width: 280px; border-radius: 10px;" /&gt;
&lt;figcaption&gt;&lt;em&gt;Port of Saint-Cast&lt;/em&gt; 圣卡斯特港, 1889 - Paul Signac&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/div&gt;
&lt;p&gt;站在展厅中，记忆倒流回那个试图在 PPT 里讲清楚 “从传统走向现代” 的高中生。那时的我，仅仅通过维基百科和一些图片来了解这些画作，并且轻易为每个流派贴上标签：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;新古典主义追求严谨构图与科学透视，后期却流于空洞的教条形式；&lt;/li&gt;
&lt;li&gt;浪漫主义在神话与激情中寻找崇高，最终却滑向情绪的浮夸；&lt;/li&gt;
&lt;li&gt;写实主义将目光投向平民与日常，却仍未突破传统叙事的框架；&lt;/li&gt;
&lt;li&gt;印象派挣脱画室，捕捉瞬息光色，却在对视觉印象的迷恋中，几乎遗忘了形体的结构；&lt;/li&gt;
&lt;li&gt;后印象派彻底转身向内：梵高燃烧内心的火焰，高更逃向象征的彼岸。他们不再追求 “世界看起来如何”，而转向 “我如何感受世界”。&lt;/li&gt;
&lt;li&gt;&amp;hellip;&amp;hellip;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;美的探索从未停歇。十年过去，我不再只是隔着屏幕，阅读那段被平滑叙述后的历史。如今站在原作前，我真正感受到了那场混乱、激烈、充满不确定性的创造过程。除了色彩与笔触的震撼，更有一种深沉的历史回响。&lt;strong&gt;与其说我在和艺术家对话，不如说我在和自己对话。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;艺术的现代之路，本就是在理性与感性、秩序与自由之间，不断裂变与重组。&lt;/p&gt;
&lt;p&gt;画布背后，都是一个个活生生的人。他们在质疑与孤独中，依然选择相信自己的眼睛。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“There really is no such thing as Art. There are only artists.”&lt;br&gt;
&amp;mdash; E. H. Gombrich, &lt;em&gt;The Story of Art&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;</content:encoded><category>Personal</category><category>Life</category><guid isPermaLink="true">https://www.junyi.dev/posts/into-the-modern/</guid></item><item><title>用 Dev Container 配置 VS Code + LaTeX 环境</title><link>https://www.junyi.dev/posts/latex-dev-container/</link><pubDate>Tue, 23 Sep 2025 13:02:12 +0800</pubDate><author>junyi.h@comp.nus.edu.sg (Junyi Hou)</author><description>
这套配置用 Dev Container1 管理 LaTeX 环境，用 Git 同步项目。拉取仓库后，在容器中打开即可编译。</description><content:encoded>&lt;p&gt;这套配置用 Dev Container&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt; 管理 LaTeX 环境，用 Git 同步项目。拉取仓库后，在容器中打开即可编译。&lt;/p&gt;
&lt;h2 id="安装编辑器扩展" &gt;
&lt;div&gt;
&lt;a href="#%e5%ae%89%e8%a3%85%e7%bc%96%e8%be%91%e5%99%a8%e6%89%a9%e5%b1%95"&gt;
#
&lt;/a&gt;
安装编辑器扩展
&lt;/div&gt;
&lt;/h2&gt;
&lt;p&gt;在 &lt;strong&gt;Cursor&lt;/strong&gt; 或 &lt;strong&gt;VSCode&lt;/strong&gt; 中，安装官方扩展 &lt;code&gt;Dev Containers&lt;/code&gt;&lt;/p&gt;
&lt;div style="display: flex; gap: 20px; margin: 20px 0;"&gt;
&lt;figure style="flex: 1; text-align: center;"&gt;
&lt;img src="dev-container-cursor.png" alt="Cursor 中的 Dev Container" style="width: 100%; height: auto; border-radius: 8px;"&gt;
&lt;figcaption&gt;Cursor 中的 Dev Container&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure style="flex: 1; text-align: center;"&gt;
&lt;img src="dev-container-vscode.png" alt="VS Code 中的 Dev Container" style="width: 100%; height: auto; border-radius: 8px;"&gt;
&lt;figcaption&gt;VS Code 中的 Dev Container&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/div&gt;
&lt;h2 id="三个步骤从-0-到能写-latex" &gt;
&lt;div&gt;
&lt;a href="#%e4%b8%89%e4%b8%aa%e6%ad%a5%e9%aa%a4%e4%bb%8e-0-%e5%88%b0%e8%83%bd%e5%86%99-latex"&gt;
#
&lt;/a&gt;
三个步骤，从 0 到能写 LaTeX
&lt;/div&gt;
&lt;/h2&gt;
&lt;p&gt;假设你已有一个 LaTeX 项目文件夹（新建空文件夹也可以），跟着做就能用：&lt;/p&gt;
&lt;h3 id="step-1-引入-dev-container-配置" &gt;
&lt;div&gt;
&lt;a href="#step-1-%e5%bc%95%e5%85%a5-dev-container-%e9%85%8d%e7%bd%ae"&gt;
##
&lt;/a&gt;
&lt;strong&gt;Step 1 引入 &lt;a href="https://github.com/paperdebugger-bot/latex-devcontainer" target="_blank" rel="noopener noreferrer"&gt;Dev Container 配置&lt;/a&gt;&lt;/strong&gt;
&lt;/div&gt;
&lt;/h3&gt;
&lt;p&gt;打开项目的终端（VSCode/Cursor 内置终端即可），执行以下命令，将预配置好的容器环境作为「子模块」引入项目（子模块不干扰主项目文件，方便后续更新）：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;git submodule add https://github.com/paperdebugger-bot/latex-devcontainer.git .devcontainer
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;如果你的项目还没初始化 Git（&lt;code&gt;git init&lt;/code&gt;），也可以直接下载配置文件解压到 &lt;code&gt;.devcontainer&lt;/code&gt; 文件夹：&lt;a href="https://github.com/paperdebugger-bot/latex-devcontainer/archive/refs/heads/main.zip" target="_blank" rel="noopener noreferrer"&gt;latex-devcontainer.zip&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="step-2-重载编辑器窗口" &gt;
&lt;div&gt;
&lt;a href="#step-2-%e9%87%8d%e8%bd%bd%e7%bc%96%e8%be%91%e5%99%a8%e7%aa%97%e5%8f%a3"&gt;
##
&lt;/a&gt;
&lt;strong&gt;Step 2 重载编辑器窗口&lt;/strong&gt;
&lt;/div&gt;
&lt;/h3&gt;
&lt;p&gt;按下快捷键 &lt;kbd&gt;Ctrl&lt;/kbd&gt; + &lt;kbd&gt;Shift&lt;/kbd&gt; + &lt;kbd&gt;P&lt;/kbd&gt;（macOS 是 &lt;kbd&gt;⌘&lt;/kbd&gt; + &lt;kbd&gt;⇧&lt;/kbd&gt; + &lt;kbd&gt;P&lt;/kbd&gt;），在弹出的命令面板中输入 &lt;code&gt;Reload Window&lt;/code&gt; 并执行，让编辑器识别新添加的容器配置。&lt;/p&gt;
&lt;h3 id="step-3-启动容器并使用" &gt;
&lt;div&gt;
&lt;a href="#step-3-%e5%90%af%e5%8a%a8%e5%ae%b9%e5%99%a8%e5%b9%b6%e4%bd%bf%e7%94%a8"&gt;
##
&lt;/a&gt;
&lt;strong&gt;Step 3 启动容器并使用&lt;/strong&gt;
&lt;/div&gt;
&lt;/h3&gt;
&lt;p&gt;重载后，编辑器会自动弹出提示「是否在 Dev Container 中打开项目」，点击 &lt;strong&gt;Reopen in Container&lt;/strong&gt;；&lt;/p&gt;
&lt;p&gt;接下来会让你选择容器版本，直接选 &lt;strong&gt;full&lt;/strong&gt; 即可 —— 等待 3-5 分钟（首次构建需下载镜像，后续会快很多），环境就搭建完成了！&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;🎉 完成后你会获得什么？&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;预装 &lt;strong&gt;TeX Live Full 发行版&lt;/strong&gt;（几乎包含所有宏包，不用手动装）；&lt;/li&gt;
&lt;li&gt;自带 &lt;strong&gt;LaTeX Workshop 插件&lt;/strong&gt;（支持实时预览、一键编译）&lt;sup id="fnref:2"&gt;&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref"&gt;2&lt;/a&gt;&lt;/sup&gt;；&lt;/li&gt;
&lt;li&gt;配置好的 &lt;strong&gt;PDF 预览&lt;/strong&gt; 功能；&lt;/li&gt;
&lt;li&gt;干净的终端环境（可直接用 &lt;code&gt;pdflatex&lt;/code&gt; &lt;code&gt;xelatex&lt;/code&gt; 等命令）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="进阶选择不同的容器版本怎么挑" &gt;
&lt;div&gt;
&lt;a href="#%e8%bf%9b%e9%98%b6%e9%80%89%e6%8b%a9%e4%b8%8d%e5%90%8c%e7%9a%84%e5%ae%b9%e5%99%a8%e7%89%88%e6%9c%ac%e6%80%8e%e4%b9%88%e6%8c%91"&gt;
#
&lt;/a&gt;
进阶选择：不同的容器版本怎么挑？
&lt;/div&gt;
&lt;/h2&gt;
&lt;p&gt;上面推荐的 &lt;strong&gt;full&lt;/strong&gt; 版本是「省事优先」，但如果你的项目&lt;em&gt;轻量化&lt;/em&gt;、或想&lt;em&gt;节省磁盘空间&lt;/em&gt;，可以选其他版本。核心区别在于「预装的 TeX 发行版」不同：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;容器版本&lt;/th&gt;
&lt;th&gt;核心 TeX 发行版&lt;/th&gt;
&lt;th&gt;特点&lt;/th&gt;
&lt;th&gt;适合场景&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;full&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;TeX Live Full&lt;/td&gt;
&lt;td&gt;预装较完整的宏包（约 6GB），减少手动安装&lt;/td&gt;
&lt;td&gt;复杂论文、自定义模板、不想处理宏包问题&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;tiny&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;TinyTeX&lt;sup id="fnref:3"&gt;&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref"&gt;3&lt;/a&gt;&lt;/sup&gt;&lt;/td&gt;
&lt;td&gt;轻量版（约 1GB），默认含 200+ 常用宏包&lt;/td&gt;
&lt;td&gt;简单报告、常规学术论文（缺包时需手动 &lt;code&gt;tlmgr install 宏包名&lt;/code&gt;）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;nano&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Tectonic&lt;sup id="fnref:4"&gt;&lt;a href="#fn:4" class="footnote-ref" role="doc-noteref"&gt;4&lt;/a&gt;&lt;/sup&gt;&lt;/td&gt;
&lt;td&gt;零配置引擎，自动处理依赖&lt;/td&gt;
&lt;td&gt;快速写 Demo、极简文档（兼容性略弱，复杂宏包可能不支持）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;💡 &lt;strong&gt;切换方法&lt;/strong&gt;：&lt;br&gt;
在启动容器时，选择对应版本即可；如果已启动容器，可通过 &lt;strong&gt;Ctrl + Shift + P → Dev Containers: Rebuild Container&lt;/strong&gt; 重新选择版本。&lt;/p&gt;
&lt;h2 id="常见问题-qa" &gt;
&lt;div&gt;
&lt;a href="#%e5%b8%b8%e8%a7%81%e9%97%ae%e9%a2%98-qa"&gt;
#
&lt;/a&gt;
常见问题 Q&amp;amp;A
&lt;/div&gt;
&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Q1：构建容器时速度很慢？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A1：&lt;/strong&gt; 首次构建需下载大镜像，建议换国内 Docker 镜像源（比如阿里云、网易云镜像），或在网络较好的环境下操作。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q2：编译中文文档乱码？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A2：&lt;/strong&gt; &lt;code&gt;full&lt;/code&gt; 和 &lt;code&gt;tiny&lt;/code&gt; 版本已预装 &lt;code&gt;xeCJK&lt;/code&gt; 宏包，编译时选择 &lt;code&gt;xelatex&lt;/code&gt; 引擎即可（LaTeX Workshop 已默认配置）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q3：如何更新容器配置？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A3：&lt;/strong&gt; 如果远程仓库的 &lt;code&gt;latex-devcontainer&lt;/code&gt; 有更新，可在项目终端执行：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;git pull --recurse-submodules
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;git submodule update --recursive --remote
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;然后重建容器（&lt;strong&gt;Rebuild Container&lt;/strong&gt;）即可应用更新。&lt;/p&gt;
&lt;p&gt;这套方案我自己用了半年，从写技术报告到学术论文，没再遇到过一次「环境报错」。如果你也厌倦了配置 LaTeX 环境，不妨试试。&lt;/p&gt;
&lt;div class="footnotes" role="doc-endnotes"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;&lt;a href="https://code.visualstudio.com/docs/devcontainers/containers" target="_blank" rel="noopener noreferrer"&gt;Dev Containers 官方文档&lt;/a&gt;：了解容器配置的更多细节&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:2"&gt;
&lt;p&gt;&lt;a href="https://marketplace.visualstudio.com/items?itemName=James-Yu.latex-workshop" target="_blank" rel="noopener noreferrer"&gt;LaTeX Workshop 插件文档&lt;/a&gt;：掌握更多编译、预览技巧&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:3"&gt;
&lt;p&gt;&lt;a href="https://yihui.org/tinytex/" target="_blank" rel="noopener noreferrer"&gt;TinyTeX 官方网站&lt;/a&gt;：轻量 TeX 发行版的使用指南&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:4"&gt;
&lt;p&gt;&lt;a href="https://yiwei.dev/posts/%e5%9f%ba%e4%ba%8etinytex%e7%9a%84%e4%b8%ad%e6%96%87%e6%9c%ac%e5%9c%b0tex%e7%8e%af%e5%a2%83/" target="_blank" rel="noopener noreferrer"&gt;基于 tinytex 的中文本地 TeX 环境&lt;/a&gt;：如何配置 tinytex 环境&amp;#160;&lt;a href="#fnref:4" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</content:encoded><category>LaTex</category><category>Containers</category><category>Development</category><category>Tech</category><guid isPermaLink="true">https://www.junyi.dev/posts/latex-dev-container/</guid></item><item><title>从树莓派到 NUC：玩具的升级</title><link>https://www.junyi.dev/posts/intel-nuc/</link><pubDate>Fri, 05 Sep 2025 13:07:09 +0800</pubDate><author>junyi.h@comp.nus.edu.sg (Junyi Hou)</author><description>
我把日常跑轻量服务的树莓派 4B 换成了 Intel NUC，处理器从 ARM 换成 x86，存储从 TF 卡换成 SSD。</description><content:encoded>&lt;p&gt;我把日常跑轻量服务的树莓派 4B 换成了 Intel NUC，处理器从 ARM 换成 x86，存储从 TF 卡换成 SSD。&lt;/p&gt;
&lt;h2 id="树莓派的功与过" &gt;
&lt;div&gt;
&lt;a href="#%e6%a0%91%e8%8e%93%e6%b4%be%e7%9a%84%e5%8a%9f%e4%b8%8e%e8%bf%87"&gt;
#
&lt;/a&gt;
树莓派的功与过
&lt;/div&gt;
&lt;/h2&gt;
&lt;p&gt;树莓派 4B 用来监控家庭网络流量、跑自动化脚本，偶尔充当开发测试环境。我还配置了&lt;strong&gt;每周日自动重启&lt;/strong&gt;的定时任务。&lt;/p&gt;
&lt;p&gt;但树莓派的短板也随着使用深度逐渐暴露。最让我焦虑的是 TF 卡的存储可靠性，即便选用了号称&amp;quot;耐用级&amp;quot;的 Endurance 卡，也无法完全避免潜在的 I/O 风险。在上一篇关于&lt;a href="https://www.junyi.dev/posts/k3s-101/" target="_blank" rel="noopener noreferrer"&gt;树莓派 5 集群配置 k3s&lt;/a&gt;的文章发布后，我的四节点集群仅稳定运行了一周，就有一个节点因突发大量 &lt;strong&gt;I/O Error&lt;/strong&gt; 彻底挂掉，而它用的正是我精心挑选的高耐久卡，寿命甚至不如 4B 上的普通卡。&lt;/p&gt;
&lt;p&gt;编译较大的项目时要等很久，同时跑多个服务也偶尔卡顿。虽然可以外接 SSD 缓解存储问题，但我最后选择了换成 NUC。&lt;/p&gt;
&lt;h2 id="换成-nuc" &gt;
&lt;div&gt;
&lt;a href="#%e6%8d%a2%e6%88%90-nuc"&gt;
#
&lt;/a&gt;
换成 NUC
&lt;/div&gt;
&lt;/h2&gt;
&lt;p&gt;NUC 的机身比树莓派大不了多少，但金属外壳明显更重。我给它装了 &lt;a href="https://ubuntu.com/download/desktop" target="_blank" rel="noopener noreferrer"&gt;Ubuntu Desktop 24.04&lt;/a&gt;。同一个 Go 项目，在树莓派上编译需要 3 分钟，在 NUC 上需要 1 分钟。&lt;/p&gt;
&lt;figure&gt;
&lt;img id="fig1" src="https://www.junyi.dev/posts/intel-nuc/nuc1.jpg" alt="The Intel NUC" max-width="500px" style="border-radius: 10px; box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.3);" /&gt;
&lt;figcaption&gt;Fig.1 - Intel NUC&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;换成内置 SSD 后，我也敢在本地跑一个小型数据库了，以前总担心频繁读写会烧卡。&lt;/p&gt;
&lt;p&gt;退役的树莓派会继续作为边缘计算节点使用。&lt;/p&gt;</content:encoded><category>Linux</category><category>Tech</category><guid isPermaLink="true">https://www.junyi.dev/posts/intel-nuc/</guid></item><item><title>MacBook Pro M1 键帽更换指南</title><link>https://www.junyi.dev/posts/replace-keyboard/</link><pubDate>Fri, 01 Aug 2025 01:16:29 +0800</pubDate><author>junyi.h@comp.nus.edu.sg (Junyi Hou)</author><description>
众所周知，键帽一旦“打油”，就很难通过简单擦拭恢复原状。我曾尝试用砂纸打磨键帽，但不仅效果更差，产生的碎屑还会掉进缝隙里。你可以在图中看到我打磨过的 Shift 和 Space，外观变得非常难看。
虽然有人说可以去 Apple 直营店更换键帽1，但据说每次只能更换几个。对于我这种整块键盘都打油的情况，显然还是自己动手来得更实际。
写这篇博客的原因是，我发现网上几乎找不到一篇完整介绍 MacBook Pro M1 键帽更换的教程。大多数视频2只演示了普通按键的拆解，也就是字母键和数字键，没有介绍长键帽，比如 Space、Caps Lock、ESC，以及方向键的处理方式。而这些键的结构明显不同，拆装方式也更复杂。</description><content:encoded>&lt;p&gt;众所周知，键帽一旦“打油”，就很难通过简单擦拭恢复原状。我曾尝试用砂纸打磨键帽，但不仅效果更差，产生的碎屑还会掉进缝隙里。你可以在图中看到我打磨过的 &lt;kbd&gt;Shift&lt;/kbd&gt; 和 &lt;kbd&gt;Space&lt;/kbd&gt;，外观变得非常难看。&lt;/p&gt;
&lt;p&gt;虽然有人说可以去 Apple 直营店更换键帽&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt;，但据说每次只能更换几个。对于我这种&lt;strong&gt;整块键盘都打油&lt;/strong&gt;的情况，显然还是自己动手来得更实际。&lt;/p&gt;
&lt;p&gt;写这篇博客的原因是，我发现网上几乎找不到一篇完整介绍 MacBook Pro M1 键帽更换的教程。大多数视频&lt;sup id="fnref:2"&gt;&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref"&gt;2&lt;/a&gt;&lt;/sup&gt;只演示了普通按键的拆解，也就是字母键和数字键，没有介绍长键帽，比如 &lt;kbd&gt;Space&lt;/kbd&gt;、&lt;kbd&gt;Caps Lock&lt;/kbd&gt;、&lt;kbd&gt;ESC&lt;/kbd&gt;，以及方向键的处理方式。而这些键的结构明显不同，拆装方式也更复杂。&lt;/p&gt;
&lt;figure&gt;
&lt;img src="https://www.junyi.dev/posts/replace-keyboard/beforeafter.png" style="border-radius:0.5rem; width: 1024px"&gt;
&lt;figcaption&gt;
上为换键帽前，下为换键帽后
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;我在淘宝上购买了全套键帽，花了 85 元，打算自己更换：&lt;/p&gt;
&lt;figure&gt;
&lt;img src="https://www.junyi.dev/posts/replace-keyboard/taobao.jpeg" style="border-radius:0.5rem; width: 256px"&gt;
&lt;figcaption&gt;
淘宝购买的全套键帽
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;后来才看到，闲鱼全套键帽每套只要 ¥20 到 ¥40&lt;sup id="fnref:3"&gt;&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref"&gt;3&lt;/a&gt;&lt;/sup&gt;。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="拆掉旧键帽" &gt;
&lt;div&gt;
&lt;a href="#%e6%8b%86%e6%8e%89%e6%97%a7%e9%94%ae%e5%b8%bd"&gt;
#
&lt;/a&gt;
拆掉旧键帽
&lt;/div&gt;
&lt;/h2&gt;
&lt;p&gt;MacBook Pro 使用的是剪刀脚结构，因此我们必须遵循剪刀脚键盘的拆解方法。&lt;/p&gt;
&lt;h3 id="普通按键拆装" &gt;
&lt;div&gt;
&lt;a href="#%e6%99%ae%e9%80%9a%e6%8c%89%e9%94%ae%e6%8b%86%e8%a3%85"&gt;
##
&lt;/a&gt;
普通按键拆装
&lt;/div&gt;
&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;操作步骤：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;定位卡扣&lt;/strong&gt;: 每个普通键帽都有四个卡扣，分别位于四个角&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;从右下角开始&lt;/strong&gt;: 用撬棒微微翘起键帽右下角，听到轻微的&amp;quot;啪&amp;quot;声表示卡扣已释放&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;接着左下角&lt;/strong&gt;: 重复同样操作释放左下角的卡扣&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;提起键帽&lt;/strong&gt;: 当下方两个卡扣都释放后，轻轻向上提起整个键帽&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;完全取下&lt;/strong&gt;: 上方卡扣会自然脱离&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;注意事项：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;⚠️ 不要用力过猛，剪刀脚结构比较脆弱&lt;/li&gt;
&lt;li&gt;⚠️ 避免直接向上拉扯，可能会损坏卡扣&lt;/li&gt;
&lt;li&gt;💡 建议使用塑料撬棒而不是金属工具，避免刮伤&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;你可以参考这个视频&lt;sup id="fnref1:2"&gt;&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref"&gt;2&lt;/a&gt;&lt;/sup&gt;，视频中演示了普通按键的拆解方法。&lt;/p&gt;
&lt;p&gt;安装键帽与拆卸过程相反，首先把键帽从上方滑到卡槽里，然后按压键帽，直到键帽完全卡入卡槽。&lt;/p&gt;
&lt;p&gt;⚠️ 注意不要大力按压键帽，因为键帽的塑料件非常脆弱。&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id="长键帽拆装shiftcaps-lockesc" &gt;
&lt;div&gt;
&lt;a href="#%e9%95%bf%e9%94%ae%e5%b8%bd%e6%8b%86%e8%a3%85shiftcaps-lockesc"&gt;
##
&lt;/a&gt;
长键帽拆装：Shift、Caps Lock、ESC
&lt;/div&gt;
&lt;/h3&gt;
&lt;p&gt;这类键帽内部通常带有一到两个平衡杆，也就是铁丝，结构更复杂。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;拆卸建议：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;同样从右下角开始，再拆左下角；&lt;/li&gt;
&lt;li&gt;拆下时注意不要用力过猛，&lt;strong&gt;避免下方金属卡槽变形&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;figure&gt;
&lt;div style="display:flex; gap:0.5rem; align-items:stretch; max-width: 1024px; margin: 0 auto;"&gt;
&lt;img src="https://www.junyi.dev/posts/replace-keyboard/capslock-3.jpg" style="border-radius:0.5rem; width: 49%; min-width:0; object-fit:cover;"&gt;
&lt;div style="display:flex; flex-direction:column; gap:0.5rem; width: 51%; min-width:0;"&gt;
&lt;img src="https://www.junyi.dev/posts/replace-keyboard/capslock-1.jpg" style="border-radius:0.5rem; width: 100%; flex:1; min-height:0; object-fit:cover;"&gt;
&lt;img src="https://www.junyi.dev/posts/replace-keyboard/capslock-2.jpg" style="border-radius:0.5rem; width: 100%; flex:1; min-height:0; object-fit:cover;"&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;figcaption&gt;
左：拆下的 Caps Lock 键帽翻面放在键位旁。右：近景，键帽底部的金属平衡杆，以及露出的剪刀脚和卡槽
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;&lt;strong&gt;安装建议：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果是 1 个平衡杆：&lt;br&gt;
把铁丝先固定在键帽上，滑入键盘的缺口，再正常安装即可。&lt;/li&gt;
&lt;li&gt;如果是 2 个平衡杆：&lt;br&gt;
将&lt;strong&gt;上方平衡杆装在键帽上&lt;/strong&gt;，&lt;strong&gt;下方平衡杆卡在键盘上&lt;/strong&gt;。&lt;br&gt;
安装时先对准上方卡槽，轻轻按入；再将下方卡扣压入孔位；&lt;/li&gt;
&lt;/ul&gt;
&lt;figure&gt;
&lt;img src="https://www.junyi.dev/posts/replace-keyboard/longkey.jpg" style="border-radius:0.5rem; width: 512px"&gt;
&lt;figcaption&gt;
只有一个平衡杆的键帽，先把平衡杆卡住缺口，然后再正常装
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h3 id="长键帽拆装space" &gt;
&lt;div&gt;
&lt;a href="#%e9%95%bf%e9%94%ae%e5%b8%bd%e6%8b%86%e8%a3%85space"&gt;
##
&lt;/a&gt;
长键帽拆装：Space
&lt;/div&gt;
&lt;/h3&gt;
&lt;p&gt;&lt;kbd&gt;Space&lt;/kbd&gt; 是最长的键帽，底下的结构和其他长键不同：左右两端各有一组剪刀脚，中间只有一个硅胶碗，另外还有两根平衡杆。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;拆卸建议：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;和其他键一样，先从右下角撬起；&lt;/li&gt;
&lt;li&gt;然后沿着靠近触控板的这一侧慢慢往左推进，直到整条前沿完全翘起；&lt;/li&gt;
&lt;li&gt;前沿脱开后，键帽会连着平衡杆一起抬起，再从两端的剪刀脚上取下，注意收好两根平衡杆。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;安装建议：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;两根平衡杆分开装：一根装进键盘上剪刀脚的卡槽里，另一根挂在键帽上；&lt;/li&gt;
&lt;li&gt;把键帽放上去，让键帽上的平衡杆勾住键盘的金属卡槽；&lt;/li&gt;
&lt;li&gt;确认两根平衡杆都是平的，并且彼此拉开到最大距离；&lt;/li&gt;
&lt;li&gt;先压 Space 的一侧，听到咔哒声，再压另一侧。&lt;/li&gt;
&lt;/ul&gt;
&lt;figure&gt;
&lt;div style="display:flex; flex-direction:column; gap:0.5rem; max-width: 1024px; margin: 0 auto;"&gt;
&lt;div style="display:flex; gap:0.5rem;"&gt;
&lt;img src="https://www.junyi.dev/posts/replace-keyboard/space-open-1.jpg" style="border-radius:0.5rem; flex:1.89; width:0; min-width:0; height:auto;"&gt;
&lt;img src="https://www.junyi.dev/posts/replace-keyboard/space-open-2.jpg" style="border-radius:0.5rem; flex:1.59; width:0; min-width:0; height:auto;"&gt;
&lt;/div&gt;
&lt;div style="display:flex; gap:0.5rem;"&gt;
&lt;img src="https://www.junyi.dev/posts/replace-keyboard/space-detail-1.jpg" style="border-radius:0.5rem; flex:2.77; width:0; min-width:0; height:auto;"&gt;
&lt;img src="https://www.junyi.dev/posts/replace-keyboard/space-detail-2.jpg" style="border-radius:0.5rem; flex:1.21; width:0; min-width:0; height:auto;"&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;figcaption&gt;
上排：从右下角开始，把 Space 靠触控板的一侧整条翘起，缝隙里能看到背光漏出来。下排：拆下后的键位，两端是剪刀脚，中间是硅胶碗；键帽背面挂着平衡杆
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h3 id="短键帽拆装方向键" &gt;
&lt;div&gt;
&lt;a href="#%e7%9f%ad%e9%94%ae%e5%b8%bd%e6%8b%86%e8%a3%85%e6%96%b9%e5%90%91%e9%94%ae"&gt;
##
&lt;/a&gt;
短键帽拆装：方向键
&lt;/div&gt;
&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;上方向键：先撬开左侧卡扣，然后&lt;strong&gt;从左向右滑出&lt;/strong&gt;；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;左/下/右方向键：先撬开右侧卡扣，然后&lt;strong&gt;从右向左滑出&lt;/strong&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在拆之前，请参考方向键构造图。&lt;/p&gt;
&lt;figure&gt;
&lt;img src="https://www.junyi.dev/posts/replace-keyboard/arrowkeys.jpg" style="border-radius:0.5rem; width: 512px"&gt;
&lt;img src="https://www.junyi.dev/posts/replace-keyboard/arrowkey_side.jpg" style="border-radius:0.5rem; width: 512px"&gt;
&lt;img src="https://www.junyi.dev/posts/replace-keyboard/arrowkey_top.jpg" style="border-radius:0.5rem; width: 512px"&gt;
&lt;figcaption&gt;
方向键构造图
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id="总结" &gt;
&lt;div&gt;
&lt;a href="#%e6%80%bb%e7%bb%93"&gt;
#
&lt;/a&gt;
总结
&lt;/div&gt;
&lt;/h2&gt;
&lt;p&gt;记得最后检查一下所有拆下来的键，防止有一些铁丝忘记取下来，我在装的最后就发现有两个铁丝忘记取出来。&lt;/p&gt;
&lt;figure&gt;
&lt;img src="https://www.junyi.dev/posts/replace-keyboard/final.jpg" style="border-radius:0.5rem; width: 512px"&gt;
&lt;figcaption&gt;
更换下来的旧键帽
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h3 id="常见问题解决" &gt;
&lt;div&gt;
&lt;a href="#%e5%b8%b8%e8%a7%81%e9%97%ae%e9%a2%98%e8%a7%a3%e5%86%b3"&gt;
##
&lt;/a&gt;
常见问题解决
&lt;/div&gt;
&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;键帽装回去后按压感觉不对怎么办？&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;检查平衡杆是否正确卡入卡槽&lt;/li&gt;
&lt;li&gt;确认剪刀脚支架没有变形或断裂&lt;/li&gt;
&lt;li&gt;重新拆卸并按正确顺序安装&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;键帽容易脱落？&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;检查四个卡扣是否都正确扣入&lt;/li&gt;
&lt;li&gt;平衡杆的安装方向是否正确&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;按键有卡顿现象？&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;检查是否有灰尘或异物卡在键帽下方&lt;/li&gt;
&lt;li&gt;确认剪刀脚支架安装位置正确&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="专业建议" &gt;
&lt;div&gt;
&lt;a href="#%e4%b8%93%e4%b8%9a%e5%bb%ba%e8%ae%ae"&gt;
##
&lt;/a&gt;
专业建议
&lt;/div&gt;
&lt;/h3&gt;
&lt;p&gt;如果你对自己的动手能力不自信，或者已经有卡扣/剪刀脚损坏，建议：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;联系 Apple 官方维修点；&lt;/li&gt;
&lt;li&gt;找可信的第三方维修；&lt;/li&gt;
&lt;li&gt;观看更多视频教程练练手再动工。&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="footnotes" role="doc-endnotes"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;&lt;a href="https://discussionschinese.apple.com/thread/252109035?sortBy=rank" target="_blank" rel="noopener noreferrer"&gt;MacbookPRO键帽打油怎么办？据说天才吧可以免费更换，是吗？&lt;/a&gt;&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:2"&gt;
&lt;p&gt;&lt;a href="https://www.bilibili.com/video/BV1UR4y1E7be/" target="_blank" rel="noopener noreferrer"&gt;哔哩哔哩：MacBook剪刀键盘拆装（键帽+剪刀支架）&lt;/a&gt;&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&amp;#160;&lt;a href="#fnref1:2" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:3"&gt;
&lt;p&gt;&lt;a href="https://post.smzdm.com/p/ao9wevx9/" target="_blank" rel="noopener noreferrer"&gt;什么值得买：Macbook Pro 键盘打油，键帽更换作业&lt;/a&gt;&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</content:encoded><category>TechShare</category><category>Tech</category><guid isPermaLink="true">https://www.junyi.dev/posts/replace-keyboard/</guid></item></channel></rss>