custom/custom when your trainer is not in the catalog: a private fork, a research loop,
Axolotl, a one-off script.
The platform runs exactly one file: experiments/<name>/train.sh. It 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 entirely yours.
1. Scaffold
experiments/my-custom/ with config.yaml (unused unless your script reads it), recipe.lock.json, README.md, and train.sh. The catalog entrypoint is kind: experiment, value: train.sh. Rename that file and submit will fail with “custom 入口不存在或越界”.
train.sh must be a file inside the experiment directory. The adapter runs:
FORGE_WORK_DIR), not the experiment folder. Point Python at "${FORGE_EXP_DIR}/train.py".
The custom adapter only compiles operation=train. sf export / sf eval against a custom experiment are not supported by this adapter.
2. Environment contract
The template already:? dies if these are missing. Do not rename them.
Also injected for every job (see the env reference):
HF_TOKEN when the server has one, CLUSTER_PROFILE, NRL_RUN_ID, recipe digests, optional STARFORGE_JUDGE_* and STARFORGE_SANDBOX_*.
Quota and the watchdog use FORGE_CLUSTER_*. Occupying more GPUs than that number will get the job warned or stopped. Pass the same numbers into accelerate launch --num_processes / torchrun --nproc_per_node.
3. Three things the script must do
- Write checkpoints, logs, and exports under
$FORGE_OUT_DIR. Anything in the scratch work tree is gone when the container exits. - Report scalars with
starforge.reportif you want console charts. Stdout is logs only. The platform does not parseloss=lines. - Honor
FORGE_CLUSTER_*.
Paths are realpath-checked and must stay inside
FORGE_OUT_DIR.
4. train.sh you can actually run
Replace the scaffold exit 1 with a launch. set -euo pipefail is already there.
Single process:
cd into a random directory and write ./checkpoints. Use the variables.
5. Metrics: starforge.report
PyPI name starforge-core, import starforge. The module does not import transformers or Ray. Reporting never raises into your training loop. No STARFORGE_TOKEN means no network (local runs, unit tests). Set STARFORGE_ENABLED=0 to force that off.
Manual loop
init() is idempotent. Nested dicts are flattened. Non-scalars that cannot be reduced to a mean are dropped. prefix= prepends a namespace if the key does not already have it.
Hardware sampling starts in init(monitor_hardware=True) unless another component already set the hardware-bridge env. Interval: STARFORGE_MONITOR_INTERVAL (seconds, default 10).
HuggingFace / TRL callback
StarForgeCallback is duck-typed (it does not subclass TrainerCallback). It calls init on train begin, log on on_log, log(..., prefix="validation") on evaluate, finish on train end.
From a reward or env
init() first. With credentials, log() opens a session without the hardware thread.
Do not POST /api/ingest/logs yourself. Stdout is already forwarded. A second path duplicates lines.
Do not hard-code the console URL. The container already has STARFORGE_ENDPOINT.
6. Observability: external vs platform
The catalog custom/custom recipe sets adapter_options.observability: external. That has two consequences:
- Submit requires
--observability-url(any URL your team uses for wandb/swanlab/etc.). Missing it fails compile:custom external observability 要求 spec.framework.observability_url. - The adapter does not rewrite
PYTHONPATH.import starforgeworks only if the wheel is in the image (or you put it onPYTHONPATHyourself).
--observability-url is stored as FORGE_EXTERNAL_OBSERVABILITY_URL in the process env. The platform does not start wandb for you.
If the recipe were observability: platform (a catalog change by the people who ship starforge-core):
--observability-urlis forbidden.- The runner prepends the capsule / kernel roots to
PYTHONPATH, sofrom starforge.report import logworks without installing the wheel in the image.
starforge-core in the image and still pass --observability-url because the published recipe is external.
STARFORGE_ENABLED=1 is independent: that is the ingest binding the server always sets when the job has an ingest token. Catalog custom still wants the extra URL field.
7. Submit
--image is required for custom. Empty FORGE_ALLOWED_IMAGE_REGISTRIES rejects custom user images (first-class frameworks can still use deployment defaults). Ask an admin to add your registry hostname.
Resolution for first-class frameworks is --image → console default → runtime registry → catalog. Custom has no catalog OCI pin (runtime.default_version is user-managed), so --image is the image.
Tags work. Pin @sha256:… in production so a moving tag cannot change the job after admit.
8. Empty charts, job died immediately, import errors
Inside the container:
9. Optional config.yaml
Nothing in the custom adapter reads Hydra. If you want sf validate to do something useful, you still only get what the custom recipe params: declares (currently empty). Treat config.yaml as your own file and parse it in train.py.
--set on submit fills spec.hyperparams. Custom does not map those onto argv unless you write that yourself.
--model / --train-data are the verl/TRL bindings. Custom ignores them unless your script looks at the JobSpec (it should not; use env and files you packed).
10. If you maintain the platform
Shipping a first-class method (new framework or a custom variant withobservability: platform) is a catalog change: core/starforge/recipes/catalog/<framework>/<recipe>/, a FrameworkAdapter, tests, a digest-pinned image, then CLI/server handshake. Steps are in the repo file docs/framework-adapters.md. Users of a deployed console do not do that from sf new.