Instruction Fine-Tuning Test Datasets
The generation loop visits each test entry, builds a prompt, calls generate, decodes the output, and cleans it so that only the model's answer remains.
Why Test Entries Need Model Answers
Once a model has been instruction-tuned, the next question is how well it follows instructions on examples it has not been trained on. A common approach is to take a held-out test set. Each entry in that set already contains an instruction, an optional input, and a reference output. The model is asked to answer every instruction, and the answers it produces are stored alongside the original entries so they can be compared later.
This article follows the practical loop that does that work. It generates a response for each entry, writes that response into the dataset, saves the result to a file, and confirms that the response landed where it should. The goal of a single pass is simple to state. The loop starts with an entry that holds an instruction, an input, and a reference output, and it must finish with a model answer attached to that same entry.
What do you think happens?
Before reading on: after the loop has produced a response for an entry, which parts of the decoded text should survive into the stored model_response field?
Reveal answer
Only the text the model generated after the prompt, with the response marker and surrounding whitespace removed
The decoded string begins with the prompt, so the code slices that part off, removes the marker '### Response:' wherever it appears, and trims whitespace. What remains is the answer alone.
Tracing One Entry Through the Loop
The loop reuses the same generate method that was used earlier in the chapter. The difference is scale: instead of generating for a single example, it runs once for each entry in the test set. The loop is driven by enumerate over test_data, so every iteration has both an index i and the entry itself. A tqdm progress bar wraps the loop, which is why the run reports how many of the 110 entries have been processed.
For one entry, the body of the loop always runs the same sequence of steps. It builds a prompt from the entry, converts that prompt into token IDs, generates a continuation, decodes the full output back into text, and cleans the text down to the answer. The cleaned answer is then written back into the entry. After that, a decision checks whether more entries remain. If they do, the body repeats for the next index. If not, the loop ends. The diagram below shows the order of operations for one entry and the decision that repeats the body.
Building the Prompt and Calling generate
The first two steps prepare the model's input. The code builds a prompt from the current entry using format_input. That prompt is then converted into token IDs with text_to_token_ids and moved to the target device. Those IDs are passed to generate, which is configured with three settings. It is told to produce at most 256 new tokens, to use the context length stored in BASE_CONFIG, and to stop when it reaches the end-of-sequence token whose ID is 50256.
The stopping condition has two parts. Generation can end because the model emits the end-of-sequence token with ID 50256, or because the 256-new-token limit is reached. Either way, the output is a sequence of token IDs, not yet text.
Decoding and Cleaning the Output
The output of generate is a sequence of token IDs that contains the prompt followed by the newly generated continuation. The code converts the whole sequence back into text with token_ids_to_text. Because the prompt was part of what was fed in, the decoded string still begins with the prompt. The answer is therefore buried at the end of the string, and cleaning is the step that isolates it.
Cleaning has three operations. First, the code slices off the first part of the string whose length equals the length of the prompt text. Second, it removes the marker '### Response:' wherever it appears. Third, it calls strip to trim surrounding whitespace. The result is the response text alone. The diagram below shows which part of the decoded string is discarded and which part is kept.
Cleaning one decoded string
Suppose the prompt text for an entry is 60 characters long, and the decoded output is the prompt followed by the model's answer. Trace how the cleaning steps reduce the decoded string to the stored response. This example is illustrative and was written for teaching.
- 1Start with the decoded string
The string begins with the full prompt text, then contains the '### Response:' marker and the newly generated answer.
- 2Slice off the prompt
Keep everything from position 60 onward. The first 60 characters, which match the prompt length, are removed.
- 3Remove the marker
Any occurrence of '### Response:' in the remaining text is deleted, so only the answer's words remain alongside stray whitespace.
- 4Strip whitespace
Leading and trailing spaces or newlines are trimmed, leaving the answer text in a clean form.
The cleaned string contains only the model's answer, and that string is what gets stored under model_response.
Storing the Response Back Into the Entry
The response is not returned from the loop or collected in a separate list. Instead, the code writes it directly into the entry it came from. It uses the index i to pick the correct dictionary inside test_data and stores the cleaned text under the key model_response. Because the dictionary is modified in place, the change is visible everywhere test_data is used afterward, including the file-writing step.
Storing the response in place is what keeps it paired with its original instruction, input, and reference output. The answer never travels separately from the entry it belongs to.
The Shape of the Saved JSON File
After the loop finishes, the updated test_data is written to a file named instruction-data-with-response.json. The file is opened in write mode 'w', and json.dump serializes the list into that file using an indent of 4 spaces. The indentation is there so the saved structure is easy for a person to read. The file holds a list, and each element of that list is one entry dictionary.
Each entry in the file keeps its instruction, input, and reference output, and it also carries the new model_response field. This makes the file a complete record of the test run rather than a bare list of answers. The diagram below shows how the fields nest relative to one another.
Why the Write Happens After the Loop
The file is not written inside the loop. The write step comes after the last entry has been processed, so the complete dataset, with every model_response filled in, is serialized in one operation. Writing once at the end means the file reflects the finished run. A partially filled file is never produced by the loop body itself.
Checking the Updated Dataset
Once the run has finished, it is worth inspecting one entry directly rather than trusting that the loop worked. The source examines a single entry from the updated test set. That entry contains the original instruction, the input, the reference output, and a model_response field whose value is the model's answer. Seeing all four keys together confirms two things: the response was added to the correct dictionary, and the original fields were not disturbed.
| Check | What to look for | What a problem looks like |
|---|---|---|
| Key presence | Every entry has a model_response key | An entry is missing model_response |
| Original fields | instruction, input, and output match the original test set | A field has changed or disappeared |
| Pairing | The response sits in the same dictionary as its instruction | A response appears under the wrong entry |
| Saved file | The file contains the same entries as the in-memory dataset | The file is from an earlier run |
Comparing the original test dataset with the updated one
Mistakes Beginners Make With the Test Loop
Common mistake Assuming the decoded string contains only the answer
Why it failsThe output of generate contains the prompt followed by the continuation, so the decoded text still begins with the prompt.
Do this insteadSlice off the first part of the string whose length equals the prompt length, then remove the marker and strip whitespace.
Common mistake Writing the file inside the loop
Why it failsThe file write is meant to happen once, after the last entry has been processed, so that the complete dataset is serialized in one operation.
Do this insteadPlace the json.dump call after the loop ends.
Common mistake Collecting responses in a separate list and forgetting to attach them
Why it failsThe loop stores each response directly in the entry it came from using the index i. A separate list would separate answers from their instructions.
Do this insteadAssign the cleaned response to test_data[i] under the key model_response.
Common mistake Running the test again without saving the previous file
Why it failsOpening the file in 'w' mode overwrites earlier output.
Do this insteadRename or copy the previous instruction-data-with-response.json before starting a new run.
Common mistake Expecting the run time to change with the logic rather than the hardware: the same code runs on both machines, so the speed difference comes from the device.
Practice Tracing a Single Entry
Take one entry with an instruction, an input, and a reference output. Write down, in order, the steps the loop performs on it: which function builds the prompt, what is passed to generate, how the decoded string is cleaned, and where the cleaned text is stored. Then say what the saved file would contain for that entry and at what point in the run the file is written.
Hints
- Start from the enumerate loop and the index i.
- Remember that the decoded string begins with the prompt, so cleaning must remove that part first.
- The file write happens once, after every entry has been processed.
Wrapping Up the Test Run
- 1Each pass through the loop builds a prompt from one entry, generates a continuation, decodes it, and cleans it down to the answer before storing it.
- 2Cleaning removes the prompt portion of the decoded string, removes the '### Response:' marker, and trims whitespace.
- 3The cleaned response is stored under
model_responsein the same dictionary as its instruction, input, and reference output. - 4The full dataset is written once, after the loop, to
instruction-data-with-response.jsonwith an indent of 4, and writing in 'w' mode overwrites earlier files. - 5Inspecting one updated entry confirms that the response was added to the right dictionary and that the original fields are intact.
Key Takeaways
- Each pass through the loop builds a prompt from one entry, generates a continuation, decodes it, and cleans it down to the answer before storing it.
- Cleaning removes the prompt portion of the decoded string, removes the '### Response:' marker, and trims whitespace.
- The cleaned response is stored under
model_responsein the same dictionary as its instruction, input, and reference output. - The full dataset is written once, after the loop, to
instruction-data-with-response.jsonwith an indent of 4, and writing in 'w' mode overwrites earlier files. - Inspecting one updated entry confirms that the response was added to the right dictionary and that the original fields are intact.