Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 0 additions & 13 deletions docs/gguf.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,6 @@ GGUF file carries weights only, so it needs a Hugging Face config and tokenizer
source. That source can be a quantized checkpoint, such as an mlx-community
4-bit, FP8, or AWQ repository; vllm-metal reads only its config and tokenizer.

## Installation

GGUF support needs an optional dependency. Install it inside the environment
created by [Installation](installation.md):

```bash
source ~/.venv-vllm-metal/bin/activate
pip install 'vllm-metal[gguf]'
```

Without it, the engine fails at model load with `ModuleNotFoundError: No module
named 'gguf'`.

## Local weights

Use a local `.gguf` file and point `--tokenizer` at the matching config and
Expand Down
28 changes: 2 additions & 26 deletions docs/stt.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,6 @@

vllm-metal supports OpenAI-compatible Speech-to-Text using Whisper and Qwen3-ASR models, running natively on Apple Silicon via MLX.

## Installation

First, install vllm-metal using the install script (see [Installation](installation.md)):

```bash
./install.sh
```

Then install the optional STT dependencies inside the virtual environment:

```bash
source .venv-vllm-metal/bin/activate
pip install 'vllm-metal[stt]'
```

### ffmpeg (Optional)

ffmpeg is only needed for non-WAV audio formats (mp3, m4a, flac, etc.):

```bash
# macOS
brew install ffmpeg

# Not required for WAV files - librosa handles those directly
```

## Quick Start

```bash
Expand All @@ -39,6 +13,8 @@ curl -X POST http://localhost:8000/v1/audio/transcriptions \
-F "file=@recording.wav"
```

Some audio formats require FFmpeg (`brew install ffmpeg`).

## Supported Models

