A screen-only arena application#
The arena example shows how to build an application
around Venn's outcome API and an ordinary vector memory. Venn returns a move,
per-move utilities and evidence, and the screen's vector. The client steps the
game, controls exploration and saves each completed game's discounted returns.
The reader is never trained. With --head, the client also compiles the memory
into a small head through the same service and evaluates it on its own.
It uses ViZDoom with its bundled Freedoom assets, not commercial game assets.
The app supports bundled scenarios and custom .cfg levels with discrete
buttons. A move presses one button; continuous controls and arbitrary action
combinations need a different client. Third-party levels and readers have
their own terms.
Watch the arena#
Download the video or read what the video shows and how it was measured.
Install and check the application#
Use Python 3.12 and a clean environment without the unrelated venn package:
python -m pip install ".[arena,embedding]"
python examples/arena.py --smoke-test --output /tmp/venn-arena-smoke
The smoke test takes four game steps through OutcomeService using simple
screen descriptors. It downloads no reader, learns no semantic policy and
reports no evaluation score. This checks the game/API plumbing, not gameplay
quality. Choose a new output directory for each invocation.
Run the native Venn image path#
The video uses a frozen pinned reader, thirds of each screen and app-owned exact FAISS GPU memory. This command selects those runtime components:
python examples/arena.py --reader-mode native --device cuda:0 \
--dtype bfloat16 --compile-norms --index-device cuda:0 \
--games 128 --decisions 431173 --level defend_the_center \
--seed 0 --eval-episodes 50 --record --download-reader \
--output /tmp/venn-arena-native
GPU indexing requires a separately installed FAISS GPU build compatible with
your CUDA environment; the default faiss-cpu distribution does not provide it.
Use --index-device cpu to keep the store on the host. Smaller GPUs may need
fewer games, an eager reader without --compile-norms, or a smaller budget.
There is no automatic batch-size change hidden in the results.
--download-reader explicitly permits fetching the pinned upstream reader.
Otherwise the reader must already be available in the local Hub cache.
The native path calls OutcomeService.decide_batch on raw uint8 screenshots,
so Venn performs preprocessing, encoding, evidence lookup and action selection.
It uses the existing venn-1 image tag and does not require fitted heads.
This example is a reimplementation of the application shown in the video. Instrumentation, evaluation scheduling and recording differ, so matching the reader and settings does not guarantee the same learning trajectory or scores.
Try other hardware, readers and levels#
The portable path accepts an image-capable SentenceTransformer reader on CPU or GPU. For example, substitute an immutable commit for a CLIP image encoder:
python examples/arena.py --reader-mode portable --device cpu --dtype float32 \
--model sentence-transformers/clip-ViT-B-32 --revision FULL_40_CHARACTER_COMMIT \
--layout whole --games 4 --decisions 20000 --level basic \
--eval-episodes 20 --download-reader --record --output /tmp/venn-arena-clip
Replace the revision placeholder; a branch or floating tag is rejected. Image support and the reader's access requirements must be checked separately. The portable path uses the selected model's ordinary image processor and passes tagged vectors to Venn. It does not claim the native GPU preprocessing protocol or native performance. CPU float32 is supported; CPU float16 is not. CPU runs are much slower than the native GPU path: in two short checks on a 96-core host shared with other jobs, this CLIP command made about one to one and a half decisions a second, so its 20,000 decisions take several hours.
Every portable reader gets a distinct experimental: profile derived from
its model and preprocessing protocol, with its pinned revision, dimensions
and field order. Its vectors cannot be relabeled as venn-1 or mixed with
another store. These tags are supported only by outcome-store geometry;
they do not enable other readers for the trained head, standard text serving
or the native image path.
Change --level to another bundled scenario or an existing .cfg file.
Set --episode-timeout and --frame-skip explicitly for the intended task.
The reward function comes from the level; maximum discounted returns need
not be a good utility estimator for every task.
Compile the memory into a head#
--head awr (or --head bc) calls OutcomeService.register_family on the
final memory, saves the head as head.pt, and runs the final evaluation twice:
once answered by the memory and once answered by the head alone, with the store
never consulted. --head-every 25600 additionally re-registers the head every
25,600 training decisions, warm-started from the previous one, and serves it
during training from the first registration on; the memory keeps being written
either way. The option vectors are text embeddings of the button names from the
same frozen reader, so the head has 49,153 parameters for the thirds layout.
python examples/arena.py --reader-mode native --device cuda:0 \
--dtype bfloat16 --compile-norms --index-device cuda:0 \
--games 128 --decisions 431173 --level defend_the_center \
--seed 0 --eval-episodes 50 --head awr --download-reader \
--output /tmp/venn-arena-head
result.json then has head with the objective, SHA-256, parameter and case
counts, registration seconds, fitting losses and the head-only evaluation.
In the development runs behind the video (the head study), heads were compiled from five independent 431,173-decision memories (about 380,000 cases each) and scored over 50 fresh games per memory, 250 games in total, with the memory and heads frozen. The memory row is that study's same-day memory-only control:
| Served by (head study) | Kills per game | Against the memory (paired 95% interval) |
|---|---|---|
| Memory, k = 11 | 6.20 | |
Head, awr objective, 49,153 parameters |
8.36 | +2.16 [+1.54, +2.95] |
Head, bc objective, 49,153 parameters |
8.26 | +2.06 [+1.26, +2.78] |
| Head compiled from a raw-pixel memory | 3.92 |
The memories themselves were built in an earlier study of five runs with 50 games each, which also measured the controls. The video's chart takes its memory, raw-pixel and random bars from it (see arena measurement details):
| Played by (earlier five-run study) | Kills per game | Score per game (kills minus the one death) |
|---|---|---|
| Memory, k = 11 | 6.17 | 5.17 |
| Memory of raw pixels | 4.94 | 3.94 |
| Random moves | 1.31 | 0.31 |
The research report compared the pixel head's 3.92 kills with the pixel memory's 3.94, which is a score per game; in kills the pixel memory scored 4.94, so the pixel head is about one kill below its memory.
Both heads beat the memory on every one of the five seeds. Registration took about 55 seconds per memory on one A100, including the label pass through the adapter, and a head answers a single screen in under half a millisecond. The same bilinear form trained to regress outcomes instead of imitating the store's answer scored 5.18, so the objective, not the head's size, is what matters. These numbers come from the research runs, not from this example script; an independent run with this command is a new experiment.
Re-registering during play did not add to this. In the same study, a loop
that served the head from the start of a further 431,173 decisions and
re-registered it every 25,600 ended at 6.2 kills against 6.3 for the
memory-only loop with the same budget. The head opened at the compiled
head's level and converged to the memory's: its agreement with the memory's
greedy move rose from 0.49 to 0.70 across the re-registrations, because the
awr objective imitates the moves the head itself had taken. Serving a head
compiled from a young store was worse: five loops that registered their first
head after 25,600 decisions from about 19,000 cases all collapsed to a single
move. Use --head-every with those results in mind; compiling a mature
memory once with --head is the demonstrated use.
Outputs and repetition#
result.json records the model protocol, hardware choices, seed, budget,
completed training episodes, online greedy-window curve and separate final
evaluations. progress.json reports progress during training. memory.npz
and memory.json retain the app-owned vectors and maximum returns; use
--load-memory /path/to/previous/output to start fresh games with that memory.
Action layout and complete representation tags must match. Unfinished games
and policy RNG are not restored, so this is memory reuse, not exact resumption.
--record writes evaluation-first.mp4 from the first final evaluation game;
with --head, it also writes evaluation-first-head.mp4 from the first game
of the head-only evaluation.
Store growth is not capped. Memory consumption scales with completed unique state/action cases and vector dimension; monitor RAM/VRAM and reduce the budget on smaller hardware. An interrupted run may have a progress report but no final memory checkpoint. Model startup, encoding, training and final evaluation costs must be reported separately when comparing hardware.
For repeated runs, use distinct --seed and --output values. With multiple
GPUs, assign independent runs to different --device cuda:N and
--index-device cuda:N pairs. Each GPU can run a separate seed if it has
enough memory. The app does not aggregate runs. Changed hardware, readers, budgets or levels produce new experiments, not
automatic reproductions of the video's claims.