Skip to content

Commit 7cd8c48

Browse files
committed
feat(vllm-model): pin sampling params for on-policy training
An agent harness chooses its own temperature and top_p. When its rollouts are used for RL, generation has to match the sampling distribution the policy is optimized under, or the rollouts are off-policy by an amount nobody measured. The harness cannot be told to change: it is a released binary, and its settings are tuned for interactive use. Gym builds the outbound engine request itself, so it can enforce this. Setting sampling_overrides on the model server applies those params to every chat request, after the client's own values, so they win. Unset, nothing changes. Gym holds no knowledge of any particular training framework here. The integrating framework sets these to its own generation config; the model server only enforces them. Signed-off-by: Ananth Subramaniam <ansubramania@nvidia.com>
1 parent 8bbcb6b commit 7cd8c48

3 files changed

Lines changed: 68 additions & 0 deletions

File tree

responses_api_models/vllm_model/app.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,13 @@ class VLLMModelConfig(BaseResponsesAPIModelConfig):
6464

6565
chat_template_kwargs: Optional[Dict[str, Any]] = None
6666

67+
# Sampling params to force on every chat request, overriding whatever the client sent. An
68+
# external harness (e.g. a CLI agent) chooses its own temperature/top_p, but on-policy RL
69+
# training requires generation to match the sampling distribution the policy is optimized under.
70+
# The integrating training framework sets these to its generation sampling params; Gym only
71+
# enforces them and holds no knowledge of any specific framework. Unset means no override.
72+
sampling_overrides: Optional[Dict[str, Any]] = None
73+
6774
# Corresponds to the extra_body of OpenAI Client.
6875
extra_body: Optional[Dict[str, Any]] = None
6976

@@ -448,6 +455,12 @@ def _preprocess_chat_completion_create_params(self, request: Request, body_dict:
448455
# No user message found — create one with just the audio blocks.
449456
body_dict.setdefault("messages", []).append({"role": "user", "content": list(audio_blocks)})
450457

458+
# Pin sampling params last so they win over anything the client sent. On-policy RL training
459+
# requires the generation to match the training worker's sampling config; an external harness
460+
# sets its own temperature/top_p, so force them here to keep captured rollouts on-policy.
461+
if self.config.sampling_overrides:
462+
body_dict.update(self.config.sampling_overrides)
463+
451464
return body_dict
452465

453466
async def chat_completions(

responses_api_models/vllm_model/configs/vllm_model_for_training.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,10 @@ policy_model:
77
model: ${policy_model_name}
88
return_token_id_information: true
99
uses_reasoning_parser: true
10+
# On-policy training: force generation to the sampling params the policy is optimized under,
11+
# overriding whatever an external harness requests, so captured rollouts stay on-policy. The
12+
# integrating training framework supplies these via the generic keys below (Gym does not know
13+
# any framework); the defaults are standard on-policy sampling when a framework sets nothing.
14+
sampling_overrides:
15+
temperature: ${oc.select:policy_generation_temperature,1.0}
16+
top_p: ${oc.select:policy_generation_top_p,1.0}

responses_api_models/vllm_model/tests/test_app.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4684,3 +4684,51 @@ async def mock_create_chat_completion(**kwargs):
46844684
)
46854685
# The tokenize endpoint must not be reached once the contract check fails.
46864686
mock_client.create_tokenize.assert_not_called()
4687+
4688+
4689+
4690+
class TestSamplingOverrides:
4691+
"""Forcing the sampling params on every request.
4692+
4693+
An external harness picks its own temperature and top_p. On-policy RL requires
4694+
generation to match the distribution the policy is optimized under, so the
4695+
server overrides whatever the client sent rather than trusting it.
4696+
"""
4697+
4698+
@staticmethod
4699+
def _server(overrides: dict[str, object] | None) -> VLLMModel:
4700+
config = VLLMModelConfig(
4701+
host="0.0.0.0",
4702+
port=8081,
4703+
base_url="http://api.openai.com/v1",
4704+
api_key="dummy_key", # pragma: allowlist secret
4705+
model="dummy_model",
4706+
entrypoint="",
4707+
name="",
4708+
return_token_id_information=False,
4709+
uses_reasoning_parser=False,
4710+
sampling_overrides=overrides,
4711+
)
4712+
return VLLMModel(config=config, server_client=MagicMock(spec=ServerClient, global_config_dict={}))
4713+
4714+
def test_overrides_replace_what_the_client_sent(self) -> None:
4715+
server = self._server({"temperature": 1.0, "top_p": 1.0})
4716+
out = server._preprocess_chat_completion_create_params(
4717+
MagicMock(), {"messages": [{"role": "user", "content": "hi"}], "temperature": 0.2, "top_p": 0.5}
4718+
)
4719+
assert out["temperature"] == 1.0
4720+
assert out["top_p"] == 1.0
4721+
4722+
def test_overrides_apply_even_when_the_client_sent_nothing(self) -> None:
4723+
server = self._server({"temperature": 1.0})
4724+
out = server._preprocess_chat_completion_create_params(
4725+
MagicMock(), {"messages": [{"role": "user", "content": "hi"}]}
4726+
)
4727+
assert out["temperature"] == 1.0
4728+
4729+
def test_unset_leaves_the_request_alone(self) -> None:
4730+
server = self._server(None)
4731+
out = server._preprocess_chat_completion_create_params(
4732+
MagicMock(), {"messages": [{"role": "user", "content": "hi"}], "temperature": 0.2}
4733+
)
4734+
assert out["temperature"] == 0.2

0 commit comments

Comments
 (0)