My first decision model: trained in 6 minutes, 72% on an independent test

I trained my first decision model this week. The first try was 30 steps on a Tesla T4 and took almost 24 minutes. The second ran on the GPU in my own workstation and took 6.

A decision model does not write anything. It reads some text, a question and a list of options, and answers with a probability for each option. That is all it does, and it is the reason I wanted to try one.

  • 71.85% accuracy on the official test split, 2000 answers the model had never seen
  • 0.78 held-out accuracy as reported by Unsloth, on its own holdout
  • 6 minutes to train
  • 102 ms per call with five questions
  • 3.6 GiB on the GPU at peak

Two accuracy numbers, so I should explain. The first came from Unsloth’s own holdout. The second came from a script I ran afterwards on the official test split. The second is the one I would quote, and Unsloth’s 0.78 would have made a nicer headline. The rest of this post is how I got both, how to read the training dashboard, and where the model is wrong. Read on 🙂

What a decision model is

You give it three things: a state (the text to decide on), a question, and a list of options. It reads all of it in one pass and returns a probability for each option. It never writes text. Models such as Jev from TypeSafe AI and its open reimplementation Kev work this way, and Unsloth’s trainer uses a small scoring head of the same design as Cloudflare’s Clef.

The Unsloth interface offers three question types:

  • Yes / no: does the customer ask for a refund?
  • Choice: which team should handle this ticket?
  • Score: how urgent is this, on levels you define?

You can ask a chat model the same things. It will write a paragraph, and your code then has to find the answer inside it. That costs output tokens, adds latency and sometimes fails to parse. A decision model skips the writing, so the answer is always one of your options.

The part I like most is the probability. At 95% I let the system act. At 55% the message goes to a human, or it asks a clarifying question. A plain text answer cannot give you that dial. In the Studio Try panel one answer took 346 ms from 224 input tokens. In my scripted test a call with five questions took a median of 102 ms.

The setup

Same method, two runs. A single epoch of about 6 minutes lifted Unsloth’s held-out accuracy from 0.41 to 0.78, and my own test on 2000 answers gave 71.85%.

SettingRun 1Run 2
GPUTesla T4, 15 GiBRTX 5070 Ti, 16 GiB
Base modelQwen3.5-4BQwen3-4B
MethodQLoRAQLoRA
Dataset subsetagent_trace_observabilityall
Steps30 (1.76 epochs)70 (1.00 epoch)
Time23m 54s6m 1s
Speed0.02 steps/s0.19 steps/s
Final training loss1.01090.8054
Held-out accuracy (Studio’s own holdout)0.37 to 0.740.41 to 0.78
Calibration error0.090.06
Independent test accuracy (400 rows, 2000 answers)not run71.85%
Independent test calibration errornot run0.026
Unsloth Studio dashboard of the finished run 2 showing held-out accuracy 0.41 to 0.78 and calibration error 0.06
The finished run 2 in Unsloth Studio. The subtitle shows held-out accuracy 0.41 to 0.78 and calibration error 0.06.

Per step, the local GPU was about 9 times faster than the T4 (roughly 48 seconds against 5 seconds). The two runs used different subsets, different base models (Qwen3.5-4B and Qwen3-4B) and different held-out sets, so I read the gap between them as encouraging, not proven.

Run 1: 30 steps on a T4

The first run took 23 minutes 54 seconds. Unsloth reported held-out accuracy going from 0.37 to 0.74, with a calibration error of 0.09.

Unsloth Studio dashboard of run 1 after 30 steps on a Tesla T4
Run 1 on the Tesla T4: 30 steps, final loss 1.0109, 0.02 steps per second.

Settings:

  • Model: Qwen3.5-4B, trained with QLoRA (a 4-bit base model with small adapters on top)
  • Train as: Decision model
  • Dataset: LocalLLaMA/typed-decisions, subset agent_trace_observability
  • Batch 8 x 2 = 16, learning rate 1e-4, 30 steps
  • Hardware: Tesla T4, 15 GiB VRAM

Then I gave it a support ticket: "I was charged twice for my Pro plan this month and the second charge still shows as pending. Please refund one of them." The question was which team should handle it, with four options.

