Keyboard shortcuts

Press ← or → to navigate between chapters

Press ? to show this help

Press Esc to hide this help

清单

你的参赛模型由两个文件组成:网络本身,以及 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 中打开这个适配器,看看每个操作符收到的参数:

这份清单是完整的,但编码很差:它忽略了敌方蚂蚁、食物、蚁丘和视野。逐段解读一份真实清单讲的是平台自家基线所用的那一份。

为什么策略头不由你来写

清单里没有读取计算图输出并产生走法的程序,这背后的原因解释了你计算图中的一个形状。

result 表达式的文档只有输出张量,不含观测。读取逐格策略意味着在你的蚂蚁所在格子上取值,而那些格子在观测里,所以除非平台把观测交回去,否则没人写得出这个程序。

平台本可以把它交回去。但裁判选择自己读取策略头,因为每个参赛模型写出的这段程序都会是同一个取值操作,一个字符都不差:通道顺序是 Ants 的规则,就像“一步只能走一格”也是规则一样。模型回答什么把这条规则写明一次,任何参赛者的副本都不可能在细节上有出入。

你得到的是一次免费的取值:这些操作不计入你的预算,而且一整类错误(轴转置了、通道顺序差一位)根本不可能发生。你失去的没有任何你用得上的东西。策略头只要不是公布的两种形状之一,审核就会在裁判可能误读它之前先拒绝。

把它写对

  1. 从头到尾读一遍一份真实的清单:七个平面输入,逐格策略输出,以及每一段的开销。
  2. 在 DataLogic Studio 中打开你的适配器,观察 JSON 部分的运行,并记住 Studio 与竞技场不一致的地方。
  3. 记住表达式语言的作用域规则:在 map、filter 和 reduce 内部,文档就是那个元素,读取外层路径得到 null,而且不会报错。
  4. 任何操作符都可以在操作符里查到,它的价格则在预算里。
  5. 在拿任何东西训练之前,先运行 tinybrains adapt,把它输出的张量与你训练器的编码器逐个元素比对。