AI is scary. It is a modern industrial revolution, and it is rapidly devaluing entire classes of intellectual labor. We are right to be nervous about what that means.

AI can also be wrong. It can misunderstand a question, invent a fact, or generate code with a bug buried somewhere unpleasant.

But one common attack on AI confuses two separate things: error and randomness.

Ask the same model the same question twice, and it may give you two different answers. That does not, by itself, make either answer wrong. Humans do exactly the same thing. Ask two programmers to solve a problem and you will get remarkably different code. Ask the same programmer again a few weeks later and you may get a third solution.

Different is not the same as wrong.

More importantly, randomness is already a hidden part of many systems we use and trust every day: hash tables, Redis sorted sets, probabilistic primality tests, some sorting algorithms, network collision avoidance, simulations, cryptography, and countless optimization algorithms.

Modern computing is stuffed with deliberate randomness.

Randomness Has to Come From Somewhere

I am going to start with a mildly mind-blowing fact:

Randomness does not naturally exist inside a deterministic computer.

Given the same complete internal state and the same inputs, an ordinary digital computer will produce the same result every time. It cannot manufacture unpredictability from nothing.

What it can do is generate a sequence that looks random.

When I first learned to program, it was on a TI-99/4A. I would play games and notice that the characters often moved the same way every time. Enemies followed the same patterns from one game to the next.

Friends would give me games they had written in TI BASIC. Their games either had the same flaw or began by asking me to type in a random number.

The human being was the entropy peripheral.

Later, as an undergraduate, I learned generators resembling this tiny linear congruential generator:

#include <stdint.h>

uint8_t random_byte(void)
{
    static uint8_t x = 1;
    x = x * 5 + 1;
    return x;
}

Because x is an eight-bit unsigned integer, arithmetic wraps around at 256. The generator produces a random-looking sequence:

1 → 6 → 31 → 156 → 13 → 66 → 75 → 120 → 89 → ...

In fact, this particular recurrence visits all 256 possible byte values before repeating.

I asked my instructor, “What if you want a different sequence?”

He said, “Set the static variable to a different value.”

“But what about a different sequence every time?”

And that is the problem.

The initial value is called the seed. Given the same seed, a pseudorandom number generator produces the same sequence every time. With this tiny generator, changing the seed merely chooses a different starting point in the same 256-value cycle.

That is why games on my TI-99/4A sometimes asked me for a number. The game used my answer as its seed.

Type the same number twice and you get the same game twice.

Borrowing Randomness From the Physical World

Computers are deterministic, but computers live in a messy physical world.

Early PCs found convenient ways to import that mess.

The original IBM PC and PC/XT did not have the battery-backed real-time clock that later became standard. DOS systems often asked the user to enter the date and time during startup. Meanwhile, the BIOS maintained a timer that advanced at roughly 18.2 ticks per second. A game could read that counter when it started and use it as a seed.

Think about everything that happened before the game read it:

You turned on the machine. You waited for it to boot. You found your DOS disk. You swapped floppies. You typed the command. Perhaps you misspelled it and tried again.

Small differences in human timing produced different low-order timer bits. That was not cryptographically secure randomness, but it was more than adequate for deciding when an alien should fly across the screen.

Once battery-backed clocks became common with the PC/AT generation, the current time became an even easier source for game seeds.

Unix and Linux had a harder problem. Cryptography needs randomness that an attacker cannot guess merely by knowing when the program started.

The Linux kernel collected environmental noise from device activity and interrupt timing: keyboard events, disk operations, network activity, and other hardware events. It mixed that information into an entropy pool and exposed random data through interfaces such as /dev/random and /dev/urandom.

Historically, /dev/random could block when the kernel believed it lacked sufficient entropy, which made it feel painfully slow on quiet systems.

I remember reading a wonderfully Slashdot-era account of someone pointing a webcam at a ficus tree while a table fan blew its leaves around. The changing pixels were converted into entropy and fed into the system’s random-number pool.

The ficus was doing cryptographic labor.

Hardware has become less horticultural since then.

Many modern x86 processors provide RDRAND and RDSEED. The first returns values from a hardware random-bit generator; the second is intended to provide seed-quality entropy for another generator.

Arm’s optional random-number extension provides the analogous RNDR and RNDRRS operations.

For people who need an independently validated fire hose of physical entropy, PCIe quantum random-number-generator cards also exist. Examples include the Quside Garnet, Qrypt Atlas QRNG, and QuintessenceLabs qStream.

But most software does not consume physical entropy for every random number it needs.

Instead, it uses physical entropy once to select a seed and then lets a fast deterministic generator expand that seed into billions of pseudorandom values:

physical entropy → seed → pseudorandom generator → lots of values

So when software behaves randomly, the bulk of that behavior is usually deterministic. The seed chooses which deterministic sequence you receive.

Hash Functions: Deterministic Chaos

Hash functions are not quite random-number generators, although they often share an important property: their outputs should be spread broadly and irregularly across the available output space.

We can abuse a fast hash function to demonstrate the idea:

import xxhash

x = xxhash.xxh3_64(b"").digest()

for _ in range(10):
    x = xxhash.xxh3_64(x).digest()
    print(int.from_bytes(x, "little") / 2**64)

On my machine, this produces:

0.043041244156103366
0.1970826139989619
0.03510320452129987
0.713691224496282
0.2837170914417562
0.101618905717078
0.7344280662414208
0.08816604967489124
0.47792870613605337
0.5724158319777755

Run the entire program again and you get exactly the same sequence.

Change:

x = xxhash.xxh3_64(b"").digest()

to:

x = xxhash.xxh3_64(b"my seed").digest()

and you get a different—but still completely deterministic—sequence.

Do not use xxHash this way for cryptographic secrets. It is a noncryptographic hash, and this is an illustration rather than a recommendation.

Randomness Inside Hash Tables

Hash tables use a hash function to decide where an object belongs inside their internal storage.

Old versions of Python made that internal layout particularly visible. Python 2.7 dictionaries did not preserve insertion order. Iterating over one exposed an arbitrary-looking order created by the table’s internal hash placement.

For example, on my machine:

>>> d = {}
>>> for word in "Alfa,Bravo,Charlie,Delta,Echo,Foxtrot,Golf,Hotel,India,Juliett".split(","):
...     d[word] = word
...
>>> print list(d.keys())
['Alfa', 'Bravo', 'Hotel', 'India', 'Echo', 'Delta',
 'Juliett', 'Golf', 'Charlie', 'Foxtrot']

That order was not alphabetical, and it was not the order in which I inserted the words. It fell out of their placement in the hash table.

Modern Python adds another layer: hashes of strings and bytes are salted using a seed selected when the interpreter starts. This helps defend against attackers deliberately constructing large numbers of hash collisions.

Python 3.7 also made dictionary insertion order part of the language specification, so ordinary dictionary iteration now hides the internal hash-table order. Sets still make the effect easier to observe.

You can control Python’s hash seed explicitly:

PYTHONHASHSEED=1 python3 -c \
'words=set("Alfa Bravo Charlie Delta Echo Foxtrot Golf Hotel India Juliett".split()); print(list(words))'

PYTHONHASHSEED=2 python3 -c \
'words=set("Alfa Bravo Charlie Delta Echo Foxtrot Golf Hotel India Juliett".split()); print(list(words))'

The two interpreters use different seeds and will generally traverse their sets in different orders.

Set the same PYTHONHASHSEED again, and you get the same behavior again.

Randomness Is Everywhere

Hash tables are only one example.

Redis sorted sets are implemented using both a hash table and a skip list. A skip list chooses the height of each newly inserted node probabilistically—essentially flipping coins to decide how many express lanes that node joins. Redis’s source code contains a function named zslRandomLevel() that performs this selection.

Randomized quicksort chooses pivots randomly to make adversarial or unfortunate input orders less likely to trigger terrible performance. Not every quicksort implementation is randomized, and not every sorting algorithm is quicksort, but randomized sorting algorithms are widely used.

Bloom filters use several hash-derived positions for each inserted item. Their operation may be completely reproducible once the hash functions and seeds have been chosen, but they rely on the same random-looking distribution.

The probabilistic Miller–Rabin primality test can select random bases when deciding whether a number is probably prime. For integers of bounded size, carefully selected fixed bases can make the test deterministic.

Classic shared or half-duplex Ethernet uses truncated binary exponential backoff. After a collision, each participant waits a randomly selected number of time slots before trying again.

Otherwise, two identical network cards that collided once could continue retrying in lockstep and collide forever.

Modern switched, full-duplex Ethernet normally has no collisions, but the old randomized protocol remains an important part of Ethernet’s history.

Randomness is not an embarrassing defect hidden inside these systems.

It is how they avoid pathological behavior, adversarial inputs, lockstep collisions, and unnecessarily complicated deterministic machinery.

And Now We Return to AI

A language model does not usually emit a single mandatory next word.

For each position, it calculates a probability distribution over many possible next tokens. One token may be very likely, several others may be plausible, and thousands may be barely possible.

A sampling algorithm then chooses among them.

That choice is ordinarily made using a pseudorandom number generator. Unless you provide a seed, the software or service will generally obtain one from the operating system or some other source of entropy.

The neural network’s calculations may be mostly deterministic. The sampling step is where deliberate randomness enters.

That randomness is useful. It allows the model to explore different phrasings, different implementations, and sometimes entirely different approaches to the same problem.

In code generation, repeatedly sampling several solutions can be more productive than demanding that the model always emit its single highest-probability answer.

But the randomness is usually controllable.

In PyTorch, for example:

generator = torch.Generator(device="cuda").manual_seed(12345)

You can then use that generator in the sampling operation:

token = torch.multinomial(
    probabilities,
    num_samples=1,
    generator=generator,
)

PyTorch also provides the simpler global form:

torch.manual_seed(12345)

That sets the seed used by PyTorch’s random-number generators.

Using the same seed does not guarantee identical output across every GPU, software release, model revision, batching configuration, or hosted service. Parallel execution can introduce additional nondeterminism.

But with the same model, prompt, parameters, runtime, and seed, you can often reproduce the same generation.

AI can be wrong.

It can generate nonsense. It can hallucinate. It can produce subtly broken code with the confidence of a man explaining cryptocurrency at a wedding.

But the fact that it gives you two different programs is not evidence that either program is wrong. It means the sampler was allowed to choose among alternatives.

So when someone complains that AI is nondeterministic, my answer is simple:

Do not be a sloppy programmer. Read the API and set the seed yourself.