OptionProbability
billing75.3%
sales20.9%
technical3.0%
account0.8%
Unsloth Try a decision panel routing a double-charge support ticket to billing at 75.3 percent
The Try panel after run 1. The model picks billing at 75.3%, with 20.9% on sales.

The answer is right. But 21% for sales on an obvious refund request tells me the model was not very sure yet. Fair enough, it had seen 30 steps 😛

One mistake: I trained on the agent_trace_observability subset and then tested on support tickets. Unsloth’s guide recommends the all subset for typed-decisions. Between the runs I read the guide properly.

Run 2: 70 steps on my own GPU

After reading the guide I changed two things. I set the subset to all, which the guide recommends for typed-decisions, and I moved training to the 16 GiB GPU in my own workstation. One more thing differs: run 2 used Qwen3-4B, not Qwen3.5-4B.

The run was 70 steps, which is exactly one epoch. Loss fell from about 1.2 to about 0.8, and the GPU read 63 °C at step 44. It took 6 minutes 1 second at 0.19 steps per second. Unsloth reported held-out accuracy of 0.78 and a calibration error of 0.06. Great success!

Unsloth Studio dashboard during run 2 at step 44 of 70
Run 2 at step 44 of 70: loss around 0.9, 0.21 steps per second, the GPU at 99% utilization and 297 W.

A calibration error of 0.06 means the model’s stated confidence differs from how often it is right by about 6 percentage points on average. That is what makes the confidence dial usable in practice.

For context, Unsloth’s guide reports held-out accuracy of 78% for Qwen3.5-0.8B and 81% for Qwen3.5-2B on its own test mix. My 0.78 is in the same range, but it is a different test, so I am not claiming a comparison. The next section is the test that matters.

How to read the training dashboard

Loss is not accuracy. If you take one thing from the dashboard, take that. These are the numbers that matter, and what each one told me.

  • Loss: the average surprise per token, or how little probability the model gave the correct answer. Lower is better. Pure guessing scores the natural log of the number of options: 0.69 for yes/no, 1.39 for four options, 1.61 for five. Averaged over the mix of questions in the test split that works out to about 1.2, which is where run 2 started. It ended at 0.81. I am assuming the dashboard averages per question, so take that with a pinch of salt.
  • Smoothed loss: the orange line. The raw blue line is noisy because each step sees only a few examples, so read the orange one.
  • Learning rate: how big each weight update is. The chart shows a warmup to a peak, then a straight decay to nearly zero. The tiny number you see at the end of a run is just the tail of that schedule.
  • Grad norm: how hard each batch pushes the weights. It spikes on hard batches. Mine moved between about 0.05 and 1.9 with no explosion. I would only worry about values of 10 or more, or NaN.
  • Eval loss: the same loss on examples the model did not train on. Run 2 drew a single point at step 70, about 0.89, close to the training loss of 0.81, so there was no sign of overfitting. One point is not a trend.
  • Epoch: one full pass over the dataset. Run 1 made 1.76 passes and run 2 made exactly one.
  • VRAM and power: run 2 used 9.02 of 15.92 GiB and drew 297 W of a 300 W limit at 99% utilization. That means the GPU was the bottleneck, which is what you want in training.

The numbers that count are the held-out accuracy before and after training, and the calibration error. Unsloth reports both when the run finishes. Calibration error is the average gap between how confident the model says it is and how often it is actually right.

The test that matters: 2000 answers on the official split

Unsloth’s 0.78 was measured on its own holdout of 400 decisions, which was most likely carved out of the training data. The official test split is data the model never saw. Neither number is wrong. They measure different data.

So I loaded the trained model in 4-bit, took every row of the typed-decisions test split and asked all of its questions, one row at a time. I had planned 1000 examples. The test split only has 400 rows. Each row carries 5 questions, which makes 2000 individual answers. No test input is an exact copy of a training input, and nothing was skipped. Unsloth’s own evaluate function gave 72.0% (1440 of 2000), within three answers of mine.

Result: 71.85% (1437 of 2000 answers, 95% interval 69.8% to 73.8%). The strongest simple baseline reached 46.1%.

Question typeAnswersAccuracy
Yes / no60080.7%
Choice60069.0%
Score80067.4%

