Skip to content
Docs

Fine-tuning API

Train an open model on your own conversations and get back a model you can download or run in batches. The API is OpenAI's fine-tuning API, so your existing code carries over.

How it works

You send examples and Gridline trains the model: there is no GPU to choose and no training code to write. A job takes three steps.

  1. Upload your examples as chat JSONL, with purpose=fine-tune.
  2. Create a job with POST /fine_tuning/jobs, naming a base model and your training file. The response carries the job's quote.
  3. Wait for succeeded. The job's fine_tuned_model is your ft: model.

Requests follow OpenAI's API at Gridline's base URL, with a Gridline token as the API key. The quickstart shows how to get one.

https://api.gridlinegpu.com/openai/v1

If you would rather run your own training script, see your own script.

Base models

These are the models you can fine-tune. All are Apache-2.0 except Meta's Llama 3.1 8B Instruct, which comes under the Llama 3.1 Community License.

Base model idSourceLicenceMax lora_rank (lora, qlora)Max lora_rank (DPO)Batches on its ft: models
qwen2.5-0.5b-instructQwen/Qwen2.5-0.5B-InstructApache-2.06464Yes
qwen2.5-1.5b-instructQwen/Qwen2.5-1.5B-InstructApache-2.06457Yes
qwen2.5-7b-instructQwen/Qwen2.5-7B-InstructApache-2.06426Yes
mistral-7b-instruct-v0.3mistralai/Mistral-7B-Instruct-v0.3Apache-2.06425Yes
qwen3-8bQwen/Qwen3-8BApache-2.06424Yes
llama-3.1-8b-instructRedHatAI/Llama-3.1-8B-InstructLlama 3.1 Community License6425Yes
gpt-oss-20bopenai/gpt-oss-20bApache-2.06464Not yet: download only
qwen3.5-9bQwen/Qwen3.5-9BApache-2.03636Not yet: download only
Exact model versions

Each base is pinned to a full Hugging Face commit. A job trains on that commit and batches serve the same one (once batch serving covers the base).

  • qwen2.5-0.5b-instruct: 7ae557604adf67be50417f59c2c2f167def9a775
  • qwen2.5-1.5b-instruct: 989aa7980e4cf806f80c7fef2b1adb7bc71aa306
  • qwen2.5-7b-instruct: a09a35458c702b33eeacc393d103063234e8bc28
  • mistral-7b-instruct-v0.3: c170c708c41dac9275d15a8fff4eca08d52bab71
  • qwen3-8b: b968826d9c46dd6066d109eabc6255188de91218
  • llama-3.1-8b-instruct: 83c927477af31b2c66de4c0210ae5fa04b28289b
  • gpt-oss-20b: 6cee5e81ee83917806bbde320786a8fb61efebee
  • qwen3.5-9b: c202236235762e1c871ad0ccb60c8ee5ba337b9a

Things to know about each model

Max lora_rank is the largest rank whose adapter fits the trainer's adapter limit: 383 MiB for LoRA and QLoRA (method lora or qlora), and the trainer's 128 MiB adapter limit for DPO. On the 7B and 8B bases, QLoRA runs on a 48 GB GPU at any rank up to 64 and is charged the LoRA rate, so it costs the same as bf16 LoRA. There, bf16 LoRA (the default) trains faster and fits more training tokens in a job; QLoRA is the default on the smaller bases.

mistral-7b-instruct-v0.3 needs every row to alternate user and assistant after an optional first system message; its chat template refuses anything else.

llama-3.1-8b-instruct is Meta's Llama 3.1 8B Instruct, staged from Red Hat's ungated copy of the same files. Its chat template starts every row with a system turn that holds two date lines, before your own system message. Tool rows are not supported on it. Built with Llama: your use of it and of its fine-tunes is subject to the Llama 3.1 Community License and Acceptable Use Policy.

A qwen3-8b fine-tune made before batch serving of qwen3-8b began kept its LoRA adapter for download and created no fine_tuned_model: that job's fine_tuned_model is null and gridline.fine_tuned_adapter names the adapter (servable false). A new job on the same files creates a fine-tuned model you can run batches on.

