<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
    <title>viiwork: LLM inference across your own GPU machines - Blog</title>
    <subtitle>viiwork runs LLM inference across a fleet of your own machines. One node per machine serves its models behind an OpenAI-compatible API, and the nodes form a mesh that sends each request to a free slot.</subtitle>
    <link rel="self" type="application/atom+xml" href="https://viiwork.pulu.app/blog/atom.xml"/>
    <link rel="alternate" type="text/html" href="https://viiwork.pulu.app/blog/"/>
    <generator uri="https://www.getzola.org/">Zola</generator>
    <updated>2026-10-27T00:00:00+00:00</updated>
    <id>https://viiwork.pulu.app/blog/atom.xml</id>
    <entry xml:lang="en">
        <title>Two RTX 5090 hosts: how much does a faster link help?</title>
        <published>2026-10-27T00:00:00+00:00</published>
        <updated>2026-10-27T00:00:00+00:00</updated>
        
        <author>
          <name>Jani Tarvainen</name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://viiwork.pulu.app/blog/kv-share-two-5090-hosts/"/>
        <id>https://viiwork.pulu.app/blog/kv-share-two-5090-hosts/</id>
        
        <content type="html" xml:base="https://viiwork.pulu.app/blog/kv-share-two-5090-hosts/">&lt;p&gt;When a long-running agent&#39;s next turn lands on the other GPU host, that host needs the conversation state to avoid reading the whole prompt again. viiwork&#39;s &lt;code&gt;kv_share&lt;/code&gt; copies a replica to its peer so a host switch can reuse most of the work already done.&lt;/p&gt;
&lt;p&gt;I ran the same coding-agent workload on two RTX 5090 machines while changing the connection between them: 1 GbE through a LAN switch, a direct 10 GbE cable carrying Tailscale traffic, and that same cable with &lt;code&gt;kv_share&lt;/code&gt; sent directly. Each host had four model slots. The agents built a todo app from a spec, and I measured both 12-session and 16-session runs over 90 minutes.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What is session affinity?&lt;/strong&gt; It is the preference to send successive turns of one conversation to the same host, where its prompt cache is likely still warm. When that host is busy, the request may move; &lt;code&gt;kv_share&lt;/code&gt; makes that move less costly. The earlier &lt;a href=&quot;https://viiwork.pulu.app/blog/session-affinity-coding-agents/&quot;&gt;session affinity study&lt;/a&gt; shows why keeping a session near its cache matters in the first place.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;the-cable-shortened-the-copy&quot;&gt;The cable shortened the copy&lt;/h2&gt;
&lt;p&gt;The chart uses the 16-session runs, where the load stayed busy and the phases are most comparable. A typical replica delta was about 247 MiB. At 1 GbE it took 3.24 seconds to send. Over the 10 GbE cable through Tailscale, it took 0.81 seconds. Bypassing Tailscale cut the send to 0.55 seconds.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://viiwork.pulu.app/blog/kv-share-two-5090-hosts/assets/kv-share-send-time.svg&quot; alt=&quot;Horizontal bar chart of median replica send time: 3.24 seconds at 1 GbE, 0.81 seconds on 10 GbE through Tailscale, and 0.55 seconds on direct 10 GbE.&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The raw cable carried 9.41 Gb/s in a single-stream test; Tailscale carried about 3.0–3.4 Gb/s. That explains why the first jump was roughly fourfold rather than tenfold. The direct socket was quicker again, but by then reading, hashing, and writing the replica at the receiver appeared to set the pace.&lt;/p&gt;
&lt;h2 id=&quot;did-turns-get-warmer&quot;&gt;Did turns get warmer?&lt;/h2&gt;
&lt;p&gt;Yes, modestly. In the 16-session comparison, warm host switches rose from &lt;strong&gt;93.8% at direct 1 GbE&lt;/strong&gt; to &lt;strong&gt;96.8% over 10 GbE with Tailscale&lt;/strong&gt;. Switches that reused some, but not enough, of the prompt fell from 5.4% to 2.2%. A faster copy was more likely to reach the other host before the next turn arrived.&lt;/p&gt;
&lt;p&gt;The direct 10 GbE run sent replicas faster still, but its warm share was 96.2%. That difference from 96.8% is within the variation between these runs. At this load, removing Tailscale improved transfer time without a clear end-to-end cache benefit. Forcing the direct cable down to 1 GbE also reproduced the switched 1 GbE send time, so the LAN switch itself was not the bottleneck.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://viiwork.pulu.app/blog/kv-share-two-5090-hosts/assets/kv-share-transfer-and-import.svg&quot; alt=&quot;Diagram showing a session switching from one GPU host to another: replica transfer shrinks with a faster link, while the receiving engine&amp;#39;s import remains about 2.8 seconds.&quot; /&gt;&lt;/p&gt;
&lt;h2 id=&quot;what-remained-slow&quot;&gt;What remained slow&lt;/h2&gt;
&lt;p&gt;An import into the receiving engine still took about &lt;strong&gt;2.8 seconds&lt;/strong&gt; at every link speed. Cold switches stayed around 1–2% in the busier runs. Traces showed a recurring software problem: viiwork remembered that it had imported a replica, but the engine could later evict that conversation. On a return visit, viiwork skipped the import even though the engine no longer held the cache. That &lt;em&gt;import-once&lt;/em&gt; bug was fixed in viiwork v2.11.1 by asking the engine what it still holds before skipping an import.&lt;/p&gt;
&lt;p&gt;For these two hosts and this workload, &lt;strong&gt;1 GbE was workable&lt;/strong&gt;: average &lt;code&gt;kv_share&lt;/code&gt; traffic was roughly 320–450 Mb/s in each direction, and most switches were warm. A 10 GbE link made transfers more timely and reduced partly warm turns. The larger remaining gains were in engine import and cache correctness, rather than more network bandwidth.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Method note: these are single 90-minute runs per configuration, not repeated statistical trials. The 12-session runs were less evenly loaded, so the comparison above uses the 16-session J, H, and L phases. A “warm” switch reused at least 90% of its prompt; “partly warm” reused 30–90%. The measurements were made on 26 October 2026 with viiwork v2.11.0-beta1 and Strata 0.2.6, before the import-once fix.&lt;/em&gt;&lt;/p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Why coding agents need session affinity</title>
        <published>2026-10-03T00:00:00+00:00</published>
        <updated>2026-10-03T00:00:00+00:00</updated>
        
        <author>
          <name>Jani Tarvainen</name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://viiwork.pulu.app/blog/session-affinity-coding-agents/"/>
        <id>https://viiwork.pulu.app/blog/session-affinity-coding-agents/</id>
        
        <content type="html" xml:base="https://viiwork.pulu.app/blog/session-affinity-coding-agents/">&lt;p&gt;A coding agent rarely sends a fresh prompt. Each turn carries the conversation so far. If it returns to a GPU process that still holds that prefix, the model can reuse it. If it lands somewhere else, the model may have to read tens of thousands of tokens again before producing its first new token.&lt;/p&gt;
