<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: InferHaven</title>
    <description>The latest articles on DEV Community by InferHaven (@inferhaven).</description>
    <link>https://dev.to/inferhaven</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3986382%2Fb70b5bca-f72b-45d8-a3f3-9996dd271234.png</url>
      <title>DEV Community: InferHaven</title>
      <link>https://dev.to/inferhaven</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/inferhaven"/>
    <language>en</language>
    <item>
      <title>Building haven bench in the open, and the flaky CI ghost it flushed out</title>
      <dc:creator>InferHaven</dc:creator>
      <pubDate>Wed, 24 Jun 2026 17:19:43 +0000</pubDate>
      <link>https://dev.to/inferhaven/building-haven-bench-in-the-open-and-the-flaky-ci-ghost-it-flushed-out-1426</link>
      <guid>https://dev.to/inferhaven/building-haven-bench-in-the-open-and-the-flaky-ci-ghost-it-flushed-out-1426</guid>
      <description>&lt;p&gt;&lt;em&gt;A debugging story from building InferHaven in the open: how a benchmark feature flushed out a flaky-CI race that had nothing to do with it.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I shipped a tokens/sec benchmark for local models. The unit tests were green, and then CI turned red in a way that looked like my fault. It wasn't. Here's the whole hunt: a chown that raced the tide, set -e, and a zsh lock file that vanished mid-sweep.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I built a small thing this week and it caught a bigger thing. That's the whole post, really. But the shape of how it happened is worth writing down, because it's the kind of story that usually gets quietly squashed into a one-line commit message and never told. Building in the open means showing the part where the harbor light flickers, not just the part where the boat docks clean.&lt;/p&gt;

&lt;p&gt;The small thing is &lt;code&gt;haven bench&lt;/code&gt;, a command that tells you how fast a model actually runs on your hardware. The bigger thing was a flaky CI failure that wore my new feature as a disguise.&lt;/p&gt;

&lt;h2&gt;
  
  
  The small thing: a number you can trust
&lt;/h2&gt;

&lt;p&gt;If you run local models, you live and die by tokens per second. It's the single number everyone in &lt;code&gt;r/LocalLLaMA&lt;/code&gt; trades like baseball cards, and yet most people read it off a vibe: "feels fast on my 3090." I wanted InferHaven to just tell you, honestly, on your own box.&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;haven bench qwen2.5-coder:3b &lt;span class="nt"&gt;--runs&lt;/span&gt; 3

  InferHaven bench — qwen2.5-coder:3b-instruct-q4_K_M
  run 1          106.0 tok/s
  run 2          106.3 tok/s
  run 3          106.0 tok/s
  generation     106.1 tok/s  &amp;lt;- decode rate &lt;span class="o"&gt;(&lt;/span&gt;avg of 3 runs&lt;span class="o"&gt;)&lt;/span&gt;
  prompt &lt;span class="nb"&gt;eval   &lt;/span&gt;3507.8 tok/s  &lt;span class="o"&gt;(&lt;/span&gt;40 prompt tokens&lt;span class="o"&gt;)&lt;/span&gt;
  load            0.17 s     &lt;span class="o"&gt;(&lt;/span&gt;run 1: weights -&amp;gt; VRAM&lt;span class="o"&gt;)&lt;/span&gt;
  total           1.39 s     &lt;span class="o"&gt;(&lt;/span&gt;run 1&lt;span class="o"&gt;)&lt;/span&gt;
  method       &lt;span class="nv"&gt;num_predict&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;128, &lt;span class="nv"&gt;seed&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0, &lt;span class="nv"&gt;temp&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0, &lt;span class="nv"&gt;runs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;p&gt;The headline is &lt;code&gt;generation&lt;/code&gt;, the decode rate. Under the hood Ollama hands back its timings in nanoseconds, and the math is just &lt;code&gt;eval_count / (eval_duration / 1e9)&lt;/code&gt;. The reason that's the honest number and not, say, &lt;code&gt;total&lt;/code&gt; is subtle and important: &lt;code&gt;eval_duration&lt;/code&gt; &lt;em&gt;excludes&lt;/em&gt; model load and prompt ingestion. So it's the pure speed of the model writing tokens, and it holds steady whether the model was cold or already warm in VRAM.&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
