> ## Documentation Index
> Fetch the complete documentation index at: https://starforge.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Fleet

> 注册机器：什么是 Fleet、节点怎么加入、排空与存活判定

**Fleet** 是一组机器加上在它们上面跑活儿的后端，有名字。它是你建出来的一条记录，不是导出的一个
环境变量：一个 console 可以同时挂着好几个 Fleet，每个作业和每个部署都记下了它被准入到哪一个。

它取代了原先那个部署级的后端开关。原因见
[ADR-0013](https://github.com/wccdev/starforge/blob/main/docs/adr/0013-an-execution-backend-is-a-registered-fleet.md)。
`FORGE_DEFAULT_FLEET_KIND` 是它留下的东西，只在 console 还没有任何 Fleet 时用来种出第一个——
首次启动读一次，别处不读。

## 一个 Fleet 记了什么

| 字段             | 取值                                        | 可改                           |
| -------------- | ----------------------------------------- | ---------------------------- |
| `kind`         | `local` \| `node` \| `kuberay` \| `slurm` | \*\*不可。\*\*它描述的是机器本身         |
| `delivery`     | `shared-mount` \| `object-pull`           | \*\*不可。\*\*改它等于挪动每个在跑作业的写入位置 |
| `capabilities` | `train`、`serve`、`env` 的任意组合               | 可                            |
| `visibility`   | `public` \| `project`                     | 可                            |
| `state`        | `active` \| `draining` \| `disabled`      | 可                            |
| `config`       | 这个 Fleet 覆盖掉的部署设置                         | 可                            |

`config` 放的是同一种 kind 的两个 Fleet 之间会不一样的那些键——slurm 的 REST 地址、KubeRay 的
namespace。写了一个没有任何设置会读的键，会打一条 warning 而不是被无声忽略：拼错的覆盖项就是
一条运维以为生效、实际没生效的设置。

<Note>
  capability 是承诺，不是偏好。没有 `serve` 的 Fleet 不是「serve 得不好」的 Fleet——点名它的
  部署会被拒。
</Note>

## 建一个

**Fleets → 新建 Fleet**。选后端、选它用来做什么；数据投递方式默认跟随后端，可以改。也可以走 API：

```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
curl -X POST https://forge.corp/api/fleets \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"id": "gpu-lab", "kind": "node", "delivery": "shared-mount",
       "capabilities": ["train", "serve"]}'
```

仅管理员。注册机器是运维做的事；挑一个 Fleet 提交是用户做的事。

一个完全没有 Fleet 的 console 会在首次启动时按 `FORGE_DEFAULT_FLEET_KIND` 种下一个，所以
零配置部署根本不用调这个接口。

## 注册一台机器

只有 `node` Fleet 需要注册机器——`kuberay` 和 `slurm` 的机器由集群管理器给，`local` 就是
console 自己这台。

在 Fleet 页面签出一个 join token——「添加机器」。它一次性、24 小时过期、只存哈希。页面会给出一行可以直接粘到
机器上的命令：

```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
curl -fsSL https://forge.corp/install.sh | FORGE_FLEET=gpu-lab FORGE_JOIN_TOKEN=<token> sh
```

机器上已经装了这个包的话：

```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
forgelet join --server https://forge.corp --fleet gpu-lab --token <token>
forgelet serve --port 7070
```

<Warning>
  join token 就是全部授权。你需要的是这台机器的 root，而不是 console 上的账号——这也正是那一行
  命令可以直接交给管这台机器的人的原因。聊天记录里的 token 在过期之前都要当成一把长期钥匙看。
</Warning>

返回的是节点的身份和一份长期凭证，写在 `FORGE_NODE_STATE_DIR` 下。\*\*凭证只返回一次。\*\*弄丢了就
只能再要一个 token、作为新节点重新加入——这是对的：一台丢了身份的机器，对一个要拿容器跟它对账的
控制面来说，就不是同一台机器了。

节点在 join 时上报自己的清单——卡名、卡数、驱动、容器运行时，以及看不看得见存储根。console 把卡名
映射到硬件系列。注册表不认识的卡会在 join 的输出里点出来，好让管理员补进去；在那之前，指定了系列
的作业不会落到这台机器上。

### 地址不是身份

节点的标识是 console 签发的 id，不是 `host:port`。搬了家的机器还是同一台机器。这带来的后果是，
两条记录合法地答在同一个地址上是可能的，所以「加入时已有另一个活着的节点答在这个地址」是一条警告
而不是拒绝——由 `forgelet join` 打出来，因为最常见的成因是一次重新加入留下了旧记录，于是 console
把一台机器的卡数了两遍。

## 存活判定

一个 `last_seen_at`，对三个阈值来读，因为判错的代价取决于这台机器是干什么的：

| capability | 静默多久  | 后果                                  |
| ---------- | ----- | ----------------------------------- |
| `train`    | 300 秒 | 不再接新活。**在跑的作业不动**                   |
| `serve`    | 45 秒  | 它上面的 revision 立刻标为 not-Ready，部署报降级  |
| `env`      | 45 秒  | 这个 Fleet 不再是候选；在途的 episode 按自己的超时失败 |

这是三种后果，不是三种状态。节点被判为 `gone` 用的是它所在 Fleet 各 capability 里**最宽松**的
那一个；更紧的规则按 capability 单独回答。「现在别往那儿发流量」和「根本别往那儿放东西」是两句
不同的话——否则一个声明了全部三项的 Fleet 会在静默 45 秒后连训练活儿都不接了。

被判为 `gone` 不会杀掉任何东西。节点失联不等于作业死亡：observe 对它上面的作业不返回信号，对账
跳过这一轮，机器回来后按真实容器状态收敛。

`shared-mount` 的节点上报说看不见存储根了，会被 cordon；挂载回来后自动解除。运维自己下的 cordon
永远不会被一次心跳解除——那一个是决定，而心跳是观察。

## 排空、cordon、移除

|                  | 作用范围 | 新活 | 在跑的活 |
| ---------------- | ---- | -- | ---- |
| Fleet `draining` | 全部节点 | 停  | 继续   |
| Fleet `disabled` | 全部节点 | 停  | 继续   |
| 节点 `draining`    | 单个节点 | 停  | 继续   |
| 节点 `cordoned`    | 单个节点 | 停  | 继续   |

它们都不杀东西，这正是重点。`cordoned` 给还会回来的机器，`draining` 给要走的机器。

移除一个 active 的节点会被拒。它的身份正是让它的容器可对账的东西，所以先排空、让活儿跑完。
`force=true` 是给已经没了的机器用的；它会以「强制」记进审计日志，因为它留下的是一个再也没人能
对账的容器。

一个 Fleet 只有在既没有节点、也没有部署钉在上面时才能删。部署不会换 Fleet，所以把它脚下的 Fleet
删掉，会留下一行后端解析不到任何东西的记录：观察不了、停不掉，卡还占着。

## 怎么挑 Fleet

| 工作负载          | 怎么挑                                                                   |
| ------------- | --------------------------------------------------------------------- |
| 训练作业          | 在能 `train`、装得下请求的系列与卡数、且提交者可见的 Fleet 里自动放置                            |
| 训练后作业         | 同一套放置，同一套闸门                                                           |
| 模型部署          | \*\*由提交者点名。\*\*只有一个能 serve 的 Fleet 时替他选；多于一个时，不点名会被拒                  |
| Volume、数据集    | 都不挑。它们是存储，按所在 Fleet 的 `delivery` 抵达作业                                 |
| Playground 会话 | 在能 `serve` 的 Fleet 里放置；只支持 `local` 和 `kuberay`                        |
| 环境            | \*\*在自己的 manifest 里点名。\*\*这个 Fleet 必须声明 `env`，地址在每个作业被准入时从当下有应答的节点里解析 |

部署是那个例外，因为它的地址比这个决定活得更久、而且永远不搬，而 Fleet 记录上没有任何字段说得清
这些机器归哪个部门、什么数据可以碰它们。平台拒绝回答一个它答不了的问题。

放置在准入这一刻带着理由拒绝——capability、可见性、或者装不装得下。没有任何 Fleet 能容纳的作业，
会在提交者还盯着响应的时候就被告知，而不是几个小时后排到队首才发现。

<Note>
  「装不装得下」是一次检查，不是一次预留。到底拿到哪几张卡，权威仍然是分配器；这里挡的只是「这个
  Fleet 从来就没有过的硬件」，免得它在队列里排到天荒地老。
</Note>

## 可见性

`project` 的 Fleet 在项目之外不可见，点名它的作业会被拒为**不存在**而不是无权限——私有 Fleet 的
名字不该从一条错误信息里漏出去。
