Sign in Get started

Training

Spawn, poll, and cancel training runs against a completed export.

View as Markdown

Training is always on an export - build one first, then train it. See the training guide for the end-to-end flow.

Pipelines

pipeline_type Output
yolox Object detection (boxes)
sm_pytorch Semantic segmentation
classification Image classification
rfdetr_detection Object detection (RT-DETR)
rfdetr_segmentation Instance segmentation (RT-DETR)
rfdetr_keypoint Keypoint / pose detection (RT-DETR)

GPU tiers

gpu_type Approx. cost Pick for
a10g (default) ~$0.30/hr YOLOX, classification, RF-DETR detection
a100 ~$2/hr Large RF-DETR, big batches
h100 ~$4/hr Last resort, only when A100 runs out of memory
auto - Resolved at submit to the cheapest tier whose memory fits the config. Runs always report the concrete tier they got.

create

Spawn a run against an existing completed export.

Source: Training.create

Arg Type Default Notes
dataset_name str required Dataset name.
export_name str required Pre-built, completed export
pipeline_type PipelineType required See table above
name str required Human-readable label (1-100 chars)
config dict | str | Path {} epochs, batch_size, learning_rate, image_size, class_overrides. A path loads a downloaded config.json
gpu_type GpuType "a10g" "auto" picks the cheapest tier the config fits
gpu_count int 1 1-4, RF-DETR only. >1 bills gpu_count x the rate
version_of_model_id str | None None Append to an existing model as a new version
wait bool True When False, returns immediately with status="queued"
poll_interval float 5.0 Seconds between polls
timeout float 7200.0 Max poll seconds (2 hours)
run = client.training.create(
    dataset_name="road-signs",
    export_name="road-signs-20260512-120000",
    pipeline_type="yolox",
    name="road-signs-detector",
    config={"epochs": 50},
    gpu_type="a10g",
    wait=True,
)
print(run.status, run.model_id)
pictograph train start road-signs road-signs-20260512-120000 \
  --pipeline yolox --name road-signs-detector --gpu a10g \
  --config '{"epochs": 50}' --no-wait
curl -s -X POST "https://api.pictograph.io/api/v1/developer/training/" \
  -H "X-API-Key: $PICTOGRAPH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"dataset_name": "road-signs", "export_name": "road-signs-20260512-120000", "pipeline_type": "yolox", "name": "road-signs-detector", "config": {"epochs": 50}, "gpu_type": "a10g"}'

Returns TrainingRun

TrainingRun · 21 fields
class TrainingRun(BaseModel):
    """A single training job."""
    id: str
    organization_id: str
    name: str
    dataset_id: str | None = None
    export_id: str | None = None
    model_id: str | None = None
    pipeline_type: Literal['yolox', 'sm_pytorch', 'classification', 'rfdetr_detection', 'rfdetr_segmentation', 'rfdetr_keypoint']
    gpu_type: Optional[Literal['a10g', 'a100', 'h100', 'auto']] = None
    status: Literal['pending', 'queued', 'running', 'completed', 'failed', 'cancelled']
    progress: int = 0
    current_epoch: int = 0
    total_epochs: int | None = None
    metrics: dict[str, Any] = {}
    config: dict[str, Any] = {}
    eta_seconds: int | None = None
    training_time_seconds: int | None = None
    error_message: str | None = None
    started_at: datetime | None = None
    completed_at: datetime | None = None
    created_at: datetime
    created_by: str | None = None

Training-time augmentation

Every pipeline accepts a standardized augmentation block inside config. The ops are applied on the fly during training - no images are added to your dataset - and each pipeline honors them through its framework’s native machinery, so boxes, polygons, and masks stay geometry-correct automatically.

