viiwork init turns a machine with GPUs and some GGUF model files into a running node. It asks a few questions and shows every file it will write. It writes nothing until you say yes, then starts the node and checks it. Running viiwork on a terminal with no config file starts the same wizard.

On Linux the engine runs in Docker. On an Apple Silicon Mac it runs natively under a user LaunchAgent: see On a Mac.

Before you start

Linux, AMD Radeon VII, MI50 or MI60 (gfx906) — the reference fleet:

  • The in-kernel amdgpu driver (/dev/kfd exists). No ROCm on the host: the wizard reads the cards from the kernel's KFD topology, and ROCm 6.2.4 is in the image.
  • Docker Engine with the Compose v2 plugin.
  • A checkout of the release to build the ROCm image from: gfx906 has no published image, so the wizard writes the config and you start the node; see When it writes the config only.

Linux, NVIDIA:

  • An NVIDIA GPU with its driver (nvidia-smi works).
  • Docker Engine with the Compose v2 plugin (docker compose version works).
  • The NVIDIA Container Toolkit. With Docker 25 or newer, its CDI spec is enough (sudo nvidia-ctk cdi generate --output=/etc/cdi/nvidia.yaml). Otherwise, register its runtime with sudo nvidia-ctk runtime configure --runtime=docker and restart Docker.

Other AMD cards are found too, but no image is verified for them yet; see When it writes the config only. A machine with no supported GPU is refused.

Mac: Apple Silicon. Run the wizard from Terminal on the Mac itself, while logged in: the node runs as a LaunchAgent in your login session, so an SSH session with nobody logged in is refused.

Both:

  • GGUF model files in one directory. The wizard downloads nothing. For example, hf download <repo> <file>.gguf --local-dir ~/models, or the data_dir of a viiwork-parrot node, which the wizard suggests by itself.
  • Ports 8086/tcp (the API and dashboards) and 7946/tcp+udp (the mesh) free on this machine, and open between the machines of the mesh.
  • For more than one machine: every machine on the same Tailscale tailnet (the default when tailscaled runs), or on the same LAN.

Download and verify

Take the newest release from the releases. Check the download against SHA256SUMS; releases.md shows how to verify its signature too.

v=vX.Y.Z                     # the newest release
os=linux_amd64               # or linux_arm64, darwin_arm64
base=https://github.com/janit/viiwork/releases/download/$v
curl -fLO "$base/viiwork_${v}_${os}.tar.gz" -fLO "$base/SHA256SUMS"
sha256sum --check --ignore-missing SHA256SUMS
# on a Mac instead: shasum -a 256 --check --ignore-missing SHA256SUMS
tar xzf "viiwork_${v}_${os}.tar.gz" && cd "viiwork_${v}_${os}"

sudo ./viiwork init          # on a Mac: ./viiwork init (no sudo)

What it asks

  1. Preflight. On Linux the wizard needs root (it writes /etc/viiwork), Docker with Compose v2, and the ports free. It stops if a config or an earlier install is already there, or if a container named viiwork exists: a node set up by hand is left alone.

  2. Hardware. It lists the GPUs and names the engine image it will run.

  3. Models. It asks for your models directory, lists each model it finds with its architecture and size, and asks which to serve and what to call them. Give each model the name the rest of your mesh uses for it, so that requests for it can go to any machine. Second shards of a split model and vision projectors are not listed. ~ means your home directory.

  4. Layout. It proposes which cards each model gets, the context per slot, and how many slots. You can accept the layout, edit one model's line, or drop a model. A layout the node would refuse is shown with the reason.

  5. Mesh. First it looks for nodes nearby, on your tailnet and on the LAN. Then it offers three choices:

    • join an existing mesh with a join code (see below);
    • start a new secured mesh;
    • start an open mesh (no secret), with a warning.

    The network is tailnet when tailscaled runs on this machine, else lan. Change mesh.network in the config afterwards if you need to. A join code carries the first node's address, so the new machine must be able to reach it.

  6. Updates. Choose whether this machine accepts mesh-wide updates (viiwork update, see Updating). Only a secured mesh can have them.

  7. Node name. The default comes from the host name. A name a node found nearby already uses is refused.

  8. Review. It shows every file with its content, the secret masked. On a yes it writes the files, pulls the engine image (several GB on Linux), starts the node, and waits until every model has loaded.

  9. Finish. It runs the viiwork-accept config and ready checks, plus the join check when the node joined a mesh. Then it prints how to reload and restart. A new secured mesh also gets the join code for the next machine.

Pressing Ctrl-C or Ctrl-D at any question leaves nothing behind.

What it writes

