<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://blog.nikosbaxevanis.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://blog.nikosbaxevanis.com/" rel="alternate" type="text/html" /><updated>2026-11-14T14:17:27+00:00</updated><id>https://blog.nikosbaxevanis.com/feed.xml</id><title type="html">blog</title><author><name>Nikos Baxevanis</name><email>nikos.baxevanis@gmail.com</email></author><entry><title type="html">Four Models and a Test That Measured Nothing</title><link href="https://blog.nikosbaxevanis.com/2026/11/12/same-prompts-three-macs-four-models-and-a-test-that-measured-nothing/" rel="alternate" type="text/html" title="Four Models and a Test That Measured Nothing" /><published>2026-11-12T00:00:00+00:00</published><updated>2026-11-12T00:00:00+00:00</updated><id>https://blog.nikosbaxevanis.com/2026/11/12/same-prompts-three-macs-four-models-and-a-test-that-measured-nothing</id><content type="html" xml:base="https://blog.nikosbaxevanis.com/2026/11/12/same-prompts-three-macs-four-models-and-a-test-that-measured-nothing/"><![CDATA[<p><em>Part 3 of <a href="/2026/11/09/same-prompts-three-macs/">Same Prompts, Three Macs</a>.</em></p>

<p>After the <a href="/2026/11/11/same-prompts-three-macs-where-the-machines-sit/">placement runs</a> I had a practical question left:
which models should each Mac load? I wanted both DeepSeek releases in use
somewhere, and on the two Mac Studios a small model beside the large one
for quick questions. I also wanted prompts closer to my own work than
arithmetic and string functions.</p>

<h2 id="four-models">Four models</h2>

<p><a href="https://github.com/antirez/ds4">DwarfStar</a> served <a href="https://huggingface.co/deepseek-ai/DeepSeek-V4-Flash-0731">DeepSeek V4 Flash 0731</a> and
<a href="https://huggingface.co/deepseek-ai/DeepSeek-V4.1-Flash-1017">DeepSeek V4.1 Flash 1017</a>. The Llama app served
<a href="https://huggingface.co/ggml-org/Qwen3.6-27B-GGUF">Qwen3.6 27B</a> and <a href="https://huggingface.co/ggml-org/Qwen3.8-27B-GGUF">Qwen3.8 27B</a> at Q8_0. The Mac Studios
stood apart, the laptop had its lid open, and the thermal level was
logged. The laptop ran only the two DeepSeeks.</p>

<p>The prompt set is the 40 graded prompts from the first post, plus 13
Ethereum fact questions with one fixed answer each, such as the number of
slots in an epoch or the selector of <code class="language-plaintext highlighter-rouge">transfer(address,uint256)</code>.</p>

<table>
  <thead>
    <tr>
      <th style="text-align: left">Machine</th>
      <th style="text-align: left">Model</th>
      <th style="text-align: right">Generic of 40</th>
      <th style="text-align: right">Facts of 13</th>
      <th style="text-align: right">Tokens/s</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left">M5 Ultra</td>
      <td style="text-align: left">DeepSeek 0731</td>
      <td style="text-align: right">35</td>
      <td style="text-align: right">13</td>
      <td style="text-align: right">56.1</td>
    </tr>
    <tr>
      <td style="text-align: left">M5 Ultra</td>
      <td style="text-align: left">DeepSeek V4.1</td>
      <td style="text-align: right">36</td>
      <td style="text-align: right">12</td>
      <td style="text-align: right">49.8</td>
    </tr>
    <tr>
      <td style="text-align: left">M5 Ultra</td>
      <td style="text-align: left">Qwen3.6</td>
      <td style="text-align: right">37</td>
      <td style="text-align: right">12</td>
      <td style="text-align: right">51.9</td>
    </tr>
    <tr>
      <td style="text-align: left">M5 Ultra</td>
      <td style="text-align: left">Qwen3.8</td>
      <td style="text-align: right">38</td>
      <td style="text-align: right">10</td>
      <td style="text-align: right">58.5</td>
    </tr>
    <tr>
      <td style="text-align: left">M3 Ultra</td>
      <td style="text-align: left">DeepSeek 0731</td>
      <td style="text-align: right">34</td>
      <td style="text-align: right">13</td>
      <td style="text-align: right">36.6</td>
    </tr>
    <tr>
      <td style="text-align: left">M3 Ultra</td>
      <td style="text-align: left">DeepSeek V4.1</td>
      <td style="text-align: right">36</td>
      <td style="text-align: right">13</td>
      <td style="text-align: right">32.4</td>
    </tr>
    <tr>
      <td style="text-align: left">M3 Ultra</td>
      <td style="text-align: left">Qwen3.6</td>
      <td style="text-align: right">37</td>
      <td style="text-align: right">12</td>
      <td style="text-align: right">41.4</td>
    </tr>
    <tr>
      <td style="text-align: left">M3 Ultra</td>
      <td style="text-align: left">Qwen3.8</td>
      <td style="text-align: right">38</td>
      <td style="text-align: right">10</td>
      <td style="text-align: right">47.4</td>
    </tr>
    <tr>
      <td style="text-align: left">M5 Max</td>
      <td style="text-align: left">DeepSeek 0731</td>
      <td style="text-align: right">37</td>
      <td style="text-align: right">12</td>
      <td style="text-align: right">29.1</td>
    </tr>
    <tr>
      <td style="text-align: left">M5 Max</td>
      <td style="text-align: left">DeepSeek V4.1</td>
      <td style="text-align: right">37</td>
      <td style="text-align: right">13</td>
      <td style="text-align: right">13.1</td>
    </tr>
  </tbody>
</table>

<p>The newer DeepSeek release wasn’t better on these prompts. It is larger,
366 GB on the Mac Studios against 153 GB, and slower. On the laptop it
doesn’t fit in memory, so DwarfStar streams part of it from disk, and it
answered at under half the speed of the older release.</p>

<p>A 27B model matched the large ones on the generic prompts. I read that as
a statement about the prompts: they are too easy to rank these models. It
doesn’t show the models are equal. Qwen3.8 lost three fact questions by
writing past the 256-token limit before reaching its answer line; the
facts in its text were right.</p>

<p>The grader was wrong again as well. It wanted a number and marked
<code class="language-plaintext highlighter-rouge">EIP-155</code> as a miss, which cost DeepSeek 0731 two fact prompts until I
read the failures. The table is after that fix.</p>

<h2 id="choosing-per-machine">Choosing per machine</h2>

<p>Score didn’t decide anything, so the choice came from speed, disk and
memory.</p>

<table>
  <thead>
    <tr>
      <th style="text-align: left">Machine</th>
      <th style="text-align: left">DwarfStar</th>
      <th style="text-align: left">Llama app</th>
      <th style="text-align: left">Reason</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left">M3 Ultra</td>
      <td style="text-align: left">DeepSeek V4.1</td>
      <td style="text-align: left">Qwen3.8</td>
      <td style="text-align: left">Has the disk for the larger file and loses 4 tokens/s</td>
    </tr>
    <tr>
      <td style="text-align: left">M5 Ultra</td>
      <td style="text-align: left">DeepSeek 0731</td>
      <td style="text-align: left">Qwen3.8</td>
      <td style="text-align: left">The fastest pairing; V4.1 nearly fills its disk</td>
    </tr>
    <tr>
      <td style="text-align: left">M5 Max</td>
      <td style="text-align: left">DeepSeek 0731</td>
      <td style="text-align: left">None</td>
      <td style="text-align: left">V4.1 runs at less than half speed</td>
    </tr>
  </tbody>
</table>

<p>I checked that the pairing works on the M5 Ultra. With DeepSeek loaded in
DwarfStar and Qwen3.8 in the Llama app, 34% of memory stayed free, swap
stayed at 4 MB, and each answered at its usual speed: 56 and 44 tokens
per second with the older release, 52 and 46 with the newer. That was a
short prompt to each, one at a time. I didn’t test both answering at
once, or long conversations in both, and I didn’t repeat it on the M3
Ultra. The order matters: DwarfStar’s manager refuses to start while
another engine holds a model, so it goes first.</p>

<p>The heat log earned its place here. Standing apart, the M3 Ultra spent
57% of this run at Moderate, all of it during the two Qwen runs in the
Llama app. The M5 Ultra spent 41% at Heavy, under the Qwens and also
under DeepSeek V4.1 in DwarfStar. In the <a href="/2026/11/11/same-prompts-three-macs-where-the-machines-sit/">previous post</a> the
M3 Ultra had stayed Nominal apart, and DwarfStar had never taken the M5
Ultra past Moderate; that was a different workload.</p>

<h2 id="a-domain-test">A domain test</h2>

<p>For prompts nearer my work, I built 40 from a private set of security
reports against Ethereum client software. Each prompt is one report, and
the task is to classify its severity as the triage team did:
Informational, Low, Medium or High, ten of each. Reports that named a
severity in their own text were left out, so the label couldn’t be read
off the page. The reports are private and none of their content is
published here.</p>

<p>The first version gave the models no scale. On the M5 Ultra, where I
tabulated the answers, every model called most reports High and none
answered Informational once. Reports argue
their own impact, and the models believed them. Scores ran from 11 to 14
of 40, where guessing gets 10.</p>

<p>The second version put the public severity scale from the
<a href="https://ethereum.org/en/bug-bounty/">Ethereum bug bounty page</a> into the prompt, with its
network-share thresholds, and a note that reporters tend to overstate
impact. The models then swung the other way and under-rated.</p>

<table>
  <thead>
    <tr>
      <th style="text-align: left">Model</th>
      <th style="text-align: right">M5 Ultra</th>
      <th style="text-align: right">M3 Ultra</th>
      <th style="text-align: right">M5 Max</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left">DeepSeek 0731</td>
      <td style="text-align: right">12</td>
      <td style="text-align: right">15</td>
      <td style="text-align: right">14</td>
    </tr>
    <tr>
      <td style="text-align: left">DeepSeek V4.1</td>
      <td style="text-align: right">11</td>
      <td style="text-align: right">9</td>
      <td style="text-align: right">11</td>
    </tr>
    <tr>
      <td style="text-align: left">Qwen3.6</td>
      <td style="text-align: right">12</td>
      <td style="text-align: right">12</td>
      <td style="text-align: right"> </td>
    </tr>
    <tr>
      <td style="text-align: left">Qwen3.8</td>
      <td style="text-align: right">13</td>
      <td style="text-align: right">13</td>
      <td style="text-align: right"> </td>
    </tr>
  </tbody>
</table>

<p>On the M5 Ultra the four models gave the same answer on 2 of the 40
reports. The same DeepSeek file at temperature 0 scored 15 on one Mac
Studio and 12 on the other; I haven’t investigated why the answers
differ between machines.</p>

<h2 id="the-control">The control</h2>

<p>The obvious objection is that these are small local models. So the same
53 Ethereum prompts went to Claude Fable 5.2, each in a fresh session
with no tools and a one-line system prompt. Unlike the local models it
was allowed to think before answering.</p>

<p>It got 13 of 13 facts and 13 of 40 on the triage prompts. It made the
same mistake, calling 14 of the 30 reports above Informational
“Informational”, and got one of the ten High reports right.</p>

<p>So the test is broken, and the models are not what it measured. A
report’s text and the public scale are not enough to recover the label a
triage team assigned, whoever reads them. My guess is that the team
uses things a report doesn’t say, such as what was already known and how
hard the attack is in practice. I haven’t tested that.</p>

<h2 id="what-i-take-from-it">What I take from it</h2>

<ul>
  <li>An oracle that is a human judgement measures agreement with that
judgement. Here there was none to measure, and without the control I
might have blamed the models.</li>
  <li>Run a strong baseline through a new benchmark before reading anything
into the scores. It cost a few minutes.</li>
  <li>The fact questions worked and were too easy. Everyone scored 10 or
more of 13.</li>
  <li>Read the failures. This series found a grader bug in every round.</li>
</ul>

<p>The next attempt needs a task whose answer is in the text or in
checkable knowledge: which component a report concerns, with the obvious
names removed, or whether a described behaviour violates the
specification. That is the remaining entry in the
<a href="/2026/11/09/same-prompts-three-macs/">series index</a>, and it is closer to the question this blog keeps
returning to: <a href="/2026/01/25/oracles-traces-triage/">choosing oracles that are real</a>.</p>

<hr />

