---
title: Train a model
description: Create an export and train a model in one call - detection, segmentation, keypoint, or classification, on a managed GPU.
section: Guides
order: 4
---
A training run is always **on an export**, never on a dataset directly. Create the export, check what went into it, then train it. Nothing is created behind your back, and an export you can see is an export you can re-train from.

Source: [`resources/training.py`](https://github.com/pictograph-io/pictograph-sdk/blob/v1.69.67/src/pictograph/resources/training.py)

```python
from pictograph import Client, TrainingRun

client = Client()

client.exports.create(
    dataset_name="road-signs",
    name="road-signs-v1",
    format="pictograph",
    include_images=True,
    wait=True,
)

run: TrainingRun = client.training.create(
    dataset_name="road-signs",
    export_name="road-signs-v1",
    pipeline_type="yolox",
    name="road-signs-detector",
    gpu_type="a10g",
    config={"epochs": 50, "batch_size": 16},
)

if run.model_id:
    client.models.download(
        model_id=run.model_id,
        output_path="./yolox.onnx",
    )
```

With `wait=True` (the default) the call blocks until the run reaches a terminal status
and `run.model_id` is populated.

## Signature

```python
client.training.create(
    dataset_name: str,
    export_name: str,
    *,
    pipeline_type: PipelineType,
    name: str,
    config: dict[str, Any] | str | Path | None = None,
    gpu_type: GpuType = "a10g",
    gpu_count: int = 1,
    version_of_model_id: str | None = None,
    wait: bool = True,
    poll_interval: float = 5.0,
    timeout: float = 7200.0,
) -> TrainingRun
```

| Argument | Default | Purpose |
| --- | --- | --- |
| `dataset_name` | required | Dataset the export belongs to |
| `export_name` | required | A **completed** export - training is always on an export |
| `pipeline_type` | required | Which pipeline to train |
| `name` | required | Run name |
| `config` | `{}` | Hyperparameters (`epochs`, `batch_size`, `learning_rate`, `image_size`) |
| `gpu_type` | `a10g` | `a10g` / `a100` / `h100` / `auto` |
| `gpu_count` | `1` | GPUs per run |
| `version_of_model_id` | `None` | Train a new version of an existing model |
| `wait` | `True` | Block until the run terminates |
| `poll_interval` | `5.0` | Seconds between polls |
| `timeout` | `7200` | Max seconds to wait (2 hours) |

## Pipelines

| `pipeline_type` | Task | Best for |
| --- | --- | --- |
| `yolox` | Object detection (boxes) | Speed, edge deployment, small datasets |
| `sm_pytorch` | Semantic segmentation | Pixel-wise class maps |
| `classification` | Image classification | Tag-style labels with no geometry |
| `rfdetr_detection` | Object detection | Higher mAP than YOLOX on harder data |
| `rfdetr_segmentation` | Instance segmentation (polygons + masks) | Best per-instance mask accuracy |
| `rfdetr_keypoint` | Keypoint / pose detection | A structured joint set per instance |

## GPU tiers

| `gpu_type` | When |
| --- | --- |
| `a10g` (default) | YOLOX, classification, RF-DETR detection |
| `a100` | Large RF-DETR, big batch sizes |
| `h100` | Last resort, only when A100 runs out of memory |
| `auto` | Let the platform pick the cheapest tier the config fits |

## How much data you need

There is no fixed image minimum. The matched images are split into train / val / test
with at least one image in train and one in val; below three images everything goes into
train. `classification` needs at least two classes; every other pipeline needs one.

The practical floor is data, not policy: a handful of images trains, but it will not
generalise.

## Async usage

Pass `wait=False` to fire-and-forget:

```python
run = client.training.create(
    dataset_name="road-signs",
    export_name="road-signs-v1",
    pipeline_type="yolox",
    name="road-signs-detector",
    wait=False,
)
print("queued:", run.id)

# Poll yourself later.
run = client.training.get(
    run_id=run.id,
)
if run.status == "completed":
    model = client.models.get(
        model_id=run.model_id,
    )
```

## Hyperparameters

`config` keys are pipeline-specific. Common ones across pipelines:

| Key | Type | Typical |
| --- | --- | --- |
| `epochs` | int | 30–100 |
| `batch_size` | int | 8 / 16 / 32 |
| `learning_rate` | float | `0.001`–`0.01` |
| `image_size` | int | `640` (YOLOX), `1024` (segmentation) |

Unsupported keys are ignored.

## Keypoint (pose) training

`rfdetr_keypoint` trains on **keypoint** annotations grouped into objects: a set of
joints per object, not a box or a mask. Three things are worth knowing first.

**Grouping is the supervision signal.** RF-DETR Keypoint is query-based and top-down, so
it needs to know which points belong to the same object. That is what `instance_id`
carries; the editor assigns it per class as you draw, so a pose is simply every joint
sharing an `instance_id`. Points with no `instance_id` train as single-joint objects.

**Name left and right explicitly.** A joint's class name travels into training, and
RF-DETR derives its horizontal-flip pairs by matching `left_*` to `right_*`. Without that
pairing, flip augmentation mirrors the image but keeps the joint identities, teaching the
model that a left wrist is a right one. Joint names with no counterpart map to identity.

**Every object still needs a box.** The box is derived from each instance's placed joints
automatically, so you never draw one. An object of a keypoint class with no joints placed
yet still trains as a plain detection, with its keypoints masked out of the loss rather
than dropped from the dataset.

```python
run = client.training.create(
    dataset_name="gymnasts",
    export_name="gymnasts-v1",
    pipeline_type="rfdetr_keypoint",
    name="gymnast-pose",
    gpu_type="a10g",
    config={"epochs": 30},
)
print(run.metrics["OKS"])   # keypoint AP is measured over OKS, not box IoU
```

Predictions come back as `keypoint` annotations carrying `instance_id`, the same shape
the editor writes, so they render, export and re-open for correction like hand-drawn
poses. Accuracy is reported as **OKS** (object keypoint similarity) rather than mAP; the
two are not comparable, which is why they have separate names.

## Errors

| Status | Exception | Cause |
| --- | --- | --- |
| 404 | `NotFoundError` | Dataset missing or has no `status_filter`-matching images |
| 422 | `ValidationError` | Pipeline or GPU invalid, or too few classes for the pipeline |
| 402 | `PaymentRequiredError` | Balance below the minimum needed to launch a GPU run |
| 408 | `PollTimeoutError` | `wait=True` and `timeout` elapsed (the run continues; poll later) |
| 5xx | `ApiError` | Training run failed - inspect `run.error_message` |

A run is charged for the GPU minutes it actually used, once it succeeds - there is no
up-front charge and no refund to chase if it fails. The 402 is a start-time balance
floor, not a pre-charge.

## See also

- [Training](/docs/api-reference/training.md) - lower-level `create / list / get / cancel` primitives
- [Models](/docs/api-reference/models.md) - download the trained weights
- [Credits](/docs/api-reference/credits.md) - `estimate("training_<gpu>")` for the live USD price
- [Local inference](/docs/local-inference.md) - run the model you just trained
- [Deployments](/docs/deployments.md) - serve it behind a URL