Skip to content

kumulativ › Docs › The shared model runtime

The shared model runtime How the three apps share one model store and one llama.cpp server on your machine.

The shared model runtime

The local-model runtime shared by every kumulativ desktop app — smartreader, smartwriter, smartdiary. One Go module, one package: engine.

The problem it solves

Each app used to carry its own copy of the same six files, its own models/ and engines/ directories, and its own llama-server sidecar. Installing a second app meant downloading the same weights a second time. Opening two apps meant two servers, each holding a model in graphics memory, with no idea of each other. And on macOS, where nothing ties a child to its parent, a crashed app could leave a server holding five gigabytes with no window left to close.

What it does instead

One store. Every app reads and writes <UserConfigDir>/kumulativ/:

kumulativ/
  models/      every GGUF any app has fetched — downloaded once, per machine
  engines/     the pinned llama.cpp build for this machine
  run/
    server.json   who is serving, on which port, and which app owns the process
    server.lock   held while deciding whether to start one
    presets.ini   per-model settings the server is started with

On first use an app moves anything found in its old private models/ and engines/ into the store — a rename, not a copy — so upgrading never re-downloads what is already on disk.

One server. The first app that needs a model starts a single llama-server in router mode (--models-dir), which serves every model in the store from one loopback port. A request names its model; the router loads it on first use, keeps at most one resident (--models-max 1), and puts it to sleep after a quiet minute (--sleep-idle-seconds), freeing its memory. All three apps run the same Qwen3.5 family, so a second app finds the weights already there; a model from another family would be swapped in on the next question, and nothing is held while nobody is asking — whichever app’s question it was.

Per-model settings (context window, reasoning off) live in presets.ini, written from the catalog in config.go when the server starts, so a model fetched by one app is served with the right settings whichever app started the server.

One owner. The app that started the server owns it: the server is that process’s child, enrolled in a Job Object on Windows so it cannot outlive it, and stopped by Runtime.Stop on a clean exit. Every other app finds it through run/server.json, checks that it is alive, and uses it as a client. When the owner exits the server goes with it; the next sibling that asks a question notices, takes the lock, and starts a fresh one — a few seconds of reload, paid once, and never two servers. On macOS and Linux, where a crash can leave a server running with no owner, the next app to start adopts it rather than starting another, and stops it at its own exit.

Using it from an app

import "github.com/stijlmassi/kumulativ-core/engine"

rt := engine.New(engine.Config{
    App:        "smartreader",
    Family:     engine.Qwen35,           // the family all three apps run
    LegacyDirs: []string{oldConfigDir},  // models/ and engines/ move to the store
})
rt.OnChange(func(up bool) { /* status chip */ })

// first run: both resumable, both report Progress
rt.EnsureEngine(ctx, onProgress)
rt.EnsureWeights(ctx, onProgress)

// before a question: server up, this app's model resident
rt.Ensure(ctx, onProgress)
text, err := rt.ChatStream(ctx, msgs, 0.3, func(tok string) { /* render */ })

// at exit: stops the server if this process owns it, else forgets it
rt.Stop()

Status() reports whether this app’s model is resident and on which backend (cuda, metal, cpu); a sleeping model reports as not running, because it holds no memory and the chip in the nav bar exists to say so.

Tests use engine.NewTestRuntime(cfg, fakeServerURL): a runtime pinned to a URL that never spawns, never polls and never stops anything.

Environment

Variable Effect
KUMULATIV_HOME Move the shared store (portable installs, tests)
<APP>_GGUF Point one app at a specific .gguf; nothing is downloaded for it
KUMULATIV_BIN_DIR Look for a staged llama-server here first
KUMULATIV_IDLE_UNLOAD_MIN Minutes of quiet before the server sleeps a model (default 1; 0 = never)
KUMULATIV_LLAMA_ASSET / KUMULATIV_LLAMA_RELEASE Pin a different llama.cpp asset or release
KUMULATIV_LLAMA_EXTRA_ARGS Extra flags appended to the server command line
KUMULATIV_FORCE_CPU Never use the CUDA build
KUMULATIV_NO_ENGINE_FETCH Refuse to download an engine
KUMULATIV_DEBUG Forward the server’s output to stderr

Pinned build

LlamaRelease in binaries.go is b10146. The router flags this package relies on (--models-dir, --models-preset, --models-max, --sleep-idle-seconds) and the per-model preset keys (c, reasoning) were verified against that build; re-verify before bumping.