<p><em>Entry dated November 12; the measurements were taken the same day.</em></p>]]></content><author><name>Nikos Baxevanis</name><email>nikos.baxevanis@gmail.com</email></author><summary type="html"><![CDATA[Part 3 of Same Prompts, Three Macs. Two DeepSeek releases and two small Qwen models, a choice per machine made on speed and disk, and a domain test on which every model, hosted or local, scored at guessing level.]]></summary></entry><entry><title type="html">Where the Machines Sit</title><link href="https://blog.nikosbaxevanis.com/2026/11/11/same-prompts-three-macs-where-the-machines-sit/" rel="alternate" type="text/html" title="Where the Machines Sit" /><published>2026-11-11T00:00:00+00:00</published><updated>2026-11-12T00:00:00+00:00</updated><id>https://blog.nikosbaxevanis.com/2026/11/11/same-prompts-three-macs-where-the-machines-sit</id><content type="html" xml:base="https://blog.nikosbaxevanis.com/2026/11/11/same-prompts-three-macs-where-the-machines-sit/"><![CDATA[<p><em>Part 2 of <a href="/2026/11/09/same-prompts-three-macs/">Same Prompts, Three Macs</a>.</em></p>

<p><img src="/assets/local-models/desk.jpg" alt="Two Mac Studios stacked and a closed MacBook Pro on a wooden desk, each with an external SSD beside it." /></p>

<p class="caption">The arrangement this post takes apart: Studios stacked, laptop lid closed.</p>

<p>The <a href="/2026/11/10/same-prompts-three-macs-higher-precision-same-answers/">previous post</a> ended with timings I no longer trusted. The
two Mac Studios were stacked, the M3 Ultra underneath and the M5 Ultra on
top. During a long session on the M5 Ultra I ran this by hand:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sudo powermetrics --samplers thermal
</code></pre></div></div>

<p>It reported a thermal pressure level of Moderate, then Heavy, and went
back to Nominal about 40 seconds after the load stopped. Thermal pressure
is the level macOS reports as it starts limiting the chip to control
heat: Nominal, Moderate, Heavy, Critical. The M3 Ultra had worked for
hours at the bottom of that stack and nobody had sampled it. Every speed
I had written down was now a number without its conditions.</p>

<p>So the runs were repeated with the level logged, and the machines were
moved.</p>

<h2 id="giving-the-agent-one-command">Giving the agent one command</h2>

<p><code class="language-plaintext highlighter-rouge">powermetrics</code> needs root, and without it macOS reports nothing useful
(<code class="language-plaintext highlighter-rouge">pmset -g therm</code> printed no level). The runs were driven by Claude Code
over SSH, and I didn’t want to hand it the administrator password. I ran
this once on each Mac instead, replacing <code class="language-plaintext highlighter-rouge">USER</code> with the account name:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>echo 'USER ALL=(root) NOPASSWD: /usr/bin/powermetrics' \
    | sudo tee /etc/sudoers.d/powermetrics
sudo chmod 440 /etc/sudoers.d/powermetrics
</code></pre></div></div>

<p>That lets one account run one program as root without a password. The
harness could then log a reading every five seconds beside each run:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sudo -n powermetrics --samplers thermal -i 5000 \
    | grep --line-buffered -E 'Sampled system activity|pressure level' \
    &gt; thermal.log &amp;
</code></pre></div></div>

<p>This is the narrow version of the access question from
<a href="/2026/10/22/local-models-and-tool-permissions/">Local Models and Tool Permissions</a>: one read-only tool, in
place of a password or general <code class="language-plaintext highlighter-rouge">sudo</code>. Two caveats belong with it.
<code class="language-plaintext highlighter-rouge">powermetrics</code> can report more than temperature, including per-process
energy use, so the rule grants a read of system activity. And the logger
runs as root, so the harness can’t kill it directly; it ends the <code class="language-plaintext highlighter-rouge">grep</code>
that reads from it, and <code class="language-plaintext highlighter-rouge">powermetrics</code> exits by itself. Removing the file
undoes the rule.</p>

<h2 id="the-runs">The runs</h2>

<p>Each run is the same 45 prompts per server from the previous post. The
table gives the share of five-second samples at each level, and tokens
per second for GLM 5.3 Flash and DeepSeek V4 Flash 0731 under
<a href="https://github.com/antirez/ds4">DwarfStar</a>.</p>

<table>
  <thead>
    <tr>
      <th style="text-align: left">Machine</th>
      <th style="text-align: left">Placement</th>
      <th style="text-align: right">Moderate</th>
      <th style="text-align: right">Heavy</th>
      <th style="text-align: left">Tokens/s</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left">M3 Ultra</td>
      <td style="text-align: left">Stacked, bottom</td>
      <td style="text-align: right">32%</td>
      <td style="text-align: right">0%</td>
      <td style="text-align: left">22.8, 36.3</td>
    </tr>
    <tr>
      <td style="text-align: left">M3 Ultra</td>
      <td style="text-align: left">Side by side</td>
      <td style="text-align: right">0%</td>
      <td style="text-align: right">0%</td>
      <td style="text-align: left">22.8, 36.4</td>
    </tr>
    <tr>
      <td style="text-align: left">M3 Ultra</td>
      <td style="text-align: left">Stacked again, bottom</td>
      <td style="text-align: right">0%</td>
      <td style="text-align: right">0%</td>
      <td style="text-align: left">22.8, 36.4</td>
    </tr>
    <tr>
      <td style="text-align: left">M5 Ultra</td>
      <td style="text-align: left">Stacked, top</td>
      <td style="text-align: right">48%</td>
      <td style="text-align: right">0%</td>
      <td style="text-align: left">33.3, 55.8</td>
    </tr>
    <tr>
      <td style="text-align: left">M5 Ultra</td>
      <td style="text-align: left">Side by side</td>
      <td style="text-align: right">12%</td>
      <td style="text-align: right">12%</td>
      <td style="text-align: left">33.8, 56.6</td>
    </tr>
    <tr>
      <td style="text-align: left">M5 Ultra</td>
      <td style="text-align: left">Stacked again, top</td>
      <td style="text-align: right">32%</td>
      <td style="text-align: right">20%</td>
      <td style="text-align: left">33.7, 56.6</td>
    </tr>
    <tr>
      <td style="text-align: left">M5 Max</td>
      <td style="text-align: left">Lid closed, flat</td>
      <td style="text-align: right">under 1%</td>
      <td style="text-align: right">96%</td>
      <td style="text-align: left">21.2, 26.2</td>
    </tr>
    <tr>
      <td style="text-align: left">M5 Max</td>
      <td style="text-align: left">Lid open, flat</td>
      <td style="text-align: right">5%</td>
      <td style="text-align: right">91%</td>
      <td style="text-align: left">23.4, 28.7</td>
    </tr>
    <tr>
      <td style="text-align: left">M5 Max</td>
      <td style="text-align: left">Lid closed again</td>
      <td style="text-align: right">5%</td>
      <td style="text-align: right">92%</td>
      <td style="text-align: left">20.4, 25.7</td>
    </tr>
  </tbody>
</table>

<p>The first M5 Ultra row covers only its DwarfStar half, because logging
started after its Llama half had run. The M5 Max is a MacBook Pro and ran
DwarfStar only.</p>

<h2 id="what-the-first-round-said">What the first round said</h2>

<p>After the side-by-side runs I wrote down a tidy story. Stacking made the
Mac Studios warmer and no slower: the M3 Ultra went from Moderate in 32%
of samples to Nominal throughout, and its tokens per second didn’t change
in the first decimal on any of the four servers. The M5 Ultra’s DwarfStar
half went from Moderate to Nominal with 33.3 and 55.8 against 33.8 and
56.6.</p>

<p>The Heavy reading that started all this was not the stack either. The M5
Ultra reached Heavy only while the Llama app read a very long prompt,
for two to four minutes at a time, and it did so standing apart as well.
DwarfStar, with these two models, never pushed it past Moderate.</p>

<p>The laptop was the machine that heat limited. With the lid closed it sat
at Heavy for 96% of the run. Opening the lid, still flat on the desk,
moved that only to 91%, yet the run finished in 47 minutes 41 seconds
against 52 minutes, and tokens per second rose by about 10%.</p>

<h2 id="putting-it-back">Putting it back</h2>

<p>One run per placement is an anecdote. The M3 Ultra’s side-by-side run
had also started from a rested machine, while its stacked run followed
hours of other work, so “apart is cooler” and “rested is cooler” were
the same observation. I restacked the Mac Studios, closed the laptop
lid, waited for all three to read Nominal, and started the identical
runs at the same moment.</p>

<p>The laptop result came back: 52 minutes 52 seconds closed, and tokens per
second fell to the closed figures. Two closed runs now agree against one
open run.</p>

<p>The M5 Ultra on top was warmer stacked, as before. Its DwarfStar half
read Moderate for four and a half minutes where it had read Nominal
apart, and Heavy rose from 12% of the run to 20%. Its speed didn’t
change.</p>

<p>The M3 Ultra’s Moderate did not come back. At the bottom of the stack,
with the M5 Ultra working above it, it stayed Nominal for all 679
samples. The first reading had followed hours of long-input tests, so it
was more likely stored heat than the stack. My tidy story was wrong about
the machine it started with.</p>

<h2 id="what-i-take-from-it">What I take from it</h2>

<ul>
  <li>Speed repeated and heat didn’t. Tokens per second agreed to within
half a token across stacked, apart and stacked again on both Mac
Studios. The thermal shares depended on what the machine had done in
the hours before.</li>
  <li>The thermal level is a coarse signal. Moderate cost nothing I could
measure on the desktops, while 91% against 96% Heavy hid a 10% speed
difference on the laptop. I now log the level and the speed together.</li>
  <li>The long-input timing I had worried about on the M3 Ultra held up. One
repeat, apart and Nominal throughout, read the same 194,819 tokens in
283 seconds against 279 stacked. I repeated only that one of seven.</li>
  <li>Scores didn’t move with heat, placement or machine.</li>
</ul>

<p>For placement, the laptop runs with its lid open. The Mac Studios can be
stacked as far as speed goes; side by side keeps the upper one cooler,
and I keep them apart when there is room.</p>

<p>Some things I didn’t measure. I logged the pressure level, without fan
speed or temperature in degrees, so I can’t say what the extra heat costs
in noise or wear. I didn’t try the stack the other way up, or the laptop
on a stand; I expect a stand with the lid open to be no worse than flat,
and that is an expectation. And each placement has at most two runs.</p>

<p>A benchmark number without the machine’s thermal state and recent
history is incomplete. I had been publishing mine that way. The
<a href="/2026/11/12/same-prompts-three-macs-four-models-and-a-test-that-measured-nothing/">next post</a> uses the same harness, with the log running, to choose
which models each Mac loads.</p>

<hr />

<p><strong>Next:</strong> <a href="/2026/11/12/same-prompts-three-macs-four-models-and-a-test-that-measured-nothing/">Four Models and a Test That Measured Nothing</a></p>]]></content><author><name>Nikos Baxevanis</name><email>nikos.baxevanis@gmail.com</email></author><summary type="html"><![CDATA[Part 2 of Same Prompts, Three Macs. Logging thermal pressure beside every run, unstacking two Mac Studios, opening a laptop lid, and then putting everything back to check the first conclusion.]]></summary></entry><entry><title type="html">Higher Precision, Same Answers</title><link href="https://blog.nikosbaxevanis.com/2026/11/10/same-prompts-three-macs-higher-precision-same-answers/" rel="alternate" type="text/html" title="Higher Precision, Same Answers" /><published>2026-11-10T00:00:00+00:00</published><updated>2026-11-12T00:00:00+00:00</updated><id>https://blog.nikosbaxevanis.com/2026/11/10/same-prompts-three-macs-higher-precision-same-answers</id><content type="html" xml:base="https://blog.nikosbaxevanis.com/2026/11/10/same-prompts-three-macs-higher-precision-same-answers/"><![CDATA[<p><em>Part 1 of <a href="/2026/11/09/same-prompts-three-macs/">Same Prompts, Three Macs</a>.</em></p>

<p>I keep the same model twice on one Mac. <a href="https://github.com/antirez/ds4">DwarfStar</a> serves it
from a higher-precision file, and the Llama app, a llama.cpp build with a
model router, serves it from a smaller one published by
<a href="https://huggingface.co/ggml-org">ggml-org</a>. The larger files cost hundreds of gigabytes of disk,
and I had assumed they bought better answers. In <a href="/2026/10/20/self-hosted-inference-on-apple-silicon/">an earlier post</a>
I wrote that I hadn’t done the paired tests needed to say so. This is
that test.</p>

