When the Fine-tune Misbehaves

By Syncfusion®, Inc. · Last updated

Debug a fine-tune from its outputs: template tokens, run-on answers, cut-offs, prose and wrong values, each traced to a cause and a fix.

When the Fine-tune Misbehaves

The fine-tune passes the ship rule: 96 of 100 eval tickets exactly right. The team is about to move on, when someone asks: "What went wrong with the other 4?"

They read them. Each one is a different bug.

None of these show up in a loss curve. You find them by reading outputs, sorting them by failure type, and tracing each type to its cause.

Simple lesson: the failures are a to-do list. Read every one while there are few.

A Real-World Example

Think about a car that passes its inspection but makes a strange noise on the way home.

The inspection measured what it measures. The noise is a clue to something it did not check. A good mechanic does not ignore it because the paperwork says "pass".

Simple lesson: a passing score with odd failures still deserves a look.

What You Will Learn

In this lesson, you will learn:

  1. The common output failures of fine-tuned models, and what each one looks like.
  2. How to sort failures by type instead of counting pass and fail.
  3. How to trace each type to a cause: data, training format, or serving.
  4. How to write an output check that catches malformed answers.
  5. What to do when the loss looks fine but the outputs do not.

Before you start

  • The predictions here are the starter kit's fixtures: base model and fine-tune on the same 100 eval tickets, with planted failures. score.py prints the same failure types.
  • The Chat Templates lesson showed the training-side causes. This lesson starts from the outputs.

TL;DR

Sort failures by type, then fix the cause:

  • Template tokens in the output: serving template or stop token.
  • Never stops: no end token in training.
  • Extra text after the JSON: a few training answers had it, or a chat habit came back.
  • Cut off: max_new_tokens or max_seq_length too small.
  • Prose instead of JSON: is the adapter really loaded?
  • Wrong values: data or labels. Read 20 and fix the data.

Count by Type, Not Just Pass and Fail

The base model fails 24 of 100, mostly by answering in prose (11 cases). The fine-tune fails 4 of 100, each in a different way.

The fine-tune's four are more interesting than the base model's 24. Each points to a bug that will grow with more traffic.

Check the Shape First

Before checking whether a ticket's values are right, check that the output is exactly one JSON object and nothing else.

A first check on every output

// A first check on every model output: exactly one JSON object, nothing else.
function problem(raw: string): string {
  const text = raw.trim();
  if (/<\|[a-z_]+\|>/.test(text)) return "template tokens";
  if (text.startsWith("```")) return "code fence";
  if (!text.startsWith("{")) return "prose";
  const end = text.lastIndexOf("}");
  try { JSON.parse(text.slice(0, text.indexOf("}") + 1)); } catch { return "invalid or cut off"; }
  return end === text.length - 1 && text.indexOf("}") === end ? "" : "text after the JSON";
}

const outputs = [
  '{"intent":"cancel","order":"2518"}',
  '{"intent":"cancel","order":"2518"}<|im_end|>',
  'Sure! I have cancelled it.',
  '{"intent":"cancel","ord',
  '{"intent":"cancel","order":"2518"}\nAnything else?',
  '```json\n{"intent":"cancel"}\n```',
];
for (const o of outputs) console.log(`${(problem(o) || "ok").padEnd(20)} ${JSON.stringify(o).slice(0, 44)}`);

A first check on every output

using System.Text.Json;
using System.Text.RegularExpressions;

// A first check on every model output: exactly one JSON object, nothing else.
string Problem(string raw)
{
    var text = raw.Trim();
    if (Regex.IsMatch(text, @"<\|[a-z_]+\|>")) return "template tokens";
    if (text.StartsWith("```")) return "code fence";
    if (!text.StartsWith("{")) return "prose";
    var end = text.LastIndexOf('}');
    try { JsonDocument.Parse(text.Substring(0, text.IndexOf('}') + 1)); } catch { return "invalid or cut off"; }
    return end == text.Length - 1 && text.IndexOf('}') == end ? "" : "text after the JSON";
}

var jsonOptions = new JsonSerializerOptions { Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping };
string[] outputs = [
    "{\"intent\":\"cancel\",\"order\":\"2518\"}",
    "{\"intent\":\"cancel\",\"order\":\"2518\"}<|im_end|>",
    "Sure! I have cancelled it.",
    "{\"intent\":\"cancel\",\"ord",
    "{\"intent\":\"cancel\",\"order\":\"2518\"}\nAnything else?",
    "```json\n{\"intent\":\"cancel\"}\n```",
];
foreach (var o in outputs)
{
    var p = Problem(o);
    var json = JsonSerializer.Serialize(o, jsonOptions);
    Console.WriteLine($"{(p == "" ? "ok" : p).PadRight(20)} {json.Substring(0, Math.Min(44, json.Length))}");
}

Expected output:

