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

# Algorithm 插件

> 在运行期给训练循环打补丁——入口签名，以及它什么时候被调用。

```python patch.py theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
def install(params):
    """由 launcher 在训练入口启动之前调用。"""
    import nemo_rl.algorithms.grpo as grpo

    original = grpo.compute_advantages

    def patched(*args, **kwargs):
        advantages = original(*args, **kwargs)
        return advantages * float(params.get("opsd.scale", 1.0))

    grpo.compute_advantages = patched
```

```yaml plugin.yaml theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
schema: forge/plugin/v1
name: opsd-patch
version: 1.2.0
kind: algorithm
entrypoint: patch:install
load: eager
```

这就是一个完整的 algorithm 插件。launcher import `patch`、调用 `install`，
你的改动在这个作业接下来的全过程中生效。

## 你的函数什么时候被调用

两种装载模式，选哪个只取决于一件事：你的补丁需不需要训练期的上下文。

<Tabs>
  <Tab title="eager（默认）">
    由 launcher 在训练入口运行**之前**调用，参数是这次作业的超参。

    ```python theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
    def install(params: Mapping[str, Any]) -> None: ...
    ```

    `params` 就是 `spec.hyperparams`——本次提交摊平后的 `--set` 覆盖值和解析后的配置值。
    补丁只需要替换一个函数时，用 eager。
  </Tab>

  <Tab title="deferred">
    由 launcher 登记，等训练入口把你需要的对象建好之后再调用。

    ```python theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
    def install(params: Mapping[str, Any], **ctx: Any) -> None:
        tokenizer = ctx["tokenizer"]
        max_seq_len = ctx["max_seq_len"]
    ```

    `ctx` 里有什么，取决于训练入口传了什么：

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

    install_deferred("opsd-patch", tokenizer=tokenizer, max_seq_len=4096)
    ```

    补丁需要 tokenizer、模型，或任何训练启动之后才存在的东西时，用 deferred。
  </Tab>
</Tabs>

<Note>
  传给 `install_deferred` 的名字是 manifest 里的 `name`，不是完整的 `<owner>/<name>`。
  传一个没登记的名字会抛错，并列出已登记的有哪些。
</Note>

## launcher 做了什么

<Steps>
  <Step title="校验注入的包">
    重算 `forge_plugins/<name>/` 的摘要，与 JobSpec 比对。不一致就停止作业。
  </Step>

  <Step title="检查 SDK 兼容性">
    manifest 声明了 `requires.core` 时，运行中的 `starforge-core` 必须满足它。
  </Step>

  <Step title="把包根加入 sys.path 并 import">
    这正是「顶层名不得遮蔽真实依赖」要在发布期就拒绝的原因。
  </Step>

  <Step title="调用或登记">
    `eager` → 立即调用 `install(params)`。
    `deferred` → 按 manifest 里的名字登记，等训练入口来调。
  </Step>
</Steps>

集群侧只装载 `kind: algorithm`。其他代码类 kind 会打一行日志跳过——
`environment` 插件由环境机制装载，`data-prep` 插件根本不离开你的笔记本。

## 怎么写这个函数

```python theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
from typing import Any, Mapping

def install(params: Mapping[str, Any]) -> None:
    ...
```

下面几条值得明说，因为违反它们会在很难定位的地方出问题：

* **出真问题就抛。** `install` 里抛异常会停止作业，这是对的：
  一个悄悄没生效的补丁，会产出一个数字含义与实验声明不符的 run。
* **保持幂等。** 多节点或带重启的执行器下，你的模块可能被 import 多次。
  防住「把已经包过一次的函数又包一次」。
* **不要在模块 import 期 import 训练框架**，除非你确定它一定在。
  放进 `install` 里 import——那时你已经确定这是训练容器。
* **配置从 `params` 读，不要从环境变量读。** `params` 会随作业记录下来，
  读的人能看到你的补丁被告知了什么。环境变量不会。

## 为什么这里把 monkey-patch 当成一等机制

<Accordion title="它不是意外，也不是权宜之计">
  对于装不了新依赖的内网集群，一个零依赖、运行期生效的补丁，
  相比分发一个 fork 过的框架镜像是决定性的优势。

  让它可接受而不是鲁莽的，是「补丁被声明、被版本化、被 digest 锁定」这件事：
  平台说得出哪个作业用了哪个补丁的哪个版本。
  一个埋在训练脚本里、没人声明过的 monkey-patch，风险一样大，但完全没有这份记录。
</Accordion>

## 两个来源，一个优先级

一个作业可以从两个地方带补丁：

| 来源                    | 代码在哪                                       |
| --------------------- | ------------------------------------------ |
| `spec.plugins`        | 平台托管的插件包，digest 锁定，注入到 `forge_plugins/`    |
| `spec.recipe.plugins` | recipe 内置的补丁名，代码在上传包的 `common/algorithms/` |

同名时**插件包优先**。显式锁定的版本必须压过隐式内置的。

## 确认成功

作业日志里每个插件一行：

```text theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
plugin  : alice/opsd-patch@1.2.0 loaded
plugin  : alice/opsd-patch@1.2.0 registered (needs runtime context, loaded by the training entrypoint)
```

两行都没有，说明实验根本没引用这个插件——检查 `plugins.lock.json` 后重新提交。