<p>The question is narrow on purpose: on the same Mac, with the same
prompts, does the higher-precision file get more right?</p>

<h2 id="what-was-compared">What was compared</h2>

<p>The first runs were on the M3 Ultra Mac Studio with 256 GB of memory, one
server loaded at a time.</p>

<table>
  <thead>
    <tr>
      <th style="text-align: left">Model</th>
      <th style="text-align: left">DwarfStar file</th>
      <th style="text-align: left">Llama file</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left"><a href="https://huggingface.co/zai-org/GLM-5.3-Flash">GLM 5.3 Flash</a></td>
      <td style="text-align: left">Q4_K</td>
      <td style="text-align: left">Q2_K</td>
    </tr>
    <tr>
      <td style="text-align: left"><a href="https://huggingface.co/deepseek-ai/DeepSeek-V4-Flash-0731">DeepSeek V4 Flash 0731</a></td>
      <td style="text-align: left">Q4_K experts, Q8 and F16 elsewhere</td>
      <td style="text-align: left">MXFP4</td>
    </tr>
  </tbody>
</table>

<p>Those names say how many bits each weight is stored in. A smaller number
is a smaller and coarser file: Q2_K keeps about two bits per weight, Q4_K
about four, and MXFP4 is a four-bit floating-point format.</p>

<p>Each server received 45 prompts. Forty have an answer a program can
check:</p>

<ul>
  <li>12 coding tasks, graded by running the returned function against tests
in a separate process.</li>
  <li>14 arithmetic and logic questions, with the answer on a marked last
line.</li>
  <li>6 tasks that must return strict JSON, compared after parsing.</li>
  <li>8 lookups in a synthetic order ledger, four in about 25,000 tokens of
input and four in about 85,000.</li>
</ul>

<p>The other five are open questions, saved for timing and not graded.
Requests used temperature 0 with thinking switched off, and the same
request body went to both servers. The expected answers are computed by
the harness, and every coding test must pass against a reference solution
and fail against an empty one before it is used. The
<a href="/assets/local-models/prompts.py">prompts</a> and the <a href="/assets/local-models/compare.py">grader</a> are published with the
series.</p>

<h2 id="one-prompt-apart">One prompt apart</h2>

<table>
  <thead>
    <tr>
      <th style="text-align: left">Model</th>
      <th style="text-align: left">Engine and file</th>
      <th style="text-align: right">Passed of 40</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left">GLM 5.3 Flash</td>
      <td style="text-align: left">DwarfStar, Q4_K</td>
      <td style="text-align: right">35</td>
    </tr>
    <tr>
      <td style="text-align: left">GLM 5.3 Flash</td>
      <td style="text-align: left">Llama, Q2_K</td>
      <td style="text-align: right">36</td>
    </tr>
    <tr>
      <td style="text-align: left">DeepSeek V4 Flash 0731</td>
      <td style="text-align: left">DwarfStar, Q4_K mixed</td>
      <td style="text-align: right">34</td>
    </tr>
    <tr>
      <td style="text-align: left">DeepSeek V4 Flash 0731</td>
      <td style="text-align: left">Llama, MXFP4</td>
      <td style="text-align: right">35</td>
    </tr>
  </tbody>
</table>

<p>By group, in the same order:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>code           11/12  10/12   9/12  10/12
math           11/14  12/14  13/14  14/14
json            5/6    6/6    6/6    6/6
ledger, 25k     4/4    4/4    3/4    2/4
ledger, 85k     4/4    4/4    3/4    3/4
</code></pre></div></div>

<p>For both models the smaller file scored one prompt higher. With 40
prompts and one run, one prompt is noise. The reading I can defend is
that no difference showed, and I can’t claim the smaller file is better.</p>

<p>I expected a gap for GLM, where the Llama file has half the bits. There
wasn’t one.</p>

<h2 id="failures-followed-the-model">Failures followed the model</h2>

<p>The misses clustered by model, and the file made little difference.
DeepSeek missed the same four prompts under both engines: two “which
city is on the line after order X” lookups, a topological sort, and a
Base64 encoder that got the padding wrong on a two-byte input. Both GLM
files got the sum of the primes below 60 wrong, with 672 and 769 where
the answer is 440. Most of the other misses were arithmetic done without
thinking, such as counting the 1 bits in 2026.</p>

<p>DeepSeek returned byte-identical text on 24 of the 45 prompts across the
two engines and files. GLM did on 7.</p>

<p>The same four servers also ran on the M5 Ultra Mac Studio, which takes
the same files, and scored 35, 36, 35 and 35.
The M5 Max MacBook Pro has half the memory and takes smaller DwarfStar
files: GLM at Q2 scored 35, and DeepSeek with most experts at Q2 scored</p>
<ol>
  <li>Across the three Macs, every run of these two models landed between
34 and 37 on these 40 prompts.</li>
</ol>

<h2 id="the-grader-had-bugs-too">The grader had bugs too</h2>

<p>A checker needs checking. I read every failure by hand, and three of them
were about the test and not the model.</p>

<p>One coding test rejected any answer whose source contained the word
“calendar”, to forbid <code class="language-plaintext highlighter-rouge">import calendar</code>. DeepSeek wrote a comment about
the Gregorian calendar and failed a correct solution under both engines.
The test now looks for the import. The table above is after that fix,
which moved both DeepSeek scores up by one.</p>

<p>One lookup answer was the right order ID without the required <code class="language-plaintext highlighter-rouge">ANSWER:</code>
prefix. I left it as a miss, because the format was part of the task. A
looser grader would have passed it.</p>

<p>“Thinking off” also had to be verified. The Llama app’s GLM kept
reasoning with the usual switches and stopped only when the request
carried <code class="language-plaintext highlighter-rouge">thinking_budget_tokens: 0</code>:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">body</span> <span class="o">=</span> <span class="p">{</span><span class="s">'model'</span><span class="p">:</span> <span class="n">model</span><span class="p">,</span> <span class="s">'temperature'</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span> <span class="s">'seed'</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
        <span class="s">'max_tokens'</span><span class="p">:</span> <span class="n">max_tokens</span><span class="p">,</span> <span class="s">'reasoning_effort'</span><span class="p">:</span> <span class="s">'none'</span><span class="p">,</span>
        <span class="s">'think'</span><span class="p">:</span> <span class="bp">False</span><span class="p">,</span> <span class="s">'thinking'</span><span class="p">:</span> <span class="p">{</span><span class="s">'type'</span><span class="p">:</span> <span class="s">'disabled'</span><span class="p">},</span>
        <span class="s">'chat_template_kwargs'</span><span class="p">:</span> <span class="p">{</span><span class="s">'enable_thinking'</span><span class="p">:</span> <span class="bp">False</span><span class="p">},</span>
        <span class="s">'thinking_budget_tokens'</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>
        <span class="s">'messages'</span><span class="p">:</span> <span class="p">[{</span><span class="s">'role'</span><span class="p">:</span> <span class="s">'user'</span><span class="p">,</span> <span class="s">'content'</span><span class="p">:</span> <span class="n">prompt</span><span class="p">}]}</span>
</code></pre></div></div>

<p>The harness records the length of any reasoning a server returns, and the
final runs show zero for all of them.</p>

<h2 id="speed-was-a-different-question">Speed was a different question</h2>

<p>DwarfStar generated DeepSeek at 36.2 tokens per second on the M3 Ultra
against 25.9 for the Llama app. For GLM the order reversed, 22.8 against
26.4. Reading the 85,000-token ledger for the first time took between
224 and 317 seconds on every server. Three of the four reused their
cache for the next three questions, at 7 to 31 seconds each. DwarfStar
with GLM read the ledger again every time, about 300 seconds per
question.</p>

<p>When I collected those figures, the two Mac Studios were stacked, and
one of them turned out to be running hot. That put every timing in
doubt, which is the <a href="/2026/11/11/same-prompts-three-macs-where-the-machines-sit/">next post</a>.</p>

<h2 id="what-this-does-and-doesnt-show">What this does and doesn’t show</h2>

<ul>
  <li>It is one run per server and 40 prompts. A gap of two or three answers
would still be noise. The test could have shown a large difference and
didn’t; it can’t show there is none.</li>
  <li>It compares two stacks as well as two files. The engines differ in chat
template and sampling code, so an effect could come from either.</li>
  <li>The prompts are short tasks with one right answer. Nothing here covers
long agent sessions, tool use, or the quality of prose.</li>
  <li>The prompts were written by the assistant that ran the test. They are
not drawn from my own work.</li>
  <li>With thinking on, the arithmetic misses would probably shrink for
every file. I didn’t measure that.</li>
</ul>

<p>I haven’t deleted the larger files. What changed is my reason for keeping
them: I can no longer say it is answer quality, so the choice between the
two servers comes down to speed, context handling and convenience.</p>

<hr />

<p><strong>Next:</strong> <a href="/2026/11/11/same-prompts-three-macs-where-the-machines-sit/">Where the Machines Sit</a></p>]]></content><author><name>Nikos Baxevanis</name><email>nikos.baxevanis@gmail.com</email></author><summary type="html"><![CDATA[Part 1 of Same Prompts, Three Macs. Two models, each kept in a large file and a small one, answered 40 checkable prompts. The scores were one prompt apart.]]></summary></entry><entry><title type="html">Same Prompts, Three Macs</title><link href="https://blog.nikosbaxevanis.com/2026/11/09/same-prompts-three-macs/" rel="alternate" type="text/html" title="Same Prompts, Three Macs" /><published>2026-11-09T00:00:00+00:00</published><updated>2026-11-12T00:00:00+00:00</updated><id>https://blog.nikosbaxevanis.com/2026/11/09/same-prompts-three-macs</id><content type="html" xml:base="https://blog.nikosbaxevanis.com/2026/11/09/same-prompts-three-macs/"><![CDATA[<p><img src="/assets/local-models/desk.jpg" alt="Two Mac Studios stacked and a closed MacBook Pro on a wooden desk, each with an external SSD beside it." /></p>

<p>In the next couple of posts, I’m going to send the same prompts to the
same models on three Macs and write down what the numbers say. Follow
along and you’ll:</p>

<ul>
  <li>see whether a higher-precision model file gets more answers right
than a smaller one of the same model</li>
  <li>see what stacking two Mac Studios, or closing a laptop lid, does to a
benchmark</li>
  <li>see a test that looked reasonable and measured nothing, and the
control that showed it</li>
</ul>

<p>This picks up where <a href="/2026/10/23/the-restore-is-the-test/">Restoring the Models on a Clean Mac</a> ended:
the models install and restore, but I had not compared them on anything.
The machines are an M3 Ultra Mac Studio with 256 GB of unified memory, an
M5 Max MacBook Pro with 128 GB, both from <a href="/2026/10/20/self-hosted-inference-on-apple-silicon/">the earlier posts</a>, and
an M5 Ultra Mac Studio with 256 GB that joined them since. The runs were
driven by Claude Code with <a href="https://www.anthropic.com/claude/fable">Claude Fable 5.2</a> over SSH; it also
wrote the prompts and the grader, which is a limit I come back to.</p>

<p>Here’s a list of posts to follow:</p>

<ul>
  <li><a href="/2026/11/10/same-prompts-three-macs-higher-precision-same-answers/">Higher Precision, Same Answers</a></li>
  <li><a href="/2026/11/11/same-prompts-three-macs-where-the-machines-sit/">Where the Machines Sit</a></li>
  <li><a href="/2026/11/12/same-prompts-three-macs-four-models-and-a-test-that-measured-nothing/">Four Models and a Test That Measured Nothing</a></li>
  <li>A domain task that can be graded (to come)</li>
</ul>