qwen3-8b trains in Qwen3's non-thinking chat format. An assistant turn may carry a <think>…</think> block only as the last turn, in the template's own form; anywhere else create refuses the file with template_token_in_content. Tool rows are not supported on it.

Training files

Upload with purpose=fine-tune. Each line is one conversation in OpenAI's chat format, a messages list:

{"messages": [{"role": "system", "content": "Classify the ticket."}, {"role": "user", "content": "The parcel never came."}, {"role": "assistant", "content": "delivery"}]}
  • Roles are system (first message only), user and assistant, each with non-empty string content. Every row needs at least one assistant message to train on.
  • An assistant message may carry OpenAI's weight: 0 keeps it as context without training on it, 1 (the default) trains on it.
  • tools, functions, name, tool_calls, content parts and the tool and function roles are refused by name, never ignored.
  • Each line is at most 1 MiB, with at most 1,024 messages. A job's training and validation files are each at most 64 MiB.
  • Keep chat-template markup out of your text. The job renders each row with the base's chat template and then looks for every message in the result. Create reads both files first. It refuses the job with 400 template_token_in_content, naming the line, the message and the token, when a message holds one of the base's control tokens as text, such as <|im_end|> on the Qwen bases, or holds an assistant </think> outside the template's own form on the Qwen3 bases. Documentation, chat logs and code often contain such strings. Anything else the template changes or drops still fails the job with template_mismatch, after a GPU has started.

The file is checked as it uploads. The first invalid line fails it, with that line's number and a code such as invalid_json, invalid_messages or line_too_large. A valid file records its row and message counts, an upper bound on its tokens from its UTF-8 bytes, and a conservative estimate of its tokens for each base, from its text. The job counts tokens exactly with the base's tokenizer before it trains.

From batch results

You can train on the answers a batch produced, for example to teach a small model what a large one wrote. POST /v1/files/distill turns a batch's requests and results into a training file and a held-out validation file, both purpose=fine-tune.

  • results_file is a batch's output_file_id (its requests are that batch's input file) or an uploaded results file in the OpenAI or Anthropic batch format with its requests_file.
  • Each result becomes one row: the request's messages plus the answer. Errored, refused, truncated and empty answers are dropped and counted.
  • Options: require_json, keep_truncated, eval_fraction (default 0.1) and seed; the same files and options always give the same rows.
  • Each file it makes names its sources in gridline.derived_from, counts toward your file limits and is kept 30 days.

As a shortcut, creating a job with a batch's output_file_id as its training_file converts it with the defaults first.

You are responsible for having the right to train on what you upload, including model outputs: check the terms of the service that produced them.

Create a job

Name a base model and your training file. Everything else has a default, and you can change the method or the settings in the same call:

job = client.fine_tuning.jobs.create(
    model="qwen2.5-7b-instruct",
    training_file="file-...",
    validation_file="file-...",  # optional
)

# LoRA instead of QLoRA, two epochs:
job = client.fine_tuning.jobs.create(
    model="qwen2.5-1.5b-instruct",
    training_file="file-...",
    method={"type": "supervised", "supervised": {"adapter": "lora", "hyperparameters": {"n_epochs": 2}}},
)

The same call with curl:

curl https://api.gridlinegpu.com/openai/v1/fine_tuning/jobs \
  -H "Authorization: Bearer $GRIDLINE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"model": "qwen2.5-7b-instruct", "training_file": "file-...", "method": "qlora"}'

The adapter method is bf16 LoRA by default on qwen2.5-7b-instruct, mistral-7b-instruct-v0.3, qwen3-8b and llama-3.1-8b-instruct, which train on 48 GB GPUs, and QLoRA by default on the smaller bases. "method": "qlora" on a 7B or 8B base also trains on a 48 GB GPU, at the same price as bf16 LoRA. gpt-oss-20b and qwen3.5-9b train with bf16 LoRA only, on 48 GB GPUs. Choose it with method: "qlora" or "lora" as a string, or OpenAI's {"type": "supervised", "supervised": {...}} object with an adapter field. Supervised fine-tuning and DPO (method.type "dpo", below) are offered; reinforcement is refused. Hyperparameters go in hyperparameters or method.supervised.hyperparameters, once:

HyperparameterRangeDefault
n_epochs1 to 101
batch_size1 to 648
learning_rate_multiplier0.01 to 10, times 2e-41
learning_rate1e-6 to 1e-2 (instead of the multiplier)2e-4
lora_rank1 up to the base's max lora_rank (Base models)16
lora_alpha1 to 25632
max_seq_length128 up to the model limit below2,048

Defaults: the method above, rank 16, alpha 32, one epoch, learning rate 2e-4. learning_rate, lora_rank, lora_alpha and max_seq_length are Gridline's; send them as dictionary keys (the OpenAI SDKs pass them through). seed and metadata work as in OpenAI's API. suffix (at most 64 characters) is accepted and ignored for now: the job records a suffix_ignored warning event. Any other field is refused by name.

Supervised fine-tuning supports max_seq_length up to 8,192 on qwen2.5-7b-instruct, qwen3-8b, llama-3.1-8b-instruct. Above 2,048, these jobs use a 48 GB GPU and support ranks up to 16. The quote shows gpu_memory_class_gb: 48. Long-context LoRA and QLoRA use the published per-token LoRA rate. DPO and the other bases support up to 2,048 tokens.

Create refuses a job, with 400 invalid_file_format and a message naming the fix, when:

  • the training file has fewer than 10 rows besides any held out for evaluation;
  • a file is over 64 MiB;
  • a Mistral file does not alternate roles.

Create also reads both files. It refuses a job with 400 template_token_in_content, and a message naming the line, the message and the token, when a message holds a control token of the base's chat template written as text: <|im_start|>, <|im_end|> or <|endoftext|> on the Qwen bases; <|eot_id|> and the other header tokens on Llama; <s> and </s> on Mistral; each other base's own markers likewise. The trainer would read it as the template's own markup. On the Qwen3 bases an assistant </think> outside the template's own form is refused the same way. Rewrite the token (for example as plain words) and upload the file again. A job created before this check fails with template_mismatch before any GPU starts, and nothing is charged.

A job that could take longer than the 2-hour v1 limit, counting its training file's tokens with the base's estimate, is refused with 400 job_too_large. The estimate is at or above the real count on ordinary text: on English, about 1.3 times the real count, or one token for about every 4 bytes. The message gives the limit, the job's duration at that count, how many training tokens fit and what to change (fewer rows or epochs, or a smaller base).

Changed in November 2026: this refusal used to be 400 invalid_file_format. Clients that matched that code for the time limit should match job_too_large. The limit also used to count each content byte as a token, which refused jobs several times smaller than the limit allows. It still does for a file uploaded before the change: upload it again to have its tokens counted.

Preference fine-tuning (DPO)

DPO trains on pairs of answers to the same prompt: one you prefer and one you don't. Send OpenAI's preference rows unchanged, one per line:

{"input": {"messages": [{"role": "user", "content": "Capital of France?"}]}, "preferred_output": [{"role": "assistant", "content": "Paris."}], "non_preferred_output": [{"role": "assistant", "content": "Lyon."}]}

input.messages holds an optional first system message, then user and assistant messages, and ends with a user message. Each output is one assistant message, and the two must differ. A file holds chat rows or preference rows, not both: a DPO job trains on preference rows and a supervised job on chat rows.

Create the job with method.type "dpo":

job = client.fine_tuning.jobs.create(
    model="qwen2.5-7b-instruct",
    training_file="file-...",
    method={"type": "dpo", "dpo": {"hyperparameters": {"beta": 0.1}}},
)

beta is over 0 and at most 2, or "auto" (0.1). The learning rate is 5e-6 times learning_rate_multiplier, or an explicit learning_rate from 5e-8 to 1e-2. The other hyperparameters, the adapter (method.dpo.adapter) and the limits are the supervised job's.

