Presence and frequency penalties (#971)

This commit is contained in:
Angelos Katharopoulos
2026-03-09 22:26:39 -07:00
committed by GitHub
parent 852119b774
commit 4a21ffdf4b
4 changed files with 188 additions and 16 deletions
+14 -2
View File
@@ -72,12 +72,24 @@ curl localhost:8080/v1/chat/completions \
- `min_p`: (Optional) A float specifying the min-p sampling parameter.
Defaults to `0.0` (disabled).
- `repetition_penalty`: (Optional) Applies a penalty to repeated tokens.
Defaults to `1.0`.
- `repetition_penalty`: (Optional) Applies a multiplicative penalty to repeated
tokens. Defaults to `0.0` (disabled).
- `repetition_context_size`: (Optional) The size of the context window for
applying repetition penalty. Defaults to `20`.
- `presence_penalty`: (Optional) Applies an additive penalty to tokens
that appeared before. Defaults to `0.0` (disabled).
- `presence_context_size`: (Optional) The size of the context window for
applying presence penalty. Defaults to `20`.
- `frequency_penalty`: (Optional) Applies an additive penalty proportional to
how many times a token appeared previously. Defaults to `0.0` (disabled).
- `frequency_context_size`: (Optional) The size of the context window for
applying frequency penalty. Defaults to `20`.
- `logit_bias`: (Optional) A dictionary mapping token IDs to their bias
values. Defaults to `None`.
+81 -8
View File
@@ -73,15 +73,28 @@ def make_logits_processors(
logit_bias: Optional[Dict[int, float]] = None,
repetition_penalty: Optional[float] = None,
repetition_context_size: Optional[int] = 20,
presence_penalty: Optional[float] = None,
presence_context_size: Optional[int] = 20,
frequency_penalty: Optional[float] = None,
frequency_context_size: Optional[int] = 20,
):
"""
Make logits processors for use with ``generate_step``.
Args:
repetition_penalty (float, optional): The penalty factor for repeating
tokens.
repetition_penalty (float, optional): A (sign-aware) multiplicative
penalty for repeating tokens.
repetition_context_size (int, optional): The number of tokens to
consider for repetition penalty. Default: ``20``.
presence_penalty (float, optional): An additive penalty to reduce
repeating tokens.
presence_context_size (int, optional): The number of tokens to consider
for the presence penalty. Default: ``20``.
frequency_penalty (float, optional): An additive penalty to reduce
repeating tokens. The tokens are penalized proportionally to their
frequency.
frequency_context_size (int, optional): The number of tokens to consider
for the frequency penalty. Default: ``20``.
logit_bias (dictionary, optional): Additive logit bias.
Returns:
@@ -96,15 +109,20 @@ def make_logits_processors(
values = mx.array(list(logit_bias.values()))
def logit_bias_processor(_, logits):
logits[:, indices] += values
return logits
return logits.at[:, indices].add(values)
logits_processors.append(logit_bias_processor)
if repetition_penalty and repetition_penalty != 0.0:
logits_processors.append(
make_repetition_penalty(repetition_penalty, repetition_context_size)
)
repetition_penalties = [
(make_repetition_penalty, repetition_penalty, repetition_context_size),
(make_presence_penalty, presence_penalty, presence_context_size),
(make_frequency_penalty, frequency_penalty, frequency_context_size),
]
for make_penalty, penalty, context_size in repetition_penalties:
if penalty is not None and penalty != 0:
logits_processors.append(make_penalty(penalty, context_size))
return logits_processors
@@ -307,3 +325,58 @@ def make_repetition_penalty(penalty: float, context_size: int = 20):
return logits
return repetition_penalty_processor
def make_presence_penalty(penalty: float, context_size: int = 20):
"""
Make a presence penalty processor.
Corresponds to the OpenAI option with the same name. Namely, subtracts
``penalty`` from a logit if the token has occured at least once in the
``context_size`` previous tokens.
Args:
penalty (float): The presence penalty to be applied.
context_size (int): The number of previous tokens to use.
Default: ``20``.
Returns:
Callable[[mx.array, List[int]], mx.array]
"""
def presence_penalty_processor(tokens, logits):
if len(tokens) > 0:
tokens = tokens[-context_size:]
logits[:, tokens] -= penalty
return logits
return presence_penalty_processor
def make_frequency_penalty(penalty: float, context_size: int = 20):
"""
Make a frequency penalty processor.
Corresponds to the OpenAI option with the same name. Namely, subtracts
``penalty`` from a logit for every time that the token has occured in the
``context_size`` previous tokens.
The difference with the presence penalty is that the more often a token
occurs the more it will be penalized.
Args:
penalty (float): The frequency penalty to be applied.
context_size (int): The number of previous tokens to use.
Default: ``20``.
Returns:
Callable[[mx.array, List[int]], mx.array]
"""
def frequency_penalty_processor(tokens, logits):
if len(tokens) > 0:
tokens = tokens[-context_size:]
logits = logits.at[:, tokens].subtract(penalty)
return logits
return frequency_penalty_processor
+35 -6
View File
@@ -395,6 +395,10 @@ class LogitsProcessorArguments:
logit_bias: Optional[Dict[int, float]]
repetition_penalty: float
repetition_context_size: int
presence_penalty: float
presence_context_size: int
frequency_penalty: float
frequency_context_size: int
@dataclass
@@ -623,6 +627,10 @@ def _make_logits_processors(args):
args.logits.logit_bias,
args.logits.repetition_penalty,
args.logits.repetition_context_size,
args.logits.presence_penalty,
args.logits.presence_context_size,
args.logits.frequency_penalty,
args.logits.frequency_context_size,
)
@@ -1208,6 +1216,10 @@ class APIHandler(BaseHTTPRequestHandler):
self.min_p = self.body.get("min_p", self.response_generator.cli_args.min_p)
self.repetition_penalty = self.body.get("repetition_penalty", 0.0)
self.repetition_context_size = self.body.get("repetition_context_size", 20)
self.presence_penalty = self.body.get("presence_penalty", 0.0)
self.presence_context_size = self.body.get("presence_context_size", 20)
self.frequency_penalty = self.body.get("frequency_penalty", 0.0)
self.frequency_context_size = self.body.get("frequency_context_size", 20)
self.xtc_probability = self.body.get("xtc_probability", 0.0)
self.xtc_threshold = self.body.get("xtc_threshold", 0.0)
self.logit_bias = self.body.get("logit_bias", None)
@@ -1256,6 +1268,25 @@ class APIHandler(BaseHTTPRequestHandler):
or self.repetition_penalty < 0
):
raise ValueError("repetition_penalty must be a non-negative float")
if (
not isinstance(self.repetition_context_size, int)
or self.repetition_context_size < 0
):
raise ValueError("repetition_context_size must be a non-negative integer")
if not isinstance(self.presence_penalty, (float, int)):
raise ValueError("Presence penalty must be must be a float")
if (
not isinstance(self.presence_context_size, int)
or self.presence_context_size < 0
):
raise ValueError("presence_context_size must be a non-negative integer")
if not isinstance(self.frequency_penalty, (float, int)):
raise ValueError("Presence penalty must be must be a float")
if (
not isinstance(self.frequency_context_size, int)
or self.frequency_context_size < 0
):
raise ValueError("frequency_context_size must be a non-negative integer")
if not isinstance(self.logprobs, bool):
raise ValueError("logprobs must be a boolean")
@@ -1265,12 +1296,6 @@ class APIHandler(BaseHTTPRequestHandler):
f"top_logprobs must be between 1 and 10 but got {self.top_logprobs:,}"
)
if (
not isinstance(self.repetition_context_size, int)
or self.repetition_context_size < 0
):
raise ValueError("repetition_context_size must be a non-negative integer")
if self.logit_bias is not None:
if not isinstance(self.logit_bias, dict):
raise ValueError("logit_bias must be a dict of int to float")
@@ -1430,6 +1455,10 @@ class APIHandler(BaseHTTPRequestHandler):
logit_bias=self.logit_bias,
repetition_penalty=self.repetition_penalty,
repetition_context_size=self.repetition_context_size,
presence_penalty=self.presence_penalty,
presence_context_size=self.presence_context_size,
frequency_penalty=self.frequency_penalty,
frequency_context_size=self.frequency_context_size,
),
stop_words=stop_words,
max_tokens=self.max_tokens,
+58
View File
@@ -116,6 +116,64 @@ class TestSampleUtils(unittest.TestCase):
new_probs = mx.softmax(apply_xtc(mx.log(probs), 0, 0.1, [0]), -1)
self.assertTrue(mx.allclose(new_probs, probs))
def test_presence_penalty(self):
from mlx_lm.sample_utils import make_presence_penalty
# Token appears multiple times - penalty applied once
tokens = mx.array([0, 0, 0, 1, 1])
logits = mx.zeros((1, 4))
processor = make_presence_penalty(0.5, context_size=5)
result = processor(tokens, logits)
# Token 0 appears 3 times, token 1 appears 2 times - both penalized once
self.assertAlmostEqual(result[0, 0].item(), -0.5)
self.assertAlmostEqual(result[0, 1].item(), -0.5)
# Tokens not in context not penalized
self.assertAlmostEqual(result[0, 2].item(), 0.0)
self.assertAlmostEqual(result[0, 3].item(), 0.0)
def test_frequency_penalty(self):
from mlx_lm.sample_utils import make_frequency_penalty
# Token appears multiple times - penalty applied proportionally
tokens = mx.array([0, 0, 0, 1, 1])
logits = mx.zeros((1, 4))
processor = make_frequency_penalty(0.5, context_size=5)
result = processor(tokens, logits)
# Token 0 appears 3 times -> 3 * 0.5 = 1.5 penalty
self.assertAlmostEqual(result[0, 0].item(), -1.5)
# Token 1 appears 2 times -> 2 * 0.5 = 1.0 penalty
self.assertAlmostEqual(result[0, 1].item(), -1.0)
# Tokens not in context not penalized
self.assertAlmostEqual(result[0, 2].item(), 0.0)
self.assertAlmostEqual(result[0, 3].item(), 0.0)
def test_make_logits_processors(self):
from mlx_lm.sample_utils import make_logits_processors
# Create processors with all three penalty types
tokens = mx.array([0, 0, 0, 1, 1])
# Use non-zero logits so repetition penalty has effect
logits = mx.array([[1.0, 0.5, 0.0, -0.5]])
processors = make_logits_processors(
repetition_penalty=1.5,
repetition_context_size=5,
presence_penalty=0.5,
presence_context_size=5,
frequency_penalty=0.25,
frequency_context_size=5,
)
# Apply all processors
for processor in processors:
logits = processor(tokens, logits)
# Token 0 (appears 3x): 1.0/1.5 - 0.5 - 0.75 = -0.5833
# Token 1 (appears 2x): 0.5/1.5 - 0.5 - 0.5 = -0.6667
# Token 2 (not in context): 0.0 (no penalty)
# Token 3 (not in context): -0.5 (no penalty)
self.assertAlmostEqual(logits[0, 0].item(), -0.5833, places=4)
self.assertAlmostEqual(logits[0, 1].item(), -0.6667, places=4)
self.assertAlmostEqual(logits[0, 2].item(), 0.0, places=4)
self.assertAlmostEqual(logits[0, 3].item(), -0.5, places=4)
if __name__ == "__main__":
unittest.main()