examples: Conway's Game of Life with one on-engine pass per generation - #272
Conversation
sbryngelson
left a comment
There was a problem hiding this comment.
The rule is right and the example runs clean. I checked the Life logic rather than trusting the
spot test, since the exact-match trick is unusual:
max(alive * is2, is3) = (alive & nb==2) | (nb==3)
which is Conway's B3/S23 in its reduced form -- a live cell with 3 neighbours is covered by the
nb==3 term, so the two branches really are all you need. 1 - clip(|nb-k|, 0, 1) is an exact
equality test because the conv output is an integer count and fp16 represents 0..8 exactly. The
numpy reference encodes the same rule independently, so the spot check isn't circular.
End-to-end on device: 100 generations, field stayed boolean, population 3228/36864, writes the
51-frame WebP, exits 0. Tests pass, ruff clean.
One thing to fix before this goes in, and it's presentation rather than correctness. The run is so
fast that three separate numbers all round to zero:
evolve OK 100 generations on the ANE (0.0s wall, 0.12 ms/step)
energy ~0.0s of ANE step time at the measured ~1.48 W rail ~ 0.0 J for the whole run
Actual values are 0.0109s and 0.0162 J. For a showpiece, a headline reading ~0.0 J for the whole run looks like broken instrumentation rather than the point you're making, which is that the whole
simulation costs almost nothing. Switching to ms/mJ below 1 says it properly:
evolve OK 100 generations on the ANE (12 ms wall, 0.11 ms/step)
energy ~11 ms of ANE step time at the measured ~1.48 W rail ~ 17 mJ for the whole run
Patch below, verified: ruff clean, 2 passed, example exits 0.
diff --git a/examples/game_of_life.py b/examples/game_of_life.py
index cef8f86..44357fc 100644
--- a/examples/game_of_life.py
+++ b/examples/game_of_life.py
@@ -28,6 +28,15 @@ def out(s=""):
sys.stdout.write(s + "\n"); sys.stdout.flush()
+def secs(s):
+ """One generation is ~0.1 ms, so a whole 100-step run rounds to '0.0s' in seconds."""
+ return f"{s * 1e3:.0f} ms" if s < 1.0 else f"{s:.1f}s"
+
+
+def joules(j):
+ return f"{j * 1e3:.0f} mJ" if j < 1.0 else f"{j:.1f} J"
+
+
def neighbor_kernel():
"""The 8-neighbour count as a fixed 3x3 conv: all ones except the centre."""
k = np.ones((1, 1, 3, 3), np.float32)
@@ -99,11 +108,11 @@ def main():
energy = ANE_RAIL_W * ane_t
out(f" {GREY}evolve{R} {CHECK} {STEPS} generations on the ANE "
- f"{DIM}({wall:.1f}s wall, {ane_t * 1e3 / STEPS:.2f} ms/step){R}")
+ f"{DIM}({secs(wall)} wall, {ane_t * 1e3 / STEPS:.2f} ms/step){R}")
out(f" {GREY}field{R} {DIM}population {pop:.0f} of {N * N} cells, "
f"{'boolean' if binary else 'NON-BOOLEAN'}{R}")
- out(f" {GREY}energy{R} {DIM}~{ane_t:.1f}s of ANE step time at the measured "
- f"~{ANE_RAIL_W} W rail ~ {BOLD}{energy:.1f} J{R}{DIM} for the whole run{R}")
+ out(f" {GREY}energy{R} {DIM}~{secs(ane_t)} of ANE step time at the measured "
+ f"~{ANE_RAIL_W} W rail ~ {BOLD}{joules(energy)}{R}{DIM} for the whole run{R}")
out()
wrote = render(frames)closes sbryngelson#173) The 8-neighbour count is a fixed 3x3 conv (all ones except the centre) with zero-padding at the border, and the birth/survival rule is elementwise: `|nb - k|` clipped to [0, 1] is the not-equal indicator, so the rule needs no equality op. The generation compiles once and is re-dispatched every step, like reaction_diffusion. Writes docs/assets/game_of_life.webp. The on-engine rule matches a numpy zero-padded reference exactly (maxdiff 0.0) and a blinker oscillates with period 2.
392e709 to
3dfcbca
Compare
sbryngelson
left a comment
There was a problem hiding this comment.
Applied as suggested -- thanks, and thanks for the rebase. Verified on device against current main: ruff clean, 2 passed, and the run reads 9 ms wall / 13 mJ instead of three zeros. Merging.
Closes #173.
Conway's Game of Life with every generation as one on-engine forward pass, in the family of
reaction_diffusion.py.pad=1, so the border is zero-padded.|nb - k|clipped to [0, 1] is the not-equal indicator, so1 - thatis the exact-match indicator and the rule needs no equality op:max(alive * is2, is3).Writes
docs/assets/game_of_life.webp(51 frames, ~620 KB, in the range of the sibling assets).Verification
tests/test_game_of_life_example.py(requires_ane): the on-engine rule matches a numpy zero-padded reference exactly for 5 generations on a random 16x16 field (maxdiff 0.0), and a blinker oscillates with period 2.ruff checkandpylint(repo flags) on the new test: clean.pyright aneforgeis unaffected.Local run