<p>Prompts, grader and scores: <a href="/assets/local-models/prompts.py">prompts.py</a>, <a href="/assets/local-models/compare.py">compare.py</a>,
<a href="/assets/local-models/results.json">results.json</a></p>]]></content><author><name>Nikos Baxevanis</name><email>nikos.baxevanis@gmail.com</email></author><summary type="html"><![CDATA[A short series measuring local models on three Macs: whether bigger model files answer better, how heat and placement change the timings, and what happened when I tried to grade them on a domain task.]]></summary></entry><entry><title type="html">Restoring the Models on a Clean Mac</title><link href="https://blog.nikosbaxevanis.com/2026/10/23/the-restore-is-the-test/" rel="alternate" type="text/html" title="Restoring the Models on a Clean Mac" /><published>2026-10-23T00:00:00+00:00</published><updated>2026-10-24T00:00:00+00:00</updated><id>https://blog.nikosbaxevanis.com/2026/10/23/the-restore-is-the-test</id><content type="html" xml:base="https://blog.nikosbaxevanis.com/2026/10/23/the-restore-is-the-test/"><![CDATA[<p><em>Part 8 of <a href="/2026/01/25/oracles-traces-triage/">Oracles, Traces, Triage</a>.</em></p>

<p>By this point I had separate model configurations for the two Macs,
measured context limits, and several runtime builds to keep track of.
The <a href="/2026/10/22/local-models-and-tool-permissions/">previous post</a> also covered the access settings I wanted
to manage separately. I needed a way to preserve that setup without
reconstructing it from terminal history.</p>

<p>This matters for the agent experiments that started the series. If I
change a model or its settings and get different results, I want to be
able to return to the earlier configuration. I also want to be able to
reset a Mac without losing the work needed to make those models run.</p>

<p>I put together an offline recovery library on an external SSD, including
the model files, pinned source, Python wheels, native runtimes, and Apple
toolchain material. Then I reset the M5 and restored from it. I handled
the administrator and license prompts myself. The installer finished with:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>RESTORED ALL EIGHT PROFILES. No persistent servers started.
</code></pre></div></div>

<p>I then started each model, checked its status, requested a generation, and
stopped it before moving to the next one. All eight passed. That was the
first full test of this M5 library on a factory-reset machine.</p>

<h2 id="what-the-clean-restore-tested">What the clean restore tested</h2>

<p>Before the reset, the qualification records said that offline component
restores had passed over independently provisioned files. They explicitly
did not claim a factory-reset restore.</p>

<p>The reset removed that uncertainty about the existing installation. It
was still a narrower inference test than the earlier qualification: I
ran generation smoke tests for all eight models, without repeating the
long-input or separate image tests. The <a href="/assets/local-inference/qualification.json">evidence summary</a> records
which checks ran before and after the reset.</p>

<p>The restore also exercised a dependency that would have been easy to
miss on a working Mac. Xcode setup initially couldn’t find the Metal
compiler. The installer then imported the archived Metal Toolchain
17F109, checked the compiler, and continued. Keeping the model files and
source alone would have left that part of the setup unfinished.</p>

<h2 id="one-repository-machine-specific-kits">One repository, machine-specific kits</h2>

<p>The storage layout settled into this shape:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>LLM/
  .git/
  restore-all.sh
  machines/
    m3-ultra-256gb/
      DwarfStar/
      Orca/
    m5-max-128gb/
      DwarfStar/
      Orca/
      optional-models/
    m5-ultra-256gb/       # Reserved; not qualified.
</code></pre></div></div>

<p>Git lives at <code class="language-plaintext highlighter-rouge">LLM/</code>, above the machine directories. It tracks the small
material that explains the installation: scripts, configuration, manifests,
and reviewed records. The large payloads remain outside Git history.</p>

<p>The model files need their own backup and checksums: recovering the Git
repository alone wouldn’t recover those ignored files. Keeping them
outside Git lets me review changes to the scripts and configuration
without putting hundreds of gigabytes of weights into each kit’s history.</p>

<p>The installed locations under <code class="language-plaintext highlighter-rouge">~/Downloads</code> stayed separate from the SSD
layout. The managers run from internal storage, including the streaming
model’s installed files. After a successful restore, the recovery SSD can
be ejected without taking the service away.</p>

<p>Each kit retained its own required assets. I avoided cross-kit links that
would make one machine’s recovery depend on another machine’s directory.
The M5 work also preserved the M3 engine pins and existing manager
interface. A cosmetic cleanup was not a sufficient reason to disturb a
working installation.</p>

<h2 id="eight-accepted-models">Eight accepted models</h2>

<p>The M3 had nine profiles. The M5 finished with eight.</p>

<p>An Orca DeepSeek conversion was still incomplete. Cancelling it meant
stopping its supervisor as well as the conversion process, after checking
their identities. Otherwise the supervisor could start a later step after
the visible process disappeared.</p>

<p>I retained the verified original MXFP4 shards as optional future-work
assets. The incomplete output and redundant internal download copies were
removed only after the SSD originals had been verified.</p>

<p>I also removed that profile from the installer, manifests, manager help,
and model tables, and renamed the M5 Orca kit to reflect its four working
models. The retained shards are explicitly optional. Restoring the eight
accepted models doesn’t depend on finishing that conversion.</p>

<h2 id="copying-the-library">Copying the library</h2>

<p>I copied the library to the other SSD and used an M2 Mac to inspect both
drives. The M2 was a verification host, not a newly qualified inference
target.</p>

<p>Both copies had the same Git revision and contained 4,082 filesystem
entries and 3,593 regular files. Names, sizes, permissions, and symlink
targets matched. The copied Git database passed its integrity check.</p>

<p>I initially started a full content hash. That reads every byte on both
drives: about 4.9 TB for these two logical copies. The first implementation
was CPU-bound. Switching the large-file hashing to the built-in OpenSSL
implementation raised observed disk reads to about 1.5 GB/s per drive.</p>

<p>I then chose the shorter verification scope. It hashed 3,400 small files
on each drive, checked all 617 tracked files against Git, and checked the
available small-file manifest entries. Those checks passed. The 193 large
files on each drive were not all fully rehashed in that comparison.</p>

<p>The quick comparison therefore verified the copied structure, Git state,
and small-file contents. It left the large model contents unverified in
that copy. Their earlier checksums don’t prove that every byte survived
the later copy unchanged.</p>

<p>The <a href="/assets/local-inference/copy-verification.json">copy-check summary</a> records the exact scope. Model weights
may be downloadable again from their upstream repositories, but that
depends on continued availability and access. It is still worth keeping
the archives that already restored successfully.</p>

<h2 id="what-i-kept-separate-from-restore">What I kept separate from restore</h2>

<p>The restore does not add firewall exceptions, change the router, configure
Tailscale, or leave persistent servers running. Those operations change
how the machine is exposed and when it consumes resources. They belong to
explicit operating steps.</p>

<p>Likewise, synchronization after independent work needs more care than
copying a folder over another folder. The two Git histories must be
preserved and reconciled, while payload differences are inspected and
verified separately. Replacing <code class="language-plaintext highlighter-rouge">.git</code> is not a merge strategy.</p>

<p>The <a href="/local-inference-operations/">command reference</a> contains the restore and lifecycle
commands for each accepted machine. It also calls out the older M3
single-model manager’s different argument syntax.</p>

<p>That finishes the installation work I needed before returning to the
agent experiments. I can restore the accepted configurations and check
that they generate, and I have records of the longer inputs they handled.
I still need to test them on the work described back in February: reading
specifications, proposing useful tests, and helping triage the failures.
Those results will determine which of these models I keep using.</p>

<hr />

<p><strong>Reference:</strong> <a href="/local-inference-operations/">Local inference operations</a></p>]]></content><author><name>Nikos Baxevanis</name><email>nikos.baxevanis@gmail.com</email></author><summary type="html"><![CDATA[Part 8 of Oracles, Traces, Triage. Resetting a Mac, restoring eight local model profiles, and separating a copied directory from a verified backup.]]></summary></entry><entry><title type="html">Local Models and Tool Permissions</title><link href="https://blog.nikosbaxevanis.com/2026/10/22/local-models-and-tool-permissions/" rel="alternate" type="text/html" title="Local Models and Tool Permissions" /><published>2026-10-22T00:00:00+00:00</published><updated>2026-10-24T00:00:00+00:00</updated><id>https://blog.nikosbaxevanis.com/2026/10/22/local-models-and-tool-permissions</id><content type="html" xml:base="https://blog.nikosbaxevanis.com/2026/10/22/local-models-and-tool-permissions/"><![CDATA[<p><em>Part 7 of <a href="/2026/01/25/oracles-traces-triage/">Oracles, Traces, Triage</a>.</em></p>

<p>After getting the local models running, I <a href="/2026/10/21/a-context-window-is-a-testable-claim/">tested their input
limits</a>. Those checks were requests to an inference server: text went
in and an answer came back. The testing agents from the earlier parts of this
series would do more with that answer. They would use it to decide which files
to read, which commands to run, and what to investigate next.</p>

<p>While choosing the models, I kept coming back to a concern about the
community-modified weights. Could a model emit a tool call asking my agent
to scan the drive and upload what it found? Would using an original
upstream model, or a different runtime, make that safe?</p>

<p>Running inference locally keeps the prompt processing on my machine. It
doesn’t stop a model from proposing an upload, and it doesn’t stop an agent
with broad permissions from carrying one out. That applies to an upstream
model too. I need to consider both the software I’m loading and the
permissions of the program acting on its output.</p>

<p>For the repository work I’ve described in this series, access to a
compiler, a checkout, and test results is useful. Access to unrelated
files in my home directory usually isn’t. The local installation is a
good point to make those choices explicit, before attaching the tools.</p>

<h2 id="start-with-what-executes">Start with what executes</h2>

<p>A model installation contains more than weights. It can include model
implementation code, tokenizer and processor code, a chat template, native
libraries, a Python environment, and an HTTP server. Some of these execute
directly. Others supply data to code that executes.</p>

<p>Custom model code can have an ordinary purpose: implementing an
architecture that the installed library does not support yet. Its presence
does not establish malicious behavior. It does add code to the review and
pinning problem. Hugging Face’s <a href="https://huggingface.co/docs/transformers/custom_models">custom-model documentation</a>
describes this extension mechanism and the explicit trust involved in
loading custom code.</p>

<p>File formats matter too. Pickle-based loading can execute code during
deserialization; Hugging Face documents that risk in its
<a href="https://huggingface.co/docs/hub/security-pickle">pickle-scanning guidance</a>. A tensor-oriented format avoids that
particular mechanism. It does not prove that a parser is free of bugs, that
the surrounding package is benign, or that the generated text is safe to
act on.</p>

<p>I therefore recorded the components together:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>weights + model code + runtime + template + server configuration
</code></pre></div></div>

<p>That includes the separate native runtime needed by Orca GLM, as well as
the model files. Revisions and hashes let me identify what I used and
notice changes later. They don’t establish that the software is benign;
the generation tests weren’t a security audit.</p>

<h2 id="then-follow-the-tool-call">Then follow the tool call</h2>

<p>Consider the path from a response to a side effect:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>model output
    |
    v
tool-call parser
    |
    v
permission checks
    |
    v
tool process ----&gt; filesystem / subprocesses / network
</code></pre></div></div>

<p>The model can propose a filesystem search. The parser can recognize it as
a valid tool request. Neither fact should authorize the search.</p>

<p>The same applies to uploads. A syntactically valid HTTP tool call can
still name an unapproved destination or contain data the process should
never have read. Giving a model a narrower system prompt does not remove
those capabilities from the process that executes its instructions.</p>

<p>For a repository investigation, I would make the checked-out repository
and an output directory available, keep unrelated home-directory data out
of the sandbox, and restrict network access at the execution boundary.
The specific permissions depend on the task. The important property is
that they are enforced outside the generated response.</p>

<p>This is a design requirement for the agent harness. The inference tests
described in these articles did not validate a complete sandbox or an
exfiltration-resistant worker.</p>

<h2 id="upstream-provenance-helps-within-its-scope">Upstream provenance helps, within its scope</h2>

<p>Using an upstream checkpoint can remove uncertainty about changes made by
another distributor. It does not remove the need to review the runtime or
constrain the agent. An upstream model can also produce an unsafe tool
request, misunderstand the task, or follow instructions found in a file.</p>

<p>Similarly, switching from MLX-VLM to MLX-LM would change part of the
software stack. It would not by itself make the model’s tool requests
trustworthy. I would still need to establish compatibility, inspect the
loaded implementation, and test the resulting service.</p>

<p>The word “uncensored” in an artifact name describes a publisher’s
positioning. It is not evidence about the absence or presence of a
backdoor. I had no finding that the Orca artifacts were malicious.</p>

<h2 id="accessing-the-server-from-another-device">Accessing the server from another device</h2>

