Skip to content

Commit 2c008fd

Browse files
authored
feat: add return_logprobs and return_token_ids to batch_generate (ml-explore#1359)
1 parent c89c93c commit 2c008fd

2 files changed

Lines changed: 63 additions & 1 deletion

File tree

mlx_lm/generate.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1877,11 +1877,19 @@ class BatchResponse:
18771877
Args:
18781878
texts: (List[str]): The generated text for each prompt.
18791879
stats (BatchStats): Statistics about the generation.
1880+
caches: Optional prompt caches for each sequence.
1881+
token_ids (Optional[List[List[int]]]): The generated token IDs for each
1882+
prompt. Only present when ``return_token_ids=True``.
1883+
logprobs (Optional[List[List[float]]]): The per-token log-probabilities
1884+
of the sampled tokens for each prompt. Only present when
1885+
``return_logprobs=True``.
18801886
"""
18811887

18821888
texts: List[str]
18831889
stats: BatchStats
18841890
caches: Optional[List[List[Any]]]
1891+
token_ids: Optional[List[List[int]]] = None
1892+
logprobs: Optional[List[List[float]]] = None
18851893

18861894

18871895
def batch_generate(
@@ -1892,6 +1900,8 @@ def batch_generate(
18921900
max_tokens: Union[int, List[int]] = 128,
18931901
verbose: bool = False,
18941902
return_prompt_caches: bool = False,
1903+
return_token_ids: bool = False,
1904+
return_logprobs: bool = False,
18951905
**kwargs,
18961906
) -> BatchResponse:
18971907
"""
@@ -1910,6 +1920,12 @@ def batch_generate(
19101920
can be per prompt if a list is provided.
19111921
return_prompt_caches (bool): Return the prompt caches in the batch
19121922
responses. Default: ``False``.
1923+
return_token_ids (bool): Return the generated token IDs in the batch
1924+
responses. Default: ``False``.
1925+
return_logprobs (bool): Return the per-token log-probability of the
1926+
sampled token for each generated token. Useful for reinforcement
1927+
learning (e.g. RLOO, PPO) where behavior log-probabilities are needed
1928+
for importance weighting. Default: ``False``.
19131929
kwargs: The remaining options get passed to :obj:`BatchGenerator`.
19141930
See :obj:`BatchGenerator` for more details.
19151931
"""
@@ -1929,6 +1945,7 @@ def batch_generate(
19291945

19301946
uids = gen.insert(prompts, max_tokens, caches=prompt_caches)
19311947
results = {uid: [] for uid in uids}
1948+
logprob_results = {uid: [] for uid in uids} if return_logprobs else None
19321949
prompt_caches = {}
19331950
with gen.stats() as stats:
19341951
while responses := gen.next_generated():
@@ -1944,13 +1961,17 @@ def batch_generate(
19441961
)
19451962
if r.finish_reason != "stop":
19461963
results[r.uid].append(r.token)
1964+
if return_logprobs:
1965+
logprob_results[r.uid].append(r.logprobs[r.token].item())
19471966
gen.close()
19481967
if verbose:
19491968
print(f"[batch_generate] Finished processing {fin}/{num_samples}")
19501969

19511970
# Return results in correct order
19521971
texts = [tokenizer.decode(results[uid]) for uid in uids]
19531972
caches = [prompt_caches[uid] for uid in uids] if return_prompt_caches else None
1973+
token_ids = [results[uid] for uid in uids] if return_token_ids else None
1974+
logprobs = [logprob_results[uid] for uid in uids] if return_logprobs else None
19541975
if verbose:
19551976
print(
19561977
f"[batch_generate] Prompt: {stats.prompt_tokens} tokens, {stats.prompt_tps:.3f} tokens-per-sec"
@@ -1960,7 +1981,7 @@ def batch_generate(
19601981
f"{stats.generation_tps:.3f} tokens-per-sec"
19611982
)
19621983
print(f"[batch_generate] Peak memory: {stats.peak_memory:.3f} GB")
1963-
return BatchResponse(texts, stats, caches)
1984+
return BatchResponse(texts, stats, caches, token_ids, logprobs)
19641985

19651986

19661987
def main():

tests/test_generate.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -806,6 +806,47 @@ def test_batch_max_kv_size_none_creates_regular_cache(self):
806806
for cache in r.prompt_cache:
807807
self.assertIsInstance(cache, KVCache)
808808

809+
def test_batch_generate_return_logprobs(self):
810+
"""Test that batch_generate returns per-token logprobs when requested."""
811+
prompts = [
812+
self.tokenizer.encode("hello"),
813+
self.tokenizer.encode("write a poem"),
814+
]
815+
max_tokens = 5
816+
response = batch_generate(
817+
self.model,
818+
self.tokenizer,
819+
prompts,
820+
max_tokens=max_tokens,
821+
return_logprobs=True,
822+
return_token_ids=True,
823+
)
824+
825+
# Check that logprobs and token_ids are returned
826+
self.assertIsNotNone(response.logprobs)
827+
self.assertIsNotNone(response.token_ids)
828+
self.assertEqual(len(response.logprobs), len(prompts))
829+
self.assertEqual(len(response.token_ids), len(prompts))
830+
831+
for i in range(len(prompts)):
832+
# token_ids and logprobs should have same length
833+
self.assertEqual(len(response.token_ids[i]), len(response.logprobs[i]))
834+
# logprobs should be non-positive (log-probabilities)
835+
for lp in response.logprobs[i]:
836+
self.assertLessEqual(lp, 0.0)
837+
838+
def test_batch_generate_no_logprobs_by_default(self):
839+
"""Test that batch_generate does not return logprobs by default."""
840+
prompts = [self.tokenizer.encode("hello")]
841+
response = batch_generate(
842+
self.model,
843+
self.tokenizer,
844+
prompts,
845+
max_tokens=3,
846+
)
847+
self.assertIsNone(response.logprobs)
848+
self.assertIsNone(response.token_ids)
849+
809850

810851
if __name__ == "__main__":
811852
unittest.main()

0 commit comments

Comments
 (0)