Skip to main content
Use 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.
Do not reach for custom just to change a learning rate. The catalog methods already wire metrics, checkpoints and images; going custom hands all three back to you.

1. Scaffold

You get 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:
Working directory is the job package root (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

  1. Write checkpoints, logs, and exports under $FORGE_OUT_DIR. Anything in the scratch work tree is gone when the container exits.
  2. Report scalars with starforge.report if you want console charts. Stdout is logs only. The platform does not parse loss= lines.
  3. Honor FORGE_CLUSTER_*.
Recipe artifact globs (what the platform expects to find under the output dir): 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:
HuggingFace Accelerate, one node, one process per GPU:
Do not 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

You do not need 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 starforge works only if the wheel is in the image (or you put it on PYTHONPATH yourself).
--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-url is forbidden.
  • The runner prepends the capsule / kernel roots to PYTHONPATH, so from starforge.report import log works without installing the wheel in the image.
You cannot flip that flag from the experiment directory. Changing it means a new recipe in the catalog. For a normal user who wants console charts today: install 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 with observability: 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.