A DPO job trains over the prompt, the preferred and the non-preferred answer, and each pair also runs a reference pass, so it trains about 2 times slower than supervised fine-tuning: the time limit fits about 1/2 as much data, and the quote counts it that way. Every DPO job trains on a 48 GB GPU. The job's eval loss is the DPO loss, which starts at 0.693 (ln 2) and falls as the model learns your preferences. DPO has its own rate on the pricing page, charged on its training tokens: the prompt and both answers, every epoch.

The quote

Every job tells you what it expects to cost before it runs. Creating a job starts it: there is no separate step to accept the quote. It comes back in the create response, in gridline.quote; to stop a job you do not want, cancel it. The time and token figures are estimates (estimate: true):

  • training_tokens_upper_bound and training_tokens_per_epoch_upper_bound: tokens times epochs, an upper bound from the file's bytes.
  • n_epochs and eval_rows.
  • estimated_seconds (OpenAI's estimated_finish is the creation time plus this) and estimated_usd, a cost estimated from that time. Both count tokens with the base's estimate, as the time limit does. estimated_usd is not what you pay: a small job often shows less than the per-job minimum it is charged.
  • basis: the estimate's assumptions, in words.
  • per_token: what you pay. A job that succeeds is charged the tokens it trained, times the epochs, at trained_usd_per_mtok, and at least minimum_usd. The charge is never more than max_charge_usd, counted at the token upper bound, and the credit check holds at least that.

The job

Once a job exists you can read it, follow its progress or stop it:

  • GET /fine_tuning/jobs/{id} returns the job; GET /fine_tuning/jobs lists yours.
  • GET /fine_tuning/jobs/{id}/events lists its progress, warnings and metrics.
  • POST /fine_tuning/jobs/{id}/cancel stops it. A job that has ended, or is registering its adapter, answers 409 invalid_state. Only a job that succeeds registers a model.

Statuses run validating_files, queued, running, then succeeded, failed or cancelled. Job ids are ftjob- followed by 32 hex characters.

Your account trains one fine-tuning job at a time. How many can be open at once is your account's limit. A new account can have 1 open, so a second create answers 429 limit_exceeded until the first ends, with your limit in error.limit. On an account allowed more, the second job is accepted and waits in validating_files with a waiting_for_capacity event until it can start, for up to 24 hours.

A new job can then stay queued for a few minutes while Gridline finds and starts a GPU for it; at busy times it can take longer. You aren't charged for time spent waiting.

Queued

You aren't charged for time spent waiting.

Running

A job must finish within 2 hours.

Succeeded

The model is yours. Download it.

Waiting for a GPU is not charged. Check the job in your dashboard or its API response: gridline.reason, when available, explains the wait and what to do. A service health check does not guarantee that a GPU can start.

  • Account capacity: wait for another job to end. An accepted job retries automatically; do not resubmit it.
  • Customer credit: check your current balance before adding more. If you already added enough, wait for the next check. Resubmit only after a failed job.
  • Gridline capacity: wait. Adding credit or submitting duplicates will not help.
  • Platform daily limit: wait for capacity; the budget resets at 00:00 UTC.

New self-serve accounts normally allow 1 open fine-tune, 2 open batches and $100 per UTC day. Your account may have different limits. An account-limit check returns the effective limit and current use in error.limit. Other refusals, including a concurrent create that fills the last slot, may only have an error code and message. A refused job create does not create or charge a job. For an open-job limit, retry after a job ends. Daily limits reset at 00:00 UTC; a deposit does not raise them. Contact [email protected] with your job ID to ask for help or a higher limit.

Errors

A failed job carries error.code and error.message, and the same code on its last event:

CodeMeaning
insufficient_quotaNot enough credit to start the job. Nothing ran.
job_unavailableNo GPU capacity for your account became free within 24 hours, or the job could not be started. Nothing ran and nothing was charged.
invalid_row, template_mismatchA training row could not be read or rendered with the base's chat template. Nothing was trained.
context_too_longNo row keeps an assistant token within max_seq_length. Raise it or shorten the rows.
time_limit_exceededTraining did not finish within the 2-hour v1 limit and was stopped by the platform. Nothing is charged.
platform_interruptedThe GPU running the job was lost and the job is not restarted. Under per-token billing nothing is charged; under pass-through billing the GPU time used is credited back.
job_expiredThe job did not finish within its time limit.
adapter_too_largeThe adapter is over its limit (383 MiB for lora and qlora, 128 MiB for DPO). Lower lora_rank.
adapter_invalidThe trained adapter is not a plain LoRA the model registry accepts.
adapter_quota_exceededYour account already holds 100 fine-tuned models. Delete some and run the job again.
adapter_registration_failedThe trained adapter could not be registered.
training_failedTraining failed for another reason.

A job that fails for a platform reason (for example training_failed or time_limit_exceeded) is not charged. A job that fails for a cause on your side after training started, such as adapter_quota_exceeded, is charged for the tokens it trained, without the per-job minimum. More open jobs than your account allows answers 429 limit_exceeded at create, with the limit in error.limit (see The job).

Eval loss

Eval loss is how you tell whether training helped: lower after than before means the model got better at your examples. Every job measures it on held-out data before and after training, with the same tokenizer and chat template. It uses your validation_file when you give one; otherwise it holds out 5% of your training rows (at least 1 row and at most 1,000).

When the job succeeds, gridline.result reports eval_loss_before, eval_loss and eval_rows, with eval_source set to validation_file or holdout, and train_loss, steps and seconds.

The same figures appear as a metrics event in GET /fine_tuning/jobs/{id}/events. Each loss is the mean cross-entropy over the assistant tokens that train. eval_rows counts the rows actually evaluated: a row with no assistant token within max_seq_length is skipped, and when none is left both losses are null. trained_tokens is counted with the base's tokenizer.

A QLoRA adapter is trained against a 4-bit quantized base and served on the bf16 base, so its outputs can differ slightly from what training measured.

Eval loss tells you the model fits your examples better. To grade its answers, run the same eval on your ft: model and on its base, with string checks, text similarity or a model grader, billed as batches.

Your ft: model

When a job succeeds, fine_tuned_model names the adapter it trained:

ft:<base-model-id>:<org>:<job-suffix>
ft:qwen2.5-7b-instruct:o3f9a1c2d4e5:7b2c9e41a0d3
  • <org> is stable for your account and reveals nothing about it. It is not a credential: every use checks that the model is yours.
  • <job-suffix> is the first 12 hex characters of the job id after ftjob-, and is unique within your account.
  • gridline.fine_tuned_adapter gives the model, whether it is servable, and the reason when it is not. fine_tuned_model stays null until the adapter is servable.
  • Fine-tunes of gpt-oss-20b and qwen3.5-9b are download-only for now: their ft: models cannot run in a batch yet, and fine_tuned_model stays null when the job succeeds. Download the adapter by the name in gridline.fine_tuned_adapter.model.
  • GET /models lists your ft: models with the catalog, for you only; GET /models/{model} returns one.
  • Download the adapter, two files, with your token: GET /gridline/adapters/{model}/files/adapter_model.safetensors and .../files/adapter_config.json, under the base URL (https://api.gridlinegpu.com/openai/v1/gridline/adapters/...). Each response carries the file's SHA-256 in x-gridline-sha256.
  • DELETE /models/{model} deletes the model: from then on it cannot be named in a batch, listed or downloaded. The same request deletes its adapter files from storage; if a storage delete fails, the failure is recorded and the files stay unreachable through the API.
  • Name it as body.model in a batch, alone or with up to 7 other ft: models of the same base; see the Batch API.
curl https://api.gridlinegpu.com/openai/v1/gridline/adapters/ft:qwen2.5-7b-instruct:o3f9a1c2d4e5:7b2c9e41a0d3/files/adapter_model.safetensors \
  -H "Authorization: Bearer $GRIDLINE_TOKEN" -o adapter_model.safetensors

Merged weights

Download merged weights: POST /v1/gridline/adapters/{model}/merged starts a merge of your adapter into its base; GET the same path for its status and file list; GET …/merged/files/{path} downloads each file.

Merged weights: download your fine-tuned model as full safetensors at no charge. Each export is kept for 7 days.

  • The merge adds the adapter to the base's bf16 weights (W + B·A × lora_alpha / r, rounded once to bf16, as peft's merge_and_unload does) and keeps the base's shard layout. Status goes queued, running, then succeeded or failed; after 7 days it reads expired, and a new POST merges again.
  • An export holds the base's files with the adapted shards changed, the base's licence (LICENSE; for Llama 3.1 8B also USE_POLICY.md, its Acceptable Use Policy), a NOTICE that says what was changed, and gridline_merge.json, which lists the modified files and every file's SHA-256. Each download is a redirect that carries the file's SHA-256 in x-gridline-sha256.
  • Built with Llama: a merged llama-3.1-8b-instruct export is a copy of Meta's Llama Materials. Its NOTICE carries Meta's attribution notice, and if you distribute it, the Llama 3.1 Community License applies to you, including its rule that the name of a model you distribute begins with "Llama".
  • 1 merge at a time and 10 a day per account; at most 4 exports and 64 GB kept per account. DELETE the same path deletes the export and cancels a pending merge.

A POST the adapter cannot have answers 400 invalid_request, "This adapter cannot be merged (code).":

CodeMeaning
merge_base_unknownThe adapter's base is not a fine-tuning base model.
merge_base_revisionThe adapter was trained on another revision of its base than the one Gridline pins.
merge_licence_unreviewedThe base's licence has not been reviewed for handing out merged copies yet.
merge_tied_lm_headThe adapter trains lm_head on a base whose lm_head shares the embedding weights (the Qwen2.5 0.5B and 1.5B bases), so it cannot be merged.

A failed merge carries error.code on the merged-weights object. Nothing is charged for a merge, failed or not; POST again to retry:

CodeMeaning
merge_adapter_changedThe adapter's files changed after the merge was requested.
merge_adapter_unavailableThe adapter's files could not be read.
merge_adapter_configadapter_config.json is not a LoRA configuration for this base.
merge_adapter_headerThe adapter's safetensors header could not be read.
merge_adapter_tensor_nameAn adapter tensor is not a lora_A or lora_B weight of a known module.
merge_adapter_dtypeAn adapter weight is not F32, BF16 or F16.
merge_adapter_pairAn adapted module is missing its lora_A or lora_B weight.
merge_adapter_shapeAn adapter weight's shape does not match its module or rank.
merge_rankThe adapter's rank is out of range.
merge_alphaThe adapter's lora_alpha is out of range.
merge_scalelora_alpha / r is not a finite number.
merge_base_tensor_missingAn adapted module has no weight in the base.
merge_base_tensor_duplicateA base weight appears in more than one shard.
merge_base_dtypeAn adapted base weight is not BF16.
merge_base_headerA base shard's safetensors header could not be read.
merge_base_digestA base file's bytes did not match the pinned SHA-256.
merge_base_unavailableHugging Face did not serve a base file at the pinned revision.
merge_base_rangeHugging Face answered a ranged read with the whole file.
merge_base_shortHugging Face returned fewer bytes than asked for.
merge_base_windowThe merge machine's read-window settings for a base file were invalid.
merge_base_pathA base file name was refused.
merge_licence_digestA licence file's bytes were not the reviewed ones.
merge_factor_unknownAn adapter weight named in the plan is not in the adapter file.
merge_factor_sizeAn adapter weight's bytes did not match its header.
merge_factor_dtypeAn adapter weight could not be decoded.
merge_factor_shapeAn adapter weight's shape did not match the base weight it adapts.
merge_source_headerA base shard's header changed while it was read.
merge_source_shortA base shard ended before its header said.
merge_source_longA base shard was longer than its header said.
merge_target_tensorTwo adapted base weights overlap in one shard.
merge_output_sizeA merged shard's size differs from the base shard's.
merge_timeoutThe merge did not finish within 15 minutes and was stopped.
merge_attempts_exhaustedThe merge was tried 3 times.
merge_machine_memoryThe base is too large for the merge machine.
merge_machine_unavailableNo merge machine could be started.
merge_machine_exitedThe merge machine stopped without a result.
merge_machine_unsetThe merge machine was not configured.
merge_spec_invalidThe merge machine could not read its instructions.
merge_credentials_unavailableStorage credentials for the merge could not be issued.
merge_result_invalidThe merge machine's files did not match the plan.
merge_readback_mismatchA stored file's bytes did not match when Gridline read them back, so the export was deleted.
merge_readback_unavailableGridline could not read the stored files back in time to check them.
merge_weights_quotaYour account already holds 4 exports or 64 GB of them. Delete one and POST again.
merge_weights_goneThe export was deleted before the merge finished.
merge_weights_prefixThe export's storage location was refused.
merge_failedThe merge failed for another reason.

Limits

  • Supervised fine-tuning on chat data, LoRA and QLoRA adapters only.
  • Full fine-tunes are not offered; merged weights are a download of your adapter merged into its base (Merged weights).
  • A job must finish within 2 hours (v1); create refuses one estimated longer. The estimate uses a conservative training speed for each base, at or below what we measured, so a job fits only if its training tokens, counting every epoch, are at most about: 16,500,000 (qwen2.5-0.5b-instruct), 5,940,000 (qwen2.5-1.5b-instruct), 5,280,000 (qwen2.5-7b-instruct, LoRA; 1,980,000 with QLoRA), 4,950,000 (mistral-7b-instruct-v0.3, LoRA; 1,848,000 with QLoRA), 4,620,000 (qwen3-8b, LoRA; 1,650,000 with QLoRA), 4,752,000 (llama-3.1-8b-instruct, LoRA; 1,782,000 with QLoRA), 858,000 (gpt-oss-20b, LoRA), 2,640,000 (qwen3.5-9b, LoRA). A bit less fits once evaluation is counted; the refusal says exactly how many.
  • Since 10 November 2026, create refuses larger jobs than before, and quotes show longer estimated times and higher estimated costs. The old estimate assumed faster training than our GPUs deliver, so jobs it admitted could run past the limit and fail.
  • One job trains at a time per account. New accounts can have 1 open at once, other accounts 3, unless Gridline has set a different limit; a refused create gives yours in error.limit.
  • At most 100 models per account, including imports. Each open job reserves one slot and space for the largest accepted adapter before training starts. After it ends, only its stored adapter counts. Wait for a job to finish, cancel it before registration, or delete a model to make room.

Your data

  • Training and validation files are stored under your account. Only your account can read them, use them in a job, or see the jobs and models made from them; any other account is answered not found.
  • Rows are never logged. A refused file names the line and a fixed error code, never its content. Job events carry counts and losses, never rows.
  • A fine-tune file is kept for 30 days after upload, and you can delete it sooner. While a job that uses it is running, it cannot be deleted and does not expire.
  • Fine-tuning jobs and their events are kept for 180 days after they end.
  • A fine-tuned model is kept until you delete it. Deleting it makes it unusable at once and requests deletion of its adapter files from storage; a batch already running keeps the copy it staged until it ends.

Your training and validation files are used only to train your fine-tuned model. We don't use them to train any other model, and we don't sell or share them, except with the infrastructure providers that run the service: RunPod and Google Cloud (the GPUs that train your model), Cloudflare R2 and Supabase (storage) and Fly.io (our API servers), which process them only to provide it. Uploaded files are deleted 30 days after upload, or sooner if you delete them. Copies made for a training run are deleted 7 days after it ends. Fine-tuning job records are deleted 180 days after the job ends. Your fine-tuned model is yours: you can download or delete it at any time.

Costs

Fine-tuning and batch jobs are paid for from your prepaid credits. A fine-tuning job is charged for its training tokens times the number of epochs, at its base model's rate, and at least the per-job minimum. It is quoted when you create it, before it starts: creating the job starts it, and the same response carries the quote, with those tokens, the minimum and the most the job can cost. When the job succeeds the charge is deducted from your credits, and it is never more than the quote. Credit is checked when the job starts; a job your credit cannot fund fails with insufficient_quota before anything runs.

What a credit is worth, the credit packs and each model's rates are on the pricing page.

Contact us

Whether you have a question about fine-tuning or batch, need help with a job, or want to share feedback, send us a note. We’ll make sure it reaches the right person.

New messageGridline / contact
0/5,000