<p>I also wanted to send requests from other devices, including my phone
through a router running Tailscale. That raised a different access
question: who could reach the model server?</p>

<p>With the service bound to loopback, an SSH tunnel can carry requests from
another machine. With a wildcard bind, the service can accept connections
through the machine’s network interfaces, subject to routing and firewall
rules. The tested servers did not provide API authentication.</p>

<p>For an already configured SSH host alias named <code class="language-plaintext highlighter-rouge">inference-mac</code>, a tunnel
can look like this:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ssh <span class="nt">-N</span> <span class="nt">-o</span> <span class="nv">ExitOnForwardFailure</span><span class="o">=</span><span class="nb">yes</span> <span class="se">\</span>
  <span class="nt">-L</span> 127.0.0.1:18000:127.0.0.1:8000 inference-mac
</code></pre></div></div>

<p>The client then talks to <code class="language-plaintext highlighter-rouge">http://127.0.0.1:18000/v1</code>. The explicit local
bind keeps this forwarded port on the client’s loopback interface. These
are the ordinary <a href="https://man.openbsd.org/ssh#L">OpenSSH local-forwarding semantics</a>. SSH access
must already be configured and its host key verified.</p>

<p>For direct LAN access I added macOS application-firewall exceptions
manually. The installer deliberately left that decision to the operator.
Apple’s <a href="https://support.apple.com/guide/mac-help/mh34041/mac">firewall controls</a> allow incoming connections by
application; those exceptions should not be confused with a rule allowing
only one source address to one model port.</p>

<p>A request from another machine returned the model listing on port 8000.
That established remote HTTP reachability. The local probe had already
established generation. I haven’t yet tested the phone’s route through
the Tailscale router.</p>

<p>For a future investigation, I would start by restricting the agent to its
checkout, working directory, and approved tools and destinations. Choosing
a model or changing the server doesn’t do that work for me.</p>

<p>The restore scripts preserve the model settings but leave firewall
changes to the operator. In the <a href="/2026/10/23/the-restore-is-the-test/">next post</a>, I describe what they do
restore, and what happened when I tried them after resetting the M5.</p>

<hr />

<p><strong>Next:</strong> <a href="/2026/10/23/the-restore-is-the-test/">Restoring the Models on a Clean Mac</a></p>]]></content><author><name>Nikos Baxevanis</name><email>nikos.baxevanis@gmail.com</email></author><summary type="html"><![CDATA[Part 7 of Oracles, Traces, Triage. The concern that came up while choosing community models, and the tool permissions I need to settle before using them for repository investigations.]]></summary></entry><entry><title type="html">Testing Longer Contexts</title><link href="https://blog.nikosbaxevanis.com/2026/10/21/a-context-window-is-a-testable-claim/" rel="alternate" type="text/html" title="Testing Longer Contexts" /><published>2026-10-21T00:00:00+00:00</published><updated>2026-10-24T00:00:00+00:00</updated><id>https://blog.nikosbaxevanis.com/2026/10/21/a-context-window-is-a-testable-claim</id><content type="html" xml:base="https://blog.nikosbaxevanis.com/2026/10/21/a-context-window-is-a-testable-claim/"><![CDATA[<p><em>Part 6 of <a href="/2026/01/25/oracles-traces-triage/">Oracles, Traces, Triage</a>.</em></p>

<p>In the <a href="/2026/10/20/self-hosted-inference-on-apple-silicon/">previous article</a>, I described getting local models
running on the two Macs. A short prompt checked that an installation could
generate an answer. The agent experiments from earlier in this series
need more than that: the model may have to read source files, a piece of a
specification, and the output of several failed test runs before deciding
what to try next.</p>

<p>All of that takes space in the context window, the input and output the
model can work with in one request. That space is measured in tokens,
which are pieces of text rather than necessarily whole words. The code
and conversation history use the same budget as the answer.</p>

<p>When I looked at the initial settings, two <a href="https://github.com/antirez/ds4">DwarfStar</a> profiles were
limited to 8,192 tokens. Several <a href="https://huggingface.co/orcarouter">Orca</a> profiles had no explicit cap in
the manager. I wanted to see how much further I could take them on these
machines, and to have a predictable failure when a request was too large.</p>

<p>The 8K settings were defaults, not measurements of what the models could
handle. Nor could I read an upstream context limit and assume the local
installation would manage it. The runtime’s memory use and handling of
long requests needed testing with the exact configurations I intended to
keep.</p>

<h2 id="a-cap-needs-a-test-on-both-sides">A cap needs a test on both sides</h2>

<p>The procedure started with a small generation and increased the input in
stages. The model had to retrieve specific records from synthetic text,
then handle a follow-up asking for those records in a different order.
An oversized request had to be rejected. A final small probe checked that
the server remained usable afterwards.</p>

<p>For each configuration I checked that:</p>

<ol>
  <li>The server reports the expected model and context configuration.</li>
  <li>The request contains the measured number of tokens after formatting.</li>
  <li>The answer contains the expected records.</li>
  <li>A follow-up preserves the required information.</li>
  <li>An over-limit request fails instead of silently discarding input.</li>
  <li>The server can still generate after that rejection.</li>
</ol>

<p>The request and the generated answer share a context budget. A
200,000-token cap is not an invitation to send 200,000 input tokens and
then ask for a long answer. The template and any other inserted tokens
count too. The server’s default output budget is a separate setting;
clients should send <code class="language-plaintext highlighter-rouge">max_tokens</code> explicitly.</p>

<p>I precomputed the test inputs before loading the large engine. Earlier,
running an auxiliary tokenizer diagnostic alongside a resident model had
caused memory pressure severe enough to interfere with SSH access. Moving
that preparation out of the inference run avoided making the diagnostic
compete with the model for memory.</p>

<h2 id="what-passed-on-the-smaller-mac">What passed on the smaller Mac</h2>

<p>The following is the accepted M5 Max, 128 GB result. The figures come from
the <a href="/assets/local-inference/qualification.json">recorded qualification summaries</a>, not from model-card maxima.</p>

<table>
  <thead>
    <tr>
      <th style="text-align: left">Collection</th>
      <th style="text-align: left">Model</th>
      <th style="text-align: right">Context cap</th>
      <th style="text-align: right">Largest tested input</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left"><a href="https://github.com/antirez/ds4">DwarfStar</a></td>
      <td style="text-align: left"><a href="https://huggingface.co/deepseek-ai/DeepSeek-V4-Flash-0731">DeepSeek V4 Flash 0731</a></td>
      <td style="text-align: right">200,000</td>
      <td style="text-align: right">197,925</td>
    </tr>
    <tr>
      <td style="text-align: left"><a href="https://github.com/antirez/ds4">DwarfStar</a></td>
      <td style="text-align: left"><a href="https://huggingface.co/deepseek-ai/DeepSeek-V4.1-Flash-1017">DeepSeek V4.1 Flash 1017</a></td>
      <td style="text-align: right">200,000</td>
      <td style="text-align: right">197,906</td>
    </tr>
    <tr>
      <td style="text-align: left"><a href="https://github.com/antirez/ds4">DwarfStar</a></td>
      <td style="text-align: left"><a href="https://huggingface.co/zai-org/GLM-5.3-Flash">GLM 5.3 Flash</a></td>
      <td style="text-align: right">200,000</td>
      <td style="text-align: right">197,918</td>
    </tr>
    <tr>
      <td style="text-align: left"><a href="https://github.com/antirez/ds4">DwarfStar</a></td>
      <td style="text-align: left"><a href="https://huggingface.co/Qwen/Qwen3.8-Flash-Next">Qwen3.8 Flash Next</a></td>
      <td style="text-align: right">200,000</td>
      <td style="text-align: right">197,847</td>
    </tr>
    <tr>
      <td style="text-align: left"><a href="https://huggingface.co/orcarouter">Orca</a></td>
      <td style="text-align: left"><a href="https://huggingface.co/orcarouter/Qwen3.8-27B-Uncensored-MLX">Qwen3.8 27B</a></td>
      <td style="text-align: right">131,072</td>
      <td style="text-align: right">129,174</td>
    </tr>
    <tr>
      <td style="text-align: left"><a href="https://huggingface.co/orcarouter">Orca</a></td>
      <td style="text-align: left"><a href="https://huggingface.co/orcarouter/Nex-N2.5-mini-Uncensored-MLX">Nex-N2.5-mini</a></td>
      <td style="text-align: right">131,072</td>
      <td style="text-align: right">129,141</td>
    </tr>
    <tr>
      <td style="text-align: left"><a href="https://huggingface.co/orcarouter">Orca</a></td>
      <td style="text-align: left"><a href="https://huggingface.co/orcarouter/OrcaSAQ-2-Cyber-27B-Uncensored-GGUF">OrcaSAQ-2 Cyber 27B</a></td>
      <td style="text-align: right">131,072</td>
      <td style="text-align: right">129,283</td>
    </tr>
    <tr>
      <td style="text-align: left"><a href="https://huggingface.co/orcarouter">Orca</a></td>
      <td style="text-align: left"><a href="https://huggingface.co/orcarouter/GLM-5.3-Flash-Uncensored-GGUF">GLM 5.3 Flash</a></td>
      <td style="text-align: right">131,072</td>
      <td style="text-align: right">129,019</td>
    </tr>
  </tbody>
</table>

<p>Each accepted M5 run used one engine and one sequence or session. The
sampled memory-pressure level remained normal. Some runs began with swap
already allocated, so I recorded the starting and peak values rather than
describing the machine as swap-free.</p>

<p>All eight passed the synthetic retrieval, follow-up, rejection, and
recovery checks at these sizes. I would still need real repository tasks
to judge whether the extra context helps with an investigation. Finding
records in generated text is a much narrower task than understanding the
interactions in a large codebase. The separate image smoke tests also
leave long-context multimodal use untested.</p>

<h2 id="the-second-request-can-be-a-different-workload">The second request can be a different workload</h2>

<p>The largest Orca requests produced the following elapsed times, including
their short answers:</p>

<table>
  <thead>
    <tr>
      <th style="text-align: left">Model</th>
      <th style="text-align: right">Initial request (seconds)</th>
      <th style="text-align: right">Follow-up (seconds)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left"><a href="https://huggingface.co/orcarouter/Qwen3.8-27B-Uncensored-MLX">Qwen3.8 27B</a></td>
      <td style="text-align: right">447.7</td>
      <td style="text-align: right">454.0</td>
    </tr>
    <tr>
      <td style="text-align: left"><a href="https://huggingface.co/orcarouter/Nex-N2.5-mini-Uncensored-MLX">Nex-N2.5-mini</a></td>
      <td style="text-align: right">138.0</td>
      <td style="text-align: right">139.6</td>
    </tr>
    <tr>
      <td style="text-align: left"><a href="https://huggingface.co/orcarouter/OrcaSAQ-2-Cyber-27B-Uncensored-GGUF">OrcaSAQ-2 Cyber 27B</a></td>
      <td style="text-align: right">869.7</td>
      <td style="text-align: right">4.6</td>
    </tr>
    <tr>
      <td style="text-align: left"><a href="https://huggingface.co/orcarouter/GLM-5.3-Flash-Uncensored-GGUF">GLM 5.3 Flash</a></td>
      <td style="text-align: right">3,208.8</td>
      <td style="text-align: right">4.6</td>
    </tr>
  </tbody>
</table>

<p>The follow-up sent the conversation again and asked for the records in
reverse order. The selected MLX serving path recomputed the conversation;
the native serving paths reused much more state. A four-second follow-up
therefore tells a very different story from a four-second cold request.</p>

<p>Background copies and downloads were active during qualification. These
are observations from the recorded workloads, not isolated throughput
benchmarks. They should not be used to rank the underlying model families.</p>

<p>The DwarfStar DeepSeek V4.1 streaming configuration made the same point:
its largest input took about thirty minutes, followed by a cached request
of about five seconds. Whether that is acceptable depends on the job. A
long unattended investigation and an interactive editing session have
different tolerances.</p>

<h2 id="what-i-can-use-these-results-for">What I can use these results for</h2>

<p>The caps now give the agent client a budget to work within, and the
rejection tests give me a way to catch regressions in how the server
enforces it. The timings will also affect how I construct requests. A
follow-up that reuses earlier computation and one that processes the
whole conversation again can have very different costs.</p>