### Whisper
Expand Down
20 changes: 5 additions & 15 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ dependencies = [
"transformers>=5.10.4,<5.18.0",
"accelerate>=0.26.0",
"safetensors>=0.4.0",
"gguf>=0.17.0",
# Speech-to-text audio processing.
"librosa>=0.10.2",
"numba>=0.59.0",
# Imported directly (model download/cache paths); keep aligned with vLLM
# 0.31.0's lower bound rather than relying on vLLM's transitive install.
# Unbounded like upstream's own form: vLLM's requirements/common.txt pins
Expand All @@ -68,25 +72,11 @@ dependencies = [
]

[project.optional-dependencies]
gguf = [
# Local GGUF model loading. Keep aligned with upstream vLLM's lower bound
# unless a Metal-specific path needs a newer API.
"gguf>=0.17.0",
]
stt = [
# Speech-to-text audio processing (Whisper models)
"librosa>=0.10.2",
"numba>=0.59.0", # Required by librosa on Python 3.12+
]
dev = [
"pytest>=9.0.2",
"pytest-asyncio>=0.21.0",
"ruff==0.16.0",
"mypy>=1.19.1",
"gguf>=0.17.0",
]
all = [
"vllm-metal[gguf,stt,dev]",
]

[project.urls]
Expand All @@ -101,7 +91,7 @@ metal = "vllm_metal:register"
# GGUF engine integration (vLLM 0.24 moved in-tree GGUF support to the
# CUDA/ROCm-only vllm-gguf-plugin; this restores the Metal-consumable layer).
# Loaded by vLLM in EngineArgs.__post_init__ and in the EngineCore/worker
# subprocesses; the target module stays importable without the `gguf` extra.
# subprocesses; GGUF loading stays lazy for other model formats.
[project.entry-points."vllm.general_plugins"]
gguf_metal = "vllm_metal.gguf.vllm_integration:register"

Expand Down
23 changes: 1 addition & 22 deletions tests/test_gguf_vllm_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -394,10 +394,7 @@ def test_register_is_idempotent() -> None:


def test_integration_imports_without_gguf_package(monkeypatch) -> None:
"""Default-install tripwire: the entry-point module must import and
register with the optional ``gguf`` package absent (vLLM loads it on every
run).
"""
"""The entry point must register without eagerly importing GGUF parsing."""
monkeypatch.setitem(sys.modules, "gguf", None)
monkeypatch.delitem(sys.modules, "vllm_metal.gguf.vllm_integration", raising=False)
monkeypatch.delitem(sys.modules, "vllm_metal.gguf.source", raising=False)
Expand All @@ -420,24 +417,6 @@ def test_marker_config_survives_pickle() -> None:
assert restored.get_name() == "gguf"


def test_loader_import_without_gguf_names_the_extra(monkeypatch) -> None:
"""The optional-dep owner gives an actionable hint when the extra is
missing (0.24 dropped ``gguf`` from vLLM's own dependencies).
"""
monkeypatch.setitem(sys.modules, "gguf", None)
monkeypatch.delitem(sys.modules, "vllm_metal.gguf.loader", raising=False)
monkeypatch.delitem(sys.modules, "vllm_metal.gguf.mlx_native", raising=False)
monkeypatch.delitem(sys.modules, "vllm_metal.gguf.adapter", raising=False)
monkeypatch.delitem(sys.modules, "vllm_metal.gguf.wrappers", raising=False)

with pytest.raises(ImportError) as exc_info:
import vllm_metal.gguf.loader # noqa: F401
assert str(exc_info.value) == (
"GGUF support requires the optional 'gguf' dependency. "
"Install it with: pip install 'vllm-metal[gguf]'"
)


def test_probe_preserves_positional_speculative_config(gguf_file) -> None:
# The wrap mirrors the upstream signature exactly, so a positionally
# passed speculative config must round-trip (the official plugin's
Expand Down
5 changes: 2 additions & 3 deletions tests/test_model_lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -1177,11 +1177,10 @@ def test_non_gguf_model_does_not_route_or_import_gguf(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""A non-GGUF model (``quantization != "gguf"``) is never routed to the
GGUF owner, and the optional ``gguf`` package is never imported.
GGUF owner, and the ``gguf`` package is never imported.

Tripwire: block the ``gguf`` package itself (``sys.modules[...] = None``
raises on import). A clean generic load proves a default install with no
gguf extra is unaffected by the lazy owner import.
raises on import). A generic load must leave the GGUF owner unimported.
"""
fake_model = SimpleNamespace(config=_text_config())
fake_tokenizer = object()
Expand Down
6 changes: 2 additions & 4 deletions vllm_metal/gguf/__init__.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
# SPDX-License-Identifier: Apache-2.0
"""Experimental GGUF support for vllm-metal (MLX-native quantized execution).

Re-exports are lazy: ``vllm_metal.gguf.vllm_integration`` is imported by every
vLLM run through the ``vllm.general_plugins`` entry point, and a default
install (no ``gguf`` extra) must not pull the loader's optional ``gguf``
dependency on that path.
Re-exports are lazy so the ``vllm.general_plugins`` entry point does not load
GGUF parsing code for other model formats.
"""

from typing import Any
Expand Down
11 changes: 2 additions & 9 deletions vllm_metal/gguf/loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,11 @@
from tempfile import TemporaryDirectory
from typing import Any, cast

from huggingface_hub import snapshot_download

try:
import gguf
except ImportError as exc:
raise ImportError(
"GGUF support requires the optional 'gguf' dependency. "
"Install it with: pip install 'vllm-metal[gguf]'"
) from exc
import gguf
import mlx.core as mx
import mlx.nn as nn
import numpy as np
from huggingface_hub import snapshot_download
from mlx.utils import tree_flatten
from mlx_lm.utils import load_config, load_model, load_tokenizer

Expand Down
9 changes: 1 addition & 8 deletions vllm_metal/gguf/mlx_native.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,7 @@

import mlx.core as mx
import numpy as np

try:
from gguf import GGML_QUANT_SIZES, GGMLQuantizationType
except ImportError as exc: # pragma: no cover - exercised only without the extra
raise ImportError(
"GGUF support requires the optional 'gguf' dependency. "
"Install it with: pip install 'vllm-metal[gguf]'"
) from exc
from gguf import GGML_QUANT_SIZES, GGMLQuantizationType

# qtypes that repack into MLX's affine representation, with their bit widths.
_BITS: dict[GGMLQuantizationType, int] = {
Expand Down
9 changes: 1 addition & 8 deletions vllm_metal/gguf/raw_block.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,7 @@

import mlx.core as mx
import numpy as np

try:
from gguf import GGML_QUANT_SIZES, GGMLQuantizationType
except ImportError as exc: # pragma: no cover - exercised only without the extra
raise ImportError(
"GGUF support requires the optional 'gguf' dependency. "
"Install it with: pip install 'vllm-metal[gguf]'"
) from exc
from gguf import GGML_QUANT_SIZES, GGMLQuantizationType

# qtypes executed from raw GGUF block bytes by the kernels below (#761).
_RAW_KERNEL_BITS: dict[GGMLQuantizationType, int] = {
Expand Down
5 changes: 2 additions & 3 deletions vllm_metal/gguf/vllm_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@
again inside the EngineCore/worker subprocesses, so the registration is
spawn-safe without import-time patching.

This module must stay importable WITHOUT the optional ``gguf`` package: a
default install loads it on every vLLM run via the entry point.
Keep GGUF parsing imports lazy: the entry point loads on every vLLM run.

# SCAFFOLDING: remove when vllm-project/vllm-gguf-plugin installs and imports
# on macOS; then depend on it for this layer (register() already no-ops when
Expand Down Expand Up @@ -156,7 +155,7 @@ def is_remote_gguf_reference(cls, model: str | None) -> bool:
"""Whether ``model`` reads as the plugin's ``repo_id:quant`` syntax.

Mirrors the official plugin's public ``is_remote_gguf`` at the shape
level while keeping the optional ``gguf`` package out of this module.
level while keeping the ``gguf`` package out of this module.
"""
return model is not None and RemoteGGUFReference.parse(model) is not None

Expand Down
Loading