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

# 从一个工程方案开始

> 一个 recipe、若干 benchmark pack、一套工具链，以及说明「什么才算更好」的验收协议。

```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
tp solution list                    # 这台机器上有哪些
tp solution show rtl-repair         # 它引用了什么，以及什么算更好
tp solution init rtl-repair         # 把它的起点复制进你的项目
```

一个团队要在某一件任务上把模型做好，需要的每一块在这个平台上都已经有了 —— 而这正是问题。一个
recipe、一个环境、两个 benchmark pack、一份 rubric、一套工具链、一种数据形状，是分散在六个地方的
七个对象，各自都没错；把它们拼成「我们是这样做 RTL 修复的」，是每个团队都要从头再做一遍的活。

他们没法互相抄的，恰恰是最花时间的那一块：**验收协议** —— 对哪个基线、怎么留出测试集、至少多少道
题、好多少才算更好。

工程方案（Solution）就是一份声明：点名那七个，并把第八个写下来。

## 工程方案不运行任何东西

它不含奖励函数、不含 harness、不含训练代码。里面每一个字段都是一个**名字**，在安装它的那个部署上
解析，平台从不执行包里的任何东西。这条边界和平台其他地方是同一条：控制面负责解析、校验、存储与调
度，作业里跑什么属于你的项目或某个插件。

一个把 rubric 正文嵌进去的方案，就是那份 rubric 的第二个副本，会和训练真正打分用的那份越走越远。
解析不到的名字会被报成缺失，那是运维能据以行动的事实；漂移了的内嵌副本根本无法被发现。

```yaml solution.yaml theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
schema: tuneplane/solution/v1
name: rtl-repair
title: RTL repair and generation
summary: >
  Train a model to write and fix Verilog against a specification, and judge it by compiling the
  result and running the official testbench.
recipe: trl/sft
benchmarks: [verilogeval-v2, rtllm-v2, ifeval]
toolchain: eda-oss
data_format: spec_rtl
keywords: [rtl, verilog, hardware, repair]
acceptance:
  baseline: prompt
  metrics:
    - key: pass@1
      min_delta: 0.05
    - key: syntax_pass
      min_delta: 0.0
    - key: inst_level_strict_acc
      min_delta: 0.0
  sample_min: 120
  split_by: design
  human_review: true
```

| 字段            | 它点名的东西                                            |
| ------------- | ------------------------------------------------- |
| `schema`      | `tuneplane/solution/v1`。其他值一律被拒，并给出这个构建能读的 schema |
| `name`        | 叶子名，且必须和放清单的那个目录同名                                |
| `title`       | 必填。列表行显示的就是它                                      |
| `summary`     | 可选的说明文字                                           |
| `recipe`      | 一次运行从哪个 recipe 起步，`<owner>/<name>`                |
| `environment` | 任务需要环境时，`<owner>/<name>@<version>`                |
| `benchmarks`  | benchmark pack id。是多个，因为一个数字从来不是全部答案              |
| `rubric`      | 没有工具能判的那部分所用的 Rubric，`<owner>/<name>`             |
| `toolchain`   | 验证阶段需要的 Toolchain Profile                         |
| `data_format` | [九种行形状](/zh-Hans/guides/data-formats)之一           |
| `template`    | `tp solution init` 复制的子目录，默认 `template`           |
| `keywords`    | 自由标签                                              |

**既不点名 recipe、也不点名环境**的清单会被拒：那样用户就没有任何可以起步的东西。不在九种之内的
`data_format` 会被拒，并附上完整列表。

## 每一个官方方案都对着 prompt 基线比

`acceptance.baseline` 接受一个模型引用，或者字符串 `prompt` —— 在同一个基座模型上只用提示词的基
线。四个随附方案用的都是 `prompt`，这是刻意的。

<Note>
  **如果在基座模型上认真写提示词就已经够用，正确的做法是把它发出去并量出来，而不是跑一次训练。**
  没有 prompt 基线的协议永远不会让这种情况浮出水面，于是训练任务照样被立项。
</Note>

协议其余部分的判断标准是一样的：提前老老实实定下来很便宜，事后再老老实实定则不可能。

| 字段                    | 它决定什么                                                                    |
| --------------------- | ------------------------------------------------------------------------ |
| `baseline`            | 一个模型引用，或 `prompt`。看过候选分数之后才选的基线不是基线                                      |
| `metrics[].key`       | 候选必须推动的那个指标                                                              |
| `metrics[].min_delta` | 好多少，是一个**变化量**而不是绝对下限。下限是对任务难度的断言，而在量出基线之前没人做得出这个断言。`0.0` 是合法的，意思是「不许退步」 |
| `metrics[].direction` | `higher`（默认）或 `lower`。延迟和面积就属于不是越高越好的                                    |
| `sample_min`          | 这次比较最少能压在多少道评测题上。四十道题的评测集翻掉一道就动 2.5%，所以低于样本分辨率的 delta 是带小数点的噪声           |
| `split_by`            | 决定训练/评测怎么切的那个元数据字段，和 `spec.data.quality.group_key` 指的是同一件事               |
| `human_review`        | 发布前是否必须有人过目。这是一个字段而不是一个假设：testbench 功能通过是证据，一个「看起来对」的 EDA 脚本不是           |

`min_delta` 不能为负。「越小越好」的指标用 `direction: lower` 表达，而不是写一个负的 delta。

每个方案都至少声明一个 `min_delta: 0.0` 的护栏指标。一个在 RTL 通过率上涨、却悄悄丢掉指令跟随的
模型并没有变好；它只是把一种能力换成了另一种，而且没有说出来。

## 随附的四个

```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
tp solution list
```

| 方案                     | 起步自       | 评测集                                  | 数据形状             | 工具链       |
| ---------------------- | --------- | ------------------------------------ | ---------------- | --------- |
| `rtl-repair`           | `trl/sft` | `verilogeval-v2`、`rtllm-v2`、`ifeval` | `spec_rtl`       | `eda-oss` |
| `testbench-generation` | `trl/sft` | `verilogeval-v2-completion`          | `testbench`      | `eda-oss` |
| `eda-script-assistant` | `trl/sft` | `humaneval`、`ifeval`                 | `script`         | `eda-oss` |
| `eda-log-triage`       | `trl/sft` | `ifeval`                             | `log_root_cause` | —         |

**`rtl-repair`** 从「模型答了」到「有工具说它能用」的路径最短，所以在一个新部署上它是第一个值得试
的。`pass@1` 必须比 prompt 基线高 0.05；`syntax_pass` 和 `inst_level_strict_acc` 不许退步；至少
120 道题；按 `design` 切分。

**`testbench-generation`** 训练模型写 bench 而不是写设计。它那两个指标故意往相反方向拉 —— 一份
bench 既要接受正确的设计，*又*要拒绝坏掉的设计，而 `assert(1)` 单看前一项就能满分。

**`eda-script-assistant`** 是公开数据最少、私有数据最多的那一个。它那两个 pack 是护栏、不是度量：
它真正被判的是团队自己脚本组成的私有题集，那种东西不可能随官方方案一起发布，只能由安装它的部署自
己声明。`human_review` 在这里是承重的 —— 能跑的脚本不等于对的脚本，它可能悄悄约束错了时钟。

**`eda-log-triage`** 不涉及仿真器、也不声明工具链，所以它是这里唯一一个没有 EDA 工具链的部署也能
跑的工作流。它按 `case` 而不是 `design` 切分：同一个故障被报了两次，是一件事实。

## 训练之前先读协议

```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
tp solution show rtl-repair
```

`show` 会打印这个方案引用了什么、它的数据形状，然后按人必须做决定的顺序打印协议：

```text theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
baseline                 prompt
pass@1                   at least +0.05
syntax_pass              must not regress
inst_level_strict_acc    must not regress
minimum tasks            120
split by                 design
human review             required
```

在第一次运行之前把它冻住。这个字段的全部价值，就在于它是先写下来的。

## 把它搭进你的项目

```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
tp solution init rtl-repair --into ./experiments
tp solution init rtl-repair --force          # 覆盖已存在的目录
```

`init` 把方案的 `template/` 目录复制到 `<into>/<name>`，然后就结束了。它不提交、不配置部署，也不
写下任何随后归平台所有的东西。文件落在你的仓库里、归你改；没有任何东西被上传，模板在你的容器里运
行，信任级别和你自己写的任何代码一样。

目标目录已存在时是拒绝而不是合并 —— 一个覆盖了一半的脚手架比两种结果都糟 —— 所以 `--force` 是你
表示「我就是要这么做」的方式。

<Warning>
  **随附的四个方案都没有带 `template/` 目录。** 对它们中任何一个执行 `tp solution init` 都会打印
  `solution '<name>' ships no template` 并以 1 退出。在某个方案带上模板之前，内置方案能用的是
  `list` 和 `show`，而 `init` 是给你自己部署发布的方案用的。
</Warning>

## 这个部署能不能跑某个方案

一个名字只有在有人核对它解析得到才值得发布，而这件核对恰恰是 SDK 做不到的 —— 它知道清单写了什么，
却完全不知道这里有哪些。两个只读接口做这件事：

