04 uop stream¶
The deciding is done by a micro-op stream: 1,000 MAP jobs, each on a group whose size
the stream draws at random, run three ways that must give the same tiles.
stream = micro_ops.Stream(seed=seed)
job, random_word, count, first, room, base, page = stream.registers(7)
top = stream.label()
stream.rand(random_word)
stream.mod(count, random_word, core_count)
stream.addi(count, count, 1) # 1 to every core
map_args = [addresses["in"], tiles, addresses["out"], base, page]
if form == "submit": # the stream picks the first core too
stream.sub(room, core_count, count)
stream.addi(room, room, 1)
stream.shr(first, random_word, 8)
stream.mod(first, first, room)
stream.submit(addresses["map"], (first, count), args=map_args)
stream.wait(0) # one command in flight
else: # the dispatcher's queue picks it
stream.enqueue(addresses["map"], count, args=map_args)
stream.add(base, base, count) # the next job's output tiles
stream.addi(page, page, STEP) # the input window moves (modulo the window)
...
stream.addi(job, job, 1)
stream.jlt(job, job_count, top)
stream.wait(0)
stream.halt(job)
| Way | Who decides | How the commands go |
|---|---|---|
hostfed |
the host | run.run_hostfed(stream): the package's stream interpreter runs the SUBMIT stream on the host and sends its commands through the host ring, each waited for as the stream says |
submit |
the X280 (core 1 of cluster 0) | run.upload(stream), run.start, run.wait_engine: the SUBMIT stream submits each command and waits for it |
enqueue |
the X280 and the dispatcher's job queue | the same calls with the ENQUEUE stream; the queue gives each job the lowest free group of its size, so several jobs are in flight |
errors |
- | three streams that end in an error event, then 20 jobs of the SUBMIT stream on the same run |
MAP (kernels/): each rank of a job adds its partition of the job's T input tiles
(tiles, a1, from tile first_tile, a4) into one tile and stores it at output page
first_page (a3) plus its rank. Groups differ between the ways
(only submit and hostfed pick the same ones), but every job writes the same tiles. The
inputs are integers in [-2, 2], so every sum is exact in bf16, and the host compares every
output tile of every way bit for bit with numpy. It also checks each core's command count
against the simulated groups (hostfed, submit) or the total of core commands
(enqueue), and gives microseconds per job, from the start to the stream's end. Each run
is traced: the groups the grid saw must be the simulator's in order for submit (one
command in flight), and the trace gives the most commands in flight at once.
The errors way uploads a stream whose first micro-op has a bad opcode (the kernel's
check ends it with reason opcode), one that loops past its step limit (steps) and one
that enqueues a job larger than the grid (never_fits). Each ends in its error event; the
run goes on, and 20 jobs of the SUBMIT stream after them are exact, every core's count is
right, and the stop is clean.
Running it¶
python uop_stream.py --card 0 # every way
python uop_stream.py --card 0 --ways hostfed # one way
Options: --jobs (1,000), --tiles T (64), --seed (the stream's xorshift seed),
--depth (ring slots, 16), --deadline (per command, seconds), --no-trace. It prints
one JSON line with ok, true only when every check above holds, and exits non-zero
without it. Reading the 59,456 output tiles (116 MB) back takes 65 to 80 s per way, in
chunks of 2 MB with a line per chunk (one every 1 to 2 s).
On a p150a (card 0, 120 cores, default arguments, X280 at the session's 1000 MHz; the 1750 MHz opt-in's figures in the last column): 1,000 jobs, 59,456 tiles, 0 wrong and every core's count right in every way; the SUBMIT and ENQUEUE streams give the same jobs, and the grid saw the simulator's groups in order.
| Way | Microseconds per job | Most commands in flight | Upload | 1750 MHz |
|---|---|---|---|---|
hostfed |
163 | 1 | - | 161 |
submit |
13.4 | 1 | 576 bytes | 11.5 |
enqueue |
9.3 | 2 | 512 bytes | 8.4 (3 in flight) |
Host-fed, every job is a round trip to the host; on the X280 it is a round trip to the
dispatcher on the same cluster. The three error streams ended with opcode,
steps and never_fits. The dispatcher's image with both engines is 27,664
bytes.