L2 · 11 · Local Models: Ollama, LM Studio, MLX
Running models on your own machine gives you privacy, offline use, zero per-token cost and a much better feel for how models behave. This page covers choosing a model that fits your hardware, quantisation, the main local runners (including MLX on Apple silicon), and when local is the right call.
By the end of this page you can
- Estimate whether a model fits in your machine's memory.
- Explain quantisation and pick a sensible format (GGUF, MLX 4-bit).
- Run models with Ollama, LM Studio, llama.cpp and MLX, and call them through the same API.
- Benchmark speed and quality, and decide local vs hosted per task.
When local makes sense
Good fit
- Sensitive data that must not leave the machine (where policy allows local AI)
- Offline work, travel
- High-volume simple tasks: classification, extraction, embeddings
- Learning and experimenting freely
- Prototyping before choosing a hosted model
Poor fit
- Hard reasoning, large refactors, long agentic tasks: frontier hosted models are much stronger
- Very long contexts on modest hardware
- Team-wide production serving (needs proper GPU infrastructure, Level 4)
Will it fit? A memory rule of thumb
memory needed ≈ parameters × (bits per weight / 8) + 20–30% overhead (context / KV cache, runtime)
| Model size | 16-bit | 8-bit | 4-bit | Runs comfortably on |
|---|---|---|---|---|
| 1–4 B | 2–8 GB | 1–4 GB | ~1–3 GB | Almost any modern laptop |
| 7–9 B | ~16 GB | ~8–9 GB | ~5–6 GB | 16 GB laptop (Apple silicon or a decent GPU) |
| 12–14 B | ~28 GB | ~15 GB | ~8–10 GB | 24–32 GB unified memory or a 12–16 GB GPU |
| 30–35 B | ~70 GB | ~35 GB | ~20 GB | 36–64 GB Mac or a 24 GB GPU |
| 70 B+ | 140 GB+ | ~75 GB | ~40 GB+ | 64–128 GB Mac or multi-GPU workstation |
Quantisation
Quantisation stores weights with fewer bits (for example 4 instead of 16). Models become 3–4× smaller and faster, with a modest quality loss that is usually acceptable at 4–8 bits.
| Format | Used by | Notes |
|---|---|---|
GGUF (e.g. Q4_K_M, Q5_K_M, Q8_0) | llama.cpp, Ollama, LM Studio | Q4_K_M is a common balance. Q5/Q6 is better quality, Q8 is near-original. |
| MLX (4-bit, 8-bit) | MLX / mlx-lm on Apple silicon | Optimised for Macs; many ready-made conversions are published by the community |
| GPU formats (AWQ, GPTQ, FP8…) | GPU servers (vLLM etc.) | Level 4 |
Rule: prefer a bigger model at 4-bit over a smaller model at 16-bit for the same memory, then test on your own tasks.
The local runners
Ollama: simplest CLI + server
ollama pull llama3.2 # small, fast general model ollama run llama3.2 "Summarise our 14-day return policy in one line: ..." ollama pull nomic-embed-text # embeddings for page 05 ollama list # installed models # OpenAI-compatible API at http://localhost:11434/v1
LM Studio: desktop app
Browse, download and chat with models in a GUI. It shows whether a model fits your memory. Start its local server to get an
OpenAI-compatible endpoint (by default on http://localhost:1234/v1). Good for comparing models side by side.
llama.cpp: the engine
llama-server -m ./models/some-model-Q4_K_M.gguf --port 8080 # OpenAI-compatible API at http://localhost:8080/v1
MLX on Apple silicon
uv tool install mlx-lm # or: pip install mlx-lm mlx_lm.generate --model mlx-community/<model>-4bit --prompt "Explain opaque types in Scala 3" mlx_lm.server --model mlx-community/<model>-4bit --port 8081 # OpenAI-compatible server
MLX also supports fine-tuning (LoRA) on a Mac, which is covered in Level 3.
One client, any runner
Because all of these speak the OpenAI-compatible API, the clients from pages 03–06 work unchanged. Just set the base URL:
| Runner | LLM_BASE_URL |
|---|---|
| Ollama | http://localhost:11434/v1 |
| LM Studio | http://localhost:1234/v1 |
| llama.cpp server | http://localhost:8080/v1 (or the port you chose) |
| mlx_lm.server | http://localhost:8081/v1 (the port you chose) |
Benchmark on your own tasks
import json
import os
import time
import httpx
BASE = os.environ.get("LLM_BASE_URL", "http://localhost:11434/v1")
MODEL = os.environ.get("LLM_MODEL", "llama3.2")
TASKS = [ # (prompt, check) - checks are deliberately simple; use golden sets for real evals
("Classify as PICKUP, RETURN, PAYMENT or OTHER. Label only: 'My card was charged twice'", lambda a: "PAYMENT" in a.upper()),
("What is 17% of 2,400? Number only.", lambda a: "408" in a),
("Return only JSON {\"sku\":string,\"qty\":int} for: two chargers, sku CHARGER", lambda a: '"qty": 2' in a or '"qty":2' in a),
]
def run(prompt: str) -> tuple[str, float, float, int]:
start, first, chunks, text = time.perf_counter(), None, 0, []
with httpx.stream("POST", f"{BASE}/chat/completions", timeout=120,
json={"model": MODEL, "stream": True, "temperature": 0,
"messages": [{"role": "user", "content": prompt}]}) as r:
r.raise_for_status()
for line in r.iter_lines():
if not line.startswith("data: ") or line.endswith("[DONE]"):
continue
delta = json.loads(line[6:])["choices"][0].get("delta", {}).get("content")
if delta:
first = first or time.perf_counter()
chunks += 1
text.append(delta)
end = time.perf_counter()
return "".join(text), (first or end) - start, end - start, chunks
if __name__ == "__main__":
passed = 0
for prompt, check in TASKS:
answer, ttft, total, chunks = run(prompt)
ok = check(answer)
passed += ok
rate = chunks / max(total - ttft, 1e-6)
print(f"{'PASS' if ok else 'FAIL'} first-token {ttft:.2f}s ~{rate:.0f} chunks/s {answer.strip()[:60]!r}")
print(f"\n{MODEL}: {passed}/{len(TASKS)} passed")
Run it for 2–3 candidate models and keep the results in your learning log. You will quickly build intuition for which sizes handle which tasks.
Practical tips
- Use instruction-tuned models ("instruct" or "chat" variants) for chat and tools, not base models.
- Context length costs memory. Increase it only when needed (most runners have a setting).
- Tool calling and JSON quality vary a lot between local models. Test with your real tools and schemas (pages 02 and 04).
- Check licences of open-weight models before using them for work. Some restrict commercial use.
- Local still needs policy approval. Data never leaves the machine, but model provenance and licences still matter to employers.
- Keep models tidy: they are several GB each (
ollama rm, LM Studio's model manager).
- Install Ollama or LM Studio. Pull a ~3B model and an ~8B model (4-bit).
- Run
bench.pyagainst both. Compare speed and pass rate. - Point the Store Assistant (page 04,
--live) at each. Which one calls tools reliably? - On a Mac: try the same model via
mlx_lm.serverand compare speed with Ollama.
Key takeaways
- Memory ≈ parameters × bits/8 plus overhead. 4-bit quantisation makes 7–14B models laptop-friendly.
- Ollama (simple), LM Studio (GUI), llama.cpp (engine) and MLX (Apple silicon) all expose OpenAI-compatible APIs.
- Local is great for privacy, offline use, volume and learning. Frontier hosted models still win on hard tasks.
- Benchmark on your own tasks, and check licences and policy.