The first run pays to haul the weights into VRAM (that &lt;code&gt;load&lt;/code&gt; line). The second run doesn't. If you quote "total time" you're really benchmarking your disk and your luck. Decode rate is the speed of the engine itself, the thing that's actually true about the model on your card. That's the figure worth screenshotting.&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;One more bit of honesty baked into the output: &lt;code&gt;prompt eval&lt;/code&gt; is gloriously noisy on a short prompt (3507 tok/s above, but it bounces between runs), because you're dividing a tiny token count by a tinier duration. So &lt;code&gt;bench&lt;/code&gt; reports it, but quietly. The number it puts in green, the one it wants you to believe, is generation. A benchmark that oversells itself is just a vibe with extra steps.&lt;/p&gt;

&lt;p&gt;I wrote it the slow way on purpose: the core tokens/sec math by hand, with a unit test seeded from a real run off my RTX 3060, so I could actually explain every line of it instead of cargo-culting a one-liner. Seven assertions, all green. Shellcheck clean. Ran it live against three models. Pushed the PR.&lt;/p&gt;

&lt;p&gt;And then CI turned red.&lt;/p&gt;

&lt;h2&gt;
  
  
  The red light
&lt;/h2&gt;

&lt;p&gt;Two smoke jobs run on every PR: a slim &lt;strong&gt;codespaces&lt;/strong&gt; stack and the full &lt;strong&gt;full-stack&lt;/strong&gt; one. Full-stack went green. Codespaces failed, and not in my test. It failed &lt;em&gt;bringing the container up&lt;/em&gt;, before my code ever ran:&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Running the postCreateCommand from devcontainer.json...
Error response from daemon: container 46a4… is not running
postCreateCommand from devcontainer.json failed with &lt;span class="nb"&gt;exit &lt;/span&gt;code 1.
&lt;span class="c"&gt;##[error]Process completed with exit code 1.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;p&gt;This is the moment that decides whether you're a good engineer or a fast one. The tempting move is: &lt;em&gt;it's my PR, it's the only thing that changed&lt;/em&gt; Poke at the test, re-run it, add a &lt;code&gt;sleep&lt;/code&gt;, wrap something in a &lt;code&gt;|| true&lt;/code&gt; and move on. But if you don't get to the root of the issue it will most likely just surface again.&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
No fix without a root cause first. A red test you "fixed" by re-running is just a bug you've agreed to meet again later, usually in front of a stranger.&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;So I did the boring thing instead and actually looked.&lt;/p&gt;

&lt;h2&gt;
  
  
  Following the evidence, not the vibe
&lt;/h2&gt;

&lt;p&gt;The accusation was "your PR broke the build." The evidence disagreed, layer by layer.&lt;/p&gt;

&lt;p&gt;What it looked like -- My PR broke CI&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-Only my branch changed
-Red appeared right after I pushed
-It's the new feature, obviously
-Just re-run it / patch the test
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;vs&lt;/p&gt;

&lt;p&gt;What the evidence said -- My PR was a bystander&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-My diff touched zero boot-path files
-Full-stack booted the SAME image fine
-The failure was in container startup, before my code ran
-main had failed this exact way before, intermittently
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Three facts did the work. First, my diff touched a CLI command, a library function, a test, and some docs. Nothing in the container's startup path. Second, the &lt;em&gt;full-stack&lt;/em&gt; job built the very same workspace image and came up clean; if my scripts could kill a boot, both jobs would die, not one. Third, the failure happened during &lt;code&gt;up&lt;/code&gt;, before the step that runs my new code ever executed.&lt;/p&gt;

&lt;p&gt;That's not a guilty feature. That's a flaky boot that happened to be standing next to me when the cops showed up.&lt;/p&gt;

&lt;p&gt;The good news is I'd wired the CI to dump the dying container's logs on failure, and the container's last words were the whole case:&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;workspace-1 | &lt;span class="nb"&gt;chown&lt;/span&gt;: cannot access &lt;span class="s1"&gt;'/home/haven/.zcompdump-46a4482c870c-5.9.lock'&lt;/span&gt;:
              No such file or directory
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;p&gt;There it is. &lt;code&gt;46a4482c870c&lt;/code&gt; is the container's own ID. &lt;code&gt;.zcompdump-…-5.9.lock&lt;/code&gt; is a zsh completion lock file, created and deleted in milliseconds while the shell builds its completion cache. And the thing that tripped over it was my entrypoint's first-boot ownership sweep: a recursive &lt;code&gt;chown -R&lt;/code&gt; over the home directory.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug: a chown that raced the tide
&lt;/h2&gt;

