Skip to content

Commit 47239e1

Browse files
authored
Add support for default top_p/top_k (#1267)
This PR enables setting default top_p/top_k values for a model. This is critical for users who use reasoning models such as DeepSeek R1, QWEN3, etc... as using default top_p=1.0 and top_k=None will provide sub-optimal outputs. ```python llamacpp_lm = guidance.models.LlamaCpp( model="Llama-3.2-3B-Instruct-UD-Q8_K_XL.gguf", echo=False, n_ctx=8192, default_sampling_params=SamplingParams( top_p=0.95, top_k=64 ) ) openai_lm = models.OpenAI( "gpt-3.5-turbo", api_key=os.environ.get("OPENAI_API_KEY"), # you can also use dict default_sampling_params={ "top_p" : 0.95 } ) vllm_lm = guidance.models.experimental.VLLMModel( model="DeepSeek-R1-0528-Qwen3-8B-fp8", base_url="http://localhost:5005/v1", api_key="NOKEY", default_sampling_params={ "top_p" : 0.95, "top_k": 64 } ) model_desc = { "model_name": "DeepSeek-R1-0528-Qwen3-8B-fp8", "litellm_params": { "model": "hosted_vllm/DeepSeek-R1-0528-Qwen3-8B-fp8", "api_key": os.environ.get("LITELLM_API_KEY", None), "api_base": "http://localhost:5005/v1" } } litellm_lm = guidance.models.experimental.LiteLLM( model_description=model_desc, echo=False, default_sampling_params={ "top_p" : 0.95, "top_k": 64 } ) ```
1 parent df0e596 commit 47239e1

14 files changed

Lines changed: 210 additions & 46 deletions

File tree

guidance/_schema.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Any, Literal, Optional, Union, Dict
1+
from typing import Any, Literal, Optional, Union, TypedDict
22

33
from pydantic import BaseModel, Field, NonNegativeInt, RootModel, model_validator, computed_field
44
from typing_extensions import Annotated
@@ -166,3 +166,7 @@ class LLInterpreterResponse(BaseModel):
166166
progress: LLProgress
167167
stop: bool
168168
temperature: Optional[float]
169+
170+
class SamplingParams(TypedDict):
171+
top_p: Optional[float]
172+
top_k: Optional[int]

guidance/_utils.py

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,16 @@
99
import functools
1010
import numpy as np
1111
import logging
12-
from typing import Union, cast
12+
from typing import Union, cast, Optional, TYPE_CHECKING
1313
import pathlib
1414
import pydantic
1515
import urllib
1616
import http
1717
import re
1818

19+
if TYPE_CHECKING:
20+
from ._schema import SamplingParams
21+
1922
logger = logging.getLogger(__name__)
2023

2124

@@ -340,3 +343,47 @@ def to_utf8_or_bytes_string(_bytes: bytes) -> str:
340343
return _bytes.decode("utf-8")
341344
except UnicodeDecodeError:
342345
return str(_bytes)
346+
347+
def apply_top_k_only(logits: np.ndarray, k: int) -> np.ndarray:
348+
if k <= 0:
349+
return logits
350+
351+
indices_to_remove = logits.argpartition(-k)[:-k]
352+
logits[indices_to_remove] = -float("inf")
353+
return logits
354+
355+
356+
def apply_top_k_and_top_p_filter(logits: np.ndarray, sampling_params: Optional['SamplingParams']) -> np.ndarray:
357+
if sampling_params is None:
358+
return logits
359+
360+
top_p = sampling_params.get("top_p", None)
361+
top_k = sampling_params.get("top_k", None)
362+
363+
if top_k is None and top_p is None:
364+
# No filtering, return logits as is
365+
return logits
366+
367+
if top_k is not None and top_p is None:
368+
return apply_top_k_only(logits, top_k)
369+
370+
# try our best to sort logits one time only
371+
sorted_logits = logits.argsort()
372+
if top_k is not None and top_k > 0:
373+
indices_to_remove = sorted_logits[:-top_k]
374+
logits[indices_to_remove] = -float("inf")
375+
376+
if top_p is not None and top_p > 0:
377+
sorted_indices = sorted_logits[::-1]
378+
sorted_logits = logits[sorted_indices]
379+
probs = softmax(sorted_logits)
380+
cumulative_probs = np.cumsum(probs)
381+
indices_to_remove = cumulative_probs > top_p
382+
if np.any(indices_to_remove):
383+
# +1 to keep the first token that exceeds the threshold
384+
first_to_remove = np.argmax(indices_to_remove) + 1
385+
# make sure we always keep at least one token
386+
sorted_indices_to_remove = sorted_indices[max(1, first_to_remove):]
387+
logits[sorted_indices_to_remove] = -float("inf")
388+
389+
return logits

guidance/models/_base/_interpreter.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import base64
2-
from typing import Generic, Iterator, TypeVar
2+
from typing import Generic, Iterator, TypeVar, Optional
3+
4+
from guidance._schema import SamplingParams
35

46
from ..._ast import (
57
ASTNode,
@@ -29,8 +31,9 @@
2931

3032

3133
class Interpreter(Generic[S]):
32-
def __init__(self, state: S):
34+
def __init__(self, state: S, default_sampling_params: Optional[SamplingParams] = None):
3335
self.state = state
36+
self.default_sampling_params = SamplingParams() if default_sampling_params is None else default_sampling_params
3437

3538
def run(self, node: ASTNode, **kwargs) -> Iterator[OutputAttr]:
3639
yield from node.simplify()._run(self, **kwargs)

guidance/models/_base/_model.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@
3535
from ._interpreter import Interpreter
3636
from ._state import State
3737

38+
from ..._schema import SamplingParams
39+
3840
if TYPE_CHECKING:
3941
from ...library._block import Block
4042

@@ -62,6 +64,7 @@ class Model:
6264
def __init__(
6365
self,
6466
interpreter: Interpreter[S],
67+
default_sampling_params: Optional[SamplingParams] = None,
6568
echo: bool = True,
6669
) -> None:
6770
self.echo = echo

guidance/models/_engine/_engine.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,11 @@
1515
EngineResponse,
1616
GenToken,
1717
GuidanceEngineMetrics,
18+
SamplingParams,
1819
)
1920

2021
from ...registry import get_exchange
21-
from ..._utils import log_cleanup, log_init, softmax
22+
from ..._utils import log_cleanup, log_init, softmax, apply_top_k_and_top_p_filter
2223
from ...visual import (
2324
ExecutionCompletedMessage,
2425
ExecutionStartedMessage,
@@ -132,6 +133,8 @@ def __call__(
132133
grammar: str,
133134
ensure_bos_token: bool = True,
134135
echo: bool = True,
136+
sampling_params: Optional[SamplingParams] = None,
137+
**kwargs: Optional[dict],
135138
) -> Iterator[EngineResponse]:
136139
"""Main entry point for the inference-parser loop. Yields EngineCallResponse objects as
137140
the parser advances through the grammar.
@@ -148,6 +151,8 @@ def __call__(
148151
Grammar (RawFunction or GrammarFunction) used to extend the prompt.
149152
ensure_bos_token: bool
150153
Ensures that the prompt ends with the BOS token.
154+
sampling_params: Optional[SamplingParams]
155+
Additional sampling parameters to apply to the logits.
151156
"""
152157
# TODO: Pass these to get_logits
153158
# images = state.images
@@ -331,6 +336,7 @@ def __call__(
331336
temperature=ll_response.temperature,
332337
k=self._top_k,
333338
force_return_unmasked_probs=echo,
339+
sampling_params=sampling_params,
334340
)
335341
last_temperature = ll_response.temperature
336342

@@ -348,6 +354,7 @@ def get_next_token_with_top_k(
348354
temperature: float,
349355
k: int = 5,
350356
force_return_unmasked_probs: bool = False,
357+
sampling_params: Optional[SamplingParams] = None,
351358
) -> EngineOutput:
352359
"""Get the next token and associated top-k tokens from the engine.
353360
@@ -369,6 +376,8 @@ def get_next_token_with_top_k(
369376
The number of top-k tokens to return.
370377
force_return_unmasked_probs: bool
371378
If True, the top-k unmasked probabilities will be returned.
379+
sampling_params : Optional[SamplingParams]
380+
Additional sampling parameters to apply to the logits.
372381
373382
Returns
374383
-------
@@ -399,9 +408,9 @@ def get_top_k(_probs: NDArray, _k: int = 5) -> list[GenToken]:
399408

400409
# compute top-k without masking
401410
probs = (
402-
softmax(np.array(logits))
411+
softmax(apply_top_k_and_top_p_filter(np.array(logits), sampling_params))
403412
if temperature < _TEMPERATURE_EPSILON
404-
else softmax(np.array(logits) / temperature)
413+
else softmax(apply_top_k_and_top_p_filter(np.array(logits) / temperature, sampling_params))
405414
)
406415

407416
top_k: list[GenToken] = []
@@ -413,11 +422,14 @@ def get_top_k(_probs: NDArray, _k: int = 5) -> list[GenToken]:
413422
if mask is not None:
414423
mask = np.frombuffer(mask, dtype=np.uint8)
415424
masked_logits = np.where(mask != 0, logits, -np.inf)
425+
416426
if temperature < _TEMPERATURE_EPSILON:
417427
masked_logits = np.where(masked_logits == np.max(masked_logits), 0, -np.inf)
418428
else:
419429
masked_logits /= temperature
420430

431+
masked_logits = apply_top_k_and_top_p_filter(masked_logits, sampling_params)
432+
421433
masked_probs = softmax(masked_logits)
422434
masked_top_k = get_top_k(masked_probs, k)
423435

guidance/models/_engine/_interpreter.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from base64 import b64decode, b64encode
22
from io import BytesIO
3-
from typing import Iterator
3+
from typing import Iterator, Optional
44
from copy import deepcopy
55
import re
66

@@ -10,15 +10,15 @@
1010
from .._base import Interpreter
1111
from ._engine import Engine, Tokenizer
1212
from ._state import EngineState
13-
from ..._schema import GenTokenExtra
13+
from ..._schema import GenTokenExtra, SamplingParams
1414

1515

1616
class EngineInterpreter(Interpreter[EngineState]):
17-
def __init__(self, engine: Engine):
18-
self.state = EngineState()
17+
def __init__(self, engine: Engine, default_sampling_params: Optional[SamplingParams] = None):
18+
super().__init__(state=EngineState(), default_sampling_params=default_sampling_params)
1919
self.engine = engine
2020
self.chat_template = self.engine.get_chat_template()
21-
21+
2222
def __deepcopy__(self, memo):
2323
"""Custom deepcopy to ensure engine is not copied."""
2424
cls = self.__class__
@@ -71,6 +71,7 @@ def grammar(self, node: GrammarNode, **kwargs) -> Iterator[OutputAttr]:
7171
grammar=node.ll_grammar(),
7272
ensure_bos_token=True,
7373
echo=False,
74+
sampling_params=self.default_sampling_params # NOTE: passing default sampling params for now
7475
)
7576

7677
delayed_bytes = b""

guidance/models/_llama_cpp.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@
55
import sys
66
from itertools import takewhile
77
from pathlib import Path
8-
from typing import TYPE_CHECKING, Union
8+
from typing import TYPE_CHECKING, Union, Optional
99
import ctypes
1010

1111
import numpy as np
1212

13-
from .._schema import GenToken, GenTokenExtra
13+
from .._schema import GenToken, GenTokenExtra, SamplingParams
1414
from .._utils import normalize_notebook_stdout_stderr, softmax
1515
from ..chat import ChatTemplate
1616
from ._base import Model
@@ -232,7 +232,8 @@ def __init__(
232232
enable_backtrack=True,
233233
enable_ff_tokens=True,
234234
enable_monitoring=True,
235-
**llama_cpp_kwargs,
235+
default_sampling_params: Optional[SamplingParams] = None,
236+
**llama_cpp_kwargs
236237
):
237238
"""Build a new LlamaCpp model object that represents a model in a given state."""
238239

@@ -245,5 +246,5 @@ def __init__(
245246
enable_monitoring=enable_monitoring,
246247
**llama_cpp_kwargs,
247248
)
248-
interpreter = EngineInterpreter(engine)
249+
interpreter = EngineInterpreter(engine, default_sampling_params=default_sampling_params)
249250
super().__init__(interpreter=interpreter, echo=echo)

guidance/models/_mock.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
import numpy as np
55

6-
from .._schema import EngineOutput
6+
from .._schema import EngineOutput, SamplingParams
77
from ..trace import TraceHandler
88
from ..visual._renderer import DoNothingRenderer
99
from ._base import Model
@@ -106,10 +106,12 @@ def get_next_token_with_top_k(
106106
temperature: float,
107107
k: int = 1,
108108
force_return_unmasked_probs: bool = False,
109+
sampling_params: Optional[SamplingParams] = None,
110+
109111
) -> EngineOutput:
110112
self.called_temperatures.append(temperature)
111113
return super().get_next_token_with_top_k(
112-
logits, logits_lat_ms, token_ids, mask, temperature, k, force_return_unmasked_probs
114+
logits, logits_lat_ms, token_ids, mask, temperature, k, force_return_unmasked_probs, sampling_params
113115
)
114116

115117
def get_logits(self, token_ids: list[int], include_all_uncached_tokens: bool = False) -> np.ndarray:
@@ -161,6 +163,7 @@ class Mock(Model):
161163
def __init__(
162164
self,
163165
byte_patterns=[],
166+
default_sampling_params: Optional[SamplingParams] = None,
164167
echo=False,
165168
force=False,
166169
**kwargs,

guidance/models/_openai.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from typing import Optional
22

3+
from guidance._schema import SamplingParams
4+
35

46
from ._base import Model
57
from ._openai_base import (
@@ -16,7 +18,8 @@
1618
class OpenAIInterpreter(OpenAIRuleMixin, OpenAIJSONMixin, OpenAIRegexMixin, BaseOpenAIInterpreter):
1719
def __init__(
1820
self,
19-
model: str,
21+
model: str,
22+
default_sampling_params: Optional[SamplingParams],
2023
api_key: Optional[str] = None,
2124
**kwargs,
2225
):
@@ -26,15 +29,17 @@ def __init__(
2629
raise Exception(
2730
"Please install the openai package version >= 1 using `pip install openai -U` in order to use guidance.models.OpenAI!"
2831
)
32+
2933
client = openai.OpenAI(api_key=api_key, **kwargs)
30-
super().__init__(model=model, client=OpenAIClientWrapper(client))
34+
super().__init__(model=model, client=OpenAIClientWrapper(client), default_sampling_params=default_sampling_params)
3135

3236

3337
class OpenAI(Model):
3438
def __init__(
3539
self,
3640
model: str,
37-
echo: bool = True,
41+
default_sampling_params: Optional[SamplingParams] = None,
42+
echo: bool = True,
3843
*,
3944
api_key: Optional[str] = None,
4045
**kwargs,
@@ -66,4 +71,4 @@ def __init__(
6671
else:
6772
interpreter_cls = OpenAIInterpreter
6873

69-
super().__init__(interpreter=interpreter_cls(model, api_key=api_key, **kwargs), echo=echo)
74+
super().__init__(interpreter=interpreter_cls(model, api_key=api_key, default_sampling_params=default_sampling_params, **kwargs), echo=echo)

guidance/models/_openai_base.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
from pydantic import BaseModel, Discriminator, Field, TypeAdapter
1010
from typing_extensions import Annotated, assert_never
1111

12+
from guidance._schema import SamplingParams
13+
1214
from .._ast import (
1315
ASTNode,
1416
GenAudio,
@@ -30,6 +32,7 @@
3032
import openai
3133
from openai.types.chat import ChatCompletionChunk
3234
from openai.types.chat.chat_completion_chunk import ChoiceLogprobs
35+
from openai import OpenAI
3336

3437

3538
def get_role_start(role: str) -> str:
@@ -183,6 +186,7 @@ def streaming_chat_completions(
183186
**kwargs,
184187
) -> ContextManager[Iterator["ChatCompletionChunk"]]:
185188
"""Streaming chat completions."""
189+
186190
return self.client.chat.completions.create(
187191
model=model,
188192
messages=TypeAdapter(list[Message]).dump_python(messages), # type: ignore[arg-type]
@@ -200,8 +204,10 @@ def __init__(
200204
self,
201205
model: str,
202206
client: BaseOpenAIClientWrapper,
207+
default_sampling_params: Optional[SamplingParams] = None,
208+
**kwargs
203209
):
204-
self.state = OpenAIState()
210+
super().__init__(state=OpenAIState(), default_sampling_params=default_sampling_params)
205211
self.model = model
206212
self.client = client
207213

@@ -249,6 +255,10 @@ def _run(self, **kwargs) -> Iterator[OutputAttr]:
249255
raise ValueError(
250256
f"OpenAI models do not support pre-filled assistant messages: got data {self.state.content}."
251257
)
258+
259+
# only process kwargs that are supported by the OpenAI API
260+
if "top_p" not in kwargs and "top_p" in self.default_sampling_params:
261+
kwargs["top_p"] = self.default_sampling_params["top_p"]
252262

253263
with self.client.streaming_chat_completions(
254264
model=self.model,

0 commit comments

Comments
 (0)