3142b70aa5
The default rack is now Pattern-only (31c7c01), but membership filters SELECTION,
not SPAWNING - so all 13 unselected guns still ran `predict` + `spawnBullets`
every tick to feed fitness tables nobody reads. Measured waste: Tsetlin alone
0.98 ms/tick, KNN 0.30, plus 10 more. This generalises the gate TMPATTERN already
had to every gun, behind `TR_VBULLET_ADMIT_ONLY` (default 1 = gate, 0 = old).
OFFLINE COST (release build, 400 ticks, min of 2 reps, 13-gun rack):
OFF 2.14 ms/tick (implied 467 ticks/s)
ON 0.04 ms/tick (implied 27667 ticks/s)
-> reclaimed 2.10 ms/tick, ~98% of the virtual-bullet cost. Tsetlin's 0.98
disappears, KNN's 0.30 disappears, only Pattern (0.017) survives.
Note the absolute scale is lower than an earlier unoptimised measurement (~6.1
ms/t) because this is a -d:release build; the ON-vs-OFF DELTA is the robust result.
LIVE TICK RATE (ONE frozen binary, 4 runs/arm x 4 rounds, all 8 concurrent, gate
varied by env only):
ON 146.2 ticks/s (142.3, 145.2, 147.9, 149.4)
OFF 86.9 ticks/s ( 95.0, 41.4, 99.1, 112.1)
NO OVERLAP: ON min 142.3 > OFF max 112.1. Excluding a game-outcome outlier in
the OFF arm, OFF max is still 112.1. Outside the noise.
**+68% tick rate**, which also makes every future A/B faster. Both arms still pay
the fixed 8192-slot ring scan in tickBullets.
SAFETY, verified not assumed: Pattern is admitted under the shipped rack, so its
own fitness keeps accumulating and the SELECTED gun is unchanged - Pattern 100% in
all 4 runs both arms, and Pattern was the ONLY gun with vShots>0 in the ON arm
while all 13 had vShots>0 in the OFF arm. So admission is the correct predicate.
Mid-round transitions are safe by construction: only predict/spawn are gated, while
tickBullets still resolves every active bullet and the feedback case still calls
the owning gun's onResult.
A TOOLING BUG THIS CAUGHT, and a correction to the task's assumption:
`acceptance_offline_vs_online.nim` IS affected (I had assumed it was not). It
compares the live per-gun vShots against an offline replay that always spawns all
guns, so under the default gate the online non-Pattern vShots are 0 while offline
is ~400 - a guaranteed mismatch. Fixed by pinning `TR_VBULLET_ADMIT_ONLY=0` inside
that test (same putEnv/defer pattern as TR_RECORD_WORLDSTATE), keeping 12/12. The
test is about offline/online METRIC parity, so it needs every gun spawning.
Unaffected (verified from source): audit_virtual_guns.nim,
measure_cornering_guns.nim, sweep_tm_pattern.nim - all offline, none read
GUN_STATS_PATH.
Guards: test_vbullet_admit_gate 12 (new, pure), test_gun_harness 39,
test_vbullet_metric 11, test_power_selection 3, test_adaptive_radar 41,
test_tfil_ring_weights 24, test_power_policy 26, test_ram_decision 28,
test_rack_membership 48, test_selector_tiebreak 19, test_tm_pattern_registration 20,
test_tm_pattern_learning 3, acceptance_offline_vs_online 12/12. ModularBot compiles.
209 lines
8.4 KiB
Nim
209 lines
8.4 KiB
Nim
## MEASUREMENT ONLY — per-tick cost of the virtual-bullet SPAWN gate
|
|
## (`TR_VBULLET_ADMIT_ONLY`), on the REAL 14-gun rack over recorded DrussGT
|
|
## movement.
|
|
##
|
|
## Gating SKIPS both `predict()` and `spawnBullets()` for a gun the current rack
|
|
## does not admit (shipped default rack = onlyPattern). This tool drives the same
|
|
## real guns + real `VirtualTracker` as `measure_parallel_vbullet_cost.nim`, but
|
|
## reports the ONE comparison the live bot cares about:
|
|
##
|
|
## OFF = pre-change: every non-TM gun predicts+spawns (ids 0..12)
|
|
## ON = shipped: only the rack-admitted guns predict+spawn (Pattern, id 5)
|
|
##
|
|
## Both arms run the SAME 13-gun rack (ids 0..12; TMSelect is disabled in the
|
|
## live bot via `EnableTmSelector=false`, TMPATTERN id 14 is separately gated).
|
|
## The gate predicate is the SHIPPED `vb.vBulletAdmitted` against the SHIPPED
|
|
## `DefaultRackMembership`, so this measures the real rule.
|
|
##
|
|
## Measured directly (not derived): predict+spawn wall time and the full
|
|
## `tickBullets` (resolve + onResult) wall time, per tick, for each arm. A
|
|
## per-gun breakdown names which costs disappear.
|
|
##
|
|
## Usage:
|
|
## nim c -r common_libs/tests/measure_vbullet_admit_gate.nim
|
|
|
|
import std/[os, strformat, times, tables, math, monotimes, algorithm, random]
|
|
import gun_harness/offline_range
|
|
import gun_harness/gun_interface
|
|
import gun_harness/virtual_bullets as vb
|
|
import gun_harness/selector
|
|
import range_guns
|
|
|
|
const
|
|
NGun = 14
|
|
NBins = len(vb.PowerBins)
|
|
TmGun = 13 ## TMSelect — disabled in the live bot
|
|
RepoRoot = currentSourcePath().parentDir.parentDir.parentDir
|
|
FixturesDir = RepoRoot / "tools" / "fixtures"
|
|
FixtureNames = [
|
|
"tr_drussgt_vs_corners.jsonl",
|
|
"tr_drussgt_vs_crazy.jsonl",
|
|
"tr_drussgt_vs_modularbot.jsonl",
|
|
"tr_drussgt_vs_modularbot_shield.jsonl",
|
|
"tr_drussgt_vs_spinbot.jsonl",
|
|
"drussgt_vs_ramfire.jsonl",
|
|
"drussgt_vs_drussgt.jsonl",
|
|
]
|
|
MaxTicks = 2000
|
|
Warmup = 150
|
|
Measure = 400
|
|
|
|
const GunNames = ["HeadOn", "Linear", "Tsetlin", "Circular", "GuessFactor",
|
|
"Pattern", "WallBounce", "Accel", "StopShot", "Displace", "AvgLead",
|
|
"DecayGF", "KNN", "TMSelect"]
|
|
|
|
proc loadFixtures(): seq[Fixture] =
|
|
for n in FixtureNames:
|
|
let p = FixturesDir / n
|
|
if fileExists(p): result.add loadFixture(p)
|
|
|
|
proc buildStates(fx: seq[Fixture], maxTicks: int): seq[seq[WorldState]] =
|
|
var ticks = maxTicks
|
|
for f in fx: ticks = min(ticks, f.states.len)
|
|
result = newSeq[seq[WorldState]](fx.len)
|
|
for k in 0..<fx.len:
|
|
result[k] = newSeq[WorldState](ticks)
|
|
for si in 0..<ticks:
|
|
var ws = fx[k].states[si]
|
|
let s0 = fx[0].states[si]
|
|
ws.selfX = s0.selfX; ws.selfY = s0.selfY
|
|
ws.selfHeading = s0.selfHeading; ws.selfSpeed = s0.selfSpeed
|
|
ws.selfRadarHeading = s0.selfRadarHeading; ws.selfEnergy = s0.selfEnergy
|
|
ws.tick = si
|
|
result[k][si] = ws
|
|
|
|
proc enemyTable(states: seq[seq[WorldState]], si: int):
|
|
Table[int, tuple[x, y: float, lastSeenTick: int, alive: bool]] =
|
|
for k in 0..<states.len:
|
|
let e = states[k][si]
|
|
result[k + 1] = (x: e.enemyX, y: e.enemyY, lastSeenTick: si, alive: true)
|
|
|
|
type
|
|
ArmResult = object
|
|
ticks: int
|
|
psNs: int64 ## predict+spawn wall time
|
|
resNs: int64 ## full tickBullets (resolve + onResult) wall time
|
|
spawns: int
|
|
|
|
proc runArm(states: seq[seq[WorldState]], gateOn: bool): ArmResult =
|
|
## Drive the real 13-gun live rack (ids 0..12) with the shipped gate.
|
|
let rack = buildAllGunDrivers(seed = 1, enableTmSelector = false)
|
|
var tracker = vb.initTracker(NGun, bmPath)
|
|
let ticks = states[0].len
|
|
for si in 0..<ticks:
|
|
let ws = states[0][si]
|
|
let measuring = si >= Warmup and si < Warmup + Measure
|
|
let t0 = getMonoTime()
|
|
for gi in 0..<NGun:
|
|
if gi == TmGun: continue # disabled in the live bot
|
|
# The SHIPPED gate predicate against the SHIPPED rack.
|
|
if not vb.vBulletAdmitted(gi, rm1v1, DefaultRackMembership, gateOn):
|
|
continue
|
|
var preds: array[NBins, GunPrediction]
|
|
for b in 0..<NBins:
|
|
preds[b] = rack[gi].predictCb(ws, bulletSpeed(vb.PowerBins[b]))
|
|
tracker.spawnBullets(gi, preds, ws, 1)
|
|
if measuring: inc result.spawns, NBins
|
|
let t1 = getMonoTime()
|
|
let et = enemyTable(states, si)
|
|
tracker.tickBullets(ws, et,
|
|
proc(gunId: GunId, binIdx: int, e: FeedbackEvent) =
|
|
rack[gunId].resultCb(e))
|
|
let t2 = getMonoTime()
|
|
if measuring:
|
|
inc result.ticks
|
|
result.psNs += (t1 - t0).inNanoseconds
|
|
result.resNs += (t2 - t1).inNanoseconds
|
|
|
|
proc perGunCosts(states: seq[seq[WorldState]]):
|
|
tuple[pred: array[NGun, float], onRes: array[NGun, float]] =
|
|
## Per-gun predict+spawn ns/tick and onResult ns/tick (always FULL rack).
|
|
let rack = buildAllGunDrivers(seed = 1, enableTmSelector = false)
|
|
var tracker = vb.initTracker(NGun, bmPath)
|
|
var counts: array[NGun, int]
|
|
var onCalls: array[NGun, int]
|
|
var acc: tuple[pred: array[NGun, float], onRes: array[NGun, float]]
|
|
let ticks = states[0].len
|
|
for si in 0..<ticks:
|
|
let ws = states[0][si]
|
|
let measuring = si >= Warmup and si < Warmup + Measure
|
|
for gi in 0..<NGun:
|
|
if gi == TmGun: continue
|
|
let t0 = getMonoTime()
|
|
var preds: array[NBins, GunPrediction]
|
|
for b in 0..<NBins:
|
|
preds[b] = rack[gi].predictCb(ws, bulletSpeed(vb.PowerBins[b]))
|
|
tracker.spawnBullets(gi, preds, ws, 1)
|
|
let dt = (getMonoTime() - t0).inNanoseconds
|
|
if measuring:
|
|
acc.pred[gi] += dt.float
|
|
inc counts[gi]
|
|
let et = enemyTable(states, si)
|
|
tracker.tickBullets(ws, et,
|
|
proc(gunId: GunId, binIdx: int, e: FeedbackEvent) =
|
|
let t0 = getMonoTime()
|
|
rack[gunId].resultCb(e)
|
|
let dt = (getMonoTime() - t0).inNanoseconds
|
|
if measuring:
|
|
acc.onRes[gunId] += dt.float
|
|
inc onCalls[gunId])
|
|
for gi in 0..<NGun:
|
|
if counts[gi] > 0: acc.pred[gi] /= counts[gi].float
|
|
if onCalls[gi] > 0: acc.onRes[gi] /= onCalls[gi].float
|
|
result = acc
|
|
|
|
proc main() =
|
|
randomize(1)
|
|
let fx = loadFixtures()
|
|
let states = buildStates(fx, MaxTicks)
|
|
echo fmt"fixtures={fx.len} ticks={states[0].len} warmup={Warmup} measured={Measure}"
|
|
echo "shipped rack: onlyPattern (DefaultRackMembership); predicate vb.vBulletAdmitted"
|
|
|
|
# Two reps, take the min (least noise).
|
|
var offPs, offRes, onPs, onRes: int64
|
|
var offSpawns, onSpawns, ticksRun: int
|
|
for rep in 0..<2:
|
|
let off = runArm(states, gateOn = false)
|
|
let on = runArm(states, gateOn = true)
|
|
if rep == 0:
|
|
offPs = off.psNs; offRes = off.resNs; offSpawns = off.spawns
|
|
onPs = on.psNs; onRes = on.resNs; onSpawns = on.spawns
|
|
ticksRun = off.ticks
|
|
else:
|
|
offPs = min(offPs, off.psNs); offRes = min(offRes, off.resNs)
|
|
onPs = min(onPs, on.psNs); onRes = min(onRes, on.resNs)
|
|
offSpawns = min(offSpawns, off.spawns); onSpawns = min(onSpawns, on.spawns)
|
|
|
|
let n = max(1, ticksRun).float
|
|
let offPSms = offPs.float / 1.0e6 / n # ms/tick
|
|
let offRSms = offRes.float / 1.0e6 / n
|
|
let onPSms = onPs.float / 1.0e6 / n
|
|
let onRSms = onRes.float / 1.0e6 / n
|
|
let offTot = offPSms + offRSms
|
|
let onTot = onPSms + onRSms
|
|
|
|
echo ""
|
|
echo "=================== PER-TICK COST: gate ON vs OFF (13-gun live rack) ==================="
|
|
echo "arm predict+spawn ms/t resolve+onResult ms/t total ms/t ticks/s spawns/t"
|
|
echo fmt"OFF {offPSms:>20.2f} {offRSms:>22.2f} {offTot:>12.2f} {1000.0/offTot:>7.0f} {offSpawns.float/n:>10.1f}"
|
|
echo fmt"ON {onPSms:>20.2f} {onRSms:>22.2f} {onTot:>12.2f} {1000.0/onTot:>7.0f} {onSpawns.float/n:>10.1f}"
|
|
echo fmt"reclaimed = {offTot - onTot:.2f} ms/tick ({100.0*(offTot-onTot)/offTot:.0f}% of the gate-OFF total)"
|
|
echo fmt"implied ticks/s: OFF {1000.0/offTot:.0f} -> ON {1000.0/onTot:.0f} (x{offTot/onTot:.2f})"
|
|
echo "(note: tickBullets also scans the fixed 8192-slot ring each tick, present in BOTH arms)"
|
|
|
|
let pg = perGunCosts(states)
|
|
echo ""
|
|
echo "=================== PER-GUN COST (full rack, names what disappears) ==================="
|
|
echo "gun predict+spawn ms/t onResult ms/t gateON"
|
|
var sumPred, sumOn: float
|
|
for gi in 0..<NGun:
|
|
if gi == TmGun: continue
|
|
sumPred += pg.pred[gi]; sumOn += pg.onRes[gi]
|
|
let admitted = vb.vBulletAdmitted(gi, rm1v1, DefaultRackMembership, true)
|
|
echo fmt"{GunNames[gi]:<12}{pg.pred[gi]/1.0e6:>24.3f}{pg.onRes[gi]/1.0e6:>18.3f}{($admitted):>10}"
|
|
echo fmt"TOTAL {sumPred/1.0e6:>24.3f}{sumOn/1.0e6:>18.3f}"
|
|
echo ""
|
|
|
|
when isMainModule:
|
|
main()
|