custom/custom runs exactly one file: experiments/<name>/train.sh. The platform does not read
a FRAMEWORK variable, does not look for a run.py, and never falls back to custom because
another adapter failed. What that file does is yours.
Scaffold
train.sh, plus config.yaml, recipe.lock.json and README.md. The recipe’s
entrypoint is train.sh by name, so renaming it fails submission with custom entrypoint is missing or escapes the experiment directory.
The adapter runs bash <absolute path to train.sh>. The working directory is the job package
root, not the experiment folder, so point Python at "${TUNEPLANE_EXP_DIR}/train.py" rather
than a relative path.
The custom adapter only compiles the training operation. tp export and tp eval against a
custom experiment are refused.
The environment contract
The scaffold dies with:? if any of these is missing. Do not rename them.
HF_HOME, HF_TOKEN and the *_DATA_DIR variables arrive too, when the deployment has them.
The full list is in the job environment reference.
Quota and the watchdog are measured against TUNEPLANE_CLUSTER_*. Occupying more GPUs than
that gets the job warned and can get it stopped, so pass the same numbers into
accelerate launch --num_processes or torchrun --nproc_per_node.
Three things your script must do
- Write everything you want to keep under
$TUNEPLANE_OUT_DIR. The scratch tree disappears when the container exits. - Report scalars with
tuneplane.report, or declare native TensorBoard/JSONL outputs for the independent collector. Stdout alone does not produce metric points. - Respect
TUNEPLANE_CLUSTER_*.
Write train.sh
set -euo pipefail is already there. Replace the scaffold’s exit 1 with a launch.
cd somewhere and write ./checkpoints. Use the variables.
Report metrics
The PyPI name istuneplane and the import is tuneplane. The reporting module pulls in
neither transformers nor Ray, and it never raises into your training loop. With no
TUNEPLANE_JOB_TOKEN it does nothing, which is what you want outside a job.
init() is idempotent. Nested dicts are flattened, and a non-scalar that cannot be reduced to a
mean is dropped. prefix= prepends a namespace when the key does not already carry one.
TunePlaneCallback is duck-typed rather than a TrainerCallback subclass. It calls init on
train begin, log on on_log, log(..., prefix="validation") on evaluate, and finish at the
end.
Hardware sampling starts with init(monitor_hardware=True), on rank zero only — one sampler per
job, not one per GPU. The interval is TUNEPLANE_JOB_MONITOR_INTERVAL, in seconds, default
10.
Two things not to do: do not POST to the ingest API yourself, because stdout is already
forwarded and a second path duplicates lines; and do not hard-code the console URL, because the
container already has TUNEPLANE_JOB_ENDPOINT.
Why —observability-url is required
The publishedcustom/custom recipe declares observability: external. Two consequences:
- Submission requires
--observability-url. Without it, compile fails withcustom external observability requires spec.framework.observability_url. Any URL your team uses works; the platform does not start wandb for you. It reaches the job asTUNEPLANE_EXTERNAL_OBSERVABILITY_URL. - The adapter does not rewrite
PYTHONPATH.import tuneplaneworks only if the wheel is in your image, or you put it onPYTHONPATHyourself.
observability: platform behaves the other way round: --observability-url
is refused, and the runner prepends the capsule roots to PYTHONPATH so
from tuneplane.report import log works with no wheel in the image. You cannot flip that from
your experiment directory — it is a property of the published recipe.
For SDK-based charts with this recipe, install tuneplane in your image and still pass
--observability-url. Native collection is a separate output declaration and deployment path.
TUNEPLANE_JOB_ENABLED=1 is separate. That is the ingest binding the server always sets when
the job has an ingest token.
Submit
--image is the image: custom has no catalogue pin, since its runtime.default_version is
user-managed. Tags work; pin @sha256:… in production so a moving tag cannot change the job
after admission.
An empty TUNEPLANE_ALLOWED_IMAGE_REGISTRIES refuses custom user images, while catalogue
frameworks can still use the deployment’s defaults. Ask an administrator to add your registry
hostname. See custom images.
Check it worked
tp job logs shows your own launch line, then your trainer’s output. The Charts tab gets its
first point after your first log() call.
Inside the container:
When it does not work
config.yaml is yours
Nothing in the custom adapter reads it. The custom recipe declares no parameters, sotp validate has nothing to check and --set values land in spec.hyperparams without
reaching your argv. Parse the file in train.py.
--model and --train-data are the verl and TRL bindings. Custom ignores them: use the
environment and the files you packed.