cd /news/large-language-models/why-does-a-local-llm-give-a-differen… · home › topics › large-language-models › article
[ARTICLE · art-166046] src=dev.to ↗ pub= topic=large-language-models verified=true sentiment=· neutral

Why does a local LLM give a different score every time, and does temperature 0 fix it?

A developer measured how much a local LLM's CV-scoring output varies by reading the model's raw next-token probabilities, using Qwen2.5-3B-Instruct (4-bit, MLX) on an 8 GB M1 Mac. A single forward pass over a pre-filled "Score: " prompt showed the model assigns 74.2% probability to a 7 and 23.3% to a 6 at temperature 1, and that llama.cpp's default sampler chain (top-k 40, top-p 0.95, min-p 0.05, temp 0.8) narrows the choice to just those two scores. The work also shows what temperature 0 does not fix, since the underlying distribution remains probabilistic.

by read7 min views1 publishedNov 9, 2026

Give a language model a CV and ask for a score from 1 to 10, and you get a number and a confident reason. Ask again and the number can move. The usual advice is "set temperature to 0", which is correct but leaves two questions open: how much does the score actually move with default settings, and what does temperature 0 not fix? Both can be measured on a laptop in a few minutes, because the score is a single token and the model hands you its probability directly.

The setup below uses Qwen2.5-3B-Instruct (4-bit, MLX) with mlx-lm 0.32.2 on an 8 GB M1 Mac. The prompts are the ones from a CV-scoring demo in the osFoundry desktop app's Models → Playground: a six-line CV for a junior data analyst role, followed by "Score this CV from 1 to 10 for a junior data analyst role. Start with the score, then one reason." The app runs local models through llama.cpp's llama-server, so the sampler settings come from there.

When a request carries no temperature, llama-server samples with its own defaults. The build bundled with the app (version 10358) lists them in --help:

--temp, --temperature N    temperature (default: 0.80)
--top-k N                  top-k sampling (default: 40, 0 = disabled)
--top-p N                  top-p sampling (default: 0.95, 1.0 = disabled)
--min-p N                  min-p sampling (default: 0.05, 0.0 = disabled)
-s, --seed SEED            RNG seed (default: -1, use random seed for -1)

These apply to every request that doesn't set its own values. When the app starts llama-server, it only adds --temp if a temperature has been configured for the server (local_inference.rs), so a request without a temperature samples at 0.8 with a random seed, which is also what most llama.cpp front ends do out of the box.

A free-form answer can start with "Score: 7", "7/10" or "I would rate this CV a 7", which makes the score hard to locate. The fix is to pre-fill the start of the reply, so the very next token the model produces is the digit:

import mlx.core as mx
from mlx_lm import load, generate
from mlx_lm.sample_utils import make_sampler

model, tok = load("mlx-community/Qwen2.5-3B-Instruct-4bit")
DIGITS = {d: tok.encode(str(d))[0] for d in range(1, 10)}

def ids_for(cv, question, prefill="Score: "):
    msgs = [{"role": "user", "content": cv + "\n\n" + question}]
    text = tok.apply_chat_template(msgs, add_generation_prompt=True, tokenize=False)
    return tok.encode(text + prefill)

def digit_probs(ids, T=1.0):
    logits = model(mx.array([ids]))[0, -1].astype(mx.float32)
    p = mx.softmax(logits / T)
    return {d: float(p[t]) for d, t in DIGITS.items()}

Qwen's tokenizer splits numbers into single digits, so "7" is one token and "10" would start with the token "1". One forward pass over the 173-token prompt gives the whole distribution of the score.

