清单
你的参赛模型由两个文件组成:网络本身,以及 manifest.json。清单声明你的计算图接收什么、返回什么(每个输入和输出的名字、数据类型和形状),并为每个输入带上一段小程序:一个适配器,把游戏的观测变成那个张量。计算图从不接触 JSON,游戏也从不接触张量。
适配器是一棵带张量操作符的 JSONLogic 表达式树:由节点求值的数据,并受节点自己计数的操作预算约束。平台把清单的原始字节哈希进你的提交,并把它们加到计算图的字节数上,得出你的大小指标。
一个回合,从头到尾
观测 (JSON) {"size": [64, 96], "mine": [[12, 30], …], …}
│
▼ 适配器 节点对你的程序求值,每个声明的输入各一次
张量 {"board": i8[1, 7, 64, 96]}
│
▼ model.onnx tract 运行你的计算图
张量 {"policy": f32[1, 5, 64, 96]}
│
▼ 裁判 平台读取你的策略头
动作 (JSON) ["N", "-", "E"]
这三步共用一个回合时限,整个席位一共 1,000 ms;每个适配器各有自己的 1,000,000 次操作的预算。
最后一个箭头属于裁判。通道顺序、在你的蚂蚁所在格子上取值(gather)以及 argmax,都是游戏的规则。模型回答什么就是全部的约定,下文有一节解释原因。
清单文件
{
"abi": "orion:model@1.0.0",
"name": "my-entry",
"version": "1",
"format": "onnx",
"description": "七个平面输入,逐格策略输出",
"inputs": [ { "name": "board", "dtype": "i8", "shape": [1, 7, "H", "W"], "adapter": <program> } ],
"outputs": [ { "name": "policy", "dtype": "f32", "shape": [1, 5, "H", "W"] } ],
"probe_dims": { "H": 120, "W": 124 }
}
| 字段 | 必须是 |
|---|---|
abi | "orion:model@1.0.0"。平台拒绝任何其他值 |
name | 你自己起的,供你自己记录。平台用它自己的 id 登记你的模型,所以这个名字只是说明模型,不标识任何东西 |
inputs | 计算图的每个输入一项,各带 name、dtype、shape 和 adapter。名字必须是计算图自己的输入名 |
outputs | 计算图的每个输出一项,各带 name、dtype 和 shape,没有适配器:由平台读取 |
probe_dims | 审核探测时每个具名维度取的大小。见下文 |
result | 不允许。平台会以 RESULT_NOT_ALLOWED 拒绝带有它的清单 |
平台忽略其他顶层键,但它们仍然是字节:会改变哈希,也会计入你的大小。
维度可以是名字
形状是一个列表,每一项是正整数或名字:
"shape": [1, 7, "H", "W"]
名字在一次调用中首次出现时绑定,不论是出现在某个输入的形状里还是某个输出的形状里,之后每次出现都必须等于那次绑定的值。写成数字的轴必须恰好等于那个数字。
名字让一个参赛模型能打赛季里的每一张地图。一个赛季的 Ants 地图边长可以是 24 到 124 之间的任意值,最多 14,880 格,而且赛季进行中还可以新增地图。全卷积网络把 H 和 W 写成名字,一个通过审核的会话就能应对所有地图。空间维度固定的参赛模型也是合法的,只是只能打它声明的那些地图;节点一收到其他尺寸的观测就会拒绝它。
自己计算索引的计算图无法给轴起名字。如果你的网络在内部用
H和W计算扁平索引(Shape→Mul→Gather这条链,也就是tensor[batch, idx]查找的常见导出形式),运行时就无法用符号对它做类型检查,从而拒绝构建执行计划。要么声明具体的空间维度、只打一种地图尺寸,要么把索引计算移出计算图。审核会报告执行计划构建失败,并附上运行时自己的报错信息。
在最大的地图上探测
审核会在全零输入上运行五次推理,中位数必须在游戏的回合时限之内,Ants 是 1,000 ms。推理按 probe_dims 运行,而它由你声明。声明 {"H": 24, "W": 24},你得到的是 24×24 地图上的结论,而赛季还可能让你打 120×124 的地图,那正是慢回合会让你吃判罚的尺寸。请声明游戏允许的最大地图:14,880 格,也就是 120×124 的基础地图。赛季进行中还可以新增地图,所以它今天最大的地图以后可能被超过(地图)。probe_dims 里没有的名字,审核按 1 探测。
适配器
每个输入有一个适配器,节点拿它对游戏发来的原样观测求值:{"var": "mine"} 是你的蚂蚁,{"var": "size.1"} 是地图的宽度。适配器必须生成一个数据类型和形状都与输入声明一致的张量,名字按上文的方式绑定。
适配器不能读取 {"secret": …}、now 或 random,注册时会拒绝尝试这么做的清单。适配器也没有文件系统和网络。它对之前回合的了解,只有你的模型写进记忆的内容,而记忆像其他字段一样随观测一起到达。同一个适配器处理同一个观测,在任何机器上的开销都相同。
两个部分:JSON 与张量
程序是作用于普通 JSON 值的 JSONLogic(var、map、filter、reduce、算术、比较),外加跨入张量再回来的张量操作符。两者之间的分界决定了你能写什么:
- 张量是不透明的。程序只能读它的形状和数据类型,别的都不行:不能对张量做索引、不能
map遍历它、不能对它做算术。 - 进入张量:
tensor、zeros、full、scatter、rle_expand和one_hot从 JSON 构建一个张量。 - 张量之间:
stack、concat、unstack、reshape、transpose、pad、crop、cast、normalize和gather。 - 回到 JSON:
argmax、to_list、shape和dtype。
JSON 部分负责从观测中挑选和重排(哪些列表、哪些字段、哪些坐标),张量部分把结果打包成你的计算图期望的形状。凡是要对数字做计算的,都放进计算图,在那里它是编译执行的。表达式语言有意不提供矩阵乘法和卷积(原因)。
最小清单
最小的完整清单向一个全卷积计算图送入两个平面:你的蚂蚁,和你见过的水域:
{
"abi": "orion:model@1.0.0",
"name": "minimal",
"format": "onnx",
"inputs": [{
"name": "board",
"dtype": "i8",
"shape": [1, 2, "H", "W"],
"adapter": {
"reshape": [
{"stack": [[
{"scatter": [{"var": "mine"}, {"var": "size"}, "i8"]},
{"rle_expand": [{"var": "water.rle"}, {"var": "size"}, "i8"]}
], 0]},
{"merge": [[1, 2], {"var": "size"}]}
]
}
}],
"outputs": [{ "name": "policy", "dtype": "f32", "shape": [1, 5, "H", "W"] }],
"probe_dims": { "H": 120, "W": 124 }
}
scatter 在一张 size 大小的地图上,在你每只蚂蚁的坐标处写入 1;rle_expand 把水域的游程列表展开成同样大小的平面;stack 把两者放到一个新的首轴上,reshape 再在前面加上声明要求的批次轴。对示例观测来说,{"var": "size"} 是 [64, 96],所以结果是 i8[1, 2, 64, 96]:H 绑定为 64,W 绑定为 96,输出也必须与这两个值一致。
在 DataLogic Studio 中打开这个适配器,看看每个操作符收到的参数:
在 DataLogic Studio 中打开——数据:《模型看到什么》一章逐步讲解的那份观测
这份清单是完整的,但编码很差:它忽略了敌方蚂蚁、食物、蚁丘和视野。逐段解读一份真实清单讲的是平台自家基线所用的那一份。
为什么策略头不由你来写
清单里没有读取计算图输出并产生走法的程序,这背后的原因解释了你计算图中的一个形状。
result 表达式的文档只有输出张量,不含观测。读取逐格策略意味着在你的蚂蚁所在格子上取值,而那些格子在观测里,所以除非平台把观测交回去,否则没人写得出这个程序。
平台本可以把它交回去。但裁判选择自己读取策略头,因为每个参赛模型写出的这段程序都会是同一个取值操作,一个字符都不差:通道顺序是 Ants 的规则,就像“一步只能走一格”也是规则一样。模型回答什么把这条规则写明一次,任何参赛者的副本都不可能在细节上有出入。
你得到的是一次免费的取值:这些操作不计入你的预算,而且一整类错误(轴转置了、通道顺序差一位)根本不可能发生。你失去的没有任何你用得上的东西。策略头只要不是公布的两种形状之一,审核就会在裁判可能误读它之前先拒绝。
把它写对
- 从头到尾读一遍一份真实的清单:七个平面输入,逐格策略输出,以及每一段的开销。
- 在 DataLogic Studio 中打开你的适配器,观察 JSON 部分的运行,并记住 Studio 与竞技场不一致的地方。
- 记住表达式语言的作用域规则:在
map、filter和reduce内部,文档就是那个元素,读取外层路径得到null,而且不会报错。 - 任何操作符都可以在操作符里查到,它的价格则在预算里。
- 在拿任何东西训练之前,先运行
tinybrains adapt,把它输出的张量与你训练器的编码器逐个元素比对。