Contents
- Start here
- Part 1 · Run Qwen locally
- What a local model is
- Local models and your use case
- Which Qwen
- Hugging Face
- Install Ollama
- Running on an NVIDIA PC
- Tokens, context and the KV cache
- Thinking mode
- Other runtimes
- Faster and smaller
- Benchmark your machine
- Use it from code
- Keep it safe
- Learn more
- Part 2 · Build on it
- Embeddings
- Retrieval (RAG)
- A vector store (Qdrant)
- Shared memory (MCP)
- Harden it
- Route the work
- A build order
- Learn more
- Part 3 · Fine-tune
- What fine-tuning changes
- One learning step
- LoRA and QLoRA
- Will it fit?
- Decide before you train
- Build the dataset
- Path A: Mac (mlx-lm)
- Path B: NVIDIA (Unsloth)
- Evaluate honestly
- Serve and ship
- Beyond SFT
- Quiz
- Learn more
- Part 4 · Abliteration
- What abliteration is
- What research found
- What it costs
- Responsibility and safety
- How people do it locally
- Evaluate
- Learn more
- Reference
- Free learning path
- Troubleshooting
- Glossary
- Sources
- Caveats
Start here
Before you begin
Written October 2026 for Qwen3.5 (0.8B / 2B / 4B / 9B), on an Apple Silicon Mac or a PC with an NVIDIA GPU. Every command, version and number was checked against a primary source that month: Ollama's and Hugging Face's documentation, PyPI, the mlx-lm and Unsloth docs, model cards, arXiv. Anything else is labelled secondary (a blog or forum) or untested. Fast-moving facts go stale, so re-check versions before you rely on them.
Who this is for. Anyone who wants a capable language model on their own machine and wants to understand what they're running. No paid API is involved anywhere, and nothing you do here leaves your computer.
How it's organised. Four parts, in the order you'd do them, plus a reference section. If you only want the next step for your machine, skip to Pick your path below.
- Part 1 — Run Qwen locally. What a local model is, whether local suits your use case, how Qwen compares with other families, what Hugging Face is, and how to install and run a model on a Mac, Linux or Windows.
- Part 2 — Build on it. Embeddings, retrieval over your own notes (RAG), a real vector store, memory shared between agents over MCP, hardening, and routing work between a local model and a hosted one.
- Part 3 — Fine-tune. When fine-tuning is the right tool, how LoRA works, what fits in your memory, how to build a dataset, train on a Mac or an NVIDIA PC, and evaluate honestly.
- Part 4 — Abliteration. What refusal removal is, what the research found, what it costs, the responsibility you take on, and how people do it locally.
Each part ends with a Learn more list of the resources it was built from. The Reference section at the end has a free learning path from zero to understanding how LLMs work, with Karpathy's courses at its core, plus troubleshooting, sources and caveats.
Pick your path
Find your hardware, run the first model it lists, then read the sections in the last column. The memory rule tells you how much of your memory a model can really use. On a Mac, the system shares that memory, so keep about a quarter free. On a discrete GPU, the system has its own RAM, so keep about 1 GB of VRAM plus room for the KV cache, which grows with context length.
| Your hardware | Memory rule | First model to run | Runtime | Read next |
|---|---|---|---|---|
| Mac, 16 GB | About 12 GB usable | qwen3.5:4b (3.3–4.0 GB), then qwen3.5:9b (6.6–7.6 GB) | Ollama | Install Ollama, Which Qwen?, Tokens, context and the KV cache |
| Mac, 24–32 GB | About 18–24 GB usable | qwen3.5:9b; on 32 GB, try Qwen3.8-27B at 4-bit (18 GB) | Ollama | Which Qwen?, Tokens, context and the KV cache, Benchmark your machine |
| Mac, 64 GB+ | About 48 GB usable at 64 GB | Qwen3.8-27B at 4-bit, or Qwen3.6-35B-A3B (22.1 GB at Q4) | Ollama or LM Studio (MLX) | Which Qwen?, Faster and smaller, Benchmark your machine |
| NVIDIA, 8–16 GB | VRAM minus about 1 GB, minus the KV cache | qwen3.5:4b on 8 GB, qwen3.5:9b on 12–16 GB | Ollama, then llama.cpp for MoE offload | Install Ollama, Running on an NVIDIA PC, Faster and smaller |
| RTX 3090 / 4090, 24 GB | About 23 GB, minus the KV cache | Qwen3.8-27B Q4_K_M (19 GB, ~4-6 GB left) | Ollama, then llama.cpp | Running on an NVIDIA PC, Tokens, context and the KV cache, Benchmark your machine |
| Two RTX 3090s, 48 GB | About 46 GB (1 GB off each card), minus the KV cache, split across both cards | Qwen3.8-27B Q8_0 (28.6 GB) or Qwen3.6-35B-A3B Q8_0 (36.9 GB) | llama.cpp layer split, or vLLM on Linux | Running on an NVIDIA PC, Benchmark your machine |
| AMD, Intel Arc or CPU-only | Same as discrete GPU; CPU-only uses system RAM | qwen3.5:4b, or qwen3.5:2b on CPU | Ollama | Install Ollama (this guide covers these briefly) |
Ollama supports AMD cards through the ROCm v7 driver on Linux, and on Windows through a ROCm v7 / HIP7 driver stack for the Radeon RX 7600 to 7900 and PRO W7500 to W7900. Other GPUs, Intel included, can use Ollama's Vulkan backend on Windows and Linux. With no usable GPU, Ollama runs on the CPU, which works but is slow. The rest of this guide is tested against Apple Silicon and NVIDIA only.
Or start from what you want to do:
| Your goal | Start here |
|---|---|
| Chat with a local model | Install Ollama, then Thinking mode |
| Write and complete code | Which Qwen? for the coder models, then Use it from code |
| Search and answer over your notes (RAG) | Embeddings, then Retrieval (RAG) and A vector store (Qdrant) |
| Agents and tool calling | Use it from code, then Shared memory (MCP) and Harden and route |
| Fine-tune on your own data | What fine-tuning changes, then Will it fit? |
| Understand the theory | Free learning path, then Tokens, context and the KV cache |
Three rules this guide keeps coming back to:
- Run it before you change it. Most "the model can't do X" problems are solved with a better prompt, retrieval or a different model, not with training or surgery.
- Test on your own task. A small set of real inputs with the answers you'd accept, written before you start, beats every leaderboard and every training-loss curve.
- Own what you ship. A local model is yours: its privacy, its mistakes, and anything you removed from it.
Part 1 · Run Qwen locally
What you're actually running
A language model is a file of numbers, called weights, plus a small program that uses them to predict the next token of text, one token at a time. Running it locally means that file sits on your disk and that program runs on your own CPU or GPU. Nothing leaves the machine unless you send it somewhere.
Three facts decide almost everything about running one well:
Memory sets the ceiling. The whole model has to be in memory while it runs. The size on disk is roughly parameters × bits ÷ 8: a 4B model at 16-bit is about 9 GB; at 4-bit, about 2.5 GB. Add a gigabyte or two for the working memory that grows with the length of the conversation (the KV cache).
Memory bandwidth sets the speed. For every generated token, the program reads the entire model from memory. So tokens per second is roughly memory bandwidth divided by model size. A Mac mini with the M4 chip (2024) reads about 120 GB/s, so a 6.6 GB model tops out near 18 tokens per second there; the current base Mac mini (M6) reads 153 GB/s, which lifts that ceiling to about 23. Real runs land a bit below either figure. A discrete GPU is several times faster because its memory is.
Quantization is what makes it fit. Models are trained in 16-bit. Storing the weights in 4-bit cuts the file to about a quarter with a small quality loss. Q4_K_M is the common default. Sizes like Q8_0 keep more quality at twice the size. Below 4-bit, quality drops quickly. A smaller model at 4-bit is almost always better than a bigger model crushed to 2-bit.
Take the model's parameter count in billions and multiply by 0.6. That's roughly the gigabytes a 4-bit copy needs, before the working memory. Then leave a quarter of your RAM free for the operating system.
Decode-speed ceiling
Every generated token reads the whole model from memory, so tokens per second ≈ memory bandwidth ÷ model size.
A ceiling, not a measurement. Dense-model benchmarks on a base M4 reach roughly 77–88% of this figure, so plan for less. No direct, attributable Qwen3.5 benchmark on a base M4 16 GB had been published as of October 2026. Measure your own machine with real prompts.
Part 1 · Run Qwen locally
Local models as a whole: is local right for your use case?
This guide is about Qwen, but the first decision is bigger than any one model family. Running a model locally is a trade. You give up the strongest models and the convenience of a hosted API. You get privacy, no per-token cost, no rate limits, offline use, and a system you fully control. Whether that trade is worth it depends on what you're doing.
Local is a clear win when:
- the data is private: health notes, client files, a codebase you can't upload, your own journal;
- the work is high-volume and repetitive: classifying thousands of emails, tagging a photo library, summarising transcripts, where API costs add up;
- you need it offline, or on a schedule, or inside a tool that must keep working when a vendor changes its terms;
- latency matters more than depth: autocomplete, quick rewrites, extraction into JSON;
- you're learning: nothing teaches how these systems work like running, breaking and tuning one yourself.
A hosted frontier model is still the right call when:
- the task needs the best reasoning available: hard maths, long multi-step agent work, subtle judgement calls where being wrong costs real money;
- you need very long documents handled reliably in one pass;
- you'll use it rarely. The setup cost of local isn't worth it for ten prompts a month.
Many good setups use both: a local model for the private, repetitive and fast work, and a hosted model for the hard calls. Part 2 builds a three-tier version of this: an always-on local box, a GPU machine on your network, and a hosted model for the hard calls.
Write your use case down in one sentence before choosing anything. "Summarise my meeting transcripts into action items, offline, on my laptop" points at a different model and runtime than "help me write Rust in my editor" or "answer questions over 2,000 PDFs."
The local landscape, October 2026
Qwen is one family among many, and "open weights" no longer means "runs on your laptop." The smallest current GLM-5.x is 320B parameters; Kimi K3 is 2.8 trillion. So this section is two tables: what fits a laptop, and the open-weight frontier you reach through an API. Every row was checked against the publisher's model card or Ollama's library page on 2026-10-15. The sizes are 4-bit downloads as Ollama or the official GGUF repo lists them.
Fits a laptop (16 GB with headroom, or 24 GB):
| Family | Publisher | Laptop sizes (4-bit) | License | Released | Notes |
|---|---|---|---|---|---|
| Qwen3.5 0.8B / 2B / 4B / 9B | Alibaba | 1.2 / 2.7 / 3.3 / 6.6 GB; 27B at 17 GB needs 24 GB | Apache-2.0 | Feb–Mar 2026 | 200+ languages, text + image input, 256K context, best small tool-caller. This guide's pick for 16 GB. |
| Qwen3.8-27B | Alibaba | 18 GB, 24 GB machines only | Apache-2.0 | Aug 2026 | The newest Qwen that fits anything portable: dense, text + image, 256K. Supersedes Qwen3.5-27B and Qwen3.6-27B (17 GB) on a 24 GB machine. |
| Gemma 4 E2B / E4B / 12B / 26B-A4B / 31B | 4.3–4.6 / 6.1–6.6 / 7.2–8.0 GB; 26B-A4B and 31B at 16–20 GB need 24 GB | Apache-2.0 | Mar–Jun 2026 | E2B, E4B and the 12B take text, image and audio; 26B-A4B and 31B are text + image. 128K context on the small ones, 256K above. Quantization-aware checkpoints hold quality at 4-bit. | |
| gpt-oss-20b | OpenAI | 14 GB; tight on 16 GB, fine on 24 GB | Apache-2.0 | Aug 2025 | 21B total, 3.6B active, adjustable reasoning effort, text only, 128K. Still OpenAI's only laptop-size open model; no successor as of Oct 2026. |
| Ministral 3 3B / 8B / 14B | Mistral | 3.0 / 6.0 / 9.1 GB | Apache-2.0 | Dec 2025 | Base, Instruct and Reasoning at each size, 256K, text + image, function calling. Mistral's 2026 open releases (Small 4 at 119B, Medium 3.5 at 128B) are too large for a laptop, so this is still its laptop line. |
| Muse Glimmer 30B | Meta | 18 GB, 24 GB machines only | Apache-2.0 | Aug 2026 | Meta's current open model, distilled from its closed Muse Spark; ~29.6B dense, text + image, 131K. Meta has shipped no Llama since Llama 4 (Apr 2025), whose smallest member is 67 GB at 4-bit. |
| Granite 4.2 3B / 8B / 30B | IBM | 2.2 / 5.3 GB; 30B at 17–18 GB needs 24 GB | Apache-2.0 | Aug 2026 | Dense reasoning family with thinking and non-thinking modes, 128K native, tool calling. Built for retrieval and tool use; small and well-behaved. |
| LFM2.5 8B-A1B / 24B-A2B | Liquid AI | 5.2 GB; 24B-A2B at 15 GB needs 24 GB | LFM Open License 1.0 (custom) | May–Oct 2026 | Sparse mixture-of-experts built for speed on CPUs and laptops (1.5B and 2.45B active), 128K, text only. The 24B-A2B shipped 2026-10-12, the newest laptop-class model in this table. |
| Nemotron 3 Nano 4B | NVIDIA | 2.8 GB | NVIDIA Nemotron Open Model License | Mar 2026 | 256K context, tool calling. The 30B-A3B sibling is 24 GB at 4-bit and needs a 32 GB machine. Licenses differ across the Nemotron line, so read each card. |
| Phi-4 mini 3.8B / 14B / reasoning-vision 15B | Microsoft | 2.5 / 9.1 GB; vision model ~10 GB + projector | MIT | Dec 2024 – Mar 2026 | The reasoning-vision 15B (Mar 2026, 16K context) is the newest Phi; there is no Phi-5 as of Oct 2026. |
| GLM-4.7-Flash 30B-A3B | Z.ai (Zhipu) | 19 GB, 24 GB machines only | MIT | Jan 2026 | The only GLM with a local Ollama tag; 198K context, text, tools and thinking. Everything in the GLM-5.x line is far too large (see below). |
| DeepSeek-R1-0528-Qwen3-8B | DeepSeek | 5.2 GB | MIT | May 2025 | The only DeepSeek-branded model that fits a laptop: R1 reasoning distilled onto Qwen3-8B, text only, 128K. Every DeepSeek V4-generation model is 284B or larger. |
| Qwen2.5-Coder 7B / 14B, Qwen3-Coder-30B-A3B | Alibaba | 4.7 / 9.0 GB; 30B-A3B at 19 GB needs 24 GB | Apache-2.0 | Nov 2024 / Jul 2025 | Both document fill-in-the-middle, which newer agentic coders (Qwen3.8-27B, Granite 4.2, Devstral Small 2) don't, so they remain the editor-autocomplete picks. |
| SmolLM3-3B | Hugging Face | 1.9 GB (GGUF from ggml-org; not in Ollama's library) | Apache-2.0 | Jul 2025 | Fully open training recipe, 128K, strong tool calling for its size. A reference model rather than a daily driver. |
The open-weight frontier (you can download these, but you run them through an API or a datacenter):
| Model | Publisher | Size | License | Released | Why a laptop user should know it |
|---|---|---|---|---|---|
| Kimi K3 | Moonshot AI | 2.8T total, 104B active, 896 experts | Kimi K3 License (custom, MIT-derived) | Jul 2026 | The largest open-weight model that exists: 1M context, text + image. About 1.5 TB at 4-bit. Shows what "frontier" means now. |
| Qwen3.8-2.4T-A95B | Alibaba | 2.4T total, 95B active | Qwen3.8-Max License (custom) | Aug 2026 | The open checkpoint of hosted Qwen3.8-Max, text only, thinking always on. Same generation as the laptop-size Qwen3.8-27B. |
| DeepSeek-V4.1-Flash | DeepSeek | 552B backbone, 8B/16B active | MIT | Sep 2026 | DeepSeek's current release: text + image, 1M context. The model its API now serves for everything. |
| GLM-5.3 | Z.ai (Zhipu) | 753B (about 40B active, per the GitHub README) | GLM-5.3 License (custom) | Aug 2026 | Z.ai's flagship coding and agent model, text only, 1M context. The first GLM to leave the MIT license. |
| GLM-5.3-Flash | Z.ai (Zhipu) | 320B total, 18B active | MIT | Aug 2026 | Natively multimodal. "Flash" no longer means laptop-class: about 200 GB at 4-bit. |
| Mistral Large 4 | Mistral | 1T total, 52B active | not stated | Oct 2026 | Announced 2026-10-06 with weights promised "by the end of the month." Not open as of 2026-10-15. Mistral Large 3 (675B, Apache-2.0, Dec 2025) is the current download. |
| MiniMax-M3 | MiniMax | ~428B total, ~23B active | MiniMax Community License (custom; restrictions on commercial use) | Jun 2026 | Natively multimodal, 1M context. Its license is stricter than the earlier M2 line's. |
| gpt-oss-120b | OpenAI | 117B total, 5.1B active | Apache-2.0 | Aug 2025 | 65 GB at 4-bit: a 64 GB-plus Mac or an 80 GB GPU. The big sibling of the laptop-size 20B. |
| Llama 4 Scout / Maverick | Meta | 109B / 400B total, 17B active | Llama 4 Community License | Apr 2025 | Still the newest Llama. Scout is 67 GB at 4-bit. Useful mainly as the anchor for Llama-format tooling. |
| Nemotron 3 Ultra | NVIDIA | 550B total, 55B active | OpenMDW 1.1 | Jun 2026 | NVIDIA's frontier open model with open data and recipes. Its license differs from the Nano line. |
| Mistral Small 4 | Mistral | 119B total, 6.5B active | Apache-2.0 | Mar 2026 | Despite "Small," about 58–74 GB at 4-bit. Merges Mistral's reasoning, vision and coding lines. |
| Qwen3-Coder-Next | Alibaba | 80B total, 3B active | Apache-2.0 | Feb 2026 | Fill-in-the-middle agentic coder at 52 GB: runs on a 64 GB desktop or Mac Studio, not a laptop. |
Why the frontier matters even if you can't run it. Three reasons. The frontier models are the API fallback for the hard calls a local model can't make. Their labs often ship a small sibling trained the same way (Qwen3.8-27B next to the 2.4T, Gemma 4 E2B next to the 31B). And their licenses are drifting away from MIT and Apache toward custom terms with revenue clauses, which matters the moment you build something on one.
Qwen4 is described as "in training" in secondary reports, with no date. NVIDIA announced a Nemotron 3.5 Bolt 74B-A9B for mid-October. JetBrains published Mellum2.1, a 12B-A2.5B coding model, on 2026-10-07 with an official GGUF at 7–8 GB; its benchmark numbers are self-reported. Treat all three as leads until their model cards exist and you've read them.
There are more families than this, and new releases arrive weekly. Treat any table like this one, including this one, as a dated snapshot, and read the model card before you commit to a family.
How to choose
- Memory first. List what you have: RAM on a Mac, GPU memory on a PC. On a Mac, unified memory is shared with the system, so keep about a quarter free. On a discrete GPU, keep about 1 GB of VRAM free plus room for the KV cache, which grows with context length. That number eliminates most of the table before you think about quality.
- Task type. Tool calling and structured output favour Qwen3.5-9B. Reasoning-heavy chat favours gpt-oss-20b if it fits. Editor autocomplete needs fill-in-the-middle (Qwen2.5-Coder). Images need a vision-capable model (Qwen3.5, Gemma 4, Ministral 3).
- Language. If you work in a language other than English, Qwen's multilingual training is a real advantage. Check the model card for the languages it lists.
- License. Apache-2.0 means no surprises. Other licenses may restrict commercial use, user counts or redistribution.
- Ecosystem. Does your runtime support it today? Does the fine-tuning tool? Do community quantizations exist? A slightly weaker model with full support beats a stronger one you can't run.
- Then test it on your task. Write ten to thirty real inputs with the answers you'd accept. Run each candidate with the same prompt. Score them. This takes an hour and it's the only step that measures what you care about. Part 3 reuses exactly this test set if you go on to fine-tune.
Public leaderboards measure general tasks on public data, and some have known defects. A model that scores five points higher on a benchmark can be worse on your inputs. Your own small test set outranks every leaderboard for your use case.
Part 1 · Run Qwen locally
Which Qwen?
Qwen is Alibaba's family of open-weight models. The useful line for a personal machine in October 2026 is Qwen3.5, released in February and March 2026. Its small models are 0.8B, 2B, 4B and 9B. All are Apache-2.0 licensed, take text and images as input, and have a 256K-token context window. Newer lines (Qwen3.6, Qwen3.8) only ship at 27B and larger, so Qwen3.5 is still the right pick for a 16 GB machine. On a 24 GB machine, Qwen3.8-27B (18 GB at 4-bit) is the newer choice.
| Model | 4-bit size on Ollama | Fits comfortably in | Good for |
|---|---|---|---|
| Qwen3.5-0.8B | 1.2–1.3 GB | anything, even a phone-class device | classification, quick drafts, the draft model for speculative decoding |
| Qwen3.5-2B | 2.7–3.1 GB | 8 GB | fast extraction and summaries; first fine-tuning experiments |
| Qwen3.5-4B | 3.3–4.0 GB | 8 GB, with headroom at 16 GB | the everyday local assistant; strong on structured output |
| Qwen3.5-9B | 6.6–7.6 GB | 16 GB | the quality pick; best local tool-calling at this size |
| Qwen3.5-27B | 17–20 GB | 24 GB | dense, not mixture-of-experts; on a 24 GB machine prefer Qwen3.8-27B (18 GB, Aug 2026) |
The sizes are what Ollama downloads for its default tags, which include the vision encoder. Larger Qwen3.5 models (35B-A3B, 122B-A10B, 397B-A17B) are mixture-of-experts and need workstation memory.
Pick by task, not by ego. Run your real prompts through the 4B first. If it does the job, you've saved memory for context and other programs. Move to the 9B when answers need more reasoning or when tool-calling has to be reliable. Tool-calling reliability drops sharply below 9B: on the BFCL benchmark the 9B scores about 66, the 4B about 50, the 2B about 44. Epoch AI rated that benchmark "Flawed" in September 2026, so treat the gap as a ranking, not a measurement.
They write a <think> block before answering. That's good for hard questions and slow for everything else. Only the 0.8B and 2B default to non-thinking. That's what the models' own chat templates do; Unsloth's Qwen3.5 page describes all four as reasoning-off by default, so check the think setting on your runtime rather than assuming. Section "Thinking mode" below shows how to switch it.
Other Qwen models you'll see. Qwen2.5-Coder (7B, 14B, 32B) is older but still the best Qwen for editor autocomplete, since it supports fill-in-the-middle. Qwen3-VL is a vision line. Qwen3-Embedding-0.6B and Qwen3-Reranker are for search and retrieval. Qwen3Guard is a safety classifier. None of these are needed to get started.
Part 1 · Run Qwen locally
Hugging Face: where the models live
What it is. Hugging Face is a company and a website, usually called "the Hub." It hosts open model weights, datasets and demos the way GitHub hosts code. It also maintains the Python libraries most of the ecosystem is built on: transformers for running models, datasets for data, peft for LoRA adapters and trl for fine-tuning. When someone says "the model is on Hugging Face," they mean a Git-like repository on the Hub containing the weight files.
Reading a model page. A model repo such as Qwen/Qwen3.5-4B has three parts worth reading:
- The model card is the README. It states the license, the architecture, the context length, benchmark numbers and how to run it. Read the license first.
- Files and versions lists the actual weights. The sidebar's parameter count is sometimes wrong for packed formats, so trust the card, not the sidebar.
- The organisation tells you who published it.
Qwenis the official account.unsloth,mlx-community,bartowskiandmradermacherare well-known community accounts that re-package official weights into other formats. Anyone can upload anything, so treat an unfamiliar account the way you'd treat an unfamiliar download site.
The three file formats you'll meet.
- safetensors is the standard format for full-precision weights. It's what Hugging Face libraries, Unsloth and Heretic load, and it can't execute code when opened. Prefer it over the older
.binor.ptpickle files, which can. - GGUF is the format for llama.cpp, Ollama and LM Studio. One file per quantization level:
Q4_K_M,Q8_0,F16. This is what you run on a laptop. - MLX repos (usually under
mlx-community) are weights converted for Apple's MLX framework, in 4-bit, 8-bit or bf16. This is whatmlx-lmloads on a Mac.
The official Qwen account publishes safetensors. The GGUF and MLX versions come from the community accounts above, and the Qwen docs point to them.
The hf command. Hugging Face's command-line tool is called hf. Install it with one of:
curl -LsSf https://hf.co/cli/install.sh | bash # macOS and Linux brew install hf # macOS with Homebrew pip install -U huggingface_hub # any Python environment
On Windows, use PowerShell: powershell -ExecutionPolicy ByPass -c "irm https://hf.co/cli/install.ps1 | iex". Then:
hf download Qwen/Qwen3.5-4B # whole repo into the cache hf download unsloth/Qwen3.5-4B-GGUF --include "*Q4_K_M*" --local-dir ./models # one quant, into a folder hf download Qwen/Qwen3.5-4B config.json # a single file
Downloads go to a cache under ~/.cache/huggingface/hub unless you pass --local-dir. Big models fill disks quickly, so know where that folder is.
Accounts and tokens. You don't need an account to download public models like Qwen. Some models are "gated," which means the publisher asks you to accept terms on the model page first. Those, and private repos, need hf auth login with a token from your account settings. Never paste a token into a shared notebook or commit it to a repo.
Running a Hub GGUF straight from Ollama. You don't have to download by hand. Ollama can pull a GGUF repo directly:
ollama run hf.co/unsloth/Qwen3.5-4B-GGUF:Q4_K_M
The part after the colon picks the quantization file from the repo; without it Ollama takes Q4_K_M when the repo has one. Each GGUF model page on the Hub also offers an "Use this model → Ollama" menu that writes the command for you. This is handy for community builds that aren't in Ollama's own library, including fine-tuned and abliterated variants, and it's exactly where the trust question above matters most.
Licenses. Qwen3.5's small models are Apache-2.0: you can use, modify and redistribute them, including commercially, if you keep the license notice. Other model families differ. Llama has its own community license. Gemma 4 and Meta's Muse Glimmer are Apache-2.0. Licenses diverge inside the Qwen3.8 line: the 27B is Apache-2.0, the 2.4T-A95B uses the Qwen3.8-Max License, and Flash-Next uses the Qwen Community License 1.0. GLM-5.3, Kimi K3 and MiniMax-M3 ship under custom licenses rather than MIT. The model card tells you. Derivatives you publish inherit the base model's terms, so name the base model in your own card.
Part 1 · Run Qwen locally
Install Ollama and run your first Qwen
Ollama is the easiest runtime on all three platforms. It wraps llama.cpp (and, on Apple Silicon, Apple's MLX: a preview since v0.19 in March 2026, the default for supported models since v0.40.0 in September), serves a local HTTP API on port 11434, and manages model files for you.
Install:
brew install ollama # macOS (or download the app from ollama.com) curl -fsSL https://ollama.com/install.sh | sh # Linux
On Windows, download the installer from ollama.com/download/windows. The macOS and Windows apps run the server in the background. On Linux, the installer sets up a service; if it isn't running, start it with ollama serve.
GPU requirements. NVIDIA GPUs need compute capability 5.0 or newer and driver 550 or newer (570 or newer for compute capability 5.0 to 6.2). AMD GPUs need the ROCm v7 driver on Linux, or the ROCm v7 / HIP7 stack on Windows for the Radeon RX 7600 to 7900 and PRO W7500–W7900 cards. Other GPUs on Windows and Linux can use Vulkan. Apple Silicon uses Metal with no setup. If Ollama can't see a GPU it falls back to the CPU, which works but is slow.
First run:
ollama pull qwen3.5:4b ollama run qwen3.5:4b "Explain unified memory in one paragraph."
Always write the tag. A bare ollama run qwen3.5 pulls the 9B, about 7 GB, because latest points at it.
Check that it works:
ollama --version # expect the latest release (v0.40.x in Oct 2026) ollama list # models on disk ollama ps # what's loaded now, and whether it's on the GPU curl -s http://127.0.0.1:11434/api/tags # the server answers on loopback
Watch your system's memory monitor during the first run. If the machine starts swapping, the model is too big for comfort. Drop to a smaller one.
Housekeeping. Models live under ~/.ollama/models on macOS, /usr/share/ollama/.ollama/models for the Linux service, and %USERPROFILE%\.ollama\models on Windows; set OLLAMA_MODELS to move them. ollama rm qwen3.5:9b deletes one. Budget 10–12 GB of disk for a 4B, a 9B and an embedding model.
Settings on a Mac
Ollama reads its settings from environment variables. How you set them depends on your OS. The block below is the set for a 16 GB Mac. Set each one with launchctl setenv, then quit and reopen Ollama from the menu bar:
launchctl setenv OLLAMA_HOST 127.0.0.1:11434 # loopback only; never 0.0.0.0 launchctl setenv OLLAMA_FLASH_ATTENTION 1 # smaller KV cache; needed for the next line launchctl setenv OLLAMA_KV_CACHE_TYPE q8_0 # halves KV-cache memory, tiny quality cost launchctl setenv OLLAMA_MAX_LOADED_MODELS 1 # one model resident at a time launchctl setenv OLLAMA_NUM_PARALLEL 1 # don't multiply the KV cache launchctl setenv OLLAMA_KEEP_ALIVE 5m # unload after five idle minutes
On a Mac with more memory, the first line is the only one that matters.
Settings on Linux
The Linux installer runs Ollama as a systemd service, so the variables go in a service override. Open one:
sudo systemctl edit ollama.service
Add the variables under a [Service] header, one Environment= line each, then save and exit:
[Service] Environment="OLLAMA_HOST=127.0.0.1:11434" Environment="OLLAMA_FLASH_ATTENTION=1" Environment="OLLAMA_KV_CACHE_TYPE=q8_0"
Reload systemd and restart Ollama so it picks them up:
sudo systemctl daemon-reload && sudo systemctl restart ollama
Settings on Windows
Ollama on Windows inherits your user environment variables.
- Quit Ollama from its icon in the taskbar tray.
- Open Settings (Windows 11) or Control Panel (Windows 10), search for "environment variables", and pick "Edit environment variables for your account". Add each variable there. Or set them from a terminal with
setx, which writes user variables by default:setx OLLAMA_HOST 127.0.0.1:11434 setx OLLAMA_FLASH_ATTENTION 1 setx OLLAMA_KV_CACHE_TYPE q8_0
- Start Ollama again from the Start menu.
setxvalues only reach programs started after you run it, so open a new terminal too.
A starting set for a 24 GB NVIDIA card
On an RTX 3090 or another 24 GB card, the limits are different. Weights for a 27B at Q4 take about 19 GB, so the KV cache is what you budget. Put these in the Linux override or the Windows variables above:
OLLAMA_HOST=127.0.0.1:11434 # loopback only; never 0.0.0.0 OLLAMA_FLASH_ATTENTION=1 # Ollama turns it on when supported; forcing it is safe OLLAMA_KV_CACHE_TYPE=q8_0 # about half the KV memory of f16; needs flash attention OLLAMA_CONTEXT_LENGTH=32768 # the FAQ default is 4096; check yours with ollama ps CUDA_VISIBLE_DEVICES=GPU-xxxxxxxx # pin a card; get the UUID from nvidia-smi -L
32K is a starting point, not a measured limit. For Qwen3.6-27B, the KV cache costs 64 KiB per token at f16, which is 2 GiB per 32K tokens (derived), and q8_0 halves that. Raise the context in steps and watch ollama ps: if the split stops showing 100% GPU, you went too far. Use the GPU's UUID rather than its number in CUDA_VISIBLE_DEVICES, because Ollama's docs say numeric ordering may vary. Leave that line out on a single-GPU box.
OLLAMA_KV_CACHE_TYPE applies to every model the server loads, and it only takes effect with flash attention on. If one model misbehaves with q8_0, go back to f16 for all of them.
Windows: native or WSL2?
Run Ollama natively on Windows unless you need a Linux-only tool. vLLM and SGLang are the common reasons: vLLM runs on Windows only through WSL2. If you do use WSL2 with an NVIDIA card, follow NVIDIA's rules:
- Install only the normal Windows NVIDIA driver. Never install a Linux display driver inside WSL. The Windows driver already exposes the GPU to it.
- If you build CUDA code inside WSL, install the WSL-Ubuntu CUDA toolkit or the
cuda-toolkit-12-xmetapackage only. Do not install thecuda,cuda-12-xorcuda-driversmetapackages there. - Use Windows 11. It needs no Insider build. On Windows 10, NVIDIA's guide still points you to the Windows Insider Program.
- Docker inside WSL2 supports only
--gpus all, not picking single GPUs.
Since v0.40.0, Ollama runs supported models (Qwen3.5, Qwen3.6, Qwen3.8 and Gemma 4 included) on Apple's MLX by default. The March 2026 preview post said MLX wanted more than 32 GB of unified memory; the v0.40.0 release notes state no memory requirement. What a 16 GB Mac does is not documented. Check ollama ps and the server log to see which runner loaded, and compare speed against a -q4_K_M tag if in doubt.
Part 1 · Run Qwen locally
Running on an NVIDIA PC: from an RTX 3060 to an RTX 3090, and two of them
On a PC, the GPU's own memory (VRAM) is the budget. A model that fits in VRAM runs fast. A model that spills into system RAM slows down sharply, because system RAM is several times slower. The RTX 3090 is the card this section centres on: 24 GB of VRAM and 936 GB/s (derived), the same capacity as a 4090.
The cards
| Card | VRAM | Bandwidth GB/s (derived) | Board power | llama.cpp 7B Q4_0 decode, t/s (FA on) |
|---|---|---|---|---|
| RTX 3060 12GB | 12 GB, 192-bit (UNCERTAIN) | 360 | 170 W (UNCERTAIN) | 76.9 |
| RTX 4060 Ti | 8 or 16 GB, 128-bit | 288 | 160/165 W, 550 W PSU | 64.0 (variant not stated) |
| RTX 3090 | 24 GB GDDR6X, 384-bit | 936 | 350 W, 750 W PSU (FE) | 161.9; 177.8 on a Sep 2026 build |
| RTX 3090 Ti | 24 GB, 384-bit | 1008 (UNCERTAIN) | 450 W, 850 W PSU | 172.3 |
| RTX 4090 | 24 GB, 384-bit | 1008 | 450 W (UNCERTAIN) | 189.0 |
| RTX 5090 | 32 GB GDDR7, 512-bit | 1792 | 575 W, 1000 W PSU | 300.4 |
The decode column is the community scoreboard in llama.cpp discussion #15013: llama-bench on a 7B Llama at Q4_0, 128 generated tokens (tg128), flash attention on. It is the cleanest like-for-like comparison there is. Bandwidth is bus width × memory data rate; NVIDIA's pages don't list it, so treat it as derived. Board power and PSU figures come from NVIDIA's product pages.
Two hardware facts matter later. The 3090 has no FP8 tensor cores, so FP8 models run as weight-only (W8A16) there. Only the 5090 has native FP4. GGUF quants run on all of these cards.
What fits on one RTX 3090 (24 GB)
| Model | Type | 4-bit file | Left for KV cache | Context and notes | Decode |
|---|---|---|---|---|---|
| Qwen3.8-27B Q4_K_M | Dense | 19 GB | ~4-6 GB | ~64K at f16 KV or ~128K at q8_0 KV, if its layout matches Qwen3.6 (UNCERTAIN) | Ceiling ~49 t/s (derived); Qwen3.6-27B at Q3_K_XL measured 32-38 (secondary) |
| Qwen3.6-35B-A3B | MoE, 3B active | 22.1 GB Q4; 16.8 GB UD-Q3_K_XL | ~0.5-1.5 GB at Q4 | Use Q3_K_XL for long context, or --n-cpu-moe | 136.8 t/s at 370 W (secondary) |
| Qwen3-Coder-30B-A3B | MoE, 3B active | 18.6 GB Q4_K_M; 17.7 GB UD-Q4_K_XL | ~4-6 GB | Moderate (UNCERTAIN) | Not measured |
| GLM-4.7-Flash Q4_K_M | MoE, 30B-A3B | 18.3 GB | ~4-5 GB | Compact KV (UNCERTAIN) | Not measured |
| gpt-oss-20b | MoE, 20.9B total | 14 GB (Ollama) | ~9-10 GB | Large; 128K UNCERTAIN | Not measured |
| Gemma 4 31B Q4_K_M | Dense, sliding window | 18.3 GB | ~4 GB, minus ~3.6 GB window cache | Tightest: 22.3 GB at 16K (user report); use -np 1 and q8_0 KV | Ceiling ~51 t/s (derived) |
The headline pick is Qwen3.8-27B at Q4_K_M. It leaves room for a useful context window, and a dense 27B is the strongest model that fits comfortably. Pick a 3B-active MoE when you want speed: it reads only the active experts per token, so it decodes several times faster than a dense model of the same file size.
Start it with llama.cpp straight from Hugging Face:
llama-server -hf ggml-org/Qwen3.8-27B-GGUF:Q4_K_M -ngl 99 -fa on -c 32768 --port 8080
-ngl 99 puts every layer on the GPU. -c sets the context window. Raise it until VRAM is nearly full, and use -ctk q8_0 -ctv q8_0 to fit about twice as much.
Only its 16 full-attention layers keep a cache. 16 layers × 4 KV heads × 256 head dim × 2 (K and V) × 2 bytes = 64 KiB per token at f16. That is 2 GiB per 32K tokens. Each extra parallel slot needs its own cache.
Will it fit, and how fast can it go?
Fit is weights + KV cache against usable memory. On a discrete GPU, usable is the VRAM minus about 1 GB. On a Mac, usable is about 75% of unified memory (measured ≈11.8 GiB on a 16 GB machine). Speed ceiling is bandwidth ÷ model size.
Estimates, not measurements. Bandwidths are bus width × data rate (derived), not NVIDIA-published figures. Real decode runs below the ceiling and drops as the context fills; see Benchmark your machine. KV sizes are shown only where the per-token constant is verified (Qwen3.6-35B-A3B: 20 KiB per token at f16, derived from the model config; q8_0 is taken as about half); for other models, leave a few GB of headroom. On two 3090s with the default layer split, each token still crosses one card at a time, so the decode ceiling uses one card's bandwidth, not the sum.
How fast it will be
The ceiling for decode is bandwidth ÷ model bytes. A 3090 reads 936 GB/s (derived), so a 19 GB model tops out near 49 tokens per second. Real runs land below the ceiling. The one cited measurement for a 27B on a 3090 is 32-38 t/s (Qwen3.6-27B at Q3_K_XL, club-3090, secondary). A 3B-active MoE breaks this rule of thumb in your favour: Qwen3.6-35B-A3B measured about 137 t/s on one 3090 (club-3090, secondary).
Decode also falls as the conversation grows, because every token reads the KV cache as well as the weights. On two 3090s, a 27B at Q6 ran ~22.8 t/s near empty context and 4.0 t/s at 95K tokens (sanj.dev, secondary). Measure at the context length you actually use.
Prefill versus decode
Generating a reply has two phases. Prefill reads your whole prompt at once and is limited by compute. Decode writes the answer one token at a time and is limited by bandwidth. Prefill is many times faster per token: a 70B on two 3090s did 393.9 t/s prefill against 16.29 t/s decode (XiongjieDai, May 2024, secondary).
Prefill is what you feel in RAG. A query with 8,000 tokens of retrieved notes spends most of its wait in prefill before the first word appears. When you compare setups for RAG, look at the pp number from llama-bench, not only tg.
Driver floor
Ollama needs an NVIDIA driver of 550 or newer, and 570 or newer for cards with compute capability 5.0 to 6.2. Every card in the table above is compute capability 8.6 or newer. On Windows, Ollama's docs ask for driver 551.61 or newer. Check yours with nvidia-smi.
The engine ladder
Start at the top and step down only when you hit a limit.
- Ollama. The easiest start. It picks the GPU split for you, and
ollama psshows how much landed on the GPU. - llama.cpp (
llama-server). Step up when you want exact control: context size, KV type,--n-cpu-moe, multi-GPU split, andllama-benchfor honest numbers. - ExLlamaV3 + TabbyAPI. Step up for the best quality per GB on a 24 GB card with its EXL3 format. Support on a 3090 rests on one user report.
- vLLM or SGLang. Step up when many agents or users hit one model at once and batching matters. Both are Linux only (vLLM on Windows runs through WSL2).
Power limiting
A 3090 draws 350 W at stock. You can cap it and lose little speed on many models:
sudo nvidia-smi -pl 280
The cap resets on reboot, so re-apply it at boot with a systemd unit. The effect depends on the model. A 250 W cap kept ~99% of decode on a 27B (jeanfbrito, single run, secondary). The same kind of cut cost ~11% on the 7B bench: 155.4 t/s at 390 W against 138.3 at 250 W. On Qwen3.6-35B-A3B, 210 W gave 114.6 t/s against 136.8 at 370 W (club-3090, secondary). A cap around 250-290 W is a reasonable start. Measure your own model before and after.
Two RTX 3090s (48 GB)
Two cards give you 48 GB, but each card still holds only its share. There are two ways to split a model.
- By layers (llama.cpp's default,
-sm layer). Each card holds a block of layers and they take turns. It is simple and works over any PCIe link. - By rows (
-sm row). Each layer is split across both cards. It needs more traffic between them.-ts 1,1sets the share per card, and-mgpicks the main GPU.
llama-server -hf ggml-org/Qwen3.8-27B-GGUF:Q8_0 -ngl 99 -fa on -sm layer -ts 1,1 -c 65536 --port 8080
In vLLM, --tensor-parallel-size 2 splits each layer across both cards. Without NVLink, vLLM's docs advise pipeline parallelism (--pipeline-parallel-size 2) instead.
What 48 GB unlocks:
- Qwen3.8-27B at Q8_0 (28.6 GB), with ~18 GB left for long context.
- Qwen3.6-35B-A3B at Q8_0 (36.9 GB).
- A 70B dense at Q4_K_M. About 16.29 t/s decode, falling to 14.34 at 8K context (XiongjieDai, May 2024, secondary).
Three hardware notes:
- NVLink. Both the 3090 and the 3090 Ti have it; the 4090 and 5090 do not. How much it helps consumer inference is UNCERTAIN. Community claims range from 10-15% to 1.6-1.8×, from the same source. Don't buy a bridge for speed alone.
- PCIe. Use a board that gives both cards Gen4 lanes. Gen3 hosts reportedly lose 30-40% on tensor parallel (secondary; older CPUs are a confound).
- Power. Two cards capped at 330 W draw ~660 W, so plan on an 850 W or larger PSU (guidance, not a measured spec).
Smaller cards (8-16 GB)
A 12 GB or 16 GB card runs Qwen3.5 9B at 4-bit with room to spare. gpt-oss-20b (14 GB) fits on a 16 GB card with little context left. For bigger MoE models, keep the expert weights in system RAM and the rest on the GPU:
llama-server -hf unsloth/Qwen3.6-35B-A3B-GGUF:UD-Q3_K_XL -ngl 99 --n-cpu-moe 20 -fa on --port 8080
--n-cpu-moe N keeps the MoE weights of the first N layers on the CPU. Start with N high enough that it loads, then lower it step by step until VRAM is nearly full. --cpu-moe moves all of them. Expect a big slowdown against a full GPU fit; how big depends on your RAM speed.
Buying a used 3090
- Check the memory.
nvidia-smishould report the full 24 GB. Run a longllama-benchat full context and watch for crashes or artefacts. - Check temperatures under load. Watch the card during a long run. Safe memory temperatures and throttle points are unconfirmed (UNCERTAIN), so compare against other owners of the same model.
- Check the PSU. NVIDIA asks for 750 W for the Founders Edition. Plan on 850 W or more for two cards (guidance).
- Plan for a second card. If you may add one later, check that your board gives both slots Gen4 lanes and your case has room.
ollama ps shows 100% GPU and you have a measured tok/s (see Benchmark your machine).
Part 1 · Run Qwen locally
Tokens, context and the KV cache
A model never sees letters or words. It sees tokens: chunks of text from a fixed vocabulary, often a whole short word, sometimes a piece of a longer one, a space, or a punctuation mark. Everything is counted in tokens: how much you can send, how much it writes, and how much memory the conversation takes.
For English, about 100 tokens make 75 words, so one token is about ¾ of a word. That is OpenAI's rule of thumb for its own tokenizers. Qwen's tokenizer differs, and code, numbers and non-English text use more tokens per word. Treat it as approximate.
The context window
The context window is how many tokens the model can see at once: the system prompt, the whole chat so far, any documents you pasted in, and the answer it is writing. The Qwen3.5-9B model card lists 262,144 tokens natively. Your runtime decides how much of that you actually get.
Ollama's default is small on most machines. Its context-length docs set it by VRAM: 4K under 24 GiB, 32K at 24–48 GiB, 256K at 48 GiB or more (the older FAQ still says 4096). Check the CONTEXT column in ollama ps for what your build and model actually loaded. A long prompt that doesn't fit is cut to fit, and you get no error, just an answer that ignores the part it never saw. If retrieval or a long document gives strangely blind answers, check the context first.
Set it per request with num_ctx inside options on Ollama's own API:
curl -s http://127.0.0.1:11434/api/chat -d '{
"model": "qwen3.5:4b",
"messages": [{"role": "user", "content": "Summarise this file..."}],
"options": {"num_ctx": 16384}
}'Set it per model with a Modelfile, so every client gets it:
printf 'FROM qwen3.5:4b\nPARAMETER num_ctx 16384\n' > Modelfile ollama create qwen-16k -f Modelfile ollama run qwen-16k
Or change the server-wide default with the OLLAMA_CONTEXT_LENGTH environment variable, for example OLLAMA_CONTEXT_LENGTH=8192 ollama serve. In llama.cpp the flag is -c (or --ctx-size).
How big the KV cache gets
While it reads your prompt, the model stores a key and a value vector for every token in every attention layer, so it doesn't recompute them. That store is the KV cache. It grows linearly with context, and it sits in memory next to the weights. The formula:
KV bytes = 2 × layers × kv_heads × head_dim × context × bytes_per_value × parallel_slots
The 2 is one key plus one value. layers counts only the layers that keep a cache. bytes_per_value is 2 for f16 and about 1 for q8_0.
Worked example: Qwen3.6-27B. Its model card lists 64 layers, but in a repeating pattern of three linear-attention (Gated DeltaNet) layers and one full-attention layer. Only the 16 full-attention layers keep a growing cache, each with 4 KV heads of dimension 256. Per token at f16:
2 × 16 × 4 × 256 × 2 bytes = 65,536 bytes = 64 KiB per token × 32,768 tokens = 2 GiB per 32K of context
- Why hybrid attention matters. If all 64 layers kept a cache, the same model would need 256 KiB per token, or 8 GiB per 32K (derived). The linear-attention layers keep a small fixed-size state instead, so the cache costs a quarter of that. Qwen3.6-35B-A3B goes further, at about 20 KiB per token (derived).
- Why q8_0 KV halves it. Storing each value in 8 bits instead of 16 cuts the cache to about half: roughly 32 KiB per token, or about 1 GiB per 32K for the 27B. The Ollama FAQ calls the precision loss very small. In Ollama, set
OLLAMA_FLASH_ATTENTION=1andOLLAMA_KV_CACHE_TYPE=q8_0(it is a global setting). In llama.cpp, use-ctk q8_0 -ctv q8_0. - Why
OLLAMA_NUM_PARALLELmultiplies it. Each parallel slot gets its own context. The FAQ says required memory scales withOLLAMA_NUM_PARALLEL×OLLAMA_CONTEXT_LENGTH. Two slots of 32K on the 27B at f16 is 4 GiB of cache, not 2. On a tight machine keep it at 1.
The 64 KiB figure is verified for Qwen3.6-27B only. Read num_hidden_layers, num_key_value_heads, head_dim and the layer layout from the model's config.json before you trust the formula for another model. Runtimes also add some fixed buffers on top, so leave headroom.
Chat templates
A chat model was trained on conversations laid out in one exact format: special tokens that mark where the system prompt, each user turn and each assistant turn start and stop, plus the switch for thinking mode. The chat template turns your list of messages into that one long string. You rarely see it, because the runtime applies it for you.
A mismatched template breaks output in quiet ways. The model sees markers it was never trained on, so it rambles, repeats itself, answers its own questions, never stops, or leaks <think> text into the answer. That is also the usual cause of gibberish after a fine-tune (Part 3).
GGUF files carry the template with them, in the tokenizer.chat_template metadata field, which the GGUF spec describes as a Jinja template. Ollama and llama.cpp read it from there, so a GGUF from the official or a trusted uploader just works. Override it only when you know the file's template is wrong, and treat a template from an unknown source as code (see "Poisoned chat templates" in Part 2).
Quantization formats
You will meet these names on model pages. Which one you can use depends on your hardware more than on taste.
| Format | What it is | Apple Silicon | Ampere (RTX 30) | Ada (RTX 40) | Blackwell (RTX 50) |
|---|---|---|---|---|---|
| GGUF k-quants (Q4_K_M, Q8_0) | llama.cpp's file format; Ollama, LM Studio | Yes | Yes | Yes | Yes |
| GGUF i-quants (IQ4_XS, IQ3_XXS) | Smaller GGUF quants from an importance matrix | Yes | Yes | Yes | Yes |
| MLX | Apple's format for mlx-lm and LM Studio's MLX engine | Yes | No | No | No |
| EXL3 | ExLlamaV3 and TabbyAPI; quality per GB | No | Yes in practice (one user report) | UNCERTAIN | UNCERTAIN |
| AWQ, GPTQ | 4-bit GPU formats for vLLM and SGLang | No | Yes | Yes | UNCERTAIN |
| FP8 | 8-bit floats, served by vLLM | No | Weight-only (W8A16) | Yes | Yes |
| NVFP4 | NVIDIA's 4-bit float format | No | Fallback kernels only | Fallback kernels only | Native FP4 tensor cores |
The Apple column is derived: the GPU-only formats run on CUDA engines, which don't run on a Mac.
How to choose:
- One person, any machine: GGUF. Start at Q4_K_M, go to IQ4_XS when you need room for context, and go up to Q8_0 when you have memory to spare.
- On a Mac, try MLX for short prompts and keep GGUF for long contexts and tight fits (see "Faster and smaller").
- On an NVIDIA card, try EXL3 for the best quality per GB on 24 GB, AWQ or GPTQ when serving many requests with vLLM, and FP8 or NVFP4 only on the cards that run them natively.
Part 1 · Run Qwen locally
Thinking mode
Qwen3.5 4B and 9B reason out loud in a <think> block before answering. It helps with maths, planning and tricky instructions. It costs time, and quantized models in thinking mode truncate answers more often because they think for longer. For extraction, classification, chat and tool calls, turn it off.
- In the terminal:
ollama run qwen3.5:4b --think=false, or type/set nothinkinside a session.--hidethinkingkeeps the reasoning but hides it. - Through the API: send a top-level
"think": falsefield on/api/chator/api/generate. Insideoptionsit is silently ignored. - In a Modelfile:
PARAMETER think falseis not supported (Ollama issue #10961). Set it per request. - In Python, MLX or llama.cpp: pass
enable_thinking=Falseto the chat template. For llama.cpp's server:--chat-template-kwargs '{"enable_thinking":false}'. - Sampling: thinking and non-thinking use different settings. See "Sampling settings" below for the official values.
Decide which mode you'll use for a task and stay consistent, including when you evaluate (Part 3) or compare models (Part 4).
Sampling settings
At each step the model gives every possible next token a probability. Sampling settings decide how one gets picked:
- temperature reshapes the probabilities. Low (0.2 to 0.7) makes the likeliest tokens likelier: steadier, more repetitive. High (around 1.0) keeps more variety. 0 always takes the top token.
- top_p keeps the smallest set of tokens whose probabilities add up to p, and drops the rest. 0.8 is tighter than 0.95.
- top_k keeps only the k likeliest tokens. 20 is a tight cut.
- min_p drops any token less likely than min_p times the top token's probability. 0 turns it off.
- presence_penalty pushes down any token that has already appeared, which fights loops. repetition_penalty (llama.cpp and Ollama call it
repeat_penalty) scales down repeated tokens; 1.0 turns it off.
What Qwen recommends. The official Qwen3.5-9B model card gives these (checked 2026-10-16):
| Mode and task | temperature | top_p | top_k | min_p | presence_penalty | repetition_penalty |
|---|---|---|---|---|---|---|
| Thinking, general | 1.0 | 0.95 | 20 | 0.0 | 1.5 | 1.0 |
| Thinking, precise coding | 0.6 | 0.95 | 20 | 0.0 | 0.0 | 1.0 |
| Non-thinking, general | 0.7 | 0.8 | 20 | 0.0 | 1.5 | 1.0 |
| Non-thinking, reasoning | 1.0 | 0.95 | 20 | 0.0 | 1.5 | 1.0 |
Its Best Practices section lists a different non-thinking reasoning set (temperature 1.0, top_p 1.0, top_k 40, presence_penalty 2.0). It also warns that support for these settings varies by framework, and that a high presence penalty can cause language mixing and a slight drop in quality. If you see Chinese words in English answers, lower presence_penalty first.
In Ollama, pass them in options on /api/chat, or bake them into a Modelfile with PARAMETER temperature 0.7 and so on:
curl -s http://127.0.0.1:11434/api/chat -d '{
"model": "qwen3.5:4b", "think": false,
"messages": [{"role": "user", "content": "Name three uses for a paperclip."}],
"options": {"temperature": 0.7, "top_p": 0.8, "top_k": 20, "min_p": 0, "presence_penalty": 1.5}
}'In llama.cpp, use flags on llama-cli or llama-server (the server also accepts them per request):
llama-server -m qwen3.5-4b-q4_k_m.gguf --port 8080 \ --temp 0.7 --top-p 0.8 --top-k 20 --min-p 0 --presence-penalty 1.5 --repeat-penalty 1.0
From an OpenAI client, temperature, top_p and presence_penalty are standard fields. top_k and min_p are not part of the OpenAI API, so pass them through extra_body; whether the server honours them is UNCERTAIN and varies by server:
resp = client.chat.completions.create(
model="qwen3.5:4b",
messages=[{"role": "user", "content": "Name three uses for a paperclip."}],
temperature=0.7, top_p=0.8, presence_penalty=1.5,
extra_body={"top_k": 20, "min_p": 0},
)Keep sampling fixed while you compare models or evaluate a fine-tune. Changing it changes the result as much as changing the model can.
Part 1 · Run Qwen locally
Other runtimes, and when to pick them
- LM Studio is a desktop app with a model browser, a chat window and an OpenAI-compatible server. On a Mac it can run both llama.cpp and MLX engines and supports speculative decoding. The best choice if you'd rather click than type.
- llama.cpp is the C++ engine underneath Ollama and LM Studio. Use it directly when you need every option: exact GPU layer offload when a model barely fits, a reranking endpoint, or the latest model support before the apps catch up.
llama-servergives you an OpenAI-compatible API. - mlx-lm is Apple's Python package for MLX, and the main Mac path once you outgrow Ollama. It's the fastest way to run small models on a Mac for short prompts, and the only Mac path for LoRA training (Part 3). It loads the whole model into GPU memory or nothing, and slows down at very long contexts.
- vLLM, SGLang and similar servers are for GPU boxes serving many users at once. Not for a laptop.
For one person on one machine, start with Ollama. Add the others when a specific need appears.
Runtimes on a PC
On Windows and Linux with an NVIDIA card, the field is wider. Versions were current on 16 October 2026 and move fast.
| Runtime | OS | Best for | Multi-GPU | Formats |
|---|---|---|---|---|
| Ollama | Windows 10 22H2+ (driver 551.61+), Linux, macOS | The easiest start | Automatic; pin cards with CUDA_VISIBLE_DEVICES (UUIDs) | GGUF |
| llama.cpp | Windows prebuilt CUDA builds; Linux | Control, benchmarking, --n-cpu-moe offload | -sm, -ts, -mg flags | GGUF |
| LM Studio | Windows x64 (AVX2) or ARM, Linux AppImage, macOS | A GUI plus a server (examples use port 1234) | Per-GPU on/off; CUDA tensor parallel in recent builds | GGUF; MLX on Mac |
| vLLM | Linux only (Windows through WSL2) | Batching, many agents at once | Tensor and pipeline parallel | AWQ, GPTQ, INT8, GGUF; FP8/FP4 weight-only on a 3090 |
| SGLang | Linux, CUDA 13 only | Serving, structured output | --tp-size | AWQ, GPTQ, FP8 (Ampere support UNCERTAIN) |
| ExLlamaV3 + TabbyAPI | Windows (triton-windows), Linux | Quality per GB on a 24 GB card; API on port 5000 | Tensor and expert parallel | EXL3 |
| KoboldCpp | Windows exe, Linux binary | One file to run; API on port 5001 | UNCERTAIN | GGUF |
It now listens on 127.0.0.1:9931, not 8080. Pass the port every time so your scripts don't break, for example llama-server -m model.gguf --host 127.0.0.1 --port 8080.
Part 1 · Run Qwen locally
Faster and smaller: engines and quantization
Once a model runs, two levers decide how well: which engine runs it, and how its weights are stored.
MLX or llama.cpp on a Mac
For the 4B and 9B class, mlx-lm is the measured decode-speed leader on Apple Silicon: roughly +56% on Qwen2.5-7B at 4-bit (about 63.7 against 40.75 tokens per second, on an M1 Max), and up to +87% on sub-1B models. The popular "3× faster" figure is mostly Ollama's own overhead; the real MLX win over raw llama.cpp is about 1.4–1.8× for decode. These are mid-2026 benchmark estimates. Two caveats decide it:
- MLX has no CPU/GPU layer offload. It loads the entire model or nothing. llama.cpp's
n_gpu_layersoffload is the difference between running and crashing when a model barely fits. - MLX's advantage inverts at long context. One test saw effective, prefill-inclusive throughput collapse to about 3 tokens per second at 8.5K context while the interface still showed about 51, and MLX ran about 50% slower than flash-attention llama.cpp past roughly 30K tokens. That hurts retrieval and long prompts, which Part 2 is full of.
The verdict for a 16 GB machine: a llama.cpp-based runtime for memory safety and long prompts; mlx-lm or LM Studio's MLX engine for short-prompt interactive use, where the roughly 1.5× decode win is real. Note that Ollama itself now defaults to MLX for Qwen3.5 on Apple Silicon (v0.40.0, see the install section), so check ollama ps and the server log to learn which runner you actually got; LM Studio's llama.cpp engine or raw llama.cpp give you the llama.cpp path for certain.
Speculative decoding is a free throughput lever. A tiny draft model proposes tokens and the bigger target model checks them in parallel. LM Studio supports it on Apple Silicon, both with multi-token-prediction (MTP) heads and with a classic draft model, for a reported 20–50% generation speedup; mlx-lm supports it natively. On 16 GB both models must fit, so it pairs best with a 4B target and a sub-1B draft, not the 9B. Ollama also lists Qwen3.5 MTP builds (4b-mtp-q4_K_M at 4.1 GB, 9b-mtp-q4_K_M at 9.0 GB, large for 16 GB).
Reported GitHub issues (on an M1 Max, not an M4; secondary evidence) show llama.cpp's MTP path slower on Metal for Qwen3.5-9B, by 11–28%. Measure before leaving it on.
vLLM on a Mac exists, but not for you. The community vllm-metal plugin runs vLLM on Apple Silicon, but it is about 1.2× slower than llama.cpp for a single user. It exists for batching and API parity on a serving box. The common 2026 pattern: develop locally with llama.cpp or mlx-lm, run heavy work on a Linux GPU machine serving vLLM, and keep the OpenAI-compatible endpoint the same so clients don't change. vllm-metal and Docker Model Runner both expose OpenAI- and Anthropic-compatible APIs.
Better than Q4_K_M at the same size
Q4_K_M is a safe floor, not the best you can do at four bits:
- Unsloth Dynamic 2.0 GGUFs start from an importance matrix and pick the best quant type per layer. Same file size and RAM as Q4_K_M, measurably higher fidelity: lower KL divergence than plain imatrix and QAT quants across Qwen3.5, Gemma 3 and Llama 4, closing about a third of the Q4_K_M to Q5_K_M gap for free. On a 16 GB machine where you can't climb a bit level, pull the
unsloth/…-GGUFDynamic build instead of a generic Q4_K_M. - IQ4_XS (about 4.25 bits per weight against Q4_K_M's about 4.5) has near-identical perplexity and saves about 400 MB on a 9B, which can be the difference between fitting a useful context or not. IQ quants decode a little slower than K quants, with a smaller penalty on Metal. Keep IQ3_XXS (about 3.0 bits per weight, approximate) for "won't fit otherwise"; it is a real quality step down.
- MLX learned quantization (DWQ) fine-tunes the quantized scales and biases against the full-precision model. A 4-bit DWQ checkpoint can reach the quality of a 6–8-bit standard quant at 4-bit size; pre-built
mlx-communitycheckpoints exist. Distil from an 8-bit source, not 16-bit.
With Qwen3.5's reasoning on, quantized variants have been measured dropping as much as about 33% on some benchmarks, and quantization roughly doubled the answer-truncation rate, because quantized models think longer and hit the output limit. If you rely on thinking for hard reasoning, use Q5_K_M, Q6_K or 8-bit for those calls, or raise the output-token limit.
The KV cache, refined. q8_0 for the KV cache (the 16 GB setting in the install section) halves it at under 0.1% loss. Four-bit is asymmetric: keys tolerate it, values don't. To squeeze a long context onto 16 GB, use a Q4 key cache with a Q8 value cache rather than Q4 for both (same memory, better quality; an estimate).
Where QAT and ternary fit. Quantization-aware training ships 4-bit, even 2-bit, checkpoints with near-bf16 quality; Gemma's QAT releases are the standout. Qwen3.5 does not ship a QAT checkpoint: it ships official GPTQ-Int4, FP8 and NVFP4, and AWQ for Qwen3.5 exists only as community quants. BitNet b1.58 ternary weights are real at 2B (BitNet-b1.58-2B4T matches full-precision 2–3B models at about 0.4 GB, arXiv 2504.12285), but there is still no 7–9B ternary model that competes with a quantized Qwen3.5-9B.
Part 1 · Run Qwen locally
Benchmark your machine
Every speed in this guide is either a cited measurement or an estimate. Your machine is the only one that counts, and measuring it takes five minutes. You want two numbers: how fast the model reads your prompt (prompt processing, or prefill) and how fast it writes the answer (generation, or decode).
The quick check: Ollama's --verbose
ollama run qwen3.5:4b --verbose "Write 200 words about rivers."
After the answer, Ollama prints a timing summary. Two lines matter:
- prompt eval rate is prefill speed in tokens per second. It decides how long you wait before the first word, and it dominates with long prompts and retrieval.
- eval rate is generation speed in tokens per second. It is the number people quote, and it is mostly set by memory bandwidth.
Ignore the first run after a model loads, since it includes load time (load duration). Run the same prompt three times and take the middle result.
Ollama computes prompt eval rate from the uncached prompt tokens only. Ask the same thing twice in one session and most of the prompt comes from cache. Change the prompt, or start a new session, when you measure prefill.
The same numbers from the API
Every non-streamed Ollama API response carries the timings. All durations are in nanoseconds:
prompt_eval_countandprompt_eval_duration: prompt tokens and the time to process the uncached ones.eval_countandeval_duration: tokens generated and the time to generate them.load_durationandtotal_duration: model load time and the whole request.
So generation speed is eval_count / eval_duration × 1e9 tokens per second, and prefill speed is prompt_eval_count / prompt_eval_duration × 1e9. In Python:
import ollama
r = ollama.generate(model="qwen3.5:4b", prompt="Write 200 words about rivers.")
print("generation tok/s:", r.eval_count / r.eval_duration * 1e9)
print("prefill tok/s: ", r.prompt_eval_count / r.prompt_eval_duration * 1e9)The prefill figure here counts cached tokens too, so on a repeated prompt it overstates. Use fresh prompts.
The proper benchmark: llama-bench
llama-bench ships with llama.cpp and is what the llama.cpp scoreboards use. Point it at a GGUF file:
llama-bench -m model.gguf -p 512 -n 128 -ngl 99 -fa on
-p 512runs a 512-token prompt-processing test (shown aspp512).-n 128runs a 128-token generation test (shown astg128).-ngl 99puts every layer on the GPU. Lower it to test partial offload.-fa onturns flash attention on. The default isauto.
Each test runs 5 times by default (-r), and the result is the average tokens per second with its standard deviation. Add -o csv or -o json to save results instead of a Markdown table.
Short tests flatter every machine, because the context is empty. -d (depth) first fills the context with that many tokens, then runs the test. Parameters take a comma-separated list, so one command shows how speed falls as context grows:
llama-bench -m model.gguf -p 512 -n 128 -ngl 99 -fa on -d 0,8192,32768
Flags checked against the llama-bench README on 2026-10-16.
Check that it is really on the GPU
A model that spills into system RAM runs several times slower, and nothing warns you. Check while it is loaded:
ollama ps
Read the PROCESSOR column. 100% GPU means the whole model is in GPU memory. 100% CPU means it is all in system RAM. A split like 48%/52% CPU/GPU means part of it spilled, and that is your speed problem: use a smaller quant, a shorter context, or a smaller model.
On an NVIDIA PC, run nvidia-smi (or watch -n 1 nvidia-smi on Linux) while the model is answering. Memory used should be close to the model size plus the KV cache, and GPU utilisation should be high during generation. On a Mac, open Activity Monitor, then Window > GPU History, and watch the Memory tab for swap.
Record your results
Copy this table into your notes and fill one row per run. Keep the runtime, quant and depth with every number, or the numbers mean nothing later.
| Machine | Runtime and version | Model and quant | Depth (tokens) | Prefill tok/s | Generation tok/s | Offload | Date |
|---|---|---|---|---|---|---|---|
| RTX 3090, FA on (reference) | llama.cpp scoreboard | Llama 2 7B Q4_0 | 0 | not recorded here | 161.9 (measured) | 100% GPU | see source |
| your machine | … | … | 0 | … | … | … | … |
| your machine | … | … | 8192 | … | … | … | … |
| your machine | … | … | 32768 | … | … | … | … |
The reference row is from the llama.cpp CUDA scoreboard (github.com/ggml-org/llama.cpp/discussions/15013). Run the same model with llama-bench and you can tell at once whether your card and drivers are healthy.
For gpt-oss-20b, Gemma 4 31B, GLM-4.7-Flash, Qwen3-Coder-30B-A3B and Muse Glimmer 30B, no source in this guide's evidence base has a measured RTX 3090 generation speed. The figures elsewhere are ceilings derived from bandwidth divided by model size (about 51 tok/s for Gemma 4 31B and 55 for Muse Glimmer at Q4, derived), and real speeds land below them. The nearest measurements are community reports, single contributors and secondary: Qwen3.6-27B at Q3_K_XL ran 32–38 tok/s, and Qwen3.6-35B-A3B ran 136.8 tok/s at 370 W. Your llama-bench row is how this gap gets filled.
Part 1 · Run Qwen locally
Use it from your own code
Every runtime above speaks the OpenAI chat-completions format, so any client library works by changing the base URL. With Ollama running:
curl -s http://127.0.0.1:11434/v1/chat/completions -H 'Content-Type: application/json' \
-d '{"model":"qwen3.5:4b","messages":[{"role":"user","content":"Say hi"}]}'In Python, with the openai package: create the client with base_url="http://127.0.0.1:11434/v1" and any non-empty api_key, then call chat.completions.create as usual. Ollama's own /api/chat endpoint adds the think switch and structured-output support (format with a JSON schema), which are worth using for extraction tasks.
Context length can be set per request or per model. Per request, send "options": {"num_ctx": 8192} on Ollama's /api/chat. Per model, create a Modelfile with FROM qwen3.5:4b and PARAMETER num_ctx 8192, then ollama create my-qwen -f Modelfile. See "Tokens, context and the KV cache" for what it costs. Keep it modest on a 16 GB machine; the KV cache grows with it.
Structured output
For extraction, ask for JSON and enforce the shape. Ollama's format field takes a JSON schema, and the reply is constrained to match it:
curl -s http://127.0.0.1:11434/api/chat -d '{
"model": "qwen3.5:4b",
"messages": [{"role": "user", "content": "Ada Lovelace published her notes in 1843. Return the name and year as JSON."}],
"stream": false,
"options": {"temperature": 0},
"format": {
"type": "object",
"properties": {"name": {"type": "string"}, "year": {"type": "integer"}},
"required": ["name", "year"]
}
}'In Python, define the shape once with Pydantic and use it for both the schema and validation:
from ollama import chat
from pydantic import BaseModel
class Person(BaseModel):
name: str
year: int
r = chat(
model="qwen3.5:4b",
messages=[{"role": "user", "content": "Ada Lovelace published her notes in 1843. Return the name and year as JSON."}],
format=Person.model_json_schema(),
options={"temperature": 0},
)
print(Person.model_validate_json(r.message.content))Ollama's docs recommend a temperature of 0 here, and also putting the schema in the prompt as text to ground the answer. The OpenAI-compatible endpoint supports the same thing through response_format.
Tool calling
A tool call is the model asking your code to run a function. The model only proposes the call. Your code decides whether to run it, runs it, and sends the result back. That decision point is where safety lives, so keep an explicit allowlist and never pass model output to a shell. This loop uses the /v1 endpoint and the openai package:
import json
from openai import OpenAI
client = OpenAI(base_url="http://127.0.0.1:11434/v1", api_key="ollama")
def word_count(text: str) -> int:
return len(text.split())
ALLOWED = {"word_count": word_count} # the only functions the model can reach
TOOLS = [{"type": "function", "function": {
"name": "word_count", "description": "Count the words in a text.",
"parameters": {"type": "object", "properties": {"text": {"type": "string"}}, "required": ["text"]}}}]
messages = [{"role": "user", "content": "How many words are in 'the quick brown fox jumps'?"}]
for _ in range(5): # cap the rounds so a confused model can't loop forever
msg = client.chat.completions.create(model="qwen3.5:4b", messages=messages, tools=TOOLS).choices[0].message
if not msg.tool_calls:
print(msg.content)
break
messages.append(msg.model_dump(exclude_none=True))
for call in msg.tool_calls:
fn = ALLOWED.get(call.function.name)
try:
result = fn(**json.loads(call.function.arguments)) if fn else "error: tool not allowed"
except Exception as e: # bad arguments go back to the model as text
result = f"error: {e}"
messages.append({"role": "tool", "tool_call_id": call.id, "content": str(result)})Ollama's /v1 endpoint supports tools but not tool_choice, so you can't force a call; describe the tools clearly instead. Qwen3.5-9B is the stronger small tool-caller if the 4B misfires.
Images
Qwen3.5 takes images as input, and Ollama's default tags include the vision encoder. In the terminal, put the image path in the prompt:
ollama run qwen3.5:4b ./photo.png "What is in this image? Be concise."
In Python, the images field takes file paths:
from ollama import chat
r = chat(model="qwen3.5:4b", messages=[
{"role": "user", "content": "What is in this image? Be concise.", "images": ["./photo.png"]}])
print(r.message.content)Over the raw API, images takes base64 strings:
IMG=$(base64 < photo.png | tr -d '\n')
curl -s http://127.0.0.1:11434/api/chat -d '{"model":"qwen3.5:4b","stream":false,
"messages":[{"role":"user","content":"What is in this image?","images":["'"$IMG"'"]}]}'The OpenAI-compatible endpoint accepts base64 images too, but not image URLs.
Compare models on your task
Leaderboards don't know your inputs. This harness runs every model on every prompt, applies a pass rule, and writes a CSV you can sort:
import csv, time
from openai import OpenAI
client = OpenAI(base_url="http://127.0.0.1:11434/v1", api_key="ollama")
MODELS = ["qwen3.5:4b", "qwen3.5:9b"] # any tags you've pulled
CASES = [ # (prompt, pass rule)
("Reply with only the capital of France.", lambda out: "paris" in out.lower()),
("Reply with only the result of 17 * 23.", lambda out: "391" in out),
]
with open("results.csv", "w", newline="") as f:
w = csv.writer(f)
w.writerow(["model", "case", "passed", "seconds", "output"])
for model in MODELS:
passed = 0
for i, (prompt, rule) in enumerate(CASES):
t = time.time()
out = client.chat.completions.create(model=model, temperature=0,
messages=[{"role": "user", "content": prompt}]).choices[0].message.content or ""
ok = rule(out)
passed += ok
w.writerow([model, i, ok, round(time.time() - t, 2), out[:200]])
print(f"{model}: {passed}/{len(CASES)} passed")Replace the two toy cases with ten to thirty real inputs from your work, and make each pass rule as strict as you would be. Keep the thinking setting the same for every model, or you are comparing two different things.
The fine-tuning eval in "Evaluate honestly" is the same loop with your base model and your fine-tuned model as the two entries in MODELS.
Speculative decoding on a GPU
The Mac section above covers speculative decoding in LM Studio and mlx-lm. On an NVIDIA card, llama-server does it with a small draft model from the same family, so the two share a tokenizer. On a 24 GB card both fit easily:
llama-server -m Qwen3.5-9B-Q4_K_M.gguf -md Qwen3.5-0.8B-Q4_K_M.gguf \ --spec-type draft-simple --spec-draft-n-max 8 \ -ngl all -ngld all -fa on --port 8080
-mdloads the draft model, and-ngld allputs all of its layers on the GPU too.--spec-type draft-simplepicks classic draft-model speculation. The default isnone, so set it explicitly.--spec-draft-n-maxis how many tokens the draft proposes per step (default 3). Older guides use--draft-maxand--draft-min; current builds have removed those flags.--port 8080matters, because llama-server now defaults to port 9931.
Flags checked against the llama-server README on 2026-10-16. Whether draft-simple works with Qwen3.5's hybrid architecture on your build is UNCERTAIN, so check the server log at startup.
Speedups depend on how often the target accepts the draft's tokens, which depends on the task. Run the same prompts with and without -md and compare generation tok/s, as in "Benchmark your machine". If it is not faster, drop it.
Part 1 · Run Qwen locally
Keep it on your machine
Local inference is private by default. Keep it that way:
- Loopback only. Ollama, LM Studio and llama.cpp have no authentication. Bound to
127.0.0.1they're reachable only from your own computer. Bound to0.0.0.0they're open to anyone on the network, and if a router forwards the port, to the internet. Ollama has had real remote-code-execution and memory-leak vulnerabilities, and scans regularly find hundreds of thousands of exposed servers. - Run the latest version. Model-file parsers are a recurring source of memory-safety bugs. Updates fix them.
- Pull from accounts you trust. A model file is input to a parser you run with your own permissions. Prefer the official library and known publishers; prefer safetensors and GGUF over pickle formats.
- Treat model output as untrusted text. If you wire a model to tools that can run commands, read files or send messages, put the approval step in your code, not in the prompt. A model can be talked into anything; your validator can't.
- Reaching it from another machine is fine over a VPN or mesh such as Tailscale, bound to that interface, with a firewall rule. Never expose port 11434 directly.
Part 1 · Run Qwen locally
Learn more: running models
For a structured route from here, see Track 3 in the free learning path. For how the model itself works, start with Tracks 0 and 2.
- Qwen documentation: the official guide, with pages for Ollama, LM Studio, llama.cpp and MLX, quantization and deployment. It still centres on the Qwen3-2507 line, but the runtime pages apply to 3.5.
- Qwen3.5 model cards on Hugging Face: architecture, benchmarks, recommended sampling, the thinking switch.
- Ollama docs: quickstart, GPU requirements, importing models, the API.
- Hugging Face Hub docs and the
hfCLI guide. - llama.cpp: the engine, with its own quantization and server documentation.
- mlx-lm: Apple's package for running and training on MLX.
- The Hugging Face LLM course: free, from tokenizers to fine-tuning, if you want the foundations.
- Andrej Karpathy's Zero to Hero: build a small language model from scratch in Python. The best way to understand what the weights are.
Part 2 · Build on it
Embeddings: how a machine measures relevance
Everything in this part, retrieval, memory and agents that look things up, rests on one operation: turning a piece of text into a point in space, so that "close" means "about the same thing." That operation is an embedding.
What an embedding is. An embedding model maps text to a fixed-length vector of floating-point numbers. For Qwen3-Embedding-0.6B, the embedder this whole part uses, that is a 1024-dimensional vector; for the lighter nomic-embed-text, 768. The model is trained so that semantically similar texts land near each other in that space: "Cancel my subscription" and "account termination" end up close even with no shared words. Each dimension is a learned latent feature. You never interpret them one at a time, only their geometric relationships.
Cosine similarity and nearest neighbours. Similarity is the cosine of the angle between two vectors: 1.0 means identical direction, 0 means unrelated. Semantic search is then two steps: embed the query, then find the stored vectors with the highest cosine similarity to it, the nearest neighbours. That is the entire retrieval mechanism.
Try it. Embedding models are pulled and called like chat models (Part 1 covers the install). The embedding endpoint is /api/embed:
ollama pull qwen3-embedding:0.6b
# Generate an embedding (current endpoint)
curl http://localhost:11434/api/embed -d '{
"model": "qwen3-embedding:0.6b",
"input": "The sky is blue because of Rayleigh scattering"
}'The response is a list of 1024 numbers. That list is the conceptual bridge to retrieval: embeddings are how a machine measures "relevance."
Which embedding model, October 2026
As of mid-2026 nomic-embed-text is no longer the best small embedder for local retrieval. The comparison below is for a 16 GB machine where the embedder has to share memory with a chat model.
| Model | Pull | Size | Dims | Context | Languages | Licence |
|---|---|---|---|---|---|---|
| Qwen3-Embedding-0.6B (default) | ollama pull qwen3-embedding:0.6b | ~639 MB | 1024, truncatable 32→1024 | 32K | 100+ | Apache-2.0 |
| nomic-embed-text (English legacy) | ollama pull nomic-embed-text | 274 MB, 137M params | 768 | 8192 | English | Apache-2.0 |
| EmbeddingGemma-300m (smallest) | ollama pull embeddinggemma | ~622 MB, under 200 MB RAM quantized | 768, truncatable to 512, 256, 128 | 2048 | 100+ | Gemma licence (not Apache-2.0) |
Qwen3-Embedding-0.6B is the default. It scores 64.3 on MTEB-Multilingual-v2 against nomic's ~62, takes 32K tokens of context against nomic's 8K so long chunks embed without truncation, covers 100+ languages, and its Matryoshka (MRL) output lets you truncate the vector to 512 or 256 dimensions to shrink a vector index roughly 2–4× at minimal recall loss. It is near drop-in: the same embed call, just a 1024-dim default, so set your vector store to 1024 cosine (or 768 or 512 if you truncate). In one multilingual retrieval test Qwen3 ranked the target chunk #1 where nomic ranked it #44. That is a single source and directional, but the gap is real because nomic is English-trained.
nomic-embed-text is the English-only legacy. 137M params, 274 MB, 768 dims, 8192-token context. Nomic's technical report (arXiv 2402.01613) calls nomic-embed-text-v1 "the first fully reproducible, open-source, open-weights, open-data, 8192 context length English text embedding model" and reports it beating OpenAI's Ada-002 and text-embedding-3-small on the short-context MTEB benchmark and the long-context LoCo benchmark. It is embedding-only. It is a lighter English-only alternative at 768 dims. Keep it for English-only corpora you have already indexed, and change every 1024 in this part to 768 if you do.
Ollama's model card historically defaulted num_ctx to 2048, and some versions truncated at 512. Set num_ctx: 8192 explicitly if you embed long chunks.
EmbeddingGemma-300m is the smallest footprint. Ollama ≥ v0.11.10, ~622 MB on disk, under 200 MB of RAM quantized, 61.15 on MTEB-Multilingual-v2, 768 dims with Matryoshka truncation to 512, 256 or 128, 100+ languages. Two caveats. Its context is only 2048 tokens, so cap chunks at about 1500 tokens of text. And it ships under the Gemma licence plus Prohibited Use Policy, not Apache-2.0; the Hugging Face card says License: gemma, and some blogs get this wrong. For an MIT- or open-first project, Qwen3-Embedding-0.6B's Apache-2.0 is cleaner.
Shipped 2026-10-06: 740M params, multimodal, 8K context, Apache-2.0, 768 dims with 512/256/128 Matryoshka, with Ollama and GGUF builds listed. Both caveats above are v1 facts. It has not yet been compared against Qwen3-Embedding-0.6B here, so the default stands.
Rerankers: the second pass
Why a reranker. A cross-encoder reranker reads the query and a candidate chunk together and scores the pair, instead of comparing two vectors made separately. The pattern: retrieve the top-k by cosine (k around 20–50), re-score each (query, chunk) pair with the reranker, keep the top 3–5 for the model's context. Adding one is the single highest-leverage upgrade to a retrieval pipeline.
Default: bge-reranker-v2-m3. 568M params, multilingual, Apache-2.0; the Q8_0 GGUF is about 600 MB. Modern alternative: Qwen3-Reranker-0.6B at 65.8 MTEB-R. The 4B reaches 69.76, but its autoregressive yes/no-logit inference is slow locally, so stay at 0.6B on a 16 GB machine. Budget 0.6–1 GB and run the reranker on demand, not resident.
The rerank PR #7219 was closed and abandoned in September 2025, not merged and pending. Run reranking through llama.cpp's llama-server --reranking --pooling rank as a small sidecar (Part 1 lists llama-server among the runtimes) and POST the query plus documents to /v1/rerank.
Most community GGUFs emit garbage scores (around 4.5e-23) because they are missing the reranker classifier tensor. Use only a GGUF converted with the official convert_hf_to_gguf.py, which extracts cls.output.weight. bge-reranker-v2-m3 GGUFs convert cleanly and are the lower-risk default.
Scope. The genuine MTEB-Multilingual ceiling in mid-2026 is 8B–12B embedders: NVIDIA's Llama-Embed-Nemotron-8B tops the multilingual MMTEB leaderboard, and KaLM-Embedding-Gemma3-12B is near it. But an 8B embedder and a 9B chat model will not co-reside in the 9–12 GB a 16 GB machine can give them. On this hardware, stop at the 0.6B–4B tier and add a reranker; push the big embedders to a GPU machine on your network.
Part 2 · Build on it
Retrieval: chat with your own notes
A local model knows nothing about your files. Retrieval-augmented generation (RAG) fixes that without touching the weights: find the few passages that matter, put them in the prompt, let the model answer from them.
Why RAG, in three reasons. First, context windows are finite and expensive; you can't paste all your notes into every prompt. Second, it keeps knowledge external to the weights, so you update facts by editing a database, not by retraining. Third, no fine-tuning is needed, and answers can cite their sources, which reduces hallucination. Part 3 covers when fine-tuning is the right tool instead; the short version is that facts belong in retrieval and behaviour belongs in training.
The mechanism: ingestion, then query
The previous section turned text into vectors and relevance into cosine similarity. Retrieval is that, applied twice.
Ingestion. Split each document into chunks of roughly 512–1024 tokens, with about 128 tokens of overlap so sentences aren't severed at the boundary. Embed every chunk with an embedding model (Qwen3-Embedding-0.6B, qwen3-embedding:0.6b, in the example below). Store the vector, the chunk's text and some metadata (file name, position) together.
Query. Embed the question with the same model. Search for the top-k nearest chunks by cosine similarity. Inject those chunks into the prompt as context, and the model generates an answer grounded in them.
The minimal version, in memory
Everything above fits in a short script. It uses Ollama's Python package, the qwen3.5:4b you pulled in Part 1, and the qwen3-embedding:0.6b embedder from the previous section, pulled the same way.
import ollama, numpy as np
def embed(text):
return np.array(ollama.embed(model="qwen3-embedding:0.6b", input=text)["embeddings"][0])
# 1. Ingest
docs = ["Qwen3.5 supports 256K context.",
"The Mac mini M4 has 120 GB/s memory bandwidth.", "..."]
store = [(d, embed(d)) for d in docs]
# 2. Retrieve top-k by cosine similarity
def cosine(a, b): return a @ b / (np.linalg.norm(a) * np.linalg.norm(b))
def retrieve(q, k=3):
qv = embed(q)
return sorted(store, key=lambda x: cosine(qv, x[1]), reverse=True)[:k]
# 3. Augment + generate
q = "How much memory bandwidth does the M4 have?"
ctx = "\n".join(d for d, _ in retrieve(q))
prompt = f"Answer using only this context:\n{ctx}\n\nQuestion: {q}"
print(ollama.generate(model="qwen3.5:4b", prompt=prompt)["response"])Start in memory, or with Chroma, to see the three steps. Then graduate to Qdrant, a vector database that runs locally and adds the pieces the rest of this section needs. Keep it bound to loopback, for the reasons Part 1 gives.
The question and the chunks must be embedded by the same model.
Beyond top-k
Naive "embed, cosine, top-k" is the floor, not the target. The local-first default in mid-2026 is a three-stage retrieve, fuse, rerank pipeline, and all of it runs on a 16 GB machine.
- Hybrid search (dense + sparse). Index both a dense vector and a sparse keyword vector per chunk. Qdrant fuses the two result lists in one Query API call with Reciprocal Rank Fusion (RRF). Dense catches paraphrase; sparse catches exact identifiers, code symbols and rare terms that cosine misses. If you already run Qdrant, this is a configuration change.
- Rerank the top 20–50. A cross-encoder reranker reads the question and each candidate chunk together and scores them, which is more accurate than comparing two vectors. Keep the top 5–10. On a corpus of a few thousand chunks this adds well under a second of query latency.
- Contextual Retrieval (Anthropic, 2024). The highest-leverage upgrade. Before embedding each chunk, prepend a short model-generated sentence that situates it in its parent document, and index that contextualised text in both the dense embedding and the sparse index. Anthropic measured: contextual embeddings alone cut top-20 retrieval failures 35% (5.7% to 3.7%); adding contextual BM25 reached 49% (to 2.9%); adding a reranker reached 67% (to 1.9%). The cost is a one-time preprocessing pass of one model call per chunk. On a paid API Anthropic quotes about $1.02 per million document tokens; locally it is zero dollars, because it is just
qwen3.5:4brunning an overnight batch. No query-time penalty, and it stacks on hybrid search and reranking.
Ollama has no rerank endpoint, so it runs as a small llama.cpp sidecar. The Embeddings section above has the setup and the GGUF gotcha.
Two cheap upgrades before heavier machinery.
- Late chunking. If your embedder has a long context (Qwen3-Embedding's 32K qualifies; EmbeddingGemma's 2K does not), embed the whole document in one pass and pool per chunk afterwards, so each vector carries document-wide context. Reported gain about 3.6% average nDCG, with no extra model calls (arXiv 2409.04701).
- HyDE, gated. Embed a hypothetical answer instead of the question. It helps vague queries but backfires in specialised domains through "knowledge leakage", so only fall back to it when the query-to-document similarity is low.
Three escalations, each with a real cost
| Method | When it earns its cost | What it costs |
|---|---|---|
| Graph RAG (LightRAG, LazyGraphRAG) | Multi-hop "connect the dots" questions | Microsoft's original GraphRAG indexed at about $33K per corpus, a non-starter locally. LightRAG (EMNLP 2025) and LazyGraphRAG fixed the economics: LazyGraphRAG "won all 96 comparisons" at GPT-4o parity while cutting indexing to about 0.1% of GraphRAG. Build the graph with qwen3.5:4b overnight. Overkill for single-fact lookup. |
| Agentic, Self-RAG, CRAG | Robustness, not as the default | One comparison found agentic RAG costs 3.3x input and 1.9x output tokens and 1.5x latency for about +2.8 NDCG@10 (arXiv 2601.07711). Self-RAG had the lowest hallucination rate (5.8%). Add a single retrieval-grading step ("is this context actually relevant?") locally; leave full agentic query planning to a GPU machine on your network or a hosted frontier model. |
| ColPali, ColQwen | PDFs, scans, visually structured corpora | Late-interaction vision retrievers beat text-chunk RAG decisively here (ColQwen3-4B was the ViDoRe leader in June; that leaderboard moves fast, so re-check). Qdrant stores the token-level multivectors natively. Enable ColBERTv2 residual quantization (256 bytes per token down to about 20–36, a 6–10x saving) so the index fits in 16 GB. On Qdrant 1.18/1.19 or later, TurboQuant / Turbo4 (4-bit, about 9x smaller, no rescoring) is also an option. |
Measure it
Don't adopt any of this on vibes. RAGAS scores a pipeline on Context Precision, Context Recall, Faithfulness and Response Relevancy, and its judge model can be a local Ollama model. Build a small gold set of question-to-expected-chunk pairs for your own corpus, then A/B each upgrade against it. Context Recall is close to Anthropic's "retrieval failure rate", so you can reproduce their before-and-after numbers on your own notes.
A model grading a model is a known bias. Treat LLM-as-judge scores as directionally useful for ranking variants, not as absolute truth. The gold set of chunks you wrote by hand is the number that counts.
Part 2 · Build on it
A real vector store: Qdrant
Why a server, not an embedded store. An embedded store (Chroma in-process, or QdrantClient(":memory:")) lives inside one Python process. That is fine for the demo in the previous section and useless the moment several programs need to read and write the same memory at once: an agent storing a note while another agent, or the same agent in a second window, searches for it. Qdrant in server mode is a standalone Rust service with a REST API on port 6333 and gRPC on port 6334, write-ahead logging for durability, and SIMD-accelerated search. It is built for concurrent clients.
Run it bound to loopback
docker run -p 127.0.0.1:6333:6333 -p 127.0.0.1:6334:6334 \ -v "$(pwd)/qdrant_storage:/qdrant/storage:z" \ qdrant/qdrant
The 127.0.0.1: prefix is the security-critical part. It publishes the two ports on loopback only, not on every interface, for the same reason Part 1 keeps Ollama on 127.0.0.1. The dashboard is at http://localhost:6333/dashboard.
On an NVIDIA PC, GPU indexing is optional. The image above runs on the CPU and needs no GPU flag. That is fine for a personal store, and it leaves the VRAM for the models. Qdrant also publishes an NVIDIA image that builds indexes on the GPU (Qdrant's GPU guide):
docker run --gpus all \ -p 127.0.0.1:6333:6333 -p 127.0.0.1:6334:6334 \ -e QDRANT__GPU__INDEXING=1 \ -v "$(pwd)/qdrant_storage:/qdrant/storage:z" \ qdrant/qdrant:gpu-nvidia-latest
--gpus all hands the container your NVIDIA GPUs. It only works once the NVIDIA Container Toolkit is installed on the host.
Create a collection
from qdrant_client import QdrantClient, models
client = QdrantClient(url="http://localhost:6333")
client.create_collection(
collection_name="agent_memory",
vectors_config=models.VectorParams(size=1024, distance=models.Distance.COSINE),
quantization_config=models.ScalarQuantization(
scalar=models.ScalarQuantizationConfig(type=models.ScalarType.INT8, always_ram=True)
),
)What the options mean. size=1024 is the dimension of the embedding model that produced the vectors (Qwen3-Embedding-0.6B; it would be 768 for nomic); distance is how two vectors are compared, cosine here. Scalar (int8) quantization maps each float32 to a 1-byte integer, cutting vector memory by about 75% with minimal accuracy loss; the originals stay on disk for rescoring. That matters on a 16 GB machine.
v1.19 (Aug 5) unified them under one memory parameter with the values pinned, cached and cold. The old on_disk, always_ram and on_disk_payload are deprecated but still work, so the snippet above runs. The same release removed the legacy /search, /recommend and /discover endpoints (use /query) and added Turbo4: 4-bit TurboQuant with no full-precision copy, about 9× smaller, no rescoring.
A collection cannot later be changed from 768 to 1024 dimensions, or from cosine to euclidean, without re-indexing. Match the embedding model before the first insert. A very common bug: a collection auto-created at the OpenAI default of 1536 dimensions rejects the 1024-dimension vectors from Qwen3-Embedding-0.6B. Set the dimension explicitly, and keep the assert in the insert below so a wrong embedder fails loudly. The same mismatch, with nomic's 768 dimensions, is a documented issue between Mem0's open-source release and nomic-embed-text.
One collection, many agents
Each point carries a JSON payload. Use it to record where a memory came from, so one store can serve any agent that speaks MCP or the OpenAI-compatible API while every memory stays attributable:
vector = embed("User prefers pytest and type hints").tolist()
assert len(vector) == 1024, f"embedder gave {len(vector)} dims, collection expects 1024"
client.upsert("agent_memory", points=[models.PointStruct(
id=1, vector=vector,
payload={"source_agent": "claude_code", "timestamp": "2026-06-18T10:00:00Z",
"tags": ["python", "preferences"], "trust_level": "high"}
)])A single shared collection plus filtered search on source_agent, trust_level and tags is the whole namespacing scheme. No second collection per agent, no separate database. Filter at query time instead.
Security notes
Qdrant has a 2026 CVE that hits this exact setup. CVE-2026-25628 (CVSS 8.5): the unauthenticated /logger endpoint accepts an attacker-controlled log path, which allows arbitrary file writes and config override, with potential master-key escalation in containers, in Qdrant 1.15.5 and earlier. Mitigations:
- Upgrade. 1.15.6 fixes this one, but run the latest release, v1.19.x at the time of writing.
- Bind to loopback (or to a private mesh-network interface only), as in the
docker runabove. Keep both 6333 and 6334 off every other interface. - Set both an API key and a master key.
- Do not expose the Web UI. The dashboard is for you, on your machine.
GHSA-3gph-6c29-p29v (High): a read-only API key or JWT is accepted on the internal gRPC API when enforce_internal_auth is on. Check the advisory for the patched version, and keep 6333 and 6334 on loopback regardless.
Part 2 · Build on it
Harden it
Part 1's "Keep it on your machine" gave the five rules: loopback only, latest version, pull from publishers you trust, treat output as untrusted text, reach it over a mesh VPN if at all. Those rules were written for one model answering one person. The moment a model gets retrieval, memory and tools, the attack surface changes shape: the model file, the stored memories, the tool descriptions and the retrieved documents are all inputs an attacker can shape. This section goes deeper on each. The next one asks which work belongs on the local box at all.
The Ollama CVE record, in detail
Why patching is not optional. Ollama ships with no built-in authentication; the maintainers' stated position is to put a proxy in front of it. Every vulnerability is therefore unauthenticated wherever the server is reachable. Two documented incidents show the pattern:
- CVE-2024-37032, "Probllama" (Wiz, disclosed May 5, 2024). A path-traversal bug in the model-pull and registry mechanism chained to unauthenticated remote code execution as root. Fixed in version 0.1.34, released May 7, 2024. Wiz counted "over 1,000 Ollama exposed instances hosting numerous AI models without any protection." The Docker default, running as root and bound to
0.0.0.0, made it worse. - CVE-2026-7482, "Bleeding Llama" (Cyera, CVSS 3.1 of 9.1, CVSS 4.0 of 8.8). A heap out-of-bounds read in the GGUF model loader:
WriteTo()trusted attacker-declared tensor shapes. It is an information leak, not code execution, but what leaks is exactly what matters on a shared box: environment variables, API keys, system prompts and other users' conversation data, pulled out through/api/createand/api/push. Cyera described roughly 300,000 Ollama servers exposed on the public internet as "immediately and broadly exploitable – no credentials required." Disclosed February 2, fixed February 25 in Ollama 0.17.1, published May 1, 2026.
"Keep Ollama at 0.17.1 or later" fixed one bug and is no longer a safe-version statement. CERT/CC VU#518910, CVE-2026-5757, describes a second GGUF heap leak, this time in the quantization engine: upload a crafted model, read heap, push it out through the registry API. As of the note's 2026-04-22 revision the vendor was unreachable and "a patch is not yet available"; the current status is unverified. The advice does not depend on it: run the latest Ollama (v0.40.1 as of October 2026), keep it on 127.0.0.1, and restrict who can create or push models.
The GGUF-parser attack class
Loading is enough. The dominant 2026 class is not an Ollama bug at all. It is memory-safety bugs in the shared llama.cpp/ggml GGUF parser that Ollama, LM Studio and others wrap. CVE-2026-33298 is an integer overflow in ggml_nbytes() leading to a heap overflow and remote code execution, fixed in llama.cpp build b7824. CVE-2025-49847 is a buffer overflow in vocabulary loading, fixed in b5662. The exploit fires during the parse, before a single token is generated. A malicious model file does its work the moment it is opened.
Three rules follow. Pull only from the official Ollama library or a verified Hugging Face organisation, never a random mirror. Verify the SHA256 for anything sensitive. Track the embedded llama.cpp build number, not just the Ollama version, because the fix often lands upstream first.
llama.cpp's llama-server had a use-after-free reachable through --sleep-idle-seconds (CVE-2026-43631 and CVE-2026-43632, CVSS 4 score 9.2). Don't use that flag.
Poisoned chat templates
A model card can look fine and still carry a payload. The tokenizer.chat_template field is a template rendered on every request. Rendered through an unsandboxed Jinja2 environment it becomes a code-execution path: CVE-2024-34359 in llama-cpp-python, and more recently SGLang CVE-2026-5760 at CVSS 9.8. Ollama uses Go templates and is less exposed. The risk matters on a GPU machine on your network running vLLM or SGLang: keep them patched, never enable trust_remote_code on a remote model (CVE-2026-27893, CVSS 8.8, fixed in vLLM 0.18.0), and never serve either API unauthenticated. Even over a mesh VPN, put an auth token in front. CVE-2026-22778 (CVSS 9.8) is an unauthenticated heap overflow reached by a crafted media URL sent to the multimodal JPEG2000 decode path.
The hardening checklist
- Bind to
127.0.0.1everywhere. Ollama, the vector database, the memory server. Never default to0.0.0.0. The loopback default is itself a strong boundary. - Patch promptly. Run the latest Ollama. Updates fix real code-execution and memory-leak bugs.
- Run as non-root.
- Prefer GGUF and safetensors over pickle formats. Pickle can execute arbitrary code on load.
- Sandbox the agent's tool-execution layer, not the inference. The risk is an agent running shell and file commands. Isolate that in Docker, Lima or Apple
container. The inference server needs network isolation, not a sandbox. - Treat shared memory as untrusted data. Tag every memory with its provenance and a
trust_level. Never auto-execute a retrieved instruction. Keep trusted and untrusted ingestion paths separate. Log every read and write. Read what is actually stored.
Memory poisoning
Poison once, exploit forever. An attacker plants instructions in a document the agent ingests. The agent writes them to memory. Days later, in a different session, a benign query retrieves them and they run. OWASP made this formal: the Top 10 for Agentic Applications (the "2026" edition, released December 2025, ASI01 to ASI10) lists Memory and Context Poisoning as ASI06. A multi-agent setup also touches ASI02 (Tool Misuse) and ASI07 (Insecure Inter-Agent Communications).
The research numbers are not comforting. AgentPoison (Chen et al., NeurIPS 2024) "achieves an average attack success rate of ≥80% with minimal impact on benign performance (≤1%) with a poison rate <0.1%." PoisonedRAG (Zou et al.) reaches a "90% attack success rate when five malicious texts are injected into a knowledge database containing millions of texts." MINJA (arXiv 2503.03704) reaches over 98% memory-injection success (76.8% end to end) using only normal user queries: it never writes to the database directly, it tricks the agent into storing the poison, and a later benign query triggers it. A fully local store with no inbound connections is still exploitable if it ingests any web or retrieved content.
ASI06 names five defence layers: input moderation, memory sanitisation with provenance, trust-aware retrieval, behavioural monitoring and forensic logging. The two cheap local wins are the ones already on the checklist: tag every memory write with its source, and never treat retrieved text as an instruction.
MCP tool poisoning and least privilege
The tool description is also an input. Official guidance now exists: the NSA AI Security Center published an MCP Cybersecurity Information Sheet in May 2026, and the Five Eyes issued joint guidance on the "careful adoption of agentic AI". The core MCP threat is tool poisoning, also called line jumping: a malicious server hides instructions in a tool description, which any agent that speaks MCP ingests as trusted context. Studies find many MCP clients perform no validation of server-provided metadata at all.
Apply least privilege. Scope each server's tools narrowly. Install only servers whose source you have read, and pin versions rather than npx-fetching whatever is current. Never auto-trust a dynamically discovered server. Watch for the confused-deputy case, where an over-broad token held by one server is abused through another.
Two prompt-injection defences that run locally
Both fit a 16 GB machine.
- Architectural: CaMeL. "Defeating Prompt Injections by Design" (arXiv 2503.18813, DeepMind) splits the work between two models. A privileged planner sees only trusted instructions; a quarantined model reads the untrusted retrieved content; tool calls are gated by capability and provenance. It solves 77% of AgentDojo tasks with provable security, against 84% undefended, at roughly 2.7× the token cost, which on a local box is free. The pattern holds even without the framework: separate the agent that plans and acts from the one that reads web or retrieved content, and never let untrusted data drive control flow.
- Guardrail model: Qwen3Guard. Apache-2.0, same family as the base model, in 0.6B, 4B and 8B sizes, covering 119 languages, in a Gen variant (full context) and a Stream variant (token-level, live). It reportedly beats WildGuard-7B on response classification (0.6B-Gen F1 78.3 against 76.8). The 0.6B, an estimated 0.4 to 0.5 GB at Q4, runs alongside
qwen3.5:4bas an input and output moderator with negligible footprint and keeps the stack multilingual. Llama Guard 4 (12B) and Granite Guardian 3.3 are heavier alternatives that won't co-reside comfortably in 16 GB.
Part 2 · Build on it
Route the work
Each tier does what it is best at. Hardening decides how a box is exposed. Routing decides what each box is trusted to do.
| Tier | What it is | What it does well |
|---|---|---|
| Always-on local box | The 16 GB machine, private, zero marginal cost | Embeddings, the vector database, the memory server, retrieval preprocessing, fast qwen3.5:4b classification and summarisation |
| A GPU machine on your network | Reached over a mesh VPN such as Tailscale (encrypted WireGuard, no port forwarding) | Heavier generation: larger Qwen models, longer contexts, CUDA-class speeds |
| A hosted frontier model | High-stakes reasoning, run remotely | Hard multi-step agentic work, complex tool orchestration, anything where correctness matters more than privacy or cost |
The local tier's memory budget fits: an embedding model such as Qwen3-Embedding-0.6B at about 639 MB, a vector database such as Qdrant at a few hundred MB, and a 3B to 9B generative model at 3.4 to 6.6 GB all coexist within the roughly 9 to 12 GB usable on a 16 GB machine.
Will the whole stack fit on 16 GB?
A 16 GB Mac keeps about 4 GB for macOS, leaving a usable window of roughly 9–12 GB. The model, the retrieval stack and the KV cache all have to land inside it.
Planning bands, not a measurement. The KV cache is shown as the 1–2 GB band the sizes above imply, not a per-token formula. Watch your memory monitor on the real machine; on a 24 GB or 32 GB machine the same arithmetic gives you much more room.
For the GPU machine, bind Ollama to the tailnet address (OLLAMA_HOST=100.x.y.z) or keep it on localhost and use Tailscale Serve. Add a host firewall rule that allows the Ollama port only on tailscale0. Never expose port 11434 to the public internet; there is no authentication behind it.
The tool-calling ceiling
There is a cliff around 7 to 9B. Qwen's published BFCL V4 results: Qwen3.5 27B about 68.5%, 9B about 66.1%, then a sharp drop to 4B about 50.3% and 2B about 43.6%. Independent diagnostics find tool-initialisation failures catastrophic below about 14B: one 3B model never invoked a single tool across nine tasks, and an arXiv diagnostic measured qwen2.5:3b at 89% errors against 0% for qwen2.5:32b and GPT-4.1.
Epoch AI rated BFCL v4 "Flawed" on 2026-09-10, with defects that could affect accuracy in 24 of 50 sampled tasks. Read the 66, 50 and 44 as a ranking, not precise scores.
Reliability compounds. Even 95% success per call, over eight steps, lands about 66% of the time.
Why multi-step agent work compounds
If each tool call succeeds with probability p, a chain of n calls succeeds with pⁿ.
A simplification. Real failures aren't independent, and a validator or retry step changes the picture. BFCL scores are a relative signal, not a per-call probability. The point holds: long unsupervised chains on a small local model fail often, so keep approval gates in the loop.
A 2026 LM Studio tool-calling evaluation found qwen3.5:4b scored 97.5% on its 40-case structured-output suite, beating much larger models. For narrow, well-specified structured calls the 4B can excel. The BFCL drop reflects harder multi-step, general agentic tasks.
The practical conclusion. Use the local 9B for memory recall, simple tool calls and structured extraction; it tool-calls well for its size. Keep multi-step autonomous agent work on a hosted frontier model, and where an agent does run locally, add approval gates and scoped harnesses rather than trusting full autonomy.
On a single 24 GB PC
The tiers above assume a 16 GB Mac plus a GPU machine elsewhere. If your one machine is a PC with a 24 GB NVIDIA card, such as an RTX 3090, the local tier and the GPU tier are the same box. Put the models in VRAM and everything else in system RAM.
| Piece | Where it runs | Budget | Source |
|---|---|---|---|
| Qwen3.8-27B Q4_K_M, the chat and tool-calling model | GPU | 19 GB | GGUF file size (ggml-org) |
Qwen3-Embedding-0.6B, qwen3-embedding:0.6b (Q8_0) | GPU | about 0.64 GB | file size (Ollama library) |
bge-reranker-v2-m3 Q8_0, as a llama-server sidecar | GPU, on demand | about 0.6 GB (budget 0.6–1 GB) | file size |
| KV cache and CUDA overhead | GPU | what is left, about 3–3.5 GB | derived |
| Qdrant or the memory server | System RAM and CPU | a few hundred MB | as in the 16 GB budget above |
That is about 20.2 GB of weights on a 24 GB card. The 3 GB or so left is the context budget. Qwen3.6-27B keeps a KV cache only in its 16 full-attention layers, about 64 KiB per token at f16, which is 2 GiB per 32K tokens. If Qwen3.8 keeps the same layout (UNCERTAIN), plan on roughly 32K tokens at f16, or about twice that with q8_0 KV. Need more room? Run the reranker only when a query needs it, or pick a smaller quant of the chat model.
Speed is an estimate, not a measurement. The bandwidth ceiling for a 27B Q4 on a 3090 is about 49 tokens per second (derived). The nearest real number is Qwen3.6-27B at Q3_K_XL, 32–38 tokens per second, from a community repo (club-3090, secondary).
A 27B clears the tool-calling cliff. The cliff in The tool-calling ceiling above sits around 7 to 9B, and tool-initialisation failures were catastrophic below about 14B. Qwen's BFCL V4 figure for Qwen3.5 27B is about 68.5%, against about 50.3% for the 4B. Qwen3.8-27B's own score is not checked here (UNCERTAIN), and BFCL is a ranking, not a precise score. On this box you can move memory writes, the extraction step Shared memory warns you not to give the 4B, onto the 27B, and try more agent work locally. Keep the approval gates, and measure on your own tasks.
If the same GPU drives your monitor, the desktop holds some VRAM before any model loads. Check nvidia-smi first and size the context to what is actually free.
Part 2 · Build on it
A build order, with thresholds
Why stages. Each piece in this part is small, and each depends on the one before it: there is no retrieval without embeddings, no shared memory without a store that several processes can reach, and no point hardening a system that doesn't run yet. The order below is one working piece a week. Each stage has a deliverable and, where the plan states one, a threshold. A threshold is a thing you can explain or measure, not a feeling of being done. If you can't clear it, the next stage will hide the gap rather than fix it.
| Stage | Build | Deliverable | Threshold to advance |
|---|---|---|---|
| 1 (week 1) | The smallest working thing: Ollama, qwen3.5:4b, the six 16 GB settings from Part 1, the memory monitor open. On an NVIDIA PC: the same model, the Mac settings skipped, nvidia-smi open as the monitor | A local chat loop you understand mechanically | You can explain why decode is bandwidth-bound and predict tokens per second from model size (a 3090 has about 936 GB/s, derived) |
| 2 (week 2) | Embeddings, then "chat with my notes": pull qwen3-embedding:0.6b, write the 30-line in-memory retrieval script | A script that answers questions over your own Markdown notes with cited chunks | Retrieval returns a sensible top-k, and you can articulate cosine similarity |
| 3 (week 3) | A real vector store: Qdrant on localhost, the collection recreated at 1024 dimensions, cosine distance and int8 quantization, your notes migrated, payload metadata added | Retrieval backed by Qdrant with source_agent and trust_level tags, inspected in the dashboard | Every stored chunk carries source_agent and trust_level, and you have checked them in the dashboard |
| 4 (week 4) | Shared memory over MCP: doobidoo/mcp-memory-service (HTTP, Python 3.12) or OpenMemory, connected to any agent that speaks MCP or the OpenAI-compatible API. Connect the easiest client first (Claude Code, for example), then a second (Codex). If a client's HTTP transport misbehaves, the mcp-remote stdio bridge is the fallback, pinned to a version above 0.1.38 rather than @latest (0.1.16–0.1.38 are reported vulnerable, CVE-2026-51997 and CVE-2026-51994; secondary sources only, verify the range) | Two agents reading and writing the same memory | A fact stored by one agent is recalled by the other |
| 5 (week 5 and after) | Harden and route: confirm every bind is 127.0.0.1; update Ollama to the latest release; reach a GPU machine on your network over a VPN or mesh such as Tailscale for heavy generation; sandbox the agent tool layer; enforce trust_level tagging and "never execute retrieved instructions"; turn on audit logging | A documented routing policy across local, a GPU machine on your network and a hosted frontier model, plus a memory-curation and audit routine | The routing policy is written down, and you have run the curation and audit routine once |
| 6 (optional, any time after stage 1) | A first LoRA: mlx-lm QLoRA on Qwen3.5-4B-4bit, or the 0.8B or 2B first, batch 1, few layers. On an NVIDIA PC: Unsloth bf16 LoRA on Qwen3.5-4B (Unsloth lists about 10 GB, and does not recommend QLoRA on Qwen3.5). Part 3 covers both paths | An adapter served by mlx_lm.server, or on a PC a GGUF loaded with ollama create | It beats the untuned base on a held-out set you wrote before training, and you recorded the peak memory |
That Ollama version was the floor for one bug, CVE-2026-7482, and is no longer a safe-version statement. Stage 5 means the latest release, not that number.
Benchmarks that change the plan
Two measurements rewrite the stages above. Memory. On a 32 GB Mac, Ollama's MLX runner (the default since v0.40 for supported models, about 2× decode per Ollama) gets the memory it is documented for, larger models run locally, and the GPU machine on your network matters less. On a 24 GB NVIDIA card the same is true, and the single-PC budget in Route the work replaces the two-box plan. Tool-calling reliability. If the 9B clears your task's reliability bar in your own tests, push more routine agent work local; if it doesn't, keep that work on a hosted frontier model. Measure it. Don't assume it.
Caveats specific to this part
- The M4 speed numbers are estimates. No direct, attributable measurement of Qwen3.5 on a base M4 16 GB exists in public sources (re-checked October 2026). The closest hard evidence is dense-model proxies in llama.cpp discussion #4167: a base M4 running LLaMA-7B Q4_0 at 24.11 tokens per second, and an M4 Mac mini with 24 GB logging
llama3.2:3bQ4_K_M at 45.95 andqwen2.5-coder:7bQ4_K_M at 22.60. Those imply an effective 92–106 GB/s, 77–88% of the 120 GB/s peak. Scaling by file size (the guide's own arithmetic) gives roughly 27–31 tokens per second for the 4B and 14–16 for the 9B; the planning ranges are about 25–35 and 13–18. Qwen3.5 is a hybrid architecture, so the dense proxy is not exact; MLX and MTP paths are unmeasured here. Benchmark your own box withollama psand real prompts. - Qwen3.5 tooling is new. Some sources noted GGUF and vision (
mmproj) edge cases in Ollama; the official library listsqwen3.5:4band:9bas natively runnable. Verify on your installed version. - KV-cache quantization isn't free on Metal. Expect a possible 5–10% decode slowdown, and test quality on multimodal or high-GQA models before committing.
- The MCP ecosystem moves fast. Client HTTP transports, Mem0's API (a v2 overhaul in 2026) and OpenMemory's transport (SSE to streamable HTTP) are all in flux. Pin versions and re-check config syntax.
- Security is ongoing, not one-time. Ollama has no authentication, so each new CVE is unauthenticated by default. Qdrant below 1.15.6 carries CVE-2026-25628, so run 1.15.6 or later, and preferably the latest. Subscribe to advisories and keep the loopback-only and patch discipline permanent.
Part 2 · Build on it
Learn more: building on it
For a structured route from here, see Track 5 in the free learning path.
- Qwen3-Embedding-0.6B model card and its Ollama tag: the embedder this part uses, 1024 dims, 32K context, Apache-2.0.
- Nomic Embed technical report (arXiv 2402.01613): the open, reproducible 8192-context embedder behind
nomic-embed-text; 137M parameters, 768-dim output, 8192-token context, Apache-2.0. - Qdrant documentation: collections, distance metrics, scalar quantization, the unified
memoryparameter from v1.19 (replacingon_disk,always_ramandon_disk_payload), and the/queryAPI that replaced the removed/search. Run 1.15.6 or later. - doobidoo/mcp-memory-service: the shared-memory server used in stage 4; pin v11.15.0 or later.
- The MCP specification, 2026-07-28 revision: stateless transport, the required
Mcp-MethodandMcp-Nameheaders, and the formal deprecation of HTTP+SSE. - OWASP Top 10 for Agentic Applications (released December 2025): memory and context poisoning is ASI06, tool misuse ASI02, insecure inter-agent communication ASI07.
- Memory and retrieval poisoning: AgentPoison (Chen et al., NeurIPS 2024), PoisonedRAG (Zou et al.) and MINJA (arXiv 2503.03704), which plants memories using only normal user queries.
- CaMeL: Defeating Prompt Injections by Design (arXiv 2503.18813): the dual-LLM pattern, a privileged planner and a quarantined reader, with provable guarantees at about 2.7× token cost.
- Memory architectures: A-Mem (arXiv 2502.12110), a Zettelkasten note graph that auto-links and evolves older notes; sleep-time compute (arXiv 2504.13171), consolidation off the latency path; and the agentic memory survey arXiv 2602.19320, including its finding that the extraction model, not the framework, bounds memory quality.
- Retrieval upgrades: late chunking (arXiv 2409.04701) and the agentic RAG cost comparison arXiv 2601.07711.
- Anthropic's Contextual Retrieval post (2024): the measured 35%, 49% and 67% cuts in retrieval failure, reproducible locally with an overnight batch on the 4B.
- RAGAS: context precision and recall, faithfulness and response relevancy, with a local Ollama model as the judge; discount LLM-as-judge wins.
Part 3 · Foundations
What fine-tuning actually changes
Tokens are pieces. A tokenizer turns text into numbered pieces: sometimes a word, sometimes part of one, punctuation or whitespace. The exact split depends on the model.
Weights are numbers. A model is billions of learned numbers that turn input tokens into a prediction of the next token. They are not a database of facts you can edit, and a chat does not change them. A conversation only changes the context.
Training changes the numbers. Inference uses the weights as they are. Training shows the model examples, scores its predictions against them, computes how each weight affected the error, and nudges the weights to reduce it. Fine-tuning is just more training, on your examples, starting from a model that already works.
There are four ways to change how a local model behaves. Only one of them touches the weights:
| Method | What changes | Reach for it when |
|---|---|---|
| Prompt / few-shot examples | Instructions read at inference time | Always first. It's free and instant to undo. |
| Retrieval (RAG) | Relevant documents placed in context | The model lacks facts: your notes, docs, codebase. |
| Better tools / validators | Code that runs and checks the output | The output must be correct, not just likely. |
| Fine-tuning | The model's weights (or an adapter) | The model has the facts but keeps answering in the wrong shape. |
It makes patterns more likely. A model trained on your docs will sound like your docs and still invent details. For knowledge, use retrieval. For correctness, use a validator. Fine-tune for behaviour.
Why can a pile of numbers hold behaviour at all? Because a program built from comparisons and arithmetic can be rewritten as multiplies, adds and ReLU, the same operations a network layer performs. Cheng Lou's handwired (MIT) does this by hand for ordinary JavaScript; its live demo goes much further. The toy below takes its first example: nothing here is Qwen, but the point carries over.
A program, written as weights
Left, a one-line program. Right, the same program rewritten as multiplies, adds and relu(x) = max(0, x), the operations a network layer performs. Move a and watch both compute.
a = 4
result = a > 4 ? 10 : 12
a = 4 above4 = relu(a − 4) − relu(a − 5) = 0 result = above4·10 + (1 − above4)·12
A program can be written as weights. That is why a trained network can also hold one: the numbers fine-tuning nudges are the same kind of object as these constants. Exact everywhere except between 4 and 5. There the gate ramps from 0 to 1 instead of jumping, so a = 4.5 gives 11 where the program gives 10. Adapted from Cheng Lou's handwired (MIT), rewritten here by hand.
Part 3 · Follow the numbers
One learning step, made visible
Supervised fine-tuning (SFT) shows the model examples of the answers you want. A forward pass predicts. A loss measures how wrong the prediction was. Backpropagation computes how each weight affected that loss. An optimizer moves the weights a little in the direction that lowers it. Repeat a few thousand times.
The toy below is one weight, one example and one step at a time. It isn't Qwen, but the arithmetic is the same arithmetic.
One weight, one example, one step
A single-weight logistic model with input 1 and no bias. p is the probability it gives the target answer. It isn't Qwen, but the update rule is the same one.
Training loss only. Falling loss means a better fit to this example. Switch to the wrong label: the loss still falls, because the model is learning the wrong answer well.
Loss is feedback, not truth. Cross-entropy loss punishes the model for giving low probability to the target. If the target is wrong, a lower loss means the model learned the wrong thing better. Flip the toy's label to "wrong" and watch the loss fall anyway. That's why the quality of your examples matters more than their quantity.
The learning rate is the step size. Too small and nothing changes. Too large and the model overshoots, then forgets what it knew. One secondary report of a Qwen3.5 LoRA on a Mac saw a learning rate of 1e-4 triple the error count against the untouched base, while 1e-5 cut it by two thirds. Start small.
Part 3 · Change fewer numbers
Full fine-tuning, LoRA and QLoRA
Full fine-tuning updates every weight. It's flexible, but you need memory for the weights, their gradients and the optimizer state for all of them. Unsloth puts it at roughly 4× the memory of LoRA.
LoRA (Hu et al., 2021, arXiv 2106.09685) freezes the original weights W and trains two small matrices per targeted layer: A (r × d_in) and B (d_out × r). Their product B·A has the same shape as W and gets added to it, scaled. The rank r is the bottleneck: at r = 16 on a 4096 × 4096 layer you train 131,072 numbers instead of 16.7 million. The adapter is a separate small file, so the base model stays untouched and you can switch adapters on and off.
QLoRA (Dettmers et al., 2023, arXiv 2305.14314) is LoRA on top of a base model stored in 4-bit. The base is smaller in memory, and the adapters still train in higher precision.
LoRA parameter counter
For one weight matrix of shape dout × din, LoRA trains A (r × din) and B (dout × r). Trainable = r·(din + dout) per matrix.
Arithmetic, not an inventory of Qwen's layers. Real counts depend on which modules you target. Higher rank means more capacity and more memory; it doesn't guarantee better results. (Qwen3.5-4B's hidden size is 2560; the 9B's is 4096.)
The standard formula scales the update by alpha / r. Unsloth and PEFT take lora_alpha. mlx-lm takes a scale value directly and errors if you pass alpha. Don't copy a config from one tool into another.
Unsloth's Qwen3.5 page says it plainly: "It is not recommended to do QLoRA (4-bit) training on the Qwen3.5 models, no matter MoE or dense." On a GPU, use bf16 LoRA on a smaller model rather than QLoRA on a bigger one. On a Mac with mlx-lm, a 4-bit base is how the 4B fits at all. Treat that as an experiment and evaluate it harder.
Part 3 · Will it fit?
Will it fit on your machine?
Start with the weights. Weight storage ≈ parameters × bits ÷ 8. A 4B model at 16-bit is about 9.3 GB of weights; at 4-bit, about 2.3 GB. (Qwen3.5-4B has more than 4 billion parameters, and a real Q4_K_M file lands nearer 2.5 GB, as in Part 1.)
Weight storage
bytes = parameters × bits ÷ 8. GB is 109 bytes; GiB is 230.
Weights only. This is not a training-fit estimate. Training adds activations, gradients, optimizer state and buffers on top. Counts include the vision encoder: 4B and 9B from the official checkpoints, 0.8B and 2B from same-architecture derived checkpoints.
Training needs far more than the weights. On top of them come activations saved for the backward pass, gradients, optimizer state, the adapter itself, and temporary buffers. Gradient checkpointing trades compute for memory. Shorter sequences and batch size 1 help most. No formula replaces measuring.
Find your real budget.
- Mac: MLX won't let the GPU use all of your unified memory. Ask it:
python -c "import mlx.core as mx; print(mx.device_info())"
Read
max_recommended_working_set_size. On a 16 GB Apple Silicon Mac it measured ≈11.8 GiB, about 75% of RAM. - NVIDIA: run
nvidia-smiand read the memory column. Close the browser and anything else using the GPU first.
What fits, Qwen3.5 LoRA, October 2026:
| Machine | Realistic target | Evidence |
|---|---|---|
| NVIDIA 8 GB | 0.8B or 2B | Unsloth bf16 LoRA: 0.8B ≈ 3 GB, 2B ≈ 5 GB |
| NVIDIA 12 GB | 4B, tight | Unsloth bf16 LoRA: 4B ≈ 10 GB |
| NVIDIA 16 GB | 4B, comfortable | same table |
| NVIDIA 24 GB | 9B, tight | Unsloth bf16 LoRA: 9B ≈ 22 GB |
| NVIDIA 32 GB (RTX 5090) | 9B, with headroom | Unsloth bf16 LoRA: 9B ≈ 22 GB; 27B ≈ 56 GB does not fit |
| 2× 24 GB (two 3090s) | 9B on one card; a split across both is UNCERTAIN | Unsloth multi-GPU uses device_map = "balanced". How well it works on the Qwen3.5 hybrids is UNCERTAIN |
| Qwen3.5-27B, any machine above | does not fit | Unsloth bf16 LoRA: 27B ≈ 56 GB, more than 48 GB |
| Mac 16 GB | 0.8B / 2B; 4B on a 4-bit base, tight | ~11.8 GiB GPU budget (measured) |
| Mac 24 GB | 4B on a 4-bit base | Secondary: 4B LoRA peaks of ~10.5–15 GB on 24 GB Macs |
| Mac 32 GB+ | 9B on a 4-bit base, plausible | Secondary: one 9B run peaked at 13.2 GB. Untested here |
Unsloth's general minimums. These cover all model families, not Qwen3.5 in particular:
| Model size | QLoRA (4-bit) | LoRA (16-bit) |
|---|---|---|
| 8B | 6 GB | 22 GB |
| 14B | 8.5 GB | 33 GB |
| 27B | 22 GB | 64 GB |
| 70B | 41 GB | 164 GB |
Unsloth needs a GPU with CUDA capability 7.0 or newer, which means RTX 20 series and up.
Unsloth advises against QLoRA on every Qwen3.5 model, so read the Qwen3.5 rows above as bf16 LoRA. In bf16, the 27B needs about 56 GB and the 35B-A3B about 74 GB. Neither fits on two 3090s.
A well-tuned 2B that does one job reliably beats a 9B that doesn't fit. Run your test set against the untrained 0.8B, 2B and 4B first. If the 2B is close, tune the 2B.
Part 3 · Before you train
Decide before you train
Write these down before you touch a training command. They take an hour and save days.
- The behaviour, in one sentence. "Turn a git diff into a one-line conventional-commit message." Not "be better at coding."
- A held-out test set. 30–100 real inputs with the answer you'd accept, written before training and never shown to the model during training.
- A pass rule you can check. Exact match, a JSON schema, a regex, a small script, or a rubric you apply by hand. "Looks good" is not a pass rule.
- The baseline score. Run the untrained model on the test set with your best prompt. If it already passes, you're done. Don't train.
- A stop rule. "If the adapter doesn't beat the baseline by X on the test set, I delete it."
Qwen3.5 4B and 9B think by default: they write a <think> block before answering. Only the 0.8B and 2B default to non-thinking. Decide which mode you'll use in production and measure the baseline in that mode. Turn thinking off with enable_thinking=False in the chat template (Hugging Face, MLX, llama.cpp) or --think=false / a top-level "think": false in Ollama.
Part 3 · The part that matters most
Build the dataset
Format. Both paths below read JSON Lines: one example per line. Use the chat format, so the model learns in the same shape it will be used in:
{"messages": [{"role": "system", "content": "You write one-line commit messages."}, {"role": "user", "content": "diff --git a/api.py ..."}, {"role": "assistant", "content": "fix(api): handle empty payload"}]}Size. For a narrow behaviour, 100–1,000 good examples is a normal starting range. Quality dominates: one wrong example teaches the wrong thing as hard as one right example teaches the right thing.
Checklist for every dataset:
- Real inputs. Use the messy inputs you actually see, not tidy ones you invent.
- Consistent answers. Two examples with the same kind of input should not disagree on format.
- Coverage. Include the edge cases and the "I can't do that" cases you want handled.
- No leaks. Remove duplicates, and make sure no test example, or a near-copy of one, is in the training data.
- Splits.
train.jsonlfor training,valid.jsonl(about 10%) to watch for overfitting during training, and your separate test set for the final decision. - Rights and privacy. Only train on text you're allowed to use. Strip secrets, keys, emails and personal data. Assume anything in the training data can come back out of the model.
Train only on the answers. You usually want the model to learn the assistant turns, not to imitate your prompts. mlx-lm has --mask-prompt. Unsloth has a train_on_responses_only helper (see its docs for the exact call).
Thinking traces. If you train on plain answers with thinking off, expect the model's reasoning to weaken. Unsloth's Qwen3.5 guidance: if you want to keep reasoning, mix in reasoning-style examples, with "a minimum of 75% reasoning." If you only ever run the model with thinking off, train with thinking off and evaluate that way.
Part 3 · Path A · Apple Silicon
Path A: a Mac with mlx-lm
mlx-lm is Apple's MLX library for language models. It's the training path with the fewest unknowns on a Mac in October 2026. It has supported the Qwen3.5 text model since v0.30.7 (Feb 2026). Its LORA.md family list still omits Qwen3/3.5; that list is stale, and the model code is there.
1 — Install (mlx-lm 0.32.0; needs macOS 15+ and Python ≥ 3.11):
uv venv --python 3.12 .venv-mlx && source .venv-mlx/bin/activate uv pip install "mlx-lm[train]" mlx_lm.lora --help
2 — Pick a base. For the 4B on a 16 GB Mac, use the 4-bit MLX conversion mlx-community/Qwen3.5-4B-4bit (≈3.0 GB). Pointing --model at a quantized model makes the run a QLoRA run. Its Hugging Face sidebar says "1.0B params"; that's a packed-weight counting quirk, not the wrong model. Vision weights are dropped on load, so this is text-only training. For a first run, use a smaller base such as the 0.8B or 2B.
3 — Put the data in a folder: data/train.jsonl, data/valid.jsonl, and data/test.jsonl for --test. mlx-lm auto-detects four formats: chat, tools, completions, text.
4 — Train with conservative settings:
mlx_lm.lora --model mlx-community/Qwen3.5-4B-4bit --train --data ./data \ --batch-size 1 --num-layers 4 --grad-checkpoint \ --max-seq-length 1024 --iters 600 --learning-rate 1e-5 --mask-prompt
Defaults worth knowing: rank 8, scale 20, Adam, learning rate 1e-5, adapters saved to ./adapters every 100 steps. If you run out of memory, shorten --max-seq-length first, then lower --num-layers. Keep Activity Monitor open and watch memory pressure, not just the loss.
Qwen3.5 mixes two layer types: in every block of four, three are Gated DeltaNet (linear attention, linear_attn) and one is normal attention. By default mlx-lm adds LoRA to every linear layer in the last --num-layers blocks, DeltaNet included. A 2026 study on Qwen3.5-0.8B (arXiv 2604.22127) found adapting the DeltaNet backbone destructive (−14.8 points on GSM8K), while attention-only adaptation beat full adaptation with 5–10× fewer trainable parameters. To try attention-only, pass a YAML config (-c config.yaml) whose lora_parameters.keys lists only the attention projections. Check the exact module names with model.named_modules() first. Untested here.
5 — Test and compare:
mlx_lm.lora --model mlx-community/Qwen3.5-4B-4bit --adapter-path adapters --data ./data --test mlx_lm.generate --model mlx-community/Qwen3.5-4B-4bit --adapter-path adapters --prompt "..."
6 — Serve it. mlx_lm.server --model mlx-community/Qwen3.5-4B-4bit --adapter-path adapters starts an OpenAI-compatible server on 127.0.0.1:8080. Use 0.32.0 or later: 0.31.3 silently ignored --adapter-path (mlx-lm issue #1248, fixed by #1249 in Aug 2026).
mlx_lm.fuse --export-gguf only supports llama, mistral and mixtral, and it refuses qwen3_5. A plausible path is mlx_lm.fuse --dequantize followed by ollama create from the fused safetensors folder (Ollama's importer registers Qwen3_5ForCausalLM). Whether mlx-lm's fused layout imports cleanly is untested. Until you've proven it, serve with mlx_lm.server.
Other trainers on a Mac:
- Unsloth added an Apple Silicon (MLX) path in Aug 2026, for SFT and LoRA. GRPO and DPO aren't covered, and no Qwen3.5-on-Mac run has been published yet.
- Axolotl is "partially usable" on a Mac: no bitsandbytes, no flash-attn, no bf16.
- LLaMA-Factory ships Qwen3.5 templates but makes no Mac support claim.
Part 3 · Path B · NVIDIA GPU
Path B: an NVIDIA PC with Unsloth
Unsloth is the standard single-GPU fine-tuning library. It has a Qwen3.5 guide and exports straight to GGUF for Ollama and llama.cpp.
Which card for training?
| Card memory | Model | Settings |
|---|---|---|
| 8 GB | Qwen3.5-2B (≈5 GB) | script defaults |
| 12–16 GB | Qwen3.5-4B (≈10 GB) | script defaults; tight on 12 GB |
| 24 GB (RTX 3090, 4090) | Qwen3.5-9B (≈22 GB), tight | max_seq_length = 1024, batch size 1 |
| 32 GB (RTX 5090) | Qwen3.5-9B | script defaults |
| 2× 24 GB | Qwen3.5-9B on one card | device_map = "balanced" to split, UNCERTAIN for Qwen3.5 |
The memory figures are Unsloth's bf16 LoRA numbers. The 1024-token setting for 24 GB is this guide's suggestion for headroom, not a measurement.
1 — Check the GPU before anything else:
nvidia-smi
Note the GPU memory and the CUDA version in the top-right corner. If the command fails, fix the NVIDIA driver first.
2 — Install. Pick one:
- Linux or WSL (uv):
uv venv unsloth_env --python 3.13 && source unsloth_env/bin/activate uv pip install unsloth --torch-backend=auto
- Native Windows (Miniconda, PowerShell). Install PyTorch for your CUDA version first; the example uses CUDA 13.0, so
cu130:conda create --name unsloth_env python==3.12 -y conda activate unsloth_env pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu130 python -c "import torch; print(torch.cuda.is_available())" pip install unsloth
The PyTorch check must print
True. Unsloth's Windows page calls Docker possibly "the easiest way for Windows users to get started" and suggests WSL if Docker doesn't see the GPU. GRPO and anything that needs vLLM require Linux or WSL. - No code: Unsloth Studio is a browser UI for the same engine. On Windows, install it with
irm https://unsloth.ai/install.ps1 | iex. On macOS, Linux or WSL, usecurl -fsSL https://unsloth.ai/install.sh | sh.
Qwen3.5 needs transformers v5; older versions won't load it. Unsloth installs v5 by default.
3 — Train. This script is adapted from Unsloth's Qwen3.5 page: the model loading, LoRA settings and trainer arguments are theirs. The dataset-mapping lines are this guide's. Run it on 10 examples and max_steps = 5 before a real run.
from unsloth import FastLanguageModel
from datasets import load_dataset
from trl import SFTTrainer, SFTConfig
model, tokenizer = FastLanguageModel.from_pretrained(
model_name = "Qwen/Qwen3.5-4B", # 12-16 GB card: 4B
# 8 GB card: "Qwen/Qwen3.5-2B"
# 24 GB card: "Qwen/Qwen3.5-9B" (tight, ~22 GB)
max_seq_length = 2048, # use 1024 for the 9B on a 24 GB card
load_in_4bit = False, # QLoRA is not recommended for Qwen3.5
load_in_16bit = True,
full_finetuning = False,
)
model = FastLanguageModel.get_peft_model(
model, r = 16, lora_alpha = 16, lora_dropout = 0, bias = "none",
target_modules = ["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"],
use_gradient_checkpointing = "unsloth", random_state = 3407,
)
data = load_dataset("json", data_files = {"train": "data/train.jsonl"})["train"]
def to_text(example): # the trainer wants a "text" column
return {"text": tokenizer.apply_chat_template(
example["messages"], tokenize = False, enable_thinking = False)}
data = data.map(to_text)
trainer = SFTTrainer(
model = model, tokenizer = tokenizer, train_dataset = data,
args = SFTConfig(
max_seq_length = 2048, # keep equal to the value above
per_device_train_batch_size = 1, # keep at 1 on 24 GB with the 9B
gradient_accumulation_steps = 4, warmup_steps = 10, max_steps = 100,
logging_steps = 1, output_dir = "outputs_qwen35", optim = "adamw_8bit",
seed = 3407, dataset_num_proc = 1,
),
)
trainer.train()
model.save_pretrained("finetuned_lora")
tokenizer.save_pretrained("finetuned_lora")The first steps can be slow. Qwen3.5 uses custom Triton kernels that compile on first use. If you run out of memory, lower max_seq_length, keep batch size 1, and leave Unsloth's gradient checkpointing on.
4 — Export for Ollama:
model.save_pretrained_gguf("gguf_out", tokenizer, quantization_method = "q4_k_m") # or "q8_0", "f16"Unsloth writes a Modelfile next to the GGUF. Point Ollama at it:
ollama create my-qwen -f <path-to-the-generated-Modelfile> ollama run my-qwen
If the output is gibberish or repeats, the chat template or end-of-sequence token doesn't match what you trained with. Unsloth names this as the usual cause.
Part 3 · The only number that counts
Evaluate honestly
- Same test, same settings. Run the base model and the tuned model on your held-out test set with the same prompt, chat template, thinking mode and sampling settings.
- Score with your pass rule, not by eyeballing a few outputs. Write the numbers down.
- Check what you might have broken. Run 20–30 general prompts outside your task: a little arithmetic, a summary, a refusal case. Narrow training can make the model worse at everything else, and LoRA reduces this risk without removing it.
- Watch for overfitting. If validation loss rises while training loss keeps falling, the model is memorising. Use an earlier checkpoint, fewer steps, or more varied data.
- Read the failures. Most bad results trace back to inconsistent or wrong training examples. Fix the data, then retrain.
- Keep the stop rule. If the adapter doesn't beat the baseline by the margin you set before training, delete it and go back to prompting or retrieval.
Part 3 · Use it
Serve and ship
- Keep the adapter separate while you're still evaluating. Merge or convert only the version you'll actually use.
- Mac:
mlx_lm.serverserves the base plus adapter on127.0.0.1:8080with an OpenAI-compatible API. - PC:
ollama createfrom the GGUF and Modelfile, then use Ollama's API (127.0.0.1:11434, OpenAI-compatible at/v1). - Importing any GGUF into Ollama:
FROM ./model.ggufin a Modelfile, thenollama create. Ollama doesn't quantize GGUF files on import, so quantize first withllama-quantizeor Unsloth's export. - Keep servers on loopback (
127.0.0.1). Neither server has authentication. - Sharing weights: Qwen3.5 small models are Apache-2.0, which allows derivatives. Include the license, name the base model, and describe the training data honestly in the model card.
Part 3 · Next steps
Beyond supervised fine-tuning
- Preference tuning (DPO) and reinforcement learning (GRPO) teach the model to prefer some answers over others, or to optimise a score you compute. Unsloth supports both on NVIDIA GPUs. GRPO with vLLM needs Linux or WSL. Neither is covered by Unsloth's Mac path as of October 2026. Get plain SFT working and evaluated first.
- Vision fine-tuning (image + text) uses Unsloth's
FastVisionModelon a GPU, ormlx-vlmon a Mac.mlx-vlm's Qwen3.5 LoRA bugs were closed in September 2026. It needs much more memory than text-only training. - Abliteration removes a model's refusal behaviour by editing weights directly, without training. It's a different technique with its own costs and responsibilities. Part 4 covers it.
Part 3 · Check yourself
Can you spot the trap?
Part 3 · Fine-tune
Learn more: fine-tuning
For a structured route from here, see Track 4 in the free learning path.
- Unsloth's fine-tuning guide for beginners and its Qwen3.5 page: the practical NVIDIA reference, with free Colab notebooks.
mlx-lmLORA.md: every flag for the Mac path, data formats, memory tips.- The Hugging Face LLM course, especially the fine-tuning chapters, and the TRL and PEFT docs that Unsloth builds on.
- Maxime Labonne's LLM course: a roadmap with notebooks, from fundamentals to fine-tuning and quantization.
- Papers: LoRA (arXiv 2106.09685), QLoRA (arXiv 2305.14314), and LoRA placement in hybrid models like Qwen3.5 (arXiv 2604.22127).
- Qwen's training docs list the frameworks the Qwen team supports: Axolotl, LLaMA-Factory, MS-SWIFT, Unsloth and verl.
Part 4 · Abliteration
What abliteration is
Instruction-tuned models refuse some requests. "Abliteration" is a technique that removes that refusal behaviour by editing the model's weights directly, without any training. This part explains how it works, what the research says it costs, how people do it on local hardware, and what you take on if you do. It's a conceptual and tooling-level map with no prompts and no recipes for harmful output.
The mechanism. Arditi et al. (NeurIPS 2024, arXiv 2406.11717) showed that in 13 open chat models up to 72B, refusal is mediated by one direction in the model's internal activations. Run a set of harmful and a set of harmless instructions through the model, record the activations at a chosen layer, and take the mean difference. That gives a unit vector r. Then orthogonalize every weight matrix that writes into the residual stream: W' = W − r rᵀ W for the embedding, attention-output and MLP-output weights. The model can no longer represent that direction, so it stops refusing. No gradient steps are involved.
Why it's called that. The word is a blend of "ablation" (removing a component to see what it does) and "obliteration." It started as interpretability research, a way to show that refusal is a simple, locatable feature, and became a tool.
What it is not. It isn't fine-tuning: there's no dataset of desired answers and nothing is learned. It isn't a jailbreak prompt: the change is permanent in the weights. And it isn't selective: it removes the model's refusal response as a whole, not only the refusals you consider mistaken.
Part 4 · Abliteration
What later research found
- It isn't always one direction. Wollschläger et al. (ICML 2025, arXiv 2502.17420) found multiple independent refusal directions and multi-dimensional "concept cones," and showed that two directions being orthogonal doesn't mean they're independent when you intervene on them.
- Refusals come in kinds. Joad et al. (arXiv 2602.02132, EMNLP 2026) separate safety refusals, capability-limit refusals ("I can't browse the web"), under-specification refusals and over-refusals, with typical cosine similarities between their directions of only 0.4 to 0.6. Removing one direction doesn't cleanly remove one kind.
- Prompt design decides whether it works. A 2026 study on Qwen3.5-2B (arXiv 2603.22061) found that topic-matched harmful and harmless prompt pairs produced no usable refusal direction at any layer tested, while unmatched pairs worked. The harmful and harmless sets need to differ in refusal and little else, which is harder than it sounds.
- Gentler edits exist. "Norm-preserving biprojected abliteration" (grimjim, Nov 2025) rescales weight rows after projection so their magnitudes don't change. "Gabliteration" (arXiv 2512.18901, a single-author preprint) uses several SVD directions with ridge regularisation.
- It can be defended against. Abu Shairah et al. (arXiv 2505.19056) show that training a model to give a justification before refusing makes the refusal harder to locate as one direction: refusal rates dropped at most 10% under abliteration, versus 70–80% for undefended models. Expect future models to be harder to abliterate.
Part 4 · Abliteration
What it costs the model
It is not free, and the cost varies a lot with the tool and settings.
- In Arditi et al.'s own evaluation, most benchmark scores stayed within confidence intervals, but TruthfulQA dropped 1 to 3.5 points on every model in their main table.
- A 2025 comparison of four abliteration tools on 16 instruction-tuned models of 7–14B (arXiv 2512.13655) measured GSM8K maths changes from +1.5 to −18.8 points, and KL divergence from the original from 0.04 to 1.6. Same idea, very different outcomes.
- Heretic's README shows the spread on one model: three abliterated Gemma-3-12B builds all reach 3 refusals in 100, with KL divergence from the original of 0.16, 0.45 and 1.04. The lower number is the one that kept more of the model.
Two things follow. First, measure capability before and after, not just refusal rate. Second, prefer methods that optimise for low divergence from the original, because "stopped refusing" is easy and "stopped refusing without getting dumber" is the actual goal.
Part 4 · Abliteration
Responsibility and safety
This section is the reason the rest of Part 4 exists. Read it before any of the routes.
What you're removing. A refusal direction doesn't separate "annoying over-refusal" from "genuinely protective refusal." The same edit that stops the model refusing to discuss a chemistry homework question also stops it refusing to help with things that hurt people. Published measurements of abliterated models show large drops in refusal across the board, not a targeted change. Whoever runs the result owns its behaviour.
Who you're responsible to.
- Yourself. Local, private use for research, writing or red-teaming your own systems is the common legitimate case. Even then, keep the model where only you can reach it.
- Anyone who uses your deployment. If you put an abliterated model behind an app, a bot or an API, you have removed a safety layer that users may assume is there. Add your own: input filtering, output review, a clear notice, and no access for minors.
- Anyone you share weights with. Publishing an abliterated model is legal under Apache-2.0, and people do. Name the base model, state what was changed and how, report the capability cost you measured, and don't describe it as "uncensored" as if that were a feature with no downside.
- Third parties. Laws on harmful content, defamation, harassment, fraud and export controls apply to what a model produces on your behalf. "The model said it" is not a defence anywhere.
Rules this guide holds to:
- Never wire an abliterated model to tools with side effects. No shell access, no file writes, no email or messaging, no browsing, no autonomous agent loops. A model with no refusal behaviour plus tools that act in the world is the single worst combination in this whole guide.
- Keep it on loopback and out of shared memory. Don't let an abliterated model write into a memory store or retrieval index that other models or people trust.
- Prefer the smallest change that solves your real problem. If the actual issue is a model refusing a benign task, try a system prompt, a different model, or a few examples first. Most "the model won't help me" problems are solved that way.
- Measure capability loss and write it down. Section "Evaluate" below.
- Don't do it to models you don't have the right to modify, and don't redistribute weights whose license forbids it.
A short decision check. Before you abliterate, answer honestly:
- What specific benign task is the base model refusing, and have I tried a prompt or a different model?
- Who else will ever be able to reach this model, and what protects them?
- Will it ever touch tools, memory or the network?
- Will I measure and record what it lost?
- If the output were shown next to my name, would I defend it?
If any answer makes you uncomfortable, stop there.
Why this section isn't a refusal to explain. The research is public and peer-reviewed, the tools are open source, and the ready-made models have hundreds of thousands of downloads. Someone who wants to do this will. The useful thing a guide can do is make the costs, the responsibilities and the safer choices clear, and point at the real literature rather than at folklore.
Part 4 · Abliteration
How people do it locally
Route A: use a published model. The lowest-effort path, and the one most people take. A widely pulled Qwen3.5 build is huihui_ai/qwen3.5-abliterated on Ollama (about 678K pulls):
ollama run huihui_ai/qwen3.5-abliterated:4b # 3.3–3.4 GB ollama run huihui_ai/qwen3.5-abliterated:9b # 6.6 GB
Always write a tag. Untagged latest is the 27B (17 GB) and won't load on a 16 GB machine. The repo also carries -Claude, -Qwopus and -OmniCoder tags, which are different fine-tunes, not plain abliterations. The publisher's model card says the 4B was also fine-tuned with TRL on non-thinking data, text only, with the multi-token-prediction weights removed, so expect it to differ from the base in more than refusals. These are third-party weights; everything in Part 1's "Keep it on your machine" applies, and so does the whole responsibility section above.
Route B: run Heretic yourself. p-e-w/heretic (AGPL-3.0, pip install -U heretic-llm, version 1.4.0 in October 2026) automates the direction search with Optuna. It optimises refusal count and KL divergence from the original together, which is why its builds tend to keep more capability. Two facts matter for a local machine: Qwen3.5's hybrid layers have been supported since v1.3.0, and basic Apple Silicon (MPS) support has existed since v1.1.0, tested on a Qwen2.5-0.5B on an M4 Max with 128 GB.
- On an NVIDIA GPU, the README's reference point is 20–30 minutes for a Qwen3-4B on an RTX 3090 (24 GB).
- On a 16 GB Mac, the 0.8B and 2B are the realistic experiment. The 4B in bf16 is 9.32 GB of weights, under the ~11.8 GiB GPU budget on paper but untested and likely tight once activations and trials are added. The 9B (19.3 GB bf16) does not fit.
- Version 1.4.0 pins
bitsandbytes~=0.49, whose 4-bit path on Apple Silicon is the slow one. Don't count on quantized loading to rescue a bigger model on a Mac.
Heretic writes Hugging Face safetensors. To run the result in Ollama, convert with llama.cpp's convert_hf_to_gguf.py (it registers Qwen3_5ForCausalLM), quantize with llama-quantize, then ollama create from the GGUF.
Route C: do it by hand. The 2024-era reference scripts (FailSpy's abliterator, Sumandora's remove-refusals-with-transformers, mlabonne's notebook) assume CUDA and a Llama-style layout with self_attn.o_proj in every block. Qwen3.5-4B and 9B have 24 Gated DeltaNet layers and 8 full-attention layers; the attention output projection is linear_attn.out_proj on the former and self_attn.o_proj on the latter, alongside mlp.down_proj. Old code breaks on this. Read Arditi's paper and Heretic's Qwen3.5 pull request (#187) before adapting anything, and treat it as a research project, not a weekend task.
OBLITERATUS. elder-plinius/OBLITERATUS (AGPL-3.0) is a larger toolkit with several extraction methods (mean difference, PCA, sparse autoencoders, whitened SVD) and an MLX backend. Its README lists no validated models, and the Qwen work documented in its research notes is a Qwen3.8-27B run on a CUDA device, so it isn't a tested Mac path for the small models. Note that telemetry is on by default on its hosted Space and opt-in locally.
Part 4 · Abliteration
Evaluate any abliterated model
Measure three things, before and after, with the same prompts and settings:
- Refusal rate on a held-out set of prompts you did not use to find the direction. Include benign prompts that the base model wrongly refused, since those are the ones you presumably cared about.
- Divergence from the original: KL divergence or perplexity on ordinary text. Heretic reports KL; for a published model, compare answers on 30 neutral prompts side by side with the base.
- A capability set you care about: a little maths, a summary, a structured-output task, your own real prompts.
Run everything with thinking on and off: the 4B and 9B think by default, and different tools read different token positions. Write the three numbers in the model card or your notes. A model that stops refusing but loses fifteen points of maths has cost you more than it gained.
Part 4 · Abliteration
Learn more: abliteration and interpretability
To understand the transformer internals this research works on, see Track 2 in the free learning path.
Start with the research, then the tools.
- The paper: Arditi et al., "Refusal in Language Models Is Mediated by a Single Direction" (arXiv 2406.11717, NeurIPS 2024). The authors' LessWrong write-up is the readable version.
- The walkthrough: Maxime Labonne, "Uncensor any LLM with abliteration" (2024). Clear code and figures, Llama-era layer names.
- The follow-ups: Wollschläger et al. 2025 on concept cones; Joad et al. 2026 on kinds of refusal; Petrov 2026 on Qwen3.5 and prompt matching; Young 2025 comparing four tools; grimjim's norm-preserving method; Abu Shairah et al. 2025 on defending against it.
- The tools: Heretic and OBLITERATUS, both with READMEs that document their methods and measured results.
- The field behind it: abliteration is applied mechanistic interpretability. Neel Nanda's getting-started guide and Anthropic's Transformer Circuits publications are where the ideas about directions, features and circuits come from.
- Published models: the huihui_ai Qwen3.5 builds on Ollama, with the caveats in Route A.
Reference
A free learning path: from zero to understanding LLMs
This guide shows you how to run and change a model. This section shows you where to learn how one works, using only free courses, videos and code.
Every link below returned a page on that date, and each free status is what the resource itself says. Courses change their terms, so check before you plan around one. Lengths marked secondary come from titles or third-party listings, not the resource itself.
Pick a starting track
| I want to… | Start with |
|---|---|
| Understand what an LLM is, without maths | Track 0 |
| Learn how neural networks learn, with code | Track 1 |
| Build a small GPT myself and see every part | Track 2 |
| Run models faster on my own hardware | Track 3 |
| Fine-tune a model on my own data | Track 4 |
| Build agents that use tools | Track 5 |
| Study it at university depth | Track 6 |
Tracks 0, 1 and 2 build on each other. Tracks 3, 4 and 5 stand alone and pair with Parts 1, 3 and 2 of this guide.
Track 0 · Orientation (an afternoon)
| Resource | By | Format and length | Teaches | Free? |
|---|---|---|---|---|
| Intro to Large Language Models | Andrej Karpathy | Video, ~1h (secondary); beginner | What an LLM is and how it is made | Free |
| How I use LLMs | Andrej Karpathy | Video, length UNCERTAIN | Practical everyday use of chat models | Free |
| Neural networks, incl. "LLMs explained briefly" | 3Blue1Brown | Animated videos, lengths UNCERTAIN; beginner | The intuition, drawn out visually | Free |
| Transformer Explainer | Polo Club (poloclub) | Interactive page, minutes; beginner | A live GPT-2 you can click through, step by step | Free to use |
| Deep Dive into LLMs like ChatGPT | Andrej Karpathy | Video, 3h31m (secondary); general | The full pipeline behind a chat model | Free |
Watch the deep dive last. It is long, and it makes more sense once the short ones have given you the words.
Track 1 · Foundations (2–4 weeks)
| Resource | By | Format and length | Teaches | Free? |
|---|---|---|---|---|
| micrograd and the first lectures of Zero to Hero | Andrej Karpathy | Repo plus video lectures; beginner | Backpropagation, built from scratch | MIT code; public YouTube |
| Practical Deep Learning | fast.ai | 9 lessons of ~90 min; Part 2 is 30h+ | Training real models first, theory after | Free |
| MIT 6.S191 Introduction to Deep Learning | MIT | ~9 weeks of lectures and labs | The core of deep learning; Lab 3 fine-tunes an LLM | Free |
| Machine Learning Crash Course | Self-paced; the LLM module is ~45 min | ML basics, with a short LLM module | Price not stated (UNCERTAIN) |
Pick one of fast.ai or MIT 6.S191, not both. fast.ai suits you if you learn by doing. MIT suits you if you want lectures first.
Track 2 · Build an LLM from scratch (6–10 weeks)
Karpathy's Zero to Hero is the centre of this track. You write every piece of a GPT yourself, in plain Python and PyTorch, and nothing is hidden behind a library.
| Resource | By | Format and length | Teaches | Free? |
|---|---|---|---|---|
| Neural Networks: Zero to Hero | Andrej Karpathy | 8 lectures, 14h32m total; beginner to intermediate | From backprop to a working GPT, including the tokenizer lecture | Public YouTube |
| Let's build GPT | Andrej Karpathy | Video, 1h56m; intermediate | A transformer written line by line | Free |
| minbpe | Andrej Karpathy | Repo; intermediate | Byte-pair encoding; matches GPT-4 tokenization only through its wrapper | MIT |
| Let's reproduce GPT-2 (124M) with build-nanogpt | Andrej Karpathy | Video, ~4h (secondary), plus repo | Training a real GPT-2 the way the labs do | Free video; the README puts the run at ~1h and ~$10 of rented compute |
| nanochat | Andrej Karpathy | Repo; advanced | A full chat model, pretraining to chat | MIT code; compute costs money: ~2h on 8×H100 (~$48), a single GPU is ~8× slower |
| LLMs-from-scratch | Sebastian Raschka | Repo, chapters 1–7 plus appendices A–E; runs on a laptop | Building, pretraining and fine-tuning a GPT-style model | Code free (Apache-2.0); the book is paid |
| The Illustrated Transformer | Jay Alammar | Illustrated article; beginner to intermediate | Attention and the transformer block, in pictures | Free (CC BY-NC-SA) |
| LLM Visualization | Brendan Bycroft | Interactive page, minutes to 1h | A small GPT, drawn as it runs | Free (MIT) |
| Attention Is All You Need | Vaswani et al. | Paper, 15 pages | The original transformer | Free |
| Deep Learning chapters 5–7 | 3Blue1Brown | Animated videos, lengths UNCERTAIN | Transformers and attention, visually | Free |
Karpathy marked nanoGPT deprecated in November 2025, and its README points to nanochat. Older tutorials still use it; the code runs, but new work happens in nanochat.
Karpathy's LLM101n has not been released. You may see it linked as a course; don't plan around it.
Track 3 · Run and serve (1–2 weeks, alongside Part 1)
| Resource | By | Format and length | Teaches | Free? |
|---|---|---|---|---|
| Ollama docs | Ollama | Reference docs | Install, GPU support, import, the API | Free (MIT) |
| llama.cpp | ggml-org | Repo and docs | The engine under most local runtimes, its quants and server | Free (MIT) |
| vLLM docs | vLLM project | Reference docs | High-throughput serving on NVIDIA GPUs | Free (Apache-2.0) |
| Unsloth docs | Unsloth | Reference docs and notebooks | Quantized models and fast fine-tuning | Free (Apache-2.0 core; Studio is AGPL) |
| llm-course, quantization notebooks | Maxime Labonne | Notebooks; self-paced | How GGUF, GPTQ and other quants are made | Free; the Handbook is paid |
Track 4 · Fine-tune and post-train (3–6 weeks, alongside Part 3)
| Resource | By | Format and length | Teaches | Free? |
|---|---|---|---|---|
| LLM Course | Hugging Face | 12 chapters, ~6–8h each | Transformers, tokenizers, datasets and fine-tuning with the Hugging Face stack | Free, no ads |
| smol course | Hugging Face | Units 1–4 released (5–6 unconfirmed) | Instruction tuning and preference alignment (DPO) on small models | Free; it says "no paid services" |
| llm-course | Maxime Labonne | Self-paced roadmap, 3 tracks, with notebooks | Fundamentals, fine-tuning, merging, quantization | Free; the Handbook is paid |
| Unsloth docs | Unsloth | Guides and Colab notebooks | LoRA and QLoRA on one GPU | Free (Apache-2.0 core; Studio is AGPL) |
Track 5 · Agents (2–4 weeks, alongside Part 2)
| Resource | By | Format and length | Teaches | Free? |
|---|---|---|---|---|
| Agents Course | Hugging Face | Units 0–4 plus 3 bonus units | Tool use, agent loops, frameworks | Free, including the certificate |
| Open-Source AI Cookbook | Hugging Face | Notebooks; length varies | Worked recipes for RAG, agents and evaluation | Free |
| Short courses | DeepLearning.AI | 49 min to 2h each | Single topics such as agents and RAG | Free only "for a limited time during beta" |
Track 6 · University courses (a full term each)
| Resource | By | Format and length | Teaches | Free? |
|---|---|---|---|---|
| CS336 Language Modeling from Scratch | Stanford | Full quarter (Spring 2026); advanced | Tokenizers, architecture, training, scaling, alignment, all built yourself | Free lectures; the full assignments need big GPUs |
| CS224N NLP with Deep Learning | Stanford | Full quarter | NLP from word vectors to transformers and LLMs | The 2024 videos are free; 2026 videos are enrolled-only |
| MIT 6.S191 | MIT | ~9 weeks | See Track 1; take it here if you skipped it | Free |
Do CS224N before CS336 if you are new to the field. CS336 assumes you can already train a model.
Stay current
- Lil'Log, Lilian Weng's blog: long, careful survey posts on agents, alignment and training. Free.
- Simon Willison's local-llms tag: short, hands-on notes on running models locally; 165 posts when checked. Free.
- r/LocalLLaMA on Reddit: a large forum for local models and often the first place a new release is discussed. Treat what you read there as anecdotal until you can reproduce it. (Reddit blocked our link check, so no link here.)
How to use this path
- Plan on 3–5 hours a week. At that pace Track 2 takes about two months (an estimate).
- Code along. Expect two to three times the video length once you type the code yourself (our estimate, not measured).
- Finish one track before you start the next. Jumping between courses feels like progress and usually isn't.
- Build something after each track. After Track 0, write down how a prompt becomes an answer in your own words. After Track 2, train a tiny model on a text you like. After Tracks 3 to 5, run, fine-tune or wire up a model from this guide on your own task.
- When something doesn't click, go back one track rather than forward one course.
Reference
Troubleshooting
| Area | Symptom | Likely cause | Fix |
|---|---|---|---|
| Run | ollama run qwen3.5 downloads 7 GB | latest is the 9B | Name the tag: qwen3.5:4b |
| Run | Model runs on CPU, very slow | GPU not detected: old driver, unsupported card, or no ROCm/CUDA | Check ollama ps and the GPU requirements; update the driver |
| Run | ollama ps shows a split such as 40%/60% CPU/GPU | Weights plus KV cache don't fit in VRAM, so some layers run on the CPU | Lower num_ctx, pick a smaller quant, or close other GPU apps until the PROCESSOR column reads 100% GPU |
| Run | Machine swaps or freezes while a model loads | Model plus KV cache exceeds free memory | Smaller model or quant, shorter num_ctx, one model at a time |
| Run | The model ignores the start of a long prompt or document, with no error | The prompt is longer than the context window and Ollama truncates it silently (issue #14259). The default context depends on VRAM: 4K under 24 GiB, 32K at 24-48 GiB | Check the CONTEXT column in ollama ps, then set num_ctx per request or OLLAMA_CONTEXT_LENGTH for the server |
| Run | Out of memory only at long context | The KV cache grows with every token. Qwen3.6-27B needs 64 KiB per token at f16, about 2 GiB per 32K (derived) | Shorten the context, or quantize the cache: OLLAMA_FLASH_ATTENTION=1 with OLLAMA_KV_CACHE_TYPE=q8_0 in Ollama, -ctk q8_0 -ctv q8_0 with flash attention on in llama.cpp |
| Run | Answers start with a long <think> block | Thinking mode is on by default for 4B/9B | Pass enable_thinking=False, or train and serve consistently with thinking on |
| Run | Thinking never ends, or loops on the same sentences | Long reasoning drifts into repetition; quantized models think longer | Set presence_penalty between 0 and 2 (Qwen suggests 1.5), raise the output-token limit, or turn thinking off for that task |
| Run | A server on the network can reach my Ollama | Bound to 0.0.0.0 | Set OLLAMA_HOST=127.0.0.1:11434 and restart |
| Build | llama-server isn't on port 8080 | It now defaults to 127.0.0.1:9931 | Pass --host 127.0.0.1 --port 8080 explicitly, and point your client at that port |
| Build | The vector store rejects new embeddings | The collection was made for 768-dim nomic vectors, but Qwen3-Embedding-0.6B returns 1024 | Recreate the collection at size 1024 with cosine distance, then re-embed every document with the same model |
| Fine-tune | Out of memory | Sequences too long, too many layers, or the model is too big | Shorten max sequence length, batch size 1, gradient checkpointing on, then a smaller model |
| Fine-tune | Loss doesn't move | Learning rate too low, or the prompt is masked wrongly | Check that the assistant turns are present; try a slightly higher LR on a 20-step run |
| Fine-tune | Loss falls, test score doesn't | Overfitting, or the test differs from the training data | More varied data, fewer steps, use an earlier checkpoint |
| Fine-tune | Gibberish or repetition after export | Chat template or end-of-sequence token mismatch | Serve with the same template you trained with |
| Fine-tune | mlx_lm.server ignores the adapter | mlx-lm 0.31.3 bug | Upgrade to 0.32.0+ |
| Fine-tune | --export-gguf error on Qwen3.5 | mlx_lm.fuse only exports llama/mistral/mixtral | Serve with mlx_lm.server |
| Fine-tune | Qwen3.5 won't load in Python | transformers older than v5 | Upgrade transformers |
| Fine-tune | First training steps very slow | Triton kernels compiling | Wait; later steps are faster |
| Abliteration | Abliterated model is coherent but worse at maths | Capability loss from the edit | Try a lower-divergence build or method; measure before trusting it |
| Platform | WSL can't see the GPU | A Linux NVIDIA driver was installed inside WSL, or the Windows driver is missing | Install only the Windows NVIDIA driver. Inside WSL install only cuda-toolkit-12-x, never cuda or cuda-drivers. Check with nvidia-smi inside WSL |
| Platform | A Windows environment variable has no effect | The Ollama tray app is still running with the old environment | Quit the tray app, set the variable for your user account, then relaunch Ollama from the Start menu or a new terminal |
| Platform | OLLAMA_HOST set in your Linux shell does nothing | The systemd service doesn't read your shell's environment | Run sudo systemctl edit ollama.service, add Environment="OLLAMA_HOST=127.0.0.1:11434" under [Service], then sudo systemctl daemon-reload and sudo systemctl restart ollama |
| Platform | The second GPU sits idle | CUDA_VISIBLE_DEVICES hides it, or the runtime put the whole model on one card | Run nvidia-smi -L, list both UUIDs in CUDA_VISIBLE_DEVICES for Ollama, and pass -ts 1,1 to llama.cpp to split layers across both |
| Platform | Ollama in Docker runs on the CPU | The container was started without GPU access | Install the NVIDIA Container Toolkit, then docker run -d --gpus=all -v ollama:/root/.ollama -p 127.0.0.1:11434:11434 --name ollama ollama/ollama |
| Platform | GPU lost after a Linux suspend and resume; Ollama falls back to CPU | A known NVIDIA driver bug | Reload the UVM driver: sudo rmmod nvidia_uvm && sudo modprobe nvidia_uvm, then restart Ollama |
Reference
Glossary
Plain-English meanings for the terms this guide uses. Linked terms jump to the section that explains them.
| Term | Meaning |
|---|---|
| A3B (active parameters) | In a name like 30B-A3B, the model holds 30B parameters but uses only about 3B for each token. Memory follows the total; speed follows the active count. |
| Abliteration | Editing a model's weights so it can no longer express the internal "refusal direction" that makes it decline requests. |
| BF16 | A 16-bit number format (bfloat16) used for full-precision weights. Two bytes per parameter. |
| Chat template | The exact wrapper of role markers and special tokens a model expects around each message. A mismatch causes gibberish or endless output. |
| Context window | The most tokens a model can see at once, prompt plus answer. Ollama calls it num_ctx and accepts it per request. |
| CUDA | NVIDIA's GPU computing platform. A runtime needs a CUDA build and a recent enough driver to use an NVIDIA card. |
| Decode | The phase that writes the answer one token at a time. Its speed is limited mostly by memory bandwidth. |
| DPO | Direct Preference Optimization. Trains on pairs of better and worse answers, with no separate reward model. |
| DWQ | MLX learned quantization. It tunes the quantized scales against the full-precision model to recover quality. |
| Embedding | A list of numbers that stands for the meaning of a text. Similar texts get nearby vectors. This guide uses Qwen3-Embedding-0.6B at 1024 dimensions. |
| EXL3 | The GPU quant format of ExLlamaV3, served by TabbyAPI. Aimed at quality per GB on 24 GB cards; 3090 support rests on one user report. |
| Fill-in-the-middle | A code-completion mode: the model gets the text before and after the cursor and writes the gap. |
| Fine-tuning | Training an existing model further on your own examples. |
| Flash attention | A faster, lower-memory way to compute attention. Ollama needs it on before it will quantize the KV cache. |
| FP8 | An 8-bit floating-point format. Ada (4090) and Blackwell (5090) run it in hardware; a 3090 only gets weight-only W8A16. |
| GGUF | The single-file model format used by llama.cpp, Ollama and LM Studio. It holds quantized weights plus metadata. |
| GQA | Grouped-query attention. Several query heads share one key/value head, which shrinks the KV cache. |
| GRPO | Group Relative Policy Optimization. A reinforcement-learning method that scores a group of sampled answers against each other. |
| Hybrid attention | Mixing layer types. In Qwen3.5, three of every four layers are Gated DeltaNet (linear attention) and one is normal attention; only the normal layers keep a growing KV cache. |
| KV cache | Memory that stores the keys and values of every token so far, so the model doesn't recompute them. It grows with context length. |
| LoRA | Training small low-rank adapter matrices beside the frozen weights instead of changing every weight. |
| MCP | Model Context Protocol. A standard way for an agent to call tools and servers, such as a shared memory store. |
| Mixture-of-experts (MoE) | A model built from many expert sub-networks, with a router that picks a few of them for each token. See A3B. |
| MLX | Apple's machine-learning framework for Apple Silicon. mlx-lm runs and fine-tunes language models on it. |
| mmproj | A separate GGUF file that holds a vision model's image projector. llama.cpp loads it beside the main GGUF to read images. |
| NVFP4 | NVIDIA's 4-bit floating-point format. Only Blackwell (5090) has native FP4 tensor cores; older cards use slower fallback kernels. |
| NVLink | A direct bridge between two GPUs. The 3090 and 3090 Ti have it; the 4090 and 5090 don't. Its benefit for llama.cpp's layer split is UNCERTAIN. |
| Offload | Keeping some layers in system RAM when the model doesn't fit in VRAM. ollama ps shows the split; it is much slower. |
| Prefill | The phase that reads the whole prompt before the first output token. Long prompts make it the slow part. |
| QLoRA | LoRA on top of a 4-bit quantized base model, to cut training memory. Unsloth doesn't recommend it for Qwen3.5. |
| Quantization | Storing weights in fewer bits, such as 4-bit, to cut memory and speed up decode at some cost in quality. Q4_K_M is a common GGUF 4-bit type. |
| RAG | Retrieval-augmented generation. Search your own documents, then hand the best passages to the model with the question. |
| Reranker | A model that scores each query and document pair to reorder search results more precisely than vector search. |
| ROCm | AMD's GPU computing platform. Ollama needs the ROCm v7 driver on Linux. |
| safetensors | The standard file format for full-precision weights. Unlike .bin or .pt pickle files, it can't run code when opened. |
| Sampling | How the next token is picked from the model's probabilities: temperature, top_p, top_k, min_p and presence_penalty. |
| SFT | Supervised fine-tuning. Training on example inputs paired with the answers you want. |
| Speculative decoding | A small draft model proposes several tokens and the big model checks them in one pass, which raises throughput. |
| Tensor parallel | Splitting each layer's maths across GPUs so they work at the same time, as in vLLM's --tensor-parallel-size 2. llama.cpp's default layer split instead gives each GPU whole layers. |
| Thinking mode | Qwen3.5 reasoning in a <think> block before it answers. Better on hard problems, slower everywhere. |
| tok/s | Tokens per second. Benchmarks report prefill (pp) and decode (tg) speeds separately. |
| Token | The unit a model reads and writes, often a piece of a word. Context, speed and price are all counted in tokens. |
| Unified memory | Apple Silicon's single memory pool shared by CPU and GPU, so a Mac has no separate VRAM. |
| VRAM | A graphics card's own memory. On a PC, the weights plus the KV cache must fit here for full speed. |
| WSL2 | Windows Subsystem for Linux, version 2. Runs Linux on Windows; vLLM needs it there. |
Reference
Sources
Every version, number and command in this guide traces to one of these, checked in October 2026.
- Running: Ollama docs (quickstart, GPU, import), Ollama Qwen3.5 tags, Hugging Face
hfCLI, Hub + Ollama, Qwen docs, QwenLM release notes - Hardware and PC runtimes: NVIDIA RTX 3090 / 3090 Ti, RTX 4060 Ti, RTX 5090, CUDA GPUs, CUDA on WSL; llama.cpp CUDA scoreboard, releases, build, server, llama-bench; Ollama GPU, FAQ, context length, Linux, Windows, Docker, issue #14259; vLLM GPU install, quantization, parallelism; ExLlamaV3, TabbyAPI, SGLang, KoboldCpp, text-generation-webui, Open WebUI, LM Studio requirements; secondary benchmarks: XiongjieDai, club-3090, jeanfbrito power sweep
mlx-lm: PyPI, LORA.md, qwen3_5 model code, fuse.py- Unsloth: Qwen3.5 fine-tuning, install, Windows install, saving to Ollama
- Qwen3.5 model cards: 4B, 9B, 2B
- Papers: LoRA arXiv 2106.09685, QLoRA arXiv 2305.14314, LoRA placement in hybrid models arXiv 2604.22127
- Ollama: importing models, thinking
- Axolotl on Mac: docs
- Abliteration: Arditi et al., Wollschläger et al., Joad et al., Petrov, Gülmez, Young, Abu Shairah et al., grimjim, Heretic, OBLITERATUS, huihui_ai on Ollama
Reference
Caveats
- Nothing here was benchmarked on one fixed machine. The memory figures come from vendor tables, one measured MLX budget on a 16 GB Mac, and labelled secondary reports. Measure your own peak.
- This field moves monthly.
mlx-lm, Unsloth,transformersand Ollama all changed Qwen3.5 support during 2026. Re-check versions before following a command. - Untested paths are labelled. Attention-only LoRA keys in
mlx-lm, the fused-safetensors-to-Ollama import, and Unsloth's Mac path for Qwen3.5 are not proven here. - The landscape tables are a snapshot, dated 2026-10-15. Model families, sizes and licenses change weekly. Every row was checked against a model card or Ollama's library page that day; the watch list under the tables names what wasn't. Re-check the model card before you commit to a family.
- Part 4 is deliberately conceptual. It explains the research and names the tools. It does not provide prompt sets, harmful examples or step-by-step editing code.
- A tuned model is still a language model. It can be confidently wrong. Keep validators and human review wherever mistakes cost something.