&lt;p&gt;That is the reason for &lt;strong&gt;session affinity&lt;/strong&gt;: successive turns of one agent session should prefer the same place, while still being able to move when capacity runs out. In September 2026 I measured the cost of moving during an overnight swarm of up to eight agents building the same todo app from a 437-line spec. The model was Qwen3.8-27B on one A4000-pair host and three Radeon VII-pair hosts. The study was the baseline for viiwork v2.7.0&#39;s affinity work.&lt;/p&gt;
&lt;h2 id=&quot;a-miss-could-cost-minutes&quot;&gt;A miss could cost minutes&lt;/h2&gt;
&lt;p&gt;On a Radeon VII pair, a cold 30,000-token prefill ran at about 200 tokens per second. That meant roughly &lt;strong&gt;2.5 minutes before the first token&lt;/strong&gt;, compared with about &lt;strong&gt;1.3 seconds&lt;/strong&gt; when the turn stayed on a warm cache. In the default-routing window, 38% of turns changed node; moved turns had a median time to first token of 111–128 seconds.&lt;/p&gt;
&lt;p&gt;The graph shows the share of turns that reused at least 80% of the previous context, excluding compaction turns. With two sessions on two slots, every turn hit. With eight sessions entering through one node and routing freely across 34 slots, the hit rate fell to 72.9%. Spreading entry points or pinning each run to a node improved the picture, though neither solved movement between GPU processes within a node.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://viiwork.pulu.app/blog/session-affinity-coding-agents/assets/affinity-hit-rates.svg&quot; alt=&quot;Bar chart of prompt-cache hit rates: 100% for two sessions on two slots, 72.9% with default routing for eight sessions, 88.9% with sessions spread across entry nodes, and 74.7% with per-run node pinning.&quot; /&gt;&lt;/p&gt;
&lt;h2 id=&quot;a-host-is-not-the-whole-cache&quot;&gt;A host is not the whole cache&lt;/h2&gt;
&lt;p&gt;In the default-routing run, the entry node repeatedly chose among peers with similar free capacity. Of 189 turns executed on the Radeon VII hosts, 129 changed &lt;strong&gt;node&lt;/strong&gt;. Even after sessions were distributed so they stopped crossing nodes, the receiver&#39;s round-robin choice still sent most turns to another &lt;strong&gt;backend&lt;/strong&gt; on the same node.&lt;/p&gt;
&lt;p&gt;That local move was cheaper because a backend could retain an older prefix in its RAM cache. In the spread-entry run, moving between backends cost a median 26 seconds to first token, versus roughly 120 seconds for a cross-node move. Good affinity therefore has two levels: prefer the same node, then the same backend, while allowing a soft fallback when either is full.&lt;/p&gt;
&lt;p&gt;There is a second constraint: a backend can evict a conversation even if routing sends the next turn back to it. On one A4000 backend, raising llama.cpp&#39;s RAM prompt cache from 8 GiB to 64 GiB removed the measured eviction misses for three sessions, at the cost of much more host memory. Placement and cache size have to work together.&lt;/p&gt;
&lt;h2 id=&quot;the-key-has-to-identify-the-session&quot;&gt;The key has to identify the session&lt;/h2&gt;
&lt;p&gt;An agent can send &lt;code&gt;X-Session-Affinity&lt;/code&gt; or &lt;code&gt;X-Session-Id&lt;/code&gt; with a stable value for its run. That gives the router a dependable key. Hashing only the system prompt and first user message would have failed in this swarm: every agent began with the same two messages, so all eight would have appeared to be one session.&lt;/p&gt;
&lt;p&gt;Affinity also has a natural limit. When the agent compacts its history, it rewrites the prompt; the old cached prefix no longer matches. Those turns were separated from the hit-rate chart because no placement rule can recover a prefix the client has replaced.&lt;/p&gt;
&lt;p&gt;The practical rule is simple: &lt;strong&gt;keep a session close to the cache that served it last, but treat affinity as a preference, not a hard pin.&lt;/strong&gt; The later &lt;a href=&quot;https://viiwork.pulu.app/blog/kv-share-two-5090-hosts/&quot;&gt;two-host &lt;code&gt;kv_share&lt;/code&gt; experiment&lt;/a&gt; asks the next question: when a session must move, how much does a faster link help move its cache with it?&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Method note: the hit threshold was 80% of the previous turn&#39;s context. The windows ran in sequence, with one model, one agent, and one task; context sizes and run mix changed over the night. The chart is a comparison of observed windows, not a controlled estimate of an affinity feature&#39;s isolated effect. The measurements are from 28–29 September 2026.&lt;/em&gt;&lt;/p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>How the viiwork mesh routes a request</title>
        <published>2026-09-15T00:00:00+00:00</published>
        <updated>2026-09-15T00:00:00+00:00</updated>
        
        <author>
          <name>Jani Tarvainen</name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://viiwork.pulu.app/blog/how-the-mesh-routes-requests/"/>
        <id>https://viiwork.pulu.app/blog/how-the-mesh-routes-requests/</id>
        
        <content type="html" xml:base="https://viiwork.pulu.app/blog/how-the-mesh-routes-requests/">&lt;p&gt;A viiwork node serves the models configured on its own machine and keeps track of what every other node serves. That is what lets a client use one OpenAI-compatible endpoint for the whole fleet, without a list of machines or backends of its own. This post follows a request through the mesh, using the rules in the &lt;a href=&quot;https://viiwork.pulu.app/docs/mesh/&quot;&gt;mesh documentation&lt;/a&gt; for viiwork v2.11.0.&lt;/p&gt;