```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
curl -H "Authorization: Bearer $TUNEPLANE_CLIENT_TOKEN" \
  https://tuneplane.your-company.com/api/solutions
curl -H "Authorization: Bearer $TUNEPLANE_CLIENT_TOKEN" \
  https://tuneplane.your-company.com/api/solutions/rtl-repair
```

列表给出 `ready` 以及没解析到的那些引用 —— 是点名而不是计数，因为人接下来要做的正是把这些装上。
详情再加上每个引用的种类和说明，以及方案声明的那份验收协议。

`ready` 的意思是**每一个**名字都解析得到。少一个 benchmark pack 的方案不是「基本就绪」：验收协议
点名了那个 pack，所以缺它跑出来的结果是对着别的东西量的。

两个路由都是只读。工程方案的安装走的是所有声明式包共用的插件接口，在这里再开一条创建路径，就是给
同一个对象搞出第二套摘要和鉴权规则。

<Note>
  **这个构建里 Toolchain Profile 还不是一个独立对象。** 方案的 `toolchain` 引用是对着部署配置的
  sandbox 镜像解析的，引用上会带一条说明。配了 sandbox 镜像的部署能跑需要工具链的方案，没配的不
  能。
</Note>

## 写你自己的

工程方案是一个 `kind: solution` 插件 —— 声明式包，所以平台只解析、从不运行它：

```text theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
our-rtl-flow/
├── plugin.yaml       # kind: solution
├── solution.yaml     # 上面那份清单
├── template/         # 可选：`tp solution init` 复制的起点
└── README.md
```

```yaml plugin.yaml theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
schema: tuneplane/plugin/v1
name: our-rtl-flow
version: 1.0.0
kind: solution
summary: How we do RTL repair here
```

```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
tp plugin publish ./our-rtl-flow
tp recipe sync                       # 把已发布的包同步到这台机器
tp solution list                     # 它现在以 <owner>/our-rtl-flow 出现在目录里
```

三条规则，在两个不同的地方被检查：

* **`template/` 之外不许有可执行文件**，发布时拒绝。根目录下的 `.py` 意味着有人期待平台去 import
  它，而这正是声明式包绝不能制造的期待。要交给用户的脚手架放在 `template/` 下，在用户自己的环境
  里运行。
* **目录名必须等于 `name`**，加载目录时拒绝。目录叫 `rtl-flow`、清单里写 `name: our-rtl-flow`，
  加载不进去。
* **内置方案不可被遮蔽。** 内置先加载、先写入者胜，所以一个叫 `rtl-repair` 的包不会替换随附的那
  个。两个人跑「同一个」方案却拿到不同协议，对**验收**协议来说比对 recipe 更糟，因为不一样的那个
  东西是结果的定义本身。

已发布的包安装在 `$TUNEPLANE_HOME/solutions/<owner>/<name>/` —— 没设 `TUNEPLANE_HOME` 时就是
`~/.tuneplane` —— 并以 `<owner>/<name>` 引用；内置的保留裸名字。开发用的 checkout 可以用
`TUNEPLANE_SOLUTION_PATH` 追加目录，在 Linux 和 macOS 上用 `:` 分隔。

## 工程方案不做什么

在依赖它之前，把边界说清楚。

* **验收协议是一份记录，不是一道门禁。** 平台不会拿一次运行去和声明的 `baseline` 比、不会按
  `sample_min` 数题目、也不会因为没达到 `min_delta` 就拒绝晋级。真正起强制作用的是
  `spec.evaluation.gates` 里按作业声明的评测门禁 —— 见[运行评测](/zh-Hans/guides/benchmarks)。
  工程方案是团队把协议写下来的地方；遵守它仍然是团队自己的事。
* **`split_by` 是一个意图声明。** 它点名你的切分应当尊重哪个字段。平台不会在提交时替你算泄漏 ——
  哪些通了、哪些没通，见[数据形状与切分](/zh-Hans/guides/data-formats)。
* **控制台还没有工程方案页面。** `tp solution` 和那两个 API 路由就是全部的界面。
* **这里没有任何东西在真实硬件上端到端打过分。** 面向硬件的那几个方案指向的工具链镜像从未在 CI 里
  构建过，指向的评测器也没有任何本项目的 runner 跑得起来；
  [RTL 评测](/zh-Hans/guides/rtl-benchmarks)写明了哪些没有被证明。

## 下一步

[`tp solution` 命令参考](/zh-Hans/cli/solution) · [RTL 评测](/zh-Hans/guides/rtl-benchmarks) ·
[数据形状与切分](/zh-Hans/guides/data-formats) · [Benchmark pack](/zh-Hans/extend/benchmark-packs) ·
[插件](/zh-Hans/extend/plugins)