<p>There is still a quality comparison to do. On the M5, smaller
quantizations made some of these configurations practical, but the
retrieval checks don’t tell me how much reasoning or coding ability I
lost. For that I need the same tasks, prompts, tool budgets, and scoring
for each candidate. I will keep those results separate from the context
qualification table.</p>

<p>For now, I have measured input limits and a repeatable procedure for
checking them. Before connecting an agent to these services, I also need
to decide what it will be allowed to do with their responses. That became
a concrete concern while choosing community-modified models, and is the
subject of the <a href="/2026/10/22/local-models-and-tool-permissions/">next article</a>.</p>

<hr />

<p><strong>Next:</strong> <a href="/2026/10/22/local-models-and-tool-permissions/">Local Models and Tool Permissions</a></p>]]></content><author><name>Nikos Baxevanis</name><email>nikos.baxevanis@gmail.com</email></author><summary type="html"><![CDATA[Part 6 of Oracles, Traces, Triage. Moving beyond an 8K default with measured inputs, explicit caps, follow-up requests, and rejection tests.]]></summary></entry><entry><title type="html">Self-Hosted Inference on Apple Silicon</title><link href="https://blog.nikosbaxevanis.com/2026/10/20/self-hosted-inference-on-apple-silicon/" rel="alternate" type="text/html" title="Self-Hosted Inference on Apple Silicon" /><published>2026-10-20T00:00:00+00:00</published><updated>2026-10-24T00:00:00+00:00</updated><id>https://blog.nikosbaxevanis.com/2026/10/20/self-hosted-inference-on-apple-silicon</id><content type="html" xml:base="https://blog.nikosbaxevanis.com/2026/10/20/self-hosted-inference-on-apple-silicon/"><![CDATA[<p><em>Part 5 of <a href="/2026/01/25/oracles-traces-triage/">Oracles, Traces, Triage</a>.</em></p>

<p>Back in February, I left this series with <a href="/2026/02/16/agents-as-fuzzers/">Agents as Fuzzers</a>.
The idea was to combine agents that read specifications and propose tests
with fuzzers that execute those tests, then check and triage the results.
The earlier posts covered the instructions, coordination, and testing
practices around that loop. I was using Claude Code, and hadn’t spent much
of the series on the model running underneath it.</p>

<p>Eight months later, I’m picking up that part. I’ve been setting up local
models on two Macs: an M3 Ultra with 256 GB of unified memory and an M5 Max
with 128 GB. I want to find out how much of this agent-assisted work I can
run on my own hardware. Before comparing the models on testing tasks, I
need an installation I can run reliably and rebuild when something goes
wrong.</p>

<p>The series outline originally put self-hosted inference on Runpod next.
This experiment ended up on Apple Silicon instead. It is a detour into the
machinery behind the agent, over four posts: getting the models running,
testing how much input they can handle, deciding what access to give them,
and restoring the whole setup after a reset. I haven’t yet measured
whether these local models can do the bug-finding work as well as the
hosted models I’ve been using.</p>

<h2 id="what-im-moving-onto-the-mac">What I’m moving onto the Mac</h2>

<p>In the earlier articles, an agent could read a repository, suggest a test,
run it, and inspect the result. The model supplies the responses, which
may include requests to use a
tool. The agent program handles the conversation and runs the permitted
tools. In this setup, the model runs in a separate server process.</p>

<p>Here I’m setting up the model side. An inference server loads the model
files and accepts requests from a client, such as an agent. The files
contain the model’s weights; the runtime is the software that uses those
weights to generate an answer. Hosting that server locally lets me keep
this part of the work on the Mac and pin the exact files and software I
use. The agent’s tools and permissions still need their own configuration.</p>

<p>This also changes the practical constraints on the experiments from
February. A model that takes several minutes to read a prompt will make
an agent’s repeated attempts expensive in time. A model that fits alone
may not fit alongside another worker. I need to know those limits before
trying the <a href="/2026/02/08/agent-teams-and-claude-swarm/">parallel-agent setup from Part 2</a> with these models.</p>

<h2 id="moving-from-256-gb-to-128-gb">Moving from 256 GB to 128 GB</h2>

<p>The M3 installation had nine model profiles. Each profile selected a model
and the settings needed to serve it. I kept separate kits for the M5,
because copying the M3 configuration to a machine with half the memory
wouldn’t tell me which models were actually usable there.</p>

<p>For several models, that meant choosing a smaller quantization: weights
stored with reduced numerical precision to reduce their size. Names such
as Q2, Q4, and 8-bit identify different choices of representation. Some
artifacts use a mixture, keeping selected parts at higher precision. The
memory saving needs to be weighed against any loss in answer quality;
getting a file to load only answers the first half of that question.</p>

<p>I organized the kits into two collections, called <a href="https://github.com/antirez/ds4">DwarfStar</a> and
<a href="https://huggingface.co/orcarouter">Orca</a>.
These names recur in the tables below. Each kit includes the model files,
runtime, settings, restore scripts, and test records. The collection name
doesn’t imply that all its models use the same engine.</p>

<p>The M5 ended up with these eight configurations:</p>

<table>
  <thead>
    <tr>
      <th style="text-align: left">Collection</th>
      <th style="text-align: left">Model</th>
      <th style="text-align: left">Selected format or loading arrangement</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left"><a href="https://github.com/antirez/ds4">DwarfStar</a></td>
      <td style="text-align: left"><a href="https://huggingface.co/deepseek-ai/DeepSeek-V4-Flash-0731">DeepSeek V4 Flash 0731</a></td>
      <td style="text-align: left">Mixed Q2/Q4 experts</td>
    </tr>
    <tr>
      <td style="text-align: left"><a href="https://github.com/antirez/ds4">DwarfStar</a></td>
      <td style="text-align: left"><a href="https://huggingface.co/deepseek-ai/DeepSeek-V4.1-Flash-1017">DeepSeek V4.1 Flash 1017</a></td>
      <td style="text-align: left">IQ2/Q2, SSD expert streaming</td>
    </tr>
    <tr>
      <td style="text-align: left"><a href="https://github.com/antirez/ds4">DwarfStar</a></td>
      <td style="text-align: left"><a href="https://huggingface.co/zai-org/GLM-5.3-Flash">GLM 5.3 Flash</a></td>
      <td style="text-align: left">Q2 with a vision encoder</td>
    </tr>
    <tr>
      <td style="text-align: left"><a href="https://github.com/antirez/ds4">DwarfStar</a></td>
      <td style="text-align: left"><a href="https://huggingface.co/Qwen/Qwen3.8-Flash-Next">Qwen3.8 Flash Next</a></td>
      <td style="text-align: left">Q4, disk-backed n-grams</td>
    </tr>
    <tr>
      <td style="text-align: left"><a href="https://huggingface.co/orcarouter">Orca</a></td>
      <td style="text-align: left"><a href="https://huggingface.co/orcarouter/Qwen3.8-27B-Uncensored-MLX">Qwen3.8 27B</a></td>
      <td style="text-align: left">MLX 8-bit</td>
    </tr>
    <tr>
      <td style="text-align: left"><a href="https://huggingface.co/orcarouter">Orca</a></td>
      <td style="text-align: left"><a href="https://huggingface.co/orcarouter/Nex-N2.5-mini-Uncensored-MLX">Nex-N2.5-mini</a></td>
      <td style="text-align: left">MLX 6-bit with selected 8-bit modules</td>
    </tr>
    <tr>
      <td style="text-align: left"><a href="https://huggingface.co/orcarouter">Orca</a></td>
      <td style="text-align: left"><a href="https://huggingface.co/orcarouter/OrcaSAQ-2-Cyber-27B-Uncensored-GGUF">OrcaSAQ-2 Cyber 27B</a></td>
      <td style="text-align: left">Mixed GGUF, mostly IQ4_XS</td>
    </tr>
    <tr>
      <td style="text-align: left"><a href="https://huggingface.co/orcarouter">Orca</a></td>
      <td style="text-align: left"><a href="https://huggingface.co/orcarouter/GLM-5.3-Flash-Uncensored-GGUF">GLM 5.3 Flash</a></td>
      <td style="text-align: left">Q2_K GGUF with an F16 vision encoder</td>
    </tr>
  </tbody>
</table>

<p>The <a href="/assets/local-inference/qualification.json">qualification record</a> identifies the selected artifacts and
the tests they passed. This isn’t a recommendation of one model over
another. I haven’t done the paired coding and reasoning tests needed to
compare, for example, the smaller GLM quantization with the one on the M3.
DeepSeek V4.1 Flash 1017 is a recent addition: its public repository records
its <a href="https://huggingface.co/deepseek-ai/DeepSeek-V4.1-Flash-1017/commits/main">release on October 17</a>, before these experiments.</p>

<h2 id="what-stays-in-memory">What stays in memory</h2>

<p>The size on disk wasn’t enough to work out what would fit. I also had to
account for what the runtime kept in memory, and what it needed while
processing a request.</p>

<p>The M5 DeepSeek V4.1 configuration streams expert weights from the
internal SSD, keeping a <code class="language-plaintext highlighter-rouge">64GB</code> cache in memory. Qwen has large n-gram
tables, described in its <a href="https://huggingface.co/Qwen/Qwen3.8-Flash-Next">model card</a>, which the selected runtime
keeps disk-backed. Both arrangements let me use artifacts larger than I
could keep entirely resident. They also make the time spent reading and
processing a new prompt worth measuring.</p>

<p>The external SSD serves a different purpose. It holds the recovery kit;
the installed models run from internal storage. I can disconnect the
external drive after restoring them.</p>

<p>I tested one engine and one sequence at a time on the M5. During the
earlier work, an auxiliary diagnostic running alongside a loaded model
caused severe memory pressure. For now, the agent-swarm experiments will
have to respect that restriction. These results don’t establish that I
can run several local workers concurrently.</p>

<h2 id="more-than-one-runtime">More than one runtime</h2>

<p>The model choices also left me with several runtimes to maintain.
<a href="https://github.com/ml-explore/mlx-lm">MLX-LM</a> and <a href="https://github.com/Blaizzy/mlx-vlm">MLX-VLM</a> are separate projects using Apple’s
MLX framework, with MLX-VLM supporting vision-language models. The Orca
Qwen and Nex installations use a pinned MLX-VLM environment. OrcaSAQ and
Orca GLM use GGUF files, a format consumed here by native llama.cpp
servers. The DwarfStar models use two archived revisions of the DS4 engine.</p>

<p>Even the two llama.cpp installations aren’t interchangeable. Orca GLM
needed a separate pinned build. One loading attempt failed a Metal
allocation; the configuration that passed used automatic fitting, a
single sequence, and <code class="language-plaintext highlighter-rouge">--load-mode none</code>. In that particular build,
<a href="https://github.com/unslothai/llama.cpp/blob/86ebfef2c6a0f3359a2a07d2c215d61b0fa885c9/common/arg.cpp#L2580"><code class="language-plaintext highlighter-rouge">none</code> disables special loading modes</a>, including mmap. It
doesn’t disable GPU inference.</p>

<p>I kept those settings with their runtime revisions so that a future
restore wouldn’t depend on remembering which build accepted which flags.
The <a href="/local-inference-operations/">operations reference</a> has the machine-specific commands.</p>

<h2 id="checking-that-a-model-can-run">Checking that a model can run</h2>

<p>Once a server starts, it can answer an HTTP request for its model list
without having generated anything. I needed a short generation test as
well as that readiness check. The managers therefore expose four actions:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>start -&gt; status -&gt; probe -&gt; stop
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">status</code> checks the owned process and HTTP readiness. <code class="language-plaintext highlighter-rouge">probe</code> asks the model
to generate an answer and checks the response. The Orca start operation
also runs a generation before reporting success. Stopping checks the
process identity, including its start time and command, before sending a
signal; a saved PID alone could refer to a different process by then.</p>

<p>Eight M5 profiles had passed the qualification procedure.
The remaining candidate, Orca DeepSeek, still had an unfinished conversion.
I stopped the conversion and its supervisor, kept the verified original
shards for possible later work, and left it out of the installer.</p>

<p>A short answer was enough to check that each restored installation could
perform inference. For the work in this series, though, the input will
include code, specifications, and the results of earlier tool calls. The
<a href="/2026/10/21/a-context-window-is-a-testable-claim/">next article</a> covers what happened when I tested much longer
requests, including how long I had to wait for the answers.</p>

<hr />

