Skip to content

fix(trtllm): report response truncation so overlong filtering can see it - #4042

Open
yupengtang wants to merge 2 commits into
NVIDIA-NeMo:mainfrom
yupengtang:fix/trtllm-report-truncated
Open

yupengtang wants to merge 2 commits into
NVIDIA-NeMo:mainfrom
yupengtang:fix/trtllm-report-truncated

Conversation

@yupengtang

@yupengtang yupengtang commented Sep 7, 2026

Copy link
Copy Markdown

Stacked on #4041. Both touch trtllm_worker_async.py and the same test, so this branch is rebased on that one and its commit shows up here too. Only the second commit, fix(trtllm): report response truncation ..., belongs to this PR. Happy to rebase onto main once #4041 lands, or to fold the two together if you would rather review them as one.

What does this PR do ?

Makes TRT-LLM report response truncation, so grpo.overlong_filtering stops silently doing nothing on that backend.

truncated marks a sample whose response hit max_new_tokens without a stop token. vLLM, SGLang and Dynamo all return it in GenerationOutputSpec; TRT-LLM returned no such column.

That is not simply a missing field. run_multi_turn_rollout starts from an all-False tensor and only overwrites it when the backend supplied one:

sample_truncated = torch.zeros(batch_size, dtype=torch.bool)      # :997
...
response_truncated = gen_metrics.pop("_response_truncated", None)
if response_truncated is not None:                                # :1055, skipped on TRT-LLM
    ...
current_batch["truncated"] = sample_truncated                     # :1169, always written

So the column exists and reads all-False on TRT-LLM. grpo.py:3374 then does:

if use_overlong_filtering:
    loss_multiplier[truncated] = 0

which zeroes nothing. Samples that ran out of budget train as if they had finished on their own, the opposite of what the flag asks for, with no error and no warning. truncation_rate under-reports for the same reason.

Observation truncation (an env observation cut to fit the context, rollouts.py:1116) was never affected, since it is computed rollout-side. Only the response side was missing.

Issues

No issue filed for this. Distinct from #2163 / #2395, which are about truncated being absent and raising KeyError; here the column is present and simply always False.

Usage

No config or API change. overlong_filtering: true now behaves on TRT-LLM the way it already does on the other backends.

Design & Code Changes

TRT-LLM already reports this per output: finish_reason == "length" is its spelling for hitting the token cap without a stop token (executor/result.py: finish_reason: Optional[Literal['stop', 'length', 'timeout', ...]]). That is the same signal BaseVllmGenerationWorker reads, so this is a plumbing fix rather than new inference.

  • generate_async collects gen.finish_reason == "length" per output and returns it as truncated.
  • The empty-batch shortcut gets the matching torch.zeros(0, dtype=torch.bool), so both branches return the same columns.

Scoped to TRT-LLM. Megatron has the same gap but reports no finish reason, and its mcore path would need truncation inferred from termination_id against the token cap. That is a judgement call about mcore semantics rather than a port of an existing signal, so I left it out rather than guess; happy to follow up if you want it in the same shape.

Before your PR is "Ready for review"

Pre checks:

  • Make sure you read and followed Contributor guidelines
  • Did you write any new necessary tests?
  • Did you run the unit tests and functional tests locally? Visit our Testing Guide for how to run tests
  • Did you add or update any necessary documentation? Visit our Document Development Guide for how to write, build and test the docs.

Additional Information

Tests

Two added in test_trtllm_worker_async.py, plus the existing test_generate_async_converts_padded_batch_and_logprobs extended so its fake outputs now carry finish_reason, which the real TRT-LLM object always has, and it asserts the new column.

  • test_generate_async_reports_truncation_from_finish_reason: "stop" and "length" in one batch produce [False, True]
  • test_generate_async_empty_batch_returns_empty_truncated: the shortcut returns an empty bool tensor rather than omitting the column