On score questions the average miss was 0.354 levels, and 97.5% of answers were within one level. By workflow, accuracy was 78.4% for customer service, 76.6% for invoice processing, 69.8% for security incidents and 62.6% for agent trace observability.

Against simple baselines

BaselineAccuracy
Always the single most common answer15.25%
Random guessing (1 over the number of options)31.75%
Most common answer for each question46.1%
My model71.85%

The part I find most interesting: the confidence dial

Can you trust the confidence? Mostly, yes. The calibration error on the test split was 0.026, and the average confidence of 0.742 sits close to the actual accuracy of 0.719. The table shows where it drifts: in the middle bins the model is a little overconfident, and at the top it is almost exact.

ConfidenceAnswersMean confidenceAccuracy
0.3 to 0.4190.3810.421
0.4 to 0.5890.4580.393
0.5 to 0.63240.5520.485
0.6 to 0.74250.6540.647
0.7 to 0.84150.7500.720
0.8 to 0.92830.8490.855
0.9 to 1.04450.9610.946

Seven rows are shown because the three bins below 0.3 are empty. The calibration error uses all ten.

That is the dial I mentioned earlier. If you act automatically only when the confidence is high, the accuracy of what you automate goes up quickly:

Act when confidence is at leastShare of answersAccuracy of those answers
0.922.3% (445)94.6%
0.836.4% (728)91.1%
0.757.3% (1145)84.2%

(445 + 283 + 415 is 1143, not 1145. I think the two missing answers sit exactly on 0.7, but I have not checked.)

In practice, I could let the model handle about a fifth of the answers on its own at 94.6% accuracy, and send everything else to a person.

The same numbers, interactive

Click through the tabs, hover over the points of the calibration chart, and move the slider to see what happens when the model only acts on answers it is sure about.

Decision model results: 2000 answers on the official test split

Qwen3-4B, fine-tuned for about 6 minutes with Unsloth (QLoRA, 70 steps), then tested on the held-out test split of typed-decisions: 400 rows, 5 questions each.

71.85%accuracy (1437 of 2000), 95% interval 69.8% to 73.8%
0.026calibration error, 10 bins
102 msmedian per call with 5 questions
3.6 GiBpeak memory on the whole GPU, 4-bit

How accurate is it?

Can you trust its confidence?

Each point is a group of answers with similar confidence. On the dashed line, “I am 80% sure” would be right 80% of the time. Hover, tap or tab to a point for details.

Pick a point on the chart.

What if it only acts when it is sure?

Move the slider. Anything below the threshold goes to a person.

0.70.80.9
handled automaticallysent to a personwrong, among the automatic ones

Source: my own run (decisions-all-100-silios), tested with a script on 400 test rows, one row at a time. Unsloth Studio reported a held-out accuracy of 0.41 before and 0.78 after training on its own internal holdout. This independent test on the official split is the lower and fairer number.

The 2000 answers come from only 400 rows and answers within a row are related, so the real uncertainty is wider than the interval shown. The “about N wrong” counts are calculated from the rounded percentages.

Speed and memory

MeasureResult
Load time (4-bit)10.5 s
Memory after loadabout 2.6 GiB allocated by PyTorch, 3,524 MiB on the whole GPU
Peak memory while runningabout 2.7 GiB allocated by PyTorch, 3,652 MiB on the whole GPU
Latency per call with 5 questions (median, 95th, 99th percentile)102 ms, 124 ms, 135 ms
Throughput9.6 calls per second, 48 questions per second
GPU monitor showing 4.25 GiB of memory in use and 99 percent utilization while the test ran
GPU monitor while the test ran: about 4.25 GiB in use on the whole card and 99% utilization. This counts everything on the GPU, including the desktop, so it is higher than the model itself.

The whole-GPU figure includes the desktop and driver overhead, so the model itself needs a little less. At roughly 3.6 GiB at peak it leaves most of a 16 GiB card free.

The local GPU also made each step about 9 times faster. At the T4's speed, 70 steps would have taken about 56 minutes. That is arithmetic, not a measurement (so don't quote me 😛).

Where it fails

Six of the ten most confident wrong answers were the same invoice question, "duplicate". The correct answer was true, and the model said false with 95% to 98% confidence. That looks like a systematic gap, not noise, and a good place to start with more training data.