{
  "augmentation": {
    "ops": [
      { "op": "flip", "p": 0.5 },
      { "op": "brightness", "factor": 0.2, "p": 0.3 }
    ],
    "framework": { "mosaic_prob": 1.0 }
  }
}
  • ops - per-image transforms in the shared augment vocabulary: flip, vflip, rotate90, rotate, shear, brightness, contrast, saturation, hue_shift, grayscale, blur, noise, cutout. Each entry carries a probability p in [0, 1] plus that op’s strength parameters.
  • framework - pipeline-specific extras applied inside the training loop: YOLOX mosaic_prob / mixup_prob / hsv_prob / degrees / translate / shear; semantic segmentation perspective. RF-DETR and classification have none.

Which ops a pipeline supports, and its defaults, are served by GET /api/v1/training/augmentation-profiles. Omit the block and the pipeline trains with its historical defaults; either way create records the resolved values into the run’s stored config, so the model’s downloadable config.json always says exactly which augmentation applied. An unsupported op, a probability outside [0, 1], or an out-of-range strength is rejected with a 400 at submit.

list

One page of training runs in your organization.

Source: Training.list

Arg Type Default Notes
dataset_name str | None None Restrict to one dataset
status TrainingStatus | None None e.g. "running"
limit int 50 Server cap: 100
offset int 0 Page offset
for run in client.training.list(status="running", limit=20):
    print(run.id, run.name, run.status, run.progress)
pictograph train list --status running --limit 20
curl -s "https://api.pictograph.io/api/v1/developer/training/?limit=20&status=running" \
  -H "X-API-Key: $PICTOGRAPH_API_KEY"

Returns list[TrainingRun]

TrainingRun · 21 fields
class TrainingRun(BaseModel):
    """A single training job."""
    id: str
    organization_id: str
    name: str
    dataset_id: str | None = None
    export_id: str | None = None
    model_id: str | None = None
    pipeline_type: Literal['yolox', 'sm_pytorch', 'classification', 'rfdetr_detection', 'rfdetr_segmentation', 'rfdetr_keypoint']
    gpu_type: Optional[Literal['a10g', 'a100', 'h100', 'auto']] = None
    status: Literal['pending', 'queued', 'running', 'completed', 'failed', 'cancelled']
    progress: int = 0
    current_epoch: int = 0
    total_epochs: int | None = None
    metrics: dict[str, Any] = {}
    config: dict[str, Any] = {}
    eta_seconds: int | None = None
    training_time_seconds: int | None = None
    error_message: str | None = None
    started_at: datetime | None = None
    completed_at: datetime | None = None
    created_at: datetime
    created_by: str | None = None

iter

Auto-paging iterator across every run in your org. Same filters as list, plus page_size and max_total. The CLI and REST page manually with limit and offset.

Source: Training.iter

Arg Type Default Notes
dataset_name str | None None Dataset name.
status TrainingStatus | None None Only runs in this state, e.g. completed or failed.
page_size int 50 Rows fetched per underlying request. Tuning only - the iterator yields every item either way.
max_total int | None None Stop after this many items. None walks everything.
for run in client.training.iter(page_size=50):
    print(run.id, run.status, run.progress)
# The CLI does not auto-page; this is a single page.
pictograph train list --limit 100
curl -s "https://api.pictograph.io/api/v1/developer/training/?limit=100&offset=100" \
  -H "X-API-Key: $PICTOGRAPH_API_KEY"

Returns OffsetPager[TrainingRun]

TrainingRun · 21 fields
class TrainingRun(BaseModel):
    """A single training job."""
    id: str
    organization_id: str
    name: str
    dataset_id: str | None = None
    export_id: str | None = None
    model_id: str | None = None
    pipeline_type: Literal['yolox', 'sm_pytorch', 'classification', 'rfdetr_detection', 'rfdetr_segmentation', 'rfdetr_keypoint']
    gpu_type: Optional[Literal['a10g', 'a100', 'h100', 'auto']] = None
    status: Literal['pending', 'queued', 'running', 'completed', 'failed', 'cancelled']
    progress: int = 0
    current_epoch: int = 0
    total_epochs: int | None = None
    metrics: dict[str, Any] = {}
    config: dict[str, Any] = {}
    eta_seconds: int | None = None
    training_time_seconds: int | None = None
    error_message: str | None = None
    started_at: datetime | None = None
    completed_at: datetime | None = None
    created_at: datetime
    created_by: str | None = None

