
Sovereign AI
LM Studio vs Ollama vs llama.cpp: One Engine, Two Wrappers
Ollama now runs upstream llama-server for GGUF, and LM Studio wraps llama.cpp too. The engine is shared; the licence, provenance and defaults are not.
Local inference arrives through a laptop. Somebody installs the tool with the best first five minutes, it works, and eighteen months later it is quietly serving nine people from a workstation under a desk. The comparison that decided it was about download size and onboarding. The one that matters is what happens to the licence, the model supply chain and the defaults when the second user arrives.
One Engine, Two Wrappers: What Actually Runs the Tokens
Scope this argument before making it: it holds along exactly one path. For GGUF models — the quantised single-file format most local deployments run — the process executing your tokens is the same project under all three tools. Safetensors take a different route, discussed below.
The mechanism has a date. Pull request #16031 merged into Ollama on 29 May 2026 and did something more surgical than "Ollama uses llama.cpp" suggests.
Remove the vendored GGML and llama.cpp backend, CGO runner, GGML-based Go model implementations, and sample. llama-server (built from upstream llama.cpp via FetchContent) is now the sole inference engine for GGUF-based models. (Safetensor based models continue to run on the new MLX engine.)
Three consequences follow, each checkable in the repository rather than inferred. The relationship is a process boundary, not a library link: the doc comment on Ollama's own NewLlamaServer constructor in llm/server.go states that all GGUF models are served via the upstream llama-server subprocess. The build is pinned: a root file named LLAMA_CPP_VERSION names the exact upstream build fetched, reading b11232 at the time of writing. And the transition is not finished: llama/compat/ is documented as a temporary in-process layer for already-published Ollama GGUFs whose metadata or tensor layout upstream does not load directly, translating them in memory at load time so users need not re-pull.
This is a reversal, not a constant. In May 2025 Ollama thanked the GGML team for a tensor library it reached directly from Go, calling that access "a portable way to design custom inference graphs." That is the arrangement #16031 deleted. The README still lists llama.cpp under Supported backends, as before — #16031 removed Ollama's own vendored runner, not a README entry.
The proprietary vendor says the same thing in its own words. LM Studio's documentation states that it supports running LLMs on Mac, Windows and Linux using llama.cpp; separately, that on Apple Silicon Macs it also supports Apple's MLX. Its 0.4.0 release post is blunter still: parallel requests work thanks to llama.cpp's open-source continuous batching implementation, adopted into LM Studio's engine.
Resist the obvious next sentence. A shared engine project is not a shared binary, and certainly not measured parity. Ollama pins one upstream build while LM Studio numbers its own llama.cpp runtime separately — 2.0.0 at the 0.4.0 release, switched through the lms runtime CLI, with an llama.cpp 2.43.0 extension packs line appearing more recently in the changelog of LM Studio Bionic, a separately versioned agent product, not the classic app. Three numbering schemes are live at once: llama.cpp's own current stable release is v0.5.0, published 23 September 2026 at the time of writing, while the b-numbered prerelease channel Ollama's pin draws from is equally current — b11243 shipped 29 September 2026, six days later.
Free, Open, and the Gap Between Them
With the engine question settled, procurement's is the one left standing — and each tool answers it in a specific document.
- llama.cpp — MIT. The LICENSE file, copyright "The ggml authors." Patch it, fork it, vendor it into a product, redistribute the result.
- Ollama — MIT. Its LICENSE file, same grant. A fork that changes the registry default or the concurrency behaviour is yours to run and to ship.
- LM Studio — a licence to use. The App Terms grant "a non-exclusive, non-transferable license to use the Software solely for Your personal and / or internal business purposes and solely in accordance with the Documentation." That is an end-user licence, not an open-source grant.
Be fair about the price: the criticism is not about cost. LM Studio removed its separate commercial-licence requirement in an announcement dated 8 July 2025 — teams no longer need to get in touch before using it at work. Its command-line tool lms is even MIT, which means the automation surface is open while the application driving it is not.
The gap between free and open is a gap in optionality, visible only under stress. Free tells you this quarter's invoice. Open tells you what happens when the vendor changes its terms, ships a regression you need patched this week, or stops shipping. Under MIT you fork, patch and keep running; llama.cpp even exposes a public llama library target with a public include directory in its build definition, so embedding it in your own binary is a supported shape rather than a workaround. Under an end-user licence you file a ticket and wait. Both are legitimate positions — but only one is a dependency you can service yourself, and only one survives the vendor.
Who Mints the GGUF You Are Running
A model tag is not a model. ollama pull and LM Studio's browser both resolve a friendly name to a quantised artefact somebody else produced, at a precision somebody else chose, from source weights you never saw converted. Ollama's own import documentation says plainly that it does not quantise GGUF models during import, and that you should prepare and quantise them first with a tool such as llama.cpp's llama-quantize. That decision is already outside the wrapper. Take the rest of it.
llama.cpp documents both halves. convert_hf_to_gguf.py converts a Hugging Face repository straight to GGUF, and llama-quantize converts that high-precision GGUF — typically F32 or BF16 — to a quantised format of your choosing. Run both and the precision trade-off becomes yours to make deliberately rather than inherit; if you are choosing that precision against a fixed card, the VRAM sizing arithmetic is the constraint to work backwards from.
#!/usr/bin/env bash
# Usage: LLAMA_CPP_BUILD=b11232 ./mint-gguf.sh google/gemma-4-E2B-it Q4_K_M
# Run from a BUILT llama.cpp checkout: convert_hf_to_gguf.py sits at the repo
# root, llama-quantize is a build artefact (cmake -B build && cmake --build
# build --target llama-quantize), and the Python conversion requirements are
# installed. A packaged llama.cpp has llama-quantize on PATH but ships no
# convert_hf_to_gguf.py, so the checkout is the environment that has both.
set -euo pipefail
REPO="${1:?usage: mint-gguf.sh <hf-org/model> [quant-type]}"
QUANT="${2:-Q4_K_M}"
# Record the engine build you converted WITH — conversion and inference are
# the same project, and both move. Read it off your checkout or the pin.
BUILD="${LLAMA_CPP_BUILD:?set to the upstream llama.cpp build tag, e.g. b11232}"
OUT="$(basename "$REPO")"
# llama-quantize is a build artefact, not something a fresh checkout puts on
# PATH. Resolve it BEFORE the download, so a missing binary fails in a second
# rather than after tens of gigabytes have moved.
QUANTIZE="${LLAMA_QUANTIZE:-$(command -v llama-quantize || echo ./build/bin/llama-quantize)}"
[ -x "$QUANTIZE" ] || {
echo "no llama-quantize at $QUANTIZE — build it (cmake --build build" >&2
echo "--target llama-quantize) or point LLAMA_QUANTIZE at one." >&2
exit 1
}
# GNU vs BSD userland: ask, do not assume.
hash_file() {
if command -v sha256sum >/dev/null 2>&1; then sha256sum "$1" | cut -d' ' -f1
else shasum -a 256 "$1" | cut -d' ' -f1; fi
}
size_of() { stat -c%s "$1" 2>/dev/null || stat -f%z "$1"; }
# 1. Source weights -> high-precision GGUF. --remote streams the HF repo.
python convert_hf_to_gguf.py \
--outfile "${OUT}-bf16.gguf" --outtype bf16 --remote "$REPO"
# 2. High-precision GGUF -> the quantisation YOU chose, not one you were handed.
# The bf16 intermediate is tens of gigabytes: delete it once step 3 verifies.
"$QUANTIZE" "${OUT}-bf16.gguf" "${OUT}-${QUANT}.gguf" "$QUANT"
# 3. The record. Hash the bytes that will actually be loaded.
cat > "${OUT}-${QUANT}.provenance.json" <<EOF
{
"artefact": "${OUT}-${QUANT}.gguf",
"sha256": "$(hash_file "${OUT}-${QUANT}.gguf")",
"bytes": $(size_of "${OUT}-${QUANT}.gguf"),
"quantisation": "${QUANT}",
"source_weights": "${REPO}",
"converted_with_llama_cpp": "${BUILD}",
"minted_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
}
EOF
echo "minted ${OUT}-${QUANT}.gguf with provenance beside it"The artefact you just produced is portable by construction. GGUF is a binary format for models run by GGML-based executors, designed for single-file deployment: models distribute and load without external files. That property is why one file loads under all three tools, and the whole basis of the exit argument. Registering it with Ollama needs no registry round trip: a Modelfile takes the GGUF location as an absolute path or one relative to the Modelfile. Ollama's default registry host is registry.ollama.ai, a constant in its own source — a default, which means a thing you can decline. That provenance record feeds a model inventory, where the governance frame for it lives.
The API Contract, and Where OpenAI-Compatibility Stops
All three speak something OpenAI-shaped, which makes swapping the runtime under a client look free. It is nearly free — and the gap is worth knowing precisely, including where nobody wrote it down.
Ollama is the one project here publishing a per-parameter matrix, and it is candid: its compatibility documentation opens by saying Ollama supports a subset of the OpenAI API. On /v1/chat/completions it marks four request fields unchecked — tool_choice, logit_bias, user and n — and among that endpoint's features marks Logprobs unchecked while Tools and reasoning/thinking control are supported. Image content is supported as base64 but not by URL. And the Responses API is supported in its non-stateful flavour only: no previous_response_id, no conversation.
The other two advertise a surface rather than a matrix. llama-server's README lists OpenAI-compatible chat completions, responses and embeddings routes. LM Studio documents five endpoints: /v1/models, /v1/responses, /v1/chat/completions, /v1/embeddings and /v1/completions. Neither project publishes a per-parameter support list, and that absence is itself the finding — not evidence that everything is honoured. Anyone telling you which fields llama-server or LM Studio silently drops is guessing, and a client built on a guess fails quietly — a parameter accepted and ignored rather than rejected.
So treat each surface as a subset you have measured, not a contract you were given. Assert every parameter your client depends on in an integration test, run it against each runtime you might deploy, and keep it in CI. That is what a self-hosted coding assistant needs against its backend, and what turns "OpenAI-compatible" from an adjective into a portability guarantee.
What Changes When the Second User Arrives
This is where a desk tool becomes a service, and where inherited defaults start deciding on your behalf. Three llama-server defaults govern concurrency, and they interact.
-np, --parallel N— the number of server slots, defaulting to-1, which means auto. The slot count is therefore something the server picks unless you pick it.-kvu, --kv-unified/-no-kvu, --no-kv-unified— use a single unified KV buffer shared across all sequences, documented as enabled by default when the number of slots is auto. Because auto is itself the slot default, the out-of-the-box arrangement is a shared buffer, not a hard per-slot partition.--kv-unified-per-slot N— a context limit per parallel slot, unset by default. Its documented arithmetic is specific: when set without-c/--ctx-size, the shared KV pool is sized ton_parallel*N.
That third row matters because the folk rule — total context divided by the slot count — is not what the current defaults do. Continuous batching is separately enabled by default, so requests interleave rather than queue. Decide the slot count and the KV strategy explicitly, and if you want a per-slot ceiling, set the lever that exists for it rather than reasoning about a division the server is not performing.
[Unit]
Description=llama-server (shared team inference endpoint)
After=network-online.target
Wants=network-online.target
[Service]
Type=exec
# Create the account first, it is not implicit:
# useradd --system --no-create-home --shell /usr/sbin/nologin llama
User=llama
Group=llama
# LLAMA_API_KEY lives here, chmod 600, root:root — systemd reads it as
# root before dropping to User=llama. It is the documented environment
# variable for --api-key, so the secret never reaches the ExecStart line,
# the process list, or anybody's shell history.
EnvironmentFile=/etc/llama-server/secrets.env
# Every flag below reverses a default that was correct on a laptop.
# --kv-unified-per-slot with no -c/--ctx-size is the documented condition
# under which the shared KV pool is sized to n_parallel * N = 4 * 8192.
ExecStart=/usr/local/bin/llama-server \
--model /srv/models/model-Q4_K_M.gguf \
--host 10.0.10.12 --port 8080 \
--parallel 4 \
--kv-unified \
--kv-unified-per-slot 8192 \
--metrics
Restart=on-failure
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
ReadOnlyPaths=/srv/models
[Install]
WantedBy=multi-user.targetLM Studio exposes the same two decisions as load options rather than flags, documented in its 0.4.0 post. Max Concurrent Predictions sets the maximum number of concurrent requests the model will process, and requests beyond that limit are queued — a different failure shape from llama-server's slots, turning a capacity problem into a latency problem. Unified KV Cache is enabled by default, and when enabled preallocated resources are not hard-partitioned per concurrent request, allowing request sizes to vary. Ollama's concurrency controls are environment variables and a different discussion; the vLLM and Ollama comparison covers them, and the point where none of these tools is the right answer.
The Defaults That Are Wrong the Moment It Leaves the Laptop
Four documented llama-server behaviours decide your exposure the moment the endpoint leaves the laptop. --api-key defaults to none. --host defaults to 127.0.0.1, so reaching the network is an explicit operator act — a good default, and the one keeping the others from mattering until somebody changes it. --metrics is disabled unless passed, leaving a shared endpoint unobservable. And /health is documented as public, with no API key check, even when a key is configured.
State that last one carefully: it is a design choice, not a flaw — a liveness probe requiring a credential is one your orchestrator cannot use. But the existence and reachability of your model server is available to anyone who can route to it. Plan the network boundary accordingly, not around the API key alone.
LM Studio's posture is documented and similar in shape. Its server does not require authentication by default; API Tokens are opt-in, toggled from the Developers Page under Server Settings, with permissions selected per token. Its serve-on-network page is direct about the consequence: any bind other than 127.0.0.1 exposes the server beyond localhost, and it recommends enabling authentication first. Ollama's bind-and-authentication behaviour is covered in detail elsewhere on this site and is the same story in a third dialect.
The moment these stop being theoretical is documented too. LM Studio runs as a background service with no GUI via llmster, described as the core of the desktop app packaged to be server-native, able to run on Linux boxes, cloud servers and GPU rigs. Useful — and the exact step where a desk tool acquires an attack surface, an uptime expectation and users who are not you. Audit-grade rigor here is unglamorous: enumerate those defaults, record which you changed and why, and show that file rather than the tool's name.
Exit Ramps, and the Long Game
Price the exits before you need them. They are not equally priced, and the cheap one is cheap for a structural reason.
- Between the three tools, along GGUF: nearly free. The format's single-file design is the mechanism. Copy the file, point a different command line at it, keep going. No re-download, no re-conversion, no format negotiation.
- Off the Ollama registry format: has a cost, and it is knowable. The artefacts needing the temporary in-process compatibility layer are precisely those whose metadata or tensor layout upstream does not read directly — the least portable things in the system. Mint your own GGUF and that layer never applies to you.
- Off GGUF entirely, to MLX: conditional, not substitutable. MLX is a second artefact path on the Safetensors side, Apple-Silicon-bound in LM Studio's case — and LM Studio's own 0.4.0 post says continuous batching "has not yet made it into our MLX engine," so the parallel-request story in section 5 is a GGUF capability there, not an MLX one. Ollama scopes its own MLX path differently, and documents it: enabled by default on macOS arm64, and on other platforms selected per backend at build time through the
OLLAMA_MLX_BACKENDSCMake option (cmake -B build . -DOLLAMA_MLX_BACKENDS=cuda_v13) rather than an environment variable you export — including a shipped Windows artefact,ollama-windows-amd64-mlx.zip, listed as MLX (CUDA). So the Safetensors path is platform-conditional per tool rather than Apple-only across the board; read each tool's documented scope before assuming your hardware carries. Leaving GGUF means re-minting from source weights, which is why the source weights and the exact conversion command are the two things worth keeping.
Promotion in the other direction — local runtime to production serving stack — is the asymmetric one, and the vLLM comparison covers why a GGUF does not simply carry across. Read it before the workstation under the desk becomes load-bearing, not after.
#!/usr/bin/env bash
# Inventory every GGUF on this host, with the tool that holds it.
# Set the roots that exist here — at least one. Unset ones skip.
set -euo pipefail
# GNU vs BSD userland: ask, do not assume.
hash_file() {
if command -v sha256sum >/dev/null 2>&1; then sha256sum "$1" | cut -d' ' -f1
else shasum -a 256 "$1" | cut -d' ' -f1; fi
}
size_of() { stat -c%s "$1" 2>/dev/null || stat -f%z "$1"; }
emit() {
printf '%s\t%s\t%s\t%s\n' \
"$(hash_file "$2")" "$(size_of "$2")" "$1" "$2"
}
# Tools that store models as named files: match on the extension.
scan_files() {
local holder="$1" root="$2"
[ -n "$root" ] && [ -d "$root" ] ||
{ echo "skip: $holder root unset or absent" >&2; return 0; }
find "$root" -type f -name '*.gguf' -print0 |
while IFS= read -r -d '' f; do emit "$holder" "$f"; done
}
# Ollama stores layers content-addressed instead: $OLLAMA_MODELS/blobs/
# sha256-<digest>, no extension ever, model tensors and metadata layers side
# by side. An extension filter finds NOTHING here and says so silently, which
# is the wrong answer to give anyone about to migrate. Match the blob name,
# then keep only files whose first four bytes are the GGUF magic.
scan_blobs() {
local holder="$1" root="$2"
[ -n "$root" ] && [ -d "$root/blobs" ] ||
{ echo "skip: $holder blobs dir unset or absent" >&2; return 0; }
find "$root/blobs" -type f -name 'sha256-*' -print0 |
while IFS= read -r -d '' f; do
[ "$(head -c 4 "$f" | tr -d '\0')" = GGUF ] || continue
emit "$holder" "$f" # filename already carries the digest; rehash anyway
done # so every row in the table was computed the same way
}
printf 'sha256\tbytes\tholder\tpath\n'
# Point these at the directories your installs actually use.
scan_files llama.cpp "${LLAMA_MODELS:-}"
scan_files lm-studio "${LMSTUDIO_MODELS:-}"
scan_blobs ollama "${OLLAMA_MODELS:-}"The long game is not choosing correctly today. All three will change again: Ollama's engine position reversed twice inside twelve months, and the pin, the runtime numbering and the compatibility layer are all explicitly transitional. Sovereignty at this layer means the durable assets sit outside whichever wrapper wins the laptop: the source weights, the conversion command, the hash of the artefact you serve, the file recording which defaults you reversed, and the test proving your client survives a runtime swap. Keep those five and the wrapper becomes what it should have been — an implementation detail you replace on a Tuesday. Same discipline the rest of a sovereign AI stack rests on, one layer further down than most teams look.
§FAQ/Common questions
Frequently asked
Is LM Studio the same as Ollama under the hood?
Along the GGUF path they now run the same engine project. Ollama's pull request #16031, merged on 29 May 2026, deleted its vendored GGML and CGO runner and made llama-server — built from upstream llama.cpp via FetchContent — the sole inference engine for GGUF-based models, spawned as a subprocess rather than linked as a library. LM Studio's documentation states it runs LLMs on Mac, Windows and Linux using llama.cpp, and its 0.4.0 release post credits llama.cpp's continuous batching implementation for its parallel requests. Same project is not the same binary, though: Ollama pins one upstream build in its LLAMA_CPP_VERSION file while LM Studio versions its own llama.cpp runtime separately, and the two ship different documented concurrency and exposure defaults. Safetensors models take a different path entirely, through Apple MLX.
Is LM Studio open source, and can I use it commercially?
Those are two different questions with two different answers. Commercially, yes and free: LM Studio removed its separate commercial-licence requirement in an announcement dated 8 July 2025, so using it at a company no longer requires getting in touch first. Open source, no: its App Terms grant a non-exclusive, non-transferable licence to use the software for personal or internal business purposes in accordance with the documentation, which is an end-user licence rather than a grant to modify, fork or redistribute. Its lms command-line tool is separately MIT-licensed. By contrast both llama.cpp and Ollama ship MIT LICENSE files, so you may patch, fork, vendor and redistribute either one. Free is a price; open is a grant, and only the grant tells you what happens when you need a fix the vendor will not ship.
Should I use llama.cpp directly instead of a wrapper?
Use it directly when you want the things a wrapper abstracts away. You get the full documented llama-server flag surface — the slot count, the KV strategy, the per-slot context lever, the API key, the bind address, the metrics endpoint — rather than a subset a wrapper chose to expose. You get an MIT licence and a public llama library target with a public include directory, so embedding it in your own binary is a supported shape. And you control which upstream build you run instead of inheriting a pin. The cost is real: no model browser, no registry, no one-line install, and you mint and manage your own GGUF artefacts. For a shared team endpoint that is usually the right trade, because every one of those costs is a one-off and every default you inherit is permanent until somebody notices it.
How many concurrent users can llama-server handle, and how do I configure it?
That depends on your model and hardware, but the controls are documented and three of them interact. The -np/--parallel flag sets the number of server slots and defaults to -1, meaning auto. The --kv-unified flag uses a single unified KV buffer shared across all sequences and is documented as enabled by default when the slot count is auto — so out of the box the KV buffer is shared rather than hard-partitioned per slot, which is not the same as the common assumption that total context is divided by the slot count. If you want a per-slot context ceiling, --kv-unified-per-slot N is the documented lever; when set without -c/--ctx-size the shared KV pool is sized to n_parallel*N. Continuous batching is separately enabled by default. Set all three explicitly rather than inheriting them.
Is it safe to expose LM Studio or llama-server to my team's network?
Not on the defaults, and both projects document why. llama-server's --api-key defaults to none and its /health endpoint is documented as public with no API key check even when a key is configured, so reachability is always discoverable. Its --metrics endpoint is disabled unless enabled, leaving a shared endpoint unobservable. LM Studio does not require authentication by default; API Tokens are opt-in from the Developers Page under Server Settings, with permissions selected per token, and its own serve-on-network documentation warns that any bind other than 127.0.0.1 exposes the server beyond localhost and recommends enabling authentication first. Both bind to loopback by default, which is the one default that protects you — treat changing it as the moment every other default needs revisiting, not as a configuration detail.
Executive Briefing
Thirty minutes to clarify your infrastructure risk
Walk us through your vendor footprint and regulatory constraints. We will tell you honestly where sovereignty creates leverage — and where it does not. No pitch deck. No obligation.