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

# 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 in the training entrypoint interpreter 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: tuneplane/plugin/v1
name: opsd-patch
version: 1.2.0
capability: algorithm
entrypoint: patch:install
load: eager
```

That is a complete algorithm plugin. The framework runtime bridge 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.

**eager (default)**

Called in the training entrypoint interpreter **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.

**deferred**

Registered in the training entrypoint interpreter, 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 tuneplane.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.

> [!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.

## What the launcher does

**1. Verifies the injected package**

Recomputes the digest of `tuneplane_plugins/<name>/` and compares it to the JobSpec. A mismatch stops
the job.

**2. Checks SDK compatibility**

If the manifest declares `requires.core`, the running `tuneplane` must satisfy it.

**3. Puts the package root on sys.path and imports**

Which is why top-level names that shadow real dependencies are refused at publish time.

**4. Calls or registers**

`eager` → calls `install(params)` immediately.
`deferred` → registers it under the manifest name for the training entrypoint to call.

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

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

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

## Explicit bindings and supported scope

`spec.recipe.plugins` declares required algorithm names. Each must be satisfied by an exact `spec.plugins` lock. There is no `common/algorithms/registry.py` fallback or compatibility adapter.

Installation occurs inside the interpreter that runs the training entrypoint, before framework code. The supported scope is `entrypoint`, including fresh ranks that re-enter the runtime bridge. It does **not** inject arbitrary Ray actors or workers spawned by a framework. Use a framework-native worker initialization hook for those. Other scopes are refused. Algorithm providers are rejected for `custom` and `evalkit`, whose execution paths do not use this algorithm bridge.

Prefer native callbacks or registries over monkey-patching when the framework offers them. In-process providers share Python dependencies; distinct wheels do not create isolated dependency environments.

## Check 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 applied in entrypoint process
plugin  : alice/opsd-patch@1.2.0 registered in entrypoint process (deferred)
```

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


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.