Reverting only trtllm_worker_async.py to main, the first fails on the missing column rather than on a signature change:

E   KeyError: 'truncated'
FAILED test_generate_async_reports_truncation_from_finish_reason

With the change (both commits on this branch):

pytest tests/unit/models/generation/trtllm/test_trtllm_worker_async.py --trtllm-only
  main:        11 passed
  this branch: 16 passed          (3 from #4041, 2 from this PR)

pytest tests/unit/models/generation/trtllm/test_trtllm_http_server.py
  7 passed, 5 skipped

No failures on either version. The 5 skips are local: my stub tensorrt_llm has no tensorrt_llm.serve submodule.

CPU only. I have no GPU box, so the tests needing a real TRT-LLM runtime did not run and I could not watch a rollout actually get filtered. The change is confined to how the output batch is assembled, which the tests above cover.

ruff 0.9.9 check, import sort and format are clean on all four files.

`stop_strings` is part of the shared generation contract -- it is declared on
`GenerationConfig` and again per sample on `GenerationDatumSpec` -- and vLLM,
SGLang and Dynamo all read it. TRT-LLM built its `SamplingParams` with
`stop_token_ids` and never `stop`, on the direct path and over HTTP, so the
same config stopped generation on the other three backends and ran on to
`max_new_tokens` here.

The two controls are not interchangeable: `stop_token_ids` cannot express a
multi-token boundary like `</answer>`, which is exactly the shape a chat or
agentic rollout stops on. Nothing errors, so a run only shows it as rollouts
that overrun their boundary.

`TrtSamplingParams` already accepts `stop` alongside `stop_token_ids`, so both
paths now pass it. The direct path merges the configured list with the
per-sample ones the way `BaseVllmGenerationWorker._merge_stop_strings` does --
one `SamplingParams` is built per batch, so a sample's stop strings apply to
the batch, matching vLLM rather than inventing a different rule. An empty
result stays `None` so TRT-LLM keeps its own default instead of receiving an
empty list.

The HTTP helper gains the argument next to `stop_token_ids`, mirroring how
NVIDIA-NeMo#3537 threaded the sampling config through after the same drift was found in
`top_k`.

Signed-off-by: Yupeng Tang <85978465+yupengtang@users.noreply.github.com>
`truncated` is the column rollouts use to mark a sample whose response hit
`max_new_tokens` without a stop token. vLLM, SGLang and Dynamo all return it;
TRT-LLM returned no such column, and `run_multi_turn_rollout` starts from an
all-False tensor and only overwrites it when the backend supplied one. So on
TRT-LLM every sample looked untruncated.

That is not only a metric: `grpo.overlong_filtering` reads this column to zero
the loss multiplier for samples that ran out of budget. With the column stuck
at False those samples train as if they had finished on their own, which is the
opposite of what the flag asks for, and nothing reports it. `truncation_rate`
under-reports for the same reason.

TRT-LLM already says this on each output -- `finish_reason == "length"` is its
spelling for hitting the token cap without a stop token, the same signal
`BaseVllmGenerationWorker` reads. Fill the column from it, and give the
empty-batch shortcut the matching empty tensor so both branches return the same
shape.

Observation truncation, set in `rollouts.py` when an env observation is cut to
fit the context, was never affected -- only the response side was missing.

Scoped to TRT-LLM. Megatron has the same gap but reports no finish reason; its
mcore path would need truncation inferred from the termination id, which is a
judgement call worth making separately.

Signed-off-by: Yupeng Tang <85978465+yupengtang@users.noreply.github.com>
@yupengtang
yupengtang requested review from a team as code owners September 7, 2026 10:25
@copy-pr-bot

copy-pr-bot Bot commented Sep 7, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@svcnvidia-nemo-ci svcnvidia-nemo-ci added the waiting-on-maintainers Waiting on maintainers to respond label Sep 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

community-request waiting-on-maintainers Waiting on maintainers to respond

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants