> ## Documentation Index
> Fetch the complete documentation index at: https://docs.baseten.co/llms.txt
> Use this file to discover all available pages before exploring further.

# Loops quickstart

> Train one step with Loops, sample from the tuned weights, and shut the session down.

Use the Loops Python SDK to create a LoRA training run, save a checkpoint, generate text from those weights, and list the checkpoint from both Python and the HTTP API. The base model throughout is `Qwen/Qwen3.5-2B`, one of the [supported base models](/loops/supported-models).

The trainer and sampler bill for their GPUs while they run; the final step shuts them down.

## Prerequisites

* A [workspace API key](/organization/api-keys), set as `BASETEN_API_KEY`.
* Python 3.12+ and [uv](https://docs.astral.sh/uv/) to install the SDK and run the script.
* The [Baseten CLI](/reference/cli/baseten/overview) to manage and shut down the run.

## Install

Install `baseten-loops` into a uv project. Create one first if you don't have it:

```bash theme={"system"}
uv init loops-quickstart
cd loops-quickstart
uv add baseten-loops
```

Verify the install by running `uv run python train_loops.py`:

<CodeGroup>
  ```python train_loops.py theme={"system"}
  from baseten.loops import ServiceClient, __version__

  print(ServiceClient)
  print("baseten-loops", __version__)
  ```

  ```output Output theme={"system"}
  <class 'baseten.loops.service_client.ServiceClient'>
  baseten-loops x.x.x
  ```
</CodeGroup>

The printed class path and resolved version confirm your project uses the Loops SDK.

## Provision a trainer

The SDK creates the run, so you don't need to run `truss loops push` first.

A Loops session pairs a trainer (forward, backward, and optimizer steps) with a sampler (generates from current weights). Constructing a [`ServiceClient`](/reference/sdk/loops/service-client) and calling [`create_lora_training_client()`](/reference/sdk/loops/service-client) provisions the trainer and returns a [`TrainingClient`](/reference/sdk/loops/training-client); the sampler comes later, when you first create a sampling client. The call blocks until the trainer is ready, which takes several minutes for a small base model like this one and can reach tens of minutes for the largest supported models. The SDK gives up waiting after an hour.

Replace the contents of `train_loops.py` with the provision step:

```python train_loops.py theme={"system"}
from baseten.loops import (
    AdamParams,
    Datum,
    ModelInput,
    SamplingParams,
    ServiceClient,
    TensorData,
)

BASE_MODEL = "Qwen/Qwen3.5-2B"

service_client = ServiceClient()
training_client = service_client.create_lora_training_client(
    base_model=BASE_MODEL,
    rank=16,
)

print(f"session_id={service_client.session_id}")
print(f"run_id={training_client.run_id}")
```

You'll append the training, sampling, and listing steps to this same file in the next three sections, then run the whole thing once at the end.

The trainer and sampler run on GPUs in your workspace and keep running after your script exits, so plan to finish with the [shut down step](#shut-down-the-session).

<Tip>
  For more training throughput, [scale the trainer](/loops/concepts#scale-the-trainer) with `replicas`. Loops shards each batch across replicas automatically; the preset node count applies to every replica. For unattended work, run the client with [`baseten loops exec`](/loops/concepts#run-the-client) when a laptop or third-party host can't provide stable outbound connectivity.
</Tip>

## Run a training round trip

The smallest complete round trip is one forward pass, one backward pass, one optimizer step, and one weight save. The block below mirrors the canonical supervised fine-tuning (SFT) example: it tokenizes a prompt-and-answer pair, masks the prompt positions from the loss, runs the round trip, and saves a named checkpoint.

Append to `train_loops.py`:

```python train_loops.py theme={"system"}
def build_sft_datum(tokenizer, prompt, answer):
    p = tokenizer.encode(prompt, add_special_tokens=False)
    a = tokenizer.encode(answer, add_special_tokens=False)
    # Loops' forward_backward does NOT shift labels internally (unlike HF
    # Trainer). Shift here so logits at position i predict token i+1:
    # drop the final input token and the first prompt mask position.
    full = p + a
    tokens = full[:-1]
    targets = [-100] * (len(p) - 1) + list(a)  # mask prompt, supervise answer
    return tokens, targets

tokens, targets = build_sft_datum(
    training_client.get_tokenizer(),
    prompt="What is the capital of France?\nAnswer:",
    answer=" Paris",
)
datum = Datum(
    model_input=ModelInput.from_ints(tokens),
    loss_fn_inputs={
        "target_tokens": TensorData(
            data=targets, dtype="int64", shape=[len(targets)]
        )
    },
)

fb = training_client.forward_backward(data=[datum]).result(timeout=600.0)
print(f"loss={fb.loss:.6f}")

optim = training_client.optim_step(
    AdamParams(learning_rate=4e-5)
).result(timeout=600.0)
print(f"optim_metrics={optim.metrics}")

save_resp = training_client.save_weights_for_sampler(name="step-1").result(timeout=600.0)
print(f"saved checkpoint at {save_resp.path}")
```

[`forward_backward()`](/reference/sdk/loops/training-client) is the first training operation you submit after provisioning. [`save_weights_for_sampler()`](/reference/sdk/loops/training-client) publishes a sampler checkpoint under `sampler_weights/` that you can deploy to inference. This checkpoint omits optimizer state, so you can't resume training from it; use [`save_state()`](/reference/sdk/loops/training-client) when you need a resumable checkpoint.

## Sample from the tuned weights

You can generate from the checkpoint you saved without deploying anything. [`create_sampling_client()`](/reference/sdk/loops/training-client) takes the [`bt://` URI](/loops/concepts#checkpoints) that `save_weights_for_sampler()` returned and binds a [`SamplingClient`](/reference/sdk/loops/sampling-client) to those weights. The first call on a run provisions the run's paired sampler, so your first `sample()` waits through its cold start; later calls reuse it. To hide that cold start behind training steps, see [Overlap trainer and sampler startup](/loops/concepts#overlap-trainer-and-sampler-startup). Append to `train_loops.py`:

```python train_loops.py theme={"system"}
tokenizer = training_client.get_tokenizer()
sampling_client = training_client.create_sampling_client(model_path=save_resp.path)

sample = sampling_client.sample(
    prompt=ModelInput.from_ints(
        tokenizer.encode("What is the capital of France?\nAnswer:", add_special_tokens=False)
    ),
    num_samples=1,
    sampling_params=SamplingParams(max_tokens=8),
)
print(f"completion={tokenizer.decode(sample.sequences[0].tokens)!r}")
```

One optimizer step barely changes a 2B model, so the completion reads like base-model output. Still, the sampler served the `step-1` weights your trainer published seconds earlier, without a restart or a deploy step in between. In a longer run, this same call is how you evaluate checkpoints mid-training.

## List checkpoints

Every `save_weights_for_sampler()` call creates a checkpoint. The bound `TrainingClient` lists them with [`list_checkpoints()`](/reference/sdk/loops/training-client), no arguments needed. Append to `train_loops.py`:

```python train_loops.py theme={"system"}
for ckpt in training_client.list_checkpoints():
    print(ckpt.id, ckpt.checkpoint_id, ckpt.created_at)
```

Now run the full script. Output values vary, but a successful run prints a session ID, run ID, loss, optimizer metrics, saved checkpoint URI, a sampled completion, and one listed checkpoint:

```bash theme={"system"}
uv run python train_loops.py
```

```output theme={"system"}
session_id=2qjl22w
run_id=run123
loss=2.456638
optim_metrics={'step': 1.0, 'learning_rate': 4e-05, 'lr': 4e-05, 'grad_norm': 74.11, ...}
saved checkpoint at bt://loops:run123/sampler_weights/step-1
completion=' 1. France is a country in'
abc123 step-1 2026-07-01 20:50:39.854000+00:00
```

You might also see warnings from `transformers` about PyTorch being unavailable and from the Hugging Face Hub about unauthenticated requests. Both are harmless here: the client only uses `transformers` for tokenization, and the tokenizer download works without a token.

For resource management outside the training process, use the Baseten CLI or REST API. Use the `run_id` your script printed when provisioning.

<Tabs>
  <Tab title="Baseten CLI">
    ```bash theme={"system"}
    baseten loops checkpoint list --run-id <run_id> --output json
    ```
  </Tab>

  <Tab title="REST API">
    ```bash theme={"system"}
    curl --request GET \
      --url "https://api.baseten.co/v1/loops/checkpoints?run_id=<run_id>" \
      --header "Authorization: Bearer $BASETEN_API_KEY"
    ```
  </Tab>
</Tabs>

The REST API returns the following shape. The CLI returns the same checkpoint fields but omits optional fields whose value is `null`.

```json REST API output theme={"system"}
{
  "checkpoints": [
    {
      "checkpoint_id": "step-1",
      "created_at": "2026-07-01T20:50:39.854Z",
      "checkpoint_type": "lora",
      "base_model": "Qwen/Qwen3.5-2B",
      "lora_adapter_config": {
        "r": 16
      },
      "size_bytes": 33638400,
      "sync_status": null,
      "id": "abc123",
      "run_id": "run123",
      "target": "sampler"
    }
  ]
}
```

Both interfaces identify checkpoints with the same fields, including the globally unique `id` and the checkpoint name. Use the ID to inspect the checkpoint files:

<Tabs>
  <Tab title="Baseten CLI">
    ```bash theme={"system"}
    baseten loops checkpoint files --checkpoint-id <checkpoint_id>
    ```

    ```output Output theme={"system"}
    NAME                    SIZE       URL
    adapter_config.json     1.2 KB     https://storage.example.com/...
    adapter_model.safetensors  32.1 MB https://storage.example.com/...
    ```
  </Tab>

  <Tab title="REST API">
    ```bash theme={"system"}
    curl --request GET \
      --url "https://api.baseten.co/v1/loops/checkpoints/<checkpoint_id>/files" \
      --header "Authorization: Bearer $BASETEN_API_KEY"
    ```

    ```json Output theme={"system"}
    {
      "presigned_urls": [
        {
          "url": "https://storage.example.com/...",
          "relative_file_name": "adapter_config.json",
          "node_rank": 0,
          "size_bytes": 1234,
          "last_modified": "2026-07-01T20:50:39.854Z"
        }
      ],
      "next_page_token": null,
      "total_count": 1
    }
    ```
  </Tab>
</Tabs>

Both interfaces return short-lived download URLs. The CLI follows every page. For REST requests, pass `next_page_token` back as the `page_token` query parameter until `next_page_token` is `null`; each page contains up to 1,000 files by default.

To download the weight files from either S3 or a Baseten volume, call [`download_checkpoint()`](/reference/sdk/loops/training-client) with the globally unique `id` value as the `checkpoint_id` argument:

```python theme={"system"}
download = training_client.download_checkpoint(
    checkpoint_id="abc123",
    dest_dir="./step-1",
)
print(download.dest_dir, download.file_count, download.bytes_written)
```

From a separate Python session where `training_client` isn't in scope, construct a `ServiceClient()` and call the same method on it. To work with presigned URLs for an S3-backed checkpoint instead, call [`get_checkpoint_archive_url()`](/reference/sdk/loops/training-client).

## Skip the cold start on re-runs

Your first run provisioned a trainer and sampler. The second run doesn't have to. Grab the `run_id` your script printed (`run_id=run123` in the example output above), point the next run at it, and Loops reuses the same trainer and sampler:

```bash theme={"system"}
export LOOPS_REUSE_FROM_RUN_ID=run123
uv run python train_loops.py
```

You can also pass the ID directly in code, which wins if both the kwarg and the environment variable are set:

```python theme={"system"}
service_client = ServiceClient(reuse_from_run_id="run123")
```

Reuse by session ID (`reuse_from_session_id`, `LOOPS_REUSE_FROM_SESSION_ID`) still works but is deprecated in favor of the run ID. From the HTTP API, send `reuse_from_session_id` in the body of `POST /v1/loops/trainers` or [`POST /v1/loops/samplers`](/reference/loops-api/samplers/create-a-sampler).

Reuse is best-effort. If the prior trainer is stopped, failed, or unhealthy, Loops provisions a fresh one and your script still runs.

## Shut down the session

The trainer and sampler bill for their GPUs until you deactivate them. While a `TrainingClient` is open, it keeps the training session warm with a background health-check ping, so close the client (or exit the script) when you finish training. Then check what's live and shut it down:

<Tabs>
  <Tab title="Baseten CLI">
    ```bash theme={"system"}
    baseten loops run list
    baseten loops run deactivate --run-id <run_id> --yes
    ```
  </Tab>

  <Tab title="REST API">
    ```bash List active runs theme={"system"}
    curl --request GET \
      --url "https://api.baseten.co/v1/loops/runs" \
      --header "Authorization: Bearer $BASETEN_API_KEY" \
      | jq '.runs |= map(select(.status.name != "INACTIVE"))'
    ```

    ```bash Deactivate the run theme={"system"}
    curl --request POST \
      --url "https://api.baseten.co/v1/loops/runs/<run_id>/deactivate" \
      --header "Authorization: Bearer $BASETEN_API_KEY"
    ```
  </Tab>
</Tabs>

[`baseten loops run list`](/reference/cli/baseten/loops-run) automatically excludes runs with `status.name` set to `INACTIVE` and shows one row per remaining run, with its run ID, base model, and status. The REST API returns inactive runs too, so the example uses `jq` to apply the same filter. Both interfaces deactivate the trainer and its paired sampler while preserving your checkpoints. You can still list checkpoints, fetch their files, and deploy them to inference after shutdown.

## Next steps

* **[Deploy a checkpoint](/loops/deploy-checkpoints)**: Serve `step-1` as a dedicated inference deployment and call it over the OpenAI-compatible route.
* **[Train on your data](/loops/train-on-your-data)**: Replace the single example with a batched loop, resumable checkpoints, and mid-training evals.
* **[Loops concepts](/loops/concepts)**: Sessions, trainers, samplers, checkpoints, and how weight sync works.
* **[Loops API reference](/reference/loops-api/overview)**: Every HTTP route, for scripting session lifecycles and CI pipelines.