<p><strong>Next:</strong> <a href="/2026/10/21/a-context-window-is-a-testable-claim/">Testing Longer Contexts</a></p>]]></content><author><name>Nikos Baxevanis</name><email>nikos.baxevanis@gmail.com</email></author><summary type="html"><![CDATA[Part 5 of Oracles, Traces, Triage. Returning to the agent experiments with a local model server: what runs on my Macs, and what needs testing before I can use it for that work.]]></summary></entry><entry><title type="html">The Theorem Holds for the Wrong Function</title><link href="https://blog.nikosbaxevanis.com/2026/10/10/the-theorem-holds-for-the-wrong-function/" rel="alternate" type="text/html" title="The Theorem Holds for the Wrong Function" /><published>2026-10-10T00:00:00+00:00</published><updated>2026-10-10T00:00:00+00:00</updated><id>https://blog.nikosbaxevanis.com/2026/10/10/the-theorem-holds-for-the-wrong-function</id><content type="html" xml:base="https://blog.nikosbaxevanis.com/2026/10/10/the-theorem-holds-for-the-wrong-function/"><![CDATA[<p><em>Part 3 of <a href="/2026/07/10/what-a-proof-covers/">What a Proof Covers</a>.</em></p>

<p>The <a href="/2026/09/10/every-list/">previous post</a> proved, for every finite list, that
reversing twice gives back the list. Here is another function with the
same property:</p>

<div class="language-lean highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="n">revId</span> <span class="err">{</span>α : <span class="kt">Type</span><span class="err">}</span> (<span class="n">xs</span> : <span class="n">List</span> α) : <span class="n">List</span> α := <span class="n">xs</span>

<span class="k">theorem</span> <span class="n">revId_twice</span> <span class="err">{</span>α : <span class="kt">Type</span><span class="err">}</span> (<span class="n">xs</span> : <span class="n">List</span> α) :
    <span class="n">revId</span> (<span class="n">revId</span> <span class="n">xs</span>) <span class="o">=</span> <span class="n">xs</span> := <span class="n">rfl</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">revId</code> returns its input. It reverses nothing, and the theorem holds
for it, with a one-word proof. Lean is correct to accept it. Doing
nothing twice does give back the list.</p>

<p>So <code class="language-plaintext highlighter-rouge">rev_twice</code> was never a proof that <code class="language-plaintext highlighter-rouge">rev</code> reverses. It was a proof of
the sentence I wrote down, and the sentence was weaker than what I
meant. That is the first of three limits, and none of them is specific
to this toy.</p>

<h2 id="limit-one-the-statement">Limit one: the statement</h2>

<p>The same weakness was in the tests all along. The round-trip property
from <a href="/2026/08/10/one-list-a-thousand-lists/">the first post</a> passes for the identity function in both
languages:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">// Wrong on purpose: returns its input.</span>
<span class="k">func</span> <span class="n">revId</span><span class="p">(</span><span class="n">xs</span> <span class="p">[]</span><span class="kt">int</span><span class="p">)</span> <span class="p">[]</span><span class="kt">int</span> <span class="p">{</span>
	<span class="k">return</span> <span class="n">xs</span>
<span class="p">}</span>

<span class="k">func</span> <span class="n">TestIdentityAlsoPasses</span><span class="p">(</span><span class="n">t</span> <span class="o">*</span><span class="n">testing</span><span class="o">.</span><span class="n">T</span><span class="p">)</span> <span class="p">{</span>
	<span class="n">rule</span> <span class="o">:=</span> <span class="k">func</span><span class="p">(</span><span class="n">xs</span> <span class="p">[]</span><span class="kt">int</span><span class="p">)</span> <span class="kt">bool</span> <span class="p">{</span>
		<span class="k">return</span> <span class="n">slices</span><span class="o">.</span><span class="n">Equal</span><span class="p">(</span>
			<span class="n">revId</span><span class="p">(</span><span class="n">revId</span><span class="p">(</span><span class="n">xs</span><span class="p">)),</span> <span class="n">xs</span><span class="p">)</span>
	<span class="p">}</span>
	<span class="k">if</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">quick</span><span class="o">.</span><span class="n">Check</span><span class="p">(</span><span class="n">rule</span><span class="p">,</span> <span class="no">nil</span><span class="p">);</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
		<span class="n">t</span><span class="o">.</span><span class="n">Fatal</span><span class="p">(</span><span class="n">err</span><span class="p">)</span>
	<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>A statement that pins the behaviour down says where each element ends
up: the result has the same length, and position <code class="language-plaintext highlighter-rouge">i</code> holds what was at
position <code class="language-plaintext highlighter-rouge">length - 1 - i</code>. Here it is in C#, run against <code class="language-plaintext highlighter-rouge">RevId</code>, which
like the Go function above returns its argument:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">mirrored</span> <span class="p">=</span>
    <span class="k">from</span> <span class="n">xs</span> <span class="k">in</span> <span class="n">lists</span><span class="p">.</span><span class="nf">ForAll</span><span class="p">()</span>
    <span class="k">let</span> <span class="n">a</span> <span class="p">=</span> <span class="n">xs</span><span class="p">.</span><span class="nf">ToArray</span><span class="p">()</span>
    <span class="k">let</span> <span class="n">b</span> <span class="p">=</span> <span class="nf">RevId</span><span class="p">(</span><span class="n">a</span><span class="p">)</span>
    <span class="k">select</span> <span class="n">a</span><span class="p">.</span><span class="n">Length</span> <span class="p">==</span> <span class="n">b</span><span class="p">.</span><span class="n">Length</span>
        <span class="p">&amp;&amp;</span> <span class="n">Enumerable</span><span class="p">.</span><span class="nf">Range</span><span class="p">(</span><span class="m">0</span><span class="p">,</span> <span class="n">a</span><span class="p">.</span><span class="n">Length</span><span class="p">)</span>
            <span class="p">.</span><span class="nf">All</span><span class="p">(</span><span class="n">i</span> <span class="p">=&gt;</span> <span class="n">b</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="p">==</span> <span class="n">a</span><span class="p">[</span><span class="n">a</span><span class="p">.</span><span class="n">Length</span> <span class="p">-</span> <span class="m">1</span> <span class="p">-</span> <span class="n">i</span><span class="p">]);</span>

<span class="n">mirrored</span><span class="p">.</span><span class="nf">Check</span><span class="p">();</span>
</code></pre></div></div>

<p>Against the identity function, Hedgehog 2.0.4 reports this, with the
line that gives the seed for a rerun left out:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>*** Failed! Falsifiable (after 1 test and 9 shrinks):

Generated values:
  [-100; -99]
</code></pre></div></div>

<p>Two different elements are the smallest list on which “unchanged” and
“reversed” disagree, and shrinking found it.</p>

<p>On the Lean side, the lemma from the previous post already does this
job. <code class="language-plaintext highlighter-rouge">rev_append</code> says <code class="language-plaintext highlighter-rouge">rev (xs ++ ys) = rev ys ++ rev xs</code>. For the
identity function that would claim <code class="language-plaintext highlighter-rouge">[1, 2] = [2, 1]</code>, so it cannot be
proved. The helper turned out to say more about reversing than the
headline theorem did.</p>

<p>Moving from tests to proofs did not remove the need to ask whether the
statement is the right one. It raised the price of asking late.</p>

<h2 id="limit-two-the-thing-it-is-about">Limit two: the thing it is about</h2>

<p><code class="language-plaintext highlighter-rouge">rev_twice</code> is about a Lean definition over mathematical lists. The
code that runs is this:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">static</span> <span class="kt">int</span><span class="p">[]</span> <span class="nf">Rev</span><span class="p">(</span><span class="kt">int</span><span class="p">[]</span> <span class="n">xs</span><span class="p">)</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">ys</span> <span class="p">=</span> <span class="p">(</span><span class="kt">int</span><span class="p">[])</span><span class="n">xs</span><span class="p">.</span><span class="nf">Clone</span><span class="p">();</span>
    <span class="n">Array</span><span class="p">.</span><span class="nf">Reverse</span><span class="p">(</span><span class="n">ys</span><span class="p">);</span>
    <span class="k">return</span> <span class="n">ys</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Nothing connects the two. The C# method works on a machine array with a
32-bit length, calls a library routine and allocates memory. The Lean
definition does none of that. The proof would be untouched if the C#
method were replaced by one that throws.</p>

<p>There are three honest ways to connect them, in rising order of cost:</p>

<ol>
  <li>Test the running code against the proved definition. The Lean
function becomes the oracle for a property test or a fuzzer. This
shows agreement on the inputs tried, and it is cheap.</li>
  <li>Run the proved code itself, by compiling the Lean definition and
calling it. Then the compiler and runtime are trusted instead.</li>
  <li>Model the C# or Go semantics, including machine integers, and prove
the real function equal to the definition. This is the only one that
earns the word “verified” for the client code, and it is a project,
not an afternoon.</li>
</ol>

<p>Machine integers deserve their own mention. Lean’s <code class="language-plaintext highlighter-rouge">Nat</code> never
overflows. A sum that wraps around, or a conversion between integer
types that changes the value, simply does not exist in a model written
with <code class="language-plaintext highlighter-rouge">Nat</code>. A proof over <code class="language-plaintext highlighter-rouge">Nat</code> is silent about the exact place such a
bug lives.</p>

<h2 id="limit-three-writing-it-in-lean">Limit three: writing it in Lean</h2>

<p>Lean is also a programming language, and a program written in it is not
proved by being written in it. Take the proved function and add
logging:</p>

<div class="language-lean highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="n">revLogged</span> (<span class="n">xs</span> : <span class="n">List</span> <span class="n">Nat</span>) : <span class="n">IO</span> (<span class="n">List</span> <span class="n">Nat</span>) := <span class="n">do</span>
  <span class="n">IO</span><span class="o">.</span><span class="n">println</span> <span class="n">s</span><span class="o">!</span><span class="s">"input: {xs}"</span>
  <span class="n">pure</span> (<span class="n">rev</span> <span class="n">xs</span>)

<span class="k">def</span> <span class="n">twiceLogged</span> (<span class="n">xs</span> : <span class="n">List</span> <span class="n">Nat</span>) : <span class="n">IO</span> (<span class="n">List</span> <span class="n">Nat</span>) := <span class="n">do</span>
  <span class="n">let</span> <span class="n">ys</span> <span class="err">←</span> <span class="n">revLogged</span> <span class="n">xs</span>
  <span class="n">revLogged</span> <span class="n">ys</span>
</code></pre></div></div>

<p>For <code class="language-plaintext highlighter-rouge">[10, 20, 30]</code> this prints two lines and returns <code class="language-plaintext highlighter-rouge">[10, 20, 30]</code>.
The returned list is restored. The two writes still happened.
<code class="language-plaintext highlighter-rouge">rev_twice</code> says nothing about what was printed, whether printing can
fail, or whether those values should be in a log at all. Claims about
effects need their own specification and their own proof.</p>

<p>The same holds for the rest of the language. Each of these is ordinary
Lean and each sits outside what a theorem about <code class="language-plaintext highlighter-rouge">rev</code> covers:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">partial def</code> accepts a function without a termination argument.</li>
  <li><code class="language-plaintext highlighter-rouge">xs[i]!</code> indexes without a bounds proof and panics at run time when
the index is out of range.</li>
  <li><code class="language-plaintext highlighter-rouge">sorry</code> closes any goal. <code class="language-plaintext highlighter-rouge">#print axioms</code> on a theorem shows whether
one is hiding underneath it.</li>
  <li><code class="language-plaintext highlighter-rouge">@[extern]</code> and <code class="language-plaintext highlighter-rouge">@[implemented_by]</code> swap a definition for other code
that the proofs never looked at.</li>
  <li>Time and memory are not part of the statement. A proved function can
still be made to run for an hour.</li>
</ul>

<h2 id="what-a-proof-covers">What a proof covers</h2>

<p>A proof establishes its statement, about its definitions, under its
assumptions. Within that it is complete in a way no test is, and I would
take one over a thousand green runs. Outside that it says nothing.</p>

<p>So when someone tells me a piece of code is formally verified, I ask
four things:</p>

<ol>
  <li>What is the statement, in one sentence? Would a wrong
implementation also satisfy it?</li>
  <li>What is it a statement about: the code that runs, or a model of it?
If a model, what ties the two together?</li>
  <li>What does it assume? Which axioms, which unproved parts, which
compiler and runtime?</li>
  <li>What is outside it? I/O, resource use, timing, everything upstream
of the function.</li>
</ol>

<p>None of this argues for skipping proofs. It argues for spending them
where the answers are good: small, pure pieces with a statement short
enough to check by eye, such as an offset calculation, a parser for a
fixed format, or a state transition. There the proved definition also
gives every implementation the same oracle to test against. The tests,
the properties and the fuzzer keep doing what a proof does not: running
the real code on real inputs.</p>

<p>A later post in this series will apply all three to a real bug.</p>]]></content><author><name>Nikos Baxevanis</name><email>nikos.baxevanis@gmail.com</email></author><summary type="html"><![CDATA[Part 3 of What a Proof Covers. The proof from the previous post also goes through for a function that reverses nothing. What a proof covers is its statement, about its definitions, under its assumptions.]]></summary></entry><entry><title type="html">Every List</title><link href="https://blog.nikosbaxevanis.com/2026/09/10/every-list/" rel="alternate" type="text/html" title="Every List" /><published>2026-09-10T00:00:00+00:00</published><updated>2026-09-10T00:00:00+00:00</updated><id>https://blog.nikosbaxevanis.com/2026/09/10/every-list</id><content type="html" xml:base="https://blog.nikosbaxevanis.com/2026/09/10/every-list/"><![CDATA[<p><em>Part 2 of <a href="/2026/07/10/what-a-proof-covers/">What a Proof Covers</a>.</em></p>