What is not perfect

  • The 2000 answers come from only 400 rows, and answers within a row are related, so the real uncertainty is wider than the interval above.
  • The correct answers are the top choice of several human annotators who often disagree (on average the labels put only 66% of the weight on the top answer), so even a perfect model could not score 100%.
  • I did not run the untuned base model on this test, so the 0.41 starting point is still Unsloth's number from its own holdout.
  • One run each. I did not repeat them with other random seeds, so I do not know how much of the 4-point gap between the runs is noise.
  • Not a clean comparison. The runs used different subsets and different base models, and only run 2 got the independent test.
  • One epoch is short. Unsloth's own results use 2.
  • No real traffic yet. A good benchmark number is not the same as good routing on messy real messages.

This is a demonstration and not a benchmark. It ran on a personal workstation.

What I will try next

  • Follow the guide's recommended settings (2 epochs, LoRA rank 16, learning rate 2e-4) and change one setting at a time, so I know what moved the accuracy.
  • Build a small dataset of my own. Real labeled messages should beat generic data for a specific task. One candidate is triaging shopper messages for an e-commerce assistant into product, order, returns or human handoff, with a refund yes/no and an urgency score decided in the same pass.
  • Use the confidence dial: act automatically when the top probability is high, and escalate when it is not.
  • Serve the model through the Decision API button and call it from my own code.

Reproduce it

You can repeat this in under an hour, and in minutes on a decent local GPU.

  1. Install Unsloth Desktop, or the Studio web UI. Training needs an NVIDIA GPU on Windows or Linux, or you can use Unsloth's free Colab and Kaggle notebooks.
  2. Open the Train tab, pick a 4B Qwen model (run 1 used Qwen3.5-4B, run 2 used Qwen3-4B), and set Train as to Decision model.
  3. Choose the dataset LocalLLaMA/typed-decisions and set Subset to all.
  4. Set the number of steps. The guide suggests 60 for a quick try, and says Qwen3.5-4B reached 76% in about 10 minutes on an L4.
  5. Click Start Training, then read the held-out accuracy and calibration error when it finishes.
  6. Click Use in Decision API, then try the model in the Try panel with your own text, question and options.
  7. If the GPU is shared with something else, such as a local LLM server, stop that first.

Takeaways

  • A decision model returns a choice and a probability, not text, and the probability is what makes it useful.
  • A tiny run moved Unsloth's held-out accuracy from 0.41 to 0.78, and my own test on 2000 answers said 72%.
  • Watch held-out accuracy and calibration error. Loss alone does not tell you the model is good.

Read the held-out accuracy first. The loss curve can wait.

Have fun! That's all folks 😀

Links

I gave a local AI one prompt. 1h 45m later it had built a voxel pagoda garden

Local AI experiment

I gave a local agent one hard prompt, “Design and create a very creative, elaborate, and detailed voxel art scene of a pagoda in a beautiful garden with trees, including some cherry blossoms. Make the scene impressive and varied and use colorful voxels. Use whatever libraries to get this done but make sure I can paste it all into a single HTML file.”, and went about my evening. An hour and 45 minutes later it had built a complete, interactive 3D Japanese garden in a single HTML file. All of it ran on one gaming desktop.

Live demo, running right here. Drag to orbit, scroll to zoom, press D for dusk. Open it full screen for the best view.

1h 45mwall-clock, start to last edit
~180tool calls (estimated)
34.9 KBone self-contained index.html
1context compaction, survived
0console errors on live load

What it built

The scene is a 72 by 72 block island of roughly 27,000 voxels, generated from a seed so you can regrow a different garden with one click. It has a multi-roof pagoda on a stone plinth, a koi pond with an open-arch red bridge, a waterfall, cherry, maple, pine and bamboo, stone lanterns, a torii gate and stepping-stone paths. On top of the geometry sit falling petals, swimming koi and a day-to-dusk cycle with a custom sky shader, stars, fireflies and lanterns that light up at night.

It also ships its own controls: Day/Dusk, Petals, Auto-rotate, two camera presets, Regrow and a PNG export. The model was not asked for any of that. It was asked for a scene.

The setup

Hardware

A normal desktop: Core i5-13600K, 64 GB DDR4 and a single RTX 5070 Ti with 16 GB of VRAM. No cloud fallback was needed.