get

Fetch a run’s current status, metrics, and progress. status is one of pending, queued, running, completed, failed, cancelled.

Source: Training.get

Arg Type Default Notes
name str | None None Run name. Pass this or the positional run id.
run_id str | None None Run id.
run = client.training.get(
    name="signs-detector",
)
print(run.status, run.progress, run.current_epoch, "/", run.total_epochs)
pictograph train status signs-detector
curl -s "https://api.pictograph.io/api/v1/developer/training/$RUN_ID" \
  -H "X-API-Key: $PICTOGRAPH_API_KEY"

Returns TrainingRun

TrainingRun · 21 fields
class TrainingRun(BaseModel):
    """A single training job."""
    id: str
    organization_id: str
    name: str
    dataset_id: str | None = None
    export_id: str | None = None
    model_id: str | None = None
    pipeline_type: Literal['yolox', 'sm_pytorch', 'classification', 'rfdetr_detection', 'rfdetr_segmentation', 'rfdetr_keypoint']
    gpu_type: Optional[Literal['a10g', 'a100', 'h100', 'auto']] = None
    status: Literal['pending', 'queued', 'running', 'completed', 'failed', 'cancelled']
    progress: int = 0
    current_epoch: int = 0
    total_epochs: int | None = None
    metrics: dict[str, Any] = {}
    config: dict[str, Any] = {}
    eta_seconds: int | None = None
    training_time_seconds: int | None = None
    error_message: str | None = None
    started_at: datetime | None = None
    completed_at: datetime | None = None
    created_at: datetime
    created_by: str | None = None

wait_for_completion

Block until a run reaches a terminal status. Use this after create(wait=False). Raises PollTimeoutError if timeout elapses; the run keeps going server-side.

Source: Training.wait_for_completion

Arg Type Default Notes
name str | None None The run’s name (positional). A UUID works here too.
run_id str | None None The run’s UUID - the keyword alternative to name.
poll_interval float 5.0 Seconds between checks (default 5s).
timeout float 7200.0 Maximum seconds to wait (default 7200 = 2h).
run = client.training.wait_for_completion(
    name="signs-detector",
    poll_interval=5.0,
    timeout=7200.0,
)
if run.status == "completed":
    model = client.models.get(
        model_id=run.model_id,
    )
    print(model.name, model.metrics)
pictograph train wait signs-detector --timeout 7200
# Poll the get endpoint until "status" reaches a terminal value.
curl -s "https://api.pictograph.io/api/v1/developer/training/$RUN_ID" \
  -H "X-API-Key: $PICTOGRAPH_API_KEY"

Returns TrainingRun

TrainingRun · 21 fields
class TrainingRun(BaseModel):
    """A single training job."""
    id: str
    organization_id: str
    name: str
    dataset_id: str | None = None
    export_id: str | None = None
    model_id: str | None = None
    pipeline_type: Literal['yolox', 'sm_pytorch', 'classification', 'rfdetr_detection', 'rfdetr_segmentation', 'rfdetr_keypoint']
    gpu_type: Optional[Literal['a10g', 'a100', 'h100', 'auto']] = None
    status: Literal['pending', 'queued', 'running', 'completed', 'failed', 'cancelled']
    progress: int = 0
    current_epoch: int = 0
    total_epochs: int | None = None
    metrics: dict[str, Any] = {}
    config: dict[str, Any] = {}
    eta_seconds: int | None = None
    training_time_seconds: int | None = None
    error_message: str | None = None
    started_at: datetime | None = None
    completed_at: datetime | None = None
    created_at: datetime
    created_by: str | None = None

cancel

Stop a run in flight and mark it cancelled. Billing is charge-on-success, so a run cancelled before it finishes is never charged. Requires member+ role.

Source: Training.cancel