<p>The <a href="/2026/08/10/one-list-a-thousand-lists/">previous post</a> checked <code class="language-plaintext highlighter-rouge">reverse (reverse xs) = xs</code> for
one list and then for a thousand generated ones. This post proves it for
every finite list, in <a href="https://lean-lang.org/">Lean</a>. I assume no Lean; each piece of
notation is explained where it first appears.</p>

<h2 id="a-list-has-two-cases">A list has two cases</h2>

<div class="language-lean highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[]                 <span class="cd">-- Empty.</span>
<span class="n">head</span> :: <span class="n">tail</span><span class="cd">       -- An element followed by a list.</span>

[<span class="mi">10</span>, <span class="mi">20</span>, <span class="mi">30</span>] <span class="o">=</span> <span class="mi">10</span> :: [<span class="mi">20</span>, <span class="mi">30</span>]
             <span class="o">=</span> <span class="mi">10</span> :: <span class="mi">20</span> :: <span class="mi">30</span> :: []
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">head</code> is an element, <code class="language-plaintext highlighter-rouge">tail</code> is another list, and <code class="language-plaintext highlighter-rouge">::</code> builds a list
from the two. There is no third case. Everything below follows from
that: a function on lists has to say what it does with <code class="language-plaintext highlighter-rouge">[]</code> and with
<code class="language-plaintext highlighter-rouge">head :: tail</code>, and a proof about lists has to cover the same two.</p>

<h2 id="reverse-defined-on-those-cases">Reverse, defined on those cases</h2>

<div class="language-lean highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="n">rev</span> <span class="err">{</span>α : <span class="kt">Type</span><span class="err">}</span> : <span class="n">List</span> α <span class="o">→</span> <span class="n">List</span> α
  <span class="o">|</span> []           <span class="o">=&gt;</span> []
  <span class="o">|</span> <span class="n">head</span> :: <span class="n">tail</span> <span class="o">=&gt;</span> <span class="n">rev</span> <span class="n">tail</span> <span class="o">++</span> [<span class="n">head</span>]
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">α</code> is the element type, <code class="language-plaintext highlighter-rouge">++</code> joins two lists, and <code class="language-plaintext highlighter-rouge">[head]</code> is the list
holding only <code class="language-plaintext highlighter-rouge">head</code>. For a non-empty list: reverse the tail, then put
the head at the end. The recursive call gets a shorter list, so it
reaches <code class="language-plaintext highlighter-rouge">[]</code> and stops.</p>

<p>Expanding the definition on a small list shows the mechanism:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>rev [10, 20, 30]
= rev [20, 30] ++ [10]
= (rev [30] ++ [20]) ++ [10]
= ((rev [] ++ [30]) ++ [20]) ++ [10]
= (([] ++ [30]) ++ [20]) ++ [10]
= [30, 20, 10]
</code></pre></div></div>

<h2 id="the-proof-has-the-same-two-cases">The proof has the same two cases</h2>

<div class="language-lean highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">theorem</span> <span class="n">rev_twice</span> <span class="err">{</span>α : <span class="kt">Type</span><span class="err">}</span> (<span class="n">xs</span> : <span class="n">List</span> α) :
    <span class="n">rev</span> (<span class="n">rev</span> <span class="n">xs</span>) <span class="o">=</span> <span class="n">xs</span> := <span class="k">by</span>
  <span class="n">induction</span> <span class="n">xs</span> <span class="k">with</span>
  <span class="o">|</span> <span class="n">nil</span> <span class="o">=&gt;</span> <span class="o">...</span>
  <span class="o">|</span> <span class="n">cons</span> <span class="n">head</span> <span class="n">tail</span> <span class="n">ih</span> <span class="o">=&gt;</span> <span class="o">...</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">nil</code> asks for the claim on <code class="language-plaintext highlighter-rouge">[]</code>. <code class="language-plaintext highlighter-rouge">cons</code> asks for it on <code class="language-plaintext highlighter-rouge">head :: tail</code>
and hands over <code class="language-plaintext highlighter-rouge">ih</code>, the induction hypothesis: <code class="language-plaintext highlighter-rouge">rev (rev tail) = tail</code>.</p>

<p>Being allowed to assume the claim for the tail can look like cheating.
It is not. Establish the rule for the empty list. Then show that
whenever it holds for a tail, it still holds after adding a head. That
covers length 0, then 1, then 2, and so on. It is one argument for all
lengths, not a loop that tries them.</p>

<h2 id="the-empty-case">The empty case</h2>

<div class="language-lean highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="o">|</span> <span class="n">nil</span> <span class="o">=&gt;</span> <span class="n">rfl</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">rev []</code> is <code class="language-plaintext highlighter-rouge">[]</code> by definition, so <code class="language-plaintext highlighter-rouge">rev (rev [])</code> reduces to <code class="language-plaintext highlighter-rouge">[]</code> and
the goal becomes <code class="language-plaintext highlighter-rouge">[] = []</code>. <code class="language-plaintext highlighter-rouge">rfl</code> closes an equality whose two sides
reduce to the same thing.</p>

<h2 id="the-non-empty-case-needs-a-helper">The non-empty case needs a helper</h2>

<p>Unfolding the definition once gives:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>rev (rev (head :: tail))
= rev (rev tail ++ [head])
</code></pre></div></div>

<p>Now <code class="language-plaintext highlighter-rouge">rev</code> is applied to two joined lists, and the definition does not
say what that is. It takes a separate fact: reversing joined lists
reverses each part and swaps their order.</p>

<div class="language-lean highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">theorem</span> <span class="n">rev_append</span> <span class="err">{</span>α : <span class="kt">Type</span><span class="err">}</span> (<span class="n">xs</span> <span class="n">ys</span> : <span class="n">List</span> α) :
    <span class="n">rev</span> (<span class="n">xs</span> <span class="o">++</span> <span class="n">ys</span>) <span class="o">=</span> <span class="n">rev</span> <span class="n">ys</span> <span class="o">++</span> <span class="n">rev</span> <span class="n">xs</span> := <span class="k">by</span>
  <span class="n">induction</span> <span class="n">xs</span> <span class="k">with</span>
  <span class="o">|</span> <span class="n">nil</span> <span class="o">=&gt;</span> <span class="n">simp</span> [<span class="n">rev</span>]
  <span class="o">|</span> <span class="n">cons</span> <span class="n">head</span> <span class="n">tail</span> <span class="n">ih</span> <span class="o">=&gt;</span>
      <span class="n">simp</span> [<span class="n">rev</span>, <span class="n">ih</span>, <span class="n">List</span><span class="o">.</span><span class="n">append_assoc</span>]
</code></pre></div></div>

<p>A proved fact used inside another proof is called a lemma. This one is
again by induction on the two cases. <code class="language-plaintext highlighter-rouge">simp</code> unfolds <code class="language-plaintext highlighter-rouge">rev</code> and rewrites
with the hypothesis for the tail; <code class="language-plaintext highlighter-rouge">List.append_assoc</code> says that the
grouping of joined lists does not matter.</p>

<p>With the lemma, the non-empty case is four steps:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>rev (rev (head :: tail))
= rev (rev tail ++ [head])
= rev [head] ++ rev (rev tail)     -- By rev_append.
= [head] ++ tail                   -- By the definition and ih.
= head :: tail
</code></pre></div></div>

<p>The head returns to the front, and the tail is restored by the
induction hypothesis. At no point was a length or an element value
chosen.</p>

<h2 id="the-complete-proof">The complete proof</h2>

<div class="language-lean highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">theorem</span> <span class="n">rev_twice</span> <span class="err">{</span>α : <span class="kt">Type</span><span class="err">}</span> (<span class="n">xs</span> : <span class="n">List</span> α) :
    <span class="n">rev</span> (<span class="n">rev</span> <span class="n">xs</span>) <span class="o">=</span> <span class="n">xs</span> := <span class="k">by</span>
  <span class="n">induction</span> <span class="n">xs</span> <span class="k">with</span>
  <span class="o">|</span> <span class="n">nil</span> <span class="o">=&gt;</span> <span class="n">rfl</span>
  <span class="o">|</span> <span class="n">cons</span> <span class="n">head</span> <span class="n">tail</span> <span class="n">ih</span> <span class="o">=&gt;</span>
      <span class="k">calc</span>
        <span class="n">rev</span> (<span class="n">rev</span> (<span class="n">head</span> :: <span class="n">tail</span>))
            <span class="o">=</span> <span class="n">rev</span> (<span class="n">rev</span> <span class="n">tail</span> <span class="o">++</span> [<span class="n">head</span>]) := <span class="n">rfl</span>
        <span class="n">_</span> <span class="o">=</span> <span class="n">rev</span> [<span class="n">head</span>] <span class="o">++</span> <span class="n">rev</span> (<span class="n">rev</span> <span class="n">tail</span>) := <span class="n">rev_append</span> <span class="n">_</span> <span class="n">_</span>
        <span class="n">_</span> <span class="o">=</span> [<span class="n">head</span>] <span class="o">++</span> <span class="n">tail</span> := <span class="k">by</span> <span class="n">rw</span> [<span class="n">ih</span>]<span class="o">;</span> <span class="n">rfl</span>
        <span class="n">_</span> <span class="o">=</span> <span class="n">head</span> :: <span class="n">tail</span> := <span class="n">rfl</span>
</code></pre></div></div>

<p>Lean checks every step. If one did not follow, the file would not
compile.</p>

<h2 id="what-was-established">What was established</h2>

<table>
  <thead>
    <tr>
      <th style="text-align: left"> </th>
      <th style="text-align: left">Inputs the rule was checked for</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left">Example test</td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">[10, 20, 30]</code></td>
    </tr>
    <tr>
      <td style="text-align: left">Property tests</td>
      <td style="text-align: left">The lists that were generated</td>
    </tr>
    <tr>
      <td style="text-align: left">Proof</td>
      <td style="text-align: left">Every finite list, of any element type</td>
    </tr>
  </tbody>
</table>

<p>This is the gap the <a href="/2026/08/10/one-list-a-thousand-lists/">previous post</a> ended on, closed. No
generator bounds, no sample count, no luck.</p>

<p>One sentence above carries more weight than it seems to: the proof is
about <code class="language-plaintext highlighter-rouge">rev</code>, the three-line Lean definition. It says that this
definition, composed with itself, is the identity. It does not mention
the C# method or the Go function from the previous post, and it does not
say that <code class="language-plaintext highlighter-rouge">rev</code> reverses anything.</p>

<p><strong>Next:</strong> <a href="/2026/10/10/the-theorem-holds-for-the-wrong-function/">The Theorem Holds for the Wrong Function</a></p>]]></content><author><name>Nikos Baxevanis</name><email>nikos.baxevanis@gmail.com</email></author><summary type="html"><![CDATA[Part 2 of What a Proof Covers. A complete Lean proof, by induction, that reversing a list twice gives back the list, read one step at a time.]]></summary></entry></feed>