Skip to content

GotA neural package tier + native training env (on #72) - #1

Draft
daveey wants to merge 30 commits into
mainfrom
daveey/gota-neural
Draft

daveey wants to merge 30 commits into
mainfrom
daveey/gota-neural

Conversation

@daveey

@daveey daveey commented Sep 25, 2026 •

Copy link
Copy Markdown
Owner

This PR builds on treeform's Metta-AI#72 (DATA arrays plus metered linear/relu/argmax) and adds the part Metta-AI#72 deferred: external weights. It adds a GotA neural package tier and a native training environment. Both share one decoder and one contract, so what a trainer learns is exactly what the hosted seat plays. Plain .bas submissions don't change.

Spec: examples/gods_of_the_arena/neural_basic.md. C ABI: examples/gods_of_the_arena/native_env.h.

What's in it

  • Merge of upstream/main. Bassy is pinned to 669a7c4, a descendant of Add metered native neural array operations Metta-AI/polyworld#72's pin. Every bots.nim keeps both the neural and the mailbox host functions.
  • sim.nim, all changes behaviour-preserving:
    • tickWorld is split into tickWorldBegin/tickWorldFinish, so an env can pause at the heroes' turn.
    • planRoute is a side-effect-free route probe.
    • HeroVm.neural is added.
    • Training-only goal counters are compiled only with -d:gotaTrainingStats.
    • Per-world scratch is thread-local, and map-derived globals are built once per map.
  • bots.nim: contract host procs now go through issue* procs, with the same recording and metrics order. Neural seats get interception hooks. ZIP packages load in loadBots. coworld.nim reads packages up to 16 MiB; the bounded read for plain BASIC is unchanged.
  • Contracts v1, pinned by SHA-256 (neural_contract.nim):
    • Observation: 1407 floats, ego-centric team frame. Self, abilities, items, 25 object slots × 40 features, spell warnings, summary, a 9×9 terrain patch, and the 16 Amendment-1 goal floats.
    • Actions: heads verb 8 / target 25 / point 49 / ability 4 / item 6. Integer decode.
    • A demonstration encoder produces BC labels and runs the mapping ceiling.
  • Package gota-neural-basic/1: a ZIP of exactly manifest.json, policy.bas and model.bin (GOTANET1, the PWNET001 FP32 MinGRU). The strict parser rejects unknown keys. coworld/gota/runtime/neural_package.py is a staging validator that mirrors it.
    • Per-seat budget: 4,000,000 neural ops per tick, separate from BASIC.
    • Host functions: gota_act, run_neural_net, neuralObservation/Logits/State/Model.
    • Recurrent state resets at match start and at first alive decision after death.
    • Each seat logs a line neural: peak_ops=… budget=… model=w… ticks=….
    • Neural seats alone get raised VM limits (40k instr), per Amendment 1. The shipped glue peaks at 1,065 instructions.
  • Native env (native_env.nim):
    • Any mix of learner and scripted seats (league-like 1+9, self-play 10, mixed).
    • Per-seat stats for every goal term; goals via gota_set_seat_goal.
    • set_seat_script, BC labels (seat_orders), a ceiling override, and DAgger shadow scripts.
    • Package-hosted seats, replay recording, and gota_net_* for G0 parity.
    • Thread-safe across handles (--mm:atomicArc --threads:on -d:useMalloc).

Evidence

All runs used Linux, Nim 2.2.10, on a RunPod cpu3c 32-vCPU box.

item result
CI test list (build.yml) at 6e62f52 all green (tests, alloc, events, phases, 6 replayEvents tests, sim, training, gen_replays)
5a no-neural parity: 20 seeds, full 28,800 ticks, base/puller/rusher mix (tools/parity_upstream.sh) fork headless replays byte-identical to upstream/main 20/20; native lib (capture on, no neural seats) final hash = upstream's and its replay re-simulates on upstream's binary with 0 mismatches 20/20
5b package seat vs ABI-driven seat, same random weights, full length (tools/test_native_env.py) identical worlds at w64 / w128 / w256
5c hosted canary: -d:coworld server, 5 random-weight w128 packages + 5 base.bas, 28,800 ticks (tools/canary.py) results written, all 10 seats exit 0, telemetry line peak_ops=486144 budget=4000000 model=w128 ticks=28797 inferences=7200 on every neural seat, replay re-simulates (9197/9197 actions, 0 mismatches)
5d ops per inference (≤1 per tick per seat) w64 218,496 · w128 486,144 · w256 1,168,896, against the 4,000,000 budget
mapping ceiling: 100 seeds, full length, base.bas routed through the contract vs plain (tools/mapping_ceiling.py) no loss. Red paired XP Δ +187±70; blue +45±77. Score 260 vs 129 and 211 vs 99. Wins 2–3% vs 0%
concurrency: 12 handles × 4 threads × 1500 steps, handles migrating (tools/test_native_concurrency.py) identical to serial (hash, obs, rewards, labels)
other acceptance (tools/test_native_env.py) label coverage 519/523 command windows; capture on == off; shadow executes nothing; goal set before reset appears in the first obs; reward == Δscore/1000; Nim and Python validators agree on 11 corruptions

Residual track: defer script (Amendment 3; 0567391, eb02c0e, c2bbdba, 66ead03)

  • What it does. Verb 0 becomes DEFER on opted-in seats. The seat's BASIC program is the script, for example base.bas verbatim. It runs every tick on the true world, and it drafts, shops, levels and buys back itself.
  • When the host consults. On each decision tick, before the script's turn, the host reads the decoded heads once (deferConsult in neural_host_hooks.nim).
    • Verb 0: the script's contract commands for that window run live, at the ticks the script issues them.
    • Any other verb: the decoded command is issued, and the script's contract commands for the window are absorbed (return 1, lastActionError unchanged). An invalid override issues nothing and counts as invalid.
  • Native. gota_set_seat_defer_script(h, seat, path|NULL) and gota_seat_defer_stats(h, seat, int64[2] = {defer, override}). gota_seat_orders returns the script's commands (issued or absorbed) as labels. The call and a shadow expert clear each other.
  • Package. "decoder": {"defer_script": true}, with policy.bas = the script. The Nim parser and neural_package.py both accept the key (boolean only), and build --defer-script sets it.
  • VM limits. Defer seats keep the neural-seat structure limits and the plain per-tick budget (20k instructions / 50k work). The consult is host-side and costs no BASIC instructions, so a script runs out of budget exactly where a plain seat would.
  • Seats without the option are unchanged.
proof (tools/test_defer.py, metta1 job 16184, lib 66ead03, full 28,800 ticks) result
a. Always-defer net (verb-0 decoder row +1, other weights 0), seat = seed mod 10, vs plain base.bas 20/20 seeds with every step's state hash and the replay bytes identical, via the ABI and via the package seat. 0 overrides. Sampled peak of plain base.bas: 12,131 instr/tick
b. Mixed net (w128 random, defer 91% / override 9%): ABI learner driven via gota_net_infer vs package seat 10/10 seeds with identical per-step hashes and identical defer/override/invalid counts (52,492 defer / 5,212 override). 10/10 diverge from plain base.bas, so overrides act
c. Seats without the option vs the pre-change lib (c14762e) 10/10. The mixed lineup (2 random learners, one with a shadow, a plain package, an override seat, capture, puller/rusher) matches on hashes, replay bytes and orders, and so does the plain all-script lineup
tools/test_native_env.py (full) ALL PASS, including the new defer_script accept/reject cases

Action mask (ef62695 header, 446b734 impl, ba8c255 docs)

  • What it adds. gota_action_mask(h, seat, uint8[187]) returns verb[8], castTarget ability[4] and 7 target rows × 25 for the paused frame.
    • attackTarget: the engine's isEnemyTarget.
    • castTarget: per ability, the faction, alive and visible rules, or self-cast.
    • castPoint / useItemAt: an occupied anchor slot.
  • Package key. "decoder": {"mask_empty_targets": true, "mask_mode": "conditional"|"static"}. Default off.
    • conditional: verb, then ability, then target by the (verb, ability) row. 0 invalid.
    • static: independent per-head masks, for PufferLib.
    • Sampling draws one uniform per head in head order first, so RNG use is unchanged.
  • Proofs (tools/test_mask.py, job 16281, full length):
    • ABI mask == host mask 20/20 seeds (134,257 decisions): mask bytes, heads and hashes all match.
    • Random w128 net: conditional 0 invalid in 10/10 games; static 283 over 10 games; unmasked 17,978 over 20 games.
    • Option-absent parity vs the pre-mask lib: 10/10. Defer proofs rerun: a 20/20, b 10/10.
    • CI list ALLPASS at ba8c255.

Open items / caveats

  • Thread scaling is about 5x at 16 threads, against about 8x for the same box with processes. atomicArc refcount traffic on the shared map refs is the likely cause.
  • Contract exactness is partial:
    • attackTarget is exact 64% of the time; the rest become attack-moves.
    • walk and castPoint are approximate, with a median point error of 1.1 tiles.
    • The ceiling still holds.
  • Not upstreamed yet. This is a draft against the fork's main, which equals upstream/main 23f3384.

🤖 Generated with Claude Code

treeform and others added 30 commits September 24, 2026 10:54
Resolve nimby.lock / coworld/dependencies.lock to bassy 669a7c4 (a
descendant of Metta-AI#72's 2c54d82, so it keeps DATA arrays + metered callbacks
and adds string restarts), and keep both Metta-AI#72's addNeuralFunctions() and
main's mailbox host functions in every bots.nim.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Header first so the training lane can bind against it; the Nim
implementation follows in native_env.nim.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e seats

- sim.nim: split tickWorld into tickWorldBegin/tickWorldFinish (pure
  refactor), planRoute (side-effect-free route probe), HeroVm.neural, and
  training-only counters behind -d:gotaTrainingStats.
- bots.nim: contract host procs routed through issue* procs (same
  recording/metrics order) plus interception hooks for neural seats;
  neural package seats (ZIP) in loadBots.
- neural_contract.nim: observation v1 (1407 floats), action v1 decoder
  and demonstration encoder, SHA-256 pinned contract texts.
- neural_actor.nim: GOTANET1 = PWNET001 layout, MinGRU FP32 actor.
- neural_package.nim: strict gota-neural-basic/1 package parser.
- native_env.nim + tools/native_env.py: the native_env.h ABI.
- neural/policy.bas: glue (draft/shop/level/buyback from base.bas).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ipted seats

- Per-world scratch (sim vision/path/walk scratch, pathing A* scratch,
  bots activeGame) is thread-local; map/lane/sight globals are built once
  per map; the edge cache is warmed before threads search; hero data
  slots bind once. Single-threaded behaviour is unchanged.
- native env: map template + process lock for create/reset; documented
  build --mm:atomicArc --threads:on -d:useMalloc (destroy from any thread).
- tools/test_native_concurrency.py: 8 handles x 4 threads x 200 steps,
  handles migrating between threads, == serial hashes/obs/rewards/labels.
- gota_seat_orders reports the previous window (lastLabel); it was being
  wiped by the next decision's frame (train lane bug report).
- tools/mapping_ceiling.py.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… tests

- gota_set_seat_shadow: a learner seat's expert script runs on the same
  frames with every world-changing host call absorbed; its contract
  commands become gota_seat_orders labels.
- coworld/gota/runtime/neural_package.py: staging validator + builder
  mirroring neural_package.nim (same rejections, tested).
- tools/test_native_env.py: label coverage, capture identity, shadow
  no-op, goals-before-reset, reward == delta score, package-vs-ABI parity
  for w64/w128/w256, validator agreement.
- native_env.h: document shadow, record/capture, save_replay, last_error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…metry in native env

A paused native world could finish its tick with the navigation world of
another handle stepped meanwhile on the same thread (worlds drifted apart
after ~1500 decisions). tickWorldFinish now rebinds it (a no-op for the
unsplit tickWorld). Native env samples replay metrics per tick so
gota_save_replay works; tools/parity_upstream.sh (item 5a).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…), proofs tool

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… sampled instruction peak

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rity beyond sampled matches)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…pty_targets)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…k_mode), proofs tool

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ead-safe gota_create, isolation tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…l seats get the plain per-tick BASIC budget; metrics use each seat's own limit; lastError thread-local doc; goal parity test

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…on RNG by kind)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…serve (hosted order; fixes native != hosted package play); replay_diff tool; goal/hosted parity test covers default goals

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ats saw maxTicks 0: time features and telemetry wrong)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rWidth; validator, tests, docs)

ActorWidths = [64, 128, 256, 384, 512]. w384 = 2,048,256 ops/inference, w512 = 3,124,224, both
under the 4,000,000/tick budget and the 2,000,000-parameter cap. infer's stack buffers are sized
from MaxActorWidth (static-asserted = the largest width), so w64-w256 compute is unchanged.
neural_package.py WIDTHS matches. test_native_env.py: package parity at all five widths and a
new width test (both loaders accept exactly the five widths, reject the rest).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants