Fine-tune a model
Serverless fine-tuning lets you fine-tune open-weight models on your own data without provisioning or managing GPU infrastructure. You upload a training dataset, choose a base model, and configure hyperparameters. Crusoe handles the rest and returns LoRA adapter checkpoints you can serve from your own inference stack.
For API-specific information, refer to the Serverless Fine-Tuning API.
Prerequisites
- An Intelligence API key generated from the API Keys page in Intelligence Foundry. See Managing API Keys for more information.
- The OpenAI Python client (
pip install openai httpx), if you use the Python examples. - A training dataset in JSON Lines (JSONL) or Parquet format, 3 GB or smaller.
1. Authenticate against the API
All API calls require an Intelligence API key and the API base URL.
-
Export the token and base URL in your shell:
export API_TOKEN='<your token>'export URL='https://api.intelligence.crusoecloud.com' -
Construct an OpenAI client pointed at the Crusoe gateway. Every Python example on this page assumes you have this
openai_clientin scope:from openai import OpenAIimport httpx, osopenai_client = OpenAI(api_key=os.environ["API_TOKEN"],base_url=f"{os.environ['URL']}/v1",http_client=httpx.Client(proxy=None, trust_env=False),)
2. Prepare a dataset
You must upload training data as a single file that's 3GB or smaller with a .jsonl or .parquet extension. Each line must be a JSON object containing a messages array of chat turns.
Dataset format
Each line is a chat conversation. The example below classifies banking customer messages into intent labels:
{
"messages": [
{
"role": "system",
"content": "You are a banking customer service intent classifier. Given a customer message, classify it into exactly one of these intents: activate_my_card, country_support, ..."
},
{
"role": "user",
"content": "Which countries are represented?"
},
{
"role": "assistant",
"content": "country_support"
}
]
}
How loss is computed
Loss is applied to assistant turns and masked on every other role.
Single-turn reasoning and multi-turn non-reasoning both work. Contact support if you need multi-turn reasoning.
Model-specific dataset quirks
Some base models have quirks that affect how you structure assistant turns:
-
llama-3-instruct: When an assistant turn contains both a tool call and content, llama-3-instruct doesn't render the content. The model wasn't pretrained to emit content before a tool call, only one or the other. Keep this format in your fine-tuning data; don't expect the model to emit content alongside a tool call.
-
gemma-4: When an assistant turn contains both a tool call and content, gemma-4 renders the content before the tool call, which often reads unnaturally. To get content after the tool call, split it into a separate assistant turn that follows the tool call.
-
gpt-oss: gpt-oss is pretrained to emit
analysisbeforefinal. If your dataset focuses only on final content, include an emptythinkingfield so the model doesn't fight its pretrained format:{"messages": [{"role": "system","content": "..."},{"role": "user","content": "Which countries are represented?"},{"role": "assistant","content": "country_support","thinking": ""}]}
3. Upload a dataset
Upload your prepared .jsonl file with purpose=fine-tune. The response includes a file ID you'll pass in the job creation step:
- Python
- curl
with open("train.jsonl", "rb") as f:
file_obj = openai_client.files.create(file=f, purpose="fine-tune")
print(file_obj.id) # save this for the job creation step
curl $URL/v1/files \
--request POST \
--header 'Content-Type: multipart/form-data' \
--header 'Accept: application/json' \
--header "Authorization: Bearer $API_TOKEN" \
--form "file=@<path-to-your-dataset>" \
--form "purpose=fine-tune"
Manage uploaded datasets
After upload, you can list every dataset in your account, download a dataset's contents, or delete a dataset you no longer need. The examples below show all three operations:
- Python
- curl
# List
files = openai_client.files.list()
# Download
file_content = openai_client.files.content(file_id)
# Delete
openai_client.files.delete(file_obj.id)
# List
curl $URL/v1/files \
--header 'Accept: application/json' \
--header "Authorization: Bearer $API_TOKEN"
# Download
curl $URL/v1/files/${FILE_ID}/content \
--header 'Accept: application/json' \
--header "Authorization: Bearer $API_TOKEN"
# Delete
curl $URL/v1/files/${FILE_ID} \
--request DELETE \
--header 'Accept: application/json' \
--header "Authorization: Bearer $API_TOKEN"
4. Launch a fine-tuning job
A job requires a training dataset and accepts an optional validation dataset. If you don't supply validation data, Crusoe holds out 10% of the training data automatically.
Create a job from either the console or the API. Select the tab for your preferred interface.
- Console
- API
-
Sign in to the Crusoe Console and switch to Intelligence Foundry using the workspace menu in the top-right corner.
-
Select Fine-Tuning in the left navigation. From this page, you can view all jobs in your project with their current status, base model, and checkpoint count.
-
Click Start a fine-tuning job and complete the form by adding a base model, training data, optional validation data, and hyperparameters.
-
Click any row on the Fine-Tuning page to see live training and validation loss, ETA, and the list of saved checkpoints.
- Python
- curl
job = openai_client.fine_tuning.jobs.create(
training_file=train_file_id,
model=model_id,
validation_file=eval_file_id,
hyperparameters={
"n_epochs": 3,
"batch_size": 64,
"learning_rate": 0.0001,
},
)
print(job.id)
curl $URL/v1/fine_tuning/jobs \
--request POST \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--header "Authorization: Bearer $API_TOKEN" \
--data '{
"model": "model-qwen-qwen3-8b-2662a271",
"training_file": "<file id from list-files response>",
"method": {
"type": "supervised",
"supervised": {
"hyperparameters": {
"batch_size": "auto",
"learning_rate": 0.0001,
"n_epochs": 4,
"checkpoint_steps": 100,
"eval_steps_per_epoch": 4,
"early_stopping_patience": 5,
"lora_variant": "lora",
"lora_rank": 32,
"lora_alpha": 32,
"lora_dropout": 0.05,
"lr_scheduler_type": "cosine",
"warmup_ratio": 0.03,
"weight_decay": 0,
"overlong_row_behavior": "error"
}
}
}
}'
Hyperparameter reference
The job creation API exposes more training configuration than the console. The table below explains the parameters that aren't self-explanatory:
| Parameter | Default | What it does |
|---|---|---|
learning_rate_multiplier | 1 | Scales the learning rate. Present mainly for OpenAI compatibility. Unless you need to scale the configured learning rate, leave it empty or set to 1. |
checkpoint_steps | 100 | How often a checkpoint is saved. Each checkpoint is a usable LoRA adapter, and the best one can be selected after the run. If you use more frequent checkpoints, the job takes longer because each save takes time. |
eval_steps_per_epoch | 4 | The number of evaluation passes per epoch. Higher values produce finer-grained loss curves but can slow the job significantly when the eval dataset is large. |
early_stopping_patience | null | The number of evaluation calls without improvement before training stops. Set to null (default) to disable early stopping. |
lora_variant | "lora" | Defines which LoRA variant to use: "lora" or "rslora". |
lr_scheduler | "cosine" | How learning rate decays over training: "cosine" (smooth decay to zero), "linear" (straight-line decay), "constant" (no decay), or "constant_with_warmup". |
warmup_ratio | 0.0 | Fraction of training steps where learning rate linearly ramps from 0 to the target. Prevents early instability when gradients are noisy; typical values are 0.03 to 0.1. |
overlong_row_behavior | "error" | What to do when a sample exceeds the maximum sequence length: "error" (fail the job), "drop" (skip the sample), or "exact" (truncate or pad to fit). |
Check job status
After you launch a job, retrieve its current status by job ID or list every job in your project. Use retrieval to poll a specific run; use the list endpoint to find a job ID you don't already have.
- Python
- curl
# Retrieve a single job
retrieved_job = openai_client.fine_tuning.jobs.retrieve(job.id)
print(retrieved_job.status)
# Paginate through previous jobs
page = openai_client.fine_tuning.jobs.list(limit=20, after=after)
for j in page.data:
print(j.id)
# Retrieve a single job
JOB_ID='ftjob-...'
curl $URL/v1/fine_tuning/jobs/${JOB_ID} \
--header 'Accept: application/json' \
--header "Authorization: Bearer $API_TOKEN"
# List jobs
curl "$URL/v1/fine_tuning/jobs?limit=20" \
--header 'Accept: application/json' \
--header "Authorization: Bearer $API_TOKEN"
Interpret a successful job response
When a job succeeds, the response includes a result_files array. Each entry is a checkpoint ID you can pass to the download endpoint in the next step. Save an ID from the response before moving on:
{
"id": "ftjob-82d5c693a9c84eb2907309225797e6f3",
"status": "succeeded",
"result_files": [
"adapter:checkpoint-1-40a614f42c3d423ab314bd691691c2c0:e32cddf8-72b6-4ea3-be48-74eece3554fe:..."
]
}
5. (Optional) Download a checkpoint
When a job finishes, every saved checkpoint is exposed as a downloadable LoRA adapter. You can list the checkpoints emitted by a job and download one to disk. See the following examples for reference:
- Python
- curl
# List the checkpoints emitted by a finished job
checkpoints = openai_client.fine_tuning.jobs.checkpoints.list(job.id).data
# Download a checkpoint's contents
content = openai_client.files.content(checkpoints[-1].id)
with open("checkpoint-best.zip", "wb") as f:
f.write(content.read())
BEST_CHECKPOINT='<checkpoint id from result_files>'
curl -s $URL/v1/files/${BEST_CHECKPOINT}/content \
--header 'Accept: application/json' \
--header "Authorization: Bearer $API_TOKEN" \
--output checkpoint-best.zip
6. Self-serve a deployment for your checkpoint
From the console's Fine-tuning jobs page, select your model to open its job details page. From the details page, find the checkpoint you want to deploy, click the corresponding three-dot menu, and select Deploy. For more information, see Self-Serve Deployments.