DraftZero FDN graph network (full_r1, best_policy_calibrated): weights, vocab, loader, goldens
Browse files- README.md +158 -0
- SHA256SUMS +9 -0
- check.json +9 -0
- config.json +96 -0
- coverage.json +34 -0
- gnn_release.py +147 -0
- goldens.jsonl.gz +3 -0
- graph_net.py +344 -0
- model.safetensors +3 -0
- vocab.json +1 -0
README.md
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: cc-by-4.0
|
| 3 |
+
tags:
|
| 4 |
+
- magic-the-gathering
|
| 5 |
+
- game-playing
|
| 6 |
+
- graph-neural-network
|
| 7 |
+
- xmage
|
| 8 |
+
---
|
| 9 |
+
|
| 10 |
+
# DraftZero FDN graph network
|
| 11 |
+
|
| 12 |
+
A graph neural network that plays Magic: The Gathering Limited with Foundations (FDN) decks, trained by imitating
|
| 13 |
+
17lands' top players. It is [DraftZero](https://github.com/danieljbrooks/draft-zero)'s strongest network on the
|
| 14 |
+
XMage engine, built on Will Wroble's [MageZero](https://github.com/WillWroble/MageZero) graph encoder and network.
|
| 15 |
+
|
| 16 |
+
- **What it does:** given the acting player's view of an XMage game as a graph, it scores every legal option
|
| 17 |
+
(*policy*) and estimates the chance of winning (*value*). A tree search can use both, or a bot can play the
|
| 18 |
+
policy's top choice directly.
|
| 19 |
+
- **What it saw in training:** 10.9 million decisions from 145,898 FDN Premier Draft games by players in 17lands'
|
| 20 |
+
60%-and-up win-rate group, replayed inside XMage. The opponent's hand is not part of its input.
|
| 21 |
+
- **Size:** 13.4M parameters (width 256), 1,932 leaf features, 22 edge labels. Float32 weights, 54 MB.
|
| 22 |
+
- **Source:** `gnn/full_r1/best_policy_calibrated` in DraftZero's runs
|
| 23 |
+
([docs/024](https://github.com/danieljbrooks/draft-zero/blob/main/docs/024-gnn-local-layers-and-games.md)).
|
| 24 |
+
|
| 25 |
+
## How strong it is
|
| 26 |
+
|
| 27 |
+
Its bot played MageZero's heuristic bot, which searches 100 simulations a decision. A search at N *simulations*
|
| 28 |
+
plays N short look-ahead games before each decision. Both bots used PIMC search: each decision deals the hidden
|
| 29 |
+
cards once into a world that fits what the bot has seen, guessing the opponent's deck from real 17lands decks, and
|
| 30 |
+
runs MageZero's tree search on it. Every deck pair was played twice with the seats swapped (paired games), which
|
| 31 |
+
lowers the variance.
|
| 32 |
+
|
| 33 |
+
| Network's simulations a decision | Won | Time a decision |
|
| 34 |
+
|---|---|---|
|
| 35 |
+
| 0: the policy's top choice | 44.7% of 103 games | ~0.2 s |
|
| 36 |
+
| 100 | 62.5% of 104 | ~7 s |
|
| 37 |
+
| 300 | 69.2% of 104 | ~20 s |
|
| 38 |
+
| 1,000 | **69.3%** of 101 | 40-65 s |
|
| 39 |
+
|
| 40 |
+
Times are from those games: up to 30 games at once on one rented RTX 3090 machine, the network served in
|
| 41 |
+
bfloat16, most of the time spent in XMage rather than in the network. On one CPU thread of a laptop, one network
|
| 42 |
+
call takes about 20 ms (batch 1, median 226 nodes a state). On held-out test games, at decisions where the human
|
| 43 |
+
made a play rather than passing, the policy's top choice was a play the human made 85.9% of the time.
|
| 44 |
+
|
| 45 |
+
## Files
|
| 46 |
+
|
| 47 |
+
| File | What |
|
| 48 |
+
|---|---|
|
| 49 |
+
| `model.safetensors` | the weights, float32 |
|
| 50 |
+
| `config.json` | the architecture, the encoder's constants (node types, value buckets, hash) and the source pins |
|
| 51 |
+
| `vocab.json` | `leaf_ids` and `edge_ids`: the raw feature ids that have embedding rows, in row order |
|
| 52 |
+
| `graph_net.py` | the network (MageZero's `NetGraph`), as DraftZero trains and serves it |
|
| 53 |
+
| `gnn_release.py` | a loader with no pickles: weights, vocab, state -> network input, option logits, the golden check |
|
| 54 |
+
| `goldens.jsonl.gz` | 300 encoded test states with this network's outputs (below) |
|
| 55 |
+
| `coverage.json` | how much of the test data the vocab covers, a baseline for checking an encoder |
|
| 56 |
+
| `check.json` | `gnn_release.py check` on this release |
|
| 57 |
+
| `SHA256SUMS` | the files' hashes |
|
| 58 |
+
|
| 59 |
+
Requirements: Python 3.10+, `torch` (2.1 or later), `numpy`, `safetensors`.
|
| 60 |
+
|
| 61 |
+
```python
|
| 62 |
+
import gnn_release as r
|
| 63 |
+
|
| 64 |
+
model, leaves, edges, config = r.load(".")
|
| 65 |
+
goldens = r.read_goldens("goldens.jsonl.gz")
|
| 66 |
+
g = goldens[0]
|
| 67 |
+
out = r.evaluate(model, [g["state"]], leaves, edges)[0]
|
| 68 |
+
print(r.option_logits(out, g["graph_type"], g["options"]), out["value"])
|
| 69 |
+
```
|
| 70 |
+
|
| 71 |
+
## How it plays: what a port needs
|
| 72 |
+
|
| 73 |
+
**1. The encoder.** The input is MageZero's state graph: typed nodes (ROOT, PLAYER, ZONE, STACK_OBJECT, PERMANENT,
|
| 74 |
+
CARD, ABILITY) with LEAF children, and edges from child to parent with a label. Leaves are XMage strings: card names,
|
| 75 |
+
type and subtype names, `Ability.toString()` rules text, decision text, step names. Each string becomes a feature id,
|
| 76 |
+
`StateEncoder.indexFor(StateEncoder.hash64(name))` (`graph_net.feature_id` in Python). Some leaves also carry a
|
| 77 |
+
number (power, toughness, counters, life). The encoder is Java, at
|
| 78 |
+
`WillWroble/mage@e4afc9c77ba7e4a6dbc24966cbdc6dc0819e0bff` (branch `graph-encoder`,
|
| 79 |
+
`Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/encoder`). DraftZero runs a vendored copy in
|
| 80 |
+
`java/mzbridge/src/org/draftzero/mzbridge/graph` on XMage `danieljbrooks/mage@48e49184` (branch
|
| 81 |
+
`v0.2-generalist`). It encodes the acting player's view, with the opponent's hand hidden (`perfectInfo` false).
|
| 82 |
+
|
| 83 |
+
A leaf outside the vocab has no embedding: its edges are dropped silently, without an error. So an encoder running on
|
| 84 |
+
another XMage version whose strings differ (rules text, decision text) plays worse without failing. To catch this,
|
| 85 |
+
count how many leaf occurrences hit `vocab.json`. On DraftZero's test data the vocab covers more than 99.99% of
|
| 86 |
+
them in priority, attack and target decisions, and 99.3% in block decisions (`coverage.json`).
|
| 87 |
+
|
| 88 |
+
**2. Decisions and options.** `graph_type` is MageZero's `ActionType` ordinal. Each legal option is a list of graph
|
| 89 |
+
nodes, and its logit is the log-sum-exp of its nodes' scores under the head the decision reads:
|
| 90 |
+
|
| 91 |
+
| Decision | Head | An option's nodes |
|
| 92 |
+
|---|---|---|
|
| 93 |
+
| Priority (`graph_type` 0) | `priority` | the ability's node, one per copy of a card (two copies of a card are one option); Pass has its own node |
|
| 94 |
+
| Target, including blocks (3) | `target` | the target's node (CARD, PERMANENT, STACK_OBJECT or PLAYER); Stop Choosing is a CARD node |
|
| 95 |
+
| Attack with a creature (3) | `target` | no = Stop Choosing's node, yes = the defending player's node |
|
| 96 |
+
| Other yes/no (5) | `use` | [no, yes] logits, no nodes |
|
| 97 |
+
|
| 98 |
+
Mulligans, trigger ordering, damage assignment and similar choices have no head. In DraftZero's games mulligans were
|
| 99 |
+
off (every opening hand was kept), and MageZero's player made the other choices without the network's policy.
|
| 100 |
+
|
| 101 |
+
**3. The value.** `value = tanh(value_x)` is the acting player's expected result in -1..1, and
|
| 102 |
+
`P(win) = sigmoid(2 * value_x)`. A temperature fitted on validation games (T = 1.607) is already folded into the value
|
| 103 |
+
head's last layer.
|
| 104 |
+
|
| 105 |
+
**4. The search in DraftZero's games.** This is MageZero's tree search, run fresh at every decision with no tree
|
| 106 |
+
reuse, on one sampled world (PIMC):
|
| 107 |
+
|
| 108 |
+
- selection by PUCT with c = 1;
|
| 109 |
+
- priors from the policy: softmax of the option logits at temperature 1.5, then 0.1 added to every option that
|
| 110 |
+
isn't Pass or a mana ability (not renormalized: MageZero's `setPriors`);
|
| 111 |
+
- uniform priors at the opponent's decisions;
|
| 112 |
+
- leaves scored by the value head, discounted by 0.99 per ply;
|
| 113 |
+
- the move with the most visits is played.
|
| 114 |
+
|
| 115 |
+
The code is `java/mzbridge/src/mage/player/ai` (`BenchPlayer`, `BenchSearch`, `GraphNet`, `GraphMCTSPlayer`) at
|
| 116 |
+
`danieljbrooks/draft-zero@2a461518`. Playing the policy's top choice alone needs no search and no hidden-card sampling.
|
| 117 |
+
|
| 118 |
+
## Checking a port with the goldens
|
| 119 |
+
|
| 120 |
+
`goldens.jsonl.gz` holds 300 decisions from the test split: 160 priority, 50 attack, 50 target and 40 block
|
| 121 |
+
decisions. They come from games the network never trained on, encoded by the Java encoder above. Each line has:
|
| 122 |
+
|
| 123 |
+
- `state`: the encoder's output in MageZero's graph-server layout: `indices` (raw feature id per node), `values`,
|
| 124 |
+
`edge_child`, `edge_parent`, `edge_label`;
|
| 125 |
+
- `graph_type`, `options` (node indices per legal option), `option_is_pass`, `human_choice` (the option the
|
| 126 |
+
17lands player took, for interest);
|
| 127 |
+
- `expected`: `option_logits`, `use`, `value` and `value_x`.
|
| 128 |
+
|
| 129 |
+
The expected outputs come from DraftZero's games inference server (`tools/imitation_scale/graph_server.py`, loading
|
| 130 |
+
the original checkpoint) on CPU in float32, one state at a time. `python gnn_release.py check .` loads this release
|
| 131 |
+
and compares. A faithful port agrees to about 1e-5 and picks the same top option in all 300 (`check.json`).
|
| 132 |
+
|
| 133 |
+
The goldens check the network, its weights and the option mapping. They don't check an encoder, because states can't
|
| 134 |
+
move between XMage versions as is; for that, use the vocab coverage above.
|
| 135 |
+
|
| 136 |
+
## Limits
|
| 137 |
+
|
| 138 |
+
- **Imitation only.** It learned from top players' moves and game results and has not trained by self-play yet.
|
| 139 |
+
- **Measured against one opponent:** MageZero's heuristic bot, with PIMC and guessed decks. There is no Elo or
|
| 140 |
+
human-play result yet.
|
| 141 |
+
- **XMage-specific input.** The features are XMage's own strings, so other engines need either XMage as the encoder
|
| 142 |
+
or a re-implementation that reproduces those strings exactly.
|
| 143 |
+
- **Search is not optional for strength.** The policy alone lost to the heuristic bot (44.7%); 100 simulations or
|
| 144 |
+
more are needed to beat it.
|
| 145 |
+
|
| 146 |
+
## Sources
|
| 147 |
+
|
| 148 |
+
| What | Where |
|
| 149 |
+
|---|---|
|
| 150 |
+
| Network | `danieljbrooks/draft-zero@2a461518985e3b2bb393bddac4649e217f8fddfe`, `src/draftzero/gameplay/graph_net.py`, a copy of MageZero's `NetGraph` (`WillWroble/MageZero@de225045`, branch `graph-encoder`) |
|
| 151 |
+
| Encoder | `WillWroble/mage@e4afc9c77ba7e4a6dbc24966cbdc6dc0819e0bff` (branch `graph-encoder`) |
|
| 152 |
+
| Engine | `danieljbrooks/mage@48e4918413392523a5f9d7915e1f33941ddea21e` (branch `v0.2-generalist`: DraftZero's FDN changes on MageZero v0.2) |
|
| 153 |
+
| Training | [docs/023](https://github.com/danieljbrooks/draft-zero/blob/main/docs/023-gnn-run-log.md) and [docs/024](https://github.com/danieljbrooks/draft-zero/blob/main/docs/024-gnn-local-layers-and-games.md) |
|
| 154 |
+
| Data | [17lands public game data](https://www.17lands.com/public_datasets), FDN Premier Draft, CC BY 4.0 |
|
| 155 |
+
|
| 156 |
+
The weights are released under CC BY 4.0, like the 17lands data they were trained on. The code (`graph_net.py`,
|
| 157 |
+
`gnn_release.py`) is MIT, as are DraftZero and MageZero. The network and encoder are Will Wroble's MageZero design;
|
| 158 |
+
DraftZero trained these weights.
|
SHA256SUMS
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
102d27b0433b0ab194dc98e012e66bfe02bdee2534385b76f317da12f8613973 README.md
|
| 2 |
+
ff5182a65c337210305ffb689263b9a8e773c2aafad68b6cfb211232f0122e05 check.json
|
| 3 |
+
6b48d3de2bc4c40b7b2f68d52823a276ec777a68908c03ab11a58206c2d77b10 config.json
|
| 4 |
+
4e57a5c497b0aca0890f3327da9ab09cf00b51ffdaec4e6eeb4b0226dcd07374 coverage.json
|
| 5 |
+
c4cfa96626fd731022f5b37e70db473bb56a4cd52712e981b96fbb4825793189 gnn_release.py
|
| 6 |
+
46d5949bb97528a1e0ea02ae255c22287fcc49d84a77feef778f2e1eb08318c8 goldens.jsonl.gz
|
| 7 |
+
8e27fc756f99460ce17418142e2243926400cbea3a72ae3d0801d9726d1cddce graph_net.py
|
| 8 |
+
d5ee8323ccca11f18b1667bc2312407d9d5696b6dacf7243d9a30461cdc29b56 model.safetensors
|
| 9 |
+
9b8ecf250963bbdbb4c18560d037e3ed547cbb154275188ac363bbcf07f0127d vocab.json
|
check.json
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"states": 300,
|
| 3 |
+
"max_abs_diff": {
|
| 4 |
+
"option_logits": 4.76837158203125e-06,
|
| 5 |
+
"value_x": 8.344650268554688e-07,
|
| 6 |
+
"use": 2.682209014892578e-07
|
| 7 |
+
},
|
| 8 |
+
"argmax_agree": 300
|
| 9 |
+
}
|
config.json
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "draftzero-fdn-gnn-full-r1",
|
| 3 |
+
"arch": {
|
| 4 |
+
"type": "graph",
|
| 5 |
+
"d_model": 256,
|
| 6 |
+
"heads": 4,
|
| 7 |
+
"ff": 1024,
|
| 8 |
+
"dropout": 0.1,
|
| 9 |
+
"passes": 2,
|
| 10 |
+
"global_layers": 2,
|
| 11 |
+
"head_hidden": 256,
|
| 12 |
+
"leaf_dropout": 0.0,
|
| 13 |
+
"local_depth": 1
|
| 14 |
+
},
|
| 15 |
+
"num_leaves": 1932,
|
| 16 |
+
"num_edge_labels": 22,
|
| 17 |
+
"parameters": 13412101,
|
| 18 |
+
"node_types": {
|
| 19 |
+
"ROOT": 0,
|
| 20 |
+
"PLAYER": 1,
|
| 21 |
+
"ZONE": 2,
|
| 22 |
+
"STACK_OBJECT": 3,
|
| 23 |
+
"PERMANENT": 4,
|
| 24 |
+
"CARD": 5,
|
| 25 |
+
"ABILITY": 6,
|
| 26 |
+
"LEAF": 7
|
| 27 |
+
},
|
| 28 |
+
"type_node_ids": {
|
| 29 |
+
"ROOT": 0,
|
| 30 |
+
"PLAYER": 499558809,
|
| 31 |
+
"ZONE": 1485202092,
|
| 32 |
+
"STACK_OBJECT": 191223077,
|
| 33 |
+
"PERMANENT": 11906542,
|
| 34 |
+
"CARD": 587455872,
|
| 35 |
+
"ABILITY": 1229966541
|
| 36 |
+
},
|
| 37 |
+
"value_bounds": [
|
| 38 |
+
-5,
|
| 39 |
+
-4,
|
| 40 |
+
-3,
|
| 41 |
+
-2,
|
| 42 |
+
-1,
|
| 43 |
+
0,
|
| 44 |
+
1,
|
| 45 |
+
2,
|
| 46 |
+
3,
|
| 47 |
+
4,
|
| 48 |
+
5,
|
| 49 |
+
6,
|
| 50 |
+
7,
|
| 51 |
+
8,
|
| 52 |
+
9,
|
| 53 |
+
10,
|
| 54 |
+
11,
|
| 55 |
+
12,
|
| 56 |
+
13,
|
| 57 |
+
14,
|
| 58 |
+
15,
|
| 59 |
+
16,
|
| 60 |
+
17,
|
| 61 |
+
18,
|
| 62 |
+
19,
|
| 63 |
+
20,
|
| 64 |
+
21,
|
| 65 |
+
26,
|
| 66 |
+
31,
|
| 67 |
+
41,
|
| 68 |
+
61
|
| 69 |
+
],
|
| 70 |
+
"feature_hash": {
|
| 71 |
+
"algorithm": "xmage_feature_hash",
|
| 72 |
+
"version": 2,
|
| 73 |
+
"bins": 2147483647,
|
| 74 |
+
"python": "graph_net.feature_id(name)",
|
| 75 |
+
"java": "StateEncoder.indexFor(StateEncoder.hash64(name))"
|
| 76 |
+
},
|
| 77 |
+
"graph_types": {
|
| 78 |
+
"PRIORITY": 0,
|
| 79 |
+
"CHOOSE_TARGET": 3,
|
| 80 |
+
"CHOOSE_USE": 5
|
| 81 |
+
},
|
| 82 |
+
"heads": {
|
| 83 |
+
"priority": "per node, read at ABILITY nodes (a play is its ability's node; Pass has one)",
|
| 84 |
+
"target": "per node, read at CARD / PERMANENT / STACK_OBJECT / PLAYER nodes (Stop Choosing is a CARD); 'attack with X?' is a target choice between Stop Choosing (no) and the defending player (yes)",
|
| 85 |
+
"use": "[no, yes] logits for a yes/no question that isn't an attack",
|
| 86 |
+
"value": "tanh(value_x), the acting player's expected result in -1..1; the fitted temperature (T = 1.607) is folded into the value head's last layer, so P(win) = sigmoid(2 * value_x)"
|
| 87 |
+
},
|
| 88 |
+
"source": {
|
| 89 |
+
"checkpoint": "danbrooks/draftzero-checkpoints:gnn/full_r1/best_policy_calibrated.pt.gz",
|
| 90 |
+
"checkpoint_sha256": "3ee0839fbb6ed9a11f414ee290d2730335cf07490d76649c49d960cdc1f60706",
|
| 91 |
+
"model_code": "danieljbrooks/draft-zero@2a461518985e3b2bb393bddac4649e217f8fddfe:src/draftzero/gameplay/graph_net.py (MageZero's NetGraph, WillWroble/MageZero@de225045c82a2ddc107b99ac833e21ab6d566279, graph-encoder)",
|
| 92 |
+
"encoder": "WillWroble/mage@e4afc9c77ba7e4a6dbc24966cbdc6dc0819e0bff (graph-encoder) Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/encoder, vendored at danieljbrooks/draft-zero@2a461518985e3b2bb393bddac4649e217f8fddfe:java/mzbridge/src/org/draftzero/mzbridge/graph",
|
| 93 |
+
"engine": "danieljbrooks/mage@48e4918413392523a5f9d7915e1f33941ddea21e (v0.2-generalist)",
|
| 94 |
+
"search": "danieljbrooks/draft-zero@2a461518985e3b2bb393bddac4649e217f8fddfe:java/mzbridge/src/mage/player/ai (BenchPlayer, BenchSearch, GraphNet, GraphMCTSPlayer)"
|
| 95 |
+
}
|
| 96 |
+
}
|
coverage.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"replay_priority": {
|
| 3 |
+
"rows": 269820,
|
| 4 |
+
"leaf_occurrences": 38247895,
|
| 5 |
+
"leaf_occurrences_in_vocab": 1.0,
|
| 6 |
+
"distinct_leaf_ids": 1777,
|
| 7 |
+
"distinct_leaf_ids_in_vocab": 0.99831,
|
| 8 |
+
"edge_labels_in_vocab": 1.0
|
| 9 |
+
},
|
| 10 |
+
"replay_attack": {
|
| 11 |
+
"rows": 69025,
|
| 12 |
+
"leaf_occurrences": 10927062,
|
| 13 |
+
"leaf_occurrences_in_vocab": 1.0,
|
| 14 |
+
"distinct_leaf_ids": 1437,
|
| 15 |
+
"distinct_leaf_ids_in_vocab": 1.0,
|
| 16 |
+
"edge_labels_in_vocab": 1.0
|
| 17 |
+
},
|
| 18 |
+
"replay_target": {
|
| 19 |
+
"rows": 10336,
|
| 20 |
+
"leaf_occurrences": 1652432,
|
| 21 |
+
"leaf_occurrences_in_vocab": 1.0,
|
| 22 |
+
"distinct_leaf_ids": 1652,
|
| 23 |
+
"distinct_leaf_ids_in_vocab": 0.99879,
|
| 24 |
+
"edge_labels_in_vocab": 1.0
|
| 25 |
+
},
|
| 26 |
+
"block": {
|
| 27 |
+
"rows": 17106,
|
| 28 |
+
"leaf_occurrences": 2581374,
|
| 29 |
+
"leaf_occurrences_in_vocab": 0.99337,
|
| 30 |
+
"distinct_leaf_ids": 1598,
|
| 31 |
+
"distinct_leaf_ids_in_vocab": 0.88924,
|
| 32 |
+
"edge_labels_in_vocab": 1.0
|
| 33 |
+
}
|
| 34 |
+
}
|
gnn_release.py
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Load and run DraftZero's FDN graph network from its public release, without pickles or MageZero.
|
| 2 |
+
|
| 3 |
+
The release (built by export_gnn_release.py) holds:
|
| 4 |
+
|
| 5 |
+
model.safetensors the network's weights, float32
|
| 6 |
+
config.json the architecture (`arch`) and the encoder's constants
|
| 7 |
+
vocab.json leaf_ids and edge_ids: the raw feature ids that have embedding rows, row order
|
| 8 |
+
graph_net.py the network (a copy of draft-zero's src/draftzero/gameplay/graph_net.py)
|
| 9 |
+
goldens.jsonl.gz encoded test states with this network's outputs, for checking a port
|
| 10 |
+
|
| 11 |
+
A state is the graph encoder's output in MageZero's graph-server layout (one dict per state):
|
| 12 |
+
|
| 13 |
+
indices [nodes] raw feature id per node: StateEncoder.indexFor(hash64(name)); typed nodes
|
| 14 |
+
are the hash of their type name, the root is 0
|
| 15 |
+
values [nodes] numeric value per node (0 for non-numeric nodes)
|
| 16 |
+
edge_child [edges] node index, local to the state
|
| 17 |
+
edge_parent [edges] node index, local to the state
|
| 18 |
+
edge_label [edges] raw hashed edge label
|
| 19 |
+
|
| 20 |
+
python gnn_release.py check <release dir> # run the goldens, print the largest differences
|
| 21 |
+
"""
|
| 22 |
+
from __future__ import annotations
|
| 23 |
+
|
| 24 |
+
import gzip
|
| 25 |
+
import json
|
| 26 |
+
import sys
|
| 27 |
+
from pathlib import Path
|
| 28 |
+
|
| 29 |
+
import numpy as np
|
| 30 |
+
import torch
|
| 31 |
+
|
| 32 |
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
| 33 |
+
try:
|
| 34 |
+
import graph_net as gn # the copy shipped beside this file in the release
|
| 35 |
+
except ImportError: # in draft-zero's tree
|
| 36 |
+
from draftzero.gameplay import graph_net as gn
|
| 37 |
+
|
| 38 |
+
# MageZero ActionType ordinals (graph_type): which head scores a decision's options
|
| 39 |
+
PRIORITY, CHOOSE_TARGET, CHOOSE_USE = 0, 3, 5
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
class Vocab:
|
| 43 |
+
"""Raw feature id -> embedding row (-1 outside the vocab)."""
|
| 44 |
+
|
| 45 |
+
def __init__(self, ids):
|
| 46 |
+
self.ids = np.asarray(ids, np.int64)
|
| 47 |
+
self._order = np.argsort(self.ids, kind="stable")
|
| 48 |
+
self._sorted = self.ids[self._order]
|
| 49 |
+
|
| 50 |
+
def __len__(self) -> int:
|
| 51 |
+
return len(self.ids)
|
| 52 |
+
|
| 53 |
+
def lookup(self, ids) -> np.ndarray:
|
| 54 |
+
ids = np.asarray(ids, np.int64)
|
| 55 |
+
pos = np.minimum(np.searchsorted(self._sorted, ids), len(self.ids) - 1)
|
| 56 |
+
return np.where(self._sorted[pos] == ids, self._order[pos], -1)
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def load(release_dir, device: str = "cpu"):
|
| 60 |
+
"""(model in eval mode, leaf vocab, edge vocab, config) from a release directory."""
|
| 61 |
+
from safetensors.torch import load_file
|
| 62 |
+
d = Path(release_dir)
|
| 63 |
+
cfg = json.loads((d / "config.json").read_text())
|
| 64 |
+
vocab = json.loads((d / "vocab.json").read_text())
|
| 65 |
+
leaves, edges = Vocab(vocab["leaf_ids"]), Vocab(vocab["edge_ids"])
|
| 66 |
+
model = gn.NetGraph(len(leaves), len(edges), cfg["arch"])
|
| 67 |
+
model.load_state_dict(load_file(str(d / "model.safetensors")))
|
| 68 |
+
return model.to(device).eval(), leaves, edges, cfg
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def to_graphs(states: list[dict], leaves: Vocab, edges: Vocab) -> gn.Graphs:
|
| 72 |
+
"""A batch of encoded states -> the network's input (as graph_server.py maps them)."""
|
| 73 |
+
parts = {k: [] for k in ("type", "row", "value", "child", "parent", "label")}
|
| 74 |
+
offsets, base = [0], 0
|
| 75 |
+
for s in states:
|
| 76 |
+
ids = np.asarray(s["indices"], np.int64)
|
| 77 |
+
types = gn.node_types(ids)
|
| 78 |
+
parts["type"].append(types.astype(np.int64))
|
| 79 |
+
parts["row"].append(np.where(types == gn.NodeType.LEAF, leaves.lookup(ids), -1))
|
| 80 |
+
parts["value"].append(np.asarray(s["values"], np.int64))
|
| 81 |
+
parts["child"].append(np.asarray(s["edge_child"], np.int64) + base)
|
| 82 |
+
parts["parent"].append(np.asarray(s["edge_parent"], np.int64) + base)
|
| 83 |
+
parts["label"].append(edges.lookup(s["edge_label"]) + 1)
|
| 84 |
+
base += len(ids)
|
| 85 |
+
offsets.append(base)
|
| 86 |
+
t = {k: torch.from_numpy(np.concatenate(v).astype(np.int64)) for k, v in parts.items()}
|
| 87 |
+
return gn.Graphs(t["type"], t["row"], t["value"], torch.tensor(offsets), t["child"], t["parent"], t["label"])
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
@torch.no_grad()
|
| 91 |
+
def evaluate(model, states: list[dict], leaves: Vocab, edges: Vocab) -> list[dict]:
|
| 92 |
+
"""Per state: priority and target (one score per node, -inf at leaves), use [no, yes], value, value_x."""
|
| 93 |
+
g = to_graphs(states, leaves, edges)
|
| 94 |
+
out = model(g)
|
| 95 |
+
res = []
|
| 96 |
+
for i in range(len(states)):
|
| 97 |
+
a, b = int(g.node_offsets[i]), int(g.node_offsets[i + 1])
|
| 98 |
+
res.append({"priority": out.priority[a:b].numpy(), "target": out.target[a:b].numpy(),
|
| 99 |
+
"use": out.use[i].numpy(), "value": float(out.value[i]), "value_x": float(out.value_x[i])})
|
| 100 |
+
return res
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def option_logits(res: dict, graph_type: int, options: list[list[int]]) -> np.ndarray:
|
| 104 |
+
"""One logit per legal option. An option is a list of node indices (one ability node per copy of a
|
| 105 |
+
card for a play, Pass's node, a target's node, Stop Choosing's node; "attack with X?" is a target
|
| 106 |
+
choice between Stop Choosing and the defending player). Its logit is the log-sum-exp of its nodes'
|
| 107 |
+
scores under the head the decision reads: priority at PRIORITY, target otherwise, and the use head's
|
| 108 |
+
[no, yes] at a CHOOSE_USE question that isn't an attack."""
|
| 109 |
+
if graph_type == CHOOSE_USE:
|
| 110 |
+
return np.asarray(res["use"][:len(options)], np.float64)
|
| 111 |
+
scores = res["priority"] if graph_type == PRIORITY else res["target"]
|
| 112 |
+
out = []
|
| 113 |
+
for nodes in options:
|
| 114 |
+
x = scores[np.asarray(nodes, np.int64)].astype(np.float64)
|
| 115 |
+
m = x.max()
|
| 116 |
+
out.append(m + np.log(np.exp(x - m).sum()) if np.isfinite(m) else -np.inf)
|
| 117 |
+
return np.asarray(out)
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
def read_goldens(path) -> list[dict]:
|
| 121 |
+
with gzip.open(path, "rt") as f:
|
| 122 |
+
return [json.loads(line) for line in f]
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
def check(release_dir, batch: int = 16) -> dict:
|
| 126 |
+
"""Run the release's goldens through this loader and report the largest differences."""
|
| 127 |
+
d = Path(release_dir)
|
| 128 |
+
model, leaves, edges, _ = load(d)
|
| 129 |
+
goldens = read_goldens(d / "goldens.jsonl.gz")
|
| 130 |
+
worst = {"option_logits": 0.0, "value_x": 0.0, "use": 0.0}
|
| 131 |
+
argmax_agree = 0
|
| 132 |
+
for a in range(0, len(goldens), batch):
|
| 133 |
+
chunk = goldens[a:a + batch]
|
| 134 |
+
for g, r in zip(chunk, evaluate(model, [g["state"] for g in chunk], leaves, edges)):
|
| 135 |
+
want = g["expected"]
|
| 136 |
+
got = option_logits(r, g["graph_type"], g["options"])
|
| 137 |
+
worst["option_logits"] = max(worst["option_logits"], float(np.max(np.abs(got - want["option_logits"]))))
|
| 138 |
+
worst["value_x"] = max(worst["value_x"], abs(r["value_x"] - want["value_x"]))
|
| 139 |
+
worst["use"] = max(worst["use"], float(np.max(np.abs(r["use"] - want["use"]))))
|
| 140 |
+
argmax_agree += int(np.argmax(got) == np.argmax(want["option_logits"]))
|
| 141 |
+
return {"states": len(goldens), "max_abs_diff": worst, "argmax_agree": argmax_agree}
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
if __name__ == "__main__":
|
| 145 |
+
if len(sys.argv) != 3 or sys.argv[1] != "check":
|
| 146 |
+
sys.exit(__doc__)
|
| 147 |
+
print(json.dumps(check(sys.argv[2]), indent=1))
|
goldens.jsonl.gz
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:46d5949bb97528a1e0ea02ae255c22287fcc49d84a77feef778f2e1eb08318c8
|
| 3 |
+
size 542894
|
graph_net.py
ADDED
|
@@ -0,0 +1,344 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""MageZero's graph network (Will Wroble, WillWroble/MageZero branch graph-encoder), for DraftZero's
|
| 2 |
+
imitation training and search (docs/022).
|
| 3 |
+
|
| 4 |
+
The network is MageZero's `NetGraph` (src/magezero/model.py at de225045, 2026-10-04), copied here
|
| 5 |
+
rather than imported: that branch's modules import each other as top-level names (`from vocab import
|
| 6 |
+
...`), and DraftZero pins MageZero v0.2 for the flat networks. The code below is the upstream model with
|
| 7 |
+
four changes, none of which changes what a default network computes:
|
| 8 |
+
|
| 9 |
+
* its sizes are constructor arguments (`arch`), so a sweep can vary them; the defaults are
|
| 10 |
+
upstream's constants, and a default network's state dict is upstream's, so it loads in
|
| 11 |
+
MageZero's own graph server (`load_checkpoint` there);
|
| 12 |
+
* `leaf_dropout`: drop a share of leaf edges while training, the graph analogue of the flat
|
| 13 |
+
networks' token dropout (0 = upstream);
|
| 14 |
+
* `local_depth`: LocalLayers per type within each pass (1 = upstream). Each extra layer re-attends
|
| 15 |
+
from the node's updated embedding to the same children; extra layers live in `local_extra`, so a
|
| 16 |
+
depth-1 network's state dict is upstream's;
|
| 17 |
+
* `forward` also returns the value head's input to its Tanh (`value_x`), which the imitation
|
| 18 |
+
trainer's cross-entropy needs, as supervised.value_logit does for the flat networks.
|
| 19 |
+
|
| 20 |
+
Input: the graph encoder's state graph (graph_tables), nodes typed ROOT, PLAYER, ZONE, STACK_OBJECT,
|
| 21 |
+
PERMANENT, CARD, ABILITY or LEAF, edges child -> parent with a label:
|
| 22 |
+
|
| 23 |
+
embeddings: typed node = type embedding; leaf = leaf vocab row + numeric value bucket
|
| 24 |
+
local layers: `passes` bottom-up passes, each `local_depth` LocalLayers per type, run in order
|
| 25 |
+
ABILITY -> CARD -> PERMANENT -> STACK_OBJECT -> ZONE -> PLAYER -> ROOT
|
| 26 |
+
every node attends over [itself, children + edge label embedding]
|
| 27 |
+
global layers: TransformerEncoder over [CLS, the state's internal nodes]
|
| 28 |
+
├── MLP per node priority (read at ABILITY nodes: a play is its ability's node; Pass has one)
|
| 29 |
+
├── MLP per node target (read at CARD / PERMANENT / STACK_OBJECT / PLAYER nodes; Stop Choosing is a CARD)
|
| 30 |
+
├── MLP on CLS choose_use (false, true)
|
| 31 |
+
└── MLP on CLS value (tanh, -1..1)
|
| 32 |
+
"""
|
| 33 |
+
from __future__ import annotations
|
| 34 |
+
|
| 35 |
+
import math
|
| 36 |
+
from enum import IntEnum
|
| 37 |
+
from typing import NamedTuple
|
| 38 |
+
|
| 39 |
+
import numpy as np
|
| 40 |
+
import torch
|
| 41 |
+
import torch.nn.functional as F
|
| 42 |
+
from torch import nn
|
| 43 |
+
|
| 44 |
+
UPSTREAM = "WillWroble/MageZero@de225045c82a2ddc107b99ac833e21ab6d566279 (graph-encoder)"
|
| 45 |
+
GLOBAL_MAX = 2 ** 31 - 1
|
| 46 |
+
|
| 47 |
+
# upstream's constants (model.py): the default architecture
|
| 48 |
+
ARCH_DEFAULT = {"type": "graph", "d_model": 512, "heads": 4, "ff": 1024, "dropout": 0.25, "passes": 2,
|
| 49 |
+
"global_layers": 2, "head_hidden": 256, "leaf_dropout": 0.0, "local_depth": 1}
|
| 50 |
+
|
| 51 |
+
# numeric value buckets: exact -5..20 (P/T, counters, mana, costs, small counts), then 21-25, 26-30,
|
| 52 |
+
# 31-40, 41-60, 61+ (life totals, library and zone sizes); everything below -5 is one bucket
|
| 53 |
+
VALUE_BOUNDS = list(range(-5, 22)) + [26, 31, 41, 61]
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
class NodeType(IntEnum):
|
| 57 |
+
"""Java FeatureGraph.Node.Type, plus the root."""
|
| 58 |
+
ROOT = 0
|
| 59 |
+
PLAYER = 1
|
| 60 |
+
ZONE = 2
|
| 61 |
+
STACK_OBJECT = 3
|
| 62 |
+
PERMANENT = 4
|
| 63 |
+
CARD = 5
|
| 64 |
+
ABILITY = 6
|
| 65 |
+
LEAF = 7
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
STAGES = (NodeType.ABILITY, NodeType.CARD, NodeType.PERMANENT, NodeType.STACK_OBJECT,
|
| 69 |
+
NodeType.ZONE, NodeType.PLAYER, NodeType.ROOT)
|
| 70 |
+
PRIORITY_TYPES = (NodeType.ABILITY,)
|
| 71 |
+
TARGET_TYPES = (NodeType.CARD, NodeType.PERMANENT, NodeType.STACK_OBJECT, NodeType.PLAYER)
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
# ------------------------------------------------------------------------------------------------
|
| 75 |
+
# ids: the Java encoder's hash (upstream vocab.py), to tell typed nodes from leaves
|
| 76 |
+
# ------------------------------------------------------------------------------------------------
|
| 77 |
+
|
| 78 |
+
_MASK = (1 << 64) - 1
|
| 79 |
+
_GOLDEN = 0x9E3779B185EBCA87
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def _mix64(z: int) -> int:
|
| 83 |
+
z = ((z ^ (z >> 30)) * 0xBF58476D1CE4E5B9) & _MASK
|
| 84 |
+
z = ((z ^ (z >> 27)) * 0x94D049BB133111EB) & _MASK
|
| 85 |
+
return z ^ (z >> 31)
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
def feature_id(name: str) -> int:
|
| 89 |
+
"""The id XMage stores for a leaf name or edge label: StateEncoder.indexFor(hash64(name))."""
|
| 90 |
+
data = name.encode("utf-8")
|
| 91 |
+
h = _mix64(_GOLDEN ^ (len(data) * _GOLDEN & _MASK))
|
| 92 |
+
full = len(data) // 8 * 8
|
| 93 |
+
for i in range(0, full, 8):
|
| 94 |
+
h ^= _mix64(int.from_bytes(data[i:i + 8], "little"))
|
| 95 |
+
h = (((h << 27 | h >> 37) & _MASK) * _GOLDEN + 0x165667B19E3779F9) & _MASK
|
| 96 |
+
h ^= _mix64(int.from_bytes(data[full:], "little"))
|
| 97 |
+
h = ((h ^ (h >> 33)) * 0xFF51AFD7ED558CCD) & _MASK
|
| 98 |
+
h = ((h ^ (h >> 33)) * 0xC4CEB9FE1A85EC53) & _MASK
|
| 99 |
+
h ^= h >> 33
|
| 100 |
+
if h == 1 << 63: # Long.MIN_VALUE: Java's -h overflows and the remainder keeps the sign
|
| 101 |
+
return -((1 << 63) % GLOBAL_MAX)
|
| 102 |
+
return (h if h < 1 << 63 else (1 << 64) - h) % GLOBAL_MAX
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
# a typed node's id is the hash of its type name and the root's id is 0; every other node is a leaf
|
| 106 |
+
TYPE_IDS = {0: NodeType.ROOT}
|
| 107 |
+
for _t in NodeType:
|
| 108 |
+
if _t not in (NodeType.ROOT, NodeType.LEAF):
|
| 109 |
+
TYPE_IDS[feature_id(_t.name)] = _t
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
def node_types(ids: np.ndarray) -> np.ndarray:
|
| 113 |
+
types = np.full(ids.shape, NodeType.LEAF, dtype=np.int8)
|
| 114 |
+
for type_id, t in TYPE_IDS.items():
|
| 115 |
+
types[ids == type_id] = t
|
| 116 |
+
return types
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
def full_arch(arch: dict | None) -> dict:
|
| 120 |
+
a = {**ARCH_DEFAULT, **(arch or {})}
|
| 121 |
+
unknown = set(a) - set(ARCH_DEFAULT)
|
| 122 |
+
if unknown:
|
| 123 |
+
raise ValueError(f"unknown graph arch keys {sorted(unknown)} (known: {sorted(ARCH_DEFAULT)})")
|
| 124 |
+
if a["type"] != "graph":
|
| 125 |
+
raise ValueError(f"arch.type {a['type']!r}: graph_net builds only 'graph'")
|
| 126 |
+
if a["d_model"] % a["heads"]:
|
| 127 |
+
raise ValueError(f"d_model {a['d_model']} is not a multiple of heads {a['heads']}")
|
| 128 |
+
return a
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
def upstream_loadable(arch: dict) -> bool:
|
| 132 |
+
"""A network MageZero's own graph server loads as it is (upstream's constants)."""
|
| 133 |
+
a = full_arch(arch)
|
| 134 |
+
return all(a[k] == ARCH_DEFAULT[k] for k in ("d_model", "heads", "ff", "passes", "global_layers", "head_hidden",
|
| 135 |
+
"local_depth"))
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
# ------------------------------------------------------------------------------------------------
|
| 139 |
+
# the network
|
| 140 |
+
# ------------------------------------------------------------------------------------------------
|
| 141 |
+
|
| 142 |
+
class Graphs(NamedTuple):
|
| 143 |
+
"""A batch of state graphs, concatenated: the network's input."""
|
| 144 |
+
node_type: torch.Tensor # [N] NodeType
|
| 145 |
+
node_row: torch.Tensor # [N] leaf vocab row; -1 for typed nodes and leaves outside the vocab
|
| 146 |
+
node_value: torch.Tensor # [N] numeric value (0 for non-numeric)
|
| 147 |
+
node_offsets: torch.Tensor # [B+1] first node of each state
|
| 148 |
+
edge_child: torch.Tensor # [E] node index (batch-wide)
|
| 149 |
+
edge_parent: torch.Tensor # [E] node index (batch-wide)
|
| 150 |
+
edge_label: torch.Tensor # [E] edge vocab row + 1; 0 for labels outside the vocab
|
| 151 |
+
|
| 152 |
+
def to(self, device):
|
| 153 |
+
return Graphs(*(t.to(device, non_blocking=True) for t in self))
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
class Out(NamedTuple):
|
| 157 |
+
priority: torch.Tensor # [N] per-node priority score (-inf at leaves)
|
| 158 |
+
target: torch.Tensor # [N] per-node target score (-inf at leaves)
|
| 159 |
+
use: torch.Tensor # [B, 2] choose_use logits (false, true)
|
| 160 |
+
value: torch.Tensor # [B] tanh(value_x)
|
| 161 |
+
value_x: torch.Tensor # [B] the value head's input to its Tanh
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
def graph_index(offsets: torch.Tensor) -> torch.Tensor:
|
| 165 |
+
"""State of each node, from a batch's node offsets."""
|
| 166 |
+
return torch.repeat_interleave(torch.arange(offsets.numel() - 1, device=offsets.device), offsets.diff())
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
def segment_logsumexp(x: torch.Tensor, seg: torch.Tensor, n: int) -> torch.Tensor:
|
| 170 |
+
"""Log-sum-exp over the rows of x that share an index in seg (values 0..n-1); returns n rows
|
| 171 |
+
(-inf for an index with no row)."""
|
| 172 |
+
index = seg.view(-1, *([1] * (x.dim() - 1))).expand_as(x)
|
| 173 |
+
with torch.no_grad(): # the max only stabilizes; the result and its gradient don't depend on it
|
| 174 |
+
m = x.new_full((n, *x.shape[1:]), float("-inf")).scatter_reduce(0, index, x, "amax")
|
| 175 |
+
m = torch.where(torch.isfinite(m), m, torch.zeros_like(m))
|
| 176 |
+
return m + x.new_zeros((n, *x.shape[1:])).index_add(0, seg, (x - m[seg]).exp()).log()
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
class LocalLayer(nn.TransformerEncoderLayer):
|
| 180 |
+
"""A pre-norm transformer layer run over each node's [node, children...] sequence, keeping only
|
| 181 |
+
the node's output token (upstream's LocalLayer)."""
|
| 182 |
+
|
| 183 |
+
def __init__(self, d_model: int, heads: int, ff: int, dropout: float):
|
| 184 |
+
super().__init__(d_model, heads, ff, dropout, activation="gelu", batch_first=True, norm_first=True)
|
| 185 |
+
|
| 186 |
+
def forward(self, node: torch.Tensor, child: torch.Tensor, seg: torch.Tensor) -> torch.Tensor:
|
| 187 |
+
"""node: [S, d] this stage's nodes; child: [E, d] child tokens (child embedding + edge label
|
| 188 |
+
embedding); seg: [E] position in `node` of each child's parent. Returns the nodes' new embeddings."""
|
| 189 |
+
S, d = node.shape
|
| 190 |
+
H, dh = self.self_attn.num_heads, self.self_attn.head_dim
|
| 191 |
+
w, b = self.self_attn.in_proj_weight, self.self_attn.in_proj_bias
|
| 192 |
+
q, k, v = F.linear(self.norm1(node), w, b).chunk(3, dim=-1)
|
| 193 |
+
k_child, v_child = F.linear(self.norm1(child), w[d:], b[d:]).chunk(2, dim=-1)
|
| 194 |
+
seg = torch.cat([torch.arange(S, device=seg.device), seg]) # each node also attends to itself
|
| 195 |
+
k = torch.cat([k, k_child]).view(-1, H, dh)
|
| 196 |
+
v = torch.cat([v, v_child]).view(-1, H, dh)
|
| 197 |
+
score = (q.reshape(S, H, dh)[seg] * k).sum(-1).float() / math.sqrt(dh) # [S+E, H]
|
| 198 |
+
attn = (score - segment_logsumexp(score, seg, S)[seg]).exp()
|
| 199 |
+
attn = F.dropout(attn, self.self_attn.dropout, self.training)
|
| 200 |
+
out = torch.zeros(S, H, dh, device=node.device).index_add_(
|
| 201 |
+
0, seg, (attn.to(v.dtype).unsqueeze(-1) * v).float())
|
| 202 |
+
node = node + self.dropout1(self.self_attn.out_proj(out.view(S, d).to(node.dtype)))
|
| 203 |
+
return node + self._ff_block(self.norm2(node))
|
| 204 |
+
|
| 205 |
+
|
| 206 |
+
def _mlp(d: int, hidden: int, out_dim: int, *tail: nn.Module) -> nn.Sequential:
|
| 207 |
+
return nn.Sequential(nn.Linear(d, hidden), nn.ReLU(), nn.Linear(hidden, out_dim), *tail)
|
| 208 |
+
|
| 209 |
+
|
| 210 |
+
class NetGraph(nn.Module):
|
| 211 |
+
def __init__(self, num_embeddings: int = 0, num_edge_labels: int = 0, arch: dict | None = None):
|
| 212 |
+
super().__init__()
|
| 213 |
+
a = self.arch = full_arch(arch)
|
| 214 |
+
d = a["d_model"]
|
| 215 |
+
self.embedding = nn.Embedding(num_embeddings, d) # leaves
|
| 216 |
+
self.edge_embedding = nn.Embedding(num_edge_labels + 1, d, padding_idx=0) # row 0: unknown label
|
| 217 |
+
self.type_embedding = nn.Embedding(len(NodeType), d)
|
| 218 |
+
self.value_embedding = nn.Embedding(len(VALUE_BOUNDS) + 1, d)
|
| 219 |
+
self.register_buffer("value_bounds", torch.tensor(VALUE_BOUNDS), persistent=False)
|
| 220 |
+
self.local_layers = nn.ModuleList(
|
| 221 |
+
nn.ModuleDict({t.name: LocalLayer(d, a["heads"], a["ff"], a["dropout"]) for t in STAGES})
|
| 222 |
+
for _ in range(a["passes"]))
|
| 223 |
+
if a["local_depth"] < 1:
|
| 224 |
+
raise ValueError(f"local_depth {a['local_depth']}: at least 1")
|
| 225 |
+
if a["local_depth"] > 1: # the layers after each type's first, per pass (absent at depth 1: upstream's layout)
|
| 226 |
+
self.local_extra = nn.ModuleList(
|
| 227 |
+
nn.ModuleDict({t.name: nn.ModuleList(LocalLayer(d, a["heads"], a["ff"], a["dropout"])
|
| 228 |
+
for _ in range(a["local_depth"] - 1)) for t in STAGES})
|
| 229 |
+
for _ in range(a["passes"]))
|
| 230 |
+
self.cls = nn.Parameter(torch.randn(d))
|
| 231 |
+
global_layer = nn.TransformerEncoderLayer(d, a["heads"], a["ff"], a["dropout"], activation="gelu",
|
| 232 |
+
batch_first=True, norm_first=True)
|
| 233 |
+
self.global_layers = nn.TransformerEncoder(global_layer, a["global_layers"], norm=nn.LayerNorm(d),
|
| 234 |
+
enable_nested_tensor=False)
|
| 235 |
+
h = a["head_hidden"]
|
| 236 |
+
self.priority_head = _mlp(d, h, 1)
|
| 237 |
+
self.target_head = _mlp(d, h, 1)
|
| 238 |
+
self.binary_head = _mlp(d, h, 2)
|
| 239 |
+
self.value_head = _mlp(d, h, 1, nn.Tanh())
|
| 240 |
+
|
| 241 |
+
def forward(self, g: Graphs) -> Out:
|
| 242 |
+
B, N = g.node_offsets.numel() - 1, g.node_type.numel()
|
| 243 |
+
|
| 244 |
+
# initial embeddings: typed nodes their type, leaves their name and numeric value
|
| 245 |
+
leaves = (g.node_row >= 0).nonzero().squeeze(1)
|
| 246 |
+
value = torch.bucketize(g.node_value[leaves], self.value_bounds, right=True)
|
| 247 |
+
h = self.type_embedding(g.node_type).index_add(
|
| 248 |
+
0, leaves, self.embedding(g.node_row[leaves]) + self.value_embedding(value))
|
| 249 |
+
|
| 250 |
+
# leaves outside the vocab have no embedding, so their edges are dropped (and, while
|
| 251 |
+
# training, a leaf_dropout share of the others)
|
| 252 |
+
child_leaf = g.node_type[g.edge_child] == NodeType.LEAF
|
| 253 |
+
keep = ~child_leaf | (g.node_row[g.edge_child] >= 0)
|
| 254 |
+
if self.training and self.arch["leaf_dropout"] > 0:
|
| 255 |
+
keep = keep & ~(child_leaf & (torch.rand(keep.shape, device=keep.device) < self.arch["leaf_dropout"]))
|
| 256 |
+
child, parent, label = g.edge_child[keep], g.edge_parent[keep], g.edge_label[keep]
|
| 257 |
+
|
| 258 |
+
# one stage per type: every node of that type, its children, and each child's parent position
|
| 259 |
+
parent_type = g.node_type[parent]
|
| 260 |
+
stages = []
|
| 261 |
+
for t in STAGES:
|
| 262 |
+
nodes = (g.node_type == t).nonzero().squeeze(1)
|
| 263 |
+
if len(nodes):
|
| 264 |
+
edges = (parent_type == t).nonzero().squeeze(1)
|
| 265 |
+
stages.append((t.name, nodes, child[edges], label[edges], torch.searchsorted(nodes, parent[edges])))
|
| 266 |
+
|
| 267 |
+
# local layers, bottom-up passes: children earlier in the order arrive updated in this pass,
|
| 268 |
+
# children of the same type or later in the order (attachments, targets, linked exile) from the last one
|
| 269 |
+
extra = getattr(self, "local_extra", None)
|
| 270 |
+
for p, layers in enumerate(self.local_layers):
|
| 271 |
+
for name, nodes, c, lbl, seg in stages:
|
| 272 |
+
kids = h[c] + self.edge_embedding(lbl)
|
| 273 |
+
x = layers[name](h[nodes], kids, seg).to(h.dtype)
|
| 274 |
+
for layer in (extra[p][name] if extra is not None else ()):
|
| 275 |
+
x = layer(x, kids, seg).to(h.dtype)
|
| 276 |
+
h = h.index_copy(0, nodes, x)
|
| 277 |
+
|
| 278 |
+
# global layers: self-attention over [CLS, internal nodes], one padded sequence per state
|
| 279 |
+
internal = (g.node_type != NodeType.LEAF).nonzero().squeeze(1)
|
| 280 |
+
gi = graph_index(g.node_offsets)[internal]
|
| 281 |
+
counts = torch.bincount(gi, minlength=B)
|
| 282 |
+
pos = torch.arange(len(internal), device=h.device) - (counts.cumsum(0) - counts)[gi] + 1
|
| 283 |
+
x = h.new_zeros(B, int(counts.max()) + 1, h.shape[1])
|
| 284 |
+
x[:, 0] = self.cls.to(h.dtype)
|
| 285 |
+
x[gi, pos] = h[internal]
|
| 286 |
+
pad = torch.arange(x.shape[1], device=h.device) > counts.unsqueeze(1)
|
| 287 |
+
x = self.global_layers(x, src_key_padding_mask=pad)
|
| 288 |
+
cls, z = x[:, 0], x[gi, pos]
|
| 289 |
+
|
| 290 |
+
def per_node(head):
|
| 291 |
+
return torch.full((N,), float("-inf"), device=h.device).index_copy(
|
| 292 |
+
0, internal, head(z).squeeze(-1).float())
|
| 293 |
+
|
| 294 |
+
vx = self.value_head[:-1](cls).squeeze(-1).float()
|
| 295 |
+
return Out(per_node(self.priority_head), per_node(self.target_head), self.binary_head(cls).float(),
|
| 296 |
+
torch.tanh(vx), vx)
|
| 297 |
+
|
| 298 |
+
|
| 299 |
+
def parameter_counts(model: NetGraph) -> dict:
|
| 300 |
+
out = {"total": 0}
|
| 301 |
+
for name, p in model.named_parameters():
|
| 302 |
+
part = name.split(".")[0]
|
| 303 |
+
out[part] = out.get(part, 0) + p.numel()
|
| 304 |
+
out["total"] += p.numel()
|
| 305 |
+
return out
|
| 306 |
+
|
| 307 |
+
|
| 308 |
+
# ------------------------------------------------------------------------------------------------
|
| 309 |
+
# batches: graph rows -> the network's input, and options -> logits
|
| 310 |
+
# ------------------------------------------------------------------------------------------------
|
| 311 |
+
|
| 312 |
+
def gather(ptr: np.ndarray, sel: np.ndarray, *arrays: np.ndarray) -> tuple:
|
| 313 |
+
"""The CSR slices of rows `sel` (ptr over `arrays`), concatenated, and their new ptr."""
|
| 314 |
+
lens = (ptr[sel + 1] - ptr[sel]).astype(np.int64)
|
| 315 |
+
new = np.zeros(len(sel) + 1, np.int64)
|
| 316 |
+
np.cumsum(lens, out=new[1:])
|
| 317 |
+
pos = np.repeat(ptr[sel] - new[:-1], lens) + np.arange(new[-1])
|
| 318 |
+
return (new,) + tuple(a[pos] for a in arrays)
|
| 319 |
+
|
| 320 |
+
|
| 321 |
+
class OptionBatch(NamedTuple):
|
| 322 |
+
"""Per option of a batch: its row, its nodes (batch-wide) and which node belongs to which option."""
|
| 323 |
+
opt_row: torch.Tensor # [O] row of each option
|
| 324 |
+
node: torch.Tensor # [K] batch-wide node index of each option node
|
| 325 |
+
node_opt: torch.Tensor # [K] option of each option node
|
| 326 |
+
n_opt: int
|
| 327 |
+
|
| 328 |
+
def to(self, device):
|
| 329 |
+
return OptionBatch(self.opt_row.to(device), self.node.to(device), self.node_opt.to(device), self.n_opt)
|
| 330 |
+
|
| 331 |
+
|
| 332 |
+
def option_logits(out: Out, ob: OptionBatch, graph_type: torch.Tensor, opt_index: torch.Tensor) -> torch.Tensor:
|
| 333 |
+
"""[O] logit of each option: log-sum-exp of its nodes' scores under the head its row's decision type
|
| 334 |
+
reads (priority at PRIORITY rows, target at CHOOSE_TARGET rows; two copies of a card are one option,
|
| 335 |
+
so their probabilities add), or the choose_use logit at CHOOSE_USE rows (options [no, yes]).
|
| 336 |
+
opt_index: [O] each option's position within its row."""
|
| 337 |
+
gt = graph_type[ob.opt_row[ob.node_opt]]
|
| 338 |
+
s = torch.where(gt == 0, out.priority[ob.node], out.target[ob.node])
|
| 339 |
+
lg = segment_logsumexp(s.float(), ob.node_opt, ob.n_opt)
|
| 340 |
+
is_use = graph_type[ob.opt_row] == 5
|
| 341 |
+
if is_use.any():
|
| 342 |
+
u = out.use.float()[ob.opt_row, opt_index.clamp(max=1)]
|
| 343 |
+
lg = torch.where(is_use, u, lg)
|
| 344 |
+
return lg
|
model.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:d5ee8323ccca11f18b1667bc2312407d9d5696b6dacf7243d9a30461cdc29b56
|
| 3 |
+
size 53671100
|
vocab.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"leaf_ids": [1440341, 1962182, 2208587, 2544383, 5626501, 7400809, 7847295, 9608361, 9942472, 15576439, 15842078, 18336802, 19294824, 19611187, 20442916, 21647753, 22212785, 24715442, 26348976, 26411638, 26576046, 26743471, 27123869, 27201461, 27617022, 29261465, 31021900, 31456499, 34398885, 35827476, 36016871, 36857849, 38420094, 39834060, 40892126, 41801327, 42434614, 42874147, 43674225, 43896700, 44824265, 46121060, 46841098, 47702099, 47708227, 47728856, 47894163, 48545723, 49433952, 50136961, 50414051, 51634314, 51930769, 56219660, 56381963, 56477097, 56634758, 56751205, 57069400, 58235508, 58674378, 60817109, 61441050, 61802711, 63336643, 64790511, 69053869, 69243589, 70143062, 70529767, 71579209, 71928681, 72522655, 75342627, 75445463, 75559423, 77799283, 79068143, 79921356, 80987291, 86889390, 87211654, 87978658, 89368553, 89403159, 89937610, 90996423, 91411481, 92775046, 95226462, 95954591, 96456902, 96502244, 97814345, 98901711, 99301007, 99427890, 99812325, 102244748, 104062034, 105259026, 105552121, 111543627, 112984472, 113987043, 114727717, 115078649, 115640700, 116292132, 116437944, 117570743, 117946022, 118083407, 120900771, 122396342, 123866549, 124716521, 127557047, 129540009, 131807887, 132043099, 134216449, 135814834, 139376906, 141362100, 142245513, 142610748, 142628996, 143242829, 143763009, 145314718, 148331029, 149397475, 149517531, 149548185, 150543704, 151915608, 152071582, 152483195, 152569543, 152770126, 152904535, 154783486, 154806888, 155153733, 155420588, 156860798, 157849559, 158177950, 161769807, 162813817, 164493930, 169656228, 170691259, 172015683, 176159025, 176543331, 177240602, 178856769, 179101397, 180508954, 181279754, 181540797, 182904966, 185769792, 188941692, 190278963, 192248464, 193465718, 194711281, 197231207, 198997643, 200515638, 200586786, 200587528, 201348254, 202332436, 202444000, 203409592, 205091846, 205231537, 205621798, 205788260, 208075154, 208422199, 209187028, 209303386, 210222412, 210313912, 210942411, 211631465, 212671933, 212925767, 214792765, 216526367, 217014610, 218205224, 219944482, 220209414, 222737121, 223570041, 224799333, 225929782, 226172277, 228001398, 229257030, 231214138, 231550675, 232638495, 233567658, 235177798, 235697470, 236109572, 236634927, 239024287, 239439331, 240188622, 240557881, 241885934, 242019349, 242560623, 244565090, 244682578, 245215842, 246314782, 246903153, 248221686, 248690035, 250401928, 250937488, 251051695, 253304701, 254778635, 255124921, 255905666, 256539945, 257229202, 258218267, 260055758, 262367082, 262583584, 262927112, 263583980, 263914887, 264131501, 264698822, 264905349, 265748383, 267819643, 268263696, 268272175, 268416926, 268614795, 269446478, 269633968, 270264912, 270806140, 272173153, 274335623, 275058553, 276271436, 276566837, 277872022, 279097986, 280175250, 281002777, 281471535, 282936815, 283277349, 283616657, 283743219, 286192078, 286208123, 286340658, 290754915, 292146107, 293302369, 294672588, 294916352, 295689009, 295949540, 298236303, 299170348, 299233256, 299339413, 302183216, 302266203, 302958216, 303007393, 303217747, 303391387, 306140224, 306767986, 308226169, 309375254, 310049329, 310610276, 311520533, 313569312, 313950034, 314919095, 315670052, 316271288, 317001748, 317097845, 317737238, 320282077, 321235475, 323085886, 324275269, 324864251, 326607987, 327564059, 327822642, 328615012, 332335403, 333119429, 334254253, 334487786, 335162619, 336976093, 337465183, 337887673, 338750121, 338831475, 340259405, 341214915, 342749688, 343360971, 344532036, 347907674, 349400708, 349614487, 350218938, 350592763, 351140735, 352071770, 352146621, 352296083, 354605954, 354871153, 358447958, 360500561, 361306221, 362223846, 362512325, 364656144, 364903885, 365100823, 369274168, 369720408, 371393579, 372210837, 373177303, 373264905, 373374922, 373811555, 374238703, 374644088, 375345723, 378167955, 378758831, 380729067, 380902880, 381106181, 381724562, 382520221, 385110298, 388170884, 391305087, 392355780, 392373278, 392967445, 393183340, 394362358, 397813009, 397842519, 397913634, 398208133, 398524568, 399579108, 401688395, 403468142, 404759524, 404989165, 405074863, 406177337, 407310688, 408598499, 409209600, 411672151, 411798609, 413331273, 413817868, 414289503, 414668887, 414715689, 417483125, 419772856, 422838138, 425039099, 425908922, 426752089, 427289935, 427730915, 427749997, 428553857, 430172399, 430599117, 431810040, 432262754, 432393266, 432622045, 434545204, 436004689, 436628072, 437371816, 438489751, 439353963, 439766774, 440191805, 440560406, 441820716, 442005403, 442272731, 443355591, 444786919, 449007826, 449086460, 452011879, 452978675, 454117479, 454199117, 454818615, 455776412, 455871989, 456110902, 456389764, 457058308, 457207635, 457831743, 458886651, 461156297, 462337250, 463252858, 465051482, 467712254, 468184418, 468916271, 469041193, 471044639, 472141738, 472247022, 473244528, 475514177, 475857585, 475872756, 476053588, 477822875, 479627258, 480192604, 480286359, 484513670, 485092985, 488515436, 489863666, 491403483, 492745338, 494740336, 495781028, 497505774, 498628461, 500979731, 501479600, 501860663, 502725437, 504579951, 505791077, 507729751, 508411864, 511302454, 511585387, 511774120, 512297293, 514658547, 517542001, 518411903, 519018980, 519662460, 521059290, 524283637, 524310728, 525563645, 526432400, 526575519, 527724311, 530130816, 531148739, 531861041, 532132901, 532153805, 532155808, 535196011, 535543957, 538741739, 540059387, 542985218, 545037260, 547726523, 548482694, 548633120, 549278577, 550648132, 550755172, 554801475, 554999823, 555782980, 559073695, 560812410, 562430808, 563178319, 567113238, 567608014, 568176078, 570146786, 572458796, 574075174, 574232134, 574570132, 576528642, 576995363, 577345960, 577984950, 578116103, 578192564, 578327451, 579818273, 580373828, 580486195, 581487291, 582294620, 583150795, 586079310, 586782611, 591843915, 593429204, 594808990, 595753163, 596482569, 597108327, 597161745, 597621914, 598452969, 599115692, 599245905, 600355528, 602047541, 602300856, 602679994, 604769639, 605771104, 607365014, 609861612, 611004085, 611652800, 612462330, 612906220, 613884535, 617050187, 617208948, 621146309, 624706238, 625753859, 629029047, 629330354, 629521660, 631315783, 633316148, 634049231, 634192656, 635102301, 636221106, 637504013, 638162895, 639302983, 639708755, 639941244, 641350743, 642324338, 642859683, 643713223, 644207749, 644290504, 645209224, 645654806, 646268637, 646278469, 648879301, 649508587, 649947097, 651063818, 651451523, 651607466, 658183045, 658311497, 660209914, 663063215, 663503735, 663784229, 663917152, 666003473, 666181616, 667380597, 668820438, 669245941, 671155727, 671390736, 672020766, 672890283, 673162425, 675972228, 678809176, 679047906, 679880620, 681991136, 682890320, 685311367, 686027225, 688870735, 689557210, 690016366, 691113461, 691579864, 693652572, 693898931, 694217607, 697164346, 700800114, 702588243, 704705076, 705374685, 705745572, 707239051, 707422687, 708990901, 709079214, 710668801, 710843686, 711619772, 711830598, 712826141, 712987826, 713073832, 716506758, 716560146, 718332679, 719162734, 721819394, 723303765, 724184920, 724838751, 724866303, 727783171, 729839090, 731394855, 732700468, 733750371, 736534376, 736842200, 737020356, 737954394, 740702845, 741633978, 742449621, 742631720, 743828030, 744924559, 745831164, 746266618, 747538775, 749393443, 749459732, 750679424, 750962180, 751410268, 752544771, 752547265, 753339829, 754086067, 755208037, 755690624, 756813771, 758562378, 758597665, 760807551, 762770871, 763852860, 764166466, 765599598, 766232307, 766616159, 766964003, 769004324, 771139323, 772229338, 774066050, 774403490, 777502366, 779252576, 780645407, 782408165, 783233761, 783971384, 784539389, 784832823, 786146011, 786642483, 789347214, 789631674, 790482088, 790978259, 793795445, 795001832, 795269605, 795422486, 795518945, 795795670, 795970052, 795975657, 796126366, 796165132, 796793800, 797018104, 797685979, 800033900, 800301881, 802103953, 806211228, 806457699, 808580132, 809138705, 809139094, 809222994, 813567067, 813916931, 815378051, 815542409, 817127959, 817791801, 818364420, 818790138, 819365591, 821033680, 821734656, 822553484, 823337364, 824375317, 825213549, 826968191, 827170562, 828979377, 829985223, 830204431, 830808238, 831915530, 832722393, 833483646, 834489206, 834573350, 835758716, 835767629, 836053716, 836650302, 837524050, 837784352, 838373840, 839022469, 839268956, 839337520, 841117176, 844032974, 844583464, 844702554, 845783909, 846159577, 846297969, 848314086, 849044060, 849191803, 850147946, 850909901, 856024454, 856231674, 857724748, 857830313, 860119761, 863146563, 863203390, 864140418, 864385355, 864850333, 865343802, 868208821, 872258727, 872594898, 872912522, 874883667, 875149788, 875193495, 875636869, 877334315, 881516106, 881956341, 882350249, 882457993, 883008540, 883715582, 885781761, 886899836, 887479417, 887539812, 887611392, 888015031, 888345622, 891193908, 891734026, 893766352, 893786252, 901446593, 901919395, 902774501, 906665816, 907918895, 909199831, 909427960, 909668328, 911770935, 914997573, 915098839, 916846327, 919855937, 920678023, 921493867, 921631808, 921811794, 922593208, 923378710, 923878123, 924822343, 926456199, 926676452, 927409569, 927457981, 927720972, 928273724, 929067687, 931707989, 932043175, 933195534, 936590391, 936648825, 937661954, 940242667, 941213167, 941967510, 943052660, 944191473, 945728694, 946488534, 948371067, 948592947, 954687402, 956953308, 957913728, 958092476, 963066985, 963473726, 963489793, 963944376, 965064875, 965355899, 966052514, 966771154, 967717900, 968586863, 969226375, 969907499, 972483381, 973690790, 974297332, 974754870, 975265061, 975575057, 975919299, 976326008, 977875024, 978098940, 978816330, 981152716, 982541258, 984469748, 985013148, 986529310, 986841842, 987129450, 987880070, 990894996, 992240222, 993879784, 994911044, 994987088, 995132474, 995763248, 996479982, 998998723, 1002433237, 1002732121, 1003002939, 1003758255, 1004735815, 1006815371, 1008271012, 1009082291, 1010759091, 1010866558, 1012027495, 1012246236, 1016349931, 1016496912, 1021809046, 1022763761, 1024424923, 1026966006, 1028870905, 1031294989, 1031591848, 1031701667, 1032225436, 1032527342, 1033020014, 1035818762, 1036250420, 1038761862, 1041462975, 1041598386, 1041772012, 1043280365, 1043731426, 1044467862, 1044600422, 1045216541, 1045538240, 1046173029, 1047612572, 1048190424, 1051060502, 1051516614, 1051660799, 1051942730, 1052033983, 1052143011, 1052227471, 1056195217, 1061407237, 1062637749, 1063103572, 1065249099, 1065465613, 1065500702, 1072383679, 1072835681, 1073120889, 1075165126, 1075917882, 1077745239, 1078820679, 1080545818, 1082923246, 1085134962, 1085372879, 1086967859, 1090703398, 1090890249, 1091995962, 1093851430, 1096076277, 1096320962, 1097589286, 1097782627, 1098992058, 1101784046, 1101823536, 1102511984, 1102673364, 1104015502, 1105362161, 1106991229, 1107615472, 1108130868, 1108696927, 1108755083, 1110063838, 1111281526, 1111567825, 1113261272, 1114064064, 1115796582, 1118324162, 1119618306, 1120392030, 1121777847, 1122108686, 1125160637, 1127171266, 1127955552, 1128500303, 1130156138, 1132494783, 1134588121, 1137616768, 1137852415, 1138895796, 1138901551, 1139288701, 1142760525, 1143065909, 1143711313, 1144137830, 1145579697, 1146220376, 1146778074, 1147724697, 1148167324, 1149081070, 1149478102, 1150333065, 1151820202, 1153133824, 1153411793, 1153815393, 1155289733, 1155926884, 1158218063, 1159939975, 1160227108, 1161616093, 1162449478, 1162626843, 1166653598, 1166808723, 1167545182, 1173790343, 1176880987, 1177319729, 1177401913, 1178081703, 1178460016, 1180929545, 1182632794, 1183413348, 1183767101, 1190854247, 1190963813, 1191044628, 1191521919, 1191928366, 1193449544, 1194966515, 1195470234, 1196521984, 1199996132, 1201340426, 1201458248, 1201491749, 1201600676, 1203881081, 1203885987, 1204323350, 1204345493, 1205603632, 1205767567, 1206415501, 1206849870, 1209570056, 1210240132, 1210311466, 1210564714, 1211229539, 1212663909, 1212693301, 1215039279, 1215367514, 1216932940, 1217254588, 1218681705, 1219390124, 1219939353, 1220552786, 1221928229, 1222364261, 1223367703, 1224398856, 1224486184, 1225067558, 1226759343, 1226815658, 1227996381, 1228491159, 1228876010, 1232627619, 1232828157, 1233238628, 1233860440, 1237750770, 1238804999, 1239575746, 1239601424, 1240876452, 1242035149, 1242708112, 1244131853, 1245499350, 1248115155, 1248333796, 1248404031, 1249115487, 1250546917, 1251505846, 1253988378, 1254209094, 1254598873, 1255052726, 1255611452, 1256322244, 1259457107, 1260555562, 1261148613, 1261496261, 1261740561, 1261970224, 1262543592, 1265823978, 1266108865, 1266283177, 1267021923, 1267202747, 1267280238, 1269333892, 1270859407, 1275009797, 1278008730, 1278980716, 1279264161, 1279953850, 1281310502, 1286694231, 1287504847, 1288086675, 1288278799, 1289441348, 1289540664, 1289634190, 1292828424, 1295047703, 1296458253, 1298018959, 1298023021, 1298734760, 1299923408, 1300250147, 1300489328, 1300647664, 1300673505, 1301325317, 1301609321, 1305177036, 1306194388, 1306397134, 1307585817, 1309664126, 1310145507, 1312802178, 1313387814, 1313793596, 1314540606, 1317604776, 1317925607, 1318581361, 1319068114, 1319088234, 1320394979, 1324237059, 1324685147, 1326046029, 1326602522, 1327579312, 1328315112, 1328512021, 1329837058, 1330806963, 1331602191, 1331812810, 1332479050, 1333159154, 1333461274, 1335208565, 1337004176, 1337445992, 1337913581, 1337966615, 1338310815, 1339814551, 1340550775, 1341171512, 1341274615, 1341996981, 1342316165, 1343262418, 1344246097, 1347335713, 1347366700, 1347415266, 1348525055, 1348873293, 1349647218, 1351238428, 1351352044, 1354460161, 1355636464, 1358029957, 1358050940, 1359057716, 1361398034, 1361581868, 1363691382, 1366066223, 1366409913, 1368983767, 1369116271, 1373000685, 1374319827, 1374633028, 1374867353, 1376344532, 1376821117, 1377688873, 1378263271, 1380651818, 1381421847, 1381536199, 1382093708, 1382489545, 1383953299, 1386293723, 1387315513, 1389079211, 1391792941, 1391920914, 1391956311, 1394538678, 1394705997, 1396320168, 1398437567, 1399305827, 1399651887, 1401249263, 1401665883, 1401837032, 1403182846, 1404004684, 1404241694, 1404420744, 1404582499, 1404957988, 1405467409, 1405509803, 1406704459, 1407060034, 1408602148, 1410036018, 1411327229, 1414199937, 1414953882, 1415453416, 1415487825, 1416053613, 1416848329, 1418027442, 1418303697, 1421503314, 1423389434, 1424308292, 1424513543, 1424636819, 1425184804, 1430919052, 1431558667, 1433399539, 1433744916, 1435094476, 1435384586, 1436127790, 1436959901, 1436977320, 1438796667, 1438987987, 1440054325, 1441181011, 1441460597, 1441724456, 1441754421, 1442239077, 1446376790, 1446465022, 1446809330, 1447452886, 1447799608, 1447872745, 1449839197, 1451078378, 1454042798, 1456729017, 1459496244, 1460247009, 1464570357, 1467267836, 1467320682, 1468845644, 1470062109, 1473402765, 1476428281, 1476589492, 1476964227, 1478578929, 1478604633, 1479193592, 1480616940, 1483472125, 1484249143, 1485670843, 1486217091, 1487332078, 1489739048, 1491135339, 1491264596, 1491561942, 1493389329, 1493735396, 1494491276, 1494808828, 1495922554, 1498456400, 1498908739, 1500568942, 1500726125, 1504591987, 1504677084, 1505264779, 1506051407, 1507569015, 1509892176, 1510264190, 1511059194, 1511059996, 1511875421, 1516381911, 1516651427, 1517277589, 1517614333, 1519512919, 1519892322, 1522283465, 1522547874, 1523810313, 1525027814, 1525230934, 1525506943, 1526871878, 1528978576, 1530591246, 1530966476, 1534936880, 1535438454, 1536204296, 1537330116, 1543041882, 1544099811, 1544654287, 1545266126, 1545648253, 1546475677, 1546798618, 1547130225, 1547943461, 1548614769, 1551931523, 1552229373, 1553574944, 1555329312, 1556720711, 1557876860, 1558391349, 1558586455, 1560406279, 1562677455, 1563170529, 1565169958, 1565494715, 1566338322, 1567400036, 1567774493, 1568532068, 1571515843, 1571648925, 1572734123, 1573136250, 1575184414, 1575262688, 1575977463, 1576146646, 1579956476, 1580020343, 1580288482, 1580915149, 1581802262, 1582582718, 1583927102, 1584991580, 1586800640, 1587129629, 1590557331, 1592106456, 1593090703, 1593188127, 1594006767, 1596019020, 1597187323, 1598774335, 1599549281, 1600492098, 1601237027, 1602617680, 1602832197, 1603324833, 1606118479, 1606536323, 1606702150, 1606867760, 1607684655, 1607957554, 1608168143, 1608854641, 1609848545, 1610917002, 1610992972, 1611598521, 1611798727, 1612733246, 1614623469, 1615989607, 1617381482, 1619179041, 1619644828, 1620310502, 1621011674, 1621918109, 1626234254, 1626389490, 1627462075, 1627777523, 1628066570, 1628127388, 1630098533, 1634525658, 1634541406, 1634647356, 1638131297, 1641465148, 1644129858, 1644801890, 1647458725, 1648092054, 1648111691, 1648559601, 1648592634, 1652362143, 1652803292, 1653809240, 1654891439, 1655176678, 1657318634, 1659223487, 1661535554, 1662142036, 1662324311, 1664959718, 1665247084, 1667084037, 1668885651, 1669412841, 1669929121, 1673135531, 1673251338, 1674460949, 1677091148, 1678529523, 1681091965, 1681300078, 1685384760, 1686367411, 1686926556, 1687921433, 1690210857, 1690805963, 1693761823, 1694066180, 1695464938, 1696060949, 1698160230, 1700389242, 1703111592, 1703887919, 1704160815, 1709980835, 1711850431, 1712596310, 1716262237, 1718659221, 1719595689, 1720547376, 1725569201, 1726851586, 1726888158, 1728101712, 1728577316, 1730660905, 1731554446, 1732804489, 1732904379, 1734604069, 1734701661, 1735983628, 1736201125, 1737690732, 1738502424, 1738860061, 1739620528, 1740605093, 1741160447, 1744391118, 1744635442, 1745482268, 1746284504, 1747242617, 1747434857, 1748642158, 1749068263, 1749127665, 1753072895, 1753272157, 1753904629, 1754084889, 1754376498, 1754595349, 1754882594, 1755931840, 1756894885, 1756927776, 1759390534, 1760501653, 1761339889, 1762950461, 1763074197, 1763140237, 1763608259, 1764752820, 1765083413, 1765095371, 1765770498, 1769313379, 1771098816, 1772417173, 1773375808, 1773508832, 1773661387, 1774635561, 1776181032, 1778412649, 1778525763, 1779746832, 1780554180, 1780875480, 1782799145, 1782936016, 1782946817, 1783895505, 1786737988, 1788342066, 1788911643, 1789389410, 1789564114, 1790220894, 1790842572, 1791576872, 1792252280, 1793416395, 1794501672, 1795411725, 1796141387, 1797874328, 1798519828, 1799310415, 1803965875, 1804352883, 1805534332, 1808808711, 1809524919, 1809578260, 1809594952, 1810379922, 1811003112, 1811482446, 1811594385, 1813125077, 1813766187, 1814767134, 1818848995, 1825173448, 1825558173, 1825908517, 1830230144, 1830753198, 1830978969, 1832386671, 1832970557, 1835964348, 1836741861, 1836753146, 1837012607, 1838294806, 1839216853, 1839444415, 1839903956, 1840319516, 1840332604, 1841699400, 1846983136, 1847393555, 1847566665, 1847817250, 1849359196, 1849756243, 1849786295, 1852146538, 1852480575, 1854004839, 1854625842, 1856517976, 1857433120, 1860190378, 1860795251, 1861899597, 1862086822, 1862914194, 1865242116, 1866180129, 1867972240, 1868416733, 1868938547, 1869120239, 1870093990, 1870476722, 1871736803, 1872971068, 1875012172, 1875703365, 1876588807, 1877053939, 1877204025, 1880038052, 1880547027, 1881366860, 1881443182, 1882046025, 1883151525, 1883572528, 1885148302, 1885927732, 1887193370, 1887973180, 1888231902, 1888454660, 1889219445, 1889413136, 1889576626, 1890124111, 1890713437, 1891530088, 1892727780, 1893681424, 1895357763, 1897634176, 1900572196, 1904510579, 1905044589, 1906567674, 1906960588, 1908518233, 1909207500, 1910677241, 1911057345, 1911869299, 1912655534, 1913917030, 1915146792, 1915394465, 1916314863, 1917842184, 1919919724, 1920356479, 1920861661, 1921600360, 1922333472, 1924244430, 1924461904, 1927066070, 1927846999, 1930368742, 1931700064, 1938064141, 1938235053, 1938345885, 1938707714, 1940252474, 1940723445, 1941263681, 1941313131, 1941500774, 1942353976, 1942525558, 1945818983, 1947300738, 1949090963, 1949483524, 1950588977, 1952163402, 1952980467, 1955410600, 1956480656, 1956633586, 1957388114, 1957618715, 1958677583, 1959734883, 1960874455, 1961873273, 1962006775, 1963938656, 1968511604, 1969972599, 1972414716, 1972420302, 1972995261, 1973855053, 1974399284, 1975225910, 1976135564, 1978210199, 1978942335, 1980867800, 1981596294, 1983403706, 1983415450, 1984450872, 1985841599, 1986902709, 1987558056, 1988240819, 1989264189, 1990361627, 1991563887, 1991695597, 1991761759, 1992057595, 1992408471, 1993127818, 1993616065, 1995939726, 1996593031, 1996738526, 1997415667, 1998387933, 1999578169, 1999757610, 2000015925, 2001070402, 2004009760, 2005430969, 2005662755, 2006732796, 2007448564, 2009615883, 2010045548, 2011155039, 2014193714, 2014617229, 2014793219, 2018967574, 2020651166, 2021668383, 2022035007, 2022634865, 2022806479, 2023253043, 2023366677, 2023900047, 2024223132, 2025205671, 2026332002, 2028717834, 2029838755, 2030850011, 2032514240, 2032569875, 2034428455, 2034688288, 2035262033, 2035448228, 2037975339, 2038171878, 2039114906, 2040709684, 2041500204, 2042918394, 2043462538, 2043910215, 2044029692, 2045947198, 2048177486, 2050316874, 2050699899, 2051331837, 2056097208, 2056774551, 2057546366, 2057929596, 2058392467, 2060348569, 2060522047, 2062726113, 2062728620, 2063974849, 2067205574, 2070281532, 2070573163, 2071145909, 2072757853, 2075210489, 2077197067, 2078479133, 2078851734, 2079252796, 2080142296, 2080835733, 2082612272, 2083452294, 2083662743, 2086617880, 2088185620, 2088255131, 2088559127, 2090170612, 2090308508, 2090336439, 2090765527, 2091244518, 2091714137, 2093511117, 2094163149, 2096003183, 2096155729, 2096273635, 2099108364, 2099471626, 2102840233, 2103041636, 2103161743, 2105935009, 2106047090, 2106591401, 2107071728, 2107736230, 2110105715, 2110131977, 2110485418, 2111871239, 2112917376, 2113768134, 2114589289, 2114981333, 2115785779, 2115860189, 2116517901, 2118565195, 2118692269, 2122442898, 2122984373, 2123233057, 2123871691, 2124361786, 2127485484, 2127541030, 2127882625, 2128062247, 2128565539, 2131944766, 2132991830, 2133081231, 2133574102, 2133786271, 2134278980, 2136337594, 2138433949, 2139035162, 2143087177, 2144083981, 2144566527, 2144963772, 2145046133, 2145462307, 2146619477, 2147090793], "edge_ids": [101213851, 142628996, 158676006, 198258688, 207409853, 240751648, 474720742, 571077171, 576496726, 609685571, 1247051105, 1292107022, 1372811207, 1391785934, 1756731660, 1899612498, 1951335677, 2010485505, 2033378404, 2058020975, 2076243686, 2101699769], "note": "raw feature ids (StateEncoder.indexFor(hash64(name))) with an embedding row, in row order; a leaf outside leaf_ids has no embedding and its edges are dropped; an edge label outside edge_ids uses the unknown-label row"}
|