Fine-tuning

Fine-tune a model on your company's data, without your data leaving the building

By the GPUwerk fleet team · Updated August 26, 2026 · 11 min read

Inference sends one prompt at a time. Fine-tuning sends everything, every support ticket, every contract, your whole codebase, through someone else's machine, and returns weights that have memorised it. It is the workload where the privacy stakes are highest, and the one people are most casual about. This guide covers what actually fits in 128 GB of unified memory, what it costs you in wall-clock time, and when you shouldn't be fine-tuning at all.

First, the uncomfortable question: should you fine-tune?

Most teams who ask us about fine-tuning should be doing retrieval instead. The distinction that holds up in practice: RAG changes what the model knows; fine-tuning changes how the model behaves. Winder.ai's 2026 decision framework puts it as retrieval handling knowledge that changes over time, and fine-tuning handling behaviour that shouldn't. BigDataBoutique is blunter, most teams should fix their prompts, build a real retrieval pipeline, and write evals, in that order, before touching a training run.

SignalReach for RAGReach for fine-tuning
Knowledge freshnessFacts change weekly, pricing, policies, tickets, inventoryThe behaviour is stable, house style, a schema, a domain's conventions
ProvenanceYou need citations, an audit trail, per-document access controlOutput correctness is judged by shape, not by source
Data you haveDocuments, no labelsThousands of real input→output pairs (resolved tickets, merged PRs, past filings)
Failure you're fixing"It doesn't know about our product""It knows, but it will not stop writing like a chatbot"
Cost pressureFrontier API calls are affordable at your volumeA tuned 14B would replace a frontier model on a high-volume task
LatencyA retrieval hop is acceptableThe retrieval hop is your latency budget

In production these are not rivals. The common shape, and the one Atlan, Winder and the rest of the 2026 guidance converge on, is fine-tune for form, retrieve for fact: a LoRA adapter that has internalised your output structure and vocabulary, fed current documents at inference time. That combination is also the one that keeps your most sensitive corpus on hardware you control, because both halves run locally.

What actually fits: the memory table

Three methods, in ascending order of memory appetite. QLoRA quantises the frozen base model to 4-bit and trains small adapters on top. LoRA keeps the base in 16-bit and trains the same adapters, better fidelity, roughly 4× the base-weight footprint. Full fine-tuning updates every parameter and needs room for optimizer states too, which is why it stays small.

The numbers below are Unsloth's published minimum VRAM requirements per model size, their documentation flags them as absolute minimums at modest sequence lengths, so treat them as a floor, not a budget. The verdict column is against the DGX Spark's 128 GB of coherent unified memory.

ModelQLoRA (4-bit)LoRA (16-bit)Fits in 128 GB?
7B5 GB19 GBBoth, with enormous headroom for long sequences and real batch sizes
8B6 GB22 GBBoth, comfortably
14B8.5 GB33 GBBoth, comfortably
32B26 GB76 GBBoth, 16-bit LoRA at 32B is the sweet spot of this machine
40B30 GB96 GBBoth; 16-bit gets tight, keep sequences moderate
70B41 GB164 GBQLoRA yes, with ~85 GB left over. 16-bit LoRA needs two linked Sparks (256 GB pooled)
90B53 GB212 GBQLoRA yes. 16-bit LoRA: no, even at 256 GB
405B237 GB950 GBNeither on one node; QLoRA is within reach of a two-Spark 256 GB pool

The headline for anyone shopping: a 70B-class model is QLoRA-tunable on one machine that plugs into a wall socket. Unsloth's floor is 41 GB, and NVIDIA's own developer blog notes plainly that these fine-tuning workloads cannot run on a 32 GB consumer GPU. The 128 GB is not there to be filled by the base weights, it's there so that after the weights you still have room for gradient checkpointing off, longer sequences, and a batch size above one, which is where training runs actually get fast.

One caveat worth internalising: these floors assume short sequences. Multiple 2026 sizing guides (Spheron, VRLA Tech) note that the published minima assume sequence lengths around 512 with batch size 1 and gradient checkpointing on, and that doubling to 1024 tokens costs roughly 1.5–2× the activation memory. If your training examples are long contracts or whole files, size for your real sequence length, not the table.

Choosing a toolchain

Worked example: LoRA on a 32B, on one node

A concrete run, a 32B base, 16-bit LoRA, on your own support ticket history. Per the table that's around 76 GB of base and adapter state, leaving real room inside 128 GB for sequence length and batch.

Your dataset is a JSONL file of chat-formatted examples. It lives on the node's local NVMe, under /workspace, and is never uploaded anywhere:

