From LoRA Adapter to One File: Packaging a Local Model for Ollama and LM Studio

A LoRA fine-tune leaves you with two artifacts: the original base model (a few gigabytes) and a small adapter (tens of megabytes) that patches it. That's great for training and useless for handing to someone. Ollama and LM Studio don't take "a base plus an adapter." They take one model.
So this is the packaging path, start to finish, run on an M1 Max. Nothing here is specific to what the model does — it's the same three moves whether you fine-tuned a coding assistant, a support bot, or something you shouldn't ship. Fuse, convert, quantize.
What you start with
base model/ # e.g. Qwen/Qwen2.5-1.5B-Instruct — the unchanged base
adapter/ # your LoRA: adapter_config.json + adapters.safetensors
The adapter is meaningless without the exact base it was trained on. Step one is to stop treating them as two things.
Step 1 — Fuse the adapter into the base
Fusing bakes the LoRA weights into the base model and writes a normal, standalone model in Hugging Face safetensors format. On Apple Silicon with mlx_lm:
python -m mlx_lm fuse \
--model Qwen/Qwen2.5-1.5B-Instruct \
--adapter-path adapter \
--save-path fused
You now have fused/ — model.safetensors, config.json, tokenizer.json, the works. One self-contained model, no adapter required. (Off the Mac, the equivalent is peft's merge_and_unload() then save_pretrained() — same idea, one call.)
fused/ is HF format, not GGUF yet. Ollama can take it as-is; LM Studio wants GGUF. Both are one step away.
Step 2 — Ollama, the easy path (it converts for you)
Here's the part people miss: Ollama converts safetensors to GGUF for you during import. You don't need llama.cpp for this at all.
cat > Modelfile <<'EOF'
FROM ./fused
PARAMETER temperature 0
EOF
ollama create my-model -f Modelfile
Ollama reads the safetensors, converts them to a GGUF internally, and registers the model. ollama run my-model "..." now works.
Gotcha #1: the chat template
The raw import doesn't always carry the model's chat template, and without it the model free-associates on your prompt instead of answering it. If ollama run gives you garbled completions, spell the template out in the Modelfile. For a Qwen2.5 model:
FROM ./fused
PARAMETER temperature 0
PARAMETER stop "<|im_start|>"
PARAMETER stop "<|im_end|>"
TEMPLATE """{{ if .System }}<|im_start|>system
{{ .System }}<|im_end|>
{{ end }}{{ if .Prompt }}<|im_start|>user
{{ .Prompt }}<|im_end|>
{{ end }}<|im_start|>assistant
{{ .Response }}<|im_end|>
"""
Recreate, and it answers properly. This one bites everyone once.
Step 3 — Get a GGUF for LM Studio
LM Studio loads GGUF files. You have two ways to get one.
The lazy way: Ollama already made one. During the import above, Ollama built a GGUF and stored it in its blob cache. Pull it straight out:
# find the model-layer blob from the manifest
MAN=~/.ollama/models/manifests/registry.ollama.ai/library/my-model/latest
DIGEST=$(python -c "import json,sys;print([l['digest'] for l in json.load(open('$MAN'))['layers'] if l['mediaType'].endswith('.model')][0])")
cp ~/.ollama/models/blobs/${DIGEST/:/-} model-f16.gguf
head -c 4 model-f16.gguf prints GGUF — it's a real, valid GGUF. On my run this was a 2.9 GB f16 file.
The portable way: llama.cpp. If you didn't go through Ollama, convert fused/ with llama.cpp's convert_hf_to_gguf.py. Either way you land on the same .gguf.
Import it into LM Studio
~/.lmstudio/bin/lms import model-f16.gguf --user-repo local/my-model
or just drop the file into ~/.lmstudio/models/<author>/<model>/. lms ls then lists it as a local model, and it loads in the GUI like anything off the hub.
Step 4 — Quantize it so it's actually shippable
A 2.9 GB f16 file is fine to run and painful to distribute. Quantization shrinks it hard for a small quality cost. With llama.cpp's llama-quantize:
llama-quantize model-f16.gguf model-Q4_K_M.gguf Q4_K_M
Real numbers from this run:
| file | precision | size |
|---|---|---|
model-f16.gguf |
16-bit | 2.9 GB |
model-Q4_K_M.gguf |
4-bit (K_M) | 941 MB |
A 3× cut. Q4_K_M is the sensible default — the best size/quality trade for most uses. Go Q5_K_M if you want a little more fidelity, Q8_0 if you barely want to lose anything (and don't mind ~half the f16 size). That single .gguf is now the whole model — hand it to Ollama (FROM ./model-Q4_K_M.gguf) or LM Studio and you're done.
Gotcha #2: the metadata labels lie a little
Don't be alarmed if LM Studio shows your 1.5 B model as "7B" or mislabels the arch — those fields come from GGUF metadata that conversion sometimes rounds or guesses. The model runs fine; the label is cosmetic.
The whole path, on one screen
# 1. fuse adapter -> standalone HF model
python -m mlx_lm fuse --model <base> --adapter-path adapter --save-path fused
# 2. Ollama (does the GGUF conversion for you)
printf 'FROM ./fused\nPARAMETER temperature 0\n' > Modelfile
ollama create my-model -f Modelfile
# 3. pull the GGUF out for LM Studio
cp ~/.ollama/models/blobs/<model-blob> model-f16.gguf
lms import model-f16.gguf --user-repo local/my-model
# 4. quantize to one small shippable file
llama-quantize model-f16.gguf model-Q4_K_M.gguf Q4_K_M
Two artifacts in, one file out, running in both tools. That "one file" is exactly why a downloaded model is a dependency you can't read — there's no adapter to notice, no source to review, just weights that do whatever they were trained to do. If that part interests you, it's the whole point of the model-poisoning-lab. But as a packaging recipe, this is all it takes.