# Copyright © 2023 Apple Inc. import math from functools import partial from typing import Callable, Dict, List, Optional import mlx.core as mx Sampler = Callable[[mx.array], mx.array] LogitsProcessor = Callable[[mx.array, mx.array], mx.array] def make_sampler( temp: float = 0.0, top_p: float = 0.0, min_p: float = 0.0, min_tokens_to_keep: int = 1, top_k: int = 0, xtc_probability: float = 0.0, xtc_threshold: float = 0.1, xtc_special_tokens: Optional[List[int]] = None, ) -> Sampler: """ Make a sampler function for use with ``generate_step``. Args: temp (float): The temperature for sampling, if 0 the argmax is used. Default: ``0``. top_p (float, optional): Nulceus sampling, higher means model considers more less likely words. min_p (float, optional): The minimum value (scaled by the top token's probability) that a token probability must have to be considered. min_tokens_to_keep (int, optional): Minimum number of tokens that cannot be filtered by min_p sampling. top_k (int, optional): The top k tokens ranked by probability to constrain the sampling to. xtc_probability (float, optional): The probability of applying XTC sampling. xtc_threshold (float, optional): The threshold the probs need to reach for being sampled. xtc_special_tokens (list(int), optional): List of special tokens IDs to be excluded from XTC sampling. Returns: Sampler: A sampler which takes log-probabilities and returns tokens. """ if temp == 0: return greedy_sampler # Create sampler chain xtc_special_tokens = [] if xtc_special_tokens is None else xtc_special_tokens sampling_methods = [] if 0 < top_p < 1.0: sampling_methods.append(lambda x: apply_top_p(x, top_p)) if min_p != 0.0: sampling_methods.append(lambda x: apply_min_p(x, min_p, min_tokens_to_keep)) if xtc_probability > 0.0: sampling_methods.append( lambda x: apply_xtc(x, xtc_probability, xtc_threshold, xtc_special_tokens) ) if top_k > 0: sampling_methods.append(lambda x: apply_top_k(x, top_k)) # Apply the sampling methods def sampler(logprobs: mx.array) -> mx.array: for method in sampling_methods: logprobs = method(logprobs) # Return the sampled token return categorical_sampling(logprobs, temp) return sampler 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, ) -> List[LogitsProcessor]: """ Make logits processors for use with ``generate_step``. Args: 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: List[LogitsProcessor]: A list of logits processors. Each processor in the list is a callable which takes an array of tokens and an array of logits and returns the updated logits. """ logits_processors = [] if logit_bias: indices = mx.array(list(logit_bias.keys())) values = mx.array(list(logit_bias.values())) def logit_bias_processor(_, logits): return logits.at[:, indices].add(values) logits_processors.append(logit_bias_processor) 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 def greedy_sampler(logprobs: mx.array) -> mx.array: return mx.argmax(logprobs, axis=-1) @partial(mx.compile, inputs=mx.random.state, outputs=mx.random.state) def apply_top_k( logprobs: mx.array, top_k: int, ) -> mx.array: """ Sample from only the top K tokens ranked by probability. Args: logprobs: A vector of log probabilities. top_k (int): Top k tokens to sample from. """ vocab_size = logprobs.shape[-1] if not isinstance(top_k, int) or not (0 < top_k < vocab_size): raise ValueError( f"`top_k` has to be an integer in the (0, {vocab_size}) interval," f" but is {top_k}." ) mask_idx = mx.argpartition(-logprobs, kth=top_k - 1, axis=-1)[..., top_k:] masked_logprobs = mx.put_along_axis( logprobs, mask_idx, mx.array(-float("inf"), logprobs.dtype), axis=-1 ) return masked_logprobs @partial(mx.compile, inputs=mx.random.state, outputs=mx.random.state) def apply_min_p( logprobs: mx.array, min_p: float, min_tokens_to_keep: int = 1, ) -> mx.array: """ Apply min-p sampling to the logprobs. Min-p keeps all tokens that are above a minimum probability, scaled by the probability of the most likely token. As a result, the filter is more aggressive given a very high-probability token. Args: logprobs: A vector of log probabilities. min_p (float): Minimum token probability. Typical values are in the 0.01-0.2 range, comparably selective as setting `top_p` in the 0.99-0.8 range. min_tokens_to_keep (int, optional): Minimum number of tokens that cannot be filtered. Default: ``1``. """ if not (0 <= min_p <= 1.0): raise ValueError( f"`min_p` has to be a float in the [0, 1] interval, but is {min_p}" ) if not isinstance(min_tokens_to_keep, int) or (min_tokens_to_keep < 1): raise ValueError( f"`min_tokens_to_keep` has to be a positive integer, but is {min_tokens_to_keep}" ) # Mask tokens that have a probability less than the max(p) * min_p top_logprobs = mx.max(logprobs, axis=-1, keepdims=True) scaled_min_p = top_logprobs + math.log(min_p) tokens_to_remove = logprobs < scaled_min_p # Ensure at least min_tokens_to_keep survive the filter if min_tokens_to_keep > 1: top_indices = mx.argpartition(logprobs, kth=-min_tokens_to_keep, axis=-1) top_indices = top_indices[..., -min_tokens_to_keep:] tokens_to_remove = mx.put_along_axis( tokens_to_remove, top_indices, mx.array(False), axis=-1, ) return mx.where(tokens_to_remove, -float("inf"), logprobs) @partial(mx.compile, inputs=mx.random.state, outputs=mx.random.state) def apply_top_p(logprobs: mx.array, top_p: float) -> mx.array: """ Apply top-p (nucleus) sampling to logits. Args: logprobs: A vector of log probabilities. top_p: The cumulative probability threshold for top-p filtering. Returns: token selected based on the top-p criterion. """ sorted_logprobs = mx.sort(logprobs, axis=-1) sorted_probs = mx.exp(sorted_logprobs.astype(mx.float32)) mass_above = mx.cumsum(sorted_probs, axis=-1, reverse=True, inclusive=False) total_mass = mass_above[..., :1] + sorted_probs[..., :1] num_dropped = (mass_above >= top_p * total_mass).sum(axis=-1, keepdims=True) threshold = mx.take_along_axis(sorted_logprobs, num_dropped, axis=-1) return mx.where(logprobs < threshold, -float("inf"), logprobs) @partial(mx.compile, inputs=mx.random.state, outputs=mx.random.state) def apply_xtc( logits: mx.array, xtc_probability: float, xtc_threshold: float, xtc_special_tokens: List[int], ) -> mx.array: """ Apply XTC sampling to the logits. Args: logits: The logits from the model's output. xtc_probability (float): Probability of XTC sampling to happen for each token xtc_threshold (float): The threshold the probs need to reach for being sampled. special_tokens_ids (list(int)): List of special tokens IDs to be excluded from XTC sampling. """ if not (0 <= xtc_threshold <= 0.5): raise ValueError( f"`threshold` has to be a float in the [0, 0.5] interval, but is {xtc_threshold}" ) if not (0 <= xtc_probability <= 1.0): raise ValueError( f"`probability` has to be a float in the [0, 1] interval, but is {xtc_probability}" ) probs = mx.softmax(logits, -1) mask = probs > mx.where(probs > xtc_threshold, probs, mx.inf).min( axis=-1, keepdims=True ) if xtc_special_tokens: mask[..., xtc_special_tokens] = False return mx.where( mx.random.uniform(0, 1) > xtc_probability, logits, mx.where(mask, -mx.inf, logits), ) @partial(mx.compile, inputs=mx.random.state, outputs=mx.random.state) def categorical_sampling(logits: mx.array, temp: float) -> mx.array: return mx.random.categorical(logits * (1.0 / temp)) def make_repetition_penalty(penalty: float, context_size: int = 20): """ Make repetition penalty processor. Paper: https://arxiv.org/abs/1909.05858 Args: penalty (float): The repetition penalty factor to be applied. context_size (int): The number of previous tokens to use. Default: ``20``. Returns: Callable[[mx.array, List[int]], mx.array]: The repetition penalty processor. """ if penalty < 0 or not isinstance(penalty, (int, float)): raise ValueError(f"penalty must be a non-negative float, got {penalty}") def repetition_penalty_processor(tokens, logits): if len(tokens) > 0: tokens = tokens[-context_size:] selected_logits = logits[:, tokens] selected_logits = mx.where( selected_logits < 0, selected_logits * penalty, selected_logits / penalty, ) logits[:, tokens] = selected_logits 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