Lesson 12 of 73
Sampling: Greedy, Temperature, Top-k, Top-p, and Min-p
A language model does not output a word. After it reads the text so far, it outputs one number for every entry in its vocabulary (the list of all tokens it knows). Each number is a score for how well that token fits as the next one. These scores are called logits. A sampler is the small piece of code that looks at the logits and picks one token id. Text generation is this step repeated: pick a token, append it to the text, run the model again, pick the next token.
The sampler matters because the same model produces very different text depending on how it picks. Always taking the top score gives repetitive text. Drawing completely at random from all tokens gives nonsense. The samplers in this lesson sit between those two extremes, and every chat or coding tool built on a local model exposes their settings.
This lesson implements the basic samplers in Java on a 6-token vocabulary, compares them on a confident and a flat distribution, and checks their rules against the llama.cpp source. Afterwards you can predict what each setting does to a distribution, and you can read the sampler chain line that llama.cpp prints. The lesson follows the transformer lesson, whose forward pass (one run of the model over the text) ends in logits, and next-token prediction, which built the bigram model (a model that predicts the next character from the previous one only) used in Example 14.
| Sampler | Effect, in plain words |
|---|---|
| Greedy | Always take the token with the highest score |
| Sampling | Draw a random token, where likely tokens are drawn more often |
| Temperature | Make the distribution more peaked or more even |
| Top-k | Keep only the k most probable tokens |
| Top-p | Keep the smallest group of top tokens whose probabilities add up to p |
| Min-p | Keep tokens that have at least min_p times the top token's probability |
Penalties, logit bias, and other samplers follow in Penalties, Logit Bias, and Sampler Chains. Most programs in this lesson use one vocabulary and one logit vector, so results can be compared across examples. The tokens are the, cat, sat, mat, dog, ran, with logits 2.0, 1.0, 0.5, 0.1, -0.5, -1.0. Think of them as the candidates for the word after "the dog sat on the".
Example 1: from logits to a probability table
Logits are hard to reason about: they can be negative, they do not add up to anything, and they have no fixed range. To draw a random token we need probabilities instead, which are positive numbers that add up to 1. The function that converts one into the other is called softmax.
Softmax works in two steps. First it raises e (about 2.718) to the power of each logit, which makes every value positive and keeps the order. Then it divides each value by the sum of all of them, so the total is 1.
This example prints the logit, the exponential, and the final probability of each token. The program subtracts the maximum logit before exponentiating. That does not change the result, and it prevents overflow (a number too large for a double) when logits are big. Look at the last column and at the final line, which shows the probabilities add up to 1.
public class LogitsToProbs {
static double[] softmax(double[] logits) {
double max = Double.NEGATIVE_INFINITY, sum = 0;
for (double l : logits) max = Math.max(max, l);
double[] p = new double[logits.length];
for (int i = 0; i < p.length; i++) { p[i] = Math.exp(logits[i] - max); sum += p[i]; }
for (int i = 0; i < p.length; i++) p[i] /= sum;
return p;
}
public static void main(String[] args) {
java.util.Locale.setDefault(java.util.Locale.ROOT);
String[] words = {"the", "cat", "sat", "mat", "dog", "ran"};
double[] logits = {2.0, 1.0, 0.5, 0.1, -0.5, -1.0};
double[] p = softmax(logits);
double sum = 0;
for (int i = 0; i < words.length; i++) {
System.out.printf("%-4s logit %5.1f exp %6.3f probability %.3f%n", words[i], logits[i], Math.exp(logits[i]), p[i]);
sum += p[i];
}
System.out.printf("sum of probabilities: %.3f%n", sum);
}
}the logit 2.0 exp 7.389 probability 0.534
cat logit 1.0 exp 2.718 probability 0.196
sat logit 0.5 exp 1.649 probability 0.119
mat logit 0.1 exp 1.105 probability 0.080
dog logit -0.5 exp 0.607 probability 0.044
ran logit -1.0 exp 0.368 probability 0.027
sum of probabilities: 1.000Take the first row by hand. A logit of 2.0 gives exp(2.0) = 7.389. The six exponentials add up to 13.836, so the has probability 7.389 / 13.836 = 0.534.
The numbers show how logits behave. A difference of one logit unit is a factor of e = 2.718 in probability: cat has a logit one below the, and its probability is 2.718 times smaller (0.196 against 0.534). Logits have no scale of their own, and only differences matter. Adding 100 to every logit gives the same probabilities, which is the reason subtracting the maximum is safe.
Example 2: greedy choice
The simplest way to pick a token is to take the one with the highest logit and ignore the rest. This is called greedy decoding. The program finds the best index with a loop that replaces the current best only when a logit is strictly larger, so on a tie the first index wins. The llama.cpp greedy sampler is written the same way.
The program runs the choice three times to show it never changes, and then tests a tie between cat and sat.
public class Greedy {
// first index of the largest value, as llama.cpp's greedy sampler does (it replaces only on a strictly larger logit)
static int argmax(double[] logits) {
int best = 0;
for (int i = 1; i < logits.length; i++) if (logits[i] > logits[best]) best = i;
return best;
}
public static void main(String[] args) {
String[] words = {"the", "cat", "sat", "mat", "dog", "ran"};
double[] logits = {2.0, 1.0, 0.5, 0.1, -0.5, -1.0};
for (int run = 1; run <= 3; run++) System.out.println("run " + run + ": " + words[argmax(logits)]);
double[] tie = {1.0, 3.0, 3.0, 0.0, 0.0, 0.0};
System.out.println("tie between cat and sat: " + words[argmax(tie)]);
}
}run 1: the
run 2: the
run 3: the
tie between cat and sat: catAll three runs print the, because the same logits always give the same answer. This is what deterministic means. In the tie test both cat and sat have logit 3.0, and cat wins because it has the lower index.
Greedy needs no probabilities, because softmax never changes the order of the logits, so the largest logit is also the largest probability. The cost of greedy is repetition. Once a context makes one continuation the best, greedy chooses it again every time the same situation returns. This is why lesson 5 saw loops such as "the the the".
Example 3: sampling by inverse CDF
Sampling draws a token at random, but not evenly: each token is drawn with the probability the model gave it. A token with probability 0.534 should come out in about 53 of 100 draws.
A computer can only produce a uniform random number u between 0 and 1, where every value is equally likely. The trick is to line the tokens up and give each one a slice of the range from 0 to 1, as wide as its probability. A u that lands in a token's slice selects that token. Because u is uniform, the chance of landing in a slice equals the slice width.
In code, the program keeps a running total of the probabilities and stops at the first token whose running total passes u. This is called inverse-CDF sampling. CDF means cumulative distribution function, which is the running total of the probabilities in a fixed token order. The program uses a java.util.Random with a fixed seed (a starting number that makes the sequence of random numbers repeatable), so the five draws are the same on every run.
The program first prints each token's interval, then draws five values of u and shows which token each one selects.
import java.util.Random;
public class InverseCdf {
static double[] softmax(double[] logits) {
double max = Double.NEGATIVE_INFINITY, sum = 0;
for (double l : logits) max = Math.max(max, l);
double[] p = new double[logits.length];
for (int i = 0; i < p.length; i++) { p[i] = Math.exp(logits[i] - max); sum += p[i]; }
for (int i = 0; i < p.length; i++) p[i] /= sum;
return p;
}
// walk along the probabilities until the running total passes u
static int sample(double[] p, double u) {
double run = 0;
for (int i = 0; i < p.length; i++) { run += p[i]; if (u < run) return i; }
return p.length - 1;
}
public static void main(String[] args) {
java.util.Locale.setDefault(java.util.Locale.ROOT);
String[] words = {"the", "cat", "sat", "mat", "dog", "ran"};
double[] p = softmax(new double[] {2.0, 1.0, 0.5, 0.1, -0.5, -1.0});
double run = 0;
for (int i = 0; i < p.length; i++) {
System.out.printf("%-4s p %.3f interval [%.3f, %.3f)%n", words[i], p[i], run, run + p[i]);
run += p[i];
}
Random rng = new Random(42);
for (int i = 0; i < 5; i++) {
double u = rng.nextDouble();
System.out.printf("u = %.3f -> %s%n", u, words[sample(p, u)]);
}
}
}the p 0.534 interval [0.000, 0.534)
cat p 0.196 interval [0.534, 0.731)
sat p 0.119 interval [0.731, 0.850)
mat p 0.080 interval [0.850, 0.930)
dog p 0.044 interval [0.930, 0.973)
ran p 0.027 interval [0.973, 1.000)
u = 0.728 -> cat
u = 0.683 -> cat
u = 0.309 -> the
u = 0.277 -> the
u = 0.666 -> catThe intervals split the range from 0 to 1 into six pieces, one per token, each as wide as the token's probability. A u below 0.534 selects the, a u from 0.534 to 0.731 selects cat, and so on. The five draws show the mapping. The value 0.728 falls in the cat interval, and 0.309 falls in the the interval. Three of the five draws are cat or the, which is expected because together they hold 73 percent of the probability.
The llama.cpp dist sampler uses the same walk. It does not normalize first. It draws u, multiplies it by the sum of the unnormalized weights exp(logit - max), and walks until the running sum reaches that target. The result is the same token.
Example 4: 10,000 draws match the probabilities
A few random draws prove nothing, because any token can come up by luck. This example checks that the walk of Example 3 really produces each token with its probability. It draws 100, 1,000, 10,000, and 100,000 tokens and compares the share of each token with its expected probability. Look at the "largest share error" column: it should shrink as the number of draws grows.
import java.util.Random;
public class ManyDraws {
static double[] softmax(double[] logits) {
double max = Double.NEGATIVE_INFINITY, sum = 0;
for (double l : logits) max = Math.max(max, l);
double[] p = new double[logits.length];
for (int i = 0; i < p.length; i++) { p[i] = Math.exp(logits[i] - max); sum += p[i]; }
for (int i = 0; i < p.length; i++) p[i] /= sum;
return p;
}
// walk along the probabilities until the running total passes u
static int sample(double[] p, double u) {
double run = 0;
for (int i = 0; i < p.length; i++) { run += p[i]; if (u < run) return i; }
return p.length - 1;
}
public static void main(String[] args) {
java.util.Locale.setDefault(java.util.Locale.ROOT);
String[] words = {"the", "cat", "sat", "mat", "dog", "ran"};
double[] p = softmax(new double[] {2.0, 1.0, 0.5, 0.1, -0.5, -1.0});
Random rng = new Random(42);
for (int n : new int[] {100, 1_000, 10_000, 100_000}) {
int[] counts = new int[6];
for (int i = 0; i < n; i++) counts[sample(p, rng.nextDouble())]++;
double worst = 0;
for (int i = 0; i < 6; i++) worst = Math.max(worst, Math.abs((double) counts[i] / n - p[i]));
System.out.printf("%,7d draws: share of the %.3f (expected %.3f), largest share error %.4f%n", n, (double) counts[0] / n, p[0], worst);
if (n == 10_000)
for (int i = 0; i < 6; i++) System.out.printf(" %-4s expected %5.0f got %5d%n", words[i], p[i] * n, counts[i]);
}
}
} 100 draws: share of the 0.530 (expected 0.534), largest share error 0.0308
1,000 draws: share of the 0.503 (expected 0.534), largest share error 0.0325
10,000 draws: share of the 0.530 (expected 0.534), largest share error 0.0043
the expected 5341 got 5298
cat expected 1965 got 1967
sat expected 1192 got 1217
mat expected 799 got 783
dog expected 438 got 466
ran expected 266 got 269
100,000 draws: share of the 0.534 (expected 0.534), largest share error 0.0013The counts approach the probabilities as the number of draws grows. The largest error in any token's share is 0.031 after 100 draws, 0.004 after 10,000, and 0.001 after 100,000. The 1,000-draw line is slightly worse than the 100-draw line, which shows that the error shrinks on average, not at every step.
At 10,000 draws the counts are close to the expected counts but not equal: 5,298 against 5,341 for the, and 466 against 438 for dog. Random counts typically deviate by about sqrt(n * p * (1 - p)). For the at n = 10,000 this is sqrt(10000 * 0.534 * 0.466), which is about 50. The deviations here are of that size, so the sampler draws each token with its probability.
One consequence matters in practice: a model that samples many tokens produces the low-probability tokens too. At a probability of 0.027, ran appears about once every 37 tokens. Over a long answer, rare tokens will appear, and some of them will be wrong. The filters later in this lesson exist to cut them off.
Example 5: temperature
Temperature is a setting that controls how adventurous the sampling is. It divides every logit by a number T before softmax. The effect follows from the gap between two tokens: if two logits differ by 1, their probabilities differ by a factor of e^1. After dividing by T = 0.5, the gap becomes 2, so the factor is e^2, which is bigger.
- A value below 1 widens the gaps between logits, so the top token gains probability and the output becomes more predictable.
- A value above 1 narrows the gaps, so the distribution flattens and less likely tokens get chosen more often.
- At
T = 1the model's own distribution is unchanged.
The name comes from physics, where a hot system moves around randomly and a cold one settles into its lowest state. The mapping is loose: here "hot" means an even distribution and "cold" means a peaked one. The analogy stops there, because this temperature has no units.
The program prints one column of probabilities per temperature. Compare the the row across columns, and notice that the order of the tokens never changes.
public class Temperature {
static double[] softmax(double[] logits, double temperature) {
double max = Double.NEGATIVE_INFINITY, sum = 0;
for (double l : logits) max = Math.max(max, l / temperature);
double[] p = new double[logits.length];
for (int i = 0; i < p.length; i++) { p[i] = Math.exp(logits[i] / temperature - max); sum += p[i]; }
for (int i = 0; i < p.length; i++) p[i] /= sum;
return p;
}
public static void main(String[] args) {
java.util.Locale.setDefault(java.util.Locale.ROOT);
String[] words = {"the", "cat", "sat", "mat", "dog", "ran"};
double[] logits = {2.0, 1.0, 0.5, 0.1, -0.5, -1.0};
double[] temps = {0.2, 0.7, 1.0, 1.5};
double[][] columns = new double[temps.length][];
System.out.print("token ");
for (int t = 0; t < temps.length; t++) { columns[t] = softmax(logits, temps[t]); System.out.printf(" T=%.1f", temps[t]); }
System.out.println();
for (int i = 0; i < words.length; i++) {
System.out.printf("%-6s ", words[i]);
for (double[] c : columns) System.out.printf(" %.3f", c[i]);
System.out.println();
}
}
}token T=0.2 T=0.7 T=1.0 T=1.5
the 0.993 0.683 0.534 0.402
cat 0.007 0.164 0.196 0.206
sat 0.001 0.080 0.119 0.148
mat 0.000 0.045 0.080 0.113
dog 0.000 0.019 0.044 0.076
ran 0.000 0.009 0.027 0.054Check the T = 0.2 column by hand. The logits become 10, 5, 2.5, 0.5, -2.5, -5. The gap between the first two is now 5, a factor of e^5 = 148. So the takes e^10 = 22,026 of a total of about 22,188, which is 0.993. The table shows it: the has probability 0.993 and the output is nearly deterministic.
At T = 0.7 the top token has 0.683. At T = 1.5 it drops to 0.402, and ran rises from 0.027 to 0.054, twice as likely. The order of the tokens never changes. Temperature changes only how much probability the leaders keep. llama.cpp defaults to a temperature of 0.80.
Example 6: temperature near zero, and how engines treat 0
If lower temperature means more predictable output, what is the limit? As T approaches 0, the top probability approaches 1, and the sampler turns into greedy decoding. At exactly 0 there is a problem: dividing by zero is undefined.
This example shows both parts. The first four lines print the top token's probability as T shrinks. The next lines show what plain floating-point math does at T = 0, and the last line shows the rule llama.cpp uses instead. In the code, applyTemperature implements that rule: at temp <= 0 it keeps only the first largest logit and sets every other logit to negative infinity.
public class TemperatureZero {
// the rule of llama.cpp's temperature sampler: at temp <= 0 keep only the first largest logit
static double[] applyTemperature(double[] logits, double temp) {
double[] out = new double[logits.length];
if (temp <= 0) {
int best = 0;
for (int i = 1; i < logits.length; i++) if (logits[i] > logits[best]) best = i;
java.util.Arrays.fill(out, Double.NEGATIVE_INFINITY);
out[best] = logits[best];
} else {
for (int i = 0; i < logits.length; i++) out[i] = logits[i] / temp;
}
return out;
}
static double[] softmax(double[] logits) {
double max = Double.NEGATIVE_INFINITY, sum = 0;
for (double l : logits) max = Math.max(max, l);
double[] p = new double[logits.length];
for (int i = 0; i < p.length; i++) { p[i] = Math.exp(logits[i] - max); sum += p[i]; }
for (int i = 0; i < p.length; i++) p[i] /= sum;
return p;
}
public static void main(String[] args) {
java.util.Locale.setDefault(java.util.Locale.ROOT);
double[] logits = {2.0, 1.0, 0.5, 0.1, -0.5, -1.0};
for (double t : new double[] {1.0, 0.1, 0.01, 0.001})
System.out.printf("T = %-5s probability of the top token: %.6f%n", t, softmax(applyTemperature(logits, t))[0]);
System.out.println("logit / 0.0 in Java: " + logits[0] / 0.0 + ", and " + logits[1] / 0.0 + " for the next one");
System.out.println("softmax of those values: " + java.util.Arrays.toString(softmax(new double[] {Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY})));
System.out.println("rule for T <= 0: " + java.util.Arrays.toString(softmax(applyTemperature(logits, 0))));
}
}T = 1.0 probability of the top token: 0.534060
T = 0.1 probability of the top token: 0.999954
T = 0.01 probability of the top token: 1.000000
T = 0.001 probability of the top token: 1.000000
logit / 0.0 in Java: Infinity, and Infinity for the next one
softmax of those values: [NaN, NaN]
rule for T <= 0: [1.0, 0.0, 0.0, 0.0, 0.0, 0.0]At T = 0.1 the top token has probability 0.99995, and at T = 0.01 it rounds to 1.000000. At T = 0 Java gives Infinity for a positive logit divided by zero. Softmax of two infinities produces NaN ("not a number"), because it computes Infinity - Infinity. A NaN cannot be used to pick a token.
Engines therefore treat T <= 0 as a special case. In the pinned llama.cpp source (the exact version this course uses), the temperature sampler keeps the first token with the highest logit, sets the other logits to negative infinity, and returns. The file common/common.h documents temp <= 0.0 as "sample greedily". The last output line shows the result: a probability of 1.0 for the top token and 0.0 for the rest. Servers inherit this rule, so "temperature": 0 in a request means greedy.
Example 7: entropy as a measure of randomness
Temperature is a knob, but how do we measure how random a distribution is? One number for this is entropy. If one token has nearly all the probability, the next token is almost certain and entropy is low. If many tokens are equally likely, the next token is hard to guess and entropy is high.
Entropy is measured in bits with the formula H = -sum(p * log2(p)). It is 0 when one token has all the probability, and log2(n) when all n tokens are equally likely. Here log2(x) is the power you raise 2 to in order to get x, so log2(8) = 3. The program computes entropy for the logits of Example 1 at several temperatures. The last six lines show each token's term of the sum at T = 1.0.
public class Entropy {
static double[] softmax(double[] logits, double temperature) {
double max = Double.NEGATIVE_INFINITY, sum = 0;
for (double l : logits) max = Math.max(max, l / temperature);
double[] p = new double[logits.length];
for (int i = 0; i < p.length; i++) { p[i] = Math.exp(logits[i] / temperature - max); sum += p[i]; }
for (int i = 0; i < p.length; i++) p[i] /= sum;
return p;
}
static double entropyBits(double[] p) {
double h = 0;
for (double v : p) if (v > 0) h -= v * Math.log(v) / Math.log(2);
return h;
}
public static void main(String[] args) {
java.util.Locale.setDefault(java.util.Locale.ROOT);
double[] logits = {2.0, 1.0, 0.5, 0.1, -0.5, -1.0};
for (double t : new double[] {0.05, 0.2, 0.7, 1.0, 1.5, 5.0, 100.0})
System.out.printf("T = %-6s entropy %.3f bits%n", t, entropyBits(softmax(logits, t)));
System.out.printf("maximum for 6 tokens: %.3f bits%n", Math.log(6) / Math.log(2));
String[] words = {"the", "cat", "sat", "mat", "dog", "ran"};
double[] p = softmax(logits, 1.0);
for (int i = 0; i < p.length; i++) // the terms of the sum at T = 1.0
System.out.printf(" %-4s p %.3f -p*log2(p) = %.3f%n", words[i], p[i], -p[i] * Math.log(p[i]) / Math.log(2));
}
}T = 0.05 entropy 0.000 bits
T = 0.2 entropy 0.066 bits
T = 0.7 entropy 1.470 bits
T = 1.0 entropy 1.938 bits
T = 1.5 entropy 2.273 bits
T = 5.0 entropy 2.556 bits
T = 100.0 entropy 2.585 bits
maximum for 6 tokens: 2.585 bits
the p 0.534 -p*log2(p) = 0.483
cat p 0.196 -p*log2(p) = 0.461
sat p 0.119 -p*log2(p) = 0.366
mat p 0.080 -p*log2(p) = 0.291
dog p 0.044 -p*log2(p) = 0.198
ran p 0.027 -p*log2(p) = 0.139Start with the last six lines, which compute the formula by hand at T = 1.0. Each token contributes -p * log2(p). The token the with probability 0.534 contributes 0.483 bits, and ran with 0.027 contributes 0.139. The six terms add up to 1.938 bits, the value in the T = 1.0 row. A token with probability near 0 or near 1 contributes almost nothing, because either p or log2(p) is near 0.
Now read the table from top to bottom. At T = 0.05 the entropy is 0.000 bits: the choice is certain. At T = 1.0 it is 1.938 bits, about as unsure as a choice among 4 equally likely tokens (2^1.938 = 3.8). As T grows the entropy approaches the maximum of 2.585 bits for six tokens, which is log2(6). At T = 100 it equals that maximum to three decimals.
Entropy returns in the mirostat sampler, which steers it toward a target. It is also related to the perplexity of lesson 5, which describes how surprised a model is by real text.
Example 8: top-k
Temperature reshapes the whole distribution, but it never removes any token. On a real vocabulary of 128,000 tokens, most entries are nonsense in the current context, and each has a tiny probability. Added together, those tiny probabilities can still be large enough that a sampler picks one of them now and then.
Top-k is the first filter that fixes this. It keeps the k most probable tokens and sets the others to 0. Then it renormalizes, which means dividing each kept probability by the kept total so the remaining probabilities add up to 1 again.
The program prints the original probabilities next to the results for k = 3 and k = 1.
public class TopK {
static double[] softmax(double[] logits) {
double max = Double.NEGATIVE_INFINITY, sum = 0;
for (double l : logits) max = Math.max(max, l);
double[] p = new double[logits.length];
for (int i = 0; i < p.length; i++) { p[i] = Math.exp(logits[i] - max); sum += p[i]; }
for (int i = 0; i < p.length; i++) p[i] /= sum;
return p;
}
// keep the k most probable tokens, set the others to 0, and renormalize
static double[] topK(double[] p, int k) {
Integer[] order = new Integer[p.length];
for (int i = 0; i < p.length; i++) order[i] = i;
java.util.Arrays.sort(order, (a, b) -> Double.compare(p[b], p[a]));
double[] out = new double[p.length];
double sum = 0;
for (int r = 0; r < Math.min(k, p.length); r++) { out[order[r]] = p[order[r]]; sum += p[order[r]]; }
for (int i = 0; i < out.length; i++) out[i] /= sum;
return out;
}
public static void main(String[] args) {
java.util.Locale.setDefault(java.util.Locale.ROOT);
String[] words = {"the", "cat", "sat", "mat", "dog", "ran"};
double[] p = softmax(new double[] {2.0, 1.0, 0.5, 0.1, -0.5, -1.0});
double[] k3 = topK(p, 3), k1 = topK(p, 1);
System.out.println("token before top-k 3 top-k 1");
for (int i = 0; i < words.length; i++)
System.out.printf("%-5s %.3f %.3f %.3f%n", words[i], p[i], k3[i], k1[i]);
System.out.println("top-k 0 or less keeps everything in llama.cpp; top-k 1 is greedy");
}
}token before top-k 3 top-k 1
the 0.534 0.629 1.000
cat 0.196 0.231 0.000
sat 0.119 0.140 0.000
mat 0.080 0.000 0.000
dog 0.044 0.000 0.000
ran 0.027 0.000 0.000
top-k 0 or less keeps everything in llama.cpp; top-k 1 is greedyWith k = 3, the survivors the, cat, and sat have 0.534, 0.196, and 0.119, which add up to 0.849. Dividing each by 0.849 gives 0.629, 0.231, and 0.140. The ratios between the survivors are unchanged. Only the total is repaired. With k = 1 only the remains, which is greedy.
In llama.cpp, --top-k defaults to 40 and a value of 0 disables the filter. The weakness of a fixed k is that it ignores the shape of the distribution. With k = 40, a confident step with one clear answer still keeps 39 other candidates. A flat step with 100 plausible words loses 60 of them. Top-p, in the next example, addresses this.
Example 9: top-p (nucleus sampling): sort, accumulate, cut
Top-p adapts the number of survivors to the distribution. It is also called nucleus sampling, because the kept set is the core of the distribution. It works in three steps:
- Sort the tokens by probability, largest first.
- Add the probabilities up from the top, keeping a running sum.
- Stop when the running sum reaches
p, and keep every token added so far.
The token that crosses the limit is included. This is also the rule in llama.cpp (cum_sum >= p). The kept set is renormalized, as in top-k.
The program prints each step for p = 0.9 on the logits of Example 1. The tokens are deliberately listed out of order, so the sort is visible: look at which token is added first.
public class TopPSteps {
static double[] softmax(double[] logits) {
double max = Double.NEGATIVE_INFINITY, sum = 0;
for (double l : logits) max = Math.max(max, l);
double[] p = new double[logits.length];
for (int i = 0; i < p.length; i++) { p[i] = Math.exp(logits[i] - max); sum += p[i]; }
for (int i = 0; i < p.length; i++) p[i] /= sum;
return p;
}
public static void main(String[] args) {
java.util.Locale.setDefault(java.util.Locale.ROOT);
String[] words = {"mat", "the", "ran", "sat", "dog", "cat"};
double[] p = softmax(new double[] {0.1, 2.0, -1.0, 0.5, -0.5, 1.0}); // Example 1's logits, shuffled
Integer[] order = new Integer[p.length];
for (int i = 0; i < p.length; i++) order[i] = i;
java.util.Arrays.sort(order, (a, b) -> Double.compare(p[b], p[a])); // 1. sort by probability
double run = 0, limit = 0.9;
int kept = 0;
for (int id : order) { // 2. accumulate
run += p[id];
kept++;
System.out.printf("add %-4s p %.3f running sum %.3f%s%n", words[id], p[id], run, run >= limit ? " >= 0.9: cut here" : "");
if (run >= limit) break; // 3. cut after the crossing token
}
System.out.printf("kept %d of %d tokens; renormalized:", kept, p.length);
for (int r = 0; r < kept; r++) System.out.printf(" %s %.3f", words[order[r]], p[order[r]] / run);
System.out.println();
}
}add the p 0.534 running sum 0.534
add cat p 0.196 running sum 0.731
add sat p 0.119 running sum 0.850
add mat p 0.080 running sum 0.930 >= 0.9: cut here
kept 4 of 6 tokens; renormalized: the 0.575 cat 0.211 sat 0.128 mat 0.086The sort puts the first even though it is second in the input. The running sums are 0.534, 0.731, 0.850, and 0.930. The sum first reaches 0.9 at mat, so mat is kept and the cut falls after it.
The kept probabilities add up to 0.930, so dividing each by 0.930 makes them sum to 1 again. For example, the rises from 0.534 to 0.575. The two dropped tokens, dog and ran, held 0.070 of the probability, which is the amount that was redistributed.
Sorting is the expensive part on a real vocabulary of 150,000 tokens. The llama.cpp implementation avoids a full sort when it can. For more than 1,024 candidates it first sorts only the top 256, and it sorts the rest only if the running sum has not reached p by then.
Example 10: top-p on a confident and a flat distribution
The point of top-p is that the number of kept tokens changes with the model's confidence. This example runs top-p with p = 0.9 on three distributions:
- the one from Example 1, called medium;
- a confident one, with logits 5, 2, 1, 0, -1, -2;
- a flat one, with logits from 1.0 down to 0.5 in steps of 0.1.
For each, the program prints how many tokens survive and how much probability they cover.
import java.util.ArrayList;
import java.util.List;
public class TopP {
static double[] softmax(double[] logits) {
double max = Double.NEGATIVE_INFINITY, sum = 0;
for (double l : logits) max = Math.max(max, l);
double[] p = new double[logits.length];
for (int i = 0; i < p.length; i++) { p[i] = Math.exp(logits[i] - max); sum += p[i]; }
for (int i = 0; i < p.length; i++) p[i] /= sum;
return p;
}
// sort by probability, accumulate, and cut after the token whose running sum first reaches p
static List<Integer> nucleus(double[] probs, double p) {
Integer[] order = new Integer[probs.length];
for (int i = 0; i < probs.length; i++) order[i] = i;
java.util.Arrays.sort(order, (a, b) -> Double.compare(probs[b], probs[a]));
List<Integer> kept = new ArrayList<>();
double run = 0;
for (int id : order) {
kept.add(id);
run += probs[id];
if (run >= p) break;
}
return kept;
}
public static void main(String[] args) {
java.util.Locale.setDefault(java.util.Locale.ROOT);
String[] words = {"the", "cat", "sat", "mat", "dog", "ran"};
double[][] logits = {{2.0, 1.0, 0.5, 0.1, -0.5, -1.0}, {5.0, 2.0, 1.0, 0.0, -1.0, -2.0}, {1.0, 0.9, 0.8, 0.7, 0.6, 0.5}};
String[] names = {"medium", "confident", "flat"};
for (int d = 0; d < logits.length; d++) {
double[] probs = softmax(logits[d]);
List<Integer> kept = nucleus(probs, 0.9);
double mass = 0;
StringBuilder sb = new StringBuilder();
for (int id : kept) { mass += probs[id]; sb.append(words[id]).append(String.format(" %.3f, ", probs[id])); }
System.out.printf("%-9s top-p 0.9 keeps %d: %scovering %.3f%n", names[d], kept.size(), sb, mass);
}
}
}medium top-p 0.9 keeps 4: the 0.534, cat 0.196, sat 0.119, mat 0.080, covering 0.930
confident top-p 0.9 keeps 1: the 0.927, covering 0.927
flat top-p 0.9 keeps 6: the 0.211, cat 0.191, sat 0.173, mat 0.156, dog 0.141, ran 0.128, covering 1.000The confident distribution keeps one token, because the alone has 0.927, which is above 0.9. The medium one keeps four tokens, covering 0.930, as in Example 9. The flat one keeps all six, since each token holds around one sixth and the running sum only passes 0.9 at the last token.
The size of the kept set follows the model's confidence, which is the advantage over top-k. With a fixed k = 3, the confident step would keep two tokens it almost never needs, and the flat step would lose three plausible ones.
A value of 1.0 disables the filter (llama.cpp returns at once when p >= 1). llama.cpp defaults to 0.95.
Example 11: min-p
Top-p has a weakness: to reach its target sum it may have to include a long tail of unlikely tokens, because many small probabilities add up. Min-p uses a different test. It keeps every token whose probability is at least min_p times the probability of the top token.
An example with real numbers: if the top token has probability 0.60 and min_p is 0.1, the threshold is 0.06. A token with 0.07 stays and a token with 0.05 is cut. The threshold scales with the model's confidence. A confident head sets a high bar, and a flat head sets a low one.
In logit terms the rule is logit >= max_logit + ln(min_p), which is how llama.cpp computes it. This is the same test, because taking the logarithm of "probability at least min_p times the top probability" gives that expression.
The program compares min-p with top-p on three distributions, including one with a strong head and a long tail of 10 tokens at 0.015 each. The output shows how many tokens each filter keeps.
public class MinP {
static double[] softmax(double[] logits) {
double max = Double.NEGATIVE_INFINITY, sum = 0;
for (double l : logits) max = Math.max(max, l);
double[] p = new double[logits.length];
for (int i = 0; i < p.length; i++) { p[i] = Math.exp(logits[i] - max); sum += p[i]; }
for (int i = 0; i < p.length; i++) p[i] /= sum;
return p;
}
static int topP(double[] p, double limit) { // how many tokens top-p keeps
double[] sorted = p.clone();
java.util.Arrays.sort(sorted);
double run = 0;
for (int i = sorted.length - 1, n = 1; i >= 0; i--, n++) { run += sorted[i]; if (run >= limit) return n; }
return p.length;
}
static int minP(double[] p, double minP) { // keep tokens with p >= minP * p_max
double max = 0;
for (double v : p) max = Math.max(max, v);
int kept = 0;
for (double v : p) if (v >= minP * max) kept++;
return kept;
}
public static void main(String[] args) {
double[] tail = {0.30, 0.25, 0.20, 0.10, 0.015, 0.015, 0.015, 0.015, 0.015, 0.015, 0.015, 0.015, 0.015, 0.015};
double[][] dists = {softmax(new double[] {5.0, 2.0, 1.0, 0.0, -1.0, -2.0}), softmax(new double[] {1.0, 0.9, 0.8, 0.7, 0.6, 0.5}), tail};
String[] names = {"confident (6 tokens)", "flat (6 tokens)", "head and tail (14)"};
for (int d = 0; d < dists.length; d++)
System.out.printf("%-22s top-p 0.95 keeps %2d, min-p 0.1 keeps %2d%n", names[d], topP(dists[d], 0.95), minP(dists[d], 0.1));
}
}confident (6 tokens) top-p 0.95 keeps 2, min-p 0.1 keeps 1
flat (6 tokens) top-p 0.95 keeps 6, min-p 0.1 keeps 6
head and tail (14) top-p 0.95 keeps 11, min-p 0.1 keeps 4On the confident distribution the min-p threshold is 0.1 * 0.927 = 0.0927, and the second token has only 0.046, so one token stays. Top-p 0.95 keeps two tokens there, because the alone (0.927) is below 0.95 and the second token brings the sum to 0.973.
On the flat distribution every token is above 0.1 * 0.211, and top-p needs all six to reach 0.95, so both keep six. On these two distributions the filters agree to within one token.
On the head-and-tail distribution they differ. Top-p with p = 0.95 keeps 11 tokens, because the four head tokens only cover 0.85 and 7 tail tokens are needed to reach 0.95. Min-p with 0.1 has a threshold of 0.03 (0.1 times 0.30), so the tail tokens at 0.015 are cut and 4 remain.
The lesson is that a long tail of unlikely tokens can add up to a large share of the probability. Top-p must include part of the tail to reach its target. Min-p ignores it. llama.cpp defaults min_p to 0.05, and --min-p 0 disables it.
Example 12: the order of the filters matters
Real engines apply several of these steps one after another, and each step sees the output of the previous one. This raises a question: does the order change the result? It does. Temperature and top-p do not commute, which means swapping them gives a different outcome.
The program applies temperature 1.5 and top-p 0.9 in both orders to the logits of Example 1. Compare which tokens survive in each line.
import java.util.ArrayList;
import java.util.List;
public class Order {
static double[] softmax(double[] logits) {
double max = Double.NEGATIVE_INFINITY, sum = 0;
for (double l : logits) max = Math.max(max, l);
double[] p = new double[logits.length];
for (int i = 0; i < p.length; i++) { p[i] = Math.exp(logits[i] - max); sum += p[i]; }
for (int i = 0; i < p.length; i++) p[i] /= sum;
return p;
}
static double[] temperature(double[] logits, double t) {
double[] out = new double[logits.length];
for (int i = 0; i < out.length; i++) out[i] = logits[i] / t;
return out;
}
// top-p on logits: kept tokens keep their logits, others become -infinity
static double[] topP(double[] logits, double limit) {
double[] p = softmax(logits);
Integer[] order = new Integer[p.length];
for (int i = 0; i < p.length; i++) order[i] = i;
java.util.Arrays.sort(order, (a, b) -> Double.compare(p[b], p[a]));
double[] out = new double[p.length];
java.util.Arrays.fill(out, Double.NEGATIVE_INFINITY);
double run = 0;
for (int id : order) { out[id] = logits[id]; run += p[id]; if (run >= limit) break; }
return out;
}
static String show(double[] logits) {
double[] p = softmax(logits);
StringBuilder sb = new StringBuilder();
String[] words = {"the", "cat", "sat", "mat", "dog", "ran"};
for (int i = 0; i < p.length; i++) if (p[i] > 0) sb.append(String.format("%s %.3f ", words[i], p[i]));
return sb.toString().trim();
}
public static void main(String[] args) {
java.util.Locale.setDefault(java.util.Locale.ROOT);
double[] logits = {2.0, 1.0, 0.5, 0.1, -0.5, -1.0};
System.out.println("temperature 1.5 first, then top-p 0.9: " + show(topP(temperature(logits, 1.5), 0.9)));
System.out.println("top-p 0.9 first, then temperature 1.5: " + show(temperature(topP(logits, 0.9), 1.5)));
}
}temperature 1.5 first, then top-p 0.9: the 0.425 cat 0.218 sat 0.156 mat 0.120 dog 0.080
top-p 0.9 first, then temperature 1.5: the 0.462 cat 0.237 sat 0.170 mat 0.130With temperature first, the distribution is the T=1.5 column of Example 5: 0.402, 0.206, 0.148, 0.113, 0.076. The first four add up to 0.869, below 0.9, so dog is needed and the sum reaches 0.945. The flatter distribution needs five tokens.
With top-p first, the cut happens on the original distribution and leaves four tokens, and temperature then reshapes only those four. The same two settings give different token sets, because raising the temperature first spreads probability into the tail, and top-p then has to keep more tokens to reach 0.9.
llama.cpp applies the filters first and temperature near the end. Its default order, from common/common.h, is penalties, DRY, top-n-sigma, top-k, typical, top-p, min-p, XTC, temperature, followed by the final dist sampler that draws the token. Penalties, DRY, top-n-sigma, typical, and XTC are covered in the next lesson, and all of them are off by default.
The tool llama-completion prints the chain it built. The commands below show it. Two details help in reading the output:
- A
?in front of a name means the sampler is disabled by its parameter value (for exampletop-kwith 0), and llama.cpp puts an empty placeholder in its place. temp-extis the temperature sampler with an optional dynamic-temperature range. The range is 0 (off) by default, so it divides the logits by the temperature as in Example 5.
The commands use the tiny random-weight model, because the log lines do not depend on the weights. The second command uses --samplers to replace the whole chain with top-k and temperature only.
llama-completion -m tiny-random-qwen2vocab-f32.gguf -p "Hi" -n 2 -no-cnv --seed 7 2>&1 | grep "sampler chain"
llama-completion -m tiny-random-qwen2vocab-f32.gguf -p "Hi" -n 2 -no-cnv --seed 7 --samplers "top_k;temperature" 2>&1 | grep "sampler chain"Representative output (from the pinned llama.cpp build; timestamps removed):
sampler chain: logits -> ?penalties -> ?dry -> ?top-n-sigma -> top-k -> ?typical -> top-p -> min-p -> ?xtc -> temp-ext -> dist
sampler chain: logits -> top-k -> temp-ext -> distWith defaults, the enabled samplers are top-k (40), top-p (0.95), min-p (0.05), temp-ext (0.80), and dist. The --samplers flag replaces the whole list and its order, which is why the second line has only top-k and temp-ext. Because temperature comes after the truncation filters, a higher temperature spreads probability over the tokens that survived and does not let removed tokens return.
Example 13: seeds, and why identical settings can still differ
A seed fixes the sequence of random numbers a sampler uses. Two samplers with the same seed and the same probabilities pick the same tokens. This is useful for tests and for reproducing a result.
The program shows three things. First, the same seed gives the same twelve draws twice. Second, a different seed gives a different sequence. Third, it shows what happens when the probabilities change slightly, which can happen when a computation runs in a different order. It changes two logits by 0.003 and counts how many of 10,000 draws change.
import java.util.Random;
public class Seeds {
static double[] softmax(double[] logits) {
double max = Double.NEGATIVE_INFINITY, sum = 0;
for (double l : logits) max = Math.max(max, l);
double[] p = new double[logits.length];
for (int i = 0; i < p.length; i++) { p[i] = Math.exp(logits[i] - max); sum += p[i]; }
for (int i = 0; i < p.length; i++) p[i] /= sum;
return p;
}
static int sample(double[] p, double u) {
double run = 0;
for (int i = 0; i < p.length; i++) { run += p[i]; if (u < run) return i; }
return p.length - 1;
}
static int[] draw(double[] logits, long seed, int n) {
Random rng = new Random(seed);
double[] p = softmax(logits);
int[] out = new int[n];
for (int i = 0; i < n; i++) out[i] = sample(p, rng.nextDouble());
return out;
}
public static void main(String[] args) {
double[] logits = {2.0, 1.0, 0.5, 0.1, -0.5, -1.0};
System.out.println("seed 7, run 1: " + java.util.Arrays.toString(draw(logits, 7, 12)));
System.out.println("seed 7, run 2: " + java.util.Arrays.toString(draw(logits, 7, 12)));
System.out.println("seed 8: " + java.util.Arrays.toString(draw(logits, 8, 12)));
// the same seed, but the logits differ in the third decimal, as can happen between kernels or batch shapes
double[] shifted = logits.clone();
shifted[0] += 0.003;
shifted[2] -= 0.003;
int[] a = draw(logits, 7, 10_000), b = draw(shifted, 7, 10_000);
int different = 0;
for (int i = 0; i < a.length; i++) if (a[i] != b[i]) different++;
System.out.println("of 10000 draws with a 0.003 change in two logits, " + different + " pick a different token");
}
}seed 7, run 1: [2, 2, 0, 3, 1, 0, 0, 3, 0, 3, 1, 2]
seed 7, run 2: [2, 2, 0, 3, 1, 0, 0, 3, 0, 3, 1, 2]
seed 8: [1, 1, 0, 2, 1, 1, 0, 1, 0, 3, 0, 3]
of 10000 draws with a 0.003 change in two logits, 24 pick a different tokenThe first two lines are identical, and a different seed gives a different sequence. In the last test two logits move by 0.003, a size that rounding differences between kernels (the GPU or CPU routines that do the math) or batch shapes can produce in half precision. Of 10,000 draws, 24 pick a different token. These are the draws whose random number falls close to an interval boundary, so the small change in the interval moves it to the neighbor.
One different token is enough to change everything after it. The context is now different, so the model produces different logits for every later step, and the rest of a generated text diverges.
A seed therefore gives repeatable output only when everything before the sampler is also identical: the same model file, build, hardware, thread count, and batch composition. Floating-point sums depend on the order of addition, which an earlier lesson on numeric precision covered, and servers change that order when requests share a batch. llama.cpp documents --seed as the RNG (random number generator) seed, with a default of -1, which means a random seed. The dist sampler draws from a std::mt19937 generator that the seed initializes.
Example 14: text from the bigram model under each setting
The earlier examples used six tokens. This example shows the settings on real generated text. The program uses the character bigram model of lesson 5: a table that counts, for each character in a small corpus, which characters follow it. The row for the current character is the token distribution for the next step.
The program generates 60 characters with seven settings, all with the same seed. The settings apply truncation first and temperature last, as in llama.cpp. After the text, the program prints the row of counts for the character after a space, and the characters that top-k 3, top-p 0.9, and min-p 0.2 would allow there. Compare the seven lines of text first, then read the last four lines.
import java.util.Map;
import java.util.Random;
import java.util.TreeMap;
public class BigramText {
static final String CORPUS = "the cat sat on the mat. the dog sat on the log. "
+ "the cat saw the dog. the dog saw the cat. a cat and a dog sat on a mat.";
// the token at sorted rank r survives top-k, min-p, and top-p (run = probability of the tokens ranked above it)
static boolean keep(int r, double prob, double run, double max, int topK, double topP, double minP) {
return (topK <= 0 || r < topK) && prob >= minP * max && (r == 0 || run < topP);
}
// settings: temperature, top-k (0 = off), top-p (1 = off), min-p (0 = off); temperature 0 means greedy
static char next(Map<Character, Integer> row, double temp, int topK, double topP, double minP, Random rng) {
int n = row.size();
char[] chars = new char[n];
double[] p = new double[n];
int i = 0;
for (Map.Entry<Character, Integer> e : row.entrySet()) { chars[i] = e.getKey(); p[i++] = e.getValue(); }
Integer[] order = new Integer[n];
for (int j = 0; j < n; j++) order[j] = j;
java.util.Arrays.sort(order, (a, b) -> Double.compare(p[b], p[a]));
if (temp <= 0) return chars[order[0]];
double total = 0;
for (double v : p) total += v;
double[] q = new double[n]; // truncation first, then temperature last (llama.cpp order)
double run = 0, max = p[order[0]] / total;
for (int r = 0; r < n; r++) {
int id = order[r];
double prob = p[id] / total;
if (keep(r, prob, run, max, topK, topP, minP)) q[id] = Math.pow(prob, 1 / temp); // dividing the log by T equals this power
run += prob;
}
double sum = 0;
for (double v : q) sum += v;
double u = rng.nextDouble() * sum;
for (int j = 0; j < n; j++) { u -= q[j]; if (q[j] > 0 && u < 0) return chars[j]; }
return chars[order[0]];
}
public static void main(String[] args) {
Map<Character, Map<Character, Integer>> table = new TreeMap<>();
for (int i = 0; i + 1 < CORPUS.length(); i++)
table.computeIfAbsent(CORPUS.charAt(i), k -> new TreeMap<>()).merge(CORPUS.charAt(i + 1), 1, Integer::sum);
String[] names = {"greedy", "T=0.7", "T=1.0", "T=1.5", "top-k 3", "top-p 0.9", "min-p 0.2"};
double[][] settings = {{0, 0, 1, 0}, {0.7, 0, 1, 0}, {1.0, 0, 1, 0}, {1.5, 0, 1, 0}, {1.0, 3, 1, 0}, {1.0, 0, 0.9, 0}, {1.0, 0, 1, 0.2}};
for (int s = 0; s < settings.length; s++) {
Random rng = new Random(5);
StringBuilder out = new StringBuilder("t");
char c = 't';
for (int i = 0; i < 59; i++) {
c = next(table.get(c), settings[s][0], (int) settings[s][1], settings[s][2], settings[s][3], rng);
out.append(c);
}
System.out.printf("%-10s %s%n", names[s], out);
}
Map<Character, Integer> space = table.get(' '); // the row after a space
System.out.println("row after ' ': " + space);
Character[] sorted = space.keySet().toArray(new Character[0]);
java.util.Arrays.sort(sorted, (x, y) -> space.get(y) - space.get(x));
for (int s = 4; s < settings.length; s++) {
StringBuilder kept = new StringBuilder();
double total = 0;
for (int count : space.values()) total += count;
int above = 0; // count of the characters ranked above
for (int r = 0; r < sorted.length; r++) {
double prob = space.get(sorted[r]) / total, max = space.get(sorted[0]) / total;
if (keep(r, prob, above / total, max, (int) settings[s][1], settings[s][2], settings[s][3])) kept.append(sorted[r]);
above += space.get(sorted[r]);
}
System.out.printf("%-10s can pick: %s%n", names[s], kept);
}
}
}greedy the the the the the the the the the the the the the the the
T=0.7 the og. t the maw dog a sat athe ca sathe sat the cathe sat
T=1.0 the mat saw cat. t cat at. d dog ca sathe sat the ca t the a
T=1.5 the mat saw cat. t cat at. d dog ca saw sathe t. t a t the a
top-k 3 the sat t the saw sat. at. sathe at the t the the a aw the a
top-p 0.9 the mat t the maw dog a sat athe ca sathe sat the ca t the a
min-p 0.2 the mat t the maw dog a sat athe ca sathe sat the ca t the a
row after ' ': {a=4, c=4, d=4, l=1, m=2, o=3, s=5, t=7}
top-k 3 can pick: tsa
top-p 0.9 can pick: tsacdo
min-p 0.2 can pick: tsacdomGreedy loops on the because each step picks the most frequent continuation: after t comes h, after h comes e, after e comes a space, and after a space comes t again. The four characters form a cycle that greedy decoding never leaves.
The sampled lines start alike because every setting uses the same seed, so the same random numbers are drawn, and the distributions are similar. A line changes where a draw falls in a region that the setting changed. T=0.7 leaves the other lines at the fifth character (og. instead of mat): the sharper distribution moves the boundaries between intervals, so the same u selects a different character. T=1.0 and T=1.5 agree for the first 38 characters and then diverge for the same reason.
The last four lines show one row of the table, the 30 characters that follow a space in the corpus, and the characters each filter leaves available:
- Top-k 3 keeps only
t,s, anda. The tie betweena,c, anddat 4 counts is broken by sort order. - Top-p 0.9 keeps six characters. The running sum reaches exactly 27/30 = 0.9 at
o, somandlare cut. - Min-p 0.2 has a threshold of
0.2 * 7/30 = 0.047, so it keepsm(2/30 = 0.067) and cuts onlyl(1/30 = 0.033).
Top-p and min-p remove nearly the same characters, which is why their lines are identical here.
The bigram model is too small to show quality differences, because each row has only a handful of options. A large model shows the effect of each setting more clearly. The criteria for choosing values are in Penalties, Logit Bias, and Sampler Chains.
Interview points
- Logits are unnormalized scores, one per vocabulary entry. Softmax turns them into probabilities, and only the differences between logits matter.
- Greedy decoding always takes the highest logit, so it is deterministic and often repeats itself. Sampling draws from the distribution with a random number and the inverse-CDF walk.
- Temperature divides the logits. Below 1 it sharpens the distribution, above 1 it flattens it, and it never changes the order of the tokens.
T <= 0is handled as greedy because the division is undefined. - Entropy measures randomness in bits: 0 for a certain choice, and
log2(n)for a uniform choice overntokens. - Top-k keeps a fixed number of tokens. Top-p keeps the smallest set whose probability reaches
p. Min-p keeps tokens above a fraction of the top probability. - Min-p scales its threshold with the model's confidence. It avoids including a long tail, which top-p can do when the head is flat.
- Filters do not commute, so the order changes the result. llama.cpp truncates first and applies temperature last by default.
- llama.cpp defaults are temperature 0.80, top-k 40, top-p 0.95, and min-p 0.05, with the other samplers disabled.
- A seed makes the random numbers repeatable. It does not make results portable across hardware, builds, or batch compositions.
- The
sampler chainlog line shows the active samplers, and a?marks a disabled one.
Summary
- A sampler maps a vector of logits to one token id.
- Softmax is
p_i = exp(l_i - max) / sum_j exp(l_j - max). Adding a constant to all logits changes nothing. - Greedy picks the first largest logit. Sampling walks the cumulative probabilities until the running total passes a random
uin [0, 1). - Temperature divides logits by
T. AtT <= 0llama.cpp keeps only the top logit. - Entropy
-sum(p log2 p)is 0 for a certain choice and at mostlog2(n). - Top-k keeps
ktokens and renormalizes. In llama.cpp, 0 disables it and 40 is the default. - Top-p keeps tokens until the cumulative probability reaches
p, including the token that crosses the limit. The default is 0.95. - Min-p keeps tokens with
p_i >= min_p * p_max, computed aslogit >= max_logit + ln(min_p). The default is 0.05. - Applying temperature before or after truncation gives different token sets. llama.cpp orders the chain penalties, DRY, top-n-sigma, top-k, typical, top-p, min-p, XTC, temperature, dist.
- A seeded sampler repeats exactly if the logits are bit-identical. Small numeric differences change a few draws, and then the rest of the text.
--samplerssets the sampler list and order forllama-completion.
