patch.py
plugin.yaml
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)
- deferred
Called in the training entrypoint interpreter before the training entrypoint runs, with the job’s hyperparameters.
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.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.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
- Raise on a real problem. An exception during
installstops 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.paramsis 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
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:plugins.lock.json and
resubmit.