<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
    <title>ThinkSpread</title>
    <subtitle>Server-grade inference architecture for local AI.</subtitle>
    <link rel="self" type="application/atom+xml" href="https://www.thinkspread.com/atom.xml"/>
    <link rel="alternate" type="text/html" href="https://www.thinkspread.com"/>
    <generator uri="https://www.getzola.org/">Zola</generator>
    <updated>2026-09-02T00:00:00+00:00</updated>
    <id>https://www.thinkspread.com/atom.xml</id>
    <entry xml:lang="en">
        <title>投机解码的经济学:同一个功能,9B 上 +41%,35B 上打平</title>
        <published>2026-09-02T00:00:00+00:00</published>
        <updated>2026-09-02T00:00:00+00:00</updated>
        
        <author>
          <name>Unknown</name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://www.thinkspread.com/blog/speculative-decoding-economics-zh/"/>
        <id>https://www.thinkspread.com/blog/speculative-decoding-economics-zh/</id>
        
        <content type="html" xml:base="https://www.thinkspread.com/blog/speculative-decoding-economics-zh/">&lt;p&gt;投机解码(我们用的是 MTP:模型自带的 next-n 草稿头)的原理一句话能讲完:
小代价猜 k 个 token,大模型一次 verify,猜中几个收几个。但&quot;到底赚不赚&quot;
取决于一个很少被拆开算的量:&lt;strong&gt;verify 的边际成本&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;我们把它测出来了。同会话四个 k 值扫描,48 轮累计:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;verify(m) ≈ 9 ms + 5.5 ms × m        (m = k+1 行)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;固定项 9ms ≈ 一个普通 decode 步。为什么多 verify 一行只要 5.5ms、而不是
再付一遍全部权重?因为 &lt;strong&gt;m 行 verify 走的是 batch GEMV&lt;/strong&gt;:权重从显存读
一次,m 行共享。稠密部分的带宽账在 m∈[2,8] 时几乎是免费的。&lt;/p&gt;
&lt;h2 id=&quot;jia-gou-jue-ding-zhuan-bu-zhuan&quot;&gt;架构决定赚不赚&lt;/h2&gt;
&lt;p&gt;那 5.5ms 的边际里藏着关键:对 MoE 模型,&lt;strong&gt;每行激活的专家不同,专家权重
不可共享&lt;/strong&gt; —— 每多一行就多读约 0.46GB 专家,约 3.2ms。这一项稠密模型没有。&lt;/p&gt;
&lt;p&gt;后果直接写在结果里。同一套 MTP 实现:&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&gt;结论&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Qwen3.5-35B-A3B&lt;/td&gt;&lt;td&gt;MoE&lt;/td&gt;&lt;td&gt;accept ≈ 0.8 才打平,上限也只有 ~1.19×&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Qwen3.5-9B&lt;/td&gt;&lt;td&gt;稠密&lt;/td&gt;&lt;td&gt;&lt;strong&gt;edit +41% · code +24% · chat +15% · en +6%,无负项&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;同一个功能,一边是&quot;约等于不赚&quot;,一边是产品级收益 —— 差别就是每行边际里
有没有那 3.2ms 的专家项。&lt;strong&gt;投机解码的性价比不是实现细节,是架构属性。&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id=&quot;shen-du-k-de-tian-dian-sao-chu-lai-bu-shi-cai-chu-lai&quot;&gt;深度 k 的甜点:扫出来,不是猜出来&lt;/h2&gt;
&lt;p&gt;9B 上把 k 从 1 扫到 6(单并发,edit/en/code 三负载,基线 45.1/45.7/45.8
tok/s):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;k=1   53.3 / 49.3 / 53.5
k=2   56.3 / 47.2 / 53.0
k=3   63.3 / 48.5 / 56.6    ← 合计最优,服务定值
k=4   65.1 / 46.5 / 48.4    (只利 edit,code 塌)
k=5+  全面衰退
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;acceptance 随深度单调下降(英文散文 k=1 时 0.85,k=3 时 0.59):猜得越深,
后面的 token 越接近瞎猜。不同负载的最优 k 不同 —— 散文偏浅、代码和编辑
撑得住深 —— 但 &lt;strong&gt;k=3 是全局最优的固定值&lt;/strong&gt;,这就是服务的默认。&lt;/p&gt;
&lt;h2 id=&quot;men-kong-rang-ta-zhi-zai-zhuan-de-di-fang-kai&quot;&gt;门控:让它只在赚的地方开&lt;/h2&gt;
&lt;p&gt;最有意思的设计不是内核,是一个阈值:&lt;strong&gt;滚动 acceptance 低于 0.60 就自动
退回普通解码&lt;/strong&gt;(&lt;code&gt;AS_SPEC_MIN_ACCEPT=0.60&lt;/code&gt;)。&lt;/p&gt;
&lt;p&gt;这个门的效果超出它的本意。agent 负载 —— 改文件、发工具调用、生成 JSON ——
acceptance 天然高(结构化文本可预测,35B 上 mid-edit 实测 0.92);中文散文
天然低(0.28-0.5)。于是一个简单的阈值自动实现了&quot;&lt;strong&gt;编辑场景开、散文场景
关&lt;/strong&gt;&quot;的正确形态,不需要负载分类器,不需要用户配置。&lt;/p&gt;
&lt;p&gt;35B 上它兜住下限:散文触发门、退回纯解码,mid-edit 负载 91 → 97.6 tok/s
(+7%)照拿。9B 上它几乎不触发,全场景放行。&lt;/p&gt;
&lt;h2 id=&quot;yi-bi-yao-cheng-shi-xie-chu-lai-de-zhang-kuai-dan-bu-sheng-dian&quot;&gt;一笔要诚实写出来的账:快,但不省电&lt;/h2&gt;
&lt;p&gt;自制的 IOReport 采样器(免 root)量了解码窗的功耗,9B 英文思考负载:&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&gt;功率&lt;/th&gt;&lt;th&gt;每 token 能量&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;k=0&lt;/td&gt;&lt;td&gt;45.8 tok/s&lt;/td&gt;&lt;td&gt;51.7 W&lt;/td&gt;&lt;td&gt;1.14 J&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;k=3&lt;/td&gt;&lt;td&gt;52.3 tok/s&lt;/td&gt;&lt;td&gt;64.3 W&lt;/td&gt;&lt;td&gt;&lt;strong&gt;1.26 J (+10%)&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;verify 的批量计算抬高了瞬时占用 —— &lt;strong&gt;MTP 用能量换时间&lt;/strong&gt;,不是免费午餐。
所以 App 形态里它对应两个档位:性能档 k=3,省电档 k=0。插电的 Mac mini
和电池上的 MacBook,答案本来就该不同。&lt;/p&gt;
&lt;h2 id=&quot;shou-wei&quot;&gt;收尾&lt;/h2&gt;
&lt;p&gt;一个功能从&quot;听起来很快&quot;到&quot;知道什么时候开&quot;,中间隔着:一个成本模型
(9 + 5.5m)、一条架构分界线(专家可不可共享)、一次 k 扫描(甜点在 3)、
一个自动门(0.60),和一笔功耗账(+10% J/tok)。这些都测完之后,
&quot;默认值该是什么&quot;就不再是观点,是查表。&lt;/p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>The economics of speculative decoding: +41% on 9B, break-even on 35B</title>
        <published>2026-09-02T00:00:00+00:00</published>
        <updated>2026-09-02T00:00:00+00:00</updated>
        
        <author>
          <name>Unknown</name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://www.thinkspread.com/blog/speculative-decoding-economics/"/>
        <id>https://www.thinkspread.com/blog/speculative-decoding-economics/</id>
        
        <content type="html" xml:base="https://www.thinkspread.com/blog/speculative-decoding-economics/">&lt;p&gt;Speculative decoding can be summarized in one sentence. A cheap draft head
