> ## 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.

# 用你自己的训练器

> custom/custom 从头到尾：train.sh、环境变量、指标、checkpoint，以及平台不会猜的事

```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
sf new my-trainer --method custom/custom
# 写 experiments/my-trainer/train.sh
sf submit my-trainer --profile h200:8 --image registry.example.com/my-trainer:v1
```

训练器不在 catalog 里时用 `custom/custom`：私有 fork、研究循环、Axolotl、一次性脚本。

平台只跑**一个**文件：`experiments/<name>/train.sh`。它不读 `FRAMEWORK` 变量，
不去找 `run.py`，也绝不会因为别的 adapter 失败就回退到 custom。那个文件里做什么，完全归你。

<Warning>
  不要为了改一个学习率就上 custom。catalog 里的方法已经接好了指标、checkpoint 和镜像；
  改用 custom 等于把这三样又还给你自己。
</Warning>

## 1. 脚手架

```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
cd my-lab
sf new my-custom --method custom/custom
```

得到 `experiments/my-custom/`：`config.yaml`（脚本不读就可以当废纸）、`recipe.lock.json`、`README.md`、`train.sh`。catalog 入口是 `kind: experiment`，`value: train.sh`。改名之后提交会报「custom 入口不存在或越界」。

`train.sh` 必须在实验目录里。adapter 实际执行：

```text theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
bash <train.sh 的绝对路径> [可选 action args]
```

工作目录是**作业包根**（`FORGE_WORK_DIR`），不是实验目录。Python 脚本写成 `"${FORGE_EXP_DIR}/train.py"`。

custom adapter 只编译 `operation=train`。对自定义实验跑 `sf export` / `sf eval`，这条 adapter 不支持。

## 2. 环境变量契约

模板里已经用 `:?` 卡住缺变量的情况。不要改名。

| 变量                                                            | 含义                                                               |
| ------------------------------------------------------------- | ---------------------------------------------------------------- |
| `FORGE_WORK_DIR`                                              | 解开后的作业包根                                                         |
| `FORGE_EXP_DIR`                                               | 本实验目录                                                            |
| `FORGE_OUT_DIR`                                               | 产物目录（已 mkdir）：`<存储根>/runs/<用户>/<实验>/<run_id>/out`。由服务端解析下发，不要自己拼 |
| `FORGE_FRAMEWORK` / `FORGE_RECIPE`                            | `custom` / `custom`（锁文件里的 recipe id 是 `custom/custom`）           |
| `FORGE_CLUSTER_NUM_NODES`                                     | 服务端记账的节点数                                                        |
| `FORGE_CLUSTER_GPUS_PER_NODE`                                 | 服务端记账的每节点卡数                                                      |
| `STARFORGE_ENDPOINT` / `STARFORGE_RUN_ID` / `STARFORGE_TOKEN` | 上报凭据。本机直接 `python train.py` 时没有                                  |
| `STARFORGE_ENABLED`                                           | 绑了 ingest 就是 `1`，否则 `0`                                          |

每个作业还会注入（见[环境变量参考](/zh-Hans/ops/configuration)）：有配置时的 `HF_TOKEN`、`CLUSTER_PROFILE`、`NRL_RUN_ID`、recipe digest，以及可选的 `STARFORGE_JUDGE_*`、`STARFORGE_SANDBOX_*`。

配额和 watchdog 按 `FORGE_CLUSTER_*` 记账。实际占卡超出这个数会被告警甚至停作业。把同样的数字传给 `accelerate launch --num_processes` / `torchrun --nproc_per_node`。

## 3. 脚本必须做的三件事

1. checkpoint、日志文件、导出写到 `$FORGE_OUT_DIR`。写在临时工作树里的东西，容器一退就没了。
2. 要看控制台曲线就用 `starforge.report`。stdout 只进日志页。平台不会去解析 `loss=` 这种行。
3. 遵守 `FORGE_CLUSTER_*`。

recipe 声明的产物 glob（平台会在产物目录下找）：