For the English CV, at temperature 1 (the model's raw probabilities):

Score Probability
7 74.2%
6 23.3%
8 1.3%
5 1.2%

The model doesn't "think" the CV is a 7. It thinks it is probably a 7, with a one-in-four chance of a 6. Temperature divides the logits before the softmax, which sharpens or flattens this:

Temperature P(7) P(6) P(8)
1.0 74.2% 23.3% 1.3%
0.8 80.2% 18.9% 0.5%
0.5 91.0% 9.0% 0.0%
0.2 99.7% 0.3% 0.0%

llama-server doesn't apply temperature alone. Its default chain runs top-k 40, then top-p 0.95, then min-p 0.05, and temperature last. Applied to these logits, top-p already cuts everything after 7 and 6 (together they are over 95%), so the default sampler picks between exactly two scores:

def llamacpp_default(logits, temp=0.8, top_k=40, top_p=0.95, min_p=0.05):
    order = mx.argsort(-logits)[:top_k]
    kept = logits[order]
    p = mx.softmax(kept)
    n = int(mx.sum(mx.cumsum(p) < top_p).item()) + 1
    idx = [i for i in range(n) if p[i].item() >= min_p * p[0].item()]
    q = mx.softmax(kept[mx.array(idx)] / temp)
    return {tok.decode([int(order[i])]): float(q[j]) for j, i in enumerate(idx)}

Then I sampled it for real: 200 generations from the same prompt with mlx-lm's sampler set to the same four values (make_sampler(temp=0.8, top_p=0.95, min_p=0.05, top_k=40)).

7 6
Predicted by the chain 80.9% 19.1%
Observed, 200 runs 162 (81%) 38 (19%)

So with default settings, about one run in five scores this CV a 6 instead of a 7. If you score two candidates once each and one gets a 6 and the other a 7, there is a real chance the gap is the draw.

The same CV and question in Japanese (222 tokens, pre-fill 「点数:」) gave a much less decided model:

Score Probability at T=1 Observed, 200 runs at the defaults
8 39.8% 108
9 23.8% 44
7 19.4% 40
10 (starts with "1": 4.1%) 8

Here the most likely score wins only about half the time, and four different scores came out of 200 runs. How much a score wobbles is not a fixed property of the model. It depends on how close the top candidates are for this particular input, which you can only see by looking at the probabilities.

With temperature 0 (greedy decoding: always the most likely token), I generated the full 40-token answer 20 times for each language. There was 1 distinct text per language, character for character. For the English CV it is "7" followed by the same reason every time, and for Japanese 「8点」.

A fixed seed does the same for sampling: 10 runs with mx.random.seed(42) reset before each gave 1 distinct text. That is useful when you want variety but need to reproduce a particular run. It is not a fairness tool, though. With a seed, a different CV gets a different sequence of draws, so two candidates are still scored by different luck.

One condition applies to all of this: every run here was a single request on an idle model. Servers that batch several requests together can produce tiny floating-point differences between runs, which only matter when two tokens are nearly tied. That was not measured here.

Greedy decoding makes the output a function of the input. It doesn't make it a stable function. I made four edits that should not change a CV's score and took the greedy score and the probabilities each time:

Edit (English) Greedy score P(top) Runner-up
none 7 74.2% 6 at 23.3%
Skills line moved up 7 69.6% 6 at 28.1%
Name changed (Mia Chen → James Miller) 7 68.5% 6 at 28.5%
One extra blank line 7 71.0% 6 at 26.1%
Question reworded 6 67.5% 5 at 19.3%

The reworded question was "Rate this CV for a junior data analyst role on a 1 to 10 scale. Give the score first, then one reason." Same task, same CV, and the greedy score drops from 7 to 6. In Japanese, all five versions stayed at 8 at temperature 0, but the top probability never went above 44%, and the runner-up switched between 9 and 7 depending on the edit.

So temperature 0 removes the randomness between runs, not the sensitivity to the prompt. For comparing CVs this means:

/completion endpoint returns token probabilities if you pass n_probs, and with the pre-fill trick above the first token is the score. A 7 at 74% and an 8 at 40% are not the same kind of answer. In the osFoundry desktop app, the same experiment is three panels with one local model in Models → Playground. Run the prompt once with the defaults to see the spread, then open the settings row and set Temp to 0.

The temperature and top-p glossary entries cover the two settings in more detail. The app is free from the osFoundry download page.

── more in #large-language-models 4 stories · sorted by recency
tooldirectory.ai · · #large-language-models
LM Studio
── more on @qwen2.5-3b-instruct 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/why-does-a-local-llm…] indexed:0 read:7min 2026-11-09 · —