← LoRA Desk
API
Tokens

Run LoRA Desk from your own code

Both metered lanes are available over HTTP. Each takes a training config as the page has read it and returns one JSON object. The division of labour: the page reads the config (it never runs it), finds every adapter and trainer knob with its line, detects the framework, the method and the model's shape, works out the effective batch, the adapter's size and a memory floor, and checks every knob against the recipe - all free, in lorakit.js. The run judges or rewrites; the page then holds the reply to its own checks (lorareply.js).

Base URL and the response shape

Every endpoint lives under https://api.skillsafe.ai/v1/app-api, and every response has one shape:

{ "ok": true,  "data":  { ... } }
{ "ok": false, "error": { "code": "...", "message": "..." } }

The token is minted for this app (the guest call takes {"slug":"lora-desk"}), so no slug header is needed afterwards. Send it as Authorization: Bearer …. The run body is the input object, sent bare.

StatusCodeMeaning
400validation_errorThe body is not a JSON object, or a required key (task, target, setup, config, facts, review) is missing.
401unauthorizedNo token, or a stale one. Mint a guest token or sign in again.
402insufficient_creditsThe balance is under min_credits. Price with /estimate first.
403forbiddenA guest token tried to run: both lanes are metered and need a personal sign-in.
404not_foundUnknown job id.
429rate_limitedBack off and retry.
5xxserver_errorTransient. Retry with the same Idempotency-Key so a retry never double-bills.

The task field comes first

One app, one model, two lanes - the task field picks the lane. An unknown or missing task is answered as review, and the reply names the lane it answered.

taskStageReturns
reviewinspectInspect: a verdict (ready, adjust or rethink), a summary, every knob against the recipe (knobs), the failure modes the config invites (risks), what is already right (keep) and next.
rewriteproduceProduce: the framework, the whole corrected config as one string, changes (knob, from, to, why), what no config can settle (unresolved) and a note. Hand it a review's knob lines in review.

The input

FieldRequiredWhat
taskyesreview or rewrite. If it is missing or neither, do review and set "task": "review".
targetyesfor rewrite: same (keep the paste's framework), unsloth, peft-trl or axolotl. same for review.
setupyesthe framework, the method, the base model and its shape, the GPU and its BF16 support, the task type and the dataset size, as the page read or was told them.
configyesthe training config as pasted (a long one keeps its head and tail, with a marked cut).
factsyesthe page's reading: every knob as set (value, line, the recipe's value), the computed numbers, the expected verdict, and every finding tagged [fix], [check] or [info] with a code.
reviewyesfor rewrite, an earlier review of this config (a verdict and knob lines), or none.
retry_notenooptional; if present, your previous reply was unusable - follow it.

The recipe the model applies

House rules

The replies