| glob               | 典型用途                      |
| ------------------ | ------------------------- |
| `checkpoints/*`    | 权重                        |
| `logs`             | 你自己写的文本日志（可选；stdout 已经在流） |
| `hf_export`        | HuggingFace 导出目录          |
| `eval/report.json` | 如果自己写了评测报告                |

路径会做 realpath，必须落在 `FORGE_OUT_DIR` 里面。

## 4. 能真正跑起来的 `train.sh`

把脚手架里的 `exit 1` 换成启动命令。`set -euo pipefail` 已经有了。

单进程：

```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
exec python "${FORGE_EXP_DIR}/train.py" \
  --output-dir "${FORGE_OUT_DIR}" \
  --config "${FORGE_EXP_DIR}/config.yaml"
```

HuggingFace Accelerate，单节点，每卡一个进程：

```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
exec accelerate launch \
  --num_processes "${FORGE_CLUSTER_GPUS_PER_NODE}" \
  "${FORGE_EXP_DIR}/train.py" \
  --output_dir "${FORGE_OUT_DIR}" \
  --config "${FORGE_EXP_DIR}/config.yaml"
```

不要 `cd` 到随便一个目录再写 `./checkpoints`。用变量。

## 5. 指标：`starforge.report`

PyPI 包名 `starforge-core`，代码里 `import starforge`。这个模块不 import transformers 或 Ray。上报失败不会把异常抛进训练循环。没有 `STARFORGE_TOKEN` 就不打网（本机直跑、单测）。设 `STARFORGE_ENABLED=0` 可以强制关掉。

### 手写循环

```python theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
from starforge.report import init, log, finish

init(hparams={"lr": 1e-5, "batch_size": 8})

for step, batch in enumerate(loader):
    loss = train_one(batch)
    log({"train/loss": float(loss)}, step=step)

finish()
```

`init()` 可重复调用。嵌套 dict 会摊平。折不成均值的非标量直接丢掉。`prefix=` 会加命名空间（键上已经有同样前缀则不加）。

`init(monitor_hardware=True)` 会起硬件采样，除非已经有组件设过 hardware-bridge 环境变量。间隔：`STARFORGE_MONITOR_INTERVAL`（秒，默认 `10`）。

### HuggingFace / TRL 回调

`StarForgeCallback` 是鸭子类型（不继承 `TrainerCallback`）。train begin 调 `init`，`on_log` 调 `log`，evaluate 时 `log(..., prefix="validation")`，train end 调 `finish`。

```python theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
from starforge.report import StarForgeCallback
from trl import SFTTrainer

trainer = SFTTrainer(..., callbacks=[StarForgeCallback()])
trainer.train()
```

### 奖励或环境里

```python theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
from starforge.report import log

log({"env/tool_success_rate": 0.83, "env/avg_turns": 3.2}, step=step)
```

不必先 `init()`。有凭据时 `log()` 会自己建会话，但不起硬件线程。

不要自己 POST `/api/ingest/logs`。stdout 已经在转。再走一条会打成双份。

不要在脚本里写死控制台 URL。容器里已经有 `STARFORGE_ENDPOINT`。

## 6. 观测：`external` 和 `platform`

catalog 里 `custom/custom` 的 `adapter_options.observability` 是 `external`。两件事：

* 提交**必须**带 `--observability-url`（你们 wandb/swanlab 一类地址）。缺了会在编译期失败：`custom external observability 要求 spec.framework.observability_url`。
* adapter **不会**改 `PYTHONPATH`。`import starforge` 只有镜像里装了 wheel（或你自己改 `PYTHONPATH`）才行。

```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
sf submit my-custom --profile h200:8 \
  --image myregistry.io/my-train:v1 \
  --observability-url https://wandb.example/my-proj
```

`--observability-url` 会变成进程环境里的 `FORGE_EXTERNAL_OBSERVABILITY_URL`。平台不会替你启动 wandb。

如果 recipe 是 `observability: platform`（要改 catalog，由发 `starforge-core` 的人做）：