Arg Type Default Notes
name str | None None Run name. Pass this or the positional run id.
run_id str | None None Run id.
client.training.cancel(
    name="signs-detector",
)
pictograph train cancel signs-detector --yes
curl -s -X POST "https://api.pictograph.io/api/v1/developer/training/$RUN_ID/cancel" \
  -H "X-API-Key: $PICTOGRAPH_API_KEY"

Returns TrainingRun

TrainingRun · 21 fields
class TrainingRun(BaseModel):
    """A single training job."""
    id: str
    organization_id: str
    name: str
    dataset_id: str | None = None
    export_id: str | None = None
    model_id: str | None = None
    pipeline_type: Literal['yolox', 'sm_pytorch', 'classification', 'rfdetr_detection', 'rfdetr_segmentation', 'rfdetr_keypoint']
    gpu_type: Optional[Literal['a10g', 'a100', 'h100', 'auto']] = None
    status: Literal['pending', 'queued', 'running', 'completed', 'failed', 'cancelled']
    progress: int = 0
    current_epoch: int = 0
    total_epochs: int | None = None
    metrics: dict[str, Any] = {}
    config: dict[str, Any] = {}
    eta_seconds: int | None = None
    training_time_seconds: int | None = None
    error_message: str | None = None
    started_at: datetime | None = None
    completed_at: datetime | None = None
    created_at: datetime
    created_by: str | None = None

bulk_cancel

Cancel many runs in one org-scoped call. Already-terminal, foreign, or missing ids land in not_found rather than failing the call.

Source: Training.bulk_cancel

Arg Type Default Notes
run_ids Sequence[str] required Run ids to act on. Ids that do not resolve are reported, not raised.
result = client.training.bulk_cancel(
    run_ids=[
        "d1f2a3b4-5c6d-4e70-8f91-a2b3c4d5e6f7",
        "e2a3b4c5-6d7e-4f81-9a02-b3c4d5e6f708",
    ],
)
print(result.count, "cancelled;", result.not_found, "skipped")
pictograph train cancel-batch \
  d1f2a3b4-5c6d-4e70-8f91-a2b3c4d5e6f7 e2a3b4c5-6d7e-4f81-9a02-b3c4d5e6f708 --yes
curl -s -X POST "https://api.pictograph.io/api/v1/developer/training/bulk-cancel" \
  -H "X-API-Key: $PICTOGRAPH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"run_ids": ["d1f2a3b4-5c6d-4e70-8f91-a2b3c4d5e6f7", "e2a3b4c5-6d7e-4f81-9a02-b3c4d5e6f708"]}'

Returns BulkActionResult

BulkActionResult · 3 fields
class BulkActionResult(BaseModel):
    """Result of a server-side bulk state-change (e.g. pause/resume)."""
    succeeded: list[str] = []
    not_found: list[str] = []
    count: int = 0

Dataset size

The worker splits the export into train / val / test. Below three images it cannot split and trains on everything; three or more guarantees at least one image each in train and val. A handful of images will train, but it will not produce a usable model - treat a few dozen per class as the practical floor.

ds = client.datasets.get(
    name="road-signs",
)
print(ds.completed_image_count, "images ready to train on")

Cost

Training is billed per GPU-minute in USD, charge-on-success: a run is charged once, after it completes, for the actual GPU minutes used. A failed, out-of-memory, or cancelled run is never charged, so there is no up-front deduction to refund. create still gates on a minimum spendable balance at submit.

estimate = client.credits.estimate(
    operation="training_a10g",
    quantity=30,
)
if not estimate.sufficient:
    raise RuntimeError(f"Need ${estimate.total_usd:.2f}, have ${estimate.remaining_usd:.2f}")
pictograph credits estimate training_a10g --quantity 30

Common errors

Status Exception Cause
402 PaymentRequiredError Below the minimum spendable balance
404 NotFoundError Dataset, export, or run missing
422 ValidationError Pipeline or GPU invalid, export not completed
408 PollTimeoutError wait=True exceeded timeout (run keeps going)

See also

  • Exports - build the export a run trains on
  • Models - download trained weights
  • Credits - live USD pricing per GPU tier
Copied to clipboard