PathModeWhat
/etc/viiwork/viiwork.yaml0644The config. Edit it freely, then sudo docker kill -s HUP viiwork.
/etc/viiwork/mesh.env0640The mesh secret (secured mesh only).
/etc/viiwork/docker-compose.yaml0644One service: host networking, the GPUs, the models directory read-only at /models, /etc/viiwork read-only, restart: always.
/etc/viiwork/install.json0644The manifest: exactly what viiwork uninstall removes.
/usr/local/bin/viiwork0755A copy of the binary.
/var/lib/viiwork/—Node state: the alias table and staged releases.
/etc/systemd/system/viiwork-engine.{service,path,timer}0644The engine helper, which makes the image follow the release (below).
/var/lib/viiwork-engine/0700The helper's own directory: the image tag the install started with, and the releases it verified.

Model weights are never copied or moved.

When it writes the config only

Only some machines get an engine image and a started node. In these cases the wizard writes the config, mesh.env and the manifest, starts nothing, and says why:

  • A development build. Only a release (vX.Y.Z, or a pre-release such as v2.6.0-beta4) has published images. Use a release.

  • An AMD Radeon VII or MI50 (gfx906). It has no published image, so build one from a checkout and start it with the example compose file:

    git clone https://github.com/janit/viiwork && cd viiwork && git checkout vX.Y.Z
    make docker                  # gfx906; needs the ROCm base images, see BUILDS.md
    sudo cp configs/docker-compose.v2.example.yaml /etc/viiwork/docker-compose.yaml
    # point the models mount at your models directory:
    sudo sed -i 's#- /models:/models:ro#- /path/to/models:/models:ro#' /etc/viiwork/docker-compose.yaml
    sudo docker compose -f /etc/viiwork/docker-compose.yaml -p viiwork up -d
    

    The wizard's config and mesh.env are already in /etc/viiwork, where the example compose file expects them.

  • Other AMD cards. No image is verified yet: viiwork pins a backend to its cards with ROCR_VISIBLE_DEVICES, which the Vulkan build ignores. Build an image for your card (BUILDS.md) and start it as above.

The config's model paths are /models/..., so mount your models directory there. A compose project you started by hand is not in the manifest: before viiwork uninstall, stop it with sudo docker compose -f /etc/viiwork/docker-compose.yaml -p viiwork down.

Joining the next machine

On any node of a secured mesh, print a join code:

sudo sh -c 'set -a; . /etc/viiwork/mesh.env; /usr/local/bin/viiwork join-code'   # Linux
~/.local/bin/viiwork join-code                                                    # Mac (reads the agent's plist)

It prints a code starting with viiwork1-. The code is the mesh secret: treat it like mesh.env, and never post it anywhere. Run the wizard on the new machine and paste the code at the mesh question. In an open mesh, viiwork join-code --open prints a code without a secret. If the wizard stopped before printing its join code, get one this way.

When the node does not come up

The files are kept. The wizard prints each backend that is not healthy, with its status and phase, followed by the node's last log lines. llama.cpp's own errors are in those lines:

  • Linux: sudo docker logs viiwork shows them all.
  • Mac: tail -f ~/Library/Logs/viiwork/viiwork.log.

The usual causes:

  • The model does not fit at the chosen context. Lower context, or give it more gpus, in the config.

  • The model needs llama.cpp flags. Some models' chat templates cannot be parsed by llama.cpp's default (Jinja) handling. The log then says chat template parsing error and suggests --no-jinja. The wizard cannot know this from a model's header. Add the model's flags to its entry, for example for TranslateGemma:

        args: ["--no-jinja", "--chat-template", "gemma"]
    

After editing the config, reload the node. It restarts the models whose entries changed:

sudo docker kill -s HUP viiwork                                   # Linux
launchctl kill HUP gui/$(id -u)/fi.viiwork.node                   # Mac

To stop the node and every model it runs, and to start it again:

sudo viiwork stop     # Linux; `viiwork stop` on a Mac, without sudo
sudo viiwork start

stop leaves the mesh first and finishes in-flight requests, so other members take the traffic, and returns once the node's API has gone quiet. On Linux it also stops the engine helper, which could otherwise bring the container back. The node stays stopped until viiwork start, or the next boot (Linux) or login (Mac). Only this machine is stopped. Both commands work from the install manifest, so a host set up by hand is stopped the way it was started.

To start over, viiwork uninstall (with sudo on Linux) removes everything in the manifest and nothing else.

Updating

A machine that accepted mesh-wide updates updates itself when you roll a release across the mesh from any node:

sudo sh -c 'set -a; . /etc/viiwork/mesh.env; /usr/local/bin/viiwork update'      # from a Linux node
~/.local/bin/viiwork update                                                       # from a Mac node

It stages the newest signed release on every node, then activates them one at a time, and stops if a node rolls itself back or does not rejoin the mesh.

