Repository navigation
[TRTLLM-17273][chore] Read result completion through the public finished property in serve and scaffolding - #21389
JunyiXu-nv wants to merge 2 commits into
Conversation
…hed property in serve and scaffolding The serve post-processors and the scaffolding workers read the private GenerationResultBase._done flag (and the scaffolding workers also called the private _aresult_step()) on result objects owned by the executor package. Switch them to the public surface: - Move the read-only finished property from GenerationResult up to GenerationResultBase, next to the _done flag it mirrors. Every result object now exposes it, including the DetokenizedGenerationResultBase records the post-processing workers hand to the stream post-processors with num_postprocess_workers > 0, which previously had no finished. - Switch the five serve reads (chat, completion, harmony and Responses stream post-processors, and the Responses streaming processor) and the six scaffolding reads to .finished. - Switch the three scaffolding _aresult_step() calls to the public async iteration step anext(); at those call sites the request is never finished and has no terminal error, so __anext__ awaits _aresult_step() and returns the handle, making the two equivalent there. - Update the Mock/SimpleNamespace fixtures that set _done on doubles fed to those post-processors to set finished instead; a Mock that only set _done would expose an auto-created, always-truthy finished. - New tests: the stream post-processors emit the final usage chunk (or request the final Responses payload) only once the result is finished, on real GenerationResultBase and DetokenizedGenerationResultBase objects; the scaffolding streaming helpers drive a handle that implements only finished and __anext__. finished stays a read-only view of _done (no setter): the executor package writes _done directly, so a stored copy would go stale. ScaffoldingResult keeps its own _done and _aresult_step; it is scaffolding's own class, not a GenerationResultBase. No behavior change. Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
|
/bot run |
|
PR_Github #81840 [ run ] triggered by Bot. Commit: |
|
No actionable comments were generated in the recent review.
|
| Layer / File(s) | Summary |
|---|---|
Public completion property tensorrt_llm/executor/result.py, tests/unittest/llmapi/test_executor.py |
GenerationResultBase exposes a read-only finished property backed by _done. GenerationResult inherits the property. Tests check its value and read-only behavior. |
Scaffolding streaming tensorrt_llm/scaffolding/worker.py, tensorrt_llm/scaffolding/contrib/AsyncGeneration/stream_generation.py, tensorrt_llm/scaffolding/contrib/AsyncGeneration/__init__.py, tests/unittest/scaffolding/test_worker_streaming_step.py |
Scaffolding helpers use async iteration and finished to track completion and set end flags. CPU-only tests check completion and step limits. |
Serving completion checks tensorrt_llm/serve/postprocess_handlers.py, tensorrt_llm/serve/responses_utils.py, tests/unittest/serve/test_postprocess_finished.py, tests/unittest/llmapi/apps/*, tests/integration/test_lists/test-db/l0_cpu.yml |
Serving post-processors use finished to gate final usage and Responses output. Tests and fixtures use the public completion field. The CPU test list includes the new post-processing test. |
Estimated code review effort: 2 (Simple) | ~12 minutes
Change: Refactor
Suggested reviewers: juney-nvidia
Merge Risk: 
Minimal · up to 176d5
-
No actionable risk was identified in the reviewed change.
Pre-merge checks: 
4 · 
1
-
Failed checks: 1 warning Passed checks: 4 passedFull details: Docstring Coverage
-
Explanation
Docstring coverage is 20.83% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentation-only edits and deletions within surviving functions are included; unchanged diff context and fully deleted functions are excluded. Analyzed 24 functions across 14 files.
Resolution
Write docstrings for the functions missing them to satisfy the coverage threshold.
-
Finishing Touches
-
Generate unit tests (beta)
-
- Create a new PR
-
- Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts
|
PR_Github #81840 [ run ] completed with state
|
Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
|
/bot run --disable-multi-gpu-test |
|
PR_Github #81859 [ run ] triggered by Bot. Commit: |
|
PR_Github #81859 [ run ] completed with state |
Dev Engineer Review
GenerationResultBasenow provides the read-onlyfinishedproperty. Serve post-processors and scaffolding workers use it instead of_done. Scaffolding also uses async iteration instead of_aresult_step(). The reported intent is no behavior change.The change aligns cross-package code with the public result API. No current review findings or severity counts were supplied. GPU coverage remains for CI.
QA Engineer Review
Modified unit tests cover property behavior and immutability, stream finalization, usage-only chunks, Responses final output, and scaffolding iteration and end flags. The CPU test list adds
unittest/serve/test_postprocess_finished.pytol0_cpu.yml. The available evidence does not establish manual-QA list coverage.The author reports passing results for focused test runs and API-stability tests. The author also reports one broader CPU run with a failure attributed to the runner missing
trtllm-render, and says the test passed when that script was onPATH. These results are author-reported, not independently verified here. GPU coverage remains for CI. Coverage verdict: needs follow-up until GPU CI coverage completes.Per-File QA Perspective
tensorrt_llm/executor/result.py—GenerationResultBase.finishedexposes the completion state as a read-only property. Verify consistent behavior forGenerationResultandDetokenizedGenerationResultBase.tensorrt_llm/scaffolding/contrib/AsyncGeneration/stream_generation.py— Async generation uses iterator advancement andfinishedfor completion. Verify completion and remaining-step behavior.tensorrt_llm/scaffolding/worker.py— Worker streaming uses async iteration andfinished. Verify output collection and end-flag behavior.tensorrt_llm/serve/postprocess_handlers.py— Streaming post-processors usefinishedto detect completion and gate final usage output. Verify chat, completion, Harmony, and Responses stream finalization.tensorrt_llm/serve/responses_utils.py— Responses streaming passesfinishedto the event generator. Verify final response emission at completion.tests/integration/test_lists/test-db/l0_cpu.yml— Addsunittest/serve/test_postprocess_finished.pyto the CPU test list.tests/unittest/llmapi/apps/test_harmony_parsing.py— Updates mocked completion state to usefinished.tests/unittest/llmapi/apps/test_responses_stream_final_consistency.py— Updates the generation-chunk fixture to usefinished.tests/unittest/llmapi/apps/test_responses_streaming_events.py— Updates mocked completion state and completion checks.tests/unittest/llmapi/apps/test_responses_usage.py— Updates the synthetic result to usefinished.tests/unittest/llmapi/apps/test_tool_parsers.py— Updates response fixtures to usefinished.tests/unittest/llmapi/test_executor.py— Tests thatfinishedbecomes true after a final response and rejects assignment.tests/unittest/scaffolding/test_worker_streaming_step.py— Adds CPU-only coverage for async iteration, step limits, and end-flag behavior.tests/unittest/serve/test_postprocess_finished.py— Adds CPU-only coverage for result-property behavior and stream finalization.l0_cpu.ymllists this test.Description
The serve stream post-processors and the scaffolding workers read the private
GenerationResultBase._doneflag on result objects owned bytensorrt_llm/executor/, and the scaffolding workers also called the private_aresult_step(). This PR switches them to the public surface of the result objects, so the upcoming relocation of this code (epic TRTLLM-17056, task TRTLLM-17273) does not carry private cross-package accesses into the new files.Part of the trtllm-serve refactor: cleanup and wrap-up (TRTLLM-17273). It must land before the Responses-package move, which relocates
ResponsesStreamingProcessor, the holder of one of these reads.Why the property moves first.
finishedwas defined only onGenerationResult. Withnum_postprocess_workers > 0, the post-processing workers callchat_stream_post_processor/completion_stream_post_processoronDetokenizedGenerationResultBaserecords (executor/postproc_worker.py,default_record_creator), which had_donebut nofinished. Moving the property up toGenerationResultBase— where_donelives — gives every result object the public view;RequestOutput.finishedis unchanged (it inherits the same property through its bases) and the api-stability references do not change.finishedstays a read-only view of_done(a property with no setter). The executor package writes_donedirectly on these objects (response handling, abort, engine-dead paths, a post-processor-hook terminate verdict, and several tests on real result objects), so a separately stored flag would go stale, and an accidentalresult.finished = ...write on a real result now raisesAttributeError.The 11 reads and 3 calls
serve/postprocess_handlers.pychat_stream_post_processorif include_usage and rsp._done:if include_usage and rsp.finished:serve/postprocess_handlers.pycompletion_stream_post_processorif include_usage and rsp._done:if include_usage and rsp.finished:serve/postprocess_handlers.pychat_harmony_streaming_post_processordone=rsp._done,done=rsp.finished,serve/postprocess_handlers.pyresponses_api_streaming_post_processorif rsp._done:if rsp.finished:serve/responses_utils.pyResponsesStreamingProcessor.process_single_outputfinished_generation=res._done,finished_generation=res.finished,scaffolding/worker.pystreaming_generate_helperwhile not generate_result._done:while not generate_result.finished:scaffolding/worker.pystreaming_generate_helperasyncio.create_task(generate_result._aresult_step())asyncio.create_task(anext(generate_result))scaffolding/worker.pystreaming_generate_helper... and not generate_result._done:... and not generate_result.finished:scaffolding/worker.pystream_generation_handlerif task.request_handle._done:if task.request_handle.finished:scaffolding/contrib/AsyncGeneration/stream_generation.pyawait task.request_handle._aresult_step()await anext(task.request_handle)scaffolding/contrib/AsyncGeneration/stream_generation.pyif task.request_handle._done:(twice)if task.request_handle.finished:scaffolding/contrib/AsyncGeneration/stream_generation.pywhile not task.request_handle._done:while not task.request_handle.finished:scaffolding/contrib/AsyncGeneration/stream_generation.pyasyncio.create_task(task.request_handle._aresult_step())asyncio.create_task(anext(task.request_handle))Out of scope, same name:
ScaffoldingResult._done/._aresult_step()(scaffolding/result.py) is scaffolding's own class, read throughself, and keeps its field;IterationResult._doneis another class; reads insidetensorrt_llm/executor/are the package's own field.The
anext()equivalence (for the scaffolding owners).GenerationResult.__anext__re-raises_terminal_errorif set, raisesStopAsyncIterationif_done, and otherwise awaitsself._aresult_step()and returnsself. At the three changed call sites the request is never finished when the call is made: two sit insidewhile not ...finishedloops with noawaitbetween the check and the call, and the third sits in aforloop that breaks as soon as the request finishes._terminal_erroris only ever set together with_done = True(executor/result.py), so at these sitesanext(handle)awaits_aresult_step()and discards the returned handle — exactly what the direct call did, including cancellation behavior underasyncio.create_task(...).cancel().Mock-fixture updates (and why).
Mock().finishedis an auto-created, always-truthyMock: a test double that only sets_donewould silently make every chunk look final after this change. EveryMock/SimpleNamespacedouble fed to the five serve sites now setsfinishedinstead of_done:tests/unittest/llmapi/apps/test_tool_parsers.py(two fixtures),test_harmony_parsing.py(two fixtures),test_responses_stream_final_consistency.py,test_responses_streaming_events.py(two fixtures plus the two test-internal reads of them), andtest_responses_usage.py. ASimpleNamespacedouble that forgetsfinishednow fails loudly withAttributeErrorinstead of passing with stale semantics.Behavior
No behavior change.
Moves
GenerationResult.finished→GenerationResultBase.finished(same body; the property additionally gained a one-line docstring). No file moves, no shims.CI bookkeeping
tensorrt_llm/executor/andtensorrt_llm/serve/responses_utils.py, which are already listed, so the multi-GPU stages run on this PR)tests/unittest/serve/test_postprocess_finished.pyregistered intests/integration/test_lists/test-db/l0_cpu.yml(afterunittest/scripts);tests/unittest/scaffolding/test_worker_streaming_step.pyis collected by the existingunittest/scaffoldingentry inl0_cpu.ymland carriespytest.mark.cpu_onlyTest Coverage
New tests (both
pytest.mark.cpu_only):tests/unittest/serve/test_postprocess_finished.py— on a realGenerationResultBaseand a realDetokenizedGenerationResultBase(the postproc-worker record type; before this PR the latter had nofinished):finishedmirrors the completion flag and rejects writes;chat_stream_post_processorandcompletion_stream_post_processorwithstream_options.include_usage=trueemit the final usage chunk only once the request is finished;responses_api_streaming_post_processorrequests the final Responses payload from its streaming processor only once finished.tests/unittest/scaffolding/test_worker_streaming_step.py—TRTLLMWorker.streaming_generate_helper,TRTLLMWorker.stream_generation_handlerand the AsyncGenerationstream_generation_handleron a fake handle that implements onlyfinishedand__anext__(no_done, no_aresult_step): steps until finished, queues one output per non-final step, stops early at the step budget, and setstask.end_flagexactly when the handle finishes.Commands run locally (CPU-only dev container, no GPU; compiled artifacts from a source build of
mainatf6c4125c79, Python code under test from this branch —tensorrt_llm.__file__printed the branch worktree):pytest -m cpu_only tests/unittest/serve/test_postprocess_finished.py tests/unittest/scaffolding -q→ 25 passedpytest tests/unittest/llmapi/test_executor.py::test_GenerationResultBase -q→ 1 passedLLM_MODELS_ROOT=... pytest -m cpu_only tests/unittest/llmapi/test_executor.py tests/unittest/llmapi/apps/test_tool_parsers.py -q→ 989 passedpytest tests/unittest/executor/test_spec_decode_stats_payload.py -q→ 22 passed (drives the chat, completion and Responses stream post-processors on real result objects)pytest tests/unittest/api_stability -q→ 64 passed (RequestOutput still exposes the committedfinished; no reference changed)LLM_MODELS_ROOT=... pytest -rs tests/unittest/llmapi/apps/test_harmony_parsing.py tests/unittest/llmapi/apps/test_harmony_channel_validation.py→ 79 passed, zero "not importable" skip linesLLM_MODELS_ROOT=... pytest -m cpu_only tests/unittest/llmapi/apps tests/unittest/disaggregated -q→ 3438 passed, 1 skipped, 2 xfailed; the one failure (test_render_standalone.py::TestCpuOnlyStartup) was the runner's own PATH missing thetrtllm-renderconsole script and passes (3 passed) with it on PATH — unrelated to this changemain,GenerationResultBaseandDetokenizedGenerationResultBasehave nofinishedattribute, so the new tests cannot pass without the property movegit grep -nE "\._done\b|_aresult_step\(" -- tensorrt_llm/serve tensorrt_llm/scaffolding→ onlyscaffolding/result.py(its own field and method)An independent review additionally ran a four-mutation gate against the new tests — adding a setter to
finished, forcing each of the three changed completion guards always-on, and reverting the scaffolding worker to the private calls — and every mutant was caught red.GPU coverage is left to CI: the streaming chat/completion examples with usage, the Responses and harmony streaming end-to-end tests, and both post-processing modes (
test_openai_chat_example,test_openai_completions_example,test_openai_responses,test_openai_chat_harmony).CI follow-up in
176d5870ca: added the missingAsyncGeneration/__init__.pyso setuptools includes the streaming helper in the installed package. Package discovery omitted it before the fix. All 17 CPU scaffolding tests pass with the exact PR scaffolding loaded frombuild_pyoutput against a compatible native runtime, including the five streaming-helper regressions. The run asserts that both the worker and AsyncGeneration handler were imported from the built package. Focused and commit hooks pass.PR Checklist
Please review the following before submitting your PR:
PR description clearly explains what and why. If using CodeRabbit's summary, please make sure it makes sense.
PR Follows TRT-LLM CODING GUIDELINES to the best of your knowledge.
Test cases are provided for new code paths (see test instructions)
If PR introduces API changes, an appropriate PR label is added - either
api-compatibleorapi-breaking. Forapi-breaking, includeBREAKINGin the PR title.Any new dependencies have been scanned for license and vulnerabilities
CODEOWNERS updated if ownership changes
Documentation updated as needed
Update tava architecture diagram if there is a significant design change in PR.
The reviewers assigned automatically/manually are appropriate for the PR.
Please check this after reviewing the above items as appropriate for this PR.
🤖 Generated with Claude Code