&lt;h2 id=&quot;nodes-find-each-other&quot;&gt;Nodes find each other&lt;/h2&gt;
&lt;p&gt;There are no peer lists. On a Tailscale tailnet, which is the default, a node asks the local tailscaled for the other online machines and contacts them. On a plain LAN it finds them with mDNS. Where neither reaches, a few seed addresses in the config are enough. Discovery repeats every minute, which is also how two halves of a split network merge again.&lt;/p&gt;
&lt;p&gt;Every machine uses two fixed ports: 8086/tcp for the API and the dashboards, and 7946 over both tcp and udp for membership gossip. In a secured mesh every machine has the same &lt;code&gt;VIIWORK_MESH_SECRET&lt;/code&gt;. Gossip is then encrypted and authenticated, a node without the secret cannot join, and requests forwarded between nodes are signed. An open mesh has no secret and has to be declared with &lt;code&gt;mesh.open: true&lt;/code&gt;; a node that has neither refuses to start.&lt;/p&gt;
&lt;h2 id=&quot;the-entry-node-looks-at-capacity&quot;&gt;The entry node looks at capacity&lt;/h2&gt;
&lt;p&gt;Every node is an entry point. Membership travels by gossip, but capacity is polled: each node asks every live member for its &lt;code&gt;/v1/capacity&lt;/code&gt; once a second. When a request arrives, the node that received it looks for hosts that serve the requested model and have a free slot. A report older than three seconds is not used for routing, so a machine that has just gone quiet stops getting work before it leaves the membership list.&lt;/p&gt;
&lt;p&gt;With performance routing, which is on by default, each node measures its own time to first token for each model and publishes the result. For a request, every host with a free slot gets a predicted time from its score and the size of the prompt, and only hosts within 1.25 times the best prediction are considered. If the entry node is one of them, it runs the request itself. Otherwise faster hosts are picked more often. A host with no score yet is priced at the fleet&#39;s median and gets one request in twenty, so it can earn a score of its own. When no host has a score, the request goes to a free local backend, else to the member with the most free slots.&lt;/p&gt;
&lt;h2 id=&quot;a-forward-happens-once&quot;&gt;A forward happens once&lt;/h2&gt;
&lt;p&gt;When the request goes to another machine, the receiving node admits it only into a free local slot. If it has none, it answers 429 at once. It never queues a forwarded request and never forwards it again, so a request cannot bounce around the mesh. A forward that is refused before its first byte (a 429, a 503 or no connection) is retried on another route, and that member is treated as full for the model until a newer capacity report arrives.&lt;/p&gt;
&lt;h2 id=&quot;when-every-slot-is-busy&quot;&gt;When every slot is busy&lt;/h2&gt;
&lt;p&gt;The entry node puts the request in a first-in, first-out queue for that model and waits up to 20 seconds (&lt;code&gt;routing.queue_timeout&lt;/code&gt;) for a slot anywhere in the mesh. If none opens, the client gets HTTP 429 with &lt;code&gt;Retry-After: 2&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&quot;steering-a-request&quot;&gt;Steering a request&lt;/h2&gt;
&lt;p&gt;Most clients can let the mesh choose. A coding agent should send &lt;code&gt;X-Session-Affinity&lt;/code&gt; or &lt;code&gt;X-Session-Id&lt;/code&gt; with a value that stays the same for the session: its turns then go to the same host, whichever node they enter by, and reuse that host&#39;s prompt cache. &lt;code&gt;?host=&amp;lt;node name&amp;gt;&lt;/code&gt; pins a request to one machine, and &lt;code&gt;?prefer=a,b&lt;/code&gt; names machines to try first when they have room; if none of them does, the request routes as if the list were not there. Both names are only compared with member names and never dialled as addresses.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://viiwork.pulu.app/docs/mesh/&quot;&gt;mesh page&lt;/a&gt; has the remaining details, including aliases, the mesh-wide model names that clients can use instead of real model names.&lt;/p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Bringing a new machine into a viiwork mesh</title>
        <published>2026-08-06T00:00:00+00:00</published>
        <updated>2026-08-06T00:00:00+00:00</updated>
        
        <author>
          <name>Jani Tarvainen</name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://viiwork.pulu.app/blog/bringing-a-node-online/"/>
        <id>https://viiwork.pulu.app/blog/bringing-a-node-online/</id>
        
        <content type="html" xml:base="https://viiwork.pulu.app/blog/bringing-a-node-online/">&lt;p&gt;A machine becomes a viiwork node through &lt;code&gt;viiwork init&lt;/code&gt;, the setup wizard. It finds the hardware and the model files, proposes a layout, and shows every file it will write before it changes anything. This post walks through a run of the wizard in viiwork v2.11.0; the &lt;a href=&quot;https://viiwork.pulu.app/install/&quot;&gt;install guide&lt;/a&gt; has the commands.&lt;/p&gt;