# /data/tickets.jsonl, one resolved ticket per line
{"messages":[{"role":"user","content":"Invoice 8812 shows VAT twice…"},
             {"role":"assistant","content":"Duplicate VAT lines come from…"}]}
# train.py, Unsloth LoRA on a 32B base
from unsloth import FastLanguageModel
from trl import SFTTrainer, SFTConfig
from datasets import load_dataset

model, tokenizer = FastLanguageModel.from_pretrained(
    model_name    = "Qwen/Qwen3-32B",
    max_seq_length = 4096,
    load_in_4bit  = False,      # 16-bit LoRA, we have the memory
)

model = FastLanguageModel.get_peft_model(
    model,
    r = 32, lora_alpha = 32, lora_dropout = 0.0,
    target_modules = ["q_proj","k_proj","v_proj","o_proj",
                      "gate_proj","up_proj","down_proj"],
    use_gradient_checkpointing = "unsloth",
)

SFTTrainer(
    model = model, tokenizer = tokenizer,
    train_dataset = load_dataset("json", data_files="/data/tickets.jsonl")["train"],
    args = SFTConfig(
        per_device_train_batch_size = 2,
        gradient_accumulation_steps = 8,   # effective batch 16
        num_train_epochs = 3,
        learning_rate = 1e-4,
        warmup_ratio = 0.03,
        lr_scheduler_type = "cosine",
        bf16 = True,
        output_dir = "/data/ckpt/tickets-32b",
        save_steps = 200,
    ),
).train()

model.save_pretrained("/data/adapters/tickets-32b")   # a few hundred MB
# run it overnight, detached, and watch memory as it warms up
nohup python train.py > /data/logs/run1.log 2>&1 &
watch -n5 nvidia-smi

# next morning: serve the base + adapter
vllm serve Qwen/Qwen3-32B --enable-lora \
  --lora-modules tickets=/data/adapters/tickets-32b \
  --host 0.0.0.0 --port 8000

Two details that matter more than the hyperparameters. First, the adapter is small, hundreds of megabytes against a 60+ GB base, which means your fine-tune is a portable artefact you can version, diff and revert without moving model weights around. Second, if you hit an out-of-memory error on unified memory, the community reports on GB10 consistently point at the same first moves: lower per_device_train_batch_size and raise gradient accumulation to hold the effective batch, cut max_seq_length to what your data actually needs, and keep gradient checkpointing on. Unified memory removes the offloading gymnastics a discrete GPU forces on you, but it is not infinite.

Honest expectations on speed

NVIDIA's developer blog publishes fine-tuning throughput for the Spark: a peak of 5,079.4 tokens per second tuning Llama 3.3 70B with QLoRA, 53,657.6 tokens per second for LoRA on Llama 3.1 8B, and 82,739.2 tokens per second full fine-tuning a Llama 3.2 3B. Treat these as NVIDIA's best-case peaks, the same blog's own summary table lists lower sustained figures, so plan capacity on the conservative side. Community results line up with the general order of magnitude, one reported Gemma-3-4B LoRA run finished 3 epochs over 10,000 examples at batch size 4 in 10–12 hours.

Set expectations accordingly. A rented H100 will finish the same run faster; that is not in dispute and it is not the point. The relevant comparison is against your calendar, not against a datacenter GPU:

The privacy architecture

Here is the part that has nothing to do with benchmarks. When you fine-tune through a hosted API service, you upload the training set. Read that again in terms of what your training set actually is: your resolved support tickets, with customer names in them. Your contract archive. Your private repository. It is not a query, it is the corpus. And the resulting weights are a lossy but real encoding of it, sitting on infrastructure you don't administer.

The equivalent problem on rented shared GPUs is quieter but the same family: a multi-tenant node means your dataset is written to storage and memory on hardware whose other tenants and whose operators you have no visibility into, under whatever jurisdiction the region actually sits in.

What we build instead, in both delivery models:

The convenient part is that the two arguments point the same way. The workload with the highest privacy stakes is also the one whose defining constraint, memory capacity, not raw speed, is exactly what a 128 GB unified-memory machine is good at, and whose duty cycle, a long job overnight, then idle, is exactly what makes a dedicated box economical rather than wasteful.

A sensible first project

Your training data, your machine.

Run tonight's fine-tune on a dedicated Spark in EU-Central, deployed in under a minute, with $50 free on your first $100 top-up. Or put the box in your own rack, where the dataset never touches a network you don't own.

Deploy a Spark Keep it on-premise