guesses &lt;code&gt;k&lt;/code&gt; tokens, the full model verifies them in one pass, and the runtime
keeps however many were correct. We use MTP, the model&#39;s own next-n prediction
heads. Whether this actually saves time depends on a quantity that is rarely
broken out: &lt;strong&gt;the marginal cost of verification&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;We measured it across four values of &lt;code&gt;k&lt;/code&gt;, over 48 rounds in one session:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;verify(m) ≈ 9 ms + 5.5 ms × m        (m = k+1 rows)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The fixed 9 ms is roughly one ordinary decode step. Why does each additional
verification row cost only 5.5 ms instead of rereading every weight? Because
the &lt;code&gt;m&lt;/code&gt; rows run as a &lt;strong&gt;batched GEMV&lt;/strong&gt;: the weights are read once and shared by
the rows. For the dense part of the model, extra rows are nearly free over
&lt;code&gt;m ∈ [2,8]&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&quot;architecture-decides-whether-it-pays&quot;&gt;Architecture decides whether it pays&lt;/h2&gt;
&lt;p&gt;The important part is hidden inside that 5.5 ms marginal cost. In a MoE model,
&lt;strong&gt;each row can activate different experts, so their weights cannot be shared&lt;/strong&gt;.
Every additional row reads about 0.46 GB of expert weights, costing roughly
3.2 ms. Dense models do not have that term.&lt;/p&gt;
&lt;p&gt;The consequence is visible in the results. This is the same MTP implementation
on both models:&lt;/p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Model&lt;/th&gt;&lt;th&gt;Architecture&lt;/th&gt;&lt;th&gt;Result&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Qwen3.5-35B-A3B&lt;/td&gt;&lt;td&gt;MoE&lt;/td&gt;&lt;td&gt;Break-even near 0.8 acceptance; ceiling about 1.19×&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Qwen3.5-9B&lt;/td&gt;&lt;td&gt;Dense&lt;/td&gt;&lt;td&gt;&lt;strong&gt;edit +41% · code +24% · chat +15% · English +6%, no negative workload&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The same feature is nearly a wash on one model and a product-level improvement
on the other. The difference is the 3.2 ms expert term in every extra row.
&lt;strong&gt;The economics of speculative decoding are an architectural property, not
just an implementation detail.&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id=&quot;the-sweet-spot-for-k-is-measured-not-guessed&quot;&gt;The sweet spot for k is measured, not guessed&lt;/h2&gt;
&lt;p&gt;On the 9B model we swept &lt;code&gt;k&lt;/code&gt; from 1 through 6 at single concurrency, using edit,
English prose, and code workloads. Their baselines were 45.1 / 45.7 / 45.8
tok/s:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;k=1   53.3 / 49.3 / 53.5
k=2   56.3 / 47.2 / 53.0
k=3   63.3 / 48.5 / 56.6    &amp;lt;- best overall; server default
k=4   65.1 / 46.5 / 48.4    (helps edit only; code collapses)
k=5+  regresses across the board
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Acceptance falls monotonically with depth. On English prose it goes from 0.85
at &lt;code&gt;k=1&lt;/code&gt; to 0.59 at &lt;code&gt;k=3&lt;/code&gt;: the farther ahead the draft predicts, the closer it
gets to guessing. Different workloads prefer different depths. Prose prefers a
shallow draft, while code and editing tolerate more. &lt;strong&gt;&lt;code&gt;k=3&lt;/code&gt; is the best fixed
global value&lt;/strong&gt;, so that is the server default for this model.&lt;/p&gt;
&lt;h2 id=&quot;gating-enable-it-only-where-it-pays&quot;&gt;Gating: enable it only where it pays&lt;/h2&gt;
&lt;p&gt;The most interesting design is not a kernel. It is a threshold: &lt;strong&gt;when rolling
acceptance falls below 0.60, fall back to ordinary decoding automatically&lt;/strong&gt;
(&lt;code&gt;AS_SPEC_MIN_ACCEPT=0.60&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;That gate does more than intended. Agent workloads such as editing files,
emitting tool calls, and generating JSON are predictable and naturally have
high acceptance (0.92 on the 35B mid-edit workload). Chinese prose is naturally
low at 0.28-0.5. One threshold therefore produces the right behavior:
&lt;strong&gt;speculate for editing and structured output; stop for prose&lt;/strong&gt;, without a
workload classifier or user configuration.&lt;/p&gt;
&lt;p&gt;On the 35B model the gate protects the floor: prose drops back to ordinary
decode, while the mid-edit workload still moves from 91 to 97.6 tok/s (+7%). On
the 9B model it rarely fires, so speculation remains enabled across workloads.&lt;/p&gt;
&lt;h2 id=&quot;one-cost-that-belongs-in-the-ledger-faster-not-more-efficient&quot;&gt;One cost that belongs in the ledger: faster, not more efficient&lt;/h2&gt;
&lt;p&gt;A rootless IOReport sampler measured power during the decode window on the 9B
English reasoning workload:&lt;/p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;/th&gt;&lt;th&gt;Throughput&lt;/th&gt;&lt;th&gt;Power&lt;/th&gt;&lt;th&gt;Energy per token&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;k=0&lt;/td&gt;&lt;td&gt;45.8 tok/s&lt;/td&gt;&lt;td&gt;51.7 W&lt;/td&gt;&lt;td&gt;1.14 J&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;k=3&lt;/td&gt;&lt;td&gt;52.3 tok/s&lt;/td&gt;&lt;td&gt;64.3 W&lt;/td&gt;&lt;td&gt;&lt;strong&gt;1.26 J (+10%)&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Batched verification raises instantaneous utilization. &lt;strong&gt;MTP trades energy for
time; it is not a free lunch.&lt;/strong&gt; In an application that naturally becomes two
modes: &lt;code&gt;k=3&lt;/code&gt; for performance and &lt;code&gt;k=0&lt;/code&gt; for efficiency. A plugged-in Mac mini and
a MacBook on battery should not have the same answer.&lt;/p&gt;
&lt;h2 id=&quot;closing-the-ledger&quot;&gt;Closing the ledger&lt;/h2&gt;
&lt;p&gt;Between &quot;this sounds fast&quot; and &quot;we know when to enable it&quot; are a cost model
(&lt;code&gt;9 + 5.5m&lt;/code&gt;), an architectural boundary (whether expert weights can be shared),
a sweep for &lt;code&gt;k&lt;/code&gt; (the sweet spot is 3), an automatic gate (0.60), and a power
measurement (+10% J/token). Once those are measured, the default is no longer
an opinion. It is a lookup.&lt;/p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>让 Neural Engine 干活:视觉塔为什么不该跑在 GPU 上</title>
        <published>2026-09-01T00:00:00+00:00</published>
        <updated>2026-09-01T00:00:00+00:00</updated>
        
        <author>
          <name>Unknown</name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://www.thinkspread.com/blog/vision-on-the-npu-zh/"/>
        <id>https://www.thinkspread.com/blog/vision-on-the-npu-zh/</id>
        
        <content type="html" xml:base="https://www.thinkspread.com/blog/vision-on-the-npu-zh/">&lt;p&gt;每台 Apple Silicon Mac 里都有两个加速器:GPU,和一颗 Neural Engine(ANE,
M5 Pro 上 16 核、约 42 TOPS int8)。跑本地大模型时,几乎所有框架的选择是
一样的:所有东西都在 GPU 上,ANE 从头到尾闲着。&lt;/p&gt;
&lt;p&gt;对纯文本模型,这个选择没什么可争的 —— LLM 解码是自回归的、访存主导、需要
灵活的 KV cache,不是 ANE 的形状。但&lt;strong&gt;视觉语言模型不同&lt;/strong&gt;:它多出来的那块
计算 —— 视觉塔(ViT)—— 恰好是 ANE 最擅长的东西。&lt;/p&gt;
&lt;h2 id=&quot;shi-jue-ta-shi-yi-ci-xing-de-jing-tai-qian-xiang&quot;&gt;视觉塔是一次性的静态前向&lt;/h2&gt;
&lt;p&gt;一张图进来,patchify 之后过 27 层 transformer,输出一串视觉 embedding,
结束。没有自回归、没有 KV cache、没有逐步增长的序列 —— 就是一个形状固定的
大前向。这正是 Core ML 编译器能吃透、ANE 能全速跑的负载。&lt;/p&gt;
&lt;p&gt;数字说话。ViT-B/16 规模的塔,M5 Pro 实测:&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&gt;能量&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;GPU (MPS)&lt;/td&gt;&lt;td&gt;15.9 ms&lt;/td&gt;&lt;td&gt;612 mJ&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;ANE&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;18.3 ms&lt;/td&gt;&lt;td&gt;&lt;strong&gt;75 mJ&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;时延几乎相同,&lt;strong&gt;每张图的能量差 8 倍&lt;/strong&gt;。ANE 跑视觉塔约 2W,GPU 干同样的事
约 8W。在插电的台式机上这是温度和风扇;在 MacBook 上这是电池。&lt;/p&gt;
&lt;h2 id=&quot;dan-zhen-zheng-de-shou-yi-bu-shi-sheng-dian-shi-bing-xing&quot;&gt;但真正的收益不是省电,是并行&lt;/h2&gt;
&lt;p&gt;省电是单张图的账。serving 场景的账不一样:&lt;strong&gt;视觉塔在 ANE 上跑的时候,
GPU 一毫秒都不用停。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Tempo9 的流水线是这样的:请求进来,聊天模板和 tokenizer 在 CPU 上跑,图像
在 ANE 上过塔 —— 而这整段时间,GPU 在解码&lt;strong&gt;别的请求&lt;/strong&gt;。等视觉 embedding
就绪,它才进入引擎的连续批处理,和在飞的解码共享同一个 forward。&lt;/p&gt;
&lt;p&gt;一个 agent 循环反复看同一张截图、多个会话各带各的图、视频逐帧进来 —— 这些
负载里,视觉预处理和语言解码是天然可重叠的两条流。用同一个 GPU 排队,
重叠就没了;分到两个处理器上,重叠是白拿的。&lt;/p&gt;
&lt;p&gt;还有第三笔账,24GB 的机器上最要紧:&lt;strong&gt;塔的权重不占 GPU 的 wired 显存预算。&lt;/strong&gt;
35B 模型贴着上限跑的时候,几百 MB 的塔权重放在哪一边,决定的是能不能再多
放一个并发的 KV。&lt;/p&gt;
&lt;h2 id=&quot;dong-tai-fen-bian-lu-zen-me-sai-jin-jing-tai-bian-yi-qi&quot;&gt;动态分辨率怎么塞进静态编译器&lt;/h2&gt;
&lt;p&gt;麻烦在于:Qwen 系的塔是 NaViT 式动态分辨率 —— patch 数随输入图变,而
Core ML 要的是编译时定死的形状。&lt;/p&gt;
&lt;p&gt;我们的解法是&lt;strong&gt;分桶&lt;/strong&gt;:按 patch 预算导出固定尺寸的模型,输入图等比缩放到
最近的桶,不足的 pad 掉、用 additive mask 屏蔽。发布策略是「一大一小」:&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&gt;典型适配&lt;/th&gt;&lt;th&gt;对应 LLM token&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;b1024&lt;/td&gt;&lt;td&gt;26 万 px&lt;/td&gt;&lt;td&gt;683×384 / 512×512&lt;/td&gt;&lt;td&gt;≤ 256&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;b4096&lt;/td&gt;&lt;td&gt;105 万 px&lt;/td&gt;&lt;td&gt;1365×768 / 1024×1024&lt;/td&gt;&lt;td&gt;≤ 1024&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;为什么只有两个桶、不是一个梯子?每个桶是一个&lt;strong&gt;独立编译的模型&lt;/strong&gt;,磁盘上
各占几百 MB —— 桶越多,下载越肥,而中间档位的收益递减。小桶伺候缩略图和
agent 截图流,大桶伺候文档和照片,够了。&lt;/p&gt;
&lt;h2 id=&quot;huan-chu-li-qi-bu-neng-huan-da-an&quot;&gt;换处理器,不能换答案&lt;/h2&gt;
&lt;p&gt;把计算从 GPU 挪到 ANE,最大的风险不是慢,是&lt;strong&gt;悄悄变错&lt;/strong&gt;:Core ML 默认
fp16,数值路径完全不同,而视觉塔的输出会直接决定模型&quot;看到&quot;什么。&lt;/p&gt;
&lt;p&gt;所以每个导出的塔要过三道门才能发布:算子驻留检查(确认真的在 ANE 上跑,
而不是静默回退 CPU)、逐桶和 fp32 参考比对(当前全部桶 cosine ≥ 0.98,
实测 min = 1.000000)、端到端 VQA 冒烟。任何一道不过,这个塔就不发布 ——
一个更快但看错图的塔,不是优化,是缺陷。&lt;/p&gt;
&lt;h2 id=&quot;shou-wei&quot;&gt;收尾&lt;/h2&gt;
&lt;p&gt;芯片上的硅是已经付过钱的。同一台 Mac,视觉走 ANE、语言走 GPU、调度走
CPU —— 三块硅同时在干活,这才对得起 Apple Silicon 这个名字。&lt;/p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Put the Neural Engine to work: why the vision tower should not run on the GPU</title>
        <published>2026-09-01T00:00:00+00:00</published>
        <updated>2026-09-01T00:00:00+00:00</updated>
        
        <author>
          <name>Unknown</name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://www.thinkspread.com/blog/vision-on-the-npu/"/>
        <id>https://www.thinkspread.com/blog/vision-on-the-npu/</id>
        
        <content type="html" xml:base="https://www.thinkspread.com/blog/vision-on-the-npu/">&lt;p&gt;Every Apple Silicon Mac contains two accelerators: a GPU and the Apple Neural
Engine (ANE). On the M5 Pro, the latter has 16 cores and roughly 42 TOPS of int8
compute. Most local inference frameworks still make the same choice: put
everything on the GPU and leave the ANE idle from start to finish.&lt;/p&gt;
&lt;p&gt;For a text-only model, that choice is reasonable. LLM decoding is autoregressive,
memory-bandwidth bound, and coupled to a dynamic KV cache. It is not a natural
ANE workload. &lt;strong&gt;Vision-language models are different.&lt;/strong&gt; Their extra component,
the vision tower (ViT), is almost exactly the kind of computation the ANE wants.&lt;/p&gt;
&lt;h2 id=&quot;a-vision-tower-is-one-static-forward-pass&quot;&gt;A vision tower is one static forward pass&lt;/h2&gt;
&lt;p&gt;An image is split into patches, passed through 27 transformer layers, and
converted into a sequence of visual embeddings. Then it is done. There is no
autoregression, no KV cache, and no sequence that grows one token at a time. It
is one large forward pass with a fixed shape, which Core ML can compile deeply
and the ANE can execute efficiently.&lt;/p&gt;
&lt;p&gt;On an M5 Pro, for a tower in the ViT-B/16 class:&lt;/p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;/th&gt;&lt;th&gt;Latency&lt;/th&gt;&lt;th&gt;Energy&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;GPU (MPS)&lt;/td&gt;&lt;td&gt;15.9 ms&lt;/td&gt;&lt;td&gt;612 mJ&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;ANE&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;18.3 ms&lt;/td&gt;&lt;td&gt;&lt;strong&gt;75 mJ&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Latency is nearly the same, while &lt;strong&gt;energy per image differs by 8×&lt;/strong&gt;. The ANE
uses about 2 W for the tower; the GPU uses about 8 W for the same work. On a
desktop that becomes heat and fan noise. On a MacBook it becomes battery life.&lt;/p&gt;
&lt;h2 id=&quot;the-larger-win-is-parallelism-not-power&quot;&gt;The larger win is parallelism, not power&lt;/h2&gt;
&lt;p&gt;Energy is the single-image account. Serving has a different ledger: &lt;strong&gt;while the
vision tower runs on the ANE, the GPU does not have to stop.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;In Tempo9, an incoming request runs its chat template and tokenizer on the CPU,
then sends the image through the tower on the ANE. During that entire interval,
the GPU can decode &lt;strong&gt;another request&lt;/strong&gt;. Once the visual embeddings are ready,
the request joins the continuous batch and shares a forward pass with the
decodes already in flight.&lt;/p&gt;
&lt;p&gt;An agent repeatedly looking at screenshots, several sessions carrying different
images, or video arriving one frame at a time all create two naturally
overlapping streams: vision preprocessing and language decoding. Queue both on
one GPU and that overlap disappears. Put them on separate processors and it is
available by construction.&lt;/p&gt;
&lt;p&gt;There is a third benefit that matters most on a 24 GB machine: &lt;strong&gt;the tower&#39;s
weights do not consume the GPU wired-memory budget.&lt;/strong&gt; When a 35B model is already
near the limit, where a few hundred megabytes of tower weights live can decide
whether another request&#39;s KV cache fits.&lt;/p&gt;
&lt;h2 id=&quot;fitting-dynamic-resolution-into-a-static-compiler&quot;&gt;Fitting dynamic resolution into a static compiler&lt;/h2&gt;
&lt;p&gt;The complication is that Qwen-family vision towers use NaViT-style dynamic
resolution. The number of patches changes with the input image, while Core ML
wants shapes fixed at compile time.&lt;/p&gt;
&lt;p&gt;We solve that with &lt;strong&gt;buckets&lt;/strong&gt;. Each model is exported for a fixed patch budget.
An image is scaled proportionally into the nearest bucket, padded, and masked
with an additive mask. The release uses one small and one large bucket:&lt;/p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Bucket&lt;/th&gt;&lt;th&gt;Pixel budget&lt;/th&gt;&lt;th&gt;Typical fit&lt;/th&gt;&lt;th&gt;LLM tokens&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;b1024&lt;/td&gt;&lt;td&gt;260K px&lt;/td&gt;&lt;td&gt;683×384 / 512×512&lt;/td&gt;&lt;td&gt;≤ 256&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;b4096&lt;/td&gt;&lt;td&gt;1.05M px&lt;/td&gt;&lt;td&gt;1365×768 / 1024×1024&lt;/td&gt;&lt;td&gt;≤ 1024&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Why two buckets instead of a ladder? Each bucket is an &lt;strong&gt;independently compiled
model&lt;/strong&gt; occupying several hundred megabytes on disk. More buckets make the
download larger while the intermediate gains diminish. The small bucket serves
thumbnails and agent screenshot streams; the large one serves documents and
photos.&lt;/p&gt;
&lt;h2 id=&quot;moving-processors-must-not-change-the-answer&quot;&gt;Moving processors must not change the answer&lt;/h2&gt;
&lt;p&gt;The largest risk in moving computation from GPU to ANE is not that it becomes
slow. It is that it becomes &lt;strong&gt;quietly wrong&lt;/strong&gt;. Core ML uses fp16 by default, its
numeric path is different, and the tower output directly determines what the
language model can see.&lt;/p&gt;
&lt;p&gt;Every exported tower therefore passes three gates before release: residency
inspection to confirm it really runs on the ANE rather than silently falling
back to the CPU; per-bucket comparison with an fp32 reference (all current
buckets have cosine similarity at least 0.98, with measured minimum 1.000000);
and an end-to-end VQA smoke test. If any gate fails, the tower does not ship. A
faster tower that sees the wrong image is not an optimization. It is a defect.&lt;/p&gt;
&lt;h2 id=&quot;closing&quot;&gt;Closing&lt;/h2&gt;
&lt;p&gt;The silicon is already paid for. Vision on the ANE, language on the GPU, and
scheduling on the CPU means all three parts of the chip are doing useful work
at once. That is what Apple Silicon should mean in practice.&lt;/p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>API reference</title>
        <published>2026-08-31T00:00:00+00:00</published>
        <updated>2026-08-31T00:00:00+00:00</updated>
        
        <author>
          <name>Unknown</name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://www.thinkspread.com/tempo9/manual/api/"/>
        <id>https://www.thinkspread.com/tempo9/manual/api/</id>
        
        <content type="html" xml:base="https://www.thinkspread.com/tempo9/manual/api/">&lt;!-- GENERATED from manual/api.md by scripts/sync_manual_to_site.py. Edit the manual, not this file. --&gt;
&lt;p&gt;One process serves four dialects on one port. Nothing needs to be enabled;
they are all live.&lt;/p&gt;
&lt;h2 id=&quot;openai&quot;&gt;OpenAI&lt;/h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Method&lt;/th&gt;&lt;th&gt;Route&lt;/th&gt;&lt;th&gt;Notes&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;GET&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;/v1/models&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Reports the one loaded model.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;GET&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;/v1/models/{id}&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Single-model lookup. Answers for any id.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;POST&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;/v1/chat/completions&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Chat. &lt;code&gt;stream&lt;/code&gt; defaults to &lt;strong&gt;false&lt;/strong&gt;.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;POST&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;/v1/responses&lt;/code&gt;&lt;/td&gt;&lt;td&gt;The Responses API, which Codex speaks.&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;anthropic&quot;&gt;Anthropic&lt;/h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Method&lt;/th&gt;&lt;th&gt;Route&lt;/th&gt;&lt;th&gt;Notes&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;POST&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;/v1/messages&lt;/code&gt;&lt;/td&gt;&lt;td&gt;What Claude Code speaks.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;POST&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;/v1/messages/count_tokens&lt;/code&gt;&lt;/td&gt;&lt;td&gt;A fast estimate. Callers budget with it; nothing decodes from it.&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;ollama-native&quot;&gt;Ollama native&lt;/h2&gt;
&lt;p&gt;For apps that integrate Ollama and offer no generic endpoint field.&lt;/p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Method&lt;/th&gt;&lt;th&gt;Route&lt;/th&gt;&lt;th&gt;Notes&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;GET&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;/api/tags&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Model list. This is the discovery call.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;GET&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;/api/version&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Reports &lt;code&gt;0.12.0-tempo9&lt;/code&gt; — semver-parseable, and tagged so it never claims to be an Ollama release.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;GET&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;/api/ps&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Loaded models. One, always resident.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;POST&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;/api/show&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Model detail. &lt;code&gt;capabilities&lt;/code&gt; reflects this build: &lt;code&gt;tools&lt;/code&gt; always, &lt;code&gt;vision&lt;/code&gt; only with &lt;code&gt;--tower&lt;/code&gt;.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;POST&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;/api/chat&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Chat.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;POST&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;/api/generate&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Bare-prompt completion. Replies use &lt;code&gt;response&lt;/code&gt;, not &lt;code&gt;message&lt;/code&gt;.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;POST&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;/api/embed&lt;/code&gt;, &lt;code&gt;/api/embeddings&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;strong&gt;501.&lt;/strong&gt; We serve no embeddings, and say so in the caller&#39;s dialect rather than 404ing.&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Three things differ from the OpenAI dialect, and each fails silently against
a client that assumes otherwise:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Streaming is newline-delimited JSON.&lt;/strong&gt; No &lt;code&gt;data: &lt;/code&gt; prefix, no &lt;code&gt;[DONE]&lt;/code&gt;
sentinel; the last object carries &lt;code&gt;done: true&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;stream&lt;/code&gt; defaults to &lt;code&gt;true&lt;/code&gt;.&lt;/strong&gt; The opposite of OpenAI.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tool-call arguments are a JSON object&lt;/strong&gt;, not a JSON string.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;authentication&quot;&gt;Authentication&lt;/h2&gt;
&lt;p&gt;None. See &lt;a href=&quot;../limits/#no-authentication&quot;&gt;Limits&lt;/a&gt;.&lt;/p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>CLI reference</title>
        <published>2026-08-31T00:00:00+00:00</published>
        <updated>2026-08-31T00:00:00+00:00</updated>
        
        <author>
          <name>Unknown</name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://www.thinkspread.com/tempo9/manual/cli/"/>
        <id>https://www.thinkspread.com/tempo9/manual/cli/</id>
        
        <content type="html" xml:base="https://www.thinkspread.com/tempo9/manual/cli/">&lt;!-- GENERATED from manual/cli.md by scripts/sync_manual_to_site.py. Edit the manual, not this file. --&gt;
&lt;pre&gt;&lt;code&gt;tempo9 (--gguf &amp;lt;.gguf&amp;gt; | --ollama &amp;lt;model[:tag]&amp;gt;) [options]
tempo9 --list-ollama
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;choosing-a-model&quot;&gt;Choosing a model&lt;/h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Flag&lt;/th&gt;&lt;th&gt;Default&lt;/th&gt;&lt;th&gt;Meaning&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;--gguf &amp;lt;path&amp;gt;&lt;/code&gt;&lt;/td&gt;&lt;td&gt;—&lt;/td&gt;&lt;td&gt;Model file. A bare &lt;code&gt;.gguf&lt;/code&gt; path as the only argument works too.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;--ollama &amp;lt;model[:tag]&amp;gt;&lt;/code&gt;&lt;/td&gt;&lt;td&gt;—&lt;/td&gt;&lt;td&gt;A model Ollama already pulled. A bare name means &lt;code&gt;:latest&lt;/code&gt;. See &lt;a href=&quot;../ollama-models/&quot;&gt;Ollama models&lt;/a&gt;.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;--list-ollama&lt;/code&gt;&lt;/td&gt;&lt;td&gt;—&lt;/td&gt;&lt;td&gt;Print what Ollama has pulled, then exit. Exits &lt;code&gt;1&lt;/code&gt; if the store is empty or absent.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;--graph &amp;lt;path&amp;gt;&lt;/code&gt;&lt;/td&gt;&lt;td&gt;built from the GGUF&lt;/td&gt;&lt;td&gt;Precomputed graph. Rarely needed — the engine builds and caches one.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;--name &amp;lt;string&amp;gt;&lt;/code&gt;&lt;/td&gt;&lt;td&gt;the file&#39;s basename, or the Ollama name&lt;/td&gt;&lt;td&gt;What &lt;code&gt;/v1/models&lt;/code&gt; and &lt;code&gt;/api/tags&lt;/code&gt; report.&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;serving&quot;&gt;Serving&lt;/h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Flag&lt;/th&gt;&lt;th&gt;Default&lt;/th&gt;&lt;th&gt;Meaning&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;--port &amp;lt;n&amp;gt;&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;11435&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Loopback port.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;--max-length &amp;lt;n&amp;gt;&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;32768&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Context length the engine is built for.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;--max-batch &amp;lt;n&amp;gt;&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;16&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Concurrent requests the engine schedules. &lt;code&gt;1&lt;/code&gt; makes the server serial end to end.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;--speculation-k &amp;lt;n&amp;gt;&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;0&lt;/code&gt;&lt;/td&gt;&lt;td&gt;MTP speculation depth (0 = off). The engine varies the effective k downward per request.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;--tower &amp;lt;dir&amp;gt;&lt;/code&gt;&lt;/td&gt;&lt;td&gt;—&lt;/td&gt;&lt;td&gt;Core ML vision tower. Required for image input.&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;There is &lt;strong&gt;no &lt;code&gt;--token&lt;/code&gt; flag&lt;/strong&gt;: nothing is authenticated. See
&lt;a href=&quot;../limits/#no-authentication&quot;&gt;Limits&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;environment&quot;&gt;Environment&lt;/h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Variable&lt;/th&gt;&lt;th&gt;Effect&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;OLLAMA_MODELS&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Where to look for Ollama&#39;s store. Defaults to &lt;code&gt;~/.ollama/models&lt;/code&gt;.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;AS_KV_OFFLOAD&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;0&lt;/code&gt; disables the on-by-default KV disk cache. See &lt;a href=&quot;../limits/#kv-cache-spills-to-disk-by-default&quot;&gt;Limits&lt;/a&gt;.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;AS_KV_OFFLOAD_GB&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Disk cap for that cache, default &lt;code&gt;16&lt;/code&gt;.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;AS_KV_OFFLOAD_PATH&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Its location, default &lt;code&gt;~/Library/Caches/tempo9/kv&lt;/code&gt;.&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Connect an app</title>
        <published>2026-08-31T00:00:00+00:00</published>
        <updated>2026-08-31T00:00:00+00:00</updated>
        
        <author>
          <name>Unknown</name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://www.thinkspread.com/tempo9/manual/connect-apps/"/>
        <id>https://www.thinkspread.com/tempo9/manual/connect-apps/</id>
        
        <content type="html" xml:base="https://www.thinkspread.com/tempo9/manual/connect-apps/">&lt;!-- GENERATED from manual/connect-apps.md by scripts/sync_manual_to_site.py. Edit the manual, not this file. --&gt;
&lt;p&gt;Tempo9 speaks the protocols your tools already speak. In most
cases connecting one is a change of address, not a change of code — and if
you have already pulled models with Ollama, there is nothing to download
either.&lt;/p&gt;
&lt;h2 id=&quot;start-the-server&quot;&gt;Start the server&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;tempo9 --gguf ~/models/qwen3-8b-q4ks.gguf
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It listens on &lt;code&gt;http://127.0.0.1:11435&lt;/code&gt;, &lt;strong&gt;loopback only&lt;/strong&gt; — the wildcard is
never bound, so nothing off this machine can reach it. Change the port with
&lt;code&gt;--port&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&quot;your-app-has-a-custom-openai-endpoint-field&quot;&gt;Your app has a &quot;custom OpenAI endpoint&quot; field&lt;/h2&gt;
&lt;p&gt;Most local-AI apps do. Fill it in:&lt;/p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Field&lt;/th&gt;&lt;th&gt;Value&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Base URL&lt;/td&gt;&lt;td&gt;&lt;code&gt;http://127.0.0.1:11435/v1&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;API key&lt;/td&gt;&lt;td&gt;anything, or leave blank&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Model&lt;/td&gt;&lt;td&gt;whatever &lt;code&gt;/v1/models&lt;/code&gt; reports&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;No key is required and none is checked, which is safe here only because the
server is loopback-bound. Do not put it behind a tunnel or a reverse proxy
and leave it that way.&lt;/p&gt;
&lt;p&gt;Apps in this category include Cherry Studio, AnythingLLM, and anything else
built on an OpenAI client — we have not tested each one, so treat this as
&quot;the field exists and this is what goes in it&quot; rather than a certification.&lt;/p&gt;
&lt;h2 id=&quot;your-app-only-knows-how-to-talk-to-ollama&quot;&gt;Your app only knows how to talk to Ollama&lt;/h2&gt;
&lt;p&gt;Some apps do not offer a generic endpoint field at all; they have an Ollama
integration and nothing else. Those work too. Point them at:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;http://127.0.0.1:11435
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Tempo9 serves Ollama&#39;s native routes alongside its own: &lt;code&gt;/api/tags&lt;/code&gt;,
&lt;code&gt;/api/chat&lt;/code&gt;, &lt;code&gt;/api/generate&lt;/code&gt;, &lt;code&gt;/api/show&lt;/code&gt;, &lt;code&gt;/api/version&lt;/code&gt;, &lt;code&gt;/api/ps&lt;/code&gt;.
Streaming is newline-delimited JSON and tool-call arguments are objects,
exactly as those clients expect.&lt;/p&gt;
&lt;h2 id=&quot;you-already-downloaded-models-with-ollama&quot;&gt;You already downloaded models with Ollama&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;Full detail, including what is never written: &lt;a href=&quot;../ollama-models/&quot;&gt;Ollama models&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Then you do not need to download them again. Ollama stores each model as a
plain GGUF file, which is what Tempo9 loads, so it can read them where they
sit:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;tempo9 --list-ollama
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;qwen3:8b            4.68 GiB  /Users/you/.ollama/models/blobs/sha256-1f3d...
llama3.2:latest     1.88 GiB  /Users/you/.ollama/models/blobs/sha256-a41c...
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;tempo9 --ollama qwen3:8b
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is &lt;strong&gt;read-only&lt;/strong&gt;. Nothing is copied, converted, moved, or written into
Ollama&#39;s store — your models stay exactly as Ollama left them, and Ollama
keeps working. If you moved the store, &lt;code&gt;OLLAMA_MODELS&lt;/code&gt; is respected.&lt;/p&gt;
&lt;p&gt;A bare name means &lt;code&gt;:latest&lt;/code&gt;, the same as &lt;code&gt;ollama run&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&quot;agent-tools&quot;&gt;Agent tools&lt;/h2&gt;
&lt;p&gt;Claude Code, Codex and similar clients do not use the OpenAI chat protocol —
they each want their own. Tempo9 serves all three from one process:&lt;/p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Protocol&lt;/th&gt;&lt;th&gt;Route&lt;/th&gt;&lt;th&gt;Spoken by&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;OpenAI Chat Completions&lt;/td&gt;&lt;td&gt;&lt;code&gt;/v1/chat/completions&lt;/code&gt;&lt;/td&gt;&lt;td&gt;most apps and SDKs&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Anthropic Messages&lt;/td&gt;&lt;td&gt;&lt;code&gt;/v1/messages&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Claude Code&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;OpenAI Responses&lt;/td&gt;&lt;td&gt;&lt;code&gt;/v1/responses&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Codex&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Ollama native&lt;/td&gt;&lt;td&gt;&lt;code&gt;/api/chat&lt;/code&gt;&lt;/td&gt;&lt;td&gt;apps with an Ollama integration&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Agent workloads are the case Tempo9 is built for: many requests at once, long
and growing context, heavy tool calling. That is where the architecture shows
up rather than in a single stream — see the benchmarks.&lt;/p&gt;
&lt;h2 id=&quot;what-it-does-not-do&quot;&gt;What it does not do&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;Also: &lt;a href=&quot;../limits/&quot;&gt;Limits&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Worth knowing before you wire something up:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;No embeddings.&lt;/strong&gt; &lt;code&gt;/api/embed&lt;/code&gt; and &lt;code&gt;/v1/embeddings&lt;/code&gt; are not served;
the Ollama route answers &lt;code&gt;501&lt;/code&gt; and says so rather than pretending.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;One model per process.&lt;/strong&gt; &lt;code&gt;/v1/models&lt;/code&gt; and &lt;code&gt;/api/tags&lt;/code&gt; report the one
that is loaded. Run a second server on another port for a second model.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Vision needs a tower.&lt;/strong&gt; Image input works with &lt;code&gt;--tower &amp;lt;dir&amp;gt;&lt;/code&gt;. A GGUF
vision projector inside an Ollama model is detected but not loaded; the
server tells you when it sees one.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No authentication.&lt;/strong&gt; By design, and only defensible because it is
loopback-bound.&lt;/li&gt;
&lt;/ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Getting started</title>
        <published>2026-08-31T00:00:00+00:00</published>
        <updated>2026-08-31T00:00:00+00:00</updated>
        
        <author>
          <name>Unknown</name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://www.thinkspread.com/tempo9/manual/getting-started/"/>
        <id>https://www.thinkspread.com/tempo9/manual/getting-started/</id>
        
        <content type="html" xml:base="https://www.thinkspread.com/tempo9/manual/getting-started/">&lt;!-- GENERATED from manual/getting-started.md by scripts/sync_manual_to_site.py. Edit the manual, not this file. --&gt;
&lt;h2 id=&quot;run-a-model&quot;&gt;Run a model&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;tempo9 --gguf ~/models/qwen3-8b-q4ks.gguf
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That is the whole setup. There is no configuration file, no daemon to
install, and no model conversion step — Tempo9 loads GGUF directly.&lt;/p&gt;
&lt;p&gt;The server listens on &lt;code&gt;http://127.0.0.1:11435&lt;/code&gt;, &lt;strong&gt;loopback only&lt;/strong&gt;. The
wildcard address is never bound, so nothing off this machine can reach it.&lt;/p&gt;
&lt;h2 id=&quot;first-request&quot;&gt;First request&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;curl http://127.0.0.1:11435/v1/chat/completions \
  -d &amp;#39;{&amp;quot;model&amp;quot;:&amp;quot;local&amp;quot;,&amp;quot;messages&amp;quot;:[{&amp;quot;role&amp;quot;:&amp;quot;user&amp;quot;,&amp;quot;content&amp;quot;:&amp;quot;hello&amp;quot;}]}&amp;#39;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No API key is required and none is checked. That is safe only because of the
loopback bind — see &lt;a href=&quot;../limits/#no-authentication&quot;&gt;Limits&lt;/a&gt; before you put it
behind a tunnel or a reverse proxy.&lt;/p&gt;
&lt;h2 id=&quot;if-you-already-use-ollama&quot;&gt;If you already use Ollama&lt;/h2&gt;
&lt;p&gt;You do not need to download anything:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;tempo9 --list-ollama
tempo9 --ollama qwen3:8b
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;See &lt;a href=&quot;../ollama-models/&quot;&gt;Ollama models&lt;/a&gt; for how that works and what it does
not touch.&lt;/p&gt;
&lt;h2 id=&quot;where-to-go-next&quot;&gt;Where to go next&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Wiring up an app you already use: &lt;a href=&quot;../connect-apps/&quot;&gt;Connect an app&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Every flag: &lt;a href=&quot;../cli/&quot;&gt;CLI reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Every route: &lt;a href=&quot;../api/&quot;&gt;API reference&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Limits</title>
        <published>2026-08-31T00:00:00+00:00</published>
        <updated>2026-08-31T00:00:00+00:00</updated>
        
        <author>
          <name>Unknown</name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://www.thinkspread.com/tempo9/manual/limits/"/>
        <id>https://www.thinkspread.com/tempo9/manual/limits/</id>
        
        <content type="html" xml:base="https://www.thinkspread.com/tempo9/manual/limits/">&lt;!-- GENERATED from manual/limits.md by scripts/sync_manual_to_site.py. Edit the manual, not this file. --&gt;
&lt;p&gt;Things worth knowing before you design around them.&lt;/p&gt;
&lt;h2 id=&quot;no-embeddings&quot;&gt;No embeddings&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;/v1/embeddings&lt;/code&gt; is not served and &lt;code&gt;/api/embed&lt;/code&gt; answers &lt;code&gt;501&lt;/code&gt; with a message
saying so. If your pipeline needs embeddings, get them elsewhere.&lt;/p&gt;
&lt;h2 id=&quot;one-model-per-process&quot;&gt;One model per process&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;/v1/models&lt;/code&gt; and &lt;code&gt;/api/tags&lt;/code&gt; report the single loaded model. To serve a
second model, run a second &lt;code&gt;tempo9&lt;/code&gt; on another port.&lt;/p&gt;
&lt;h2 id=&quot;vision-needs-a-core-ml-tower&quot;&gt;Vision needs a Core ML tower&lt;/h2&gt;
&lt;p&gt;Image input works when the server is started with &lt;code&gt;--tower &amp;lt;dir&amp;gt;&lt;/code&gt;, where
&lt;em&gt;dir&lt;/em&gt; is the directory containing &lt;code&gt;tower_meta.json&lt;/code&gt; — the &lt;code&gt;coreml/&lt;/code&gt;
&lt;strong&gt;subdirectory&lt;/strong&gt; of an exported tower, not its parent:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;--tower ~/models/qwen3vl_tower/coreml     # correct
--tower ~/models/qwen3vl_tower            # missingAsset: .../tower_meta.json
--tower ~/models/…/model-mmproj.gguf      # same error — a GGUF projector is not a tower
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A GGUF mmproj is what a tower is exported &lt;em&gt;from&lt;/em&gt;, offline; it is not a
drop-in. A GGUF
vision projector inside an Ollama model &lt;strong&gt;is detected but not loaded&lt;/strong&gt; — the
server prints a line saying so rather than leaving you to wonder why images
do nothing.&lt;/p&gt;
&lt;h2 id=&quot;no-authentication&quot;&gt;No authentication&lt;/h2&gt;
&lt;p&gt;Nothing checks a key, and there is no flag to make it. This is defensible
only because the listener binds &lt;code&gt;127.0.0.1&lt;/code&gt; explicitly and never the
wildcard, so the server is unreachable from other machines.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Do not put it behind a tunnel, a reverse proxy, or a port forward and
leave it that way.&lt;/strong&gt; That removes the only thing protecting it.&lt;/p&gt;
&lt;h2 id=&quot;kv-cache-spills-to-disk-by-default&quot;&gt;KV cache spills to disk, by default&lt;/h2&gt;
&lt;p&gt;Tempo9 keeps reusable KV prefixes in RAM and, when they overflow the RAM
watermark, &lt;strong&gt;writes them to disk&lt;/strong&gt; so a later session restores from SSD
instead of re-computing the prompt. This is on by default.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What it does and does not buy you.&lt;/strong&gt; Measured on a restarted engine with the
same prompt, against the engine&#39;s own &lt;code&gt;restored=&lt;/code&gt; counter and with the cache
directory wiped as a control: with the cache preserved the first request after
a restart reports &lt;code&gt;restored=47&lt;/code&gt; spans and the &lt;em&gt;second&lt;/em&gt; request takes 0.61 s;
with the directory wiped the same request reports &lt;code&gt;restored=0&lt;/code&gt; and takes
5.20 s. The restore is real and it is worth 8.5x — but &lt;strong&gt;the first request
after a restart pays full prefill either way&lt;/strong&gt;. An earlier version of this page
implied that first request came back fast. It does not.&lt;/p&gt;
&lt;p&gt;What that means for your disk:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Cache files live in &lt;code&gt;~/Library/Caches/tempo9/kv&lt;/code&gt; (&lt;code&gt;AS_KV_OFFLOAD_PATH&lt;/code&gt;
moves it), capped at &lt;strong&gt;16 GB&lt;/strong&gt; (&lt;code&gt;AS_KV_OFFLOAD_GB&lt;/code&gt; changes the cap; oldest
entries are evicted first).&lt;/li&gt;
&lt;li&gt;The location is deliberate: macOS may purge it under disk pressure, and
Time Machine does not back it up. Losing the cache costs a re-prefill,
never data.&lt;/li&gt;
&lt;li&gt;The cache &lt;strong&gt;survives restarts&lt;/strong&gt; — the on-disk index reloads at startup.&lt;/li&gt;
&lt;li&gt;A cache from a different model, dtype or KV layout is detected by a
config hash and wiped rather than served back wrong.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To keep the disk untouched:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;AS_KV_OFFLOAD=0 tempo9 --gguf model.gguf
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;An unwritable or full cache directory is not an error — the engine logs it
and runs without offload.&lt;/p&gt;
&lt;h2 id=&quot;ollama-s-store-is-read-never-written&quot;&gt;Ollama&#39;s store is read, never written&lt;/h2&gt;
&lt;p&gt;Using &lt;code&gt;--ollama&lt;/code&gt; opens Ollama&#39;s blobs in place. Nothing is copied,
converted, moved, or written into their store. The one file Tempo9 creates
is a symlink in &lt;strong&gt;its own&lt;/strong&gt; cache — see
&lt;a href=&quot;../ollama-models/#why-a-symlink&quot;&gt;Ollama models&lt;/a&gt;.&lt;/p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Models and quantizations</title>
        <published>2026-08-31T00:00:00+00:00</published>
        <updated>2026-08-31T00:00:00+00:00</updated>
        
        <author>
          <name>Unknown</name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://www.thinkspread.com/tempo9/manual/models/"/>
        <id>https://www.thinkspread.com/tempo9/manual/models/</id>
        
        <content type="html" xml:base="https://www.thinkspread.com/tempo9/manual/models/">&lt;!-- GENERATED from manual/models.md by scripts/sync_manual_to_site.py. Edit the manual, not this file. --&gt;
&lt;p&gt;The question before downloading anything is &quot;will my file run&quot;. Two axes
decide it, and neither is visible from the other: the &lt;strong&gt;quantization&lt;/strong&gt; and
the &lt;strong&gt;architecture&lt;/strong&gt;. A third, the tokenizer, hides inside the second.&lt;/p&gt;
&lt;p&gt;Nothing here is inferred from source. Every &lt;code&gt;verified&lt;/code&gt; below means a model
in that format answered prompts on this machine.&lt;/p&gt;
&lt;h2 id=&quot;quantizations&quot;&gt;Quantizations&lt;/h2&gt;
&lt;p&gt;GGUF blocks are used &lt;strong&gt;as they are&lt;/strong&gt;. Nothing is dequantized on load, so a
Q4_K file costs Q4_K memory rather than expanding to FP16.&lt;/p&gt;
&lt;h3 id=&quot;verified-a-real-model-in-this-format-was-served&quot;&gt;Verified — a real model in this format was served&lt;/h3&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;GGUF type&lt;/th&gt;&lt;th&gt;Block&lt;/th&gt;&lt;th&gt;Seen in&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;Q8_0&lt;/code&gt;&lt;/td&gt;&lt;td&gt;32 / 34 B&lt;/td&gt;&lt;td&gt;Qwen3 0.6B, Qwen3.5 0.8B&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;Q4_K&lt;/code&gt; (&lt;code&gt;Q4_K_S&lt;/code&gt;, &lt;code&gt;Q4_K_M&lt;/code&gt;, &lt;code&gt;UD-Q4_K_XL&lt;/code&gt;)&lt;/td&gt;&lt;td&gt;256 / 144 B&lt;/td&gt;&lt;td&gt;most of the table below&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;Q5_K&lt;/code&gt;&lt;/td&gt;&lt;td&gt;256 / 176 B&lt;/td&gt;&lt;td&gt;as the companion type inside &lt;code&gt;Q4_K_M&lt;/code&gt; files&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;Q6_K&lt;/code&gt;&lt;/td&gt;&lt;td&gt;256 / 210 B&lt;/td&gt;&lt;td&gt;Qwen3.5 4B Q6_K&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;Q3_K&lt;/code&gt; (&lt;code&gt;Q3_K_M&lt;/code&gt;)&lt;/td&gt;&lt;td&gt;256 / 110 B&lt;/td&gt;&lt;td&gt;Qwen3.5 35B-A3B&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;MXFP4&lt;/code&gt;&lt;/td&gt;&lt;td&gt;32 / 17 B&lt;/td&gt;&lt;td&gt;GPT-OSS 20B&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;F32&lt;/code&gt;&lt;/td&gt;&lt;td&gt;—&lt;/td&gt;&lt;td&gt;MoE routing gates, zero-copy&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;&lt;code&gt;Q4_K_M&lt;/code&gt; and &lt;code&gt;Q5_K_M&lt;/code&gt; files are &lt;strong&gt;mixed&lt;/strong&gt;: some layers promote &lt;code&gt;attn_v&lt;/code&gt; or
&lt;code&gt;ffn_down&lt;/code&gt; to &lt;code&gt;Q6_K&lt;/code&gt; while the rest stay &lt;code&gt;Q4_K&lt;/code&gt;. That is handled per segment,
which is why &lt;code&gt;Q5_K&lt;/code&gt; and &lt;code&gt;Q6_K&lt;/code&gt; appear as companions above without a
single-format file of their own.&lt;/p&gt;
&lt;h3 id=&quot;supported-but-no-model-in-this-format-has-been-run-here&quot;&gt;Supported, but no model in this format has been run here&lt;/h3&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;GGUF type&lt;/th&gt;&lt;th&gt;Note&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;Q4_0&lt;/code&gt;&lt;/td&gt;&lt;td&gt;The ARM edge tier. Code path present on all sides.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;F16&lt;/code&gt; / &lt;code&gt;BF16&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Converted to FP32 at load. Fine for the KB–MB tensors that use it; a whole model in F16 is not the intended path.&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h3 id=&quot;refused-with-an-error-that-says-why&quot;&gt;Refused, with an error that says why&lt;/h3&gt;
&lt;p&gt;There is &lt;strong&gt;no silent fallback&lt;/strong&gt;. A file in one of these formats fails at
load with an actionable message rather than running slowly or badly:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;IQ1&lt;/code&gt; – &lt;code&gt;IQ4&lt;/code&gt; · &lt;code&gt;Q2_K&lt;/code&gt; · &lt;code&gt;Q4_1&lt;/code&gt; · &lt;code&gt;Q5_0&lt;/code&gt; · &lt;code&gt;Q5_1&lt;/code&gt; ·
&lt;code&gt;Q4_0_4_4&lt;/code&gt; / &lt;code&gt;4_8&lt;/code&gt; / &lt;code&gt;8_8&lt;/code&gt; (removed from GGUF upstream) · &lt;code&gt;TQ*&lt;/code&gt; ·
&lt;code&gt;NVFP4&lt;/code&gt; · &lt;code&gt;Q8_K&lt;/code&gt; (an activation-only type — in a weight table it means the
file is corrupt)&lt;/p&gt;
&lt;p&gt;If your file is one of these, the usual fix is a &lt;code&gt;Q4_K_M&lt;/code&gt; or &lt;code&gt;Q8_0&lt;/code&gt; build of
the same model.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;On picking a quantization for serving&lt;/strong&gt;, &lt;code&gt;Q4_K_S&lt;/code&gt; is generally preferred
over &lt;code&gt;Q4_K_M&lt;/code&gt; here: no faster &lt;code&gt;Q6_K&lt;/code&gt; kernel exists, so the promoted tensors
in a &lt;code&gt;_M&lt;/code&gt; file cost more than they return.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;architectures&quot;&gt;Architectures&lt;/h2&gt;
&lt;p&gt;GGUF direct load accepts seven: &lt;code&gt;qwen35&lt;/code&gt;, &lt;code&gt;qwen35moe&lt;/code&gt;, &lt;code&gt;gemma4&lt;/code&gt;, &lt;code&gt;qwen2vl&lt;/code&gt;,
&lt;code&gt;qwen3&lt;/code&gt;, &lt;code&gt;qwen3moe&lt;/code&gt;, and &lt;code&gt;llama&lt;/code&gt; — the last covering the Llama line and most
of Mistral.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The architecture field does not tell you whether a file will run&lt;/strong&gt;, because
the tokenizer is a second axis and invisible from there. Mistral 7B v0.3 and
Llama 3.2 are both &lt;code&gt;architecture=llama&lt;/code&gt;; one writes a SentencePiece
tokenizer and the other writes &lt;code&gt;gpt2&lt;/code&gt;.&lt;/p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Family&lt;/th&gt;&lt;th&gt;Arch&lt;/th&gt;&lt;th&gt;Tokenizer&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Llama 3.1 / 3.2 / 3.3&lt;/td&gt;&lt;td&gt;&lt;code&gt;llama&lt;/code&gt;&lt;/td&gt;&lt;td&gt;gpt2&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Mistral v0.1 – v0.3&lt;/td&gt;&lt;td&gt;&lt;code&gt;llama&lt;/code&gt;&lt;/td&gt;&lt;td&gt;SentencePiece&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Llama 2, CodeLlama, Vicuna&lt;/td&gt;&lt;td&gt;&lt;code&gt;llama&lt;/code&gt;&lt;/td&gt;&lt;td&gt;SentencePiece&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Qwen3.5 / Qwen3 / Gemma 4&lt;/td&gt;&lt;td&gt;own&lt;/td&gt;&lt;td&gt;gpt2 / gemma4&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Both sides of that split work. A dialect the engine does not implement is
&lt;strong&gt;refused by name&lt;/strong&gt; — never guessed — because a vocabulary read with the
wrong algorithm still produces ids, and those ids decode into fluent text
from the wrong distribution. A wrong answer that reads well is worse than an
error.&lt;/p&gt;
&lt;h2 id=&quot;verified-models&quot;&gt;Verified models&lt;/h2&gt;
&lt;p&gt;Produced by serving requests, not by reading a registry:
&lt;code&gt;bash harness/support_matrix.sh&lt;/code&gt;.&lt;/p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Model&lt;/th&gt;&lt;th&gt;Load&lt;/th&gt;&lt;th&gt;Text&lt;/th&gt;&lt;th&gt;10k context&lt;/th&gt;&lt;th&gt;Thinking&lt;/th&gt;&lt;th&gt;Image&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;gemma4-12b-it-q4ks&lt;/td&gt;&lt;td&gt;ok&lt;/td&gt;&lt;td&gt;PASS&lt;/td&gt;&lt;td&gt;PASS&lt;/td&gt;&lt;td&gt;—&lt;/td&gt;&lt;td&gt;PASS&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;gptoss-20b-mxfp4&lt;/td&gt;&lt;td&gt;ok&lt;/td&gt;&lt;td&gt;PASS&lt;/td&gt;&lt;td&gt;PASS&lt;/td&gt;&lt;td&gt;PASS&lt;/td&gt;&lt;td&gt;—&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;llama31-8b-q4km&lt;/td&gt;&lt;td&gt;ok&lt;/td&gt;&lt;td&gt;PASS&lt;/td&gt;&lt;td&gt;PASS&lt;/td&gt;&lt;td&gt;—&lt;/td&gt;&lt;td&gt;—&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;llama32-1b-q4km&lt;/td&gt;&lt;td&gt;ok&lt;/td&gt;&lt;td&gt;PASS&lt;/td&gt;&lt;td&gt;PASS&lt;/td&gt;&lt;td&gt;—&lt;/td&gt;&lt;td&gt;—&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;ministral3-3b-q4km&lt;/td&gt;&lt;td&gt;ok&lt;/td&gt;&lt;td&gt;PASS&lt;/td&gt;&lt;td&gt;PASS&lt;/td&gt;&lt;td&gt;—&lt;/td&gt;&lt;td&gt;—&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;mistral7b-v03-q4km&lt;/td&gt;&lt;td&gt;ok&lt;/td&gt;&lt;td&gt;PASS&lt;/td&gt;&lt;td&gt;PASS&lt;/td&gt;&lt;td&gt;—&lt;/td&gt;&lt;td&gt;—&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;qwen3-0.6b-q8_0&lt;/td&gt;&lt;td&gt;ok&lt;/td&gt;&lt;td&gt;PASS&lt;/td&gt;&lt;td&gt;PASS&lt;/td&gt;&lt;td&gt;PASS&lt;/td&gt;&lt;td&gt;—&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;qwen35-0.8b-q8_0&lt;/td&gt;&lt;td&gt;ok&lt;/td&gt;&lt;td&gt;PASS&lt;/td&gt;&lt;td&gt;PASS&lt;/td&gt;&lt;td&gt;PASS&lt;/td&gt;&lt;td&gt;—&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;qwen35-35b-a3b-q3km&lt;/td&gt;&lt;td&gt;ok&lt;/td&gt;&lt;td&gt;PASS&lt;/td&gt;&lt;td&gt;PASS&lt;/td&gt;&lt;td&gt;PASS&lt;/td&gt;&lt;td&gt;PASS&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;qwen35-4b-q4ks&lt;/td&gt;&lt;td&gt;ok&lt;/td&gt;&lt;td&gt;PASS&lt;/td&gt;&lt;td&gt;PASS&lt;/td&gt;&lt;td&gt;PASS&lt;/td&gt;&lt;td&gt;—&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;qwen35-4b-q6k&lt;/td&gt;&lt;td&gt;ok&lt;/td&gt;&lt;td&gt;PASS&lt;/td&gt;&lt;td&gt;PASS&lt;/td&gt;&lt;td&gt;PASS&lt;/td&gt;&lt;td&gt;—&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;qwen35-9b-imat-q4ks&lt;/td&gt;&lt;td&gt;ok&lt;/td&gt;&lt;td&gt;PASS&lt;/td&gt;&lt;td&gt;PASS&lt;/td&gt;&lt;td&gt;PASS&lt;/td&gt;&lt;td&gt;PASS&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;qwen38-27b-q4ks&lt;/td&gt;&lt;td&gt;ok&lt;/td&gt;&lt;td&gt;PASS&lt;/td&gt;&lt;td&gt;PASS&lt;/td&gt;&lt;td&gt;—&lt;/td&gt;&lt;td&gt;PASS&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;qwen3vl-4b-q4km&lt;/td&gt;&lt;td&gt;ok&lt;/td&gt;&lt;td&gt;PASS&lt;/td&gt;&lt;td&gt;PASS&lt;/td&gt;&lt;td&gt;—&lt;/td&gt;&lt;td&gt;PASS&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;qwen3vlmoe-30b-a3b-q4km&lt;/td&gt;&lt;td&gt;ok&lt;/td&gt;&lt;td&gt;PASS&lt;/td&gt;&lt;td&gt;PASS&lt;/td&gt;&lt;td&gt;—&lt;/td&gt;&lt;td&gt;PASS&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;tinyllama-1.1b-q4km&lt;/td&gt;&lt;td&gt;ok&lt;/td&gt;&lt;td&gt;PASS&lt;/td&gt;&lt;td&gt;N/A&lt;/td&gt;&lt;td&gt;—&lt;/td&gt;&lt;td&gt;—&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;A blank cell means &lt;strong&gt;nothing was run&lt;/strong&gt; — no vision front end paired with that
model, or no separate thinking mode to test. &lt;code&gt;N/A&lt;/code&gt; means the model never
claimed it: TinyLlama declares a 2048-token context, so a 10k prompt is
outside what it offers. Neither is a failure, and both are printed rather
than omitted, because the gap between &quot;not tested&quot; and &quot;does not work&quot; is
the entire reason to publish a table instead of a claim.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;10k context&lt;/code&gt; column exists because every check here was once a short
prompt, and the table was green for two models that produced word salad past
about 8k.&lt;/p&gt;
&lt;h2 id=&quot;what-the-model-registry-is-not&quot;&gt;What the model registry is not&lt;/h2&gt;
&lt;p&gt;The engine&#39;s C++ registry lists roughly thirty model families — LLaMA v1–v4,
ChatGLM, DeepSeek, GLM-5, MiniMax-M2, Baichuan, the Qwen line. Those reach
the engine through a Python conversion path, and &lt;strong&gt;none of them is verified
on Metal&lt;/strong&gt;. Reading that registry as a support list would overstate this
page by an order of magnitude, which is why the page is generated by running
models instead.&lt;/p&gt;
&lt;p&gt;Commonly recommended for a 24 GB Mac, and where they stand:&lt;/p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Model&lt;/th&gt;&lt;th&gt;Status&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Llama 3.3 8B&lt;/td&gt;&lt;td&gt;yes — verified at 3.1 8B&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Mistral 7B v0.3&lt;/td&gt;&lt;td&gt;yes&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;GPT-OSS 20B&lt;/td&gt;&lt;td&gt;yes — MXFP4&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Mistral Small 3.2 24B&lt;/td&gt;&lt;td&gt;architecture yes, this size untested&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Qwen3.6-27B&lt;/td&gt;&lt;td&gt;untested&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Gemma 4 31B&lt;/td&gt;&lt;td&gt;architecture yes, this size untested&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Phi-4&lt;/td&gt;&lt;td&gt;no&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Ollama models</title>
        <published>2026-08-31T00:00:00+00:00</published>
        <updated>2026-08-31T00:00:00+00:00</updated>
        
        <author>
          <name>Unknown</name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://www.thinkspread.com/tempo9/manual/ollama-models/"/>
        <id>https://www.thinkspread.com/tempo9/manual/ollama-models/</id>
        
        <content type="html" xml:base="https://www.thinkspread.com/tempo9/manual/ollama-models/">&lt;!-- GENERATED from manual/ollama-models.md by scripts/sync_manual_to_site.py. Edit the manual, not this file. --&gt;
&lt;p&gt;If you already pulled models with Ollama, Tempo9 can run them &lt;strong&gt;where they
sit&lt;/strong&gt; — no download, no copy, no conversion.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;tempo9 --list-ollama
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;qwen3:8b            4.68 GiB  /Users/you/.ollama/models/blobs/sha256-1f3d...
llama3.2:latest     1.88 GiB  /Users/you/.ollama/models/blobs/sha256-a41c...
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;tempo9 --ollama qwen3:8b
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A bare name means &lt;code&gt;:latest&lt;/code&gt;, the same as &lt;code&gt;ollama run&lt;/code&gt;. If you moved the
store, &lt;code&gt;OLLAMA_MODELS&lt;/code&gt; is respected.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Ollama does not need to be running.&lt;/strong&gt; This is not a protocol; nothing
talks to the Ollama process. It is a read of its files.&lt;/p&gt;
&lt;h2 id=&quot;how-it-works&quot;&gt;How it works&lt;/h2&gt;
&lt;p&gt;Ollama has no model format of its own. It stores plain GGUF wrapped in an
OCI-style content-addressed layout for deduplication and versioning:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;~/.ollama/models/
├── manifests/registry.ollama.ai/library/qwen3/8b   small JSON — the path IS the name
└── blobs/sha256-1f3d...                            bare GGUF — the weights
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The manifest lists layers:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;json&quot;&gt;{&amp;quot;schemaVersion&amp;quot;:2,&amp;quot;layers&amp;quot;:[
  {&amp;quot;mediaType&amp;quot;:&amp;quot;application/vnd.ollama.image.model&amp;quot;,   &amp;quot;digest&amp;quot;:&amp;quot;sha256:1f3d…&amp;quot;,&amp;quot;size&amp;quot;:4831838208},
  {&amp;quot;mediaType&amp;quot;:&amp;quot;application/vnd.ollama.image.template&amp;quot;,&amp;quot;digest&amp;quot;:&amp;quot;sha256:a7c1…&amp;quot;,&amp;quot;size&amp;quot;:1482}
]}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The weights are the &lt;code&gt;.model&lt;/code&gt; layer. Its digest &lt;code&gt;sha256:1f3d…&lt;/code&gt; names the file
&lt;code&gt;blobs/sha256-1f3d…&lt;/code&gt; — &lt;strong&gt;colon becomes dash&lt;/strong&gt;. That blob is byte-identical to
the GGUF you would download yourself, and Tempo9 loads GGUF, so it simply
opens it.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;registry.ollama.ai/library&lt;/code&gt; is implicit in Ollama&#39;s display convention, so
that manifest shows as &lt;code&gt;qwen3:8b&lt;/code&gt;. Models from elsewhere keep their full
&lt;code&gt;host/namespace/model:tag&lt;/code&gt; name, and still answer to the short form.&lt;/p&gt;
&lt;h2 id=&quot;what-is-deliberately-not-used&quot;&gt;What is deliberately not used&lt;/h2&gt;
&lt;p&gt;The manifest also carries &lt;code&gt;template&lt;/code&gt; and &lt;code&gt;params&lt;/code&gt; layers. Tempo9 ignores
them and uses the &lt;strong&gt;chat template embedded in the GGUF&lt;/strong&gt; — the one the model
author shipped.&lt;/p&gt;
&lt;p&gt;Ollama&#39;s template layer is written in Go&#39;s &lt;code&gt;text/template&lt;/code&gt;; ours is Jinja.
Honouring theirs would mean implementing a second template engine to obtain
a transcription of something we already have in the original.&lt;/p&gt;
&lt;h2 id=&quot;why-a-symlink&quot;&gt;Why a symlink&lt;/h2&gt;
&lt;p&gt;Tempo9 creates one file, in its own cache, never in Ollama&#39;s store:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;~/.cache/tempo9/ollama/qwen3-8b.gguf -&amp;gt; ~/.ollama/models/blobs/sha256-1f3d...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The engine currently decides GGUF-vs-graph by the &lt;strong&gt;file extension&lt;/strong&gt;, and
Ollama&#39;s blobs have none. Handed a blob path directly it refuses, and it
refuses silently. The link works around that, and gives the engine&#39;s graph
cache a legible key besides — that cache is keyed on basename, size and
mtime, and &lt;code&gt;qwen3-8b.gguf&lt;/code&gt; identifies itself where a digest would not.&lt;/p&gt;
&lt;p&gt;A link left pointing at a blob that a re-pull replaced is detected and
rewritten, so a re-pulled tag never serves last week&#39;s weights.&lt;/p&gt;
&lt;h2 id=&quot;what-it-will-not-do&quot;&gt;What it will not do&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Never writes to Ollama&#39;s store.&lt;/strong&gt; Read-only: directory listing, file
reads, attribute lookups. Nothing else.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Skips a model whose blob is missing.&lt;/strong&gt; Interrupted pulls leave manifests
behind; listing one would turn a clean &quot;not found&quot; into a confusing load
failure later.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Trusts the file over the manifest for size&lt;/strong&gt;, because a truncated pull
leaves the advertised size intact and the bytes short.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Does not load a GGUF vision projector.&lt;/strong&gt; It is detected and reported;
image input needs &lt;code&gt;--tower&lt;/code&gt;. See &lt;a href=&quot;../limits/#vision-needs-a-core-ml-tower&quot;&gt;Limits&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>本地推理是为一个人打字设计的。Agent 不是那样跑的。</title>
        <published>2026-08-30T00:00:00+00:00</published>
        <updated>2026-08-30T00:00:00+00:00</updated>
        
        <author>
          <name>Unknown</name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://www.thinkspread.com/blog/agents-are-not-one-person-typing-zh/"/>
        <id>https://www.thinkspread.com/blog/agents-are-not-one-person-typing-zh/</id>
        
        <content type="html" xml:base="https://www.thinkspread.com/blog/agents-are-not-one-person-typing-zh/">&lt;p&gt;llama.cpp 和 MLX 都很好,而且在一件事上比我们强:&lt;strong&gt;一个人、一个流、一个
问题&lt;/strong&gt;。这篇文章不打算争那一格 —— 我们在 M5 Pro 上单流 prefill 是对手的
0.90×,7k 上下文单流更是 0.64×,输得很清楚。&lt;/p&gt;
&lt;p&gt;但那不是 agent 的形状。Agent 的形状是:&lt;strong&gt;好几个同时在跑、每轮重发一大段
system prompt、不停调工具、偶尔塞一张图进来。&lt;/strong&gt; 一旦切到这个形状,排序会
翻过来 —— 有几格甚至不是&quot;慢&quot;,是&lt;strong&gt;开不起来&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;Tempo9 是把服务器端那套东西(continuous batching、paged KV、异构执行)
搬到 Apple Silicon 上的一次尝试。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2 id=&quot;yi-bing-fa-xia-dui-shou-bu-shi-man-shi-qi-bu-lai&quot;&gt;一、并发下,对手不是慢,是起不来&lt;/h2&gt;
&lt;p&gt;同一台 24 GB M5 Pro,同一个 GGUF 文件,同一个负载生成器。
&lt;strong&gt;effective tok/s&lt;/strong&gt;(端到端,含 prefill):&lt;/p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;上下文 · 并发&lt;/th&gt;&lt;th&gt;Tempo9&lt;/th&gt;&lt;th&gt;llama.cpp&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;~1k · 1 路&lt;/td&gt;&lt;td&gt;848 / 899&lt;/td&gt;&lt;td&gt;660 / 566&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;~1k · 8 路&lt;/td&gt;&lt;td&gt;&lt;strong&gt;1294 / 1429&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;410 / 368&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;~2.4k · 8 路&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;strong&gt;1661 / 1735&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;strong&gt;无法服务&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;~7k · 8 路&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;strong&gt;3755 / 3753&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;strong&gt;无法服务&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;~7k · 1 路&lt;/td&gt;&lt;td&gt;2505&lt;/td&gt;&lt;td&gt;&lt;strong&gt;3884&lt;/strong&gt;(我们输)&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;最后一行留着,因为它是真的。但&lt;strong&gt;中间那两行是产品能不能用的分界&lt;/strong&gt;:
2.4k 上下文 + 8 个并发,在这台机器上 llama-server 开不了服(加载期
Metal OOM)。&lt;/p&gt;
&lt;p&gt;并发下引用用户真正感受到、且重复区间不重叠的 TTFT。四个 agent
共享 system prompt、各自做不同工作:&lt;/p&gt;
&lt;div class=&quot;evidence&quot; role=&quot;group&quot; aria-label=&quot;四个 Agent 共用 1.5K 与 10K system prompt 时的首 token 延迟区间&quot;&gt;
&lt;p class=&quot;evidence-kicker&quot;&gt;四个 Agent · 越短越好&lt;/p&gt;
&lt;p class=&quot;evidence-headline&quot;&gt;四组测试里，Tempo9 最慢的一次都快于 llama.cpp 最快的一次&lt;/p&gt;
&lt;div class=&quot;range-axis&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;span class=&quot;range-axis-scale&quot;&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;4&lt;/span&gt;&lt;span&gt;6&lt;/span&gt;&lt;span&gt;7 秒&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;range-metric&quot;&gt;1.5K prompt · p50&lt;/div&gt;
&lt;div class=&quot;range-row&quot;&gt;&lt;span class=&quot;range-label&quot;&gt;Tempo9&lt;/span&gt;&lt;span class=&quot;range-track&quot;&gt;&lt;span class=&quot;range-segment&quot; style=&quot;--from:8%;--span:4.7%&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;range-value&quot;&gt;&lt;b&gt;0.56–0.89 秒&lt;/b&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;range-row&quot;&gt;&lt;span class=&quot;range-label&quot;&gt;llama.cpp&lt;/span&gt;&lt;span class=&quot;range-track&quot;&gt;&lt;span class=&quot;range-segment compare&quot; style=&quot;--from:21.7%;--span:11%&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;range-value&quot;&gt;1.52–2.29 秒&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;range-metric&quot;&gt;1.5K prompt · p95&lt;/div&gt;
&lt;div class=&quot;range-row&quot;&gt;&lt;span class=&quot;range-label&quot;&gt;Tempo9&lt;/span&gt;&lt;span class=&quot;range-track&quot;&gt;&lt;span class=&quot;range-segment&quot; style=&quot;--from:13.1%;--span:19.3%&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;range-value&quot;&gt;&lt;b&gt;0.92–2.27 秒&lt;/b&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;range-row&quot;&gt;&lt;span class=&quot;range-label&quot;&gt;llama.cpp&lt;/span&gt;&lt;span class=&quot;range-track&quot;&gt;&lt;span class=&quot;range-segment compare&quot; style=&quot;--from:70.6%;--span:22.6%&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;range-value&quot;&gt;4.94–6.52 秒&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;range-metric&quot;&gt;10K prompt · p50&lt;/div&gt;
&lt;div class=&quot;range-row&quot;&gt;&lt;span class=&quot;range-label&quot;&gt;Tempo9&lt;/span&gt;&lt;span class=&quot;range-track&quot;&gt;&lt;span class=&quot;range-segment&quot; style=&quot;--from:13.4%;--span:.2%&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;range-value&quot;&gt;&lt;b&gt;0.94–0.95 秒&lt;/b&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;range-row&quot;&gt;&lt;span class=&quot;range-label&quot;&gt;llama.cpp&lt;/span&gt;&lt;span class=&quot;range-track&quot;&gt;&lt;span class=&quot;range-segment compare&quot; style=&quot;--from:22.9%;--span:0%&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;range-value&quot;&gt;1.60 秒&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;range-metric&quot;&gt;10K prompt · p95&lt;/div&gt;
&lt;div class=&quot;range-row&quot;&gt;&lt;span class=&quot;range-label&quot;&gt;Tempo9&lt;/span&gt;&lt;span class=&quot;range-track&quot;&gt;&lt;span class=&quot;range-segment&quot; style=&quot;--from:23%;--span:2%&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;range-value&quot;&gt;&lt;b&gt;1.61–1.75 秒&lt;/b&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;range-row&quot;&gt;&lt;span class=&quot;range-label&quot;&gt;llama.cpp&lt;/span&gt;&lt;span class=&quot;range-track&quot;&gt;&lt;span class=&quot;range-segment compare&quot; style=&quot;--from:90.3%;--span:0%&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;range-value&quot;&gt;6.32 秒&lt;/span&gt;&lt;/div&gt;
&lt;p class=&quot;chart-note&quot;&gt;每格跑两到三轮，每轮 12 个请求；横轴为首 token 等待时间。&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;每轮 12 个请求,所以报多轮区间而不是倍数。10k 不是刻意构造的压力测试,
大致就是 OpenClaw 每轮重发的 system prompt 尺寸。&lt;/p&gt;
&lt;p&gt;这里不比较并发 decode 吞吐:五轮重复实验里,Tempo9 每请求 decode 中位数
仍有 18–33% 的离散,关掉投机也没有消失。在找到未控制变量之前,这个数字
不适合拿来比较引擎。&lt;/p&gt;
&lt;h2 id=&quot;er-sheng-ming-128k-shang-xia-wen-bu-yong-an-shang-xian-yu-fu-kv&quot;&gt;二、声明 128K 上下文,不用按上限预付 KV&lt;/h2&gt;
&lt;div class=&quot;evidence&quot; role=&quot;group&quot; aria-label=&quot;声明四种上下文容量时的 KV 与工作内存占用&quot;&gt;
&lt;p class=&quot;evidence-kicker&quot;&gt;KV + 工作内存 · 越低越好&lt;/p&gt;
&lt;p class=&quot;evidence-headline&quot;&gt;声明 128K 上下文，Tempo9 启动时仍只占&lt;strong&gt;约 0.55 GB&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;kv-panel&quot;&gt;
&lt;div class=&quot;kv-group&quot;&gt;&lt;div class=&quot;kv-group-title&quot;&gt;8K&lt;/div&gt;&lt;div class=&quot;bar-row&quot;&gt;&lt;span class=&quot;bar-label&quot;&gt;Tempo9&lt;/span&gt;&lt;span class=&quot;bar-track&quot;&gt;&lt;span class=&quot;bar-fill&quot; style=&quot;--w:14%&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;bar-value&quot;&gt;557 MB&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;bar-row&quot;&gt;&lt;span class=&quot;bar-label&quot;&gt;llama.cpp&lt;/span&gt;&lt;span class=&quot;bar-track&quot;&gt;&lt;span class=&quot;bar-fill compare&quot; style=&quot;--w:14.3%&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;bar-value&quot;&gt;505–573 MB&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;
&lt;div class=&quot;kv-group&quot;&gt;&lt;div class=&quot;kv-group-title&quot;&gt;32K&lt;/div&gt;&lt;div class=&quot;bar-row&quot;&gt;&lt;span class=&quot;bar-label&quot;&gt;Tempo9&lt;/span&gt;&lt;span class=&quot;bar-track&quot;&gt;&lt;span class=&quot;bar-fill&quot; style=&quot;--w:14%&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;bar-value&quot;&gt;547–558 MB&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;bar-row&quot;&gt;&lt;span class=&quot;bar-label&quot;&gt;llama.cpp&lt;/span&gt;&lt;span class=&quot;bar-track&quot;&gt;&lt;span class=&quot;bar-fill compare&quot; style=&quot;--w:26.3%&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;bar-value&quot;&gt;981–1051 MB&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;
&lt;div class=&quot;kv-group&quot;&gt;&lt;div class=&quot;kv-group-title&quot;&gt;64K&lt;/div&gt;&lt;div class=&quot;bar-row&quot;&gt;&lt;span class=&quot;bar-label&quot;&gt;Tempo9&lt;/span&gt;&lt;span class=&quot;bar-track&quot;&gt;&lt;span class=&quot;bar-fill&quot; style=&quot;--w:14%&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;bar-value&quot;&gt;536–558 MB&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;bar-row&quot;&gt;&lt;span class=&quot;bar-label&quot;&gt;llama.cpp&lt;/span&gt;&lt;span class=&quot;bar-track&quot;&gt;&lt;span class=&quot;bar-fill compare&quot; style=&quot;--w:51%&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;bar-value&quot;&gt;2034–2041 MB&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;
&lt;div class=&quot;kv-group&quot;&gt;&lt;div class=&quot;kv-group-title&quot;&gt;128K&lt;/div&gt;&lt;div class=&quot;bar-row&quot;&gt;&lt;span class=&quot;bar-label&quot;&gt;Tempo9&lt;/span&gt;&lt;span class=&quot;bar-track&quot;&gt;&lt;span class=&quot;bar-fill&quot; style=&quot;--w:14%&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;bar-value&quot;&gt;&lt;b&gt;547–557 MB&lt;/b&gt;&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;bar-row&quot;&gt;&lt;span class=&quot;bar-label&quot;&gt;llama.cpp&lt;/span&gt;&lt;span class=&quot;bar-track&quot;&gt;&lt;span class=&quot;bar-fill warn&quot; style=&quot;--w:91.4%&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;bar-value&quot;&gt;3.6 GB · OOM&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p class=&quot;chart-note&quot;&gt;刻度为 0–4 GB；测的是填入上下文前的启动分配。&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;机制不是&quot;我们更省&quot;,是&lt;strong&gt;KV 放在哪里&lt;/strong&gt;:对手的 KV 是 Metal wired buffer,
和 16 GB 权重抢同一份 GPU wired 预算;我们的 KV 分页在 CPU 侧、按需触碰,
不占那份预算。&lt;/p&gt;
&lt;p&gt;说清楚它&lt;strong&gt;不是&lt;/strong&gt;什么:这量的是&lt;strong&gt;声明容量的预付成本&lt;/strong&gt;,不是&quot;填满 128K
token 后内存也不涨&quot;。真装进去的 token 当然要占内存 —— 分页只是让它按需
付费,而不是开机就按最坏情况预留。&lt;/p&gt;
&lt;h2 id=&quot;san-gong-ju-diao-yong-you-yi-ge-ke-yi-he-dui-de-fen-shu&quot;&gt;三、工具调用有一个可以核对的分数&lt;/h2&gt;
&lt;p&gt;这是我们最想强调的一格,因为&lt;strong&gt;本地推理圈几乎没人公布过&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;用&lt;strong&gt;官方 BFCL v4 评测器&lt;/strong&gt;,打我们自己的 OpenAI 端点(也就是说 HTTP、
chat template、工具调用解析这一整条真实链路都在测量范围内):&lt;/p&gt;
&lt;div class=&quot;score-pair&quot; aria-label=&quot;BFCL v4 工具调用得分&quot;&gt;
&lt;div&gt;&lt;strong&gt;85.19%&lt;/strong&gt;&lt;span&gt;Non-Live AST&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;strong&gt;76.61%&lt;/strong&gt;&lt;span&gt;Live AST&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;details class=&quot;score-detail&quot;&gt;&lt;summary&gt;查看分类明细&lt;/summary&gt;
&lt;table&gt;&lt;tr&gt;&lt;th&gt;BFCL v4 分类&lt;/th&gt;&lt;th&gt;Tempo9&lt;/th&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;simple (Python)&lt;/td&gt;&lt;td&gt;95.25%&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;multiple&lt;/td&gt;&lt;td&gt;94.50%&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;parallel&lt;/td&gt;&lt;td&gt;89.00%&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;parallel multiple&lt;/td&gt;&lt;td&gt;83.50%&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Non-Live irrelevance&lt;/td&gt;&lt;td&gt;84.17%&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/details&gt;
&lt;p&gt;对 agent 开发者来说,&quot;能不能稳定地把工具调对&quot;比 tok/s 重要得多,而这个
问题在本地模型圈基本靠感觉。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;而且这一格是我们跑砸了才有的。&lt;/strong&gt; 第一次跑官方评测器,&lt;code&gt;multiple&lt;/code&gt; 只有
&lt;strong&gt;24%&lt;/strong&gt;。不是模型不行 —— 报错逐条指着同一件事:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Incorrect type for parameter &amp;#39;side2&amp;#39;. Expected type integer, got str.
Parameter value: &amp;#39;4&amp;#39;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;模型的 XML 方言 &lt;code&gt;&amp;lt;parameter=side1&amp;gt;5&amp;lt;/parameter&amp;gt;&lt;/code&gt; 不携带类型,我们的服务端
就把 &lt;code&gt;&quot;5&quot;&lt;/code&gt; 原样发出去了。任何做严格校验的工具都会拒绝。修法是按工具
schema 里声明的类型转换 —— 不是猜:声明为 &lt;code&gt;string&lt;/code&gt; 的参数即使长得像数字
也必须保持字符串,否则订单号和电话号会被静默转成整数。&lt;/p&gt;
&lt;p&gt;这次解析器修复把分数从 24% 拉到 93.50%；上表引用的后续全量重测为 94.50%。&lt;/p&gt;
&lt;p&gt;更值得说的是&lt;strong&gt;为什么我们自己之前没发现&lt;/strong&gt;:我们有一套自研判分器,它给
字符串参数加了一层宽松的重解析,于是同一批输出它报 85.5%。那层重解析在
做 A/B 时是无害的(两臂同等对待),&lt;strong&gt;但它把一个真实缺陷从视野里抹掉了,
抹掉的时长正好等于缺陷存在的时长&lt;/strong&gt;。会把 bug 归一化掉的仪器,测的是另一
个程序。&lt;/p&gt;
&lt;h2 id=&quot;si-san-tao-xie-yi-gai-yi-xing-di-zhi-jiu-neng-jie&quot;&gt;四、三套协议,改一行地址就能接&lt;/h2&gt;
&lt;p&gt;一个服务端同时说 &lt;strong&gt;OpenAI、Anthropic、Ollama&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;tempo9 --gguf &amp;lt;model.gguf&amp;gt; --port 11435
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# OpenAI 客户端    base_url = http://127.0.0.1:11435/v1
# Anthropic 客户端  base_url = http://127.0.0.1:11435    (/v1/messages)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;原生 &lt;code&gt;/v1/messages&lt;/code&gt; 的意思是:&lt;strong&gt;用 Anthropic SDK 写的 agent 可以直接指
过来&lt;/strong&gt;,不需要转接层。默认端口 11435 紧邻 Ollama 的 11434,方便并存切换。&lt;/p&gt;
&lt;h2 id=&quot;wu-kan-tu-de-shi-hou-liao-tian-bu-qia&quot;&gt;五、看图的时候聊天不卡&lt;/h2&gt;
&lt;p&gt;不是&quot;支持多模态&quot;,是&lt;strong&gt;视觉塔和 LLM 能同时跑而不互相饿死&lt;/strong&gt;。&lt;/p&gt;
&lt;div class=&quot;evidence&quot; role=&quot;group&quot; aria-label=&quot;不带图片，以及同一张图片由 NPU 或 GPU 处理时的语言模型解码吞吐&quot;&gt;
&lt;p class=&quot;evidence-kicker&quot;&gt;纯文本基线 · 同一张图 · 只改视觉计算位置&lt;/p&gt;
&lt;p class=&quot;evidence-headline&quot;&gt;带一张图时，视觉计算放到 NPU，LLM 解码快 &lt;strong&gt;2.8×&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;bar-row&quot;&gt;&lt;span class=&quot;bar-label&quot;&gt;不带图&lt;/span&gt;&lt;span class=&quot;bar-track&quot;&gt;&lt;span class=&quot;bar-fill compare&quot; style=&quot;--w:100%&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;bar-value&quot;&gt;73.6 tok/s&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;bar-row&quot;&gt;&lt;span class=&quot;bar-label&quot;&gt;带图 · NPU&lt;/span&gt;&lt;span class=&quot;bar-track&quot;&gt;&lt;span class=&quot;bar-fill&quot; style=&quot;--w:35.2%&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;bar-value&quot;&gt;&lt;b&gt;25.9 tok/s&lt;/b&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;bar-row&quot;&gt;&lt;span class=&quot;bar-label&quot;&gt;带图 · GPU&lt;/span&gt;&lt;span class=&quot;bar-track&quot;&gt;&lt;span class=&quot;bar-fill warn&quot; style=&quot;--w:12.8%&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;bar-value&quot;&gt;9.4 tok/s&lt;/span&gt;&lt;/div&gt;
&lt;p class=&quot;chart-note&quot;&gt;不带图是纯文本基线。处理同一张图时，把视觉计算放到 NPU，可以把 GPU 留给 LLM 解码。&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;这里必须说反直觉的一半:&lt;strong&gt;塔自己在 NPU 上更慢&lt;/strong&gt;,3931 vs 462 ms/图,慢
8.5 倍。价值不在塔快,在&lt;strong&gt;它不占 GPU&lt;/strong&gt; —— 塔在 NPU 跑时 GPU 功率只有
0.35 W,约等于空载。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The NPU path is slower in isolation, but faster at the system level.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;争用仍然存在(NPU 那条路要吃 CPU 的一半,和宿主侧的路由、采样撞车),
所以结论是&quot;没那么糟&quot;,不是&quot;免费&quot;。&lt;/p&gt;
&lt;h2 id=&quot;mei-zuo-cheng-de&quot;&gt;没做成的&lt;/h2&gt;
&lt;p&gt;发布页通常只写好消息。这一节写我们试过、测过、然后被数字否掉的东西 ——
因为在这个领域,&lt;strong&gt;&quot;作者会公布自己被证伪的假设&quot;本身就是一种可信度&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;投机解码在散文上不赚钱。&lt;/strong&gt; 这个 GGUF 自带 MTP 头,逐位置接受率也健康
(位置 1 = 0.818)。但最优深度只做到 11.03 ms/提交,而我们不开投机的
基线就是 11.4 ms/步 —— &lt;strong&gt;净收益约等于零&lt;/strong&gt;。llama.cpp 开 MTP 有 +18%,
因为&lt;strong&gt;他们的基线比我们慢 30%&lt;/strong&gt;。投机解码赚的是&quot;把慢基线藏起来&quot;的钱,
基线已经快的一方没有这笔钱可赚。&lt;/p&gt;
&lt;p&gt;结构化输出上倒是有 +13%(k=4 到 104.9 tok/s,单流过 100),但它会让贪心
输出随投机深度确定性地改变 —— 而&quot;关掉温度就能复现&quot;是很多人拿来做测试和
缓存的性质。我们在 BFCL 2501 例上量过:开投机&lt;strong&gt;精度没有可测出的变化&lt;/strong&gt;
(73.0% vs 73.2%,McNemar p = 0.557),但 1.9% 的调用逐字节不同。
&lt;strong&gt;所以它是一个显式开关,不是默认值。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;MoE 专家分组是负的。&lt;/strong&gt; −10 ~ −17%。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;tile 大小已经在最优点。&lt;/strong&gt; 我们一度认为 MoE 的矩阵乘在 tile 填充上有
浪费,扫了一遍:auto 480 ms、bm32 490、bm64 516、bm16 588。自动规则
已经挑到最好的了,那个&quot;看起来很有道理&quot;的假设是错的。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;归约优化让内核慢了 9%。&lt;/strong&gt; 我们判断 GDN 的行内核受限于串行归约的延迟,
把两次归约折成一次深度 —— 结果慢了 9%。前提就是错的:它贴着的是发射
吞吐,不是深度。但那次失败给归约网络定了价(占该内核 16%),从而把
下一步准确指向了真正的地方:同一个 head 的 128 个 simdgroup 各自把 q/k
重读了一遍,占 51%。改成一个 simdgroup 管 4 行之后,内核快 25%,端到端
prefill 快 7%,而且输出逐字节一致。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;冷启动的权重驻留只值一次 180 ms。&lt;/strong&gt; 我们一度以为&quot;Mac 上放一会儿再问
就变慢&quot;是个大问题,量下来:同进程内重复 prefill 是 2055 / 1875 / 1872 /
1871 ms,30 秒间隔不会让它变冷。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2 id=&quot;kou-jing&quot;&gt;口径&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;机器&lt;/strong&gt;:Apple M5 Pro,24 GB 统一内存,macOS 默认配置。
&lt;strong&gt;&lt;code&gt;iogpu.wired_limit_mb = 0&lt;/code&gt;(未提高)&lt;/strong&gt; —— 抬高它是系统级改动、非默认
用户体验,而且对所有引擎一视同仁;本机实测抬到 22000 会触发 SoC
watchdog 硬复位。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;模型&lt;/strong&gt;:Qwen3.5-35B-A3B q3km GGUF,15.98 GiB,&lt;strong&gt;两侧读同一个文件&lt;/strong&gt;。
视觉塔 Qwen3-VL-4B(Core ML,bucket 4096)。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;llama.cpp&lt;/strong&gt;:b10307 (fc3f10b),源码构建,Metal+BLAS。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;BFCL&lt;/strong&gt;:官方 &lt;code&gt;bfcl-eval&lt;/code&gt;,&lt;code&gt;single_turn&lt;/code&gt; 全套 3641 例,经 HTTP 打我们
自己的 &lt;code&gt;/v1/chat/completions&lt;/code&gt;。官方 overall 一栏含 multi-turn /
web_search / memory 三组,我们只跑了 single_turn,所以&lt;strong&gt;只引用分组分数,
不引用 overall&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;各图的负载定义、脚本、原始数据与未采用的假设,见
&lt;code&gt;results_p4b_boundary.md&lt;/code&gt; / &lt;code&gt;results_p6_mtp.md&lt;/code&gt; / &lt;code&gt;results_p7_power.md&lt;/code&gt; /
&lt;code&gt;results_p13_serving_retest.md&lt;/code&gt; / &lt;code&gt;results_p16_spec_variance.md&lt;/code&gt;
/ &lt;code&gt;results_p8_effective.md&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Local inference was designed for one person typing. Agents do not work that way.</title>
        <published>2026-08-30T00:00:00+00:00</published>
        <updated>2026-08-30T00:00:00+00:00</updated>
        
        <author>
          <name>Unknown</name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://www.thinkspread.com/blog/agents-are-not-one-person-typing/"/>
        <id>https://www.thinkspread.com/blog/agents-are-not-one-person-typing/</id>
        
        <content type="html" xml:base="https://www.thinkspread.com/blog/agents-are-not-one-person-typing/">&lt;p&gt;llama.cpp and MLX are both very good at one shape of work: &lt;strong&gt;one person, one
stream, one question&lt;/strong&gt;. This article is not going to argue about that cell. On
an M5 Pro, our single-stream prefill is 0.90× the other engine&#39;s, and at a 7K
context it is 0.64×. We lose clearly.&lt;/p&gt;
&lt;p&gt;But that is not the shape of an agent. Agents run &lt;strong&gt;several sessions at once,
resend a large system prompt on every turn, call tools continuously, and
occasionally add an image.&lt;/strong&gt; Change the workload to that shape and the ordering
changes. In a few cells, the difference is not &quot;slow&quot; but &lt;strong&gt;cannot start&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Tempo9 is an attempt to bring server-side machinery, including continuous
batching, paged KV, and heterogeneous execution, to Apple Silicon.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2 id=&quot;1-under-concurrency-the-other-engine-does-not-merely-slow-down&quot;&gt;1. Under concurrency, the other engine does not merely slow down&lt;/h2&gt;
&lt;p&gt;The same 24 GB M5 Pro, the same GGUF file, and the same load generator. These
numbers are &lt;strong&gt;effective tok/s&lt;/strong&gt;, end to end including prefill:&lt;/p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Context · concurrency&lt;/th&gt;&lt;th&gt;Tempo9&lt;/th&gt;&lt;th&gt;llama.cpp&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;~1K · 1 stream&lt;/td&gt;&lt;td&gt;848 / 899&lt;/td&gt;&lt;td&gt;660 / 566&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;~1K · 8 streams&lt;/td&gt;&lt;td&gt;&lt;strong&gt;1294 / 1429&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;410 / 368&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;~2.4K · 8 streams&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;strong&gt;1661 / 1735&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;strong&gt;cannot serve&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;~7K · 8 streams&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;strong&gt;3755 / 3753&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;strong&gt;cannot serve&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;~7K · 1 stream&lt;/td&gt;&lt;td&gt;2505&lt;/td&gt;&lt;td&gt;&lt;strong&gt;3884&lt;/strong&gt; (we lose)&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The last row stays because it is true. The middle two are the boundary between
a product that can run the workload and one that cannot: with a 2.4K context
and eight concurrent sessions, &lt;code&gt;llama-server&lt;/code&gt; cannot start on this machine. It
runs out of Metal memory while loading.&lt;/p&gt;
&lt;p&gt;For concurrency, the user-visible number with non-overlapping repeat ranges is
time to first token. Four agents share one system prompt and perform different
work:&lt;/p&gt;
&lt;div class=&quot;evidence&quot; role=&quot;group&quot; aria-label=&quot;Time to first token ranges for four agents sharing 1.5K and 10K system prompts&quot;&gt;
&lt;p class=&quot;evidence-kicker&quot;&gt;Four agents · lower is better&lt;/p&gt;
&lt;p class=&quot;evidence-headline&quot;&gt;Every Tempo9 range ends before the matching llama.cpp range&lt;/p&gt;
&lt;div class=&quot;range-axis&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;span class=&quot;range-axis-scale&quot;&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;4&lt;/span&gt;&lt;span&gt;6&lt;/span&gt;&lt;span&gt;7 s&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;range-metric&quot;&gt;1.5K prompt · p50&lt;/div&gt;
&lt;div class=&quot;range-row&quot;&gt;&lt;span class=&quot;range-label&quot;&gt;Tempo9&lt;/span&gt;&lt;span class=&quot;range-track&quot;&gt;&lt;span class=&quot;range-segment&quot; style=&quot;--from:8%;--span:4.7%&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;range-value&quot;&gt;&lt;b&gt;0.56–0.89 s&lt;/b&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;range-row&quot;&gt;&lt;span class=&quot;range-label&quot;&gt;llama.cpp&lt;/span&gt;&lt;span class=&quot;range-track&quot;&gt;&lt;span class=&quot;range-segment compare&quot; style=&quot;--from:21.7%;--span:11%&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;range-value&quot;&gt;1.52–2.29 s&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;range-metric&quot;&gt;1.5K prompt · p95&lt;/div&gt;
&lt;div class=&quot;range-row&quot;&gt;&lt;span class=&quot;range-label&quot;&gt;Tempo9&lt;/span&gt;&lt;span class=&quot;range-track&quot;&gt;&lt;span class=&quot;range-segment&quot; style=&quot;--from:13.1%;--span:19.3%&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;range-value&quot;&gt;&lt;b&gt;0.92–2.27 s&lt;/b&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;range-row&quot;&gt;&lt;span class=&quot;range-label&quot;&gt;llama.cpp&lt;/span&gt;&lt;span class=&quot;range-track&quot;&gt;&lt;span class=&quot;range-segment compare&quot; style=&quot;--from:70.6%;--span:22.6%&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;range-value&quot;&gt;4.94–6.52 s&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;range-metric&quot;&gt;10K prompt · p50&lt;/div&gt;
&lt;div class=&quot;range-row&quot;&gt;&lt;span class=&quot;range-label&quot;&gt;Tempo9&lt;/span&gt;&lt;span class=&quot;range-track&quot;&gt;&lt;span class=&quot;range-segment&quot; style=&quot;--from:13.4%;--span:.2%&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;range-value&quot;&gt;&lt;b&gt;0.94–0.95 s&lt;/b&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;range-row&quot;&gt;&lt;span class=&quot;range-label&quot;&gt;llama.cpp&lt;/span&gt;&lt;span class=&quot;range-track&quot;&gt;&lt;span class=&quot;range-segment compare&quot; style=&quot;--from:22.9%;--span:0%&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;range-value&quot;&gt;1.60 s&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;range-metric&quot;&gt;10K prompt · p95&lt;/div&gt;
&lt;div class=&quot;range-row&quot;&gt;&lt;span class=&quot;range-label&quot;&gt;Tempo9&lt;/span&gt;&lt;span class=&quot;range-track&quot;&gt;&lt;span class=&quot;range-segment&quot; style=&quot;--from:23%;--span:2%&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;range-value&quot;&gt;&lt;b&gt;1.61–1.75 s&lt;/b&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;range-row&quot;&gt;&lt;span class=&quot;range-label&quot;&gt;llama.cpp&lt;/span&gt;&lt;span class=&quot;range-track&quot;&gt;&lt;span class=&quot;range-segment compare&quot; style=&quot;--from:90.3%;--span:0%&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;range-value&quot;&gt;6.32 s&lt;/span&gt;&lt;/div&gt;
&lt;p class=&quot;chart-note&quot;&gt;Ranges across two to three runs per cell; twelve requests per run. Axis: seconds to first token.&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;Each run contains twelve requests, so the table reports ranges across runs
rather than a multiplier. The 10K system prompt is not a synthetic stress test;
it is approximately what OpenClaw resends on every turn.&lt;/p&gt;
&lt;p&gt;Concurrent decode throughput is intentionally absent. Across five repeats,
Tempo9&#39;s median per-request decode rate still varies by 18–33%, and disabling
speculation does not remove the variation. Until the uncontrolled variable is
identified, that number is not suitable for comparing engines.&lt;/p&gt;
&lt;h2 id=&quot;2-declaring-128k-of-context-should-not-charge-for-all-of-it-up-front&quot;&gt;2. Declaring 128K of context should not charge for all of it up front&lt;/h2&gt;
&lt;div class=&quot;evidence&quot; role=&quot;group&quot; aria-label=&quot;KV and working-memory footprint at four declared context capacities&quot;&gt;
&lt;p class=&quot;evidence-kicker&quot;&gt;KV + working memory · lower is better&lt;/p&gt;
&lt;p class=&quot;evidence-headline&quot;&gt;Declare 128K. Tempo9 still starts at &lt;strong&gt;about 0.55 GB&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;kv-panel&quot;&gt;
&lt;div class=&quot;kv-group&quot;&gt;&lt;div class=&quot;kv-group-title&quot;&gt;8K&lt;/div&gt;&lt;div class=&quot;bar-row&quot;&gt;&lt;span class=&quot;bar-label&quot;&gt;Tempo9&lt;/span&gt;&lt;span class=&quot;bar-track&quot;&gt;&lt;span class=&quot;bar-fill&quot; style=&quot;--w:14%&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;bar-value&quot;&gt;557 MB&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;bar-row&quot;&gt;&lt;span class=&quot;bar-label&quot;&gt;llama.cpp&lt;/span&gt;&lt;span class=&quot;bar-track&quot;&gt;&lt;span class=&quot;bar-fill compare&quot; style=&quot;--w:14.3%&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;bar-value&quot;&gt;505–573 MB&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;
&lt;div class=&quot;kv-group&quot;&gt;&lt;div class=&quot;kv-group-title&quot;&gt;32K&lt;/div&gt;&lt;div class=&quot;bar-row&quot;&gt;&lt;span class=&quot;bar-label&quot;&gt;Tempo9&lt;/span&gt;&lt;span class=&quot;bar-track&quot;&gt;&lt;span class=&quot;bar-fill&quot; style=&quot;--w:14%&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;bar-value&quot;&gt;547–558 MB&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;bar-row&quot;&gt;&lt;span class=&quot;bar-label&quot;&gt;llama.cpp&lt;/span&gt;&lt;span class=&quot;bar-track&quot;&gt;&lt;span class=&quot;bar-fill compare&quot; style=&quot;--w:26.3%&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;bar-value&quot;&gt;981–1051 MB&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;
&lt;div class=&quot;kv-group&quot;&gt;&lt;div class=&quot;kv-group-title&quot;&gt;64K&lt;/div&gt;&lt;div class=&quot;bar-row&quot;&gt;&lt;span class=&quot;bar-label&quot;&gt;Tempo9&lt;/span&gt;&lt;span class=&quot;bar-track&quot;&gt;&lt;span class=&quot;bar-fill&quot; style=&quot;--w:14%&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;bar-value&quot;&gt;536–558 MB&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;bar-row&quot;&gt;&lt;span class=&quot;bar-label&quot;&gt;llama.cpp&lt;/span&gt;&lt;span class=&quot;bar-track&quot;&gt;&lt;span class=&quot;bar-fill compare&quot; style=&quot;--w:51%&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;bar-value&quot;&gt;2034–2041 MB&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;
&lt;div class=&quot;kv-group&quot;&gt;&lt;div class=&quot;kv-group-title&quot;&gt;128K&lt;/div&gt;&lt;div class=&quot;bar-row&quot;&gt;&lt;span class=&quot;bar-label&quot;&gt;Tempo9&lt;/span&gt;&lt;span class=&quot;bar-track&quot;&gt;&lt;span class=&quot;bar-fill&quot; style=&quot;--w:14%&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;bar-value&quot;&gt;&lt;b&gt;547–557 MB&lt;/b&gt;&lt;/span&gt;&lt;/div&gt;&lt;div class=&quot;bar-row&quot;&gt;&lt;span class=&quot;bar-label&quot;&gt;llama.cpp&lt;/span&gt;&lt;span class=&quot;bar-track&quot;&gt;&lt;span class=&quot;bar-fill warn&quot; style=&quot;--w:91.4%&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;bar-value&quot;&gt;3.6 GB · OOM&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p class=&quot;chart-note&quot;&gt;Scale: 0–4 GB. Startup allocation before the declared context is filled.&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;The mechanism is not simply &quot;we use less memory.&quot; It is &lt;strong&gt;where the KV cache
lives&lt;/strong&gt;. The other engine stores KV in a Metal wired buffer that competes with
16 GB of weights for the same GPU wired-memory budget. Tempo9 pages KV on the
CPU side and touches pages on demand, outside that budget.&lt;/p&gt;
&lt;p&gt;It is equally important to say what this measurement is &lt;strong&gt;not&lt;/strong&gt;. It measures
the up-front cost of declared capacity, not a claim that memory stays flat after
128K tokens are actually resident. Real tokens consume real memory. Paging lets
the runtime pay as they arrive instead of reserving the worst case at startup.&lt;/p&gt;
&lt;h2 id=&quot;3-tool-calling-has-a-score-that-can-be-checked&quot;&gt;3. Tool calling has a score that can be checked&lt;/h2&gt;
&lt;p&gt;This is the result we most want to emphasize because local inference runtimes
rarely publish it.&lt;/p&gt;
&lt;p&gt;We use the &lt;strong&gt;official BFCL v4 evaluator&lt;/strong&gt; against Tempo9&#39;s own OpenAI endpoint.
That means the measurement includes the real HTTP path, chat template, and tool
call parser rather than an isolated model invocation:&lt;/p&gt;
&lt;div class=&quot;score-pair&quot; aria-label=&quot;BFCL v4 tool-call scores&quot;&gt;
&lt;div&gt;&lt;strong&gt;85.19%&lt;/strong&gt;&lt;span&gt;Non-Live AST&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;strong&gt;76.61%&lt;/strong&gt;&lt;span&gt;Live AST&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;details class=&quot;score-detail&quot;&gt;&lt;summary&gt;Category breakdown&lt;/summary&gt;
&lt;table&gt;&lt;tr&gt;&lt;th&gt;BFCL v4 category&lt;/th&gt;&lt;th&gt;Tempo9&lt;/th&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;simple (Python)&lt;/td&gt;&lt;td&gt;95.25%&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;multiple&lt;/td&gt;&lt;td&gt;94.50%&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;parallel&lt;/td&gt;&lt;td&gt;89.00%&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;parallel multiple&lt;/td&gt;&lt;td&gt;83.50%&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Non-Live irrelevance&lt;/td&gt;&lt;td&gt;84.17%&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/details&gt;
&lt;p&gt;For an agent developer, &quot;does it call the right tool reliably?&quot; matters more
than tok/s. Local model stacks have mostly answered that question by intuition.&lt;/p&gt;
&lt;p&gt;This result also exists because our first run failed badly. The official
evaluator gave &lt;code&gt;multiple&lt;/code&gt; only &lt;strong&gt;24%&lt;/strong&gt;. The model was not the problem; the errors
all pointed at the same thing:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Incorrect type for parameter &amp;#39;side2&amp;#39;. Expected type integer, got str.
Parameter value: &amp;#39;4&amp;#39;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The model&#39;s XML dialect, such as &lt;code&gt;&amp;lt;parameter=side1&amp;gt;5&amp;lt;/parameter&amp;gt;&lt;/code&gt;, carries no
type information. Our server emitted &lt;code&gt;&quot;5&quot;&lt;/code&gt; unchanged, so any tool with strict
validation rejected it. The fix is to convert according to the declared tool
schema, not to guess. A parameter declared as &lt;code&gt;string&lt;/code&gt; must remain a string even
when it looks numeric, or order numbers and phone numbers will be silently
turned into integers.&lt;/p&gt;
&lt;p&gt;That parser fix moved the score from 24% to 93.50%. The later full retest shown
above measured 94.50%.&lt;/p&gt;
&lt;p&gt;The more useful lesson is why we had not found it ourselves. Our evaluator had
a permissive reparsing layer for string parameters and scored the same output
at 85.5%. That normalization was harmless for A/B tests because both arms were
treated equally, &lt;strong&gt;but it erased a real defect for exactly as long as the defect
existed&lt;/strong&gt;. An instrument that normalizes a bug away is measuring a different
program.&lt;/p&gt;
&lt;h2 id=&quot;4-three-protocols-one-address-change&quot;&gt;4. Three protocols, one address change&lt;/h2&gt;
&lt;p&gt;One server speaks &lt;strong&gt;OpenAI, Anthropic, and Ollama&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;tempo9 --gguf &amp;lt;model.gguf&amp;gt; --port 11435
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# OpenAI client       base_url = http://127.0.0.1:11435/v1
# Anthropic client    base_url = http://127.0.0.1:11435    (/v1/messages)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A native &lt;code&gt;/v1/messages&lt;/code&gt; endpoint means an agent written against the Anthropic
SDK can point to Tempo9 directly, without a translation layer. Port 11435 sits
next to Ollama&#39;s 11434 so both servers can run side by side.&lt;/p&gt;
&lt;h2 id=&quot;5-looking-at-an-image-should-not-stop-the-conversation&quot;&gt;5. Looking at an image should not stop the conversation&lt;/h2&gt;
&lt;p&gt;The useful property is not merely &quot;supports multimodal.&quot; It is that &lt;strong&gt;the
vision tower and the LLM can run at the same time without starving each other&lt;/strong&gt;.&lt;/p&gt;
&lt;div class=&quot;evidence&quot; role=&quot;group&quot; aria-label=&quot;LLM decode throughput without an image and with the same image processed on the NPU or GPU&quot;&gt;
&lt;p class=&quot;evidence-kicker&quot;&gt;Text baseline · same image · placement only&lt;/p&gt;
&lt;p class=&quot;evidence-headline&quot;&gt;With an image, LLM decode is &lt;strong&gt;2.8× faster&lt;/strong&gt; when vision runs on the NPU&lt;/p&gt;
&lt;div class=&quot;bar-row&quot;&gt;&lt;span class=&quot;bar-label&quot;&gt;No image&lt;/span&gt;&lt;span class=&quot;bar-track&quot;&gt;&lt;span class=&quot;bar-fill compare&quot; style=&quot;--w:100%&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;bar-value&quot;&gt;73.6 tok/s&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;bar-row&quot;&gt;&lt;span class=&quot;bar-label&quot;&gt;1 image · NPU&lt;/span&gt;&lt;span class=&quot;bar-track&quot;&gt;&lt;span class=&quot;bar-fill&quot; style=&quot;--w:35.2%&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;bar-value&quot;&gt;&lt;b&gt;25.9 tok/s&lt;/b&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;bar-row&quot;&gt;&lt;span class=&quot;bar-label&quot;&gt;1 image · GPU&lt;/span&gt;&lt;span class=&quot;bar-track&quot;&gt;&lt;span class=&quot;bar-fill warn&quot; style=&quot;--w:12.8%&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;bar-value&quot;&gt;9.4 tok/s&lt;/span&gt;&lt;/div&gt;
&lt;p class=&quot;chart-note&quot;&gt;No image is the text-only baseline. With the same image, putting vision on the NPU keeps the GPU available for LLM decoding.&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;The counterintuitive half matters: &lt;strong&gt;the tower itself is slower on the NPU&lt;/strong&gt;,
3931 versus 462 ms per image, an 8.5× difference. The value is not a faster
tower; it is leaving the GPU available. While the tower runs on the NPU, GPU
power is only 0.35 W, roughly idle.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The NPU path is slower in isolation, but faster at the system level.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Contention does not vanish. The NPU path still uses part of the CPU and collides
with host-side routing and sampling. The conclusion is &quot;less bad,&quot; not &quot;free.&quot;&lt;/p&gt;
&lt;h2 id=&quot;what-did-not-work&quot;&gt;What did not work&lt;/h2&gt;
&lt;p&gt;Release pages usually contain only good news. This section records what we
tried, measured, and then rejected, because in this field &lt;strong&gt;publishing the
hypotheses that the numbers disproved is itself evidence of reliability&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Speculative decoding does not pay on prose.&lt;/strong&gt; This GGUF includes an MTP head,
and first-position acceptance is healthy at 0.818. The best depth reaches 11.03
ms per committed step, while ordinary decoding is already 11.4 ms per step. The
net gain is approximately zero. llama.cpp gains 18% from MTP because &lt;strong&gt;its
baseline is 30% slower&lt;/strong&gt;. Speculation earns its return by hiding a slower
baseline; a fast baseline has less to hide.&lt;/p&gt;
&lt;p&gt;Structured output is different: at &lt;code&gt;k=4&lt;/code&gt;, single-stream decode reaches 104.9
tok/s, a 13% gain and above 100 tok/s. Across 2501 BFCL cases, speculation causes
no measurable accuracy change (73.0% versus 73.2%, McNemar p = 0.557), although
1.9% of calls differ byte for byte. It also changes greedy output
deterministically with speculation depth, while many tests and caches rely on
temperature zero being reproducible. &lt;strong&gt;That is why speculation remains an
explicit switch instead of the default.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Grouping MoE experts regressed by 10–17%.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The tile size was already optimal.&lt;/strong&gt; We suspected wasted padding in the MoE
matrix multiplies, then swept it: auto 480 ms, bm32 490, bm64 516, bm16 588. The
automatic rule had already chosen the best point. The plausible hypothesis was
wrong.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A reduction optimization made the kernel 9% slower.&lt;/strong&gt; We thought the GDN row
kernel was limited by serial reduction latency and folded two reductions into
one deeper reduction. The result regressed by 9%. The premise was wrong: the
kernel sat against issue throughput, not depth. The failure still priced the
reduction network at 16% of the kernel and pointed to the actual target: 128
simdgroups for one head reread q/k independently, accounting for 51%. Assigning
four rows to each simdgroup made the kernel 25% faster and end-to-end prefill 7%
faster, with byte-identical output.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Keeping cold-start weights resident is worth only one 180 ms event.&lt;/strong&gt; We
suspected that a Mac slowed down after sitting idle. Measured in one process,
repeated prefill took 2055 / 1875 / 1872 / 1871 ms. A 30-second pause did not
make it cold again.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2 id=&quot;method&quot;&gt;Method&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Machine:&lt;/strong&gt; Apple M5 Pro, 24 GB unified memory, stock macOS settings.
&lt;strong&gt;&lt;code&gt;iogpu.wired_limit_mb = 0&lt;/code&gt; (not raised).&lt;/strong&gt; Raising it is a system-level
modification rather than the default experience, and it would benefit every
engine. On this machine, setting it to 22000 triggers an SoC watchdog hard
reset.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Model:&lt;/strong&gt; Qwen3.5-35B-A3B q3km GGUF, 15.98 GiB. &lt;strong&gt;Both engines read the same
file.&lt;/strong&gt; The vision tower is Qwen3-VL-4B (Core ML, bucket 4096).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;llama.cpp:&lt;/strong&gt; b10307 (fc3f10b), built from source with Metal and BLAS.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;BFCL:&lt;/strong&gt; the official &lt;code&gt;bfcl-eval&lt;/code&gt;, all 3641 &lt;code&gt;single_turn&lt;/code&gt; cases, sent over
HTTP to Tempo9&#39;s own &lt;code&gt;/v1/chat/completions&lt;/code&gt;. The official overall column also
includes multi-turn, web search, and memory groups that we did not run, so we
publish group scores rather than overall.&lt;/li&gt;
&lt;li&gt;Workload definitions, scripts, raw data, and rejected hypotheses are in
&lt;code&gt;results_p4b_boundary.md&lt;/code&gt;, &lt;code&gt;results_p6_mtp.md&lt;/code&gt;, &lt;code&gt;results_p7_power.md&lt;/code&gt;,
&lt;code&gt;results_p13_serving_retest.md&lt;/code&gt;, &lt;code&gt;results_p16_spec_variance.md&lt;/code&gt;, and
&lt;code&gt;results_p8_effective.md&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
</content>
        
    </entry>
</feed>
