[RL] Expose top-p-only sampling masks (#33593)

This commit is contained in:
Nan Jiang
2026-08-14 16:40:27 -07:00
committed by GitHub
parent 90b3db6dd8
commit be804c1b83
6 changed files with 68 additions and 16 deletions
@@ -795,6 +795,7 @@ class ChatCompletionRequest(BaseModel):
return_prompt_token_ids: bool = False return_prompt_token_ids: bool = False
return_token_ids: bool = False return_token_ids: bool = False
return_meta_info: bool = False return_meta_info: bool = False
return_sampling_mask: bool = False
reasoning_effort: ReasoningEffortType = Field( reasoning_effort: ReasoningEffortType = Field(
default=None, default=None,
description="Constrains effort on reasoning for reasoning models. " description="Constrains effort on reasoning for reasoning models. "
@@ -816,6 +816,9 @@ class OpenAIServingChat(OpenAIServingBase):
if not request.messages: if not request.messages:
return "Messages cannot be empty." return "Messages cannot be empty."
if request.return_sampling_mask and not request.return_meta_info:
return "return_sampling_mask requires return_meta_info=true."
media_error = self._validate_media_content(request) media_error = self._validate_media_content(request)
if media_error: if media_error:
return media_error return media_error
@@ -1004,6 +1007,7 @@ class OpenAIServingChat(OpenAIServingBase):
return_logprob=request.logprobs, return_logprob=request.logprobs,
logprob_start_len=-1, logprob_start_len=-1,
top_logprobs_num=request.top_logprobs or 0, top_logprobs_num=request.top_logprobs or 0,
return_sampling_mask=request.return_sampling_mask,
stream=request.stream, stream=request.stream,
return_text_in_logprobs=True, return_text_in_logprobs=True,
modalities=processed_messages.modalities, modalities=processed_messages.modalities,
+6 -4
View File
@@ -2528,11 +2528,13 @@ class Scheduler(
self._add_request_to_queue(req) self._add_request_to_queue(req)
return return
if req.return_sampling_mask and req.sampling_params.top_k == TOP_K_ALL: uses_top_k_or_top_p_truncation = (
req.sampling_params.top_k != TOP_K_ALL or req.sampling_params.top_p < 1.0
)
if req.return_sampling_mask and not uses_top_k_or_top_p_truncation:
error_msg = ( error_msg = (
"return_sampling_mask requires finite top_k; top_p-only sampling " "return_sampling_mask cannot return the full vocabulary; set "
"is valid but can return huge masks in the tail, blowing up " "top_p < 1 or a finite top_k."
"metadata, so we need a safety cap."
) )
req.set_finish_with_abort(error_msg) req.set_finish_with_abort(error_msg)
self.init_req_max_new_tokens(req) self.init_req_max_new_tokens(req)
+41 -12
View File
@@ -18,15 +18,13 @@ register_amd_ci(est_time=320, suite="stage-b-test-1-gpu-small-amd")
_MAX_NEW_TOKENS = 4 _MAX_NEW_TOKENS = 4
_TOP_P = 0.99 _TOP_P = 0.99
_TOP_P_SMALL = 1e-5
_TOP_K = 10 _TOP_K = 10
_SAMPLING_SEED = 1234 _SAMPLING_SEED = 1234
_SERVER_ARGS = ( _SERVER_ARGS = (
"--mem-fraction-static", "--mem-fraction-static",
"0.7", "0.7",
) )
_INVALID_SAMPLING_MASK_ERROR = (
"top_p-only sampling is valid but can return huge masks in the tail"
)
class SamplingMaskTestMixin: class SamplingMaskTestMixin:
@@ -81,11 +79,6 @@ class SamplingMaskTestMixin:
self.assertIn(output_id, sampling_mask) self.assertIn(output_id, sampling_mask)
return sampling_masks return sampling_masks
def _assert_rejects_unbounded_sampling_mask(self, sampling_params):
response = self._post_generate(sampling_params)
self.assertEqual(response.status_code, 400, response.text)
self.assertIn(_INVALID_SAMPLING_MASK_ERROR, response.text)
class TestSamplingMask(SamplingMaskTestMixin, CustomTestCase): class TestSamplingMask(SamplingMaskTestMixin, CustomTestCase):
@classmethod @classmethod
@@ -191,16 +184,48 @@ class TestSamplingMask(SamplingMaskTestMixin, CustomTestCase):
expected_logprob = math.log(probs[output_id] / support_mass) expected_logprob = math.log(probs[output_id] / support_mass)
self.assertAlmostEqual(mask_logprob, expected_logprob, delta=1e-2) self.assertAlmostEqual(mask_logprob, expected_logprob, delta=1e-2)
def test_generate_rejects_unbounded_sampling_mask(self): def test_generate_returns_top_p_only_sampling_mask(self):
self._assert_rejects_unbounded_sampling_mask( self._generate_sampling_masks(
{ {
"temperature": 1.0, "temperature": 1.0,
"top_p": _TOP_P, "top_p": _TOP_P_SMALL,
"max_new_tokens": _MAX_NEW_TOKENS, "max_new_tokens": _MAX_NEW_TOKENS,
"ignore_eos": True, "ignore_eos": True,
} }
) )
self._assert_rejects_unbounded_sampling_mask(
def test_chat_completions_returns_top_p_only_sampling_mask(self):
response = requests.post(
self.base_url + "/v1/chat/completions",
json={
"model": self.model,
"messages": [{"role": "user", "content": "Name a capital city."}],
"temperature": 1.0,
"top_p": _TOP_P_SMALL,
"max_tokens": _MAX_NEW_TOKENS,
"ignore_eos": True,
"return_sampling_mask": True,
"return_meta_info": True,
"return_token_ids": True,
},
timeout=60,
)
self.assertEqual(response.status_code, 200, response.text)
choice = response.json()["choices"][0]
output_ids = choice["token_ids"]
meta_info = choice["meta_info"]
sampling_masks = meta_info["output_token_sampling_mask"]
sampling_logprobs = meta_info["output_token_sampling_logprobs"]
self.assertEqual(len(output_ids), _MAX_NEW_TOKENS)
self.assertEqual(len(sampling_masks), len(output_ids))
self.assertEqual(len(sampling_logprobs), len(output_ids))
for output_id, sampling_mask in zip(output_ids, sampling_masks):
self.assertIn(output_id, sampling_mask)
def test_generate_rejects_full_vocabulary_sampling_mask(self):
response = self._post_generate(
{ {
"temperature": 1.0, "temperature": 1.0,
"top_p": 1.0, "top_p": 1.0,
@@ -208,6 +233,10 @@ class TestSamplingMask(SamplingMaskTestMixin, CustomTestCase):
"ignore_eos": True, "ignore_eos": True,
} }
) )
self.assertEqual(response.status_code, 400, response.text)
self.assertIn(
"return_sampling_mask cannot return the full vocabulary", response.text
)
class TestSamplingMaskDeterministic(SamplingMaskTestMixin, CustomTestCase): class TestSamplingMaskDeterministic(SamplingMaskTestMixin, CustomTestCase):
@@ -124,6 +124,7 @@ class TestChatCompletionRequest(unittest.TestCase):
self.assertEqual(request.messages[0].content, "Hello") self.assertEqual(request.messages[0].content, "Hello")
self.assertEqual(request.temperature, None) # default self.assertEqual(request.temperature, None) # default
self.assertFalse(request.stream) # default self.assertFalse(request.stream) # default
self.assertFalse(request.return_sampling_mask)
self.assertEqual(request.tool_choice, "none") # default when no tools self.assertEqual(request.tool_choice, "none") # default when no tools
def test_image_content_hash_validation(self): def test_image_content_hash_validation(self):
@@ -278,9 +278,12 @@ class ServingChatTestCase(unittest.TestCase):
None, None,
) )
self.basic_req.return_sampling_mask = True
self.basic_req.return_meta_info = True
adapted, processed = self.chat._convert_to_internal_request(self.basic_req) adapted, processed = self.chat._convert_to_internal_request(self.basic_req)
self.assertIsInstance(adapted, GenerateReqInput) self.assertIsInstance(adapted, GenerateReqInput)
self.assertFalse(adapted.stream) self.assertFalse(adapted.stream)
self.assertTrue(adapted.return_sampling_mask)
self.assertEqual(adapted.session_id, "session-1") self.assertEqual(adapted.session_id, "session-1")
self.assertEqual(processed, self.basic_req) self.assertEqual(processed, self.basic_req)
@@ -295,6 +298,18 @@ class ServingChatTestCase(unittest.TestCase):
with self.subTest(field=field), self.assertRaisesRegex(ValueError, field): with self.subTest(field=field), self.assertRaisesRegex(ValueError, field):
self.chat._convert_to_internal_request(req, self.fastapi_request) self.chat._convert_to_internal_request(req, self.fastapi_request)
def test_validate_request_rejects_sampling_mask_without_meta_info(self):
req = ChatCompletionRequest(
model="x",
messages=[{"role": "user", "content": "Hi?"}],
return_sampling_mask=True,
)
self.assertEqual(
self.chat._validate_request(req),
"return_sampling_mask requires return_meta_info=true.",
)
def test_convert_to_internal_request_rejects_stream_return_meta_info(self): def test_convert_to_internal_request_rejects_stream_return_meta_info(self):
req = ChatCompletionRequest( req = ChatCompletionRequest(
model="x", model="x",