Text Generation with a Trained GPT Model
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 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 then confirms that everything landed where it should. The goal of a single pass is simple to state: start with an entry that holds an instruction, an input, and a reference output, and finish with a model answer attached to that same entry.
What do you think happens?
When is the output file written: inside the loop for each entry, or after the loop finishes?
Reveal answer
After the loop finishes, once for the whole dataset.
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.
Tracing One Entry Through a Pass
The loop reuses the same generate method that was used earlier in the chapter. The difference is that it now runs once for each entry in the test set instead of once for a single example. The loop is driven by enumerate over test_data, so each 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 first step is to build a prompt from that entry using format_input. The 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 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 output of generate is a sequence of token IDs that contains the prompt followed by the newly generated continuation.
The sequence shows the order of operations for one entry, and the decision at the end repeats the body until every entry is finished. Each iteration does the same steps in the same order. What changes from one iteration to the next is only the entry being processed.
Cleaning the Decoded Output
The code converts the whole sequence of token IDs back into text with token_ids_to_text. Because the sequence began with the prompt, the decoded string still starts with the prompt. The answer is the part that follows it. To isolate the answer, the code does three things: it slices off the first part of the string whose length equals the length of the prompt text, it removes the marker '### Response:' wherever it appears, and it calls strip to trim surrounding whitespace. What remains is the response text alone.
Storing Each Response in Place
The response is not returned from the loop or collected in a separate list. The code writes it directly into the entry it came from, using the index i to pick the right dictionary inside test_data. Because the dictionary is modified in place, the change is visible everywhere test_data is used afterward, including the file-writing step. This is why the response stays paired with its original instruction, input, and reference output.
- enumerate supplies both the index i and the entry on each iteration
- The index selects the exact dictionary inside
test_datato change - Assigning a new key modifies that dictionary in place, so no copy is returned
The three original fields are not replaced. The new key model_response is added beside them, so the same dictionary now carries the question, the reference, and the model's answer together.
What the Saved JSON Holds
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 with an indent of 4 spaces. The indentation makes the saved structure easy to read. Each entry in the file keeps its instruction, input, output, and the new model_response field, so the file is a complete record of the test run rather than a bare list of answers.
Checking One Entry After the Run
Once the run has finished, it is worth inspecting one entry directly rather than trusting that the loop worked. The check looks at a single entry from the updated test set and confirms that four keys are present together: the original instruction, the input, the reference output, and a model_response field whose value is the model's answer. Seeing all four confirms that the response went into the correct dictionary and that the original fields were not disturbed.
Verifying a single stored entry
A test run has finished. How do you confirm that one entry received its model response correctly?
- 1Pick an entry
Select one dictionary from the updated test set, such as the entry at index 0.
- 2Check the original keys
Confirm that instruction, input, and output are still present with their original values.
- 3Check the new key
Confirm that
model_responseexists and holds a string containing only the model's answer, with no echoed prompt and no '### Response:' marker. - 4Compare the four together
All four keys appearing in the same dictionary shows the response was attached to the entry it was generated from.
The entry holds its original instruction, input, and reference output, plus a model_response field. The response is paired with the item it answers, so the check passes.
Run Time and Hardware
The generation loop calls the model once per entry, so the total run time depends heavily on the hardware that executes those calls. The source reports the run time for the 110-entry test set on two machines. The progress bar in the run output counts entries as they finish and reports the average speed in iterations per second.
The same code runs on both machines, so the difference comes from the hardware rather than from any change in the logic. When you plan a test run, the number of entries and the speed of the device together determine how long you will wait.
Common Mistakes When Storing Responses
Common mistake Writing the file inside the loop
Why it failsThe file is written once, after the loop, so that the complete dataset with every
model_responsefilled in is serialized in one operation.Do this insteadKeep the json.dump call after the loop body finishes.
Common mistake Keeping the echoed prompt in the stored response
What it looks likeA stored response that begins with the instruction text again.
Why it failsThe decoded output begins with the prompt, so the answer is only the part after the prompt-length slice.
Do this insteadApply the prompt-length slice, remove the '### Response:' marker, and strip whitespace before storing.
Common mistake Expecting a new list to be returned from the loop
Why it failsThe response is written directly into the dictionary it came from, so
test_dataalready contains the change.Do this insteadUse test_data after the loop; no separate collection of responses is needed.
Common mistake Running again without renaming the previous output
Why it failsOpening the file in 'w' mode overwrites the earlier
instruction-data-with-response.json.Do this insteadRename or copy the old file before starting a new run.
Common mistake Assuming a quick run on one machine will be quick on another, even though the code is identical
Practice: Trace a Stored Response
A single test entry has the instruction 'Name a fruit', an empty input, and the reference output 'Apple'. The decoded generation output reads: 'Name a fruit ### Response: Banana'. Trace what the loop stores in model_response. Explain which parts are removed, and state what the entry looks like after the response is stored.
Hints
- Start from the decoded string and ask which portion is the echoed prompt.
- The slice removes text whose length equals the prompt text, then the marker is removed, then whitespace is trimmed.
- Check whether the original instruction, input, and output keys are still present after the change.
Key Takeaways
- The generation loop visits each test entry in order, builds a prompt with
format_input, calls generate, decodes the output, and cleans it so only the model's answer remains. - Cleaning slices off the echoed prompt by its length, removes the '### Response:' marker, and strips whitespace before the answer is stored.
- Each response is written in place as
model_responseinside the same dictionary as its instruction, input, and output, so it stays paired with the item it answers. - The full dataset is written once after the loop to
instruction-data-with-response.jsonusingjson.dumpwith indent 4, and opening the file in 'w' mode overwrites earlier results. - Checking one entry after the run confirms that all four keys are present together and that the original fields were not disturbed.