← back to Exo
MLX LM type stubs
ff00b165c5ad87a29e769b58a28bc70df464c436 · 2025-11-06 13:59:29 -0800 · rltakashige
Files touched
M .github/scripts/bench.pyR100 typings/.gitkeep .mlx_typings/.gitkeepR099 typings/mlx/core/__init__.pyi .mlx_typings/mlx/core/__init__.pyiR100 typings/mlx/core/cuda/__init__.pyi .mlx_typings/mlx/core/cuda/__init__.pyiR100 typings/mlx/core/distributed/__init__.pyi .mlx_typings/mlx/core/distributed/__init__.pyiR100 typings/mlx/core/metal/__init__.pyi .mlx_typings/mlx/core/metal/__init__.pyiR100 typings/mlx/core/random/__init__.pyi .mlx_typings/mlx/core/random/__init__.pyiR100 typings/mlx/nn/__init__.pyi .mlx_typings/mlx/nn/__init__.pyiR100 typings/mlx/nn/init.pyi .mlx_typings/mlx/nn/init.pyiR100 typings/mlx/nn/layers/__init__.pyi .mlx_typings/mlx/nn/layers/__init__.pyiR100 typings/mlx/nn/layers/activations.pyi .mlx_typings/mlx/nn/layers/activations.pyiR100 typings/mlx/nn/layers/base.pyi .mlx_typings/mlx/nn/layers/base.pyiR100 typings/mlx/nn/layers/containers.pyi .mlx_typings/mlx/nn/layers/containers.pyiR100 typings/mlx/nn/layers/convolution.pyi .mlx_typings/mlx/nn/layers/convolution.pyiR100 typings/mlx/nn/layers/convolution_transpose.pyi .mlx_typings/mlx/nn/layers/convolution_transpose.pyiR100 typings/mlx/nn/layers/distributed.pyi .mlx_typings/mlx/nn/layers/distributed.pyiR100 typings/mlx/nn/layers/dropout.pyi .mlx_typings/mlx/nn/layers/dropout.pyiR100 typings/mlx/nn/layers/embedding.pyi .mlx_typings/mlx/nn/layers/embedding.pyiR100 typings/mlx/nn/layers/linear.pyi .mlx_typings/mlx/nn/layers/linear.pyiR100 typings/mlx/nn/layers/normalization.pyi .mlx_typings/mlx/nn/layers/normalization.pyiR100 typings/mlx/nn/layers/pooling.pyi .mlx_typings/mlx/nn/layers/pooling.pyiR100 typings/mlx/nn/layers/positional_encoding.pyi .mlx_typings/mlx/nn/layers/positional_encoding.pyiR100 typings/mlx/nn/layers/quantized.pyi .mlx_typings/mlx/nn/layers/quantized.pyiR100 typings/mlx/nn/layers/recurrent.pyi .mlx_typings/mlx/nn/layers/recurrent.pyiR100 typings/mlx/nn/layers/transformer.pyi .mlx_typings/mlx/nn/layers/transformer.pyiR100 typings/mlx/nn/layers/upsample.pyi .mlx_typings/mlx/nn/layers/upsample.pyiR100 typings/mlx/nn/losses.pyi .mlx_typings/mlx/nn/losses.pyiR100 typings/mlx/nn/utils.pyi .mlx_typings/mlx/nn/utils.pyiR100 typings/mlx/utils.pyi .mlx_typings/mlx/utils.pyiA .mlx_typings/mlx_lm/__init__.pyiA .mlx_typings/mlx_lm/convert.pyiA .mlx_typings/mlx_lm/generate.pyiA .mlx_typings/mlx_lm/models/__init__.pyiA .mlx_typings/mlx_lm/models/base.pyiA .mlx_typings/mlx_lm/models/bitlinear_layers.pyiA .mlx_typings/mlx_lm/models/cache.pyiA .mlx_typings/mlx_lm/models/switch_layers.pyiA .mlx_typings/mlx_lm/sample_utils.pyiA .mlx_typings/mlx_lm/tokenizer_utils.pyiA .mlx_typings/mlx_lm/utils.pyiM justfileM pyproject.tomlM src/exo/engines/mlx/utils_mlx.pyM src/exo/worker/download/impl_shard_downloader.pyM src/exo/worker/runner/generate.py
Diff
commit ff00b165c5ad87a29e769b58a28bc70df464c436
Author: rltakashige <rl.takashige@gmail.com>
Date: Thu Nov 6 13:59:29 2025 -0800
MLX LM type stubs
---
.github/scripts/bench.py | 1 -
{typings => .mlx_typings}/.gitkeep | 0
{typings => .mlx_typings}/mlx/core/__init__.pyi | 2 +-
.../mlx/core/cuda/__init__.pyi | 0
.../mlx/core/distributed/__init__.pyi | 0
.../mlx/core/metal/__init__.pyi | 0
.../mlx/core/random/__init__.pyi | 0
{typings => .mlx_typings}/mlx/nn/__init__.pyi | 0
{typings => .mlx_typings}/mlx/nn/init.pyi | 0
.../mlx/nn/layers/__init__.pyi | 0
.../mlx/nn/layers/activations.pyi | 0
{typings => .mlx_typings}/mlx/nn/layers/base.pyi | 0
.../mlx/nn/layers/containers.pyi | 0
.../mlx/nn/layers/convolution.pyi | 0
.../mlx/nn/layers/convolution_transpose.pyi | 0
.../mlx/nn/layers/distributed.pyi | 0
.../mlx/nn/layers/dropout.pyi | 0
.../mlx/nn/layers/embedding.pyi | 0
{typings => .mlx_typings}/mlx/nn/layers/linear.pyi | 0
.../mlx/nn/layers/normalization.pyi | 0
.../mlx/nn/layers/pooling.pyi | 0
.../mlx/nn/layers/positional_encoding.pyi | 0
.../mlx/nn/layers/quantized.pyi | 0
.../mlx/nn/layers/recurrent.pyi | 0
.../mlx/nn/layers/transformer.pyi | 0
.../mlx/nn/layers/upsample.pyi | 0
{typings => .mlx_typings}/mlx/nn/losses.pyi | 0
{typings => .mlx_typings}/mlx/nn/utils.pyi | 0
{typings => .mlx_typings}/mlx/utils.pyi | 0
.mlx_typings/mlx_lm/__init__.pyi | 2 +
.mlx_typings/mlx_lm/convert.pyi | 45 +++
.mlx_typings/mlx_lm/generate.pyi | 324 +++++++++++++++++++
.mlx_typings/mlx_lm/models/__init__.pyi | 1 +
.mlx_typings/mlx_lm/models/base.pyi | 47 +++
.mlx_typings/mlx_lm/models/bitlinear_layers.pyi | 26 ++
.mlx_typings/mlx_lm/models/cache.pyi | 354 +++++++++++++++++++++
.mlx_typings/mlx_lm/models/switch_layers.pyi | 79 +++++
.mlx_typings/mlx_lm/sample_utils.pyi | 148 +++++++++
.mlx_typings/mlx_lm/tokenizer_utils.pyi | 168 ++++++++++
.mlx_typings/mlx_lm/utils.pyi | 195 ++++++++++++
justfile | 2 +-
pyproject.toml | 6 +-
src/exo/engines/mlx/utils_mlx.py | 9 +-
src/exo/worker/download/impl_shard_downloader.py | 8 +-
src/exo/worker/runner/generate.py | 24 +-
45 files changed, 1417 insertions(+), 24 deletions(-)
diff --git a/.github/scripts/bench.py b/.github/scripts/bench.py
index 4f607b69..44733da1 100644
--- a/.github/scripts/bench.py
+++ b/.github/scripts/bench.py
@@ -1,6 +1,5 @@
#!/usr/bin/env python3
-# type: ignore
"""
Unified benchmark script for EXO.
Runs single or multi-stage benchmarks with configurable load patterns.
diff --git a/typings/.gitkeep b/.mlx_typings/.gitkeep
similarity index 100%
rename from typings/.gitkeep
rename to .mlx_typings/.gitkeep
diff --git a/typings/mlx/core/__init__.pyi b/.mlx_typings/mlx/core/__init__.pyi
similarity index 99%
rename from typings/mlx/core/__init__.pyi
rename to .mlx_typings/mlx/core/__init__.pyi
index 8edb9832..48680a80 100644
--- a/typings/mlx/core/__init__.pyi
+++ b/.mlx_typings/mlx/core/__init__.pyi
@@ -2614,7 +2614,7 @@ type MX_ARRAY_TREE = (
| Mapping[str, MX_ARRAY_TREE]
)
-def eval(*args: MX_ARRAY_TREE) -> None:
+def eval(*args: MX_ARRAY_TREE | None) -> None:
"""
Evaluate an :class:`array` or tree of :class:`array`.
diff --git a/typings/mlx/core/cuda/__init__.pyi b/.mlx_typings/mlx/core/cuda/__init__.pyi
similarity index 100%
rename from typings/mlx/core/cuda/__init__.pyi
rename to .mlx_typings/mlx/core/cuda/__init__.pyi
diff --git a/typings/mlx/core/distributed/__init__.pyi b/.mlx_typings/mlx/core/distributed/__init__.pyi
similarity index 100%
rename from typings/mlx/core/distributed/__init__.pyi
rename to .mlx_typings/mlx/core/distributed/__init__.pyi
diff --git a/typings/mlx/core/metal/__init__.pyi b/.mlx_typings/mlx/core/metal/__init__.pyi
similarity index 100%
rename from typings/mlx/core/metal/__init__.pyi
rename to .mlx_typings/mlx/core/metal/__init__.pyi
diff --git a/typings/mlx/core/random/__init__.pyi b/.mlx_typings/mlx/core/random/__init__.pyi
similarity index 100%
rename from typings/mlx/core/random/__init__.pyi
rename to .mlx_typings/mlx/core/random/__init__.pyi
diff --git a/typings/mlx/nn/__init__.pyi b/.mlx_typings/mlx/nn/__init__.pyi
similarity index 100%
rename from typings/mlx/nn/__init__.pyi
rename to .mlx_typings/mlx/nn/__init__.pyi
diff --git a/typings/mlx/nn/init.pyi b/.mlx_typings/mlx/nn/init.pyi
similarity index 100%
rename from typings/mlx/nn/init.pyi
rename to .mlx_typings/mlx/nn/init.pyi
diff --git a/typings/mlx/nn/layers/__init__.pyi b/.mlx_typings/mlx/nn/layers/__init__.pyi
similarity index 100%
rename from typings/mlx/nn/layers/__init__.pyi
rename to .mlx_typings/mlx/nn/layers/__init__.pyi
diff --git a/typings/mlx/nn/layers/activations.pyi b/.mlx_typings/mlx/nn/layers/activations.pyi
similarity index 100%
rename from typings/mlx/nn/layers/activations.pyi
rename to .mlx_typings/mlx/nn/layers/activations.pyi
diff --git a/typings/mlx/nn/layers/base.pyi b/.mlx_typings/mlx/nn/layers/base.pyi
similarity index 100%
rename from typings/mlx/nn/layers/base.pyi
rename to .mlx_typings/mlx/nn/layers/base.pyi
diff --git a/typings/mlx/nn/layers/containers.pyi b/.mlx_typings/mlx/nn/layers/containers.pyi
similarity index 100%
rename from typings/mlx/nn/layers/containers.pyi
rename to .mlx_typings/mlx/nn/layers/containers.pyi
diff --git a/typings/mlx/nn/layers/convolution.pyi b/.mlx_typings/mlx/nn/layers/convolution.pyi
similarity index 100%
rename from typings/mlx/nn/layers/convolution.pyi
rename to .mlx_typings/mlx/nn/layers/convolution.pyi
diff --git a/typings/mlx/nn/layers/convolution_transpose.pyi b/.mlx_typings/mlx/nn/layers/convolution_transpose.pyi
similarity index 100%
rename from typings/mlx/nn/layers/convolution_transpose.pyi
rename to .mlx_typings/mlx/nn/layers/convolution_transpose.pyi
diff --git a/typings/mlx/nn/layers/distributed.pyi b/.mlx_typings/mlx/nn/layers/distributed.pyi
similarity index 100%
rename from typings/mlx/nn/layers/distributed.pyi
rename to .mlx_typings/mlx/nn/layers/distributed.pyi
diff --git a/typings/mlx/nn/layers/dropout.pyi b/.mlx_typings/mlx/nn/layers/dropout.pyi
similarity index 100%
rename from typings/mlx/nn/layers/dropout.pyi
rename to .mlx_typings/mlx/nn/layers/dropout.pyi
diff --git a/typings/mlx/nn/layers/embedding.pyi b/.mlx_typings/mlx/nn/layers/embedding.pyi
similarity index 100%
rename from typings/mlx/nn/layers/embedding.pyi
rename to .mlx_typings/mlx/nn/layers/embedding.pyi
diff --git a/typings/mlx/nn/layers/linear.pyi b/.mlx_typings/mlx/nn/layers/linear.pyi
similarity index 100%
rename from typings/mlx/nn/layers/linear.pyi
rename to .mlx_typings/mlx/nn/layers/linear.pyi
diff --git a/typings/mlx/nn/layers/normalization.pyi b/.mlx_typings/mlx/nn/layers/normalization.pyi
similarity index 100%
rename from typings/mlx/nn/layers/normalization.pyi
rename to .mlx_typings/mlx/nn/layers/normalization.pyi
diff --git a/typings/mlx/nn/layers/pooling.pyi b/.mlx_typings/mlx/nn/layers/pooling.pyi
similarity index 100%
rename from typings/mlx/nn/layers/pooling.pyi
rename to .mlx_typings/mlx/nn/layers/pooling.pyi
diff --git a/typings/mlx/nn/layers/positional_encoding.pyi b/.mlx_typings/mlx/nn/layers/positional_encoding.pyi
similarity index 100%
rename from typings/mlx/nn/layers/positional_encoding.pyi
rename to .mlx_typings/mlx/nn/layers/positional_encoding.pyi
diff --git a/typings/mlx/nn/layers/quantized.pyi b/.mlx_typings/mlx/nn/layers/quantized.pyi
similarity index 100%
rename from typings/mlx/nn/layers/quantized.pyi
rename to .mlx_typings/mlx/nn/layers/quantized.pyi
diff --git a/typings/mlx/nn/layers/recurrent.pyi b/.mlx_typings/mlx/nn/layers/recurrent.pyi
similarity index 100%
rename from typings/mlx/nn/layers/recurrent.pyi
rename to .mlx_typings/mlx/nn/layers/recurrent.pyi
diff --git a/typings/mlx/nn/layers/transformer.pyi b/.mlx_typings/mlx/nn/layers/transformer.pyi
similarity index 100%
rename from typings/mlx/nn/layers/transformer.pyi
rename to .mlx_typings/mlx/nn/layers/transformer.pyi
diff --git a/typings/mlx/nn/layers/upsample.pyi b/.mlx_typings/mlx/nn/layers/upsample.pyi
similarity index 100%
rename from typings/mlx/nn/layers/upsample.pyi
rename to .mlx_typings/mlx/nn/layers/upsample.pyi
diff --git a/typings/mlx/nn/losses.pyi b/.mlx_typings/mlx/nn/losses.pyi
similarity index 100%
rename from typings/mlx/nn/losses.pyi
rename to .mlx_typings/mlx/nn/losses.pyi
diff --git a/typings/mlx/nn/utils.pyi b/.mlx_typings/mlx/nn/utils.pyi
similarity index 100%
rename from typings/mlx/nn/utils.pyi
rename to .mlx_typings/mlx/nn/utils.pyi
diff --git a/typings/mlx/utils.pyi b/.mlx_typings/mlx/utils.pyi
similarity index 100%
rename from typings/mlx/utils.pyi
rename to .mlx_typings/mlx/utils.pyi
diff --git a/.mlx_typings/mlx_lm/__init__.pyi b/.mlx_typings/mlx_lm/__init__.pyi
new file mode 100644
index 00000000..fee89807
--- /dev/null
+++ b/.mlx_typings/mlx_lm/__init__.pyi
@@ -0,0 +1,2 @@
+import models as models
+import tokenizer_utils as tokenizer_utils
diff --git a/.mlx_typings/mlx_lm/convert.pyi b/.mlx_typings/mlx_lm/convert.pyi
new file mode 100644
index 00000000..aff4de7b
--- /dev/null
+++ b/.mlx_typings/mlx_lm/convert.pyi
@@ -0,0 +1,45 @@
+"""
+This type stub file was generated by pyright.
+"""
+
+import argparse
+from typing import Callable, Optional, Union
+
+import mlx.nn as nn
+
+def mixed_quant_predicate_builder(
+ recipe: str, model: nn.Module, group_size: int = ...
+) -> Callable[[str, nn.Module, dict], Union[bool, dict]]: ...
+
+QUANT_RECIPES = ...
+MODEL_CONVERSION_DTYPES = ...
+
+def convert(
+ hf_path: str,
+ mlx_path: str = ...,
+ quantize: bool = ...,
+ q_group_size: int = ...,
+ q_bits: int = ...,
+ q_mode: str = ...,
+ dtype: Optional[str] = ...,
+ upload_repo: str = ...,
+ revision: Optional[str] = ...,
+ dequantize: bool = ...,
+ quant_predicate: Optional[
+ Union[Callable[[str, nn.Module, dict], Union[bool, dict]], str]
+ ] = ...,
+ trust_remote_code: bool = ...,
+): # -> None:
+ ...
+def configure_parser() -> argparse.ArgumentParser:
+ """
+ Configures and returns the argument parser for the script.
+
+ Returns:
+ argparse.ArgumentParser: Configured argument parser.
+ """
+
+def main(): # -> None:
+ ...
+
+if __name__ == "__main__": ...
diff --git a/.mlx_typings/mlx_lm/generate.pyi b/.mlx_typings/mlx_lm/generate.pyi
new file mode 100644
index 00000000..4711fce0
--- /dev/null
+++ b/.mlx_typings/mlx_lm/generate.pyi
@@ -0,0 +1,324 @@
+"""
+This type stub file was generated by pyright.
+"""
+
+import contextlib
+from dataclasses import dataclass
+from typing import Any, Callable, Generator, List, Optional, Tuple, Union
+
+import mlx.core as mx
+import mlx.nn as nn
+from transformers import PreTrainedTokenizer
+
+from .tokenizer_utils import TokenizerWrapper
+
+DEFAULT_PROMPT = ...
+DEFAULT_MAX_TOKENS = ...
+DEFAULT_TEMP = ...
+DEFAULT_TOP_P = ...
+DEFAULT_MIN_P = ...
+DEFAULT_TOP_K = ...
+DEFAULT_XTC_PROBABILITY = ...
+DEFAULT_XTC_THRESHOLD = ...
+DEFAULT_MIN_TOKENS_TO_KEEP = ...
+DEFAULT_SEED = ...
+DEFAULT_MODEL = ...
+DEFAULT_QUANTIZED_KV_START = ...
+
+def str2bool(string): # -> bool:
+ ...
+def setup_arg_parser(): # -> ArgumentParser:
+ """Set up and return the argument parser."""
+
+generation_stream = ...
+
+@contextlib.contextmanager
+def wired_limit(
+ model: nn.Module, streams: Optional[List[mx.Stream]] = ...
+): # -> Generator[None, Any, None]:
+ """
+ A context manager to temporarily change the wired limit.
+
+ Note, the wired limit should not be changed during an async eval. If an
+ async eval could be running pass in the streams to synchronize with prior
+ to exiting the context manager.
+ """
+@dataclass
+class GenerationResponse:
+ """
+ The output of :func:`stream_generate`.
+
+ Args:
+ text (str): The next segment of decoded text. This can be an empty string.
+ token (int): The next token.
+ from_draft (bool): Whether the token was generated by the draft model.
+ logprobs (mx.array): A vector of log probabilities.
+ prompt_tokens (int): The number of tokens in the prompt.
+ prompt_tps (float): The prompt processing tokens-per-second.
+ generation_tokens (int): The number of generated tokens.
+ generation_tps (float): The tokens-per-second for generation.
+ peak_memory (float): The peak memory used so far in GB.
+ finish_reason (str): The reason the response is being sent: "length", "stop" or `None`
+ """
+
+ text: str
+ token: int
+ logprobs: mx.array
+ from_draft: bool
+ prompt_tokens: int
+ prompt_tps: float
+ generation_tokens: int
+ generation_tps: float
+ peak_memory: float
+ finish_reason: Optional[str] = ...
+
+def maybe_quantize_kv_cache(
+ prompt_cache, quantized_kv_start, kv_group_size, kv_bits
+): # -> None:
+ ...
+def generate_step(
+ prompt: mx.array,
+ model: nn.Module,
+ *,
+ max_tokens: int = ...,
+ sampler: Optional[Callable[[mx.array], mx.array]] = ...,
+ logits_processors: Optional[List[Callable[[mx.array, mx.array], mx.array]]] = ...,
+ max_kv_size: Optional[int] = ...,
+ prompt_cache: Optional[Any] = ...,
+ prefill_step_size: int = ...,
+ kv_bits: Optional[int] = ...,
+ kv_group_size: int = ...,
+ quantized_kv_start: int = ...,
+ prompt_progress_callback: Optional[Callable[[int], int]] = ...,
+ input_embeddings: Optional[mx.array] = ...,
+) -> Generator[Tuple[mx.array, mx.array], None, None]:
+ """
+ A generator producing token ids based on the given prompt from the model.
+
+ Args:
+ prompt (mx.array): The input prompt.
+ model (nn.Module): The model to use for generation.
+ max_tokens (int): The maximum number of tokens. Use``-1`` for an infinite
+ generator. Default: ``256``.
+ sampler (Callable[mx.array, mx.array], optional): A sampler for sampling a
+ token from a vector of log probabilities. Default: ``None``.
+ logits_processors (List[Callable[[mx.array, mx.array], mx.array]], optional):
+ A list of functions that take tokens and logits and return the processed
+ logits. Default: ``None``.
+ max_kv_size (int, optional): Maximum size of the key-value cache. Old
+ entries (except the first 4 tokens) will be overwritten.
+ prompt_cache (List[Any], optional): A pre-computed prompt cache. Note, if
+ provided, the cache will be updated in place.
+ prefill_step_size (int): Step size for processing the prompt.
+ kv_bits (int, optional): Number of bits to use for KV cache quantization.
+ None implies no cache quantization. Default: ``None``.
+ kv_group_size (int): Group size for KV cache quantization. Default: ``64``.
+ quantized_kv_start (int): Step to begin using a quantized KV cache.
+ when ``kv_bits`` is non-None. Default: ``0``.
+ prompt_progress_callback (Callable[[int], int]): A call-back which takes the
+ prompt tokens processed so far and the total number of prompt tokens.
+ input_embeddings (mx.array, optional): Input embeddings to use instead of or in
+ conjunction with prompt tokens. Default: ``None``.
+
+ Yields:
+ Tuple[mx.array, mx.array]: One token and a vector of log probabilities.
+ """
+
+def speculative_generate_step(
+ prompt: mx.array,
+ model: nn.Module,
+ draft_model: nn.Module,
+ *,
+ num_draft_tokens: int = ...,
+ max_tokens: int = ...,
+ sampler: Optional[Callable[[mx.array], mx.array]] = ...,
+ logits_processors: Optional[List[Callable[[mx.array, mx.array], mx.array]]] = ...,
+ prompt_cache: Optional[Any] = ...,
+ prefill_step_size: int = ...,
+ kv_bits: Optional[int] = ...,
+ kv_group_size: int = ...,
+ quantized_kv_start: int = ...,
+) -> Generator[Tuple[mx.array, mx.array, bool], None, None]:
+ """
+ A generator producing token ids based on the given prompt from the model.
+
+ Args:
+ prompt (mx.array): The input prompt.
+ model (nn.Module): The model to use for generation.
+ draft_model (nn.Module): The draft model for speculative decoding.
+ num_draft_tokens (int, optional): The number of draft tokens for
+ speculative decoding. Default: ``2``.
+ max_tokens (int): The maximum number of tokens. Use``-1`` for an infinite
+ generator. Default: ``256``.
+ sampler (Callable[[mx.array], mx.array], optional): A sampler for sampling a
+ token from a vector of log probabilities. Default: ``None``.
+ logits_processors (List[Callable[[mx.array, mx.array], mx.array]], optional):
+ A list of functions that take tokens and logits and return the processed
+ logits. Default: ``None``.
+ prompt_cache (List[Any], optional): A pre-computed prompt cache. Note, if
+ provided, the cache will be updated in place. The cache must be trimmable.
+ prefill_step_size (int): Step size for processing the prompt.
+ kv_bits (int, optional): Number of bits to use for KV cache quantization.
+ None implies no cache quantization. Default: ``None``.
+ kv_group_size (int): Group size for KV cache quantization. Default: ``64``.
+ quantized_kv_start (int): Step to begin using a quantized KV cache.
+ when ``kv_bits`` is non-None. Default: ``0``.
+
+ Yields:
+ Tuple[mx.array, mx.array, bool]: One token, a vector of log probabilities,
+ and a bool indicating if the token was generated by the draft model
+ """
+
+def stream_generate(
+ model: nn.Module,
+ tokenizer: Union[PreTrainedTokenizer, TokenizerWrapper],
+ prompt: Union[str, mx.array, List[int]],
+ max_tokens: int = ...,
+ draft_model: Optional[nn.Module] = ...,
+ **kwargs,
+) -> Generator[GenerationResponse, None, None]:
+ """
+ A generator producing text based on the given prompt from the model.
+
+ Args:
+ model (nn.Module): The model to use for generation.
+ tokenizer (PreTrainedTokenizer): The tokenizer.
+ prompt (Union[str, mx.array, List[int]]): The input prompt string or
+ integer tokens.
+ max_tokens (int): The maximum number of tokens to generate.
+ Default: ``256``.
+ draft_model (Optional[nn.Module]): An optional draft model. If provided
+ then speculative decoding is used. The draft model must use the same
+ tokenizer as the main model. Default: ``None``.
+ kwargs: The remaining options get passed to :func:`generate_step`.
+ See :func:`generate_step` for more details.
+
+ Yields:
+ GenerationResponse: An instance containing the generated text segment and
+ associated metadata. See :class:`GenerationResponse` for details.
+ """
+
+def generate(
+ model: nn.Module,
+ tokenizer: Union[PreTrainedTokenizer, TokenizerWrapper],
+ prompt: Union[str, List[int]],
+ verbose: bool = ...,
+ **kwargs,
+) -> str:
+ """
+ Generate a complete response from the model.
+
+ Args:
+ model (nn.Module): The language model.
+ tokenizer (PreTrainedTokenizer): The tokenizer.
+ prompt (Union[str, List[int]]): The input prompt string or integer tokens.
+ verbose (bool): If ``True``, print tokens and timing information.
+ Default: ``False``.
+ kwargs: The remaining options get passed to :func:`stream_generate`.
+ See :func:`stream_generate` for more details.
+ """
+@dataclass
+class BatchStats:
+ """
+ An data object to hold generation stats.
+
+ Args:
+ prompt_tokens (int): The number of prompt tokens processed.
+ prompt_tps (float): The prompt processing tokens-per-second.
+ prompt_time (float): The time in seconds spent in prompt processing.
+ generation_tokens (int): The number of generated tokens.
+ generation_tps (float): The tokens-per-second for generation.
+ generation_time (float): The time in seconds spent in generation .
+ peak_memory (float): The peak memory used so far in GB.
+ """
+
+ prompt_tokens: int = ...
+ prompt_tps: float = ...
+ prompt_time: float = ...
+ generation_tokens: int = ...
+ generation_tps: float = ...
+ generation_time: float = ...
+ peak_memory: float = ...
+
+@dataclass
+class BatchResponse:
+ """
+ An data object to hold a batch generation response.
+
+ Args:
+ texts: (List[str]): The generated text for each prompt.
+ stats (BatchStats): Statistics about the generation.
+ """
+
+ texts: List[str]
+ stats: BatchStats
+
+@dataclass
+class Batch:
+ uids: List[int]
+ y: mx.array
+ logprobs: mx.array
+ max_tokens: List[int]
+ num_tokens: List[int]
+ cache: List[Any]
+ def __len__(self): # -> int:
+ ...
+ def filter(self, keep_idx: List[int]): # -> None:
+ ...
+ def extend(self, other): # -> None:
+ ...
+
+class BatchGenerator:
+ @dataclass
+ class Response:
+ uid: int
+ token: int
+ logprobs: mx.array
+ finish_reason: Optional[str]
+
+ def __init__(
+ self,
+ model,
+ max_tokens: int = ...,
+ stop_tokens: Optional[set] = ...,
+ sampler: Optional[Callable[[mx.array], mx.array]] = ...,
+ completion_batch_size: int = ...,
+ prefill_batch_size: int = ...,
+ prefill_step_size: int = ...,
+ ) -> None: ...
+ def insert(
+ self, prompts, max_tokens: Union[List[int], int, None] = ...
+ ): # -> list[Any]:
+ ...
+ def stats(self): # -> BatchStats:
+ ...
+ def next(self): # -> list[Any]:
+ ...
+
+def batch_generate(
+ model,
+ tokenizer,
+ prompts: List[int],
+ max_tokens: Union[int, List[int]] = ...,
+ verbose: bool = ...,
+ **kwargs,
+) -> BatchResponse:
+ """
+ Generate responses for the given batch of prompts.
+
+ Args:
+ model (nn.Module): The language model.
+ tokenizer (PreTrainedTokenizer): The tokenizer.
+ prompt (List[List[int]]): The input prompts.
+ verbose (bool): If ``True``, print tokens and timing information.
+ Default: ``False``.
+ max_tokens (Union[int, List[int]): Maximum number of output tokens. This
+ can be per prompt if a list is provided.
+ kwargs: The remaining options get passed to :obj:`BatchGenerator`.
+ See :obj:`BatchGenerator` for more details.
+ """
+
+def main(): # -> None:
+ ...
+
+if __name__ == "__main__": ...
diff --git a/.mlx_typings/mlx_lm/models/__init__.pyi b/.mlx_typings/mlx_lm/models/__init__.pyi
new file mode 100644
index 00000000..e09bd4fc
--- /dev/null
+++ b/.mlx_typings/mlx_lm/models/__init__.pyi
@@ -0,0 +1 @@
+import cache as cache
diff --git a/.mlx_typings/mlx_lm/models/base.pyi b/.mlx_typings/mlx_lm/models/base.pyi
new file mode 100644
index 00000000..e549e624
--- /dev/null
+++ b/.mlx_typings/mlx_lm/models/base.pyi
@@ -0,0 +1,47 @@
+"""
+This type stub file was generated by pyright.
+"""
+
+from dataclasses import dataclass
+from typing import Optional
+
+import mlx.core as mx
+
+@dataclass
+class BaseModelArgs:
+ @classmethod
+ def from_dict(cls, params): # -> Self:
+ ...
+
+def create_causal_mask(
+ N: int,
+ offset: int = ...,
+ window_size: Optional[int] = ...,
+ right_padding: Optional[mx.array] = ...,
+ left_padding: Optional[mx.array] = ...,
+): # -> array:
+ ...
+def create_attention_mask(
+ h, cache=..., window_size: Optional[int] = ..., return_array: bool = ...
+): # -> array | Literal['causal'] | None:
+ ...
+def create_ssm_mask(h, cache=...): # -> None:
+ ...
+def quantized_scaled_dot_product_attention(
+ queries: mx.array,
+ q_keys: tuple[mx.array, mx.array, mx.array],
+ q_values: tuple[mx.array, mx.array, mx.array],
+ scale: float,
+ mask: Optional[mx.array],
+ group_size: int = ...,
+ bits: int = ...,
+) -> mx.array: ...
+def scaled_dot_product_attention(
+ queries,
+ keys,
+ values,
+ cache,
+ scale: float,
+ mask: Optional[mx.array],
+ sinks: Optional[mx.array] = ...,
+) -> mx.array: ...
diff --git a/.mlx_typings/mlx_lm/models/bitlinear_layers.pyi b/.mlx_typings/mlx_lm/models/bitlinear_layers.pyi
new file mode 100644
index 00000000..fa1caa82
--- /dev/null
+++ b/.mlx_typings/mlx_lm/models/bitlinear_layers.pyi
@@ -0,0 +1,26 @@
+"""
+This type stub file was generated by pyright.
+"""
+
+import mlx.nn as nn
+
+def bitnet_quantize(model, quantization_config: dict): ...
+def make_bitlinear_kernel():
+ """
+ Custom Metal kernel that performs matrix multiplication directly on
+ packed weights and scales the output. This eliminates the need to
+ store unpacked weights in memory.
+ """
+
+_bitlinear_kernel = ...
+
+class BitLinear(nn.Module):
+ """
+ BitLinear module with memory-efficient weight handling.
+ """
+ def __init__(
+ self, in_features, out_features, bias=..., invert_weight_scales=...
+ ) -> None: ...
+ def execute_matmul_kernel(self, x, packed_weights): ...
+ def __call__(self, x): # -> array:
+ ...
diff --git a/.mlx_typings/mlx_lm/models/cache.pyi b/.mlx_typings/mlx_lm/models/cache.pyi
new file mode 100644
index 00000000..30fe1b85
--- /dev/null
+++ b/.mlx_typings/mlx_lm/models/cache.pyi
@@ -0,0 +1,354 @@
+"""
+This type stub file was generated by pyright.
+"""
+
+from typing import Any, Dict, List, Optional
+
+import mlx.nn as nn
+from mlx.core import array
+
+def make_prompt_cache(
+ model: nn.Module, max_kv_size: Optional[int] = ...
+) -> List[KVCache | Any]:
+ """
+ Construct the model's cache for use in generation.
+
+ This function will defer the cache construction to the model if it has a
+ ``make_cache`` method, otherwise it will make a default KV cache.
+
+ Args:
+ model (nn.Module): The language model.
+ max_kv_size (Optional[int]): If provided and the model does not have a
+ ``make_cache`` method, a ``RotatingKVCache`` is used with a maximum
+ size of ``max_kv_size``
+ """
+
+def save_prompt_cache(
+ file_name: str, cache: List[Any], metadata: Dict[str, str] = ...
+) -> None:
+ """
+ Save a pre-computed prompt cache to a file.
+
+ Args:
+ file_name (str): The ``.safetensors`` file name.
+ cache (List[Any]): The model state.
+ metadata (Dict[str, str]): Optional metadata to save along with model
+ state.
+ """
+
+def load_prompt_cache(
+ file_name, return_metadata=...
+): # -> tuple[list[Any], Any] | list[Any]:
+ """
+ Load a prompt cache from a file.
+
+ Args:
+ file_name (str): The ``.safetensors`` file name.
+ return_metadata (bool): Whether or not to return metadata.
+ Default: ``False``.
+
+ Returns:
+ List[Any] or Tuple[List[Any], Dict[str, str]]: The prompt cache and
+ the metadata if requested.
+ """
+
+def can_trim_prompt_cache(cache: List[Any]) -> bool:
+ """
+ Check if model's cache can be trimmed.
+ """
+
+def trim_prompt_cache(cache: List[Any], num_tokens: int) -> List[Any]:
+ """
+ Trim the model's cache by the given number of tokens.
+
+ This function will trim the cache if possible (in-place) and return the
+ number of tokens that were trimmed.
+
+ Args:
+ cache (List[Any]): The model's cache.
+ num_tokens (int): The number of tokens to trim.
+
+ Returns:
+ (int): The number of tokens that were trimmed.
+ """
+
+def create_attention_mask(
+ N: int, offset: int, return_array: bool, window_size: Optional[int]
+): # -> array | Literal['causal'] | None:
+ ...
+
+class _BaseCache:
+ @property
+ def state(self): # -> list[Any]:
+ ...
+ @state.setter
+ def state(self, v): # -> None:
+ ...
+ @property
+ def meta_state(self): # -> Literal['']:
+ ...
+ @meta_state.setter
+ def meta_state(self, v): # -> None:
+ ...
+ def is_trimmable(self): # -> Literal[False]:
+ ...
+ @classmethod
+ def from_state(cls, state, meta_state): # -> Self:
+ ...
+
+class ConcatenateKVCache(_BaseCache):
+ """ConcatenateKVCache the simplest KV cache implementation.
+
+ Can be used as a mock KV cache or when large blocks are being processed at
+ a time in which case KVCache isn't necessarily faster. Consider using the
+ KVCache with a larger step size before using this cache.
+ """
+ def __init__(self) -> None: ...
+ def update_and_fetch(self, keys, values): # -> tuple[Any | array, Any | array]:
+ ...
+ @property
+ def state(self): # -> tuple[Any | array | None, Any | array | None]:
+ ...
+ @state.setter
+ def state(self, v): # -> None:
+ ...
+ def is_trimmable(self): # -> Literal[True]:
+ ...
+ def trim(self, n): # -> int:
+ ...
+ def make_mask(self, *args, **kwargs): # -> array | Literal['causal'] | None:
+ ...
+
+class QuantizedKVCache(_BaseCache):
+ step = ...
+ def __init__(self, group_size: int = ..., bits: int = ...) -> None: ...
+ def update_and_fetch(self, keys, values): # -> Any:
+ ...
+ @property
+ def state(
+ self,
+ ): # -> tuple[Any | tuple[array, array, array] | None, Any | tuple[array, array, array] | None] | Any:
+ ...
+ @state.setter
+ def state(self, v): # -> None:
+ ...
+ @property
+ def meta_state(self): # -> tuple[str, ...]:
+ ...
+ @meta_state.setter
+ def meta_state(self, v): # -> None:
+ ...
+ def is_trimmable(self): # -> Literal[True]:
+ ...
+ def trim(self, n): # -> int:
+ ...
+ def make_mask(self, *args, **kwargs): # -> array | Literal['causal'] | None:
+ ...
+
+class KVCache(_BaseCache):
+ step = ...
+ def __init__(self) -> None: ...
+ def update_and_fetch(self, keys, values): # -> tuple[array | Any, array | Any]:
+ ...
+ @property
+ def state(
+ self,
+ ) -> tuple[array, array]: ...
+ @state.setter
+ def state(self, v) -> None: ...
+ def is_trimmable(self): # -> Literal[True]:
+ ...
+ def trim(self, n): # -> int:
+ ...
+ def to_quantized(
+ self, group_size: int = ..., bits: int = ...
+ ) -> QuantizedKVCache: ...
+ def make_mask(self, *args, **kwargs): # -> array | Literal['causal'] | None:
+ ...
+
+class RotatingKVCache(_BaseCache):
+ step = ...
+ def __init__(self, max_size, keep=...) -> None: ...
+ def update_and_fetch(
+ self, keys, values
+ ): # -> tuple[array | Any, array | Any] | tuple[array | Any, array | Any | None]:
+ ...
+ @property
+ def state(
+ self,
+ ): # -> tuple[Any | array, Any | array] | tuple[Any | array | None, Any | array | None]:
+ ...
+ @state.setter
+ def state(self, v): # -> None:
+ ...
+ @property
+ def meta_state(self): # -> tuple[str, ...]:
+ ...
+ @meta_state.setter
+ def meta_state(self, v): # -> None:
+ ...
+ def is_trimmable(self): # -> bool:
+ ...
+ def trim(self, n): # -> int:
+ ...
+ def to_quantized(
+ self, group_size: int = ..., bits: int = ...
+ ) -> QuantizedKVCache: ...
+ def make_mask(
+ self, N: int, window_size: Optional[int] = ..., return_array: bool = ...
+ ): # -> array | Literal['causal'] | None:
+ ...
+
+class ArraysCache(_BaseCache):
+ def __init__(self, size, left_padding: Optional[List[int]] = ...) -> None: ...
+ def __setitem__(self, idx, value): # -> None:
+ ...
+ def __getitem__(self, idx): ...
+ @property
+ def state(self): # -> list[Any | array] | list[array]:
+ ...
+ @state.setter
+ def state(self, v): # -> None:
+ ...
+ def filter(self, batch_indices): # -> None:
+ """
+ In-place filter to keep just the given indices in the cache.
+ """
+
+ def extend(self, other): # -> None:
+ """
+ In-place extend this cache with the other cache.
+ """
+
+ def make_mask(self, N: int): # -> array | None:
+ ...
+
+class MambaCache(ArraysCache):
+ def __init__(self, left_padding: Optional[List[int]] = ...) -> None: ...
+
+class ChunkedKVCache(KVCache):
+ def __init__(self, chunk_size) -> None: ...
+ def maybe_trim_front(self): # -> None:
+ ...
+ def update_and_fetch(self, keys, values): # -> tuple[array, array]:
+ ...
+ def trim(self, n): # -> int:
+ ...
+ @property
+ def meta_state(self): # -> tuple[str, ...]:
+ ...
+ @meta_state.setter
+ def meta_state(self, v): # -> None:
+ ...
+
+class CacheList(_BaseCache):
+ def __init__(self, *caches) -> None: ...
+ def __getitem__(self, idx): ...
+ def is_trimmable(self): # -> bool:
+ ...
+ def trim(self, n): ...
+ @property
+ def state(self): # -> list[Any]:
+ ...
+ @state.setter
+ def state(self, v): # -> None:
+ ...
+ def filter(self, batch_indices): # -> None:
+ """
+ In-place filter to keep just the given indices in the cache.
+ """
+
+ def extend(self, other): # -> None:
+ """
+ In-place extend this cache with the other cache.
+ """
+
+class BatchKVCache(_BaseCache):
+ step = ...
+ def __init__(self, left_padding: List[int]) -> None:
+ """
+ The BatchKV cache expects inputs to be left-padded.
+
+ E.g. the following prompts:
+
+ [1, 3, 5]
+ [7]
+ [2, 6, 8, 9]
+
+ Should be padded like so:
+
+ [0, 1, 3, 5]
+ [0, 0, 0, 7]
+ [2, 6, 8, 9]
+
+ And ``left_padding`` specifies the amount of padding for each.
+ In this case, ``left_padding = [1, 3, 0]``.
+ """
+
+ def update_and_fetch(self, keys, values): # -> tuple[array | Any, array | Any]:
+ ...
+ @property
+ def state(
+ self,
+ ): # -> tuple[Any | array | None, Any | array | None, array | Any, array | Any]:
+ ...
+ @state.setter
+ def state(self, v): # -> None:
+ ...
+ def is_trimmable(self): # -> Literal[True]:
+ ...
+ def trim(self, n): # -> int | float:
+ ...
+ def make_mask(self, N: int, return_array: bool = ..., **kwargs): # -> array:
+ ...
+ def filter(self, batch_indices): # -> None:
+ """
+ In-place filter to keep just the given indices in the cache.
+ """
+
+ def extend(self, other): # -> None:
+ """
+ In-place extend this cache with the other cache.
+ """
+
+class BatchRotatingKVCache(_BaseCache):
+ step = ...
+ def __init__(self, max_size, left_padding: List[int]) -> None: ...
+ def update_and_fetch(
+ self, keys, values
+ ): # -> tuple[array | Any, array | Any] | tuple[array | Any, array | Any | None]:
+ ...
+ @property
+ def state(
+ self,
+ ): # -> tuple[Any | array | None, Any | array | None, array | Any, array | Any]:
+ ...
+ @state.setter
+ def state(self, v): # -> None:
+ ...
+ @property
+ def meta_state(self): # -> tuple[str, ...]:
+ ...
+ @meta_state.setter
+ def meta_state(self, v): # -> None:
+ ...
+ def is_trimmable(self): # -> bool:
+ ...
+ def trim(self, n): # -> int:
+ ...
+ def to_quantized(
+ self, group_size: int = ..., bits: int = ...
+ ) -> QuantizedKVCache: ...
+ def make_mask(
+ self, N: int, window_size: Optional[int] = ..., return_array: bool = ...
+ ): # -> array:
+ ...
+ def filter(self, batch_indices): # -> None:
+ """
+ In-place filter to keep just the given indices in the cache.
+ """
+
+ def extend(self, other): # -> None:
+ """
+ In-place extend this cache with the other cache.
+ """
diff --git a/.mlx_typings/mlx_lm/models/switch_layers.pyi b/.mlx_typings/mlx_lm/models/switch_layers.pyi
new file mode 100644
index 00000000..c50c999a
--- /dev/null
+++ b/.mlx_typings/mlx_lm/models/switch_layers.pyi
@@ -0,0 +1,79 @@
+"""
+This type stub file was generated by pyright.
+"""
+
+from functools import partial
+
+import mlx.core as mx
+import mlx.nn as nn
+
+class QuantizedSwitchLinear(nn.Module):
+ def __init__(
+ self,
+ input_dims: int,
+ output_dims: int,
+ num_experts: int,
+ bias: bool = ...,
+ group_size: int = ...,
+ bits: int = ...,
+ mode: str = ...,
+ ) -> None: ...
+ @property
+ def input_dims(self): # -> int:
+ ...
+ @property
+ def output_dims(self): # -> int:
+ ...
+ @property
+ def num_experts(self): # -> int:
+ ...
+ def __call__(self, x, indices, sorted_indices=...): # -> array:
+ ...
+
+class SwitchLinear(nn.Module):
+ def __init__(
+ self, input_dims: int, output_dims: int, num_experts: int, bias: bool = ...
+ ) -> None: ...
+ @property
+ def input_dims(self): # -> int:
+ ...
+ @property
+ def output_dims(self): # -> int:
+ ...
+ @property
+ def num_experts(self): # -> int:
+ ...
+ def __call__(self, x, indices, sorted_indices=...): ...
+ def to_quantized(
+ self, group_size: int = ..., bits: int = ..., mode: str = ...
+ ): # -> QuantizedSwitchLinear:
+ ...
+
+@partial(mx.compile, shapeless=True)
+def swiglu(x, gate): ...
+
+class SwiGLU(nn.Module):
+ def __init__(self) -> None: ...
+ def __call__(self, x, gate): ...
+
+class SwitchGLU(nn.Module):
+ def __init__(
+ self,
+ input_dims: int,
+ hidden_dims: int,
+ num_experts: int,
+ activation=...,
+ bias: bool = ...,
+ ) -> None: ...
+ def __call__(self, x, indices) -> mx.array: ...
+
+class SwitchMLP(nn.Module):
+ def __init__(
+ self,
+ input_dims: int,
+ hidden_dims: int,
+ num_experts: int,
+ activation=...,
+ bias: bool = ...,
+ ) -> None: ...
+ def __call__(self, x, indices) -> mx.array: ...
diff --git a/.mlx_typings/mlx_lm/sample_utils.pyi b/.mlx_typings/mlx_lm/sample_utils.pyi
new file mode 100644
index 00000000..bc6955a7
--- /dev/null
+++ b/.mlx_typings/mlx_lm/sample_utils.pyi
@@ -0,0 +1,148 @@
+"""
+This type stub file was generated by pyright.
+"""
+
+from functools import partial
+from typing import Callable, Dict, List, Optional
+
+import mlx.core as mx
+
+def make_sampler(
+ temp: float = ...,
+ top_p: float = ...,
+ min_p: float = ...,
+ min_tokens_to_keep: int = ...,
+ top_k: int = ...,
+ xtc_probability: float = ...,
+ xtc_threshold: float = ...,
+ xtc_special_tokens: List[int] = ...,
+) -> Callable[[mx.array], mx.array]:
+ """
+ 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:
+ Callable[mx.array, mx.array]:
+ A sampler which takes log-probabilities and returns tokens.
+ """
+
+def make_logits_processors(
+ logit_bias: Optional[Dict[int, float]] = ...,
+ repetition_penalty: Optional[float] = ...,
+ repetition_context_size: Optional[int] = ...,
+): # -> list[Any]:
+ """
+ Make logits processors for use with ``generate_step``.
+
+ Args:
+ repetition_penalty (float, optional): The penalty factor for repeating
+ tokens.
+ repetition_context_size (int, optional): The number of tokens to
+ consider for repetition penalty. Default: ``20``.
+ logit_bias (dictionary, optional): Additive logit bias.
+
+ Returns:
+ List[Callable[[mx.array, mx.array], mx.array]]:
+ 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.
+ """
+
+@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.
+ """
+
+@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 = ...
+) -> 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``.
+
+ """
+
+@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.
+ """
+
+@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.
+ """
+
+@partial(mx.compile, inputs=mx.random.state, outputs=mx.random.state)
+def categorical_sampling(logits, temp): # -> array:
+ ...
+def make_repetition_penalty(
+ penalty: float, context_size: int = ...
+): # -> Callable[..., Any]:
+ """
+ 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.
+ """
diff --git a/.mlx_typings/mlx_lm/tokenizer_utils.pyi b/.mlx_typings/mlx_lm/tokenizer_utils.pyi
new file mode 100644
index 00000000..a0a8355f
--- /dev/null
+++ b/.mlx_typings/mlx_lm/tokenizer_utils.pyi
@@ -0,0 +1,168 @@
+"""
+This type stub file was generated by pyright.
+"""
+
+from functools import partial
+from pathlib import Path
+
+from transformers import PreTrainedTokenizerFast
+
+class StreamingDetokenizer:
+ """The streaming detokenizer interface so that we can detokenize one token at a time.
+
+ Example usage is as follows:
+
+ detokenizer = ...
+
+ # Reset the tokenizer state
+ detokenizer.reset()
+
+ for token in generate(...):
+ detokenizer.add_token(token.item())
+
+ # Contains the whole text so far. Some tokens may not be included
+ # since it contains whole words usually.
+ detokenizer.text
+
+ # Contains the printable segment (usually a word) since the last
+ # time it was accessed
+ detokenizer.last_segment
+
+ # Contains all the tokens added so far
+ detokenizer.tokens
+
+ # Make sure that we detokenize any remaining tokens
+ detokenizer.finalize()
+
+ # Now detokenizer.text should match tokenizer.decode(detokenizer.tokens)
+ """
+
+ __slots__ = ...
+ def reset(self): ...
+ def add_token(self, token): ...
+ def finalize(self): ...
+ @property
+ def last_segment(self):
+ """Return the last segment of readable text since last time this property was accessed."""
+
+class NaiveStreamingDetokenizer(StreamingDetokenizer):
+ """NaiveStreamingDetokenizer relies on the underlying tokenizer
+ implementation and should work with every tokenizer.
+
+ Its complexity is O(T^2) where T is the longest line since it will
+ repeatedly detokenize the same tokens until a new line is generated.
+ """
+ def __init__(self, tokenizer) -> None: ...
+ def reset(self): # -> None:
+ ...
+ def add_token(self, token): # -> None:
+ ...
+ def finalize(self): # -> None:
+ ...
+ @property
+ def text(self): # -> str:
+ ...
+
+class SPMStreamingDetokenizer(StreamingDetokenizer):
+ """A streaming detokenizer for SPM models.
+
+ It adds tokens to the text if the next token starts with the special SPM
+ underscore which results in linear complexity.
+ """
+ def __init__(self, tokenizer, trim_space=...) -> None: ...
+ def reset(self): # -> None:
+ ...
+ def add_token(self, token): # -> None:
+ ...
+ def finalize(self): # -> None:
+ ...
+
+class BPEStreamingDetokenizer(StreamingDetokenizer):
+ """A streaming detokenizer for OpenAI style BPE models.
+
+ It adds tokens to the text if the next token starts with a space similar to
+ the SPM detokenizer.
+ """
+
+ _byte_decoder = ...
+ _space_matches = ...
+ def __init__(self, tokenizer) -> None: ...
+ def reset(self): # -> None:
+ ...
+ def add_token(self, token): # -> None:
+ ...
+ def finalize(self): # -> None:
+ ...
+ @classmethod
+ def make_byte_decoder(cls): # -> None:
+ """See https://github.com/openai/gpt-2/blob/master/src/encoder.py for the rationale."""
+
+class TokenizerWrapper:
+ """A wrapper that combines an HF tokenizer and a detokenizer.
+
+ Accessing any attribute other than the ``detokenizer`` is forwarded to the
+ huggingface tokenizer.
+ """
+ def __init__(self, tokenizer, detokenizer_class=..., eos_token_ids=...) -> None: ...
+ def add_eos_token(self, token: str): # -> None:
+ ...
+ @property
+ def has_thinking(self): # -> bool:
+ ...
+ @property
+ def think_start(self): # -> str | None:
+ ...
+ @property
+ def think_end(self): # -> str | None:
+ ...
+ @property
+ def has_tool_calling(self): # -> bool:
+ ...
+ @property
+ def tool_call_start(self): # -> str | None:
+ ...
+ @property
+ def tool_call_end(self): # -> str | None:
+ ...
+ @property
+ def detokenizer(self): # -> NaiveStreamingDetokenizer:
+ """
+ Get a stateful streaming detokenizer.
+ """
+
+ def __getattr__(self, attr): # -> set[Any] | Any:
+ ...
+ def __setattr__(self, attr, value): # -> None:
+ ...
+
+class NewlineTokenizer(PreTrainedTokenizerFast):
+ """A tokenizer that replaces newlines with <n> and <n> with new line."""
+ def __init__(self, *args, **kwargs) -> None: ...
+ def encode(self, text, **kwargs): # -> list[int]:
+ ...
+ def encode_batch(self, texts, **kwargs): ...
+ def decode(self, *args, **kwargs): # -> str:
+ ...
+ def batch_decode(self, *args, **kwargs): # -> list[str]:
+ ...
+
+def load_tokenizer(
+ model_path: Path,
+ tokenizer_config_extra=...,
+ return_tokenizer=...,
+ eos_token_ids=...,
+) -> (
+ TokenizerWrapper
+ | type[SPMStreamingDetokenizer]
+ | partial[SPMStreamingDetokenizer]
+ | type[BPEStreamingDetokenizer]
+ | type[NaiveStreamingDetokenizer]
+):
+ """Load a huggingface tokenizer and try to infer the type of streaming
+ detokenizer to use.
+
+ Note, to use a fast streaming tokenizer, pass a local file path rather than
+ a Hugging Face repo ID.
+ """
+
+def no_bos_or_eos(sequence: list, bos: int, eos: int) -> list: ...
diff --git a/.mlx_typings/mlx_lm/utils.pyi b/.mlx_typings/mlx_lm/utils.pyi
new file mode 100644
index 00000000..99b207d1
--- /dev/null
+++ b/.mlx_typings/mlx_lm/utils.pyi
@@ -0,0 +1,195 @@
+"""
+This type stub file was generated by pyright.
+"""
+
+import os
+from pathlib import Path
+from typing import Any, Callable, Dict, Optional, Tuple, Type, Union
+
+import mlx.nn as nn
+from transformers.utils.auto_docstring import ModelArgs
+
+from .tokenizer_utils import TokenizerWrapper
+
+if os.getenv("MLXLM_USE_MODELSCOPE", "False").lower() == "true": ...
+else: ...
+MODEL_REMAPPING = ...
+MAX_FILE_SIZE_GB = ...
+
+def compute_bits_per_weight(model): ...
+def hf_repo_to_path(hf_repo): # -> Path:
+ ...
+def load_config(model_path: Path) -> dict: ...
+def load_model(
+ model_path: Path,
+ lazy: bool = False,
+ strict: bool = True,
+ model_config: dict[str, Any] = {},
+ get_model_classes: Callable[
+ [dict[str, Any]], Tuple[Type[nn.Module], Type[ModelArgs]]
+ ] = ...,
+) -> Tuple[nn.Module, dict[str, Any]]:
+ """
+ Load and initialize the model from a given path.
+
+ Args:
+ model_path (Path): The path to load the model from.
+ lazy (bool): If False eval the model parameters to make sure they are
+ loaded in memory before returning, otherwise they will be loaded
+ when needed. Default: ``False``
+ strict (bool): Whether or not to raise an exception if weights don't
+ match. Default: ``True``
+ model_config (dict, optional): Optional configuration parameters for the
+ model. Defaults to an empty dictionary.
+ get_model_classes (Callable[[dict], Tuple[Type[nn.Module], Type]], optional):
+ A function that returns the model class and model args class given a config.
+ Defaults to the ``_get_classes`` function.
+
+ Returns:
+ Tuple[nn.Module, dict[str, Any]]: The loaded and initialized model and config.
+
+ Raises:
+ FileNotFoundError: If the weight files (.safetensors) are not found.
+ ValueError: If the model class or args class are not found or cannot be instantiated.
+ """
+
+def load(
+ path_or_hf_repo: str,
+ tokenizer_config=...,
+ model_config=...,
+ adapter_path: Optional[str] = ...,
+ lazy: bool = ...,
+ return_config: bool = ...,
+ revision: str = ...,
+) -> Union[
+ Tuple[nn.Module, TokenizerWrapper],
+ Tuple[nn.Module, TokenizerWrapper, Dict[str, Any]],
+]:
+ """
+ Load the model and tokenizer from a given path or a huggingface repository.
+
+ Args:
+ path_or_hf_repo (Path): The path or the huggingface repository to load the model from.
+ tokenizer_config (dict, optional): Configuration parameters specifically for the tokenizer.
+ Defaults to an empty dictionary.
+ model_config(dict, optional): Configuration parameters specifically for the model.
+ Defaults to an empty dictionary.
+ adapter_path (str, optional): Path to the LoRA adapters. If provided, applies LoRA layers
+ to the model. Default: ``None``.
+ lazy (bool): If ``False`` eval the model parameters to make sure they are
+ loaded in memory before returning, otherwise they will be loaded
+ when needed. Default: ``False``
+ return_config (bool: If ``True`` return the model config as the last item..
+ revision (str, optional): A revision id which can be a branch name, a tag, or a commit hash.
+ Returns:
+ Union[Tuple[nn.Module, TokenizerWrapper], Tuple[nn.Module, TokenizerWrapper, Dict[str, Any]]]:
+ A tuple containing the loaded model, tokenizer and, if requested, the model config.
+
+ Raises:
+ FileNotFoundError: If config file or safetensors are not found.
+ ValueError: If model class or args class are not found.
+ """
+
+def make_shards(weights: dict, max_file_size_gb: int = ...) -> list:
+ """
+ Splits the weights into smaller shards.
+
+ Args:
+ weights (dict): Model weights.
+ max_file_size_gb (int): Maximum size of each shard in gigabytes.
+
+ Returns:
+ list: List of weight shards.
+ """
+
+def create_model_card(
+ path: Union[str, Path], hf_path: Union[str, Path, None]
+): # -> None:
+ """
+ Uploads the model to Hugging Face hub.
+
+ Args:
+ path (Union[str, Path]): Local path to the model.
+ hf_path (Union[str, Path, None]): Path to the original Hugging Face model.
+ """
+
+def upload_to_hub(path: str, upload_repo: str): # -> None:
+ """
+ Uploads the model to Hugging Face hub.
+
+ Args:
+ path (str): Local path to the model.
+ upload_repo (str): Name of the HF repo to upload to.
+ """
+
+def save_model(
+ save_path: Union[str, Path], model: nn.Module, *, donate_model: bool = ...
+) -> None:
+ """Save model weights and metadata index into specified directory."""
+
+def quantize_model(
+ model: nn.Module,
+ config: dict,
+ group_size: int,
+ bits: int,
+ mode: str = ...,
+ quant_predicate: Optional[Callable[[str, nn.Module], Union[bool, dict]]] = ...,
+) -> Tuple[nn.Module, dict]:
+ """
+ Applies quantization to the model weights.
+
+ Args:
+ model (nn.Module): The model to be quantized.
+ config (dict): Model configuration.
+ group_size (int): Group size for quantization.
+ bits (int): Bits per weight for quantization.
+ mode (str): The quantization mode.
+ quant_predicate (Callable): A callable that decides how to quantize
+ each layer based on the path. Accepts the layer `path` and the
+ `module`. Returns either a bool to signify quantize/no quantize or
+ a dict of quantization parameters to pass to `to_quantized`.
+
+ Returns:
+ Tuple: Tuple containing quantized model and config.
+ """
+
+def save_config(config: dict, config_path: Union[str, Path]) -> None:
+ """Save the model configuration to the ``config_path``.
+
+ The final configuration will be sorted before saving for better readability.
+
+ Args:
+ config (dict): The model configuration.
+ config_path (Union[str, Path]): Model configuration file path.
+ """
+
+def save(
+ dst_path: Union[str, Path],
+ src_path_or_repo: Union[str, Path],
+ model: nn.Module,
+ tokenizer: TokenizerWrapper,
+ config: Dict[str, Any],
+ donate_model: bool = ...,
+): # -> None:
+ ...
+def common_prefix_len(list1, list2): # -> int:
+ """
+ Calculates the length of the common prefix of two lists.
+
+ Args:
+ list1: The first list of strings.
+ list2: The second list of strings.
+
+ Returns:
+ The length of the common prefix. Returns 0 if lists are empty
+ or do not match at the first element.
+ """
+
+def does_model_support_input_embeddings(model: nn.Module) -> bool:
+ """
+ Check if the model supports input_embeddings in its call signature.
+ Args:
+ model (nn.Module): The model to check.
+ Returns:
+ bool: True if the model supports input_embeddings, False otherwise.
+ """
diff --git a/justfile b/justfile
index a61d0bb8..2ef99049 100644
--- a/justfile
+++ b/justfile
@@ -1,5 +1,5 @@
fmt:
- uv run ruff format src typings
+ uv run ruff format src .mlx_typings
lint:
uv run ruff check --fix src
diff --git a/pyproject.toml b/pyproject.toml
index 2113642a..12ff2bdf 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -81,7 +81,7 @@ build-backend = "uv_build"
###
[tool.basedpyright]
-include = [".venv/lib/mlx", "src"]
+include = [".venv/lib/mlx", ".venv/lib/mlx_lm", "src"]
typeCheckingMode = "strict"
failOnWarnings = true
@@ -97,8 +97,8 @@ reportUnnecessaryTypeIgnoreComment = "error"
pythonVersion = "3.13"
pythonPlatform = "Darwin"
-exclude = ["**/.venv", "**/venv", "**/__pycache__", "**/exo_scripts", "**/.direnv", "**/rust"]
-stubPath = "typings"
+exclude = ["**/.venv", "**/venv", "**/__pycache__", "**/exo_scripts", "**/.direnv", "**/rust", "**/.github"]
+stubPath = ".mlx_typings"
[[tool.basedpyright.executionEnvironments]]
root = "src"
diff --git a/src/exo/engines/mlx/utils_mlx.py b/src/exo/engines/mlx/utils_mlx.py
index d1216e73..8d7bde05 100644
--- a/src/exo/engines/mlx/utils_mlx.py
+++ b/src/exo/engines/mlx/utils_mlx.py
@@ -9,10 +9,10 @@ from mlx_lm.models.cache import KVCache
from mlx_lm.sample_utils import make_sampler
try:
- from mlx_lm.tokenizer_utils import load_tokenizer # type: ignore
+ from mlx_lm.tokenizer_utils import load_tokenizer
except ImportError:
from mlx_lm.tokenizer_utils import load as load_tokenizer # type: ignore
-from mlx_lm.utils import load_model # type: ignore
+from mlx_lm.utils import load_model
from pydantic import RootModel
import mlx.core as mx
@@ -167,12 +167,11 @@ def shard_and_load(
f"loading model from {model_path} with strategy {model_shard_meta.strategy}"
)
- model, config = load_model(model_path, lazy=True, strict=False) # type: ignore
+ model, config = load_model(model_path, lazy=True, strict=False)
runner_print(f"{config=}")
assert isinstance(model, nn.Module)
- tokenizer = load_tokenizer(model_path) # type: ignore
- tokenizer = cast(TokenizerWrapper, tokenizer)
+ tokenizer = cast(TokenizerWrapper, load_tokenizer(model_path))
runner_print(f"Group size: {group.size()}, group rank: {group.rank()}")
diff --git a/src/exo/worker/download/impl_shard_downloader.py b/src/exo/worker/download/impl_shard_downloader.py
index a00ac5a7..d6c59a80 100644
--- a/src/exo/worker/download/impl_shard_downloader.py
+++ b/src/exo/worker/download/impl_shard_downloader.py
@@ -31,7 +31,7 @@ async def build_base_shard(model_id: str) -> ShardMetadata:
)
-async def build_full_shard(model_id: str) -> PipelineShardMetadata | None:
+async def build_full_shard(model_id: str) -> PipelineShardMetadata:
base_shard = await build_base_shard(model_id)
return PipelineShardMetadata(
model_meta=base_shard.model_meta,
@@ -150,11 +150,9 @@ class ResumableShardDownloader(ShardDownloader):
# print("get_shard_download_status")
async def _status_for_model(
model_id: str,
- ) -> tuple[Path, RepoDownloadProgress] | None:
+ ) -> tuple[Path, RepoDownloadProgress]:
"""Helper coroutine that builds the shard for a model and gets its download status."""
shard = await build_full_shard(model_id)
- if shard is None:
- return None
return await download_shard(
shard, self.on_progress_wrapper, skip_download=True
)
@@ -168,8 +166,6 @@ class ResumableShardDownloader(ShardDownloader):
for task in asyncio.as_completed(tasks):
try:
result = await task
- if result is None:
- continue
path, progress = result
yield (path, progress)
except Exception as e:
diff --git a/src/exo/worker/runner/generate.py b/src/exo/worker/runner/generate.py
index d1497263..9fe58d40 100644
--- a/src/exo/worker/runner/generate.py
+++ b/src/exo/worker/runner/generate.py
@@ -35,16 +35,18 @@ generation_stream = mx.new_stream(mx.default_device())
def maybe_quantize_kv_cache(
- prompt_cache: list[Any],
+ prompt_cache: list[KVCache | Any],
quantized_kv_start: int,
kv_group_size: int,
kv_bits: int | None,
) -> None:
if kv_bits is None:
return
- for e, c in enumerate(prompt_cache): # type: ignore[type-arg]
- if hasattr(c, "to_quantized") and c.offset >= quantized_kv_start: # type: ignore[type-arg]
- prompt_cache[e] = c.to_quantized(group_size=kv_group_size, bits=kv_bits) # type: ignore[type-arg]
+ for e, c in enumerate(prompt_cache):
+ if (
+ hasattr(c, "to_quantized") and c.offset >= quantized_kv_start # type: ignore
+ ):
+ prompt_cache[e] = c.to_quantized(group_size=kv_group_size, bits=kv_bits)
def generate_step(
@@ -189,7 +191,7 @@ def generate_step(
quantize_cache_fn(prompt_cache)
start_time = time.time()
- mx.eval([c.state for c in prompt_cache]) # type: ignore
+ mx.eval([c.state for c in prompt_cache])
eval_time = time.time() - start_time
prompt_processed_tokens += n_to_process
@@ -221,9 +223,17 @@ def generate_step(
n = 0
while True:
- mx.eval(y, logprobs)
+ assert y is not None
+ assert logprobs is not None
+ if n != max_tokens:
+ next_y, next_logprobs = _step(y)
+ mx.async_eval(next_y, next_logprobs)
+ if n == 0:
+ mx.eval(y)
+ prompt_progress_callback(total_prompt_tokens, total_prompt_tokens)
+ if n == max_tokens:
+ break
yield int(y.item()), logprobs
- n += 1
if n % 256 == 0:
mx.clear_cache()
if n == max_tokens:
← 19e90572 set max_transmit_size on gossipsub to 1MB. Fixes large messa
·
back to Exo
·
stop benching on all pushes 6bcac37d →