TinyBrains 的构成
参赛者的循环是:提交、验证、试赛、比赛、复盘。平台把接收请求、决定打哪些对局、进行对局和运行模型拆成独立的部分,让每个部分按各自的负载伸缩。
本章面向贡献者和在本地运行平台的人。参加托管的比赛不需要部署这里的任何东西。
组成部分
| 部分 | 职责 |
|---|---|
| Web | 浏览器应用:登录、排行榜、对局与回放、提交与上传、赛季自己的页面,以及网站的社区页面 |
| Soma | 公开 HTTP API、会话、提交、赛季、运行器网关和共享的数据库模式,外加五个时钟:admit、pair、count、withdraw 和 reap |
| Kalam | 认领对局、进行回合、记录结果和回放。审核运行器还会运行每次提交的审核,交由 Soma 裁定 |
| Ants | 确定性的游戏卡带,及其回放查看器 |
Orion 的 models 实体 | 适配器求值和 ONNX 推理,在任何需要它的节点内部 |
tinybrains(CLI) | 同一套对局循环,在笔记本上运行,不需要服务器:供参赛者使用,也用来检验天梯的那一套 |
ants-starter | 参赛者要克隆的唯一仓库:一个可以直接参赛的模型、它的训练脚本和训练好的对手 |
Soma 和 Kalam 不包含任何服务端代码。两者都是 Orion 的包(频道和工作流定义、SQL、连接器和小型 wasm 插件),以 Orion 服务器镜像的形式发布,服务器在启动时加载这个包。
没有独立的模型运行服务。模型是 Orion 服务器自身管理的实体:一行记录,保存着清单和模型文件的引用(一个存储连接器、一个对象键和一个 sha256: 摘要)。节点获取该对象,重新计算哈希,读取计算图,然后通过 tract 从 LRU 会话缓存中提供服务。只有 Kalam 运行模型:审核运行器负责审核它们,其他每个副本负责用它们比赛。Soma 节点提供网站服务并持有数据库所有者角色,从不解析参赛者的模型。
Postgres 存储版本、赛季、对局记录、席位和评分事件。对象存储保存提交的两个文件和回放文件。仓库地图指出了每个组件对应的代码。
调度与执行之间只有一张对局表
Soma 的 pair 时钟插入一场待进行的对局及其席位。Kalam 认领这一行,进行对局,然后写入结果、回放键和对局的最后一帧来完成它。随后 Soma 的 count 时钟计入结果,并把这一行标记为已评分。这一行既是队列项,也是持久的历史记录:平台没有消息代理,也没有任何时钟会调用 Kalam 去分派对局。
认领带有租约和令牌,所以平台能收回失联工作节点手上的对局,而过期的工作节点也无法完成别人的尝试。时钟用运行栅栏和名册纪元来隔离自己的写入,所以过期的调度或评分工作无法改动赛况。两者合在一起,让参赛者的历史记录中不会出现重复或归属错误的结果。
时钟
Soma 的五个定时时钟做出每一项竞赛决定,平台的其他部分不做任何决定。每个时钟在 Soma 的所有节点上只运行一个实例。
| 时钟 | 周期 | 决定 |
|---|---|---|
| admit | 20 s | 是否让上传的提交(或管理员的基线)通过审核:检查上传内容和清单,排队交给审核运行器,再对该运行器的报告应用平台的策略 |
| pair | 15 s | 把哪些对局排入队列:先是等待中候选版本的试赛,然后是天梯需求所要求的对局,都在赛季启用中的地图上进行 |
| count | 10 s | 把每场结束的对局计入评分,并裁定每场结束的试赛:晋级或拒绝 |
| withdraw | 60 s | 取消已无人能进行的排队对局,并在赛季结算完成后结束它 |
| reap | 15 s | 把租约已失效(运行器崩溃或失去响应)的已认领对局放回队列,第三次失效时将其判为失败,原因为 LEASE_LAPSED |
时钟的工作都是对共享数据库模式执行的 SQL。两个 wasm 插件承担 SQL 不擅长的运算,tb.rating(TrueSkill)和 tb.pairing(谁和谁打);它们做出的每个决定都由时钟写入,此外不决定任何事情。版本的生命周期从参赛者的视角跟随一个版本走过这些时钟。disabled 是基线独有的状态:基线在管理员启用之前一直不参赛。
赛季
平台管理员为一个游戏创建赛季,设定时间窗口和规则,平台在所有地方都用 slug 来指代它。一个游戏可以同时运行多个赛季;没有指明赛季的读取会解析为该游戏精选的公开赛季。赛季的时间窗口决定它的状态(未开始、进行中,提交截止后为结算中),当每个评分都稳定后,或在赛季管理员请求时,withdraw 时钟结束它。结束后排名仍可查看。
赛季分公开和私有,参赛资格分开放和受限。受限赛季只接纳其管理员列出的参与者;私有赛季只对这些参与者、赛季管理员和平台管理员可见,对其他任何人,每个路由的回应都和赛季不存在时一样。两种读取用的是同一组语句:公开路由在没有查看者的情况下运行它们并缓存结果,而 /v1/private 下登录后的副本以会话的查看者身份运行它们,不缓存。
赛季有自己的管理员。由平台管理员指定,他们只运营这一个赛季(它的参与者、地图、基线、运行器和消息),不持有任何平台角色。赛季管理员不能参加自己运营的赛季。
赛季的地图和基线由赛季管理员上传进去,没有任何发布版本附带它们。Soma 用赛季自己的引擎检查地图文件,admit 时钟像审核任何提交一样审核基线的两个文件。两者上传后都处于停用状态,管理员可以在赛季结束前随时启用或停用。每个赛季的地图都必须落在 Ants 发布版本中五张基础地图的范围之内,所以今天通过审核的模型能打管理员日后添加的任何地图。新赛季从空白开始,导入另一个赛季的地图和基线;参赛者用一字节不差的同一份文件重新参赛,审核和试赛会在新赛季下再判一次。
运行器与运行器网关
Kalam 运行器是一个同时进行多场对局的 Orion 节点:默认两场,部署可以设得更多,最多六十四场。它不持有数据库密码,也不持有存储桶密钥:它用运行器密钥换取一个短期令牌,同时报告自己能承接多少场对局,然后通过 Soma 的运行器网关(/v1/runner/*)认领、续约、完成和释放对局。网关的路由就是认领语句本身,不保存任何状态;网关的数据库角色 runner_gate 只能访问执行相关的列。你可以把运行器放在任何地方的一张办公桌上,一个被攻破的运行器最多也只能走出糟糕的棋。认领携带对局所遵循的每一项条款(地图、回合时限、回合上限、席位被判负的判罚上限,以及节点可以拒绝这一行的次数),都取自拥有这场对局的赛季。
运行器密钥要么属于平台机群,要么属于某一个赛季。持平台密钥的运行器可以打任何机群策略允许平台参与的赛季;由赛季管理员生成的赛季密钥只服务于该赛季,按其策略(own、platform 或 both)的允许范围进行对局或审核。网关在每次认领时都检查该策略,所以平台管理员可以在赛季进行中把它交给平台机群,而被攻破的赛季密钥最多也只能在它自己的天梯上比赛。
一次参赛会经过什么
- Web 或已认证的客户端请 Soma 记录两个哈希。Soma 回应两个预签名
PUTURL,然后由参赛者上传:平台上没有任何东西会从互联网抓取文件。 - Soma 的 admit 时钟检查两个文件都已到达、清单的哈希与参赛者声明的一致,据此重建注册信息,并把该版本排队交给审核运行器。
- 一个 Kalam 审核运行器通过运行器网关认领它,在自己的节点上注册它,运行节点的审核(获取、重新计算哈希、读取计算图、探测),在游戏的参考观测上运行它,然后删除它并报告测得的结果。admit 时钟对这份报告应用平台的策略,验证这个候选版本,然后 pair 时钟把它的试赛排入队列。
- 每个 Kalam 副本的 roster 时钟根据数据库,在自己的节点上注册、审核并激活该版本。没有任何时钟会调用副本。
- Kalam 认领一行对局记录,并确认自己的节点能够提供每个席位的模型。
- Ants 生成观测;每个席位一次
model_infer,完成适配、运行并返回张量;Kalam 读取策略头,把动作交给 Ants。Kalam 重复这个回合直到对局结束。 - Kalam 上传回放,并用自己的认领令牌记录结果和对局的最后一帧。
- Soma 的 count 时钟计入结果或裁定试赛。试赛通过即让版本晋级;之后的常规对局更新它的评分。
- Soma 提供版本状态、对局结果、排名,以及带签名的回放读取。
在卡带之外,游戏状态是不透明的。适配器是提交上来的数据,由节点的表达式引擎求值;参赛者不上传任何工作流。张量从不离开模型调用,只有一个例外:策略头。result 表达式的文档只包含输出张量,所以它无法按单位所在的格子取值。这一取值(gather)是游戏规则的一部分:由 Kalam 执行,没有参赛者需要写它。
发布版本与引擎摘要
每个仓库发布自己的产物,不提交任何生成出来的东西。Ants 发布的是 GitHub 发布版本:组件、它的清单、基础地图和查看器,打成一个以组件摘要命名的压缩包。Soma、Kalam 和 Web 从版本标签发布镜像;CLI 发布二进制文件和一个 Homebrew 配方。
天梯的引擎就是某一个 Ants 发布版本。Soma 的镜像声明它的摘要并用它评判地图,每个运行器用它比赛,网站的查看器和本书用它重新模拟回放,入门套件为参赛者锁定它。它们必须一致。摘要不同的运行器什么也认领不到,用另一个引擎构建的查看器会画出一场看似合理却从未发生过的对局。发布一个 Ants 发布版本就等于部署它,所以赛季进行中的部署要锁定其镜像所来自的发布版本。
部署与伸缩
本地平台把 Soma(它的路由和时钟)托管在一个 Orion 实例上,使用共享的 Postgres 运行时状态和 Redis。每个 Kalam 副本都是独立的 Orion:单个实例,状态在本地,没有集群配置块。平台不运行任何边车服务。
共享的调度器状态让 Soma 的时钟跨主机协调。每个 Kalam 副本保存自己的状态,所以多个副本可以同时进行对局,而不必争抢一把全局锁。出于同样的原因,模型由每个副本自己的时钟在该副本上注册,没有任何东西会把模型推送到副本。模型是节点各自的实体,由数据库来保持整个机群一致。
一个存储桶,两个地址。Soma 为参赛者的上传按公开接口签名,而每个节点通过内部接口获取。SigV4 会把主机名一起签进去,所以两者共用一个变量,得到的 URL 只有一方能用。
每个插件和卡带组件都带有一个对其摘要的 Ed25519 签名。签名由持有该部署信任密钥的人生成;节点会隔离那些依赖无法验证的组件的频道。
无论本地还是生产环境,部署路径都是 Compose;生产环境用第二个 compose 文件在前面放一个 Caddy,并从托管服务获取数据库和存储桶。没有任何东西会自动伸缩,发布版本靠手动部署。
必须守住的边界
只有 Soma 的 count 时钟会写入竞赛评分的更新。运行器不持有任何数据库凭据:网关以 runner_gate 身份运行它的语句,这个角色只能访问执行字段、运行器身份和审核报告,外加对名册的列级读取,看不到裁定结果和评分。节点按摘要获取模型文件,所以你可以证明参赛的就是 Soma 审核通过的那一个。平台可以撤回排队中的对局,但已经在进行的对局仍归属于配对时的那些版本。回放和对局记录保留引擎摘要和 Orion 版本,供你追查某个结果时使用。
如果你改动了某条边界,要把它的生产方和消费方放在一起测试。包能加载、健康检查接口能应答,并不能证明审核、对局或回放的流程没有问题。