* **禁止** `--observability-url`。
* runner 把 capsule / 内核根加到 `PYTHONPATH` 前面，镜像里不装 wheel 也能 `from starforge.report import log`。

实验目录改不了这个开关。要换就得在 catalog 里发新 recipe。普通用户今天想看控制台曲线：**镜像里装 `starforge-core`**，同时仍要带 `--observability-url`，因为已发布的 recipe 是 `external`。

`STARFORGE_ENABLED=1` 是另一件事：服务端绑了 ingest token 就会设。目录 custom 仍然要额外的 URL 字段。

## 7. 提交

```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
git add -A && git commit -m "custom trainer"
sf submit my-custom --profile h200:8 \
  --image myregistry.io/my-train:v1 \
  --observability-url https://wandb.example/my-proj
```

custom 的 `--image` 是必填。`FORGE_ALLOWED_IMAGE_REGISTRIES` 为空时**拒绝**自定义用户镜像（一等框架仍可用部署默认镜像）。让管理员把仓库主机名加进去。

一等框架的解析顺序是 `--image` → 控制台默认 → runtime 注册表 → catalog。custom 的 `runtime.default_version` 是 `user-managed`，没有 catalog 里的 OCI 钉死，所以 `--image` 就是镜像。

tag 能用。生产用 `@sha256:…`，避免准入之后 tag 还在飘。

## 8. 图表空、秒退、import 失败

| 现象                               | 查什么                                                                   |
| -------------------------------- | --------------------------------------------------------------------- |
| 几秒就失败，日志是「还没填训练命令」或 `exit 2`     | 脚手架 `train.sh` 没换成自己的命令。                                              |
| 日志在走，图表不动                        | 没调 `log()` / `StarForgeCallback`。或 `import starforge` 失败。或还在加载权重（再等）。 |
| `ModuleNotFoundError: starforge` | 镜像没装 wheel，且 recipe 是 `external`，PYTHONPATH 不会被改。                     |
| 本机能 `import starforge`，作业里不能     | `train.sh` 用的 python 不是镜像 venv 里那个。把 `PATH` 对齐。                       |
| 图表空、日志也空                         | 容器没起来。状态 `PENDING` / 拉镜像。看事件页。                                        |
| 图表空，服务端 ingest 报错                | `FORGE_INGEST_URL` 是 `127.0.0.1`，或 GPU 节点访问不到。                        |
| checkpoint 消失                    | 写到了 cwd 或 `/tmp`。用 `$FORGE_OUT_DIR/checkpoints`。                      |
| 白名单错误                            | `--image` 的仓库主机不在 `FORGE_ALLOWED_IMAGE_REGISTRIES`。                   |
| `custom adapter 尚不支持 'export'`   | custom 没有 export/eval 编译路径。                                           |

进容器里可以：

```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
python -c "import starforge.report; print('ok')"
echo "$FORGE_OUT_DIR" "$FORGE_CLUSTER_GPUS_PER_NODE"
env | grep STARFORGE
```

## 9. 可选的 `config.yaml`

custom adapter 不读 Hydra。想让 `sf validate` 有用，也只覆盖 **custom recipe 的 `params:`** 里声明的键（目前是空的）。把 `config.yaml` 当你自己的文件，在 `train.py` 里解析。

提交时的 `--set` 会进 `spec.hyperparams`。custom 不会把它映射成 argv，除非你自己写。

`--model` / `--train-data` 是 verl/TRL 的绑定。custom 不用，除非你去读 JobSpec（不要；用环境和你打进包里的文件）。

## 10. 如果你在维护平台

要发一等方法（新框架，或 `observability: platform` 的 custom 变体）是 catalog 变更：`core/starforge/recipes/catalog/<framework>/<recipe>/`、一个 `FrameworkAdapter`、测试、钉 digest 的镜像，然后 CLI/服务端握手。步骤在仓库 `docs/framework-adapters.md`。已经部署好的控制台用户，不能靠 `sf new` 完成这件事。