How the time was spent

Time (EEST)Phase
22:22 – 22:30Project folder, HTML template and a generator script
22:30 – 23:30Scene build and iteration: pagoda, garden, trees, water, bridge, waterfall
23:30 – 00:02Atmosphere polish: sky shader, sun sprite, fog and dusk color grading
00:02 – 00:06Verification passes: pond and bridge close-ups, voxel probes
00:06 – 00:07Final polish: open-arch bridge, taller waterfall, horizon haze

Times are reconstructed from file timestamps, because no start stamp was taken. October 11 to 12, 2026.

The part I find most interesting: it checked its own work

Most of those 180 tool calls were not writing code. They were a loop, repeated over and over:

patch the file→headless test→screenshot→local vision review→patch again

The checks were concrete. A syntax check, headless generation across three seeds, a live browser load, and programmatic probes of exact voxel coordinates to confirm that the bridge deck, arch, railing, waterfall and koi really exist where they should. The screenshots were reviewed by the vision encoder of the same local model.

The run also crossed the 120K context window once and was compacted. It carried on cleanly because the verification results had been written to files in the workspace and could be re-probed, not just remembered in the conversation. That is a habit worth copying when you work with any long-running agent.

What is not perfect

  • The far horizon of the lake is a little too crisp at dusk.
  • The lantern lights pool visibly if you zoom in close at night.
  • A programmatic probe proves a thing is there, not that it looks right. I would still look at the final scene with my own eyes, and so should you.
  • No token counters were captured during the run, so there is no throughput figure here. This is one task on one run, a demonstration and not a benchmark.

Links

Qwen3-Next-80B-A3B on 16 GB VRAM + 64 GB RAM

Qwen3-Next-80B-A3B on 16 GB VRAM + 64 GB RAM
80Bweights in total
3Bused for each token
512experts in each MoE layer
10 + 1experts picked per token, plus one shared

A mixture-of-experts layer holds many small networks, the experts. A router reads each token and picks a few of them. The rest do nothing for that token, so they do not need to be in the fastest memory.

Three places to keep weights

GPU VRAM, 16 GB
896 GB/s on an RTX 5070 Ti. The GPU reads from here at full speed.
System RAM, 64 GB
Tens of GB/s. Holds what does not fit in VRAM.
NVMe SSD
A few GB/s. Holds the whole model file, so it always has every expert.

Try it: where do the 10 experts live?

One layer is drawn as 512 squares. Change the size of the model file and see how the squares split across the three tiers. Then send a token.

16 GB~48 GB: Q4 file of this model200 GB
in VRAM in RAM on SSD only picked by the router
This token: 1 from VRAM, 9 from RAM, 0 from SSD
25%of experts in VRAM
75%in RAM
0%on SSD only
28.9rough speed ceiling, tokens/s

Illustration, not a measurement. Assumptions: about 12 GB of VRAM and 56 GB of RAM are free for experts, experts are spread evenly, the router picks them at random, and the 3B active weights at 4-bit (about 1.8 GB) are read once per token at 896 GB/s from VRAM, 40 GB/s from RAM and 4 GB/s from the SSD. Real routing is uneven and real programs differ, so treat the speed as an upper bound for the idea, not a forecast.

What to remember

  • A 4-bit copy of this model is about 45 to 50 GB. That fits in 64 GB of RAM, so the SSD is rarely touched.
  • A model larger than RAM must read experts from the SSD again and again. Then the SSD speed sets the token speed.
  • Programs differ. Many, such as llama.cpp, put fixed groups of weights in VRAM and RAM and let the operating system load the rest from disk. Only some move single experts between tiers while running.

The GPU in this example: RTX 5070 Ti

SpecValueWhy it matters here
Video memory16 GB GDDR7The VRAM tier. Anything larger spills into RAM.
Memory bus256-bitTogether with the memory speed, sets the bandwidth.
Memory speed28 Gbps per pin256 bits x 28 Gbps / 8 = 896 GB/s.
Memory bandwidth896 GB/sThe VRAM speed used in the estimate above.
Host linkPCIe 5.0The path between the GPU and system RAM.

Source for the model figures: Qwen3-Next-80B-A3B model card (80B total, 3B active, 512 experts, 10 active, 1 shared).