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

> Patch a training loop at runtime — the entrypoint signature, and when it is called.

```python patch.py theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
def install(params):
    """Called by the launcher before the training entrypoint starts."""
    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
```

That is a complete algorithm plugin. The launcher imports `patch`, calls `install`, and your change
is in effect for the rest of the job.

## When your function is called

Two load modes, and the choice is about one thing: whether your patch needs the training context.

<Tabs>
  <Tab title="eager (default)">
    Called by the launcher **before** the training entrypoint runs, with the job's hyperparameters.

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

    `params` is `spec.hyperparams` — the flattened `--set` overrides and the resolved config values
    for this submission. Use eager when your patch only needs to replace a function.
  </Tab>

  <Tab title="deferred">
    Registered by the launcher, then called by the training entrypoint once it has built the objects
    you need.

    ```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"]
    ```

    What lands in `ctx` is whatever the training entrypoint passes:

    ```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)
    ```

    Use deferred when the patch needs a tokenizer, a model, or anything else that does not exist
    until training has started.
  </Tab>
</Tabs>

<Note>
  The name passed to `install_deferred` is the manifest's `name`, not the full `<owner>/<name>`.
  Calling it with an unregistered name raises and lists what is registered.
</Note>

## What the launcher does

<Steps>
  <Step title="Verifies the injected package">
    Recomputes the digest of `forge_plugins/<name>/` and compares it to the JobSpec. A mismatch stops
    the job.
  </Step>

  <Step title="Checks SDK compatibility">
    If the manifest declares `requires.core`, the running `starforge-core` must satisfy it.
  </Step>

  <Step title="Puts the package root on sys.path and imports">
    Which is why top-level names that shadow real dependencies are refused at publish time.
  </Step>

  <Step title="Calls or registers">
    `eager` → calls `install(params)` immediately.
    `deferred` → registers it under the manifest name for the training entrypoint to call.
  </Step>
</Steps>

Only `kind: algorithm` is loaded on the cluster. Other executable kinds are skipped with a log line —
an `environment` plugin is loaded by the environment machinery, and a `data-prep` plugin never
leaves your laptop.

## Writing the function

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

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

Rules that are worth stating because breaking them fails in confusing places:

* **Raise on a real problem.** An exception during `install` stops the job. That is correct: a patch
  that silently did not apply produces a run whose numbers mean something other than what the
  experiment claims.
* **Be idempotent.** With multi-node or restart-based executors your module can be imported more than
  once. Guard against double-wrapping a function you already wrapped.
* **Do not import the training framework at module import time** unless you are certain it is
  present. Import inside `install`, where you know the container is the training container.
* **Read configuration from `params`, not from the environment.** `params` is recorded with the job,
  so a reader can see what your patch was told. An environment variable is not.

## Why monkey-patching is a supported mechanism here

<Accordion title="It is not an accident, and it is not a workaround">
  For a cluster on an isolated network that cannot install new dependencies, a zero-dependency patch
  applied at runtime is a decisive advantage over shipping a forked framework image.

  What makes it acceptable rather than reckless is that the patch is declared, versioned and digest
  pinned: the platform can say which patch, at which version, ran in which job. An undeclared
  monkey-patch buried in a training script has all the same risks and none of that record.
</Accordion>

## Two sources, one resolution order

A job can carry patches from two places:

| Source                | Where the code lives                                                                 |
| --------------------- | ------------------------------------------------------------------------------------ |
| `spec.plugins`        | Platform-hosted plugin packages, digest locked, injected into `forge_plugins/`       |
| `spec.recipe.plugins` | Patch names built into a recipe, code in the uploaded package's `common/algorithms/` |

When both name the same patch, **the plugin package wins**. An explicitly locked version has to beat
an implicit built-in.

## Confirm it worked

The job log shows one line per plugin:

```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)
```

If you see neither, the experiment does not reference the plugin — check `plugins.lock.json` and
resubmit.