review

  1. verdict - copy the expected verdict from facts: rethink when a method-level finding is at fix (no adapter, the base does not fit, a GPU without BF16, Unsloth with assistant_only_loss), adjust when any other finding is at fix, ready when none is.
  2. summary - one sentence, at most 35 words: what this run is and the one thing that matters most.
  3. knobs - at most 14 entries, every [fix] finding first, then [check] findings, then knobs already right that are worth confirming. An [info] finding becomes a check entry when it flags something worth changing and an ok entry when it only confirms a choice; one about a choice the recipe advises against (for example QLoRA where bf16 LoRA fits) always appears here or in risks, though it does not change the verdict. Each: knob (the config's own name, for example lora_alpha or learning_rate - or, for a finding about no single parameter, the short label facts gives it, such as memory, method or effective batch), now (the value as set, or "not set"), recipe (the value the recipe wants, concrete - for example 32 or 2e-4), level (fix, check or ok, matching the finding), why (one sentence, at most 25 words, citing the recipe rule).
  4. risks - at most 4 entries, one per failure mode this config invites: looks_like (the symptom in training, for example loss spikes), cause (the config choice, one sentence), fix (one sentence). [] when none.
  5. keep - at most 5 short strings: choices already right. [] when none.
  6. next - one sentence, at most 30 words: what to do before training.
{
  "task": "review",
  "verdict": "ready | adjust | rethink",
  "summary": "one sentence",
  "knobs": [ { "knob": "lora_alpha", "now": "16", "recipe": "32", "level": "fix", "why": "one sentence" } ],
  "risks": [ { "looks_like": "symptom", "cause": "one sentence", "fix": "one sentence" } ],
  "keep": [ "a choice already right" ],
  "next": "one sentence"
}

The page holds it to three checks: the verdict must be the one its rule gives (rethink when a method-level finding is at fix - no adapter, a base that does not fit, a GPU without BF16, Unsloth with assistant_only_loss; adjust when any other finding is at fix; ready otherwise), every fix-level finding must be taken up in knobs, and a quoted now for r, lora_alpha or learning_rate must match the config.

rewrite

  1. framework - unsloth, peft-trl or axolotl: the paste's own when target is same, otherwise target.
  2. config - the whole runnable config as one string, in that framework (Python for unsloth and peft-trl, YAML for axolotl), keeping the person's base model, dataset, paths, output directory, epochs and every unrelated line. Correct every [fix] finding the config can settle and every [check] finding that has a recipe value: all-linear targets, lora_alpha = 2 x r, rank from the task row, the LR from the method's row, effective batch under 32 (prefer lowering accumulation), bf16=True and fp16 off, the current TRL argument names, and the Unsloth defaults when the framework is unsloth. When the base does not fit in bf16, switch to QLoRA (4-bit NF4 base, LR 2e-4). When review is not none, apply its knob lines that correct a wrong value unless they contradict the recipe; a line that only notes an alternative that would also work (for example, bf16 LoRA that would also fit) is advisory - mention it in unresolved or note and do not restructure the method for it. To bring the effective batch under 32, make the smallest reduction that does it. Comments in the config may say what changed, briefly.
  3. changes - at most 16 entries, one per knob you changed: knob, from (as set, or "not set"), to (exactly as it now appears in the config), why (one sentence, at most 20 words).
  4. unresolved - at most 5 short strings: what no config can settle (for example a GPU without BF16, or a base that does not fit even in 4-bit), and anything to check before running. [] when none.
  5. note - one sentence, at most 30 words: what to run first.
{
  "task": "rewrite",
  "framework": "unsloth | peft-trl | axolotl",
  "config": "the whole corrected config",
  "changes": [ { "knob": "lora_alpha", "from": "16", "to": "32", "why": "one sentence" } ],
  "unresolved": [ "what the config cannot settle" ],
  "note": "one sentence"
}

The page scans the rewritten config with the same engine that read the paste: it must read as the requested framework, keep the base model, leave nothing at fix that a config can settle, and contain every to value its change list claims. It shows a line diff against the paste.

Worked examples - every lane, every sample

These are the page's three free samples. Each input is exactly what the page sends; each reply is the saved reply the sample replays, shipped as the validating model wrote it; the last line is the page's own check of that reply.

An Unsloth notebook, 8B QLoRA - review

An Unsloth + TRL config, QLoRA; the page finds 9 findings, 6 at fix, so a review should say adjust.

Input:

{
 "task": "review",
 "target": "same",
 "setup": "Framework: Unsloth + TRL\nMethod: QLoRA (4-bit NF4 base, bf16 adapters)\nBase model: Llama 3.1 8B (\"unsloth/Meta-Llama-3.1-8B-Instruct-bnb-4bit\") - 8.03B parameters, 32 layers, hidden 4096, MLP 14336\nGPU: 1 x NVIDIA L4 24 GB (BF16 yes)\nTask: General SFT; dataset rows: 12000",
 "config": "from unsloth import FastLanguageModel, is_bfloat16_supported\nfrom trl import SFTTrainer, SFTConfig\nfrom datasets import load_dataset\n\nmax_seq_length = 2048\nmodel, tokenizer = FastLanguageModel.from_pretrained(\n    model_name = \"unsloth/Meta-Llama-3.1-8B-Instruct-bnb-4bit\",\n    max_seq_length = max_seq_length,\n    dtype = None,\n    load_in_4bit = True,\n)\n\nmodel = FastLanguageModel.get_peft_model(\n    model,\n    r = 16,\n    target_modules = [\"q_proj\", \"k_proj\", \"v_proj\", \"o_proj\"],\n    lora_alpha = 16,\n    lora_dropout = 0.05,\n    bias = \"none\",\n    use_gradient_checkpointing = \"unsloth\",\n    random_state = 3407,\n)\n\ndataset = load_dataset(\"json\", data_files = \"recipes.jsonl\", split = \"train\")\n\ntrainer = SFTTrainer(\n    model = model,\n    tokenizer = tokenizer,\n    train_dataset = dataset,\n    args = SFTConfig(\n        dataset_text_field = \"text\",\n        max_seq_length = max_seq_length,\n        per_device_train_batch_size = 8,\n        gradient_accumulation_steps = 8,\n        warmup_steps = 5,\n        num_train_epochs = 3,\n        learning_rate = 2e-5,\n        fp16 = not is_bfloat16_supported(),\n        bf16 = is_bfloat16_supported(),\n        logging_steps = 1,\n        optim = \"adamw_8bit\",\n        seed = 3407,\n        output_dir = \"outputs\",\n    ),\n)\ntrainer.train()\n",
 "facts": "Knobs as set (value - where - recipe):\n- method: QLoRA (4-bit NF4 base, bf16 adapters) - recipe: LoRA unless memory forces QLoRA\n- base model: unsloth/Meta-Llama-3.1-8B-Instruct-bnb-4bit (line 7, from_pretrained()) - recipe: size class decides\n- r: 16 (line 15, get_peft_model()) - recipe: 16-32\n- lora_alpha: 16 (line 17, get_peft_model()) - recipe: 2 x r = 32\n- target_modules: [q_proj, k_proj, v_proj, o_proj] (line 16, get_peft_model()) - recipe: all linear: q_proj, k_proj, v_proj, o_proj, gate_proj, up_proj, down_proj\n- lora_dropout: 0.05 (line 18, get_peft_model()) - recipe: 0 (fused kernels)\n- bias: none (line 19, get_peft_model()) - recipe: none\n- use_rslora: not set - recipe: on only at r >= 32, SFT at scale\n- learning_rate: 2e-5 (line 37, SFTConfig()) - recipe: 2e-4\n- per-device batch: 8 (line 33, SFTConfig()) - recipe: effective batch under 32\n- gradient_accumulation_steps: 8 (line 34, SFTConfig()) - recipe: effective batch under 32\n- effective batch: 8 x 8 x 1 = 64 - recipe: under 32\n- precision: bf16=is_bfloat16_supported() (line 39, SFTConfig()) - recipe: bf16=True, never fp16\n- optim: adamw_8bit (line 41, SFTConfig()) - recipe: adamw_8bit\n- gradient checkpointing: unsloth (line 20, get_peft_model()) - recipe: \"unsloth\"\n- max length: max_seq_length=2048 (line 32, SFTConfig()) - recipe: on SFTConfig as max_length\n- packing: not set - recipe: template before packing\n- epochs: 3 (line 36, SFTConfig()) - recipe: -\n- seed: 3407 (line 42, SFTConfig()) - recipe: fixed\nComputed: effective batch 64, at most 131,072 tokens per optimizer step; adapter 13,631,488 trainable parameters (0.17% of 8.03B) on 4 modules x 32 layers at r 16; memory floor 6.44 GB per GPU of 24 GB (base 5.31 GB + adapter, gradients and optimizer 0.13 GB + 1 GB context), activations extra\nExpected verdict: adjust\nFindings (9):\n[fix] alpha_ratio (lora_alpha): lora_alpha is 16 with r 16 (ratio 1); the recipe sets lora_alpha = 2 x r = 32.\n[fix] targets_mlp (target_modules): target_modules leaves out gate_proj, up_proj, down_proj - the MLP layers matter most. Target all linear modules: q_proj, k_proj, v_proj, o_proj, gate_proj, up_proj, down_proj.\n[fix] lr_low (learning_rate): learning_rate is 2e-5, below the LoRA table's floor of 5e-5 - this looks like a full fine-tuning LR. LoRA runs about 10x the full-FT LR; 2e-4 is the QLoRA standard.\n[fix] batch_high (effective batch): Effective batch is 8 x 8 x 1 GPU = 64; the recipe was validated under 32 - going higher is an untested extrapolation, not a free throughput win.\n[check] fp16_fallback (precision): fp16 is set to the inverse of is_bfloat16_supported() (line 38, SFTConfig()), so a card without BF16 silently trains in fp16; the recipe gates on BF16 support and refuses fp16.\n[check] unsloth_dropout (lora_dropout): lora_dropout is 0.05; Unsloth's fused kernels assume 0 - a nonzero value forfeits the speedup.\n[check] unsloth_padding_free (padding_free): padding_free is not set to False; Unsloth's compiled trainer can raise a padding_free/max_length error even from a plain SFTConfig - pass padding_free=False explicitly.\n[fix] stale_tokenizer (tokenizer=): SFTTrainer(tokenizer=...) (line 28, SFTTrainer()) is the old, removed form; current TRL takes processing_class=tokenizer.\n[fix] stale_max_seq_length (max length): max_seq_length on SFTConfig (line 32, SFTConfig()) was renamed: current TRL takes max_length on SFTConfig.",
 "review": "none"
}

Reply:

{
 "task": "review",
 "verdict": "adjust",
 "summary": "A QLoRA fine-tune of Llama 3.1 8B on a single L4 24GB GPU; the sharpest problem is a learning rate far below the LoRA range, which will under-train the adapter.",
 "knobs": [
  {
   "knob": "lora_alpha",
   "now": "16",
   "recipe": "32",
   "level": "fix",
   "why": "Alpha should always be set to twice the rank; with a rank of 16 the value here should be 32, not 16."
  },
  {
   "knob": "target_modules",
   "now": "q_proj, k_proj, v_proj, o_proj",
   "recipe": "q_proj, k_proj, v_proj, o_proj, gate_proj, up_proj, down_proj",
   "level": "fix",
   "why": "Leaving out the three feed-forward projections skips where most of the adaptation capacity lives; attention-only coverage is the older, weaker setup."
  },
  {
   "knob": "learning_rate",
   "now": "2e-5",
   "recipe": "2e-4",
   "level": "fix",
   "why": "This rate reads like a full fine-tune setting; a LoRA adapter needs roughly ten times that, with 2e-4 as the QLoRA standard."
  },
  {
   "knob": "effective batch",
   "now": "64",
   "recipe": "under 32",
   "level": "fix",
   "why": "Eight per-device times eight accumulation steps on one GPU reaches 64, double the validated ceiling; lower the accumulation first."
  },
  {
   "knob": "tokenizer",
   "now": "tokenizer=tokenizer",
   "recipe": "processing_class=tokenizer",
   "level": "fix",
   "why": "The current trainer version expects the tokenizer under a renamed argument; the old one has been removed."
  },
  {
   "knob": "max_seq_length",
   "now": "2048 (on SFTConfig)",
   "recipe": "max_length",
   "level": "fix",
   "why": "This option was renamed on the trainer's configuration class; the old name no longer has any effect there."
  },
  {
   "knob": "fp16",
   "now": "not is_bfloat16_supported()",
   "recipe": "False",
   "level": "check",
   "why": "Gating precision on whether the hardware supports full precision means a card without it will silently train in half precision."
  },
  {
   "knob": "lora_dropout",
   "now": "0.05",
   "recipe": "0",
   "level": "check",
   "why": "This framework's fused kernels assume no dropout on the adapter; a nonzero value here forfeits the speed benefit."
  },
  {
   "knob": "padding_free",
   "now": "not set",
   "recipe": "False",
   "level": "check",
   "why": "This compiled trainer can raise an error about padding and sequence length unless this option is explicitly set to false."
  },
  {
   "knob": "bias",
   "now": "none",
   "recipe": "none",
   "level": "ok",
   "why": "Already matches the recipe's default for adapter bias."
  },
  {
   "knob": "optim",
   "now": "adamw_8bit",
   "recipe": "adamw_8bit",
   "level": "ok",
   "why": "Already the optimizer the recipe expects for this framework."
  },
  {
   "knob": "use_gradient_checkpointing",
   "now": "unsloth",
   "recipe": "unsloth",
   "level": "ok",
   "why": "Already set to the framework-specific mode that saves meaningful memory."
  },
  {
   "knob": "random_state",
   "now": "3407",
   "recipe": "fixed (e.g. 3407)",
   "level": "ok",
   "why": "A fixed seed is already set, as the recipe expects for a reproducible run."
  },
  {
   "knob": "r",
   "now": "16",
   "recipe": "16-32",
   "level": "ok",
   "why": "Rank already sits inside the recommended range for general supervised fine-tuning."
  }
 ],
 "risks": [
  {
   "looks_like": "loss barely moving across training",
   "cause": "The learning rate is set roughly ten times below the LoRA range, which under-trains the adapter.",
   "fix": "Raise it toward the QLoRA standard of 2e-4."
  },
  {
   "looks_like": "fluent output that misses the task's substance",
   "cause": "Adaptation only touches the attention projections and skips the feed-forward layers that carry most of the capacity.",
   "fix": "Target all seven linear modules, including the feed-forward gate, up and down projections."
  },
  {
   "looks_like": "sudden loss spikes or divergence depending on the hardware it runs on",
   "cause": "Precision falls back to half precision whenever full-precision support isn't detected, instead of always forcing it on.",
   "fix": "Force the precision flag to always use full bf16 and drop the fallback."
  },
  {
   "looks_like": "a crash when the trainer is built or on the very first step",
   "cause": "The tokenizer is passed under an argument name the current trainer has removed, and the padding behavior is left unset.",
   "fix": "Pass the tokenizer under the current argument name and explicitly turn off padding-free training."
  }
 ],
 "keep": [
  "bias left at none",
  "8-bit AdamW optimizer",
  "framework-specific gradient checkpointing mode",
  "fixed random seed",
  "rank within the general fine-tuning range"
 ],
 "next": "Fix the alpha ratio, add the missing feed-forward targets, raise the learning rate, and cut the effective batch under 32 before starting this run."
}

The page's check: kept - nothing to flag

An Unsloth notebook, 8B QLoRA - rewrite

An Unsloth + TRL config, QLoRA; the page finds 9 findings, 6 at fix, so a review should say adjust. This rewrite was handed the review above in its review field.

Input:

{
 "task": "rewrite",
 "target": "same",
 "setup": "Framework: Unsloth + TRL\nMethod: QLoRA (4-bit NF4 base, bf16 adapters)\nBase model: Llama 3.1 8B (\"unsloth/Meta-Llama-3.1-8B-Instruct-bnb-4bit\") - 8.03B parameters, 32 layers, hidden 4096, MLP 14336\nGPU: 1 x NVIDIA L4 24 GB (BF16 yes)\nTask: General SFT; dataset rows: 12000",
 "config": "from unsloth import FastLanguageModel, is_bfloat16_supported\nfrom trl import SFTTrainer, SFTConfig\nfrom datasets import load_dataset\n\nmax_seq_length = 2048\nmodel, tokenizer = FastLanguageModel.from_pretrained(\n    model_name = \"unsloth/Meta-Llama-3.1-8B-Instruct-bnb-4bit\",\n    max_seq_length = max_seq_length,\n    dtype = None,\n    load_in_4bit = True,\n)\n\nmodel = FastLanguageModel.get_peft_model(\n    model,\n    r = 16,\n    target_modules = [\"q_proj\", \"k_proj\", \"v_proj\", \"o_proj\"],\n    lora_alpha = 16,\n    lora_dropout = 0.05,\n    bias = \"none\",\n    use_gradient_checkpointing = \"unsloth\",\n    random_state = 3407,\n)\n\ndataset = load_dataset(\"json\", data_files = \"recipes.jsonl\", split = \"train\")\n\ntrainer = SFTTrainer(\n    model = model,\n    tokenizer = tokenizer,\n    train_dataset = dataset,\n    args = SFTConfig(\n        dataset_text_field = \"text\",\n        max_seq_length = max_seq_length,\n        per_device_train_batch_size = 8,\n        gradient_accumulation_steps = 8,\n        warmup_steps = 5,\n        num_train_epochs = 3,\n        learning_rate = 2e-5,\n        fp16 = not is_bfloat16_supported(),\n        bf16 = is_bfloat16_supported(),\n        logging_steps = 1,\n        optim = \"adamw_8bit\",\n        seed = 3407,\n        output_dir = \"outputs\",\n    ),\n)\ntrainer.train()\n",
 "facts": "Knobs as set (value - where - recipe):\n- method: QLoRA (4-bit NF4 base, bf16 adapters) - recipe: LoRA unless memory forces QLoRA\n- base model: unsloth/Meta-Llama-3.1-8B-Instruct-bnb-4bit (line 7, from_pretrained()) - recipe: size class decides\n- r: 16 (line 15, get_peft_model()) - recipe: 16-32\n- lora_alpha: 16 (line 17, get_peft_model()) - recipe: 2 x r = 32\n- target_modules: [q_proj, k_proj, v_proj, o_proj] (line 16, get_peft_model()) - recipe: all linear: q_proj, k_proj, v_proj, o_proj, gate_proj, up_proj, down_proj\n- lora_dropout: 0.05 (line 18, get_peft_model()) - recipe: 0 (fused kernels)\n- bias: none (line 19, get_peft_model()) - recipe: none\n- use_rslora: not set - recipe: on only at r >= 32, SFT at scale\n- learning_rate: 2e-5 (line 37, SFTConfig()) - recipe: 2e-4\n- per-device batch: 8 (line 33, SFTConfig()) - recipe: effective batch under 32\n- gradient_accumulation_steps: 8 (line 34, SFTConfig()) - recipe: effective batch under 32\n- effective batch: 8 x 8 x 1 = 64 - recipe: under 32\n- precision: bf16=is_bfloat16_supported() (line 39, SFTConfig()) - recipe: bf16=True, never fp16\n- optim: adamw_8bit (line 41, SFTConfig()) - recipe: adamw_8bit\n- gradient checkpointing: unsloth (line 20, get_peft_model()) - recipe: \"unsloth\"\n- max length: max_seq_length=2048 (line 32, SFTConfig()) - recipe: on SFTConfig as max_length\n- packing: not set - recipe: template before packing\n- epochs: 3 (line 36, SFTConfig()) - recipe: -\n- seed: 3407 (line 42, SFTConfig()) - recipe: fixed\nComputed: effective batch 64, at most 131,072 tokens per optimizer step; adapter 13,631,488 trainable parameters (0.17% of 8.03B) on 4 modules x 32 layers at r 16; memory floor 6.44 GB per GPU of 24 GB (base 5.31 GB + adapter, gradients and optimizer 0.13 GB + 1 GB context), activations extra\nExpected verdict: adjust\nFindings (9):\n[fix] alpha_ratio (lora_alpha): lora_alpha is 16 with r 16 (ratio 1); the recipe sets lora_alpha = 2 x r = 32.\n[fix] targets_mlp (target_modules): target_modules leaves out gate_proj, up_proj, down_proj - the MLP layers matter most. Target all linear modules: q_proj, k_proj, v_proj, o_proj, gate_proj, up_proj, down_proj.\n[fix] lr_low (learning_rate): learning_rate is 2e-5, below the LoRA table's floor of 5e-5 - this looks like a full fine-tuning LR. LoRA runs about 10x the full-FT LR; 2e-4 is the QLoRA standard.\n[fix] batch_high (effective batch): Effective batch is 8 x 8 x 1 GPU = 64; the recipe was validated under 32 - going higher is an untested extrapolation, not a free throughput win.\n[check] fp16_fallback (precision): fp16 is set to the inverse of is_bfloat16_supported() (line 38, SFTConfig()), so a card without BF16 silently trains in fp16; the recipe gates on BF16 support and refuses fp16.\n[check] unsloth_dropout (lora_dropout): lora_dropout is 0.05; Unsloth's fused kernels assume 0 - a nonzero value forfeits the speedup.\n[check] unsloth_padding_free (padding_free): padding_free is not set to False; Unsloth's compiled trainer can raise a padding_free/max_length error even from a plain SFTConfig - pass padding_free=False explicitly.\n[fix] stale_tokenizer (tokenizer=): SFTTrainer(tokenizer=...) (line 28, SFTTrainer()) is the old, removed form; current TRL takes processing_class=tokenizer.\n[fix] stale_max_seq_length (max length): max_seq_length on SFTConfig (line 32, SFTConfig()) was renamed: current TRL takes max_length on SFTConfig.",
 "review": "Verdict: adjust - A QLoRA fine-tune of Llama 3.1 8B on a single L4 24GB GPU; the sharpest problem is a learning rate far below the LoRA range, which will under-train the adapter.\n- [fix] lora_alpha: 16 -> 32 (Alpha should always be set to twice the rank; with a rank of 16 the value here should be 32, not 16.)\n- [fix] target_modules: q_proj, k_proj, v_proj, o_proj -> q_proj, k_proj, v_proj, o_proj, gate_proj, up_proj, down_proj (Leaving out the three feed-forward projections skips where most of the adaptation capacity lives; attention-only coverage is the older, weaker setup.)\n- [fix] learning_rate: 2e-5 -> 2e-4 (This rate reads like a full fine-tune setting; a LoRA adapter needs roughly ten times that, with 2e-4 as the QLoRA standard.)\n- [fix] effective batch: 64 -> under 32 (Eight per-device times eight accumulation steps on one GPU reaches 64, double the validated ceiling; lower the accumulation first.)\n- [fix] tokenizer: tokenizer=tokenizer -> processing_class=tokenizer (The current trainer version expects the tokenizer under a renamed argument; the old one has been removed.)\n- [fix] max_seq_length: 2048 (on SFTConfig) -> max_length (This option was renamed on the trainer's configuration class; the old name no longer has any effect there.)\n- [check] fp16: not is_bfloat16_supported() -> False (Gating precision on whether the hardware supports full precision means a card without it will silently train in half precision.)\n- [check] lora_dropout: 0.05 -> 0 (This framework's fused kernels assume no dropout on the adapter; a nonzero value here forfeits the speed benefit.)\n- [check] padding_free: not set -> False (This compiled trainer can raise an error about padding and sequence length unless this option is explicitly set to false.)\n- [ok] bias: none -> none (Already matches the recipe's default for adapter bias.)\n- [ok] optim: adamw_8bit -> adamw_8bit (Already the optimizer the recipe expects for this framework.)\n- [ok] use_gradient_checkpointing: unsloth -> unsloth (Already set to the framework-specific mode that saves meaningful memory.)\n- [ok] random_state: 3407 -> fixed (e.g. 3407) (A fixed seed is already set, as the recipe expects for a reproducible run.)\n- [ok] r: 16 -> 16-32 (Rank already sits inside the recommended range for general supervised fine-tuning.)\n- risk: loss barely moving across training - The learning rate is set roughly ten times below the LoRA range, which under-trains the adapter.; Raise it toward the QLoRA standard of 2e-4.\n- risk: fluent output that misses the task's substance - Adaptation only touches the attention projections and skips the feed-forward layers that carry most of the capacity.; Target all seven linear modules, including the feed-forward gate, up and down projections.\n- risk: sudden loss spikes or divergence depending on the hardware it runs on - Precision falls back to half precision whenever full-precision support isn't detected, instead of always forcing it on.; Force the precision flag to always use full bf16 and drop the fallback.\n- risk: a crash when the trainer is built or on the very first step - The tokenizer is passed under an argument name the current trainer has removed, and the padding behavior is left unset.; Pass the tokenizer under the current argument name and explicitly turn off padding-free training."
}

Reply:

{
 "task": "rewrite",
 "framework": "unsloth",
 "config": "from unsloth import FastLanguageModel\nfrom trl import SFTTrainer, SFTConfig\nfrom datasets import load_dataset\n\nmax_seq_length = 2048\nmodel, tokenizer = FastLanguageModel.from_pretrained(\n    model_name = \"unsloth/Meta-Llama-3.1-8B-Instruct-bnb-4bit\",\n    max_seq_length = max_seq_length,\n    dtype = None,\n    load_in_4bit = True,\n)\n\nmodel = FastLanguageModel.get_peft_model(\n    model,\n    r = 16,\n    target_modules = [\"q_proj\", \"k_proj\", \"v_proj\", \"o_proj\", \"gate_proj\", \"up_proj\", \"down_proj\"],\n    lora_alpha = 32,\n    lora_dropout = 0,\n    bias = \"none\",\n    use_gradient_checkpointing = \"unsloth\",\n    random_state = 3407,\n)\n\ndataset = load_dataset(\"json\", data_files = \"recipes.jsonl\", split = \"train\")\n\ntrainer = SFTTrainer(\n    model = model,\n    processing_class = tokenizer,\n    train_dataset = dataset,\n    args = SFTConfig(\n        dataset_text_field = \"text\",\n        max_length = max_seq_length,\n        per_device_train_batch_size = 8,\n        gradient_accumulation_steps = 3,\n        warmup_steps = 5,\n        num_train_epochs = 3,\n        learning_rate = 2e-4,\n        bf16 = True,\n        fp16 = False,\n        padding_free = False,\n        logging_steps = 1,\n        optim = \"adamw_8bit\",\n        seed = 3407,\n        output_dir = \"outputs\",\n    ),\n)\ntrainer.train()\n",
 "changes": [
  {
   "knob": "lora_alpha",
   "from": "16",
   "to": "32",
   "why": "Alpha must always be twice the rank; with r at 16 it has to be 32, not 16."
  },
  {
   "knob": "target_modules",
   "from": "q_proj, k_proj, v_proj, o_proj",
   "to": "q_proj, k_proj, v_proj, o_proj, gate_proj, up_proj, down_proj",
   "why": "All seven linear modules matter most; attention-only targeting skips the MLP layers that carry most capacity."
  },
  {
   "knob": "learning_rate",
   "from": "2e-5",
   "to": "2e-4",
   "why": "2e-5 reads like a full fine-tune rate; the QLoRA standard adapter rate is 2e-4."
  },
  {
   "knob": "gradient_accumulation_steps",
   "from": "8",
   "to": "3",
   "why": "Per-device 8 times accumulation 8 gave an effective batch of 64; lowering accumulation keeps it at 24, under 32."
  },
  {
   "knob": "bf16",
   "from": "is_bfloat16_supported()",
   "to": "True",
   "why": "This GPU is confirmed BF16-capable, so precision should be forced on rather than runtime-checked."
  },
  {
   "knob": "fp16",
   "from": "not is_bfloat16_supported()",
   "to": "False",
   "why": "Precision must never fall back to fp16 on hardware that already supports full bf16."
  },
  {
   "knob": "lora_dropout",
   "from": "0.05",
   "to": "0",
   "why": "The fused kernels used here assume zero dropout; a nonzero value forfeits the speed gain."
  },
  {
   "knob": "padding_free",
   "from": "not set",
   "to": "False",
   "why": "The compiled trainer can raise a padding or length error unless this is passed explicitly."
  },
  {
   "knob": "tokenizer",
   "from": "tokenizer=tokenizer",
   "to": "processing_class=tokenizer",
   "why": "Current TRL removed the tokenizer argument; the trainer now takes processing_class instead."
  },
  {
   "knob": "max_length",
   "from": "max_seq_length (on SFTConfig)",
   "to": "max_length",
   "why": "SFTConfig renamed max_seq_length to max_length; the old name no longer has any effect there."
  }
 ],
 "unresolved": [],
 "note": "Run a short smoke test of a few hundred steps to confirm the new rate and batch settings before the full three-epoch run."
}

The page's check: kept - nothing to flag. The rewrite rescans with 0 at fix (a review of it would say ready).

PEFT + TRL, 14B on a 4090 - review

A PEFT + TRL config, bf16 LoRA; the page finds 3 findings, 1 at fix, so a review should say rethink.

Input:

{
 "task": "review",
 "target": "same",
 "setup": "Framework: PEFT + TRL\nMethod: LoRA (bf16 base)\nBase model: Qwen2.5 14B (\"Qwen/Qwen2.5-14B-Instruct\") - 14.8B parameters, 48 layers, hidden 5120, MLP 13824\nGPU: 1 x RTX 4090 24 GB (BF16 yes)\nTask: General SFT; dataset rows: 8000",
 "config": "import torch\nfrom datasets import load_dataset\nfrom peft import LoraConfig\nfrom transformers import AutoModelForCausalLM, AutoTokenizer\nfrom trl import SFTConfig, SFTTrainer\n\nmodel_id = \"Qwen/Qwen2.5-14B-Instruct\"\ntokenizer = AutoTokenizer.from_pretrained(model_id)\nmodel = AutoModelForCausalLM.from_pretrained(\n    model_id, torch_dtype=torch.bfloat16, attn_implementation=\"sdpa\"\n)\n\npeft_config = LoraConfig(\n    r=16,\n    lora_alpha=32,\n    lora_dropout=0.0,\n    bias=\"none\",\n    target_modules=\"all-linear\",\n    task_type=\"CAUSAL_LM\",\n)\n\nargs = SFTConfig(\n    output_dir=\"qwen14b-support-lora\",\n    max_length=4096,\n    per_device_train_batch_size=4,\n    gradient_accumulation_steps=4,\n    learning_rate=1e-4,\n    lr_scheduler_type=\"cosine\",\n    warmup_ratio=0.03,\n    num_train_epochs=2,\n    bf16=True,\n    gradient_checkpointing=True,\n    optim=\"adamw_torch\",\n    assistant_only_loss=True,\n)\n\ntrainer = SFTTrainer(\n    model=model,\n    args=args,\n    train_dataset=load_dataset(\"json\", data_files=\"support.jsonl\")[\"train\"],\n    processing_class=tokenizer,\n    peft_config=peft_config,\n)\ntrainer.train()\n",
 "facts": "Knobs as set (value - where - recipe):\n- method: LoRA (bf16 base) - recipe: LoRA unless memory forces QLoRA\n- base model: Qwen/Qwen2.5-14B-Instruct (line 7) - recipe: size class decides\n- r: 16 (line 14, LoraConfig()) - recipe: 16-32\n- lora_alpha: 32 (line 15, LoraConfig()) - recipe: 2 x r = 32\n- target_modules: all-linear (line 18, LoraConfig()) - recipe: all linear: q_proj, k_proj, v_proj, o_proj, gate_proj, up_proj, down_proj\n- lora_dropout: 0 (line 16, LoraConfig()) - recipe: 0\n- bias: none (line 17, LoraConfig()) - recipe: none\n- use_rslora: not set - recipe: on only at r >= 32, SFT at scale\n- learning_rate: 1e-4 (line 27, SFTConfig()) - recipe: 1e-4 (5e-5 very conservative)\n- per-device batch: 4 (line 25, SFTConfig()) - recipe: effective batch under 32\n- gradient_accumulation_steps: 4 (line 26, SFTConfig()) - recipe: effective batch under 32\n- effective batch: 4 x 4 x 1 = 16 - recipe: under 32\n- precision: bf16=True (line 31, SFTConfig()) - recipe: bf16=True, never fp16\n- optim: adamw_torch (line 33, SFTConfig()) - recipe: adamw_8bit\n- gradient checkpointing: True (line 32, SFTConfig()) - recipe: on when memory is tight\n- max length: max_length=4096 (line 24, SFTConfig()) - recipe: on SFTConfig as max_length\n- packing: not set - recipe: template before packing\n- epochs: 2 (line 30, SFTConfig()) - recipe: -\n- seed: not set - recipe: fixed\nComputed: effective batch 16, at most 65,536 tokens per optimizer step; adapter 68,812,800 trainable parameters (0.47% of 14.8B) on 7 modules x 48 layers at r 16; memory floor 29.5 GB per GPU of 24 GB (base 27.5 GB + adapter, gradients and optimizer 1.03 GB + 1 GB context), activations extra\nExpected verdict: rethink\nFindings (3):\n[info] seed_missing (seed): No seed is set; seed the run (SFTConfig(seed=...)) so it reproduces.\n[info] optim_32bit (optim): optim is adamw_torch; adamw_8bit cuts optimizer-state memory with negligible quality impact at adapter scale.\n[fix] wont_fit (memory): The floor alone is 29.5 GB against 24 GB per GPU (LoRA (bf16 base) of Qwen2.5 14B) before any activations - QLoRA is the recipe's answer when the base does not fit in bf16.",
 "review": "none"
}

Reply:

{
 "task": "review",
 "verdict": "rethink",
 "summary": "A plain LoRA fine-tune of Qwen2.5 14B on a single RTX 4090; the adapter and trainer knobs are sound, but the base model's memory footprint alone exceeds this GPU's 24 GB.",
 "knobs": [
  {
   "knob": "memory",
   "now": "29.5 GB floor on a 24 GB GPU (bf16 base, no quantization)",
   "recipe": "QLoRA (4-bit NF4 base)",
   "level": "fix",
   "why": "The computed memory floor exceeds the GPU's capacity before any activation memory, so the base does not fit in bf16 on this card."
  },
  {
   "knob": "seed",
   "now": "not set",
   "recipe": "fixed (e.g. 3407)",
   "level": "check",
   "why": "No seed is set anywhere in the training configuration, so this run will not reproduce exactly between attempts."
  },
  {
   "knob": "optim",
   "now": "adamw_torch",
   "recipe": "adamw_8bit",
   "level": "check",
   "why": "This optimizer keeps its state in full precision, using more memory than the 8-bit variant the recipe prefers."
  },
  {
   "knob": "lora_alpha",
   "now": "32",
   "recipe": "32",
   "level": "ok",
   "why": "Already set to twice the rank, as the recipe always requires."
  },
  {
   "knob": "target_modules",
   "now": "all-linear",
   "recipe": "q_proj, k_proj, v_proj, o_proj, gate_proj, up_proj, down_proj",
   "level": "ok",
   "why": "Already targets every linear projection, including the feed-forward layers that carry most of the capacity."
  },
  {
   "knob": "learning_rate",
   "now": "1e-4",
   "recipe": "1e-4",
   "level": "ok",
   "why": "Already at the recipe's conservative rate for a plain bf16 LoRA adapter."
  },
  {
   "knob": "effective batch",
   "now": "16",
   "recipe": "under 32",
   "level": "ok",
   "why": "Four per-device times four accumulation steps on one GPU stays safely under the validated ceiling."
  },
  {
   "knob": "bf16",
   "now": "True",
   "recipe": "True",
   "level": "ok",
   "why": "Precision is already forced to full bf16 with no half-precision fallback."
  },
  {
   "knob": "max_length",
   "now": "4096 (current argument name)",
   "recipe": "max_length (current, correct name)",
   "level": "ok",
   "why": "Already uses the current argument name on the trainer's configuration class rather than a renamed, removed one."
  }
 ],
 "risks": [
  {
   "looks_like": "an out-of-memory crash at model load or on the first training step",
   "cause": "The computed memory floor alone is above the GPU's full 24 GB before any activation memory is added.",
   "fix": "Switch to QLoRA with a 4-bit NF4 base so the frozen weights fit in far less memory."
  },
  {
   "looks_like": "a run that cannot be reproduced when repeated",
   "cause": "No random seed is set anywhere in the training configuration.",
   "fix": "Set a fixed seed value on the trainer's configuration."
  },
  {
   "looks_like": "higher GPU memory use than necessary just for optimizer state",
   "cause": "The optimizer keeps its state in full precision instead of the 8-bit variant.",
   "fix": "Switch the optimizer to its 8-bit form."
  }
 ],
 "keep": [
  "alpha already twice the rank",
  "all linear projections targeted, including feed-forward layers",
  "learning rate at the conservative end for this method",
  "effective batch comfortably under the ceiling",
  "precision forced to full bf16 with no fallback"
 ],
 "next": "Switch to QLoRA with a 4-bit NF4 base before running, since the bf16 base alone does not fit on this GPU's 24 GB."
}

The page's check: kept - nothing to flag

PEFT + TRL, 14B on a 4090 - rewrite

A PEFT + TRL config, bf16 LoRA; the page finds 3 findings, 1 at fix, so a review should say rethink. This rewrite was handed the review above in its review field.

Input:

{
 "task": "rewrite",
 "target": "same",
 "setup": "Framework: PEFT + TRL\nMethod: LoRA (bf16 base)\nBase model: Qwen2.5 14B (\"Qwen/Qwen2.5-14B-Instruct\") - 14.8B parameters, 48 layers, hidden 5120, MLP 13824\nGPU: 1 x RTX 4090 24 GB (BF16 yes)\nTask: General SFT; dataset rows: 8000",
 "config": "import torch\nfrom datasets import load_dataset\nfrom peft import LoraConfig\nfrom transformers import AutoModelForCausalLM, AutoTokenizer\nfrom trl import SFTConfig, SFTTrainer\n\nmodel_id = \"Qwen/Qwen2.5-14B-Instruct\"\ntokenizer = AutoTokenizer.from_pretrained(model_id)\nmodel = AutoModelForCausalLM.from_pretrained(\n    model_id, torch_dtype=torch.bfloat16, attn_implementation=\"sdpa\"\n)\n\npeft_config = LoraConfig(\n    r=16,\n    lora_alpha=32,\n    lora_dropout=0.0,\n    bias=\"none\",\n    target_modules=\"all-linear\",\n    task_type=\"CAUSAL_LM\",\n)\n\nargs = SFTConfig(\n    output_dir=\"qwen14b-support-lora\",\n    max_length=4096,\n    per_device_train_batch_size=4,\n    gradient_accumulation_steps=4,\n    learning_rate=1e-4,\n    lr_scheduler_type=\"cosine\",\n    warmup_ratio=0.03,\n    num_train_epochs=2,\n    bf16=True,\n    gradient_checkpointing=True,\n    optim=\"adamw_torch\",\n    assistant_only_loss=True,\n)\n\ntrainer = SFTTrainer(\n    model=model,\n    args=args,\n    train_dataset=load_dataset(\"json\", data_files=\"support.jsonl\")[\"train\"],\n    processing_class=tokenizer,\n    peft_config=peft_config,\n)\ntrainer.train()\n",
 "facts": "Knobs as set (value - where - recipe):\n- method: LoRA (bf16 base) - recipe: LoRA unless memory forces QLoRA\n- base model: Qwen/Qwen2.5-14B-Instruct (line 7) - recipe: size class decides\n- r: 16 (line 14, LoraConfig()) - recipe: 16-32\n- lora_alpha: 32 (line 15, LoraConfig()) - recipe: 2 x r = 32\n- target_modules: all-linear (line 18, LoraConfig()) - recipe: all linear: q_proj, k_proj, v_proj, o_proj, gate_proj, up_proj, down_proj\n- lora_dropout: 0 (line 16, LoraConfig()) - recipe: 0\n- bias: none (line 17, LoraConfig()) - recipe: none\n- use_rslora: not set - recipe: on only at r >= 32, SFT at scale\n- learning_rate: 1e-4 (line 27, SFTConfig()) - recipe: 1e-4 (5e-5 very conservative)\n- per-device batch: 4 (line 25, SFTConfig()) - recipe: effective batch under 32\n- gradient_accumulation_steps: 4 (line 26, SFTConfig()) - recipe: effective batch under 32\n- effective batch: 4 x 4 x 1 = 16 - recipe: under 32\n- precision: bf16=True (line 31, SFTConfig()) - recipe: bf16=True, never fp16\n- optim: adamw_torch (line 33, SFTConfig()) - recipe: adamw_8bit\n- gradient checkpointing: True (line 32, SFTConfig()) - recipe: on when memory is tight\n- max length: max_length=4096 (line 24, SFTConfig()) - recipe: on SFTConfig as max_length\n- packing: not set - recipe: template before packing\n- epochs: 2 (line 30, SFTConfig()) - recipe: -\n- seed: not set - recipe: fixed\nComputed: effective batch 16, at most 65,536 tokens per optimizer step; adapter 68,812,800 trainable parameters (0.47% of 14.8B) on 7 modules x 48 layers at r 16; memory floor 29.5 GB per GPU of 24 GB (base 27.5 GB + adapter, gradients and optimizer 1.03 GB + 1 GB context), activations extra\nExpected verdict: rethink\nFindings (3):\n[info] seed_missing (seed): No seed is set; seed the run (SFTConfig(seed=...)) so it reproduces.\n[info] optim_32bit (optim): optim is adamw_torch; adamw_8bit cuts optimizer-state memory with negligible quality impact at adapter scale.\n[fix] wont_fit (memory): The floor alone is 29.5 GB against 24 GB per GPU (LoRA (bf16 base) of Qwen2.5 14B) before any activations - QLoRA is the recipe's answer when the base does not fit in bf16.",
 "review": "Verdict: rethink - A plain LoRA fine-tune of Qwen2.5 14B on a single RTX 4090; the adapter and trainer knobs are sound, but the base model's memory footprint alone exceeds this GPU's 24 GB.\n- [fix] memory: 29.5 GB floor on a 24 GB GPU (bf16 base, no quantization) -> QLoRA (4-bit NF4 base) (The computed memory floor exceeds the GPU's capacity before any activation memory, so the base does not fit in bf16 on this card.)\n- [check] seed: not set -> fixed (e.g. 3407) (No seed is set anywhere in the training configuration, so this run will not reproduce exactly between attempts.)\n- [check] optim: adamw_torch -> adamw_8bit (This optimizer keeps its state in full precision, using more memory than the 8-bit variant the recipe prefers.)\n- [ok] lora_alpha: 32 -> 32 (Already set to twice the rank, as the recipe always requires.)\n- [ok] target_modules: all-linear -> q_proj, k_proj, v_proj, o_proj, gate_proj, up_proj, down_proj (Already targets every linear projection, including the feed-forward layers that carry most of the capacity.)\n- [ok] learning_rate: 1e-4 -> 1e-4 (Already at the recipe's conservative rate for a plain bf16 LoRA adapter.)\n- [ok] effective batch: 16 -> under 32 (Four per-device times four accumulation steps on one GPU stays safely under the validated ceiling.)\n- [ok] bf16: True -> True (Precision is already forced to full bf16 with no half-precision fallback.)\n- [ok] max_length: 4096 (current argument name) -> max_length (current, correct name) (Already uses the current argument name on the trainer's configuration class rather than a renamed, removed one.)\n- risk: an out-of-memory crash at model load or on the first training step - The computed memory floor alone is above the GPU's full 24 GB before any activation memory is added.; Switch to QLoRA with a 4-bit NF4 base so the frozen weights fit in far less memory.\n- risk: a run that cannot be reproduced when repeated - No random seed is set anywhere in the training configuration.; Set a fixed seed value on the trainer's configuration.\n- risk: higher GPU memory use than necessary just for optimizer state - The optimizer keeps its state in full precision instead of the 8-bit variant.; Switch the optimizer to its 8-bit form."
}

Reply:

{
 "task": "rewrite",
 "framework": "peft-trl",
 "config": "import torch\nfrom datasets import load_dataset\nfrom peft import LoraConfig\nfrom transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig\nfrom trl import SFTConfig, SFTTrainer\n\nmodel_id = \"Qwen/Qwen2.5-14B-Instruct\"\ntokenizer = AutoTokenizer.from_pretrained(model_id)\n\nbnb_config = BitsAndBytesConfig(\n    load_in_4bit=True,\n    bnb_4bit_quant_type=\"nf4\",\n    bnb_4bit_compute_dtype=torch.bfloat16,\n)\nmodel = AutoModelForCausalLM.from_pretrained(\n    model_id, quantization_config=bnb_config, attn_implementation=\"sdpa\"\n)\n\npeft_config = LoraConfig(\n    r=16,\n    lora_alpha=32,\n    lora_dropout=0.0,\n    bias=\"none\",\n    target_modules=\"all-linear\",\n    task_type=\"CAUSAL_LM\",\n)\n\nargs = SFTConfig(\n    output_dir=\"qwen14b-support-lora\",\n    max_length=4096,\n    per_device_train_batch_size=4,\n    gradient_accumulation_steps=4,\n    learning_rate=2e-4,\n    lr_scheduler_type=\"cosine\",\n    warmup_ratio=0.03,\n    num_train_epochs=2,\n    bf16=True,\n    gradient_checkpointing=True,\n    optim=\"adamw_8bit\",\n    assistant_only_loss=True,\n    seed=3407,\n)\n\ntrainer = SFTTrainer(\n    model=model,\n    args=args,\n    train_dataset=load_dataset(\"json\", data_files=\"support.jsonl\")[\"train\"],\n    processing_class=tokenizer,\n    peft_config=peft_config,\n)\ntrainer.train()\n",
 "changes": [
  {
   "knob": "quantization_config",
   "from": "not set (bf16 base only)",
   "to": "BitsAndBytesConfig(load_in_4bit=True, bnb_4bit_quant_type=\"nf4\", bnb_4bit_compute_dtype=torch.bfloat16)",
   "why": "The bf16 memory floor already exceeds this GPU's 24 GB; quantize the frozen base to fit."
  },
  {
   "knob": "learning_rate",
   "from": "1e-4",
   "to": "2e-4",
   "why": "Quantizing the base moves this to the QLoRA row; its standard adapter rate is 2e-4."
  },
  {
   "knob": "optim",
   "from": "adamw_torch",
   "to": "adamw_8bit",
   "why": "The 8-bit optimizer cuts memory with little quality cost, useful now that the base is quantized."
  },
  {
   "knob": "seed",
   "from": "not set",
   "to": "3407",
   "why": "No seed was set anywhere; fixing one makes the run reproducible between attempts."
  }
 ],
 "unresolved": [
  "Confirm the new four-bit memory footprint actually fits the 24 GB GPU before a full run.",
  "Confirm support.jsonl is in a messages-shaped format, since assistant_only_loss expects it."
 ],
 "note": "Run a short sanity check after quantizing to four-bit to confirm memory fits and loss moves before the full two-epoch run."
}

The page's check: kept - nothing to flag. The rewrite rescans with 0 at fix (a review of it would say ready).

An Axolotl QLoRA YAML - review

An Axolotl config, QLoRA; the page finds 2 findings, 0 at fix, so a review should say ready.

Input:

{
 "task": "review",
 "target": "same",
 "setup": "Framework: Axolotl YAML\nMethod: QLoRA (4-bit NF4 base, bf16 adapters)\nBase model: Llama 3.1 8B (\"meta-llama/Llama-3.1-8B-Instruct\") - 8.03B parameters, 32 layers, hidden 4096, MLP 14336\nGPU: 1 x NVIDIA A100 40 GB (BF16 yes)\nTask: General SFT; dataset rows: 30000",
 "config": "base_model: meta-llama/Llama-3.1-8B-Instruct\nload_in_4bit: true\nadapter: qlora\nlora_r: 32\nlora_alpha: 64\nlora_dropout: 0.0\nlora_target_linear: true\n\ndatasets:\n  - path: ./data/tickets.jsonl\n    type: chat_template\nchat_template: llama3\nsequence_len: 2048\nsample_packing: true\n\nmicro_batch_size: 4\ngradient_accumulation_steps: 4\nnum_epochs: 3\nlearning_rate: 0.0002\noptimizer: adamw_bnb_8bit\nlr_scheduler: cosine\nwarmup_ratio: 0.05\n\nbf16: auto\ngradient_checkpointing: true\nseed: 3407\noutput_dir: ./outputs/llama31-8b-tickets-qlora\n",
 "facts": "Knobs as set (value - where - recipe):\n- method: QLoRA (4-bit NF4 base, bf16 adapters) - recipe: LoRA unless memory forces QLoRA\n- base model: meta-llama/Llama-3.1-8B-Instruct (line 1) - recipe: size class decides\n- r: 32 (line 4) - recipe: 16-32\n- lora_alpha: 64 (line 5) - recipe: 2 x r = 64\n- target_modules: all linear (lora_target_linear) (line 7) - recipe: all linear: q_proj, k_proj, v_proj, o_proj, gate_proj, up_proj, down_proj\n- lora_dropout: 0 (line 6) - recipe: 0\n- bias: not set - recipe: none\n- use_rslora: not set - recipe: on only at r >= 32, SFT at scale\n- learning_rate: 2e-4 (line 19) - recipe: 2e-4\n- per-device batch: 4 (line 16) - recipe: effective batch under 32\n- gradient_accumulation_steps: 4 (line 17) - recipe: effective batch under 32\n- effective batch: 4 x 4 x 1 = 16 - recipe: under 32\n- precision: bf16=auto (line 24) - recipe: bf16=True, never fp16\n- optim: adamw_bnb_8bit (line 20) - recipe: adamw_8bit\n- gradient checkpointing: True (line 25) - recipe: on when memory is tight\n- max length: sequence_len=2048 (line 13) - recipe: on SFTConfig as max_length\n- packing: True (line 14) - recipe: template before packing\n- epochs: 3 (line 18) - recipe: -\n- seed: 3407 (line 26) - recipe: fixed\nComputed: effective batch 16, at most 32,768 tokens per optimizer step; adapter 83,886,080 trainable parameters (1.04% of 8.03B) on 7 modules x 32 layers at r 32; memory floor 7.09 GB per GPU of 40 GB (base 5.31 GB + adapter, gradients and optimizer 0.78 GB + 1 GB context), activations extra\nExpected verdict: ready\nFindings (2):\n[info] packing_on (packing): Packing is on: apply the chat template before packing, and decode a handful of packed sequences before trusting the pipeline.\n[info] lora_would_fit (method): bf16 LoRA would fit too (floor about 16.7 GB of 40 GB); the recipe's default is LoRA, with QLoRA only when memory forces it.",
 "review": "none"
}

Reply:

{
 "task": "review",
 "verdict": "ready",
 "summary": "A QLoRA fine-tune of Llama 3.1 8B on a single A100 40GB; every adapter and trainer knob already matches the recipe, though a plain bf16 LoRA run would also fit.",
 "knobs": [
  {
   "knob": "method",
   "now": "QLoRA (4-bit NF4 base, bf16 adapters)",
   "recipe": "LoRA (bf16 base) also fits here (~16.7 GB floor)",
   "level": "check",
   "why": "The computed bf16 LoRA memory floor is about 16.7 GB on this 40 GB card, so quantizing the base was not required."
  },
  {
   "knob": "sample_packing",
   "now": "true",
   "recipe": "template applied before packing, then spot-check decoded sequences",
   "level": "ok",
   "why": "Packing is on and the dataset already applies the chat template beforehand; still worth spot-checking a few decoded sequences."
  },
  {
   "knob": "lora_r",
   "now": "32",
   "recipe": "16-32",
   "level": "ok",
   "why": "Rank sits at the top of the general fine-tuning range for this size of dataset."
  },
  {
   "knob": "lora_alpha",
   "now": "64",
   "recipe": "64",
   "level": "ok",
   "why": "Already set to twice the rank, as the recipe always requires."
  },
  {
   "knob": "learning_rate",
   "now": "0.0002",
   "recipe": "2e-4",
   "level": "ok",
   "why": "Matches the recipe's standard learning rate for a QLoRA run."
  },
  {
   "knob": "lora_target_linear",
   "now": "true",
   "recipe": "q_proj, k_proj, v_proj, o_proj, gate_proj, up_proj, down_proj",
   "level": "ok",
   "why": "Already targets every linear module rather than only the attention projections."
  },
  {
   "knob": "bf16",
   "now": "auto",
   "recipe": "True",
   "level": "ok",
   "why": "This card supports full precision, so automatic precision resolves to bf16 with no half-precision fallback."
  },
  {
   "knob": "optimizer",
   "now": "adamw_bnb_8bit",
   "recipe": "adamw_8bit",
   "level": "ok",
   "why": "Already an 8-bit AdamW variant, matching the recipe's memory-saving optimizer choice."
  },
  {
   "knob": "seed",
   "now": "3407",
   "recipe": "fixed (e.g. 3407)",
   "level": "ok",
   "why": "A fixed seed is already set, as the recipe expects for a reproducible run."
  },
  {
   "knob": "effective batch",
   "now": "16",
   "recipe": "under 32",
   "level": "ok",
   "why": "Four per-device times four accumulation steps on one GPU stays safely under the validated ceiling."
  }
 ],
 "risks": [
  {
   "looks_like": "slower throughput than necessary from quantizing when it wasn't required",
   "cause": "QLoRA was chosen even though a plain bf16 LoRA adapter would already fit in this GPU's memory.",
   "fix": "Try a bf16 LoRA run first, since the quantization overhead is unneeded here."
  },
  {
   "looks_like": "garbled or truncated examples hidden inside packed sequences",
   "cause": "Sample packing is enabled and has not yet been verified by decoding examples.",
   "fix": "Decode and inspect a few packed sequences before committing to a full run."
  }
 ],
 "keep": [
  "rank at the top of the general fine-tuning range",
  "alpha correctly twice the rank",
  "all linear projections targeted",
  "full bf16 precision with no fallback",
  "fixed random seed"
 ],
 "next": "Decode a handful of packed sequences to confirm the chat template applied correctly, and consider trying plain bf16 LoRA first since it would also fit."
}

The page's check: kept - nothing to flag

An Axolotl QLoRA YAML - rewrite

An Axolotl config, QLoRA; the page finds 2 findings, 0 at fix, so a review should say ready. This rewrite was handed the review above in its review field.

Input:

{
 "task": "rewrite",
 "target": "same",
 "setup": "Framework: Axolotl YAML\nMethod: QLoRA (4-bit NF4 base, bf16 adapters)\nBase model: Llama 3.1 8B (\"meta-llama/Llama-3.1-8B-Instruct\") - 8.03B parameters, 32 layers, hidden 4096, MLP 14336\nGPU: 1 x NVIDIA A100 40 GB (BF16 yes)\nTask: General SFT; dataset rows: 30000",
 "config": "base_model: meta-llama/Llama-3.1-8B-Instruct\nload_in_4bit: true\nadapter: qlora\nlora_r: 32\nlora_alpha: 64\nlora_dropout: 0.0\nlora_target_linear: true\n\ndatasets:\n  - path: ./data/tickets.jsonl\n    type: chat_template\nchat_template: llama3\nsequence_len: 2048\nsample_packing: true\n\nmicro_batch_size: 4\ngradient_accumulation_steps: 4\nnum_epochs: 3\nlearning_rate: 0.0002\noptimizer: adamw_bnb_8bit\nlr_scheduler: cosine\nwarmup_ratio: 0.05\n\nbf16: auto\ngradient_checkpointing: true\nseed: 3407\noutput_dir: ./outputs/llama31-8b-tickets-qlora\n",
 "facts": "Knobs as set (value - where - recipe):\n- method: QLoRA (4-bit NF4 base, bf16 adapters) - recipe: LoRA unless memory forces QLoRA\n- base model: meta-llama/Llama-3.1-8B-Instruct (line 1) - recipe: size class decides\n- r: 32 (line 4) - recipe: 16-32\n- lora_alpha: 64 (line 5) - recipe: 2 x r = 64\n- target_modules: all linear (lora_target_linear) (line 7) - recipe: all linear: q_proj, k_proj, v_proj, o_proj, gate_proj, up_proj, down_proj\n- lora_dropout: 0 (line 6) - recipe: 0\n- bias: not set - recipe: none\n- use_rslora: not set - recipe: on only at r >= 32, SFT at scale\n- learning_rate: 2e-4 (line 19) - recipe: 2e-4\n- per-device batch: 4 (line 16) - recipe: effective batch under 32\n- gradient_accumulation_steps: 4 (line 17) - recipe: effective batch under 32\n- effective batch: 4 x 4 x 1 = 16 - recipe: under 32\n- precision: bf16=auto (line 24) - recipe: bf16=True, never fp16\n- optim: adamw_bnb_8bit (line 20) - recipe: adamw_8bit\n- gradient checkpointing: True (line 25) - recipe: on when memory is tight\n- max length: sequence_len=2048 (line 13) - recipe: on SFTConfig as max_length\n- packing: True (line 14) - recipe: template before packing\n- epochs: 3 (line 18) - recipe: -\n- seed: 3407 (line 26) - recipe: fixed\nComputed: effective batch 16, at most 32,768 tokens per optimizer step; adapter 83,886,080 trainable parameters (1.04% of 8.03B) on 7 modules x 32 layers at r 32; memory floor 7.09 GB per GPU of 40 GB (base 5.31 GB + adapter, gradients and optimizer 0.78 GB + 1 GB context), activations extra\nExpected verdict: ready\nFindings (2):\n[info] packing_on (packing): Packing is on: apply the chat template before packing, and decode a handful of packed sequences before trusting the pipeline.\n[info] lora_would_fit (method): bf16 LoRA would fit too (floor about 16.7 GB of 40 GB); the recipe's default is LoRA, with QLoRA only when memory forces it.",
 "review": "Verdict: ready - A QLoRA fine-tune of Llama 3.1 8B on a single A100 40GB; every adapter and trainer knob already matches the recipe, though a plain bf16 LoRA run would also fit.\n- [check] method: QLoRA (4-bit NF4 base, bf16 adapters) -> LoRA (bf16 base) also fits here (~16.7 GB floor) (The computed bf16 LoRA memory floor is about 16.7 GB on this 40 GB card, so quantizing the base was not required.)\n- [ok] sample_packing: true -> template applied before packing, then spot-check decoded sequences (Packing is on and the dataset already applies the chat template beforehand; still worth spot-checking a few decoded sequences.)\n- [ok] lora_r: 32 -> 16-32 (Rank sits at the top of the general fine-tuning range for this size of dataset.)\n- [ok] lora_alpha: 64 -> 64 (Already set to twice the rank, as the recipe always requires.)\n- [ok] learning_rate: 0.0002 -> 2e-4 (Matches the recipe's standard learning rate for a QLoRA run.)\n- [ok] lora_target_linear: true -> q_proj, k_proj, v_proj, o_proj, gate_proj, up_proj, down_proj (Already targets every linear module rather than only the attention projections.)\n- [ok] bf16: auto -> True (This card supports full precision, so automatic precision resolves to bf16 with no half-precision fallback.)\n- [ok] optimizer: adamw_bnb_8bit -> adamw_8bit (Already an 8-bit AdamW variant, matching the recipe's memory-saving optimizer choice.)\n- [ok] seed: 3407 -> fixed (e.g. 3407) (A fixed seed is already set, as the recipe expects for a reproducible run.)\n- [ok] effective batch: 16 -> under 32 (Four per-device times four accumulation steps on one GPU stays safely under the validated ceiling.)\n- risk: slower throughput than necessary from quantizing when it wasn't required - QLoRA was chosen even though a plain bf16 LoRA adapter would already fit in this GPU's memory.; Try a bf16 LoRA run first, since the quantization overhead is unneeded here.\n- risk: garbled or truncated examples hidden inside packed sequences - Sample packing is enabled and has not yet been verified by decoding examples.; Decode and inspect a few packed sequences before committing to a full run."
}

Reply:

{
 "task": "rewrite",
 "framework": "axolotl",
 "config": "base_model: meta-llama/Llama-3.1-8B-Instruct\nload_in_4bit: true\nadapter: qlora\nlora_r: 32\nlora_alpha: 64\nlora_dropout: 0.0\nlora_target_linear: true\n\ndatasets:\n  - path: ./data/tickets.jsonl\n    type: chat_template\nchat_template: llama3\nsequence_len: 2048\nsample_packing: true\n\nmicro_batch_size: 4\ngradient_accumulation_steps: 4\nnum_epochs: 3\nlearning_rate: 0.0002\noptimizer: adamw_bnb_8bit\nlr_scheduler: cosine\nwarmup_ratio: 0.05\n\nbf16: auto\ngradient_checkpointing: true\nseed: 3407\noutput_dir: ./outputs/llama31-8b-tickets-qlora\n",
 "changes": [],
 "unresolved": [
  "Spot-check a few decoded packed sequences before trusting the pipeline, since the chat template is applied prior to packing.",
  "A plain bf16 LoRA run without quantization would also fit this GPU's 40 GB, if lower overhead matters more than the extra memory headroom."
 ],
 "note": "Nothing here needs changing; spot-check a handful of decoded packed sequences before launching the full three-epoch run."
}

The page's check: kept - nothing to flag. The rewrite rescans with 0 at fix (a review of it would say ready); remaining: info:packing_on, info:lora_would_fit.

The findings CSV for the first sample

level,code,knob,finding
fix,alpha_ratio,lora_alpha,lora_alpha is 16 with r 16 (ratio 1); the recipe sets lora_alpha = 2 x r = 32.
fix,targets_mlp,target_modules,"target_modules leaves out gate_proj, up_proj, down_proj - the MLP layers matter most. Target all linear modules: q_proj, k_proj, v_proj, o_proj, gate_proj, up_proj, down_proj."
fix,lr_low,learning_rate,"learning_rate is 2e-5, below the LoRA table's floor of 5e-5 - this looks like a full fine-tuning LR. LoRA runs about 10x the full-FT LR; 2e-4 is the QLoRA standard."
fix,batch_high,effective batch,"Effective batch is 8 x 8 x 1 GPU = 64; the recipe was validated under 32 - going higher is an untested extrapolation, not a free throughput win."
check,fp16_fallback,precision,"fp16 is set to the inverse of is_bfloat16_supported() (line 38, SFTConfig()), so a card without BF16 silently trains in fp16; the recipe gates on BF16 support and refuses fp16."
check,unsloth_dropout,lora_dropout,lora_dropout is 0.05; Unsloth's fused kernels assume 0 - a nonzero value forfeits the speedup.
check,unsloth_padding_free,padding_free,padding_free is not set to False; Unsloth's compiled trainer can raise a padding_free/max_length error even from a plain SFTConfig - pass padding_free=False explicitly.
fix,stale_tokenizer,tokenizer=,"SFTTrainer(tokenizer=...) (line 28, SFTTrainer()) is the old, removed form; current TRL takes processing_class=tokenizer."
fix,stale_max_seq_length,max length,"max_seq_length on SFTConfig (line 32, SFTConfig()) was renamed: current TRL takes max_length on SFTConfig."

Build the input yourself

Load the engine in node (global.window = global; require("./lorakit.js"); require("./lorareply.js")), then:

const K = LoraKit, R = LoraReply;
const form = K.form({ config: fs.readFileSync("train.py", "utf8"), model: "auto", gpu: "l4", gpus: "1", task: "sft", rows: "12000", target: "same" });
// form.fw: unsloth | peft-trl | axolotl | json | other     form.method: qlora | lora | lora8 | none
// form.knobs: {r, alpha, targets, lr, batch, accum, ...} - each {value, line, call}; form.effBatch, form.adapter, form.mem.floor (bytes)
// form.findings: [{code, level, knob, text}] - level is fix, check or info;  form.expect: ready | adjust | rethink
K.findingsCSV(form.findings)            // level, code, knob, finding
const built = K.buildInput(form, "review");      // built.ok is false, with built.note, until the paste has knobs to read
const wire = K.toWire(built.input);              // POST this to /estimate and /run
// then: const reply = R.normalize(R.parseReply(job.output.output), "review");
// R.check(reply, built)                         // {status: kept or flagged, notes}
// const rw = K.buildInput(form, "rewrite", R.digest(reply));   // hand the review to the rewrite
// R.check(R.normalize(R.parseReply(job2.output.output), "rewrite"), rw).f2   // the rewrite, scanned again

Setup keys: config (the paste), model (auto reads it from the config, a shape key such as llama-3.1-8b, or other), gpu (empty, or a card key such as l4, rtx4090, a100_80, spark), gpus, task (sft, scale or rl), rows (optional) and target (for a rewrite: same, unsloth, peft-trl or axolotl). Get a token for the steps below from /tokens.html.

Step 1 — a tiny client helper

One function that sends JSON and unwraps the response. Paste a personal token from /tokens.html, or leave it empty and mint a guest token in step 2.

BASE="https://api.skillsafe.ai/v1/app-api"
TOKEN="paste-your-token-here"   # from https://lora-desk.skillsafe.ai/tokens.html
api() { # api METHOD PATH [JSON-BODY]
  curl -s -X "$1" "$BASE$2" -H "Content-Type: application/json" \
    ${TOKEN:+-H "Authorization: Bearer $TOKEN"} ${3:+--data "$3"}
}

Step 2 — a token

A guest token is enough for /me and /estimate. A run is metered, so it needs a personal token: sign in on /tokens.html and copy it from there.

# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN=$(curl -s -X POST "$BASE/guest" -H "Content-Type: application/json" -d '{"slug":"lora-desk"}' | jq -r '.data.token')

Step 3 — GET /me

Who the token is (subject_type: user or guest) and the credit balance.

api GET /me | jq '.data | {subject_type, credits}' 

Step 4 — POST /estimate

Free. The body is the run input itself, the bare object (never wrapped in {"input": ...}). hold_credits is a reservation, not the price; min_credits is what the balance must cover; model_alias reads gpt-terra.

# input.json: the run input object - {"task": "review", "target": "same", "setup": "Framework: Unsloth + TRL\nMethod: QLoRA ...", "config": "from unsloth import ...", "facts": "Knobs as set ...", "review": "none"}
# - see the input table
api POST /estimate "$(cat input.json)" | jq '.data | {hold_credits, min_credits, model, model_alias, markup_bps, warnings}' 

Step 5 — POST /run, then poll the job

Send an Idempotency-Key built from the lane and the input: a retry with the same key never bills twice. The page uses lora-desk:<task>:<inputHash>:a<attempt> - the lane is part of the key, so a review and a rewrite of the same config never collide - and its one automatic reformat retry is a new attempt number, named in the page as one extra run. A review replies with verdict, summary and knobs; a rewrite with framework, config and changes; the code below prints either.

KEY="lora-desk:review:$(shasum -a 256 input.json | cut -c1-16):a1"
JOB=$(curl -s -X POST "$BASE/run" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
  -H "Idempotency-Key: $KEY" --data @input.json | jq -r '.data.job_id')
while :; do
  J=$(api GET "/jobs/$JOB"); S=$(echo "$J" | jq -r '.data.status')
  case "$S" in succeeded|failed|cancelled) break;; esac; sleep 2
done
echo "$J" | jq -r '.data.output.output' > reply.json   # one JSON object, the reply contract
echo "$J" | jq '.data | {charged_credits, truncated}'
jq -r '.verdict // .framework, .summary // .note, ((.knobs // .changes)[] | "\(.knob): \(.now // .from) -> \(.recipe // .to)")' reply.json
jq -r '.config // empty' reply.json > rewritten_config   # a rewrite: the whole corrected config

Step 6 — POST /run-stream

The same run as Server-Sent Events, with the same input.json. Browsers receive progress ticks rather than text deltas; the final done event carries the reply.

# Server-Sent Events: tick/delta events while the job runs, then a final event with the job
curl -N -s -X POST "$BASE/run-stream" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
  -H "Idempotency-Key: lora-desk:review:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json