On a Mac the llama.cpp build moves with the release: staging fetches the build the release is pinned to, checked against the sha256 signed into that release, beside the one running now, and the node always runs the build of its own binary's pin, so a rollback takes the engine back too. Builds no kept release runs are removed once a release confirms. A llamacpp.binary you pointed anywhere else is left exactly as written.

On Linux the engine moves with the release. The node cannot swap its own Docker image, so the wizard installs a small helper on the host, viiwork engine-sync, run as root by the systemd units viiwork-engine.path and viiwork-engine.timer. When a release is staged the helper pulls and checks its image, so a bad image fails before any machine restarts; when it is activated the helper points the compose file at it and restarts the node, and on a rollback it points it back. It verifies every release's signature itself, with the keys in the host's own binary, and requires the image's viiwork to be exactly the signed release's; from the node's state directory, which the container can write, it takes nothing but a version name. Once the node confirms a release, the helper also updates /usr/local/bin/viiwork. journalctl -u viiwork-engine.service shows what it did. releases.md has the details, and what each node checks.

A machine set up by an earlier release (v2.6.0 or a beta) has no helper, and its updates move the viiwork binary only. To add it, run init from the new release's binary (downloaded and checked as above):

sudo ./viiwork init

On an existing Docker install init offers only this: it writes the three units, installs itself as /usr/local/bin/viiwork, records both in install.json so that uninstall removes them, and starts the helper. The config, the compose file and the running node are left alone. Until then, if a release ever needs a newer engine, staging on that machine is refused with the reason. To opt in later, add this to the config and restart the node (a reload does not apply it):

update:
  enabled: true

Uninstalling

sudo /usr/local/bin/viiwork uninstall [--yes] [--delete-models] [--keep-images]   # Linux
~/.local/bin/viiwork uninstall [--yes] [--delete-models]                           # Mac, no sudo

Uninstall reads the manifest and lists everything it will remove, with sizes. Then it asks you to type uninstall. If this node is the mesh's last member, it says so first: the alias table, and in a secured mesh the only copy of the secret, go with it. After you confirm, it:

  1. stops the node the normal way, so it leaves the mesh and in-flight requests finish (if the node cannot be stopped, it removes nothing);
  2. on Linux, runs docker compose down for its own project only, and removes the engine image unless you pass --keep-images (or another container still uses it);
  3. removes the files and the binary, the engine helper's units (stopped before the node, so it cannot start it again) and directory, and the state directory if the install created it (a state directory that was there before is left alone).

It never prunes, never touches another compose project, and leaves any file you added to /etc/viiwork.

Model weights are kept, so a reinstall downloads nothing. --delete-models also deletes the model files this node served. It lists them first and asks for its own confirmation, and it never deletes anything else in the models directory.

A host set up by hand has no manifest, so uninstall refuses rather than guess. viiwork uninstall --from-config lists what the config points at and removes nothing.

On a Mac

On an Apple Silicon Mac, run ./viiwork init without sudo. The wizard asks the same questions, with two differences:

  • Planning. The Mac has one GPU, whose memory it shares with everything else on the machine. Models are planned against the Metal budget (iogpu.wired_limit_mb, about 75% of RAM by default), minus headroom. A model that does not fit is shown with the reason.
  • What gets installed. No Docker. Instead the wizard fetches llama.cpp at the release this binary was built against. It downloads it from the release's fixed URL, without GitHub's API, and checks it against the sha256 signed into this binary, then installs a LaunchAgent.
PathModeWhat
~/.config/viiwork/viiwork.yaml0644The config: absolute paths, gpu.vendor: apple, each model's llamacpp.binary.
~/Library/LaunchAgents/fi.viiwork.node.plist0600The agent, with the mesh secret in its environment.
~/.local/share/viiwork/llama.cpp/<tag>/—The llama.cpp build, quarantine attribute cleared. A build already there (from scripts/macos/fetch-llama.sh) is reused and left alone by uninstall. The manifest records the directory above it (llama_root), where updates fetch their builds.
~/.local/bin/viiwork0755A copy of the binary.
~/.local/state/viiwork/, ~/Library/Logs/viiwork/—Node state, and the log.
~/.config/viiwork/install.json0644The manifest.

~/.local/bin is not on a Mac's default PATH. Add it once, so that viiwork join-code, viiwork top and the rest work by name:

echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.zprofile && . ~/.zprofile

After setup:

  • Reload the config with launchctl kill HUP gui/$(id -u)/fi.viiwork.node.
  • Restart the node with launchctl kickstart -k gui/$(id -u)/fi.viiwork.node.
  • The node runs while you are logged in. A sleeping Mac leaves the mesh, so run caffeinate -s while it serves on power.
  • Remove the install with viiwork uninstall, without sudo.
  • macos.md covers the rest: Tailscale builds, memory, and what reads as unavailable.