&lt;p&gt;Here's the entire bug, and it's a beauty because there's almost nothing to it:&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt;
&lt;span class="c"&gt;# ...&lt;/span&gt;
&lt;span class="nb"&gt;chown&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;HAVEN_USER&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;HAVEN_USER&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;HOME_DIR&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;p&gt;&lt;code&gt;chown -R&lt;/code&gt; walks the tree, builds a list of things to change, then changes them. If a transient file (say, a zsh completion lock) exists when the walk lists it but is &lt;em&gt;gone&lt;/em&gt; by the time &lt;code&gt;chown&lt;/code&gt; reaches it, &lt;code&gt;chown&lt;/code&gt; exits non-zero. And &lt;code&gt;set -e&lt;/code&gt; says: any command that fails, abort the script. So the entrypoint dies. So the container exits. So forty seconds later, when the devcontainer tries to run its setup step, the daemon shrugs and says &lt;em&gt;that container is not running.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It's a time-of-check versus time-of-use race, and like all races it only loses sometimes, which is exactly why &lt;code&gt;main&lt;/code&gt; was usually green and only occasionally, mysteriously, wasn't. My PR didn't cause it. My PR just rolled the dice enough times to hit it.&lt;/p&gt;

&lt;p&gt;The fix is the same guard the &lt;em&gt;other&lt;/em&gt; recursive chowns in that file already had. I'd just missed these two:&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;- chown -R "${HAVEN_USER}:${HAVEN_USER}" "${HOME_DIR}"
&lt;/span&gt;&lt;span class="gi"&gt;+ chown -R "${HAVEN_USER}:${HAVEN_USER}" "${HOME_DIR}" 2&amp;gt;/dev/null || true
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;p&gt;&lt;code&gt;|| true&lt;/code&gt; tells &lt;code&gt;set -e&lt;/code&gt; to let this one slide, and a vanished lock file stops being a death sentence for the whole container. I proved the mechanism in isolation first (a deliberately failing &lt;code&gt;chown -R&lt;/code&gt; under &lt;code&gt;set -e&lt;/code&gt; halts the script; the guarded version sails right past it), then shipped it as its own small PR, separate from the benchmark, so the history reads honestly: here's a feature, and here's an unrelated bug the feature flushed out.&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
The instinct that &lt;code&gt;|| true&lt;/code&gt; is "hiding errors" is usually correct. But here the error &lt;em&gt;is&lt;/em&gt; the bug. A recursive chown racing a temp file is a known, benign class of failure, and the surrounding chowns in this same script already tolerate it. The band-aid would've been re-running CI until it was green. Tolerating a known race at the exact line it occurs is the actual fix.&lt;br&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually want you to take from this
&lt;/h2&gt;

&lt;p&gt;The benchmark is nice. Go run &lt;code&gt;haven bench&lt;/code&gt; on your own card and post the number. Honest decode rates are good for everyone, and the more of them in the wild the less anyone has to guess.&lt;/p&gt;

&lt;p&gt;But the part I think is worth more than the feature is the shape of the hunt:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A green unit test is not a green build.&lt;/strong&gt; My math was perfect. The bug was three layers away from my math, in startup code I didn't touch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make your failures talk.&lt;/strong&gt; That "dump logs on failure" step cost me five minutes to write months ago and handed me the entire diagnosis in one line. Future-you is a stranger; leave them evidence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resist the re-run.&lt;/strong&gt; The single most expensive habit in software is treating a flaky test as noise. Flaky almost always means &lt;em&gt;real bug, intermittent trigger.&lt;/em&gt; This one had been quietly failing for weeks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fix the root, label it honestly.&lt;/strong&gt; The race got its own PR with its own explanation. Nobody reading the history six months from now has to wonder why a chown grew a &lt;code&gt;|| true&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's the whole reason I'm building this in the open. Not because the wins make good screenshots, but because the &lt;em&gt;misses&lt;/em&gt; are where the actual craft lives, and most of the industry hides them.&lt;/p&gt;

&lt;p&gt;Both PRs are merged. The light's back to steady.&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;git clone https://github.com/InferHaven/inferhaven-core
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;inferhaven-core
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env
&lt;span class="nv"&gt;$ &lt;/span&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;ssh haven@localhost
&lt;span class="nv"&gt;$ &lt;/span&gt;haven bench    &lt;span class="c"&gt;# tell me what your card does&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;p&gt;Float your boat up to the dock, &lt;a href="https://github.com/InferHaven/inferhaven-core" rel="noopener noreferrer"&gt;clone the repo&lt;/a&gt;, run the benchmark, and if you want the managed version when it's ready, the waitlist on the homepage is the way in. The lighthouse is on. And now it doesn't flicker.&lt;/p&gt;

&lt;p&gt;— Ethan L.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>testing</category>
      <category>bash</category>
      <category>buildinpublic</category>
    </item>
  </channel>
</rss>
