Local Models
Build a Chatbot: From API Call to Deployed Bot
Chapter 3 · Running a Model Locally
Every provider so far lives on someone else's computer. A local model runs on yours: the same chat conversation, answered by software on your own machine, with no account, no key, no
per-token bill and no data leaving the building. Thanks to the interface from chapter 2 it is not a new kind of bot, only a different base_url. This chapter is about the decisions around
that: whether it is worth doing, what your hardware can hold, and how to check and compare a local model without fooling yourself.
Should You Run One Locally?
| Reason | What it means | What it costs you |
|---|---|---|
| Privacy | Messages never leave your machine or network | You are now responsible for securing that machine |
| Cost | No per-token charges; the marginal cost of a message is electricity | The hardware, and the time to run it; heavy use can still outgrow a laptop |
| Offline and control | Works without internet; no vendor changing or retiring the model under you | You do the updating and the monitoring |
| Learning | The best way to see what a model does with no product around it | Time spent on setup instead of the bot |
The usual trade-off, a general expectation I did not measure here, is that models small enough for a laptop are less capable and slower than large hosted ones, so a local model is a good fit for a simple, private or offline bot and a poor fit when you need the best answers. This course keeps both options open for exactly that reason, and the comparison section below shows how to find out for your own prompts rather than trusting a rule of thumb.
This Machine, as a Worked Example
Whether a model runs well depends on the hardware, so here is the real survey of the machine this course is being built on (Windows, queried with PowerShell's CIM commands):
| Part | Measured |
|---|---|
| Memory (RAM) | 29.8 GB |
| CPU | AMD Ryzen 7 8745H, 8 cores, 16 threads |
| Graphics | AMD Radeon 780M, an integrated GPU that shares system memory (Windows reported "2 GB", but that figure is not meaningful for an integrated GPU, so I draw no conclusion from it) |
| NVIDIA GPU | None (nvidia-smi is not installed) |
| Free disk | 457.4 GB |
What this means is partly a documentation question. Ollama's GPU page lists supported AMD cards on Windows through ROCm as discrete Radeon RX 7000-series and Radeon PRO cards; the Radeon 780M is
not mentioned. The page says Vulkan is enabled by default when its backend is installed and warns that an integrated GPU can be unstable on mixed integrated/discrete systems. So on this machine
the safe expectation is CPU inference, with GPU acceleration an experiment I did not try. A discrete NVIDIA card would be the straightforward case (the docs give a minimum compute capability of 5.0 and a
recent driver). Your own survey takes one PowerShell command per line above (Get-CimInstance Win32_ComputerSystem, Win32_VideoController, Win32_Processor).
Memory Arithmetic
The first question is whether the model fits in memory at all. A model's weights take roughly parameters × bits per weight ÷ 8 bytes: a 7-billion-parameter model stored at 16 bits is 7 × 10⁹ × 2 bytes = 14 GB; at 4 bits (a common quantised format that trades some quality for size) it is about 3.5 GB. The small module below does the sum and prints a table for a given amount of memory. It is deliberately a lower bound: real files are somewhat larger, and the context (the conversation the model holds in memory) and the runtime need more on top, by an amount that depends on the model's architecture, so the module does not pretend to estimate them.
Run for this machine's 29.8 GB:
Read the right-hand column with care. "Fits" means only that the weights alone are smaller than all the machine's memory. Windows, your browser and your bot need memory too, so leave generous headroom: an 8B model at 4-bit (about 4 GB of weights) is comfortable on this machine, a 32B model at 4-bit (16 GB) is probably possible but tight, and a 70B model is out of reach at any of these precisions. The sizes in the table are round numbers for illustration, not a list of specific models.
The Two Servers You Will Meet
A local model needs a program that loads it and answers requests. Both common choices offer the OpenAI-compatible interface that OpenAICompatProvider already speaks. All of the following
comes from their documentation, and none of it was run here:
| Ollama | llama.cpp (llama-server) | |
|---|---|---|
| What it is | An application that downloads, stores and serves models, with a command-line tool | The reference C/C++ inference engine; you supply or download model files |
| Get a model | ollama pull <name>; models are stored under C:\Users\<you>\.ollama\models on Windows (change with OLLAMA_MODELS) | -m <file> for a file you have, or -hf <user>/<model>[:quant] to download from Hugging Face |
| Address | http://localhost:11434; binds to 127.0.0.1 only unless you set OLLAMA_HOST | Listens on 127.0.0.1; the README I read gives port 9931 as the default, so pass --port explicitly rather than rely on it |
| OpenAI-style base URL | http://localhost:11434/v1/; a key is required by some client libraries but ignored | /v1/chat/completions and /v1/models under its address; --api-key turns authentication on |
| Context size | Default depends on GPU memory: 4k tokens under 24 GiB of VRAM, 32k for 24-48 GiB, 256k for 48 GiB or more; change with OLLAMA_CONTEXT_LENGTH | -c / --ctx-size; 0 (the default) loads the size from the model |
| Readiness | Check GET /v1/models; ollama ps shows loaded models and the CPU/GPU split | /health returns 503 "Loading model" until ready, then 200 |
| Loading behaviour | Models stay loaded 5 minutes by default (OLLAMA_KEEP_ALIVE); one request at a time per model by default (OLLAMA_NUM_PARALLEL); requests queue up to 512 then get a 503 | Runs the model you start it with |
Ollama's OpenAI-compatibility page lists the request fields it supports on /v1/chat/completions (including messages, max_tokens, stream,
tools, temperature and seed) and the ones it does not (tool_choice, n, user and logit_bias, among others). Our provider
sends only model, messages and max_tokens, so it is within the supported set. The page also lists a /v1/responses endpoint (stateless only), a reminder that
the "OpenAI-compatible" surface keeps growing and differs at the edges between servers.
The commands you would run, from the documentation and not run here:
The Code: A Local Provider and a Startup Check
A local provider is an OpenAICompatProvider with three sensible defaults: no key, Ollama's address, and a long timeout, because the first request after the server starts (or
after it unloaded the model) includes loading the model into memory, which can take far longer than a normal reply. list_models asks the server what it offers, which doubles as a "is it
running?" check:
The check against the scripted server (it answers with the OpenAI shape, {"data": [{"id": ...}]}), and against a real address where nothing is listening, which is the situation on this machine:
The second result is the useful one: with no server, the bot gets a typed ProviderUnavailable with a message saying where it looked, instead of a stack trace from deep inside an HTTP library.
Checking at startup turns "the first user finds out the model server is off" into "the operator finds out before anyone is waiting". Exercise 2 builds that check.
Two design notes. First, the context-size defaults above matter for chapter 4: a conversation that is longer than the server's context window cannot all be sent to the model, so the bot must budget
history by tokens against the actual limit of the running server, not an assumed one (I have not verified what Ollama does when a prompt exceeds the window; read its documentation for your version
before relying on any behaviour). Second, the provider's contract tests from chapter 2 apply unchanged, because local_provider is an OpenAICompatProvider: the suite is now 45 tests, all passing.
Comparing a Local Model With a Hosted One, Honestly
"Is the local model good enough?" is only answerable with your prompts, so build a small harness once and reuse it. This one sends each prompt to every provider with no history and times the call:
Here it is running on two fake providers with scripted delays (50 ms and 400 ms), to show the harness and the shape of its output. The labels say so deliberately; these are not the speeds of any real model:
When you run it for real, a few rules keep the numbers honest:
- Exclude the first call. A local model's first request includes loading it, so it is much slower than the rest (the idea behind Exercise 3's warm-up).
- Repeat and take the median, and look at the minimum and maximum. One run says very little.
- Same prompts, same system prompt, same
max_tokensfor every provider. - Record the conditions: the model name and size, the quantisation, the context size, CPU or GPU, and what else was running.
- Judge quality separately from speed: read the answers side by side without looking at which provider wrote which, and decide against your bot's actual job.
- Tokens per second is not comparable across servers' tokenisers. Different models count tokens differently, so compare time for the same task as well.
Hands-On Exercises
Use bot.sizing to print the table for a 16 GB machine and for your own machine's RAM (look it up with the PowerShell command in this chapter). Compute the weights for a 12B model at 4-bit and 8-bit. Then say, in your own words, three things the table does not include that you would still need memory for.
Write require_model(base_url, model): it asks the server for its models, exits with a helpful message if the server cannot be reached, and exits with a different helpful message if the server is up but does not have the model you want. Run it against the scripted server (model present, model missing) and against an address with no server.
Extend the harness into benchmark(label, provider, prompts, runs=3): do one warm-up call that is not counted, then run the prompts several times and report the median, minimum and maximum tokens per second. Run it on two fake providers with different delays. Explain why the warm-up matters for a real local model.
Chapter 3 Quick Reference
- A local model is the same
OpenAICompatProviderwith a differentbase_url:local_provider(model)= Ollama'shttp://localhost:11434/v1, no key, 300 s timeout (the first call loads the model) - Reasons for: privacy, cost per message, offline use, control, learning. Costs: capability and speed on laptop-sized models (a general expectation; measure your own), setup and operations
- Weights memory ≈ parameters × bits ÷ 8: 7B is 14 GB at 16-bit and 3.5 GB at 4-bit; a lower bound, since context and runtime need more
- This machine: 29.8 GB RAM, Ryzen 7 8745H, integrated Radeon 780M (not in Ollama's listed supported GPUs; expect CPU), no NVIDIA, 457 GB free
- Ollama (
11434,ollama pull,ollama ps, key ignored, context default 4k under 24 GiB VRAM, models unload after 5 minutes, one request at a time per model by default); llama.cpp (llama-server,-m/-hf,--api-key,/health503 until loaded) - Check the server at startup (
list_models): a clearProviderUnavailablebeats a stack trace - Honest comparison: exclude the warm-up, repeat and take the median, same prompts and settings, record the conditions, judge quality separately
- Check the current docs for the version you use before repeating a warning