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

# Run a fine-tune job

> Fine-tune a model on rented GPUs with a Compute job, monitor training from logs and SSH, and publish the merged weights to mka1-repos.

Use a Compute job when you need GPUs for a finite workload such as fine-tuning.
You bring an ordinary container image and a command; Compute allocates GPU capacity, runs the workload to completion, streams durable logs, and deallocates the hardware automatically.

Compute has no fine-tuning schema.
A job is a generic container run, so this guide fine-tunes with [ms-swift](https://github.com/modelscope/ms-swift) purely as workload payload — any training stack works the same way.

<Note>
  This guide rents GPUs and runs your own training container.
  For the managed fine-tuning API that trains platform models from uploaded JSONL files, see [Fine-tune a model](/docs/fine-tuning).
</Note>

## Before you start

You need:

| Requirement                                | Notes                                                                                                                                                                                                                        |
| ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| API key                                    | Send it as `Authorization: Bearer <mka1-api-key>` on every request.                                                                                                                                                          |
| Compute enabled for your organization      | Compute is a fail-closed allowlist because real money is at stake. If requests return `403 tenant_disabled`, ask a cluster administrator to enable your organization.                                                        |
| A container image with your training stack | Any Linux GPU image the provider can pull without interactive authentication. It must run as root with `bash`, `curl`, `base64`, and GNU coreutils available, allow outbound HTTPS, and keep the workload in the foreground. |
| SSH public key                             | Optional. Supply one at create time if you want to debug the run interactively. Compute never generates or holds private keys.                                                                                               |

Jobs move through `requested` → `allocating` → `provisioning` → `running` → `finalizing`, then land in a terminal state:

* A workload that exits `0` ends as `succeeded`; a failure in any phase ends as `failed`.
* Explicit termination, a binding limit, or an exhausted organization budget moves the job through `terminating` to `terminated`.

<Warning>
  **Billing starts at allocation, not readiness.**

  Spend accrues from the moment hardware is allocated, so provisioning time and failed boots cost money.
  Set `limits.max_runtime_hours` and `limits.max_cost_usd` on every job; a job that hits a binding limit is terminated and deallocated immediately.
</Warning>

## Step 1 - Pick an accelerator

List the curated accelerator catalog and choose one by its stable `name`.

<CodeGroup>
  ```ts MKA1 SDK theme={null}
  import { SDK } from '@meetkai/mka1';

  const sdk = new SDK({
    bearerAuth: 'Bearer <mka1-api-key>',
  });

  const accelerators = await sdk.computeCatalog.listAccelerators({});
  console.log(accelerators.data);
  ```

  ```bash curl theme={null}
  curl "https://apigw.mka1.com/api/v1/compute/catalog/accelerators" \
    --header 'Authorization: Bearer <mka1-api-key>'
  ```
</CodeGroup>

Each entry describes the hardware, the GPU counts you can request, and the interconnects it supports:

```json theme={null}
{
  "name": "nvidia-rtx-4090-24gb",
  "display_name": "NVIDIA GeForce RTX 4090 24GB",
  "vendor": "nvidia",
  "memory_gb": 24,
  "gpu_counts": [1, 2, 3, 4, 5, 6, 7, 8],
  "interconnects": ["pcie"]
}
```

## Step 2 - Check price and availability

Optionally request a quote before creating anything.
A quote is a live observation of the market: it returns whether the configuration is currently obtainable and the hourly price range, and it reserves nothing.

<CodeGroup>
  ```ts MKA1 SDK theme={null}
  const quote = await sdk.computeCatalog.createComputeQuote({
    computeQuoteRequest: {
      resourceType: 'job',
      compute: {
        accelerator: 'nvidia-rtx-4090-24gb',
        gpuCount: 1,
        ephemeralDiskGb: 100,
      },
      container: {
        image: 'modelscope-registry.us-west-1.cr.aliyuncs.com/modelscope-repo/modelscope:ubuntu22.04-cuda12.9.1-py312-torch2.10.0-vllm0.19.1-modelscope1.35.4-swift4.1.3',
      },
      access: { ssh: true },
    },
  });
  console.log(quote.available, quote.priceUsdHr);
  ```

  ```bash curl theme={null}
  curl https://apigw.mka1.com/api/v1/compute/catalog/quotes \
    --request POST \
    --header 'Content-Type: application/json' \
    --header 'Authorization: Bearer <mka1-api-key>' \
    --data '{
      "resource_type": "job",
      "compute": {
        "accelerator": "nvidia-rtx-4090-24gb",
        "gpu_count": 1,
        "ephemeral_disk_gb": 100
      },
      "container": {
        "image": "modelscope-registry.us-west-1.cr.aliyuncs.com/modelscope-repo/modelscope:ubuntu22.04-cuda12.9.1-py312-torch2.10.0-vllm0.19.1-modelscope1.35.4-swift4.1.3"
      },
      "access": {"ssh": true}
    }'
  ```
</CodeGroup>

```json theme={null}
{
  "available": true,
  "price_usd_hr": {"min": 0.38, "max": 0.69},
  "observed_at": "2026-07-29T12:00:00Z",
  "expires_at": null
}
```

An unavailable configuration is still a `200` response, with `available: false` and null price bounds.

## Step 3 - Create the job

Create the job with a required `Idempotency-Key` header.
Retrying with the same key and the same body returns the same job with a `200` instead of creating a duplicate; the same key with a different body returns `409 idempotency_conflict`.

This example runs a small LoRA fine-tune of Qwen2.5-0.5B-Instruct and merges the adapter, matching the platform smoke test:

<CodeGroup>
  ```ts MKA1 SDK theme={null}
  const job = await sdk.computeJobs.createJob({
    idempotencyKey: '<unique-request-id>',
    computeJobCreate: {
      name: 'qwen-fine-tune',
      compute: {
        accelerator: 'nvidia-rtx-4090-24gb',
        gpuCount: 1,
        ephemeralDiskGb: 100,
      },
      container: {
        image: 'modelscope-registry.us-west-1.cr.aliyuncs.com/modelscope-repo/modelscope:ubuntu22.04-cuda12.9.1-py312-torch2.10.0-vllm0.19.1-modelscope1.35.4-swift4.1.3',
        command: [
          'bash',
          '-lc',
          'swift sft --model Qwen/Qwen2.5-0.5B-Instruct --dataset tatsu-lab/alpaca#64 --max_steps 4 --per_device_train_batch_size 1 --max_length 256 --lora_rank 4 --save_steps 4 --logging_steps 1 --output_dir /tmp/output && swift export --adapters $(ls -d /tmp/output/*/checkpoint-* | tail -1) --merge_lora true --output_dir /tmp/merged',
        ],
        env: { USE_HF: '1' },
      },
      access: { sshPublicKey: 'ssh-ed25519 AAAA... you@example.com' },
      limits: { maxRuntimeHours: 2, maxCostUsd: 5 },
    },
  });
  console.log(job.id, job.state);
  ```

  ```bash curl theme={null}
  curl https://apigw.mka1.com/api/v1/compute/jobs \
    --request POST \
    --header 'Content-Type: application/json' \
    --header 'Authorization: Bearer <mka1-api-key>' \
    --header 'Idempotency-Key: <unique-request-id>' \
    --data '{
      "name": "qwen-fine-tune",
      "compute": {
        "accelerator": "nvidia-rtx-4090-24gb",
        "gpu_count": 1,
        "ephemeral_disk_gb": 100
      },
      "container": {
        "image": "modelscope-registry.us-west-1.cr.aliyuncs.com/modelscope-repo/modelscope:ubuntu22.04-cuda12.9.1-py312-torch2.10.0-vllm0.19.1-modelscope1.35.4-swift4.1.3",
        "command": ["bash", "-lc", "swift sft --model Qwen/Qwen2.5-0.5B-Instruct --dataset tatsu-lab/alpaca#64 --max_steps 4 --per_device_train_batch_size 1 --max_length 256 --lora_rank 4 --save_steps 4 --logging_steps 1 --output_dir /tmp/output && swift export --adapters $(ls -d /tmp/output/*/checkpoint-* | tail -1) --merge_lora true --output_dir /tmp/merged"],
        "env": {"USE_HF": "1"}
      },
      "access": {"ssh_public_key": "ssh-ed25519 AAAA... you@example.com"},
      "limits": {"max_runtime_hours": 2, "max_cost_usd": 5}
    }'
  ```
</CodeGroup>

Field notes:

| Field                                         | Notes                                                                                                                                                                                                  |
| --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `compute.accelerator`, `compute.gpu_count`    | Required. A catalog accelerator name and 1-8 GPUs on a single node.                                                                                                                                    |
| `compute.ephemeral_disk_gb`                   | Required, with no default. Size it for the image plus every download; the ms-swift image above alone needs most of 60 GB.                                                                              |
| `container.command` or `container.script_b64` | Exactly one is required. `command` is an argv; `script_b64` is a base64-encoded bash script, which is easier for multi-step workloads.                                                                 |
| `container.env`                               | Plain environment variables. `MKA1_*` names are reserved and `HF_ENDPOINT` is platform-owned, so both are rejected.                                                                                    |
| `container.secret_env`                        | Environment variables resolved from stored secrets at launch. Use this for tokens so they never appear in provider pod specs — see the [mka1-repos section](#use-mka1-repos-for-datasets-and-weights). |
| `limits`                                      | Optional but recommended. Runtime and cost ceilings are independent, and either one terminates the job when hit.                                                                                       |

The response is `201` with the durable job in state `requested`:

```json theme={null}
{
  "id": "job_9f2kQxWv3bT8mLpZ",
  "name": "qwen-fine-tune",
  "state": "requested",
  "reason": null,
  "compute": {"accelerator": "nvidia-rtx-4090-24gb", "gpu_count": 1, "ephemeral_disk_gb": 100},
  "accrued_usd": 0,
  "limits": {"max_runtime_hours": 2, "max_cost_usd": 5},
  "endpoints": [],
  "created_at": "2026-07-29T12:01:00Z",
  "started_at": null,
  "terminal_at": null
}
```

Creation succeeding means the request was accepted, not that capacity exists.
Capacity exhaustion, provider errors, and bootstrap failures surface later through `state`, `reason`, logs, and events — never as an HTTP error on a create that already returned `201`.

## Step 4 - Poll until it finishes

<CodeGroup>
  ```ts MKA1 SDK theme={null}
  const job = await sdk.computeJobs.getJob({ id: 'job_9f2kQxWv3bT8mLpZ' });
  console.log(job.state, job.accruedUsd, job.ssh?.command);
  ```

  ```bash curl theme={null}
  curl https://apigw.mka1.com/api/v1/compute/jobs/job_9f2kQxWv3bT8mLpZ \
    --header 'Authorization: Bearer <mka1-api-key>'
  ```
</CodeGroup>

While running, the job reports the hardware it actually obtained, the hourly price captured at allocation, spend so far, and a ready-to-use SSH command if you supplied a key:

```json theme={null}
{
  "id": "job_9f2kQxWv3bT8mLpZ",
  "name": "qwen-fine-tune",
  "state": "running",
  "reason": null,
  "compute": {"accelerator": "nvidia-rtx-4090-24gb", "gpu_count": 1, "ephemeral_disk_gb": 100},
  "hardware": {"accelerator": "nvidia-rtx-4090-24gb", "gpu_count": 1, "gpu_memory_gb": 24},
  "price_usd_hr": 0.44,
  "accrued_usd": 0.07,
  "allocated_at": "2026-07-29T12:03:12Z",
  "ssh": {"host": "203.0.113.7", "port": 22022, "user": "root", "command": "ssh -p 22022 root@203.0.113.7"},
  "limits": {"max_runtime_hours": 2, "max_cost_usd": 5},
  "endpoints": [],
  "created_at": "2026-07-29T12:01:00Z",
  "started_at": "2026-07-29T12:04:40Z",
  "terminal_at": null
}
```

When the workload exits `0`, the job moves through `finalizing` to `succeeded`, `exit_code` is recorded, the provider resource is deallocated automatically, and spend stops accruing.
A non-zero exit ends in `failed` with a `reason` explaining why.
Terminal jobs stay readable for audit and usage.

## Step 5 - Read logs and events

Logs are durable and cursor-paginated, and they distinguish your workload's output from system output.

<CodeGroup>
  ```ts MKA1 SDK theme={null}
  const logs = await sdk.computeJobs.listJobLogs({
    id: 'job_9f2kQxWv3bT8mLpZ',
    source: 'user',
    order: 'desc',
    limit: 50,
  });
  for (const line of logs.data) {
    console.log(line.timestamp, line.stream, line.message);
  }
  ```

  ```bash curl theme={null}
  curl "https://apigw.mka1.com/api/v1/compute/jobs/job_9f2kQxWv3bT8mLpZ/logs?source=user&order=desc&limit=50" \
    --header 'Authorization: Bearer <mka1-api-key>'
  ```
</CodeGroup>

```json theme={null}
{
  "data": [
    {
      "id": "log_01J1",
      "timestamp": "2026-07-29T12:06:02Z",
      "source": "user",
      "stream": "stdout",
      "message": "{'loss': 2.31, 'epoch': 0.25, 'step': 1}"
    }
  ],
  "next_cursor": null,
  "total": 412
}
```

Use `source=system` for allocation and bootstrap output, and `order=asc` (the default) to page oldest-first.
Lifecycle transitions such as `provider_allocated`, `workload_started`, and `workload_exited` are also available as structured events:

<CodeGroup>
  ```ts MKA1 SDK theme={null}
  const events = await sdk.computeJobs.listJobEvents({ id: 'job_9f2kQxWv3bT8mLpZ' });
  console.log(events.data.map((event) => event.type));
  ```

  ```bash curl theme={null}
  curl "https://apigw.mka1.com/api/v1/compute/jobs/job_9f2kQxWv3bT8mLpZ/events" \
    --header 'Authorization: Bearer <mka1-api-key>'
  ```
</CodeGroup>

## Step 6 - Stop a job early

Termination is explicit, idempotent, and returns the current resource.

<CodeGroup>
  ```ts MKA1 SDK theme={null}
  const job = await sdk.computeJobs.terminateJob({ id: 'job_9f2kQxWv3bT8mLpZ' });
  console.log(job.state);
  ```

  ```bash curl theme={null}
  curl https://apigw.mka1.com/api/v1/compute/jobs/job_9f2kQxWv3bT8mLpZ/terminate \
    --request POST \
    --header 'Authorization: Bearer <mka1-api-key>'
  ```
</CodeGroup>

There is no `DELETE`: the record is retained for audit, usage, and reconciliation.
A job that hits `limits.max_runtime_hours`, `limits.max_cost_usd`, or an organization budget is terminated the same way, with no grace period and no partial-result guarantee, so publish artifacts from inside the workload before it exits.

## Use mka1-repos for datasets and weights

Compute stores no workload artifacts.
The golden path is mka1-repos, the platform's Hugging Face-compatible repository service for models and datasets: your job pulls the dataset from it and publishes the merged weights back to it, all from inside the workload.

Two platform conventions make this work:

1. Compute always injects `HF_ENDPOINT` into every workload, pointing at the artifact endpoint configured for your cluster.
   Hugging Face-compatible tooling — `huggingface_hub`, ms-swift with `USE_HF=1`, vLLM — resolves `<org>/<name>` identifiers against it, so workloads reach mka1-repos without any code changes.
   You cannot override `HF_ENDPOINT` yourself.
2. mka1-repos authenticates with your MKA1 API key.
   Store the key as a Compute secret and inject it as `HF_TOKEN` through `secret_env`, so it never appears inline in the job spec.

Create the secret once:

<CodeGroup>
  ```ts MKA1 SDK theme={null}
  const secret = await sdk.computeSecrets.createComputeSecret({
    computeSecretCreate: {
      name: 'artifact-repository',
      data: { HF_TOKEN: '<mka1-api-key>' },
    },
  });
  console.log(secret.id, secret.keys);
  ```

  ```bash curl theme={null}
  curl https://apigw.mka1.com/api/v1/compute/secrets \
    --request POST \
    --header 'Content-Type: application/json' \
    --header 'Authorization: Bearer <mka1-api-key>' \
    --data '{
      "name": "artifact-repository",
      "data": {"HF_TOKEN": "<mka1-api-key>"}
    }'
  ```
</CodeGroup>

Secret values are write-only; the response returns only the id and key names:

```json theme={null}
{
  "id": "sec_Vb3nRk8sQw1xYz2M",
  "name": "artifact-repository",
  "keys": ["HF_TOKEN"],
  "created_at": "2026-07-29T11:58:00Z"
}
```

Then write the workload as a script that trains from your dataset repository and publishes the merged weights:

```bash train-and-publish.sh theme={null}
set -euo pipefail

swift sft \
  --model Qwen/Qwen2.5-0.5B-Instruct \
  --dataset acme/support-conversations \
  --num_train_epochs 1 \
  --per_device_train_batch_size 1 \
  --lora_rank 8 \
  --output_dir /tmp/output

swift export \
  --adapters $(ls -d /tmp/output/*/checkpoint-* | tail -1) \
  --merge_lora true \
  --output_dir /tmp/merged

python -c "
from huggingface_hub import HfApi
api = HfApi()
api.create_repo('acme/qwen2.5-0.5b-support', exist_ok=True)
api.upload_folder(repo_id='acme/qwen2.5-0.5b-support', folder_path='/tmp/merged')
"
```

Encode it with `base64 < train-and-publish.sh` and reference the secret in the create body:

```json theme={null}
{
  "container": {
    "image": "modelscope-registry.us-west-1.cr.aliyuncs.com/modelscope-repo/modelscope:ubuntu22.04-cuda12.9.1-py312-torch2.10.0-vllm0.19.1-modelscope1.35.4-swift4.1.3",
    "script_b64": "<base64 of train-and-publish.sh>",
    "env": {"USE_HF": "1"},
    "secret_env": {
      "HF_TOKEN": {"secret_id": "sec_Vb3nRk8sQw1xYz2M", "key": "HF_TOKEN"}
    }
  }
}
```

When the job succeeds, the merged weights are downloadable from `acme/qwen2.5-0.5b-support` by anyone in your organization, and a Compute service can serve them directly — see [Deploy a model server](/docs/compute-deployment).

<Note>
  A mid-run termination destroys any work not already published.
  Publishing from inside the workload, as above, is the supported checkpointing mechanism in this version.
</Note>

## Track spend

Every job response carries `accrued_usd`, and the usage endpoint aggregates spend across resources for a time window:

<CodeGroup>
  ```ts MKA1 SDK theme={null}
  const usage = await sdk.computeUsage.getComputeUsage({ resourceType: 'job' });
  console.log(usage.summary);
  ```

  ```bash curl theme={null}
  curl "https://apigw.mka1.com/api/v1/compute/usage?resource_type=job" \
    --header 'Authorization: Bearer <mka1-api-key>'
  ```
</CodeGroup>

```json theme={null}
{
  "window": {"from": "2026-06-29T12:10:00Z", "to": "2026-07-29T12:10:00Z"},
  "summary": {"total_usd": 12.41, "job_usd": 4.12, "service_usd": 8.29},
  "data": [
    {
      "resource_type": "job",
      "resource_id": "job_9f2kQxWv3bT8mLpZ",
      "name": "qwen-fine-tune",
      "billable_seconds": 1560,
      "accrued_usd": 0.19
    }
  ],
  "next_cursor": null,
  "total": 6
}
```

Billing is metered in whole minutes per resource, and Compute spend draws from the same organization budgets as every other MKA1 service.

## API reference

For the full request and response schemas, open the Compute groups in the [API Reference](/api-reference/introduction).

## See also

* [Deploy a model server](/docs/compute-deployment) - serve the weights this job produced behind an authenticated endpoint.
* [Fine-tune a model](/docs/fine-tuning) - the managed fine-tuning API for platform models.