&lt;h2 id=&quot;what-the-machine-needs&quot;&gt;What the machine needs&lt;/h2&gt;
&lt;p&gt;The wizard supports three kinds of machine: Linux with Radeon VII, MI50 or MI60 cards (gfx906), Linux with NVIDIA cards, and Apple Silicon Macs. Other AMD cards are found too, but no image is verified for them yet, so on those the wizard &lt;a href=&quot;https://viiwork.pulu.app/docs/setup/#when-it-writes-the-config-only&quot;&gt;writes the config only&lt;/a&gt;. On Linux the engine runs in Docker; on a Mac it runs natively under a LaunchAgent. Every machine needs GGUF model files in one directory, because the wizard downloads nothing, and ports 8086 and 7946 open between the machines of the mesh.&lt;/p&gt;
&lt;h2 id=&quot;the-wizard-proposes-a-layout&quot;&gt;The wizard proposes a layout&lt;/h2&gt;
&lt;p&gt;After its preflight checks, the wizard lists the GPUs and asks for your models directory. It shows each model it finds with its architecture and size, and asks which ones to serve and under what name. Use the same name on every machine, so a request for it can run on any of them.&lt;/p&gt;
&lt;p&gt;It then proposes which cards each model gets, the context per slot and the number of slots. You can accept the proposal, edit one model&#39;s line or drop a model, and a layout the node would refuse is shown together with the reason. On a Mac the plan is made against the memory that Metal can use, since the GPU shares it with everything else on the machine.&lt;/p&gt;
&lt;h2 id=&quot;joining-or-starting-a-mesh&quot;&gt;Joining or starting a mesh&lt;/h2&gt;
&lt;p&gt;Next the wizard looks for nodes nearby, on your tailnet and on the LAN, and offers three choices: join an existing mesh with a join code, start a new secured mesh, or start an open mesh without a secret. A join code carries the address of a node already in the mesh and the mesh secret, so the new machine must be able to reach that node, and the code has to be kept as private as the secret itself.&lt;/p&gt;
&lt;p&gt;Before writing, the wizard shows every file with its content and the secret masked. On a yes it writes them, starts the node (on NVIDIA and on a Mac) and waits until every model has loaded, then runs its acceptance checks. On a gfx906 machine it stops after writing the configuration, because there is no published image for those cards; you build the image from the same release and start it with the example compose file.&lt;/p&gt;
&lt;h2 id=&quot;checking-that-it-serves&quot;&gt;Checking that it serves&lt;/h2&gt;
&lt;p&gt;Once the node runs, ask it what the mesh serves:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;sh&quot;&gt;curl http://localhost:8086/v1/models
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The dashboard on port 8086 shows the node, &lt;code&gt;/mesh&lt;/code&gt; shows the whole mesh, and &lt;code&gt;viiwork top&lt;/code&gt; shows the same in a terminal. When another machine joins, its models appear in that list on every node.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://viiwork.pulu.app/docs/setup/&quot;&gt;setup page&lt;/a&gt; lists every file the wizard writes and covers updating and uninstalling.&lt;/p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Where viiwork came from</title>
        <published>2026-07-16T00:00:00+00:00</published>
        <updated>2026-07-16T00:00:00+00:00</updated>
        
        <author>
          <name>Jani Tarvainen</name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://viiwork.pulu.app/blog/where-viiwork-came-from/"/>
        <id>https://viiwork.pulu.app/blog/where-viiwork-came-from/</id>
        
        <content type="html" xml:base="https://viiwork.pulu.app/blog/where-viiwork-came-from/">&lt;p&gt;viiwork did not start as a project. It started as a problem I kept having at my desk.&lt;/p&gt;