ok                   "{\"intent\":\"cancel\",\"order\":\"2518\"}"
template tokens      "{\"intent\":\"cancel\",\"order\":\"2518\"}<
prose                "Sure! I have cancelled it."
invalid or cut off   "{\"intent\":\"cancel\",\"ord"
text after the JSON  "{\"intent\":\"cancel\",\"order\":\"2518\"}\
code fence           "```json\n{\"intent\":\"cancel\"}\n```"

This check uses the first closing brace, which is fine for the flat ticket JSON. score.py in the kit parses properly, so nested objects work too.

Spot Run-on Answers

A model that never stops often repeats itself. A cheap check: does any short piece of text appear three or more times?

Detect repetition

// Spot a run-on answer: does any 8-character piece repeat 3+ times?
function repeats(text: string, size = 8): number {
  const seen = new Map<string, number>();
  for (let i = 0; i + size <= text.length; i++) {
    const piece = text.slice(i, i + size);
    seen.set(piece, (seen.get(piece) ?? 0) + 1);
  }
  return Math.max(0, ...seen.values());
}

const answers = [
  '{"intent":"cancel","order":"2518","priority":"high"}',
  '{"intent":"refund"}"}"}"}"}"}"}"}"}"}"}"}"}',
  '{"intent":"where"}{"intent":"where"}{"intent":"where"}',
];
for (const a of answers) console.log(`max repeats ${repeats(a)}  ${repeats(a) >= 3 ? "RUN-ON" : "ok"}`);

Detect repetition

// Spot a run-on answer: does any 8-character piece repeat 3+ times?
int Repeats(string text, int size = 8)
{
    var seen = new Dictionary<string, int>();
    for (var i = 0; i + size <= text.Length; i++)
    {
        var piece = text.Substring(i, size);
        seen[piece] = seen.GetValueOrDefault(piece) + 1;
    }
    return seen.Values.Prepend(0).Max();
}

string[] answers = [
    "{\"intent\":\"cancel\",\"order\":\"2518\",\"priority\":\"high\"}",
    "{\"intent\":\"refund\"}\"}\"}\"}\"}\"}\"}\"}\"}\"}\"}\"}\"}",
    "{\"intent\":\"where\"}{\"intent\":\"where\"}{\"intent\":\"where\"}",
];
foreach (var a in answers) Console.WriteLine($"max repeats {Repeats(a)}  {(Repeats(a) >= 3 ? "RUN-ON" : "ok")}");

Expected output:

max repeats 1  ok
max repeats 10  RUN-ON
max repeats 3  RUN-ON

From Failure to Cause

You seeLook first atFix
Template tokens (such as im_end or [INST]) in the answerThe serving code that builds the promptBuild the prompt with the chat template and stop on the end-of-turn token
JSON, then more JSON or junkThe training answers' last tokenAdd the end-of-turn token; check with check_template.py; retrain
JSON, then "Let me know..."Training answers with a friendly sentenceFind and fix those rows; add a stop sequence
JSON cut in the middlemax_new_tokens, then max_seq_lengthRaise the limit that is too small
A chatty sentence, no JSONWhether the adapter is loaded at allLog the adapter version with every answer
Right shape, wrong valueThe labels in trainingRead 20 cases; fix labels (Data Quality lesson)

Low loss plus broken outputs means formatting or serving.

If the curves are healthy but the answers are malformed, do not train longer. The model learned what you gave it. Look at the template, the end token, the length limits, and the code that serves it.

What to ask when a fine-tune misbehaves in production

  • What does the failure look like? Ask for three raw outputs, not a summary.
  • Which versions (base, adapter, dataset, serving code) produced them?
  • Did it pass the eval? If yes, add these cases to the eval so it cannot pass again.
  • Is it safe to leave on? If not, roll back first.

Do's and Don'ts

  • Do count failures by type.
  • Do read every failure when there are only a few.
  • Do add every production failure to the eval set.
  • Do log the adapter version with every answer.
  • Don't train longer to fix a formatting bug.
  • Don't hide failures with a lenient parser without fixing the cause.
  • Don't call a 96% pass "done" before reading the other 4%.

Common mistakes

MistakeWrong ideaCorrect ideaWhat goes wrong
Only pass/fail counts"96% passed, good enough."Sort the 4% by type.A serving bug ships and grows with traffic.
Lenient parsing"Strip the junk after the JSON."Fix why the junk is there.The real bug stays, and gets worse on new inputs.
Training longer"More epochs will fix it."Check template, end token, limits.Hours of GPU time for the same bug.
No version logging"It must be the new adapter."Log base, adapter, dataset, code versions.You debug the wrong model.
Failures not added to the eval"We fixed it this time."Every failure becomes an eval case.The same bug comes back next release.

Check Your Understanding

Exercises

What to Remember

  • Read the failures, sorted by type, especially when there are few.
  • Template tokens, run-ons and cut-offs are formatting or serving bugs, not training bugs.
  • Check the shape of every output before checking its values.
  • Low loss + broken outputs: check template, end token, limits and serving code.
  • Every production failure becomes an eval case.

Next: Evaluate Before and After, how to prove a fine-tune helped, and catch what it broke.