NereidForge Trainer SDK for Python¶
The Trainer SDK is for teams that bring their own training framework — a detection model from another stack, a proprietary network, a classifier with special preprocessing, an LLM/VLM fine-tuning recipe. It is the only integration contract between a training component and Forge, and it is deliberately tiny: environment variables in, versioned artifacts out.
- Package:
nereidforge-trainer(import namenereidforge_trainer), Apache-2.0, Python ≥ 3.11, one runtime dependency (boto3). - No base class, no callbacks into the platform. Forge never imports the trainer's code and the trainer never imports Forge's. Any framework, any library and any weights are allowed; a trainer whose upstream framework is not permissively licensed simply ships in its own image and repository.
- Dogfooded: the in-tree vision trainers (the torch trainer covering all vision tasks, and the PaddleX detection trainer) are built on this same SDK, so custom trainers get no second-class contract.
Developer documentation
This page is a hands-on introduction. The package source, the full torch
reference trainer and the capability-profile schema live in the
NereidForge repository (app/builders/sdk/, app/builders/trainer-torch/)
— see NereidAI.
When to use it¶
| You want to… | Use |
|---|---|
| Train with the built-in torch / PaddleX / LLM trainers and standard options | The Training form — no SDK needed |
| Run your own framework or training code as a Forge job | A custom trainer image built with this SDK |
| Run a one-off Python script without building an image | The custom_script_uri channel on a CV job (see Training) |
| Write a reusable, registerable trainer that appears in the Training form with its own architectures and device support | This SDK + a trainer image + capability profile |
Install¶
The wheel ships with the Forge release (offline-friendly — there is no internet dependency in the machine room):
# from the release source tree, or from any internal wheel mirror
pip install ./app/builders/sdk
Building a self-contained wheel for a customer image:
python -m pip wheel app/builders/sdk --no-deps -w ./wheels
# -> wheels/nereidforge_trainer-<version>-py3-none-any.whl
The package also installs the nf-trainer scaffolding CLI:
nf-trainer init my-detector --name acme/detector --task detection \
--architecture acme-det --framework acme
nf-trainer validate my-detector/trainer-profile.json # 0 ok · 1 invalid · 2 unreadable
nf-trainer profile my-detector/trainer-profile.json # pretty-print
init writes a ready-to-fill project into an empty directory: train.py,
nf_profile.py, trainer_profile.py, trainer-profile.json, Dockerfile,
README.md, .gitignore. It adds no framework dependency — you choose
the stack.
The contract¶
Environment variables¶
| Variable | Direction | Meaning |
|---|---|---|
INPUT_URI |
in | Dataset root: s3://bucket/prefix…, or a local directory in a smoke test |
OUTPUT_DIR |
in | Artifact destination: s3://bucket/models/<job-id>/, or a local directory |
CONFIG_JSON |
in | Flat training config — inline JSON or a path/URI to a JSON file |
ARTIFACT_ID |
in | Artifact basename (the job id); every artifact is named <ARTIFACT_ID>.<ext> |
S3_ENDPOINT · S3_ACCESS_KEY · S3_SECRET_KEY · S3_REGION · S3_BUCKET |
in | Object-store connection, injected by the platform (defaults exist for in-cluster use) |
NF_DEVICE |
in | Baked per vendor image: cpu / cuda / rocm / xpu |
NPROC |
in | Processes per node for multi-GPU torchrun-style launches |
SCRIPT |
in | Entrypoint selector: train (default) · evaluate · export · catalog · profile |
CONFIG_JSON is deliberately a flat JSON object — read it with one call and
treat unknown keys as your own hyperparameters:
export INPUT_URI=s3://nereidforge/datasets/ds-12/3/
export OUTPUT_DIR=s3://nereidforge/models/job-778/
export ARTIFACT_ID=job-778
export CONFIG_JSON='{"epochs": 50, "imgsz": 640, "batch": 16, "arch": "acme-det"}'
python train.py
Output artifacts¶
Publish to OUTPUT_DIR with the artifact id as the basename:
| Object | Vision | LLM / VLM | Consumed for |
|---|---|---|---|
<id>.onnx |
required | — | Edge export / serving graph (opset 17; detector heads export without baked NMS) |
<id>.metadata.json |
required | — | Native model manifest — the only document the inference side trusts |
<id>.metrics.json |
required | required | Final flat metrics plus weights_uri / onnx_uri / manifest_uri — registers the version precisely |
<id>.pt (or framework weights, e.g. .pdparams) |
recommended | adapter directory | Resuming / re-export; LoRA adapters (adapter_config.json …) for LLM |
progress.json |
required | required | Live progress polled by the Forge worker (progress_pct, current_epoch, total_epochs) |
history.jsonl |
optional | typical | Per-step loss curves |
If weights_uri / onnx_uri are missing from <id>.metrics.json, the worker
falls back to scanning the output prefix (.onnx preferred, then
.pt/.pth/.safetensors); setting the URIs explicitly is the precise path.
Datasets¶
download_dataset(INPUT_URI) mirrors every object under the prefix, preserving
keys, into /data by default. The canonical layouts are the same ones the
Datasets page describes: COCO or normalized TXT box labels
(images + per-image label files + a manifest YAML) for detection/segmentation/
pose, ImageFolder for classification, and sharegpt JSONL (+ images/) for
LLM/VLM. An un-split vision corpus is split deterministically at runtime using
the configured split_ratio (default 0.8).
API surface¶
Everything is imported from the top-level package:
from nereidforge_trainer import (
read_config, write_json, Progress,
download_dataset, publish_file, upload_file, s3_client, parse_uri, list_objects,
build_manifest, manifest_outputs,
resolve_augmentation, augmentation_summary,
)
| API | Purpose |
|---|---|
read_config(raw="") -> dict |
Read CONFIG_JSON (inline JSON or file/URI); empty → {} |
write_json(path, data) |
Atomic-ish local JSON helper |
Progress(output_uri, total_epochs) |
.log(epoch, metrics) appends to history and publishes progress.json; .write(epoch, metrics, key="progress.json") writes one record. Percentage caps at 99 until the job finalizes |
download_dataset(input_uri, local_root="/data") |
Mirror an s3:// prefix (or copy a local dir) and return the local root |
publish_file(local_path, output_uri, name) |
Upload to s3://… or copy into a local directory when OUTPUT_DIR is local — the same code runs in-cluster and in a laptop smoke test. Returns the URI |
upload_file(...) |
Strict-S3 variant |
s3_client() · parse_uri(uri) · list_objects(...) |
Low-level S3 helpers wired to the injected env vars |
build_manifest(...) |
Build the native <id>.metadata.json: task, architecture, framework, input size, labels, per-task output tensor contract, preprocessing, metrics, dataset provenance, license |
manifest_outputs(task) |
The standard output-tensor contract for a task (detection, instance_seg, semantic_seg, pose, classification…) |
resolve_augmentation(cfg) · augmentation_summary(aug) |
Shared augmentation policy: presets off/light/medium/strong plus clamped per-knob overrides — the same knobs the UI offers |
A minimal trainer¶
The whole lifecycle — config in, dataset download, progress, artifacts out:
import os
from nereidforge_trainer import (
Progress, read_config, write_json,
download_dataset, publish_file, build_manifest,
)
cfg = read_config()
artifact_id = os.environ.get("ARTIFACT_ID", "model")
output_uri = os.environ.get("OUTPUT_DIR", "")
data_root = download_dataset(os.environ["INPUT_URI"]) # s3:// or local
epochs = int(cfg.get("epochs", 50))
labels = load_class_names(data_root) # your code
progress = Progress(output_uri, epochs)
for epoch in range(1, epochs + 1):
train_one_epoch(epoch, cfg, data_root) # your framework
metrics = evaluate(epoch, cfg, data_root) # {"mAP50": ..., ...}
progress.log(epoch, metrics) # -> progress.json
# Framework-native checkpoint (resume / re-export)
save_checkpoint(f"/tmp/{artifact_id}.pt", labels)
weights_uri = publish_file(f"/tmp/{artifact_id}.pt", output_uri,
f"{artifact_id}.pt")
# Portable inference graph
export_onnx(f"/tmp/{artifact_id}.onnx")
onnx_uri = publish_file(f"/tmp/{artifact_id}.onnx", output_uri,
f"{artifact_id}.onnx")
# Native manifest — labels are 0-based, boxes are in model-input space
manifest = build_manifest(
task="detection", architecture=cfg.get("arch", "custom"),
framework="acme", input_size=int(cfg.get("imgsz", 640)),
labels=labels, metrics=metrics,
dataset={"version": cfg.get("dataset_version", "")},
)
manifest_path = f"/tmp/{artifact_id}.metadata.json"
write_json(manifest_path, manifest)
manifest_uri = publish_file(manifest_path, output_uri,
f"{artifact_id}.metadata.json")
# Final metrics document registers the model version precisely
final = {
**metrics,
"task": "detection",
"architecture": cfg.get("arch", "custom"),
"framework": "acme",
"epochs": epochs,
"weights_uri": weights_uri,
"onnx_uri": onnx_uri,
"manifest_uri": manifest_uri,
}
write_json(f"/tmp/{artifact_id}.metrics.json", final)
publish_file(f"/tmp/{artifact_id}.metrics.json", output_uri,
f"{artifact_id}.metrics.json")
Console output follows one convention: plain print(..., flush=True) with
bracketed prefixes ([train], [data], [export], [evaluate],
[progress]); a user error exits with code 2.
Capability profile¶
A trainer image announces what it can do. The image carries
/app/trainer-profile.json and prints the same document when launched with
SCRIPT=profile (python /app/nf_profile.py). Forge stores the announcement
and renders tasks, architectures and device choices in the Training form from
it — no UI code changes per trainer.
{
"schema": "nf.trainer-profile/1",
"name": "acme/detector",
"framework": {"name": "acme", "version": ">=1.0"},
"devices": ["cpu", "cuda"],
"tier": "extended",
"requires": ["extended.custom_trainer"],
"tasks": [
{
"id": "detection",
"label": "Object detection",
"tier": "extended",
"default_arch": "acme-det",
"architectures": [
{"id": "acme-det", "label": "Acme Detector", "size": 640,
"params_m": 12.4, "tier": "extended", "license": "proprietary"}
]
}
],
"capabilities": [
{"id": "export.onnx", "tier": "core", "entrypoint": "export"}
],
"entrypoints": {
"train": "python /app/train.py",
"evaluate": "python /app/evaluate.py",
"export": "python /app/export.py",
"catalog": "python /app/catalog.py",
"profile": "python /app/nf_profile.py"
},
"env": ["INPUT_URI", "OUTPUT_DIR", "CONFIG_JSON", "ARTIFACT_ID"],
"artifacts": ["<id>.onnx", "<id>.metadata.json", "<id>.metrics.json", "<id>.pt"],
"license": {"code": "proprietary", "weights": "per-model"}
}
Profile rules:
devicesare per image:cpu,cuda,rocm,xpu. Ship one image per supported silicon and bakeNF_DEVICEinto it;--alias-typestyle catch-all images are not a thing.- Only announce tasks and architectures you actually implement;
default_archmust exist in the architecture list. tier(core/extended) may be set at profile, task or architecture level — the most specific wins; anextendedentry must list non-emptyrequiresentitlement keys, checked when the job is created.- Licenses are recorded, not enforced: declare the trainer code license and the per-model weights license; incompatible stacks stay isolated inside the customer's own image.
- The profile schema (
nf.trainer-profile/1) is versioned independently from the SDK package version. - The profile script must be named
nf_profile.py— a file calledprofile.pyat/appshadows the Python standard library.
Register the image¶
An administrator registers the image under System → Images (category
train): a built-context image can announce its own profile, or the profile
JSON is pasted/uploaded against the image record. Its tasks and architectures
then appear in the Training form, and the accelerator choices offered are the
intersection of the profile's devices and what the cluster can allocate.
Build and smoke-test locally¶
The generated Dockerfile installs the SDK from a pinned wheel (or a
build-arg wheel for air-gapped builds), bakes ENV NF_DEVICE=cpu, and
dispatches on SCRIPT:
ARG SDK_WHEEL=
COPY ${SDK_WHEEL} /tmp/sdk.whl
RUN pip install --no-cache-dir /tmp/sdk.whl
# ...your framework and code...
ENTRYPOINT ["sh", "-c", "case \"${SCRIPT:-train}\" in \
evaluate) exec python /app/evaluate.py;; \
export) exec python /app/export.py;; \
profile) exec python /app/nf_profile.py;; \
*) exec python /app/train.py;; esac"]
Because publish_file() falls back to a local copy when OUTPUT_DIR is not
an s3:// URI, the exact image can be smoke-tested on a laptop with a local
dataset:
docker run --rm \
-v ./sample-dataset:/data:ro -v ./out:/out \
-e INPUT_URI=/data -e OUTPUT_DIR=/out -e ARTIFACT_ID=smoke \
-e CONFIG_JSON='{"epochs":1,"imgsz":640,"batch":4,"arch":"acme-det"}' \
registry.example.com/forge/acme-detector:v1
# -> ./out/smoke.onnx, smoke.metadata.json, smoke.metrics.json, progress.json
Responsibilities¶
| The platform owns | The trainer owns |
|---|---|
| Scheduling, CPU/GPU allocation per vendor image | Reading CONFIG_JSON and validating its own hyperparameters |
Dataset versioning, delivery to INPUT_URI, splitting |
Consuming the dataset layout, the actual training loop |
| Injecting S3 credentials and endpoints | Publishing progress.json while the job runs |
| Progress UI, logs, job state machine | Exporting ONNX + manifest (vision) and writing final metrics |
Registration from <id>.metrics.json, evaluation, optimization, deployment |
Framework-native checkpoints; honest licenses in profile/manifest |
Checkpoints and continuation¶
The SDK performs no automatic resume. A trainer saves its own checkpoint
(<id>.pt-style); continued/incremental training is driven through the config
key base_weights_uri (an s3:// URI or a local path): materialize the
previous weights, transfer the backbone, re-initialize changed heads yourself,
and record the lineage (base URI, previous labels, added classes) in the
manifest's extra section. The optional SCRIPT=export entrypoint re-exports an
existing checkpoint to ONNX + manifest without training; SCRIPT=evaluate
writes only a metrics document.
Current status¶
- The SDK has no tagged release yet and is consumed from the dev branch; pin your build to a commit SHA. The in-tree torch vision trainer (all vision tasks) and the PaddleX detection trainer are the first two consumers.
- LLM/VLM built-in trainers already honor the same environment-variable and
progress.json/metrics.jsonconventions (adapter directory asweights_uri); SDK helper and profile coverage for LLM/VLM custom trainers is being aligned next. - The
custom_script_uriscript channel is a quick-experiment path for CV jobs; the image + profile path described here is the supported way to distribute and register a reusable trainer.
Next steps¶
- Training — how jobs, accelerators and custom images are selected from the UI.
- Datasets and Annotations — the layouts that
arrive at
INPUT_URI. - Models — how the registered version is evaluated and deployed.