&lt;h2 id=&quot;a-pile-of-cheap-vram&quot;&gt;A pile of cheap VRAM&lt;/h2&gt;
&lt;p&gt;The hardware came first. gfx906 cards — Radeon VII, MI50, MI60 — had fallen off the back of the datacenter and onto the secondhand market, and for the price of one current GPU you could buy a stack of them with more VRAM between them than anything I could otherwise afford. They are not fast by today&#39;s standards, but a model either fits in VRAM or it does not, and these fit. I put them in whatever machines I had, pointed llama.cpp at a directory of GGUF files, and started serving models on the local network.&lt;/p&gt;
&lt;p&gt;That worked for exactly one machine.&lt;/p&gt;
&lt;h2 id=&quot;viiwork-1-and-the-wall-it-built&quot;&gt;viiwork 1, and the wall it built&lt;/h2&gt;
&lt;p&gt;The first version of viiwork was the thin layer I wrapped around that: a process that launched a model, gave it a port, and answered an OpenAI-shaped API. One model, one process, one port. When I wanted a second model I started a second process on a second port. When I bought a second machine I did it all again over there.&lt;/p&gt;
&lt;p&gt;The trouble was never one machine. It was the seams between them.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Every model was its own instance file, with its own port, and the ports followed per-family schemes I had to keep straight in my head.&lt;/li&gt;
&lt;li&gt;A model&#39;s id came from its GGUF filename, so renaming a file quietly renamed the model.&lt;/li&gt;
&lt;li&gt;And every machine carried a hand-written &lt;code&gt;peers.hosts&lt;/code&gt; list naming every other machine. Add a node and you edited that list &lt;em&gt;everywhere&lt;/em&gt; — on the new box and on all the old ones — and if one copy drifted, requests fell into a hole.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;By the time the fleet was three machines — &lt;code&gt;gb1&lt;/code&gt;, &lt;code&gt;forge-white&lt;/code&gt;, &lt;code&gt;yeti&lt;/code&gt; — bringing a fourth online was an afternoon of editing config on four hosts to tell them all about each other. I was maintaining a map of the network by hand, on every machine, in triplicate. The inference worked. The bookkeeping was the whole job.&lt;/p&gt;
&lt;h2 id=&quot;the-idea-that-became-version-2&quot;&gt;The idea that became version 2&lt;/h2&gt;
&lt;p&gt;The rewrite came from one stubborn thought: the machines are already on a network that knows which of them are up. Why am I telling each one what the others are?&lt;/p&gt;
&lt;p&gt;Everything the machines were already on — the tailnet, the LAN — could answer &quot;who else is here&quot; without my help. So viiwork 2 threw out the peer lists and let the mesh form itself:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;One node per machine.&lt;/strong&gt; A single process supervises every model on the box, instead of one process per model. Its &lt;code&gt;models:&lt;/code&gt; list replaces the pile of instance files.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Two fixed ports, the same everywhere.&lt;/strong&gt; The API and dashboards on 8086, membership gossip on 7946. No more port schemes to remember.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A mesh with no roster.&lt;/strong&gt; A node advertises itself and finds the others through tailscaled, or through mDNS on a LAN. Adding a machine means writing that one machine&#39;s config and nothing anywhere else. A machine that stops is dropped from routing within seconds.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Explicit model names.&lt;/strong&gt; A node serves exactly the name you give a model, and the same name on every machine means a request for it can run on any of them.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Routing that knows about slots.&lt;/strong&gt; A request goes to a local backend with a free slot, or to the member with the most free, or waits briefly in a queue — instead of landing on whichever host a peer list happened to name first.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The config that used to live in four places collapsed into one file per machine that only describes that machine. The map of the network stopped being something I maintained and became something the mesh worked out for itself.&lt;/p&gt;
&lt;p&gt;That change is the whole reason there is a v1-to-v2 migration guide: v2 refuses a v1 &lt;code&gt;viiwork.yaml&lt;/code&gt; at startup, because almost every assumption underneath it had moved. If you are coming across from that era, &lt;a href=&quot;https://viiwork.pulu.app/docs/migrating-to-v2/&quot;&gt;migrating to v2&lt;/a&gt; is the map.&lt;/p&gt;
&lt;h2 id=&quot;where-it-is-now&quot;&gt;Where it is now&lt;/h2&gt;
&lt;p&gt;Everything since has been built on that spine. Aliases let a stable name like &lt;code&gt;stable-coder&lt;/code&gt; point at a real model and be switched mesh-wide from any machine. kv_share carries a session&#39;s warm state to the next host so a busy machine no longer means a cold read. Releases are reproducible and signed, so a node will only update itself to a build the publisher actually stood behind. The reference fleet is still those gfx906 cards, now joined by NVIDIA boxes and a couple of Apple Silicon Macs that run the engine natively.&lt;/p&gt;
&lt;p&gt;But the shape of it has not changed since the rewrite. Switch a machine on, and within seconds its models are usable from every other node, with nothing edited anywhere else. That is the thing I wanted at my desk, back when I was keeping the network map in my head and losing. If you want to see it from the other end, &lt;a href=&quot;https://viiwork.pulu.app/blog/bringing-a-node-online/&quot;&gt;bringing a new machine into a mesh&lt;/a&gt; is a single machine&#39;s first &lt;code&gt;viiwork init&lt;/code&gt;.&lt;/p&gt;
</content>
        
    </entry>
</feed>
