Concepts / Text Generation with a Trained GPT Model

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.

  • Programming

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?

  • Inside the loop, once for each entry

  • After the loop finishes, once for the whole dataset

Reveal answer
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.

Test entryinstruction, input, outputBuild promptformat_inputToken IDstext_to_token_ids, moved todevicegeneratemax 256 new tokens, stop atID 50256Decodetoken_ids_to_textCleanslice, remove marker, stripStore model_responsewritten into the same entryMore entries?next index iWrite fileafter the loop endsreads fieldsprompt textinput IDsprompt plus continuationdecoded stringresponse textentry updatedyes, repeat bodyno
What happens to a test entry, step by step, from prompt construction through generate, decode, and cleanup, before the loop moves to the next entry?

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.

Echoed promptinstruction and input textAnswer texttrimmed with stripResponse marker### Response:Answer textmodel's continuationkept
Within the decoded text, which portion is the echoed prompt that gets removed, and which portion is the answer that is kept?

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.

python
Notes on the code
  • enumerate supplies both the index i and the entry on each iteration
  • The index selects the exact dictionary inside test_data to change
  • Assigning a new key modifies that dictionary in place, so no copy is returned
instructionName a primary colorinstructionName a primary colorinputempty stringinputempty stringoutputreference answeroutputreference answermodel_responsecleaned model answer
What does a single test entry look like before and after the model's cleaned response is added to it?

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.

instruction-data-with-response.jsonlist of entriesEntryone per test iteminstructioninputoutputmodel_responsecontainskeykeykeykey
How are the test entries and their new response fields nested inside the saved JSON file?

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.

Worked example

Verifying a single stored entry

A test run has finished. How do you confirm that one entry received its model response correctly?

  1. 1
    Pick an entry

    Select one dictionary from the updated test set, such as the entry at index 0.

  2. 2
    Check the original keys

    Confirm that instruction, input, and output are still present with their original values.

  3. 3
    Check the new key

    Confirm that model_response exists and holds a string containing only the model's answer, with no echoed prompt and no '### Response:' marker.

  4. 4
    Compare the four together

    All four keys appearing in the same dictionary shows the response was attached to the entry it was generated from.

Result

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 fails

    The file is written once, after the loop, so that the complete dataset with every model_response filled in is serialized in one operation.

    Do this instead

    Keep the json.dump call after the loop body finishes.

  • Common mistake

    Keeping the echoed prompt in the stored response

    What it looks like

    A stored response that begins with the instruction text again.

    Why it fails

    The decoded output begins with the prompt, so the answer is only the part after the prompt-length slice.

    Do this instead

    Apply 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 fails

    The response is written directly into the dictionary it came from, so test_data already contains the change.

    Do this instead

    Use test_data after the loop; no separate collection of responses is needed.

  • Common mistake

    Running again without renaming the previous output

    Why it fails

    Opening the file in 'w' mode overwrites the earlier instruction-data-with-response.json.

    Do this instead

    Rename 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

MEDIUM

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_response inside 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.json using json.dump with 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.

Related concepts

More concepts starting with T