Skip to content

fix(trtllm): honor stop_strings on both TRT-LLM generation paths - #4041

Open
yupengtang wants to merge 1 commit into
NVIDIA-NeMo:mainfrom
yupengtang:fix/trtllm-honor-stop-strings
Open

fix(trtllm): honor stop_strings on both TRT-LLM generation paths#4041
yupengtang wants to merge 1 commit into
NVIDIA-NeMo:mainfrom
yupengtang:fix/trtllm-honor-stop-strings

Conversation

@yupengtang

@yupengtang yupengtang commented Sep 7, 2026

Copy link
Copy Markdown

What does this PR do ?

Makes the TRT-LLM backend honor stop_strings, which it silently dropped on both of its generation paths.

stop_strings is part of the shared generation contract: it is declared on GenerationConfig (interfaces.py:227) and again per sample on GenerationDatumSpec (:336). vLLM, SGLang and Dynamo all read it. TRT-LLM builds its SamplingParams with stop_token_ids and never stop, so the same config stops generation on the other three backends and runs 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 raises, so a run shows this only as rollouts that overrun the boundary the config asked for.

Both TRT-LLM paths are affected:

path before after
direct generate_async() stop_token_ids only stop from config ∪ per-sample
HTTP (NeMo-Gym rollouts) stop_token_ids only stop from config

Measured on the direct path with stop_strings: ["</answer>"] configured:

[before]  configured stop_strings : ['</answer>']
          SamplingParams got stop : <key absent>
          stop_token_ids          : [9]

[after]   configured stop_strings : ['</answer>']
          SamplingParams got stop : ['</answer>']
          stop_token_ids          : [9]

Issues

No issue filed; found while comparing which generation-config keys each backend reads. TRT-LLM was the only backend not reading stop_strings.

Usage

No config or API change. policy.generation.stop_strings simply takes effect on TRT-LLM now, as it already did elsewhere.

policy:
  generation:
    backend: trtllm
    stop_strings: ["</answer>"]   # previously ignored on this backend

Design & Code Changes

tensorrt_llm.SamplingParams already accepts stop alongside stop_token_ids (sampling_params.py: stop: Optional[Union[str, List[str]]] = None), so this is a plumbing fix rather than a new capability.

  • trtllm_worker_async.py: adds _merge_stop_strings, mirroring BaseVllmGenerationWorker._merge_stop_strings. One SamplingParams is built per batch here, so a sample's stop strings apply to the batch, the same shape vLLM has. I matched the reference backend rather than inventing a stricter per-sample rule.
  • trtllm_http_server.py: _build_sampling_params gains stop_strings next to stop_token_ids, threaded through create_app and start_server, mirroring how fix(trtllm): Align HTTP server sampling params, use generate_async #3537 threaded the sampling config after the same drift was found in top_k.
  • An empty result stays None rather than [], so TRT-LLM keeps its own default.

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

Five added, and the three existing assertions that pin the exact SamplingParams kwargs updated for the new stop.

test_trtllm_worker_async.py (run with --trtllm-only):

  • test_build_sampling_params_forwards_configured_stop_strings
  • test_merge_stop_strings_unions_config_and_per_sample
  • test_merge_stop_strings_returns_none_when_nothing_configured

test_trtllm_http_server.py (these carry no trtllm marker, so they run without the flag):

  • test_http_sampling_params_forward_stop_strings
  • test_http_sampling_params_empty_stop_strings_stay_none
pytest tests/unit/models/generation/trtllm/test_trtllm_http_server.py
  main:     5 passed, 5 skipped
  this PR:  7 passed, 5 skipped

pytest tests/unit/models/generation/trtllm/test_trtllm_worker_async.py --trtllm-only
  main:    11 passed
  this PR: 14 passed

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

CPU only. I don't have a GPU box, so the tests needing a real TRT-LLM runtime did not run here and I could not exercise a rollout end to end. The change is confined to how SamplingParams is constructed, which is what the tests above cover.

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

No docs change: docs/design-docs/sampling-params.md documents top_k spelling across backends and does not enumerate stop-string behaviour. Happy to add a line there if you would rather have it recorded.

`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>
@yupengtang
yupengtang requested review from a team as code owners September 7, 2026 09:37
@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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant