How to simulate
everything (all at once)
Toby Ord
The Complete Turing Machine
The Universal Turing Machine (or
UTM) is well known. It is a Turing machine that takes as input a
binary representation of a Turing machine and a binary representation
of some arbitrary input and simulates the workings of the specified
Turing machine on the specified input.
In what follows we'll assume three
tape Turing machines (input, work, output), with a binary alphabet,
where 0 corresponds to an unmarked square and 1 is marked. Input
to the Turing machine is assumed to be finite (i.e. there are only
finitely many 1s on the tape). While I speak of a single input,
it could easily encode finitely many individual binary strings into
the single binary string.
Less well known is what I call the Complete
Turing Machine. It simulates in parallel all Turing machines
on all input. There is a small trick to this, but it is not difficult.
Since Turing machines can go on forever, we can't wait for one to
finish before simulating the next, but have to simulate them in
parallel. It is easy enough to simulate two Turing machines in parallel,
performing a step from the first then a step from the second and
so forth:
step 1 of machine 1, step 1 of machine
2,
step 2 of machine 1, step 2 of machine 2,
...
This approach can only interleave finitely
many infinite sequences, but we require infinitely many Turing machines.
Thankfully, there is a well known trick to interleave infinitely
many infinite sequences into a single infinite sequence. For example:
step 1 of machine 1,
step 2 of machine 1, step 1 of machine 2,
step 3 of machine 1, step 2 of machine 2, step 1 of machine 3
step 4 of machine 1, step 3 of machine 2, step 2 of machine 3, step
1 of machine 4
...
In this way, every Turing machine gets
to execute every step (if a Turing machine halts we just skip over
each remaining step allocated to it).
We could specify a first draft of the Complete
Turing Machine in C-like pseudocode:
for (i = 0; ; i++)
{
initialise_tm(tm[i],
i);
for (j = 0; j <=
i; j++)
compute_next_step(tm[j]);
}
tm[] is an (extendable) array of representations
of configurations of Turing machines including information about
their transition tables, their current tape, their current state,
and the location of the tape head. It starts uninitialised.
initialise_tm(tm, i) is a function that
initialises a Turing machine representation to start in the starting
state, with a blank tape, the tape head at the start position, and
using the ith transition table (on some recursive coding of all
legal transition tables).
compute_next_step(tm) is a function that
takes such a representation and simulates a single computational
step on it. It is a standard core part of a Universal Turing machine.
This will simulate all Turing machines
(including non-terminating ones) running on a blank input tape.
We actually want all Turing machines on
all inputs, but we can achieve this by a small modification to the
initialisation function. Instead of using i as a code for a transition
table, it now treats i as coding a pair of natural numbers (x,
y) (on some recursive coding of all pairs of naturals)
then initialises the Turing machine to use the xth
transition table and sets the input tape to the yth
input tape (on some recursive coding of all finitely marked input
tapes). Since the coding guarantees that all (x,y)
pairs will be in this list (possibly using an ordering like the
triangular one above), each transition table, input pair will be
initialised at some point and every step of their computations will
be run.
So far so good. We now have a machine that
will simulate all Turing machines on all finite inputs. The existence
of such a Turing machine is known, but not all that well known.
This is a shame, because it is a pretty amazing thing: in some ways
much more amazing than the Universal Turing machine. Firstly, it
computes all Turing computable functions (and evaluates them on
all inputs). Obviously it also semi-computes all Turing semi-computable
partial functions (it evaluates them on all inputs where there is
a defined answer).
However, thinking of what it does in terms
of functions misses most of the glory of this machine. Consider
sorting a finite list of natural numbers. This is a Turing computable
function, but can be done in many ways. There is quick sort, merge
sort, insertion sort, selection sort, bubble sort, and all their
friends. The CTM doesn't just sort lists, it sorts them using each
and every one of these distinct algorithms. It even sorts them with
every subtle variation on each of these algorithms.
At the first level of simulation, the CTM
simulates all and only Turing machines. What about programs in C?
Or functional and logical languages such as Haskell and Prolog?
Or unusual models of computation like neural nets. We might think
that there are algorithms that can be expressed in these types of
languages which can't be expressed on a Turing machine and we'd
like to simulate these too. Well, for each of these there is a Turing
machine that acts as an interpreter for that language, and there
is also a Turing machine that uses such an interpreter to run all
programs in that language on all inputs (analogous to the CTM itself).
So the CTM simulates a Turing machine which simulates all Haskell
programs and another which simulates all Prolog programs and another
which simulates all neural networks etc. The CTM's calculation thus
includes simulations of all programs in all Turing complete languages
and sub-Turing languages (most of which are yet to be invented)
which are run on all inputs, but they are nested one level down
— simulations inside simulations.
The CTM also simulates physical phenomena.
If our universe is Turing computable, then it will be simulated
by this CTM. Indeed since there are infinitely many Turing machines
that express the same algorithm, it will be simulated infinitely
many times in parallel. The CTM will also simulate it with every
possible recursive set of initial conditions. In between these threads
will be simulations of the hailstone numbers, chess, tropical cyclones,
the game of life, lolcats, and this entire simulation itself.
Adding oracular computation
What can't a CTM simulate?
I'm sure many readers will immediately
think of their favourite uncomputable function or process. Others
will think of counting arguments: there are only a countable infinity
of Turing machines and there are an uncountable infinity of processes,
so almost all of them must be impossible to simulate. For example,
consider an endless simulation of an empty room with a light bulb
that flashes on and off: for each one minute block of time, the
light stays on or stays off, but at the end of each minute it may
or may not change to the other state. There are uncountably many
such sequences of flashes, as they correspond directly to the infinite
bitstrings (e.g. a string where the nth bit is 1 if and
only if the light is on during the nth minute). There are
thus uncountably many lightbulb simulations, and so not enough Turing
machines to be able to simulate all of them. Similarly, there is
a lightbulb simulation whose pattern of flashes spells out a lookup
table for the halting problem and we're going to have a lot of trouble
simulating that one.
This is a compelling argument. Surprisingly
it is wrong.
It turns out that we can simulate all such
sequences of flashing lights so long as we do them simultaneously.
Let's start with two parallel simulations:
one with the light off for the first minute and one with it on.
We then fork each of these simulations, simulating both ways the
next minute could go. We continue in this way, forking all the existing
simulations at each stage and having an exponentially increasing
number of simulations in parallel. We are building up an infinite
binary tree of simulations in a breadth first manner.
Infinite binary trees are remarkable for
having an uncountable infinity of branches (each of which
is infinitely long) coming down from the root, but are comprised
of only a countable infinity of vertices and edges. This
is a pretty surprising feature, and is only possible because the
branches overlap so much with each other (each branch overlaps with
an uncountable infinity of other ones). Because of this strange
fact, this tree structure allows us to simulate the seemingly unsimulable.
The individual edges on the tree correspond
to the simulated minutes of time. The fact that they are countable
allows our parallel simulation to get through all of them. The branches
correspond to the entire infinite sequences of light flashes. For
each of these branches (an uncountable infinity) every stage of
that branch gets simulated at a particular point in the computation.
Each branch is simulated in temporal order and the stages depend
appropriately on the stages before. For example, if the heat of
the glass bulb is modelled, its level will depend upon the exact
sequence of flashes that lead up to that moment and this will be
modelled perfectly. In other words, this elaborate branching computation
will faithfully simulate each of these uncountable infinity of sequences
in parallel.
How exactly could this process be made
precise? Let's represent each simulation by an oracle machine. This
is a Turing machine with an additional read-only tape that is called
an oracle tape. The state transition table is allowed to refer to
what is under the tape head on the oracle tape as well as what is
under the tape head on the input tape or the work tape. There is
nothing mysterious so far. What is normally mysterious about oracle
machines is that we usually consider that this oracle tape begins
with infinitely much input on it. If the oracle tape begins with
an infinitely long bitstring on it, the oracle machine can potentially
do all kinds of interesting things, such as solving the halting
problem. However let's set such possibilities aside and consider
it with an oracle tape that consists only of zeros. This won't allow
it to compute anything a Turing machine can't compute, and there
is a Turing machine that can simulate any such (blank-oracle) oracle
machine. Indeed this simulating Turing machine is just a trivial
modifiication of a Universal Turing machine.
Consider an oracle machine that simulates
a room with a light in it, and has this light be on during the nth
simulated minute if and only if the nth square of the oracle
tape is a 1. As the oracle tape starts with only 0s, it simulates
a light that is always off. Now consider a Turing machine that simulates
two such oracle machines in parallel, but before it simulates the
nth timestep for each oracle machine, it alters the representation
of the configuration of the second machine to put a 1 on the nth
square on its tape. Since the simulated machine couldn't have looked
at a square that far away yet, it will just behave as if it were
given an infinite bitstring of 1s. In this case it will simulate
a light that is always on. So the main Turing machine will be simulating
a light that is always on and a light that is always off in parallel.
Now lets add the infinite branching, with
some more C-like pseudocode:
initialise_om_with_room_simulation_program(om[0])
copies = 1;
for (i = 0; ; i++)
{
for (j = 0; j <
copies; j++)
{
fork(om[j],
om[copies+j]);
set_o_tape_square(om[copies+j],
i, 1);
}
copies *= 2;
for (j = 0; j <= copies;
j++)
compute_next_step(om[j]);
}
initialise_om_with_room_simulation_program(om)
sets up the specified oracle machine with the transition table that
will make it perform a simulation of the room with the flashing
lightbulb (whose state in minute n will be determined by
the nth square of its initially blank oracle tape).
set_o_tape_square(om, i, value) just alters
the ith square of the oracle tape of the oracle machine to be equal
to the given value.
fork(om1, om2) simply sets the entire
configuration of the oracle machine om2 (its tapes, tape heads,
current state, and transition table) to be the same as that of om1.
These nested loops build up the infinite
binary tree of computations in a breadth first manner. The outer
loop counts the number of timesteps and the inner loops do the forking
and the next step for each sequence. For each sequence of flashes,
this program will simulate it: even a sequence that encodes a lookup
table for the halting problem. There is no paradox here. There would
be if a Turing machine could produce just one simulation that encoded
a lookup table for the halting problem, but this one is produced
along with everything else and can't be isolated, so it can't produce
a paradox.
This is related to the fact that while
most of the individual sequences of bits have infinite Kolmogorov
complexity, the set of all such sequences has very little Kolmogorov
complexity. It is simpler than the sum of the complexities of its
parts. Indeed for some of these parts, the whole is simpler than
just that one part. This may seem odd, but is also true of, say,
a sphere. It can be decomposed into interlocking parts of extreme
complexity, but the whole is simpler than one of these parts, and
the whole cannot in general be used to derive a particular part.
So where does this get us? What can we
simulate using this trick? While the particular branching simulation
trick was used with a particular scenario in mind (a room with a
lightbulb), it can be applied much more broadly than that. We can
make the initialisation function initialise the starting oracle
machine to have any transition table we like and any input we like.
The pseudocode above can thus be used to do general purpose oracular
computation. For example, if we set up the starting machine to treat
its oracle tape as coding the halting problem, we could run a machine
that uses this to calculate the busy beaver function, or Chaitin's
Omega constant, or to implement the uncomputable AI algorithm, AIXI.
Of course it will go wrong in all but one branch of the entire branching
tree of simulations, but in that branch it will correctly use its
halting problem oracle to compute another non-Turing-computable
function.
The branching trick is analogous to Borges's
story about the Library of Babel, which contains all possible 410
page books of a certain format. This library contains many of the
great historical works, but they bear no causal relation to the
originals. They are merely there because everything is. Similarly,
we have all infinite bitstrings, so are guaranteed to have all the
most interesting ones. If this were all, it wouldn't be that interesting,
but this library of infinite strings are not the end result. They
feed into the oracle machines and can lead to infinite computations
of exquisite complexity.
We've seen that we can simulate any particular
oracle machine with all oracle tapes simultaneously. But why hard
code in a particular oracle machine? We could follow the UTM and
have our branching computation take the oracle machine to run as
an input. Or we follow the CTM and combine the branching trick of
this section with the interleaving trick of the previous section.We
can thus have a single Turing machine that simulates all oracle
machines in parallel, running on all inputs, and with access to
all oracle tapes.
Since a Turing machine is equivalent to
an oracle machine with a blank oracle, this computation effectively
includes simulations of all Turing machines on all inputs too. However,
if you really want you could interleave this branching computation
with a regular CTM, explicitly simulating all Turing machines on
all inputs as well as all oracle machines with all oracle tapes
on all inputs. This even has the advantage that your addition of
all the oracle machines only slows down the Turing machine simulation
by a factor of two.
Adding infinite inputs
When I have said 'all inputs' so far in
this piece, I have meant all finite inputs. But do we need to keep
to this restriction? No.
Let us return from oracle machines to Turing
machines. The branching trick from the previous section can be used
to allow us to simulate a particular Turing machine on all infinite
inputs. We simply initialise the original Turing machine to have
a blank input tape and then fork it at every timestep, with one
copy having the next square of the input tape set to 1. This is
exactly the same as the oracle machine version, but changing the
input tape instead of the oracle tape. This will run that Turing
machine on all infinite inputs in parallel.
As before, all these tricks can be combined.
The only difficulty is combining the two types of branching trick
to allow oracle machines with infinite inputs. Instead of forking
each simulation into two at each timestep (0 and 1), just fork it
into four. Put 0 on both the nth square of the input tape
and the nth square of the oracle tape on the first one,
put 0 and 1 respectively for the second, 1 and 0 for the third,
1 and 1 for the fourth.This infinite quaternary tree does the job
of both binary trees.
Note that the infinite input trick also
generates all finite inputs, so we no longer need to have the complication
of running through all Turing machine, input pairs from the original
CTM specification.
Bringing it all together
We now have a single Turing machine that
simulates:
• all
Turing machines on all inputs (finite or infinite),
• all
oracle machines with all oracles on all inputs (finite or infinite).
We shall call this a Hyper-Complete
Turing Machine, or HCTM.
This is a very startling object! I think
many people would be surprised that a Turing machine is powerful
enough to simulate so many things. If our universe is Turing computable,
then this simulates it. If our universe is not Turing computable,
this might still simulate it — so long as there is some way
to do so with access to an infinite amount of information. For example,
it simulates universes which contain rods of arbitrary real valued
lengths. It also simulates hypercomputational
universes in which you can build accelerating Turing machines that
do each step in half the time of the step before, allowing an infinite
amount of computation in a finite time. It simulates universes in
which there are physical devices that can (contra Gödel) decide
the truth of any sentence in first order arithmetic.
However there are things that require too
much information to be simulated. While it can store a countable
infinity of infinite precision real numbers, it cannot store an
uncountable infinity of them, so I don't see how it could faithfully
simulate all universes that contain an uncountable infinity of rods
with real valued lengths.
If consciousness is created by a certain
kind of computational process (even a non-Turing computable one),
then it appears that it would produce conscious observers in some
of its simulated universes. Indeed, it would produce infinitely
many of them, having a great variety of experiences (perhaps all
possible experiences). I don't know whether simulating all universes
really would create consciousness, but lets at least see what would
follow from that.
David Deutsch has argued that there are
virtual reality environments that Turing machines cannot simulate
(by listing the environments each Turing machine simulates and diagonalising
out to have the nth minute of the impossible simulation
be different to the nth minute generated by the nth Turing
machine). This is true if we are only allowed to simulate a single
environment at once. However, if I were not a flesh and blood human
hooked up to a computer, but a computational process within a computer,
then the simulation could fork that process as well. For example,
take the simulated room with a flashing light and add a human being.
If that simulated human being were conscious, then when the computation
forks, a conscious being would behold the light being on and a conscious
being would behold the light being off. In the nth minute,
there would be 2n different conscious states
being simulated with memories of all the different sequences to
that point. If the conscious being were inside the simulation like
this, then (contra Deutsch) they could observe any environment made
up of individually simulable pieces. There could even be branches
of the computation with conscious observers who have access to programmable
hypercomputers, who can play with them to their heart's content.
I have said a lot about the CTM and HCTM
simulating computations and physical processes. One might object
that they aren't really simulating things. For example, one might
think that the CTM is only directly simulating Turing machines and
that indirect simulation is not real simulation, so it is not simulating
Haskell programs or physical environments. I'm not an expert in
how to define simulation, but let me say that if this is correct,
then the computer on your desk never simulates physical processes
either, for there are similar levels of indirection there. For example,
it might be simulating a java virtual machine, which is simulating
a bouncing ball. But at least as we typically use the word, we say
that the computer is simulating a bouncing ball.
One might instead object that branching
computations are not really simulating the contents of all the branches,
since when a simulation is forked the newly made copy didn't really
do the first part of the computation. I find this objection unpersuasive
too, as routine simulations often require the forking of computational
threads and we normally talk about these threads as if they really
do share a common history.
However, I'm prepared to accept that there
may be narrow senses of simulation in which some of the things that
go on in these grand computation aren't being 'simulated'. That
is fine, I'm just arguing that there is a broader sense of simulation
in which they are, and that this sense seems to closely match common
usage.
What is the relevance of CTMs and HCTMs?
I have to admit that I can't see any practical upshot. It is doubtful
that we could run the computation. Firstly, the branching computation
trick forces an exponential slowdown, making timely results unlikely.
Even if this were considered acceptable, it would require the Turing
machine to be able to safely operate for ever and to use an infinite
amount of memory. Moreover, even if we could get around this, it
would appear to have no use to the outside world. Internally, it
would either generate no conscious experience, or it would generate
a vast amount, presumably with as much suffering as bliss.
If the CTM and HCTM have any use at all,
it is likely to be in theoretical developments in computer science,
physics, or philosophy. For example, they might bear on debates
about whether the universe is Turing computable, and whether the
fundamental layer of reality is a CTM or HCTM. These would be a
pretty plausible candidate, for while they are relatively simple
to specify, they would give rise to a vast amount of complexity
within some of their computational branches, and apparently enough
to fit all of our physical laws and initial conditions. Indeed Jürgen
Schmidthuber has explicitly suggested that the CTM might be
this ultimate layer, and Max
Tegmark has suggested that all the richness of mathematical
objects might be this ultimate layer. This HCTM lies somewhere in
between.
As a final thought, consider that the HCTM
is itself a Turing machine. It will thus be simulated somewhere
within the standard CTM. While all the messing around with branching
simulations has been useful for showing us just how much we can
simulate (all at once!) we don't actually need to explicitly code
it up. The CTM eventually finds an HCTM and thus already contains
these simulations of oracle machines and infinite inputs. I mentioned
before that while the only things a CTM directly simulates are Turing
machines, some of these Turing machines simulate Neural Networks
or Haskell programs, so the CTM indirectly simulates these too.
Similarly, the CTM directly simulates an HCTM which then directly
simulates preposterously many non-recursive things, so the CTM also
(indirectly) simulates all of these too.
|