Files
unslothai--unsloth/studio/backend/tests/test_llama_cpp_mtp_detection.py
wehub-resource-sync e93507a09c
Lockfile supply-chain audit / lockfile supply-chain audit (push) Has been cancelled
Windows Studio GGUF CI / GPU prebuilt resolves without Visual Studio (push) Has been cancelled
Windows Studio GGUF CI / setup.ps1 unit tests (VS 2026 / CMake guard) (push) Has been cancelled
Windows Studio GGUF CI / real-VS detection (VS 2022) (push) Has been cancelled
Windows Studio GGUF CI / real-VS detection (VS 2026) (push) Has been cancelled
Windows Studio GGUF CI / VC++ runtime detect + install round-trip (windows-2025-vs2026) (push) Has been cancelled
Windows Studio GGUF CI / VC++ runtime detect + install round-trip (windows-latest) (push) Has been cancelled
Windows Studio Update CI / Studio Updating Tests (push) Has been cancelled
Wheel CI / Wheel build + content sanity + import smoke (push) Has been cancelled
Lint CI / Source lint (Python + shell + YAML + JSON + safety nets) (push) Has been cancelled
MLX CI on Mac M1 / dispatch (push) Has been cancelled
Security audit / advisory audit (pip + npm + cargo) (push) Has been cancelled
Security audit / pip scan-packages :: extras (push) Has been cancelled
Security audit / pip scan-packages :: studio (push) Has been cancelled
Security audit / pip scan-packages :: hf-stack (push) Has been cancelled
Security audit / npm scan-packages (Studio frontend tarballs) (push) Has been cancelled
Security audit / workflow-trigger lint (pull_request_target / cache-poisoning) (push) Has been cancelled
Security audit / pytest tests/security (push) Has been cancelled
Security audit / npm provenance + new install-script diff (push) Has been cancelled
Studio API CI / Studio API & Auth Tests (push) Has been cancelled
Backend CI / (Python 3.10) (push) Has been cancelled
Backend CI / (Python 3.11) (push) Has been cancelled
Backend CI / (Python 3.12) (push) Has been cancelled
Backend CI / (Python 3.13) (push) Has been cancelled
Backend CI / Repo tests (CPU) (push) Has been cancelled
Frontend CI / Frontend build + bundle sanity (push) Has been cancelled
Studio GGUF CI / OpenAI, Anthropic API tests (push) Has been cancelled
Studio GGUF CI / Tool calling Tests (push) Has been cancelled
Studio GGUF CI / JSON, images (push) Has been cancelled
Mac Studio GGUF CI / OpenAI, Anthropic API tests (push) Has been cancelled
Mac Studio GGUF CI / Tool calling Tests (push) Has been cancelled
Mac Studio GGUF CI / JSON, images (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-14) (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-15) (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-26) (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-15-intel) (push) Has been cancelled
Mac Studio API CI / Studio API & Auth Tests (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-26-intel) (push) Has been cancelled
Mac Studio UI CI / Chat UI Tests (push) Has been cancelled
Studio Tauri CI / Tauri Linux debug build (no codesign) (push) Has been cancelled
Mac Studio Update CI / Studio Updating Tests (push) Has been cancelled
Studio UI CI / Chat UI Tests (push) Has been cancelled
Windows Studio API CI / Studio API & Auth Tests (push) Has been cancelled
Windows Studio UI CI / Chat UI Tests (push) Has been cancelled
Studio Update CI / Studio Updating Tests (push) Has been cancelled
Core / Core (HF=default + TRL=default) (push) Has been cancelled
Core / Core (HF=4.57.6 + TRL<1) (push) Has been cancelled
Core / Core (HF=latest + TRL=latest) (push) Has been cancelled
Core / llama.cpp build + smoke (push) Has been cancelled
Windows Studio GGUF CI / OpenAI, Anthropic API tests (push) Has been cancelled
Windows Studio GGUF CI / Tool calling Tests (push) Has been cancelled
Windows Studio GGUF CI / JSON, images (push) Has been cancelled
Windows Studio GGUF CI / Studio install + inference without Visual Studio (push) Has been cancelled
Studio export capability / capability (macos-latest) (push) Has been cancelled
Studio export capability / capability (ubuntu-latest) (push) Has been cancelled
Studio export capability / capability (windows-latest) (push) Has been cancelled
Cross-platform parity / parity (macos-latest) (push) Has been cancelled
Cross-platform parity / parity (windows-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
Studio load-orchestrator CI / test (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:59:56 +08:00

1971 lines
70 KiB
Python

# SPDX-License-Identifier: AGPL-3.0-only
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
"""Tests for the MTP auto-detection path (llama.cpp #22673).
Pins three contracts: name-based detector, user-override detector, and
the _already_in_target_state mirror that prevents needless reloads.
"""
from __future__ import annotations
import inspect
import os
import struct
import sys
import types as _types
from pathlib import Path
_BACKEND_DIR = str(Path(__file__).resolve().parent.parent)
if _BACKEND_DIR not in sys.path:
sys.path.insert(0, _BACKEND_DIR)
_loggers_stub = _types.ModuleType("loggers")
_loggers_stub.get_logger = lambda name: __import__("logging").getLogger(name)
sys.modules.setdefault("loggers", _loggers_stub)
_structlog_stub = _types.ModuleType("structlog")
_structlog_stub.get_logger = lambda *a, **k: __import__("logging").getLogger("stub")
sys.modules.setdefault("structlog", _structlog_stub)
_httpx_stub = _types.ModuleType("httpx")
for _exc in (
"ConnectError",
"TimeoutException",
"ReadTimeout",
"ReadError",
"RemoteProtocolError",
"CloseError",
):
setattr(_httpx_stub, _exc, type(_exc, (Exception,), {}))
_httpx_stub.Timeout = type("T", (), {"__init__": lambda s, *a, **k: None})
_httpx_stub.Client = type(
"C",
(),
{
"__init__": lambda s, **kw: None,
"__enter__": lambda s: s,
"__exit__": lambda s, *a: None,
},
)
sys.modules.setdefault("httpx", _httpx_stub)
import pytest
from core.inference.llama_cpp import (
LlamaCppBackend,
_GPU_OFFLOAD_OVERRIDE_FLAGS,
_THREAD_OVERRIDE_FLAGS,
_backfill_usage_from_timings,
_build_ngram_mod_flags,
_canonicalize_spec_mode,
_extra_args_set_any_flag,
_extra_args_set_spec_type,
_is_mtp_model_name,
_mla_mtp_auto_enabled,
)
# Synthetic GGUF helper (mirrors test_gguf_metadata.py).
_GGUF_MAGIC = 0x46554747
_VTYPE_STRING = 8
_VTYPE_UINT32 = 4
def _enc_string(s: str) -> bytes:
b = s.encode("utf-8")
return struct.pack("<Q", len(b)) + b
def _enc_kv_string(key: str, value: str) -> bytes:
return _enc_string(key) + struct.pack("<I", _VTYPE_STRING) + _enc_string(value)
def _enc_kv_uint32(key: str, value: int) -> bytes:
return _enc_string(key) + struct.pack("<I", _VTYPE_UINT32) + struct.pack("<I", value)
def _write_minimal_gguf(
path: Path,
*,
arch: str,
nextn: int | None,
extra_uint32: dict[str, int] | None = None,
) -> Path:
"""Header-only GGUF with arch + optional nextn_predict_layers."""
extra_uint32 = dict(extra_uint32 or {})
body = _enc_kv_string("general.architecture", arch)
kv_count = 1
if nextn is not None:
body += _enc_kv_uint32(f"{arch}.nextn_predict_layers", nextn)
kv_count += 1
for k, v in extra_uint32.items():
body += _enc_kv_uint32(k, v)
kv_count += 1
header = struct.pack("<IIQQ", _GGUF_MAGIC, 3, 0, kv_count)
path.write_bytes(header + body)
return path
# _is_mtp_model_name helper.
@pytest.mark.parametrize(
"identifier",
[
"unsloth/Qwen3.6-27B-MTP-GGUF",
"unsloth/Qwen3.6-35B-A3B-MTP-GGUF",
"unsloth/qwen3.6-27b-mtp-gguf",
"unsloth/Qwen3.6-27B-Mtp-GGUF",
"unsloth/Qwen3.6-27B-MTP-GGUF:UD-Q4_K_XL",
],
)
def test_is_mtp_model_name_detects_marker_in_identifier(identifier):
assert _is_mtp_model_name(identifier) is True
@pytest.mark.parametrize(
"identifier",
[
"unsloth/Qwen3-27B-GGUF",
"unsloth/Llama-3.1-8B-Instruct-GGUF",
"google/gemma-3-4b-it",
# mtp inside an org name should not match.
"mtp-research/foo",
"MTPower/bar",
],
)
def test_is_mtp_model_name_does_not_overmatch(identifier):
assert _is_mtp_model_name(identifier) is False
def test_is_mtp_model_name_handles_none():
assert _is_mtp_model_name(None) is False
assert _is_mtp_model_name(None, None) is False
assert _is_mtp_model_name("", "") is False
def test_is_mtp_model_name_detects_marker_in_filename(tmp_path):
gguf = tmp_path / "Qwen3.6-27B-MTP-Q4_K_M.gguf"
gguf.write_bytes(b"")
assert _is_mtp_model_name("local-model", str(gguf)) is True
def test_is_mtp_model_name_filename_case_insensitive(tmp_path):
gguf = tmp_path / "qwen3.6-35b-a3b-mtp-q4_k_m.gguf"
gguf.write_bytes(b"")
assert _is_mtp_model_name(None, str(gguf)) is True
def test_is_mtp_model_name_ignores_non_mtp_filename(tmp_path):
gguf = tmp_path / "Qwen3.6-27B-Q4_K_M.gguf"
gguf.write_bytes(b"")
assert _is_mtp_model_name("local-model", str(gguf)) is False
# _already_in_target_state MTP promotion.
class _FakeProcess:
"""Minimal stand-in so is_loaded returns True."""
def terminate(self):
pass
def wait(self, timeout = None):
return 0
def kill(self):
pass
def poll(self):
return 0
def _mtp_backend(**overrides):
"""MTP-named GGUF backend that's already running with draft-mtp."""
backend = LlamaCppBackend()
backend._process = _FakeProcess()
backend._healthy = True
backend._model_identifier = "unsloth/Qwen3.6-27B-MTP-GGUF"
backend._hf_variant = "Q4_K_M"
backend._requested_n_ctx = 8192
backend._cache_type_kv = None
backend._speculative_type = "draft-mtp"
# Fixture simulates Auto having auto-promoted to draft-mtp. Tests
# override _requested_spec_mode for a forced mode or the
# user---spec-type-extra-args path.
backend._requested_spec_mode = "auto"
backend._chat_template_override = None
backend._is_vision = False
backend._extra_args = None
backend._extra_args_source = None
backend._gguf_path = None
for key, value in overrides.items():
setattr(backend, key, value)
return backend
def test_already_in_target_state_matches_when_request_omits_spec_for_mtp_model():
# Duplicate /load with no spec must match a running draft-mtp backend.
backend = _mtp_backend()
assert (
backend._already_in_target_state(
gguf_path = None,
model_identifier = "unsloth/Qwen3.6-27B-MTP-GGUF",
hf_variant = "Q4_K_M",
n_ctx = 8192,
cache_type_kv = None,
speculative_type = None,
chat_template_override = None,
extra_args = None,
is_vision = False,
)
is True
)
def test_already_in_target_state_matches_when_request_uses_default_for_mtp_model():
backend = _mtp_backend()
assert (
backend._already_in_target_state(
gguf_path = None,
model_identifier = "unsloth/Qwen3.6-27B-MTP-GGUF",
hf_variant = "Q4_K_M",
n_ctx = 8192,
cache_type_kv = None,
speculative_type = "default",
chat_template_override = None,
extra_args = None,
is_vision = False,
)
is True
)
def test_already_in_target_state_auto_request_matches_auto_backend_for_non_mtp_model():
# In the requested-mode round-trip model, Auto-vs-Auto matches regardless
# of model name. The resolved emission (--spec-default vs draft-mtp) is
# handled by the load path and reflected in _speculative_type; the
# short-circuit only cares whether the *intent* changed.
backend = _mtp_backend(
_model_identifier = "unsloth/Qwen3.6-27B-GGUF",
_speculative_type = "default",
)
assert (
backend._already_in_target_state(
gguf_path = None,
model_identifier = "unsloth/Qwen3.6-27B-GGUF",
hf_variant = "Q4_K_M",
n_ctx = 8192,
cache_type_kv = None,
speculative_type = None,
chat_template_override = None,
extra_args = None,
is_vision = False,
)
is True
)
def test_already_in_target_state_explicit_off_still_mismatches_mtp_backend():
backend = _mtp_backend()
assert (
backend._already_in_target_state(
gguf_path = None,
model_identifier = "unsloth/Qwen3.6-27B-MTP-GGUF",
hf_variant = "Q4_K_M",
n_ctx = 8192,
cache_type_kv = None,
speculative_type = "off",
chat_template_override = None,
extra_args = None,
is_vision = False,
)
is False
)
# User override via extra_args (unsloth run / unsloth studio run).
@pytest.mark.parametrize(
"extra_args",
[
["--spec-type", "none"],
["--spec-type", "ngram-mod"],
["--spec-type", "draft-mtp"],
["--spec-type=none"],
["--top-k", "20", "--spec-type", "ngram-simple", "--seed", "42"],
["--spec-default"],
],
)
def test_extra_args_set_spec_type_detects_user_override(extra_args):
assert _extra_args_set_spec_type(extra_args) is True
@pytest.mark.parametrize(
"extra_args",
[
None,
[],
# Scalar tuning knobs compose safely with auto-emitted --spec-type.
["--spec-draft-n-max", "4"],
["--spec-ngram-mod-n-match", "32"],
["--draft-max", "32"],
["--top-k", "20", "--seed", "42"],
],
)
def test_extra_args_set_spec_type_passes_on_non_spec_type_args(extra_args):
assert _extra_args_set_spec_type(extra_args) is False
@pytest.mark.parametrize(
"extra_args",
[
["-ngl", "12"],
["--gpu-layers", "12"],
["--n-gpu-layers=12"],
["-fit", "off"],
["--fit=off"],
],
)
def test_extra_args_detect_gpu_offload_overrides(extra_args):
assert _extra_args_set_any_flag(extra_args, _GPU_OFFLOAD_OVERRIDE_FLAGS) is True
@pytest.mark.parametrize("extra_args", [["-t", "8"], ["--threads=8"]])
def test_extra_args_detect_thread_overrides(extra_args):
assert _extra_args_set_any_flag(extra_args, _THREAD_OVERRIDE_FLAGS) is True
def test_windows_full_offload_flags_use_current_llama_server_args():
src = inspect.getsource(LlamaCppBackend.load_model)
stale_checkpoint_flag = "--checkpoint-" + "every-n-tokens"
assert '"--cache-ram"' in src
assert '"--ctx-checkpoints"' in src
assert '"--no-cache-prompt"' in src
assert stale_checkpoint_flag not in src
def test_load_model_sets_threads_once():
src = inspect.getsource(LlamaCppBackend.load_model)
assert src.count('cmd.extend(["--threads", str(') == 1
def test_llama_cpp_annotations_stay_python39_safe():
src = inspect.getsource(LlamaCppBackend.generate_chat_completion)
helper_src = inspect.getsource(_extra_args_set_any_flag)
assert "Generator[str | dict" not in src
assert "set[str] | frozenset[str]" not in helper_src
def test_already_in_target_state_user_spec_type_override_matches_clean_backend():
# User --spec-type none suppressed auto-MTP; repeat /load must not re-promote.
backend = _mtp_backend(
_speculative_type = None,
_requested_spec_mode = None,
_extra_args = ["--spec-type", "none"],
)
assert (
backend._already_in_target_state(
gguf_path = None,
model_identifier = "unsloth/Qwen3.6-27B-MTP-GGUF",
hf_variant = "Q4_K_M",
n_ctx = 8192,
cache_type_kv = None,
speculative_type = None,
chat_template_override = None,
extra_args = ["--spec-type", "none"],
is_vision = False,
)
is True
)
def test_already_in_target_state_local_file_mtp_match(tmp_path):
# Local-file load: -MTP marker comes from the filename.
gguf = tmp_path / "Qwen3.6-35B-A3B-MTP-Q4_K_M.gguf"
gguf.write_bytes(b"")
backend = _mtp_backend(
_model_identifier = "local-qwen-mtp",
_gguf_path = str(gguf),
_hf_variant = None,
)
assert (
backend._already_in_target_state(
gguf_path = str(gguf),
model_identifier = "local-qwen-mtp",
hf_variant = None,
n_ctx = 8192,
cache_type_kv = None,
speculative_type = None,
chat_template_override = None,
extra_args = None,
is_vision = False,
)
is True
)
def test_already_in_target_state_vision_mtp_match():
# llama.cpp #22673: MTP is compatible with mmproj. A vision MTP load
# with auto/default spec must match a backend already running draft-mtp.
backend = _mtp_backend(_is_vision = True)
assert (
backend._already_in_target_state(
gguf_path = None,
model_identifier = "unsloth/Qwen3.6-27B-MTP-GGUF",
hf_variant = "Q4_K_M",
n_ctx = 8192,
cache_type_kv = None,
speculative_type = None,
chat_template_override = None,
extra_args = None,
is_vision = True,
)
is True
)
def test_already_in_target_state_vision_mtp_default_matches():
backend = _mtp_backend(_is_vision = True)
assert (
backend._already_in_target_state(
gguf_path = None,
model_identifier = "unsloth/Qwen3.6-27B-MTP-GGUF",
hf_variant = "Q4_K_M",
n_ctx = 8192,
cache_type_kv = None,
speculative_type = "default",
chat_template_override = None,
extra_args = None,
is_vision = True,
)
is True
)
def test_already_in_target_state_vision_off_matches_vision_backend():
# Vision loads drop speculative decoding at the route level (req -> "off").
# _already_in_target_state compares canonical requested modes; a vision
# backend with _requested_spec_mode="off" matches req "off" or None+vision.
backend = _mtp_backend(
_model_identifier = "unsloth/Qwen3-VL-4B-Instruct-GGUF",
_is_vision = True,
_speculative_type = None,
_requested_spec_mode = "off",
)
assert (
backend._already_in_target_state(
gguf_path = None,
model_identifier = "unsloth/Qwen3-VL-4B-Instruct-GGUF",
hf_variant = "Q4_K_M",
n_ctx = 8192,
cache_type_kv = None,
speculative_type = "off",
chat_template_override = None,
extra_args = None,
is_vision = True,
)
is True
)
# GGUF-metadata-based detection (nextn_predict_layers).
@pytest.mark.parametrize(
"arch, nextn",
[
# Verified against real Unsloth MTP GGUFs (qwen35 / qwen35moe).
("qwen35", 1),
("qwen35moe", 1),
# Future-proofing: any arch + n>0 should match.
("qwen3moe", 2),
("hypothetical_future_arch", 4),
],
)
def test_read_gguf_metadata_captures_nextn_predict_layers(tmp_path, arch, nextn):
gguf = _write_minimal_gguf(
tmp_path / "model.gguf",
arch = arch,
nextn = nextn,
extra_uint32 = {f"{arch}.block_count": 4},
)
backend = LlamaCppBackend()
backend._read_gguf_metadata(str(gguf))
assert backend._nextn_predict_layers == nextn
def test_read_gguf_metadata_leaves_nextn_unset_for_non_mtp_arch(tmp_path):
gguf = _write_minimal_gguf(
tmp_path / "model.gguf",
arch = "qwen3",
nextn = None,
extra_uint32 = {"qwen3.block_count": 4},
)
backend = LlamaCppBackend()
backend._read_gguf_metadata(str(gguf))
assert backend._nextn_predict_layers is None
def test_read_gguf_metadata_zero_nextn_is_falsy(tmp_path):
# bool(0) is False, so the spec block short-circuits.
gguf = _write_minimal_gguf(
tmp_path / "model.gguf",
arch = "qwen35",
nextn = 0,
extra_uint32 = {"qwen35.block_count": 4},
)
backend = LlamaCppBackend()
backend._read_gguf_metadata(str(gguf))
assert backend._nextn_predict_layers == 0
assert bool(backend._nextn_predict_layers) is False
def test_unload_resets_nextn_predict_layers():
# MTP state from a previous load must not bleed into the next load.
backend = LlamaCppBackend()
backend._nextn_predict_layers = 1
backend.unload_model()
assert backend._nextn_predict_layers is None
# llama-server capability probe.
def _make_fake_llama_server(path: Path, help_text: str) -> Path:
"""Bash stub that prints `help_text` on --help."""
path.write_text(f"#!/usr/bin/env bash\ncat <<'EOF'\n{help_text}\nEOF\n")
path.chmod(0o755)
return path
_NEEDS_BASH = pytest.mark.skipif(
sys.platform == "win32",
reason = "fake llama-server is a bash stub; Windows has no direct executor",
)
def _clear_caps_cache():
LlamaCppBackend._capability_cache.clear()
@_NEEDS_BASH
def test_probe_server_capabilities_detects_draft_mtp(tmp_path):
# Original naming from llama.cpp #22673.
fake = _make_fake_llama_server(
tmp_path / "llama-server",
"--spec-type none,draft-simple,draft-eagle3,draft-mtp,"
"ngram-simple,ngram-map-k,ngram-map-k4v,ngram-mod,ngram-cache",
)
_clear_caps_cache()
caps = LlamaCppBackend.probe_server_capabilities(str(fake))
assert caps["found"] is True
assert caps["mtp_token"] == "draft-mtp"
assert caps["supports_mtp"] is True
@_NEEDS_BASH
def test_probe_server_capabilities_uses_binary_library_env(tmp_path, monkeypatch):
fake = _make_fake_llama_server(
tmp_path / "llama-server",
"--spec-type none,mtp,ngram-simple\n",
)
captured = {}
monkeypatch.setattr(
"core.inference.llama_cpp.child_env_without_native_path_secret",
lambda: {"LD_LIBRARY_PATH": "/already-there"},
)
def fake_run(cmd, **kwargs):
captured["cmd"] = cmd
captured["env"] = kwargs.get("env")
return _types.SimpleNamespace(stdout = "--spec-type none,mtp,ngram-simple\n", stderr = "")
monkeypatch.setattr("core.inference.llama_cpp.subprocess.run", fake_run)
_clear_caps_cache()
caps = LlamaCppBackend.probe_server_capabilities(str(fake))
assert caps["found"] is True
assert caps["supports_mtp"] is True
assert captured["cmd"] == [str(fake), "--help"]
assert captured["env"] is not None
ld_dirs = captured["env"]["LD_LIBRARY_PATH"].split(os.pathsep)
assert str(fake.parent) in ld_dirs
assert "/already-there" in ld_dirs
@_NEEDS_BASH
def test_probe_server_capabilities_detects_renamed_mtp(tmp_path):
# Renamed upstream: draft-mtp -> mtp.
fake = _make_fake_llama_server(
tmp_path / "llama-server",
"--spec-type [none|mtp|ngram-cache|ngram-simple|ngram-map-k|ngram-map-k4v|ngram-mod]",
)
_clear_caps_cache()
caps = LlamaCppBackend.probe_server_capabilities(str(fake))
assert caps["mtp_token"] == "mtp"
assert caps["supports_mtp"] is True
@_NEEDS_BASH
def test_probe_server_capabilities_reports_outdated_binary(tmp_path):
# Pre-MTP llama.cpp: only ngram variants.
fake = _make_fake_llama_server(
tmp_path / "llama-server",
"--spec-type none,ngram-simple,ngram-mod",
)
_clear_caps_cache()
caps = LlamaCppBackend.probe_server_capabilities(str(fake))
assert caps["found"] is True
assert caps["mtp_token"] is None
assert caps["supports_mtp"] is False
def test_probe_server_capabilities_handles_missing_binary():
_clear_caps_cache()
caps = LlamaCppBackend.probe_server_capabilities("/no/such/llama-server")
assert caps["found"] is False
assert caps["supports_mtp"] is False
assert caps["supports_cache_ram"] is False
assert caps["supports_ctx_checkpoints"] is False
assert caps["supports_no_cache_prompt"] is False
# ngram-mod flag flavor detection (new vs legacy llama-server).
# Help-text fixtures mirror the actual `llama-server --help` block
# layout (flag on its own line; description indented underneath).
_POST_RENAME_HELP = """\
--spec-draft-n-max N number of tokens to draft for speculative decoding (default: 16)
(env: LLAMA_ARG_SPEC_DRAFT_N_MAX)
--spec-draft-n-min N minimum number of draft tokens to use for speculative decoding (default: 0)
(env: LLAMA_ARG_SPEC_DRAFT_N_MIN)
--spec-draft-p-min, --draft-p-min P minimum speculative decoding probability (greedy) (default: 0.75)
(env: LLAMA_ARG_SPEC_DRAFT_P_MIN)
--spec-ngram-mod-n-min N minimum number of ngram tokens (default: 48)
--spec-ngram-mod-n-max N maximum number of ngram tokens (default: 64)
--spec-ngram-mod-n-match N ngram-mod lookup length (default: 24)
--spec-type none,draft-simple,draft-mtp,ngram-mod comma-separated list of types of speculative decoding to use
(env: LLAMA_ARG_SPEC_TYPE)
--draft, --draft-n, --draft-max N the argument has been removed. use --spec-draft-n-max or --spec-ngram-mod-n-max
(env: LLAMA_ARG_DRAFT_MAX)
--draft-min, --draft-n-min N the argument has been removed. use --spec-draft-n-min or --spec-ngram-mod-n-min
(env: LLAMA_ARG_DRAFT_MIN)
--spec-ngram-size-n N the argument has been removed. use the respective --spec-ngram-*-size-n or --spec-ngram-mod-n-match
"""
_LEGACY_HELP = """\
--draft, --draft-n, --draft-max N number of tokens to draft for speculative decoding (default: 8)
(env: LLAMA_ARG_DRAFT_MAX)
--draft-min, --draft-n-min N minimum number of draft tokens to use for speculative decoding (default: 0)
(env: LLAMA_ARG_DRAFT_MIN)
--spec-ngram-size-n N ngram lookup length (default: 24)
--spec-type none,ngram-mod,ngram-simple comma-separated list of types of speculative decoding to use
"""
_CACHE_FLAGS_HELP = """\
--cache-ram N store prompt cache in RAM (default: 0)
--ctx-checkpoints N number of context checkpoints (default: 0)
--no-cache-prompt do not reuse prompt cache
"""
@_NEEDS_BASH
def test_probe_detects_post_rename_ngram_mod_flavor(tmp_path):
fake = _make_fake_llama_server(tmp_path / "llama-server", _POST_RENAME_HELP)
_clear_caps_cache()
caps = LlamaCppBackend.probe_server_capabilities(str(fake))
assert caps["found"] is True
assert caps["ngram_mod_flavor"] == "new"
assert caps["supports_ngram_mod"] is True
assert caps["spec_draft_n_max_flag"] == "--spec-draft-n-max"
@_NEEDS_BASH
def test_probe_detects_legacy_ngram_mod_flavor(tmp_path):
fake = _make_fake_llama_server(tmp_path / "llama-server", _LEGACY_HELP)
_clear_caps_cache()
caps = LlamaCppBackend.probe_server_capabilities(str(fake))
assert caps["found"] is True
assert caps["ngram_mod_flavor"] == "legacy"
assert caps["supports_ngram_mod"] is True
assert caps["spec_draft_n_max_flag"] == "--draft-max"
@_NEEDS_BASH
def test_probe_ignores_removal_stub_descriptions(tmp_path):
# Post-rename binary: legacy flags present but with "argument has been
# removed" descriptions; must not be detected as legacy.
fake = _make_fake_llama_server(tmp_path / "llama-server", _POST_RENAME_HELP)
_clear_caps_cache()
caps = LlamaCppBackend.probe_server_capabilities(str(fake))
assert caps["ngram_mod_flavor"] == "new"
@_NEEDS_BASH
def test_probe_no_ngram_mod_on_minimal_binary(tmp_path):
# Pre-anything: neither set present.
fake = _make_fake_llama_server(
tmp_path / "llama-server",
"--spec-type none\n--threads N\n",
)
_clear_caps_cache()
caps = LlamaCppBackend.probe_server_capabilities(str(fake))
assert caps["ngram_mod_flavor"] is None
assert caps["supports_ngram_mod"] is False
@_NEEDS_BASH
def test_probe_detects_windows_cache_flags(tmp_path):
fake = _make_fake_llama_server(tmp_path / "llama-server", _CACHE_FLAGS_HELP)
_clear_caps_cache()
caps = LlamaCppBackend.probe_server_capabilities(str(fake))
assert caps["supports_cache_ram"] is True
assert caps["supports_ctx_checkpoints"] is True
assert caps["supports_no_cache_prompt"] is True
@_NEEDS_BASH
def test_probe_reports_windows_cache_flags_absent_for_older_binary(tmp_path):
fake = _make_fake_llama_server(tmp_path / "llama-server", "--threads N\n")
_clear_caps_cache()
caps = LlamaCppBackend.probe_server_capabilities(str(fake))
assert caps["supports_cache_ram"] is False
assert caps["supports_ctx_checkpoints"] is False
assert caps["supports_no_cache_prompt"] is False
def test_build_ngram_mod_flags_new():
flags = _build_ngram_mod_flags({"ngram_mod_flavor": "new"})
assert flags == [
"--spec-ngram-mod-n-match",
"24",
"--spec-ngram-mod-n-min",
"48",
"--spec-ngram-mod-n-max",
"64",
]
def test_build_ngram_mod_flags_legacy():
flags = _build_ngram_mod_flags({"ngram_mod_flavor": "legacy"})
assert flags == ["--spec-ngram-size-n", "24", "--draft-min", "48", "--draft-max", "64"]
def test_build_ngram_mod_flags_empty_when_unsupported():
assert _build_ngram_mod_flags({"ngram_mod_flavor": None}) == []
assert _build_ngram_mod_flags(None) == []
assert _build_ngram_mod_flags({}) == []
def test_build_ngram_mod_flags_respects_custom_values():
flags = _build_ngram_mod_flags({"ngram_mod_flavor": "new"}, n_match = 16, n_min = 24, n_max = 32)
assert flags == [
"--spec-ngram-mod-n-match",
"16",
"--spec-ngram-mod-n-min",
"24",
"--spec-ngram-mod-n-max",
"32",
]
@_NEEDS_BASH
def test_probe_server_capabilities_caches_by_mtime(tmp_path):
# Same (path, mtime) -> cache hit. Bumped mtime -> re-probe.
fake = _make_fake_llama_server(
tmp_path / "llama-server",
"--spec-type none,ngram-mod",
)
_clear_caps_cache()
caps1 = LlamaCppBackend.probe_server_capabilities(str(fake))
assert caps1["supports_mtp"] is False
import os
import time
_make_fake_llama_server(
fake,
"--spec-type none,draft-mtp,ngram-mod",
)
new_mtime = int(time.time()) + 2
os.utime(fake, (new_mtime, new_mtime))
caps2 = LlamaCppBackend.probe_server_capabilities(str(fake))
assert caps2["mtp_token"] == "draft-mtp"
assert caps2["supports_mtp"] is True
# spec_draft_n_max plumbing (first-class --spec-draft-n-max override).
def test_already_in_target_state_matches_when_draft_n_max_unset():
# None on the request means "platform default"; matches any backend.
backend = _mtp_backend(_spec_draft_n_max = None)
assert (
backend._already_in_target_state(
gguf_path = None,
model_identifier = "unsloth/Qwen3.6-27B-MTP-GGUF",
hf_variant = "Q4_K_M",
n_ctx = 8192,
cache_type_kv = None,
speculative_type = None,
spec_draft_n_max = None,
chat_template_override = None,
extra_args = None,
is_vision = False,
)
is True
)
def test_already_in_target_state_matches_when_draft_n_max_equals_backend():
backend = _mtp_backend(_spec_draft_n_max = 4)
assert (
backend._already_in_target_state(
gguf_path = None,
model_identifier = "unsloth/Qwen3.6-27B-MTP-GGUF",
hf_variant = "Q4_K_M",
n_ctx = 8192,
cache_type_kv = None,
speculative_type = None,
spec_draft_n_max = 4,
chat_template_override = None,
extra_args = None,
is_vision = False,
)
is True
)
def test_already_in_target_state_mismatches_when_draft_n_max_differs():
backend = _mtp_backend(_spec_draft_n_max = 4)
assert (
backend._already_in_target_state(
gguf_path = None,
model_identifier = "unsloth/Qwen3.6-27B-MTP-GGUF",
hf_variant = "Q4_K_M",
n_ctx = 8192,
cache_type_kv = None,
speculative_type = None,
spec_draft_n_max = 8,
chat_template_override = None,
extra_args = None,
is_vision = False,
)
is False
)
def test_already_in_target_state_draft_n_max_ignored_when_not_mtp():
# ngram-mod backend; spec_draft_n_max is MTP-only and must not force
# a reload against a non-MTP active spec.
backend = _mtp_backend(
_speculative_type = "ngram-mod",
_requested_spec_mode = "ngram",
_spec_draft_n_max = None,
)
assert (
backend._already_in_target_state(
gguf_path = None,
model_identifier = "unsloth/Qwen3.6-27B-MTP-GGUF",
hf_variant = "Q4_K_M",
n_ctx = 8192,
cache_type_kv = None,
speculative_type = "ngram-mod",
spec_draft_n_max = 8,
chat_template_override = None,
extra_args = None,
is_vision = False,
)
is True
)
# Sub-3B MTP gate -- tiny dense models regress with the MTP draft head, so
# load_model falls back to ngram-mod (when the binary supports it) instead of
# draft-mtp. The reload-skip mirror must follow the same fallback so a sub-3B
# reload-with-default doesn't bounce a correctly-configured ngram-mod/off backend.
def _patch_probe(monkeypatch, ngram_supported):
"""Force probe_server_capabilities to a deterministic result so tests
don't depend on whatever llama-server is on PATH."""
fake = {
"found": True,
"mtp_token": "draft-mtp",
"supports_mtp": True,
"ngram_mod_flavor": "new" if ngram_supported else None,
"supports_ngram_mod": bool(ngram_supported),
"spec_draft_n_max_flag": "--spec-draft-n-max",
}
monkeypatch.setattr(
LlamaCppBackend,
"probe_server_capabilities",
classmethod(lambda cls, binary = None: fake),
)
monkeypatch.setattr(
LlamaCppBackend,
"_find_llama_server_binary",
classmethod(lambda cls: "/fake/llama-server"),
)
def test_already_in_target_state_sub_3b_falls_back_to_ngram_mod_when_supported(monkeypatch):
# 0.8B MTP request -- load_model would have promoted to ngram-mod (no MTP
# head); reload check must match a ngram-mod backend.
_patch_probe(monkeypatch, ngram_supported = True)
backend = _mtp_backend(
_model_identifier = "unsloth/Qwen3.5-0.8B-MTP-GGUF",
_speculative_type = "ngram-mod",
_spec_draft_n_max = None,
)
assert (
backend._already_in_target_state(
gguf_path = None,
model_identifier = "unsloth/Qwen3.5-0.8B-MTP-GGUF",
hf_variant = "Q4_K_M",
n_ctx = 8192,
cache_type_kv = None,
speculative_type = None,
chat_template_override = None,
extra_args = None,
is_vision = False,
)
is True
)
def test_already_in_target_state_sub_3b_falls_back_to_off_when_no_ngram(monkeypatch):
# 0.8B + binary lacks ngram-mod -> fall back to off.
_patch_probe(monkeypatch, ngram_supported = False)
backend = _mtp_backend(
_model_identifier = "unsloth/Qwen3.5-0.8B-MTP-GGUF",
_speculative_type = None,
_spec_draft_n_max = None,
)
assert (
backend._already_in_target_state(
gguf_path = None,
model_identifier = "unsloth/Qwen3.5-0.8B-MTP-GGUF",
hf_variant = "Q4_K_M",
n_ctx = 8192,
cache_type_kv = None,
speculative_type = None,
chat_template_override = None,
extra_args = None,
is_vision = False,
)
is True
)
def test_already_in_target_state_4b_mtp_request_promotes_as_before(monkeypatch):
# 4B is above the 3B threshold -> auto-promote still applies.
_patch_probe(monkeypatch, ngram_supported = True)
backend = _mtp_backend(
_model_identifier = "unsloth/Qwen3.5-4B-MTP-GGUF",
_speculative_type = "draft-mtp",
_spec_draft_n_max = None,
)
assert (
backend._already_in_target_state(
gguf_path = None,
model_identifier = "unsloth/Qwen3.5-4B-MTP-GGUF",
hf_variant = "Q4_K_M",
n_ctx = 8192,
cache_type_kv = None,
speculative_type = None,
chat_template_override = None,
extra_args = None,
is_vision = False,
)
is True
)
def test_already_in_target_state_2b_falls_back_to_ngram_below_threshold(monkeypatch):
# 2.0B is below the 3B threshold -> ngram-mod fallback, not draft-mtp.
# Clean-bench shows 2B regresses with draft-mtp.
_patch_probe(monkeypatch, ngram_supported = True)
backend = _mtp_backend(
_model_identifier = "unsloth/Qwen3.5-2B-MTP-GGUF",
_speculative_type = "ngram-mod",
_spec_draft_n_max = None,
)
assert (
backend._already_in_target_state(
gguf_path = None,
model_identifier = "unsloth/Qwen3.5-2B-MTP-GGUF",
hf_variant = "Q4_K_M",
n_ctx = 8192,
cache_type_kv = None,
speculative_type = None,
chat_template_override = None,
extra_args = None,
is_vision = False,
)
is True
)
# usage backfill from timings (Studio UI t/s widget fix).
def test_backfill_usage_from_timings_fills_when_completion_tokens_zero():
out = _backfill_usage_from_timings(
{"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0},
{"prompt_n": 42, "predicted_n": 128, "predicted_per_second": 100.0},
)
assert out["completion_tokens"] == 128
assert out["prompt_tokens"] == 42
assert out["total_tokens"] == 170
def test_backfill_usage_from_timings_fills_when_usage_missing():
out = _backfill_usage_from_timings(
None,
{"prompt_n": 42, "predicted_n": 128, "predicted_per_second": 100.0},
)
assert out["completion_tokens"] == 128
assert out["prompt_tokens"] == 42
assert out["total_tokens"] == 170
def test_backfill_usage_from_timings_preserves_real_usage():
# Non-zero completion_tokens means llama-server reported correctly;
# do not overwrite.
real = {"prompt_tokens": 50, "completion_tokens": 200, "total_tokens": 250}
out = _backfill_usage_from_timings(real, {"predicted_n": 999, "prompt_n": 999})
assert out is real
assert out["completion_tokens"] == 200
def test_backfill_usage_from_timings_passthrough_when_timings_empty():
assert _backfill_usage_from_timings(None, None) is None
assert _backfill_usage_from_timings(None, {}) is None
usage = {"completion_tokens": 0}
# No timings.predicted_n -> nothing to fill, return as-is.
assert _backfill_usage_from_timings(usage, {"prompt_ms": 5.0}) is usage
# ── _canonicalize_spec_mode (pure) ─────────────────────────────────
@pytest.mark.parametrize(
"value, expected",
[
# New canonical values pass through unchanged.
("auto", "auto"),
("mtp", "mtp"),
("ngram", "ngram"),
("mtp+ngram", "mtp+ngram"),
("off", "off"),
("ngram-simple", "ngram-simple"),
# Legacy wire values map onto the new vocabulary.
("default", "auto"),
("draft-mtp", "mtp"),
("ngram-mod", "ngram"),
# Comma-chained legacy values (e.g. from persisted state) collapse
# to the right canonical mode.
("ngram-mod,draft-mtp", "mtp+ngram"),
("draft-mtp,ngram-mod", "mtp+ngram"),
("draft-mtp,mtp", "mtp"),
("ngram-mod,ngram", "ngram"),
# Case and whitespace are ignored.
(" AUTO ", "auto"),
("MTP", "mtp"),
("MTP+Ngram", "mtp+ngram"),
# None / empty / whitespace pass through as None.
(None, None),
("", None),
(" ", None),
# Non-string inputs collapse to None.
(42, None),
(True, None),
# Unknown strings fall back to "auto" (safe default).
("bogus", "auto"),
],
)
def test_canonicalize_spec_mode(value, expected):
assert _canonicalize_spec_mode(value) == expected
# ── _build_speculative_flags resolver matrix ──────────────────────
def _resolver_backend(
monkeypatch,
*,
ngram_supported = True,
mtp_token = "draft-mtp",
):
"""Backend with a deterministic probe so the resolver is hermetic."""
fake = {
"found": True,
"mtp_token": mtp_token,
"supports_mtp": bool(mtp_token),
"ngram_mod_flavor": "new" if ngram_supported else None,
"supports_ngram_mod": bool(ngram_supported),
"spec_draft_n_max_flag": "--spec-draft-n-max",
}
monkeypatch.setattr(
LlamaCppBackend,
"probe_server_capabilities",
classmethod(lambda cls, binary = None: fake),
)
backend = LlamaCppBackend()
backend._nextn_predict_layers = None
return backend
def _flags_dict(flags):
"""Parse the spec-flag list into a {flag: value} dict; collapses repeated
flags by keeping the last (only --spec-type can repeat, and never does
in our resolver)."""
out = {}
i = 0
while i < len(flags):
token = flags[i]
if i + 1 < len(flags) and not flags[i + 1].startswith("--"):
out[token] = flags[i + 1]
i += 2
else:
out[token] = True
i += 1
return out
_MTP_MODEL = "unsloth/Qwen3.6-27B-MTP-GGUF"
_NON_MTP_MODEL = "unsloth/Qwen3-7B-Instruct-GGUF"
_SUB_3B_MTP_MODEL = "unsloth/Qwen3.5-0.8B-MTP-GGUF"
@pytest.mark.parametrize(
"requested, gpus, model, expect_spec_type, expect_n_max, expect_ngram_knobs",
[
# ── auto + MTP model + 3B+: GPU = mtp only, CPU = chain ──
("auto", True, _MTP_MODEL, "draft-mtp", "2", False),
("auto", False, _MTP_MODEL, "ngram-mod,draft-mtp", "3", True),
# ── auto + non-MTP: emit --spec-default ──
("auto", True, _NON_MTP_MODEL, None, None, False),
("auto", False, _NON_MTP_MODEL, None, None, False),
# ── auto + sub-3B MTP: fallback to ngram-mod ──
("auto", True, _SUB_3B_MTP_MODEL, "ngram-mod", None, True),
("auto", False, _SUB_3B_MTP_MODEL, "ngram-mod", None, True),
# ── mtp forced: MTP-only on BOTH platforms ──
("mtp", True, _MTP_MODEL, "draft-mtp", "2", False),
("mtp", False, _MTP_MODEL, "draft-mtp", "3", False),
# ── mtp forced on sub-3B: engage anyway ──
("mtp", True, _SUB_3B_MTP_MODEL, "draft-mtp", "2", False),
# ── mtp forced on non-MTP: default back (no head/drafter) ──
("mtp", True, _NON_MTP_MODEL, None, None, False),
# ── ngram forced: ngram-mod alone on BOTH platforms ──
("ngram", True, _MTP_MODEL, "ngram-mod", None, True),
("ngram", False, _MTP_MODEL, "ngram-mod", None, True),
("ngram", True, _NON_MTP_MODEL, "ngram-mod", None, True),
# ── mtp+ngram forced: chain on BOTH platforms ──
("mtp+ngram", True, _MTP_MODEL, "ngram-mod,draft-mtp", "2", True),
("mtp+ngram", False, _MTP_MODEL, "ngram-mod,draft-mtp", "3", True),
("mtp+ngram", True, _SUB_3B_MTP_MODEL, "ngram-mod,draft-mtp", "2", True),
# ── mtp+ngram forced on non-MTP: keep ngram, drop draft-mtp ──
("mtp+ngram", True, _NON_MTP_MODEL, "ngram-mod", None, True),
# ── off: nothing emitted ──
("off", True, _MTP_MODEL, None, None, False),
("off", False, _MTP_MODEL, None, None, False),
# ── legacy values round-trip to the canonical emission ──
("default", True, _MTP_MODEL, "draft-mtp", "2", False),
("draft-mtp", True, _MTP_MODEL, "draft-mtp", "2", False),
("ngram-mod", True, _MTP_MODEL, "ngram-mod", None, True),
("ngram-mod,draft-mtp", False, _MTP_MODEL, "ngram-mod,draft-mtp", "3", True),
# ── ngram-simple: pass through ──
("ngram-simple", True, _MTP_MODEL, "ngram-simple", None, False),
],
)
def test_build_speculative_flags_matrix(
monkeypatch, requested, gpus, model, expect_spec_type, expect_n_max, expect_ngram_knobs
):
backend = _resolver_backend(monkeypatch)
flags = backend._build_speculative_flags(
speculative_type = requested,
spec_draft_n_max = None,
extra_args = None,
model_identifier = model,
model_path = None,
gpus = gpus,
binary = "/fake/llama-server",
)
parsed = _flags_dict(flags)
if expect_spec_type is None:
assert "--spec-type" not in parsed
else:
assert parsed.get("--spec-type") == expect_spec_type
if expect_n_max is None:
assert "--spec-draft-n-max" not in parsed
else:
assert parsed.get("--spec-draft-n-max") == expect_n_max
if expect_ngram_knobs:
assert "--spec-ngram-mod-n-match" in parsed
assert "--spec-ngram-mod-n-min" in parsed
assert "--spec-ngram-mod-n-max" in parsed
else:
assert "--spec-ngram-mod-n-match" not in parsed
def test_build_speculative_flags_user_extra_args_owns_spec_type(monkeypatch):
# User --spec-type in extra_args bypasses the dropdown entirely.
backend = _resolver_backend(monkeypatch)
flags = backend._build_speculative_flags(
speculative_type = "mtp", # would normally force MTP
spec_draft_n_max = None,
extra_args = ["--spec-type", "ngram-mod"],
model_identifier = _MTP_MODEL,
model_path = None,
gpus = True,
binary = "/fake/llama-server",
)
# Resolver emits nothing -- the user's extra_args carries the --spec-type,
# and the resolver records requested_spec_mode = None.
assert flags == []
assert backend.requested_spec_mode is None
assert backend.speculative_type is None
@pytest.mark.parametrize("mode", ["auto", "mtp", "ngram", "mtp+ngram", "off"])
def test_build_speculative_flags_round_trips_requested_mode(monkeypatch, mode):
# The status round-trip is the contract that lets the UI dropdown
# restore its picked value after reload / refresh.
backend = _resolver_backend(monkeypatch)
backend._build_speculative_flags(
speculative_type = mode,
spec_draft_n_max = None,
extra_args = None,
model_identifier = _MTP_MODEL,
model_path = None,
gpus = True,
binary = "/fake/llama-server",
)
assert backend.requested_spec_mode == mode
def test_build_speculative_flags_user_draft_n_max_override(monkeypatch):
backend = _resolver_backend(monkeypatch)
flags = backend._build_speculative_flags(
speculative_type = "mtp",
spec_draft_n_max = 5,
extra_args = None,
model_identifier = _MTP_MODEL,
model_path = None,
gpus = True,
binary = "/fake/llama-server",
)
parsed = _flags_dict(flags)
assert parsed.get("--spec-draft-n-max") == "5"
assert backend.spec_draft_n_max == 5
def test_build_speculative_flags_mtp_token_missing_emits_spec_default(monkeypatch):
# Outdated llama-server with no MTP support: forced MTP must degrade (warned)
# and emit --spec-default so an inherited LLAMA_ARG_SPEC_TYPE=draft-mtp (CLI
# wins over env) can't make the child attempt MTP the gate budgeted off.
backend = _resolver_backend(monkeypatch, mtp_token = None)
flags = backend._build_speculative_flags(
speculative_type = "mtp",
spec_draft_n_max = None,
extra_args = None,
model_identifier = _MTP_MODEL,
model_path = None,
gpus = True,
binary = "/fake/llama-server",
)
assert "--spec-type" not in flags
assert "--spec-default" in flags
# Degraded to non-speculative; the user's choice is still reflected.
assert backend.speculative_type == "default"
assert backend.requested_spec_mode == "mtp"
assert backend.spec_fallback_reason == "binary_no_mtp"
def test_forced_mtp_on_non_mtp_model_defaults_back(monkeypatch):
# Forcing MTP on a model with no head/drafter must NOT emit draft-mtp:
# llama-server aborts on it ("failed to measure MTP context memory")
# rather than no-op'ing. Default back to --spec-default instead.
backend = _resolver_backend(monkeypatch)
flags = backend._build_speculative_flags(
speculative_type = "mtp",
spec_draft_n_max = None,
extra_args = None,
model_identifier = _NON_MTP_MODEL,
model_path = None,
gpus = True,
binary = "/fake/llama-server",
)
assert "--spec-type" not in flags
assert "--spec-default" in flags
assert backend.speculative_type == "default"
assert backend.requested_spec_mode == "mtp"
def test_forced_mtp_ngram_on_non_mtp_model_keeps_ngram(monkeypatch):
# mtp+ngram on a non-MTP model drops the doomed draft-mtp chain but keeps
# the ngram half, which needs no head.
backend = _resolver_backend(monkeypatch)
flags = backend._build_speculative_flags(
speculative_type = "mtp+ngram",
spec_draft_n_max = None,
extra_args = None,
model_identifier = _NON_MTP_MODEL,
model_path = None,
gpus = True,
binary = "/fake/llama-server",
)
parsed = _flags_dict(flags)
assert parsed.get("--spec-type") == "ngram-mod"
assert backend.speculative_type == "ngram-mod"
assert backend.requested_spec_mode == "mtp+ngram"
# ── Auto drops embedded MTP for MLA models (GLM-5.2 et al.) ───────────
#
# llama.cpp's MLA/DSA MTP path runs ~2x slower than no speculation (GLM-5.2
# bench), so Auto downgrades it to ngram-mod (or spec-off). The clean
# metadata separator from non-MLA MTP (Qwen, kept on draft-mtp) is
# self._kv_lora_rank. Forced mtp / mtp+ngram and separate drafters (Gemma)
# stay on draft-mtp; UNSLOTH_MLA_MTP_ENABLED=1 re-enables Auto promotion.
# GLM-5.2's repo name has no "MTP" marker, so its MTP signal is metadata-only
# (nextn_predict_layers) -- exactly the embedded-MLA case we gate.
_GLM_MLA_MODEL = "unsloth/GLM-5.2-GGUF"
def _mla_resolver_backend(
monkeypatch,
*,
ngram_supported = True,
kv_lora_rank = 512,
nextn = 1,
):
"""Resolver backend posing as an embedded-MTP MLA model (kv_lora_rank set)."""
backend = _resolver_backend(monkeypatch, ngram_supported = ngram_supported)
backend._nextn_predict_layers = nextn
backend._kv_lora_rank = kv_lora_rank
return backend
@pytest.mark.parametrize("gpus", [True, False])
def test_auto_mla_embedded_mtp_falls_back_to_ngram(monkeypatch, gpus):
# Auto + MLA embedded MTP + ngram supported -> ngram-mod on BOTH platforms
# (the CPU chain ngram-mod,draft-mtp is dropped: no draft-mtp for MLA).
backend = _mla_resolver_backend(monkeypatch)
flags = backend._build_speculative_flags(
speculative_type = "auto",
spec_draft_n_max = None,
extra_args = None,
model_identifier = _GLM_MLA_MODEL,
model_path = None,
gpus = gpus,
binary = "/fake/llama-server",
)
parsed = _flags_dict(flags)
assert parsed.get("--spec-type") == "ngram-mod"
assert "--spec-draft-n-max" not in parsed
assert "--spec-ngram-mod-n-match" in parsed
assert backend.speculative_type == "ngram-mod"
assert backend.requested_spec_mode == "auto"
assert backend.spec_fallback_reason == "mla_mtp_disabled"
assert backend.spec_draft_n_max is None
def test_auto_mla_embedded_mtp_no_ngram_disables_spec(monkeypatch):
# Auto + MLA embedded MTP + no ngram-mod support -> emit nothing (spec-off),
# mirroring the sub-3B no-ngram path. Still flagged as a policy downgrade.
backend = _mla_resolver_backend(monkeypatch, ngram_supported = False)
flags = backend._build_speculative_flags(
speculative_type = "auto",
spec_draft_n_max = None,
extra_args = None,
model_identifier = _GLM_MLA_MODEL,
model_path = None,
gpus = True,
binary = "/fake/llama-server",
)
assert "--spec-type" not in flags
assert backend.speculative_type is None
assert backend.requested_spec_mode == "auto"
assert backend.spec_fallback_reason == "mla_mtp_disabled"
def test_auto_non_mla_embedded_mtp_keeps_draft_mtp(monkeypatch):
# Auto + embedded MTP + NON-MLA (kv_lora_rank None, e.g. Qwen) -> unchanged:
# still draft-mtp at the platform default. No policy downgrade.
backend = _mla_resolver_backend(monkeypatch, kv_lora_rank = None)
flags = backend._build_speculative_flags(
speculative_type = "auto",
spec_draft_n_max = None,
extra_args = None,
model_identifier = _MTP_MODEL,
model_path = None,
gpus = True,
binary = "/fake/llama-server",
)
parsed = _flags_dict(flags)
assert parsed.get("--spec-type") == "draft-mtp"
assert parsed.get("--spec-draft-n-max") == "2"
assert backend.speculative_type == "draft-mtp"
assert backend.spec_fallback_reason is None
def test_auto_mla_separate_drafter_keeps_mtp(monkeypatch):
# Auto + MLA + a separate drafter (mtp_draft_path) -> the drafter exemption
# wins over the MLA gate: still draft-mtp (Gemma-style external drafter is
# not the slow embedded MLA/DSA path).
backend = _mla_resolver_backend(monkeypatch)
flags = backend._build_speculative_flags(
speculative_type = "auto",
spec_draft_n_max = None,
extra_args = None,
model_identifier = _GLM_MLA_MODEL,
model_path = None,
gpus = True,
binary = "/fake/llama-server",
mtp_draft_path = "/fake/mtp-draft.gguf",
)
parsed = _flags_dict(flags)
assert parsed.get("--spec-type") == "draft-mtp"
assert backend.speculative_type == "draft-mtp"
assert backend.spec_fallback_reason is None
def test_auto_non_mtp_mla_model_unaffected(monkeypatch):
# Auto + MLA but NO embedded MTP head (kv_lora_rank set, nextn None, e.g.
# GLM-4.7-Flash) -> non-MTP default; no accidental ngram drop.
backend = _mla_resolver_backend(monkeypatch, nextn = None)
flags = backend._build_speculative_flags(
speculative_type = "auto",
spec_draft_n_max = None,
extra_args = None,
model_identifier = "unsloth/GLM-4.7-Flash-GGUF",
model_path = None,
gpus = True,
binary = "/fake/llama-server",
)
assert "--spec-default" in flags
assert "ngram-mod" not in flags
assert backend.speculative_type == "default"
assert backend.spec_fallback_reason is None
@pytest.mark.parametrize(
"mode, expect_spec_type, expect_n_max",
[
("mtp", "draft-mtp", "2"),
("mtp+ngram", "ngram-mod,draft-mtp", "2"),
],
)
def test_forced_mtp_on_mla_still_engages(monkeypatch, mode, expect_spec_type, expect_n_max):
# Explicit override engages the deliberately-slower MTP route on MLA models,
# regardless of the Auto gate. No policy downgrade reason.
backend = _mla_resolver_backend(monkeypatch)
flags = backend._build_speculative_flags(
speculative_type = mode,
spec_draft_n_max = None,
extra_args = None,
model_identifier = _GLM_MLA_MODEL,
model_path = None,
gpus = True,
binary = "/fake/llama-server",
)
parsed = _flags_dict(flags)
assert parsed.get("--spec-type") == expect_spec_type
assert parsed.get("--spec-draft-n-max") == expect_n_max
assert backend.speculative_type == "draft-mtp"
assert backend.requested_spec_mode == mode
assert backend.spec_fallback_reason is None
def test_env_flag_reenables_auto_mla_mtp(monkeypatch):
# UNSLOTH_MLA_MTP_ENABLED=1 -> Auto promotes MLA embedded MTP to draft-mtp
# again (the forward hook for when llama.cpp optimizes the path).
monkeypatch.setenv("UNSLOTH_MLA_MTP_ENABLED", "1")
backend = _mla_resolver_backend(monkeypatch)
flags = backend._build_speculative_flags(
speculative_type = "auto",
spec_draft_n_max = None,
extra_args = None,
model_identifier = _GLM_MLA_MODEL,
model_path = None,
gpus = True,
binary = "/fake/llama-server",
)
parsed = _flags_dict(flags)
assert parsed.get("--spec-type") == "draft-mtp"
assert backend.speculative_type == "draft-mtp"
assert backend.spec_fallback_reason is None
@pytest.mark.parametrize("value", ["1", "true", "yes", "on", "TRUE", "On"])
def test_mla_mtp_auto_enabled_truthy_values(monkeypatch, value):
monkeypatch.setenv("UNSLOTH_MLA_MTP_ENABLED", value)
assert _mla_mtp_auto_enabled() is True
@pytest.mark.parametrize("value", ["0", "false", "no", "off", "", " ", "bogus"])
def test_mla_mtp_auto_disabled_default_and_falsy(monkeypatch, value):
monkeypatch.setenv("UNSLOTH_MLA_MTP_ENABLED", value)
assert _mla_mtp_auto_enabled() is False
def test_mla_mtp_auto_disabled_when_unset(monkeypatch):
monkeypatch.delenv("UNSLOTH_MLA_MTP_ENABLED", raising = False)
assert _mla_mtp_auto_enabled() is False
def test_read_gguf_metadata_captures_kv_lora_rank(tmp_path):
# GLM-5.2-style header: MLA (kv_lora_rank) + embedded MTP (nextn) populate
# both fields, so the Auto gate sees an MLA embedded-MTP model.
gguf = _write_minimal_gguf(
tmp_path / "model.gguf",
arch = "glm-dsa",
nextn = 1,
extra_uint32 = {
"glm-dsa.block_count": 4,
"glm-dsa.attention.kv_lora_rank": 512,
},
)
backend = LlamaCppBackend()
backend._read_gguf_metadata(str(gguf))
assert backend._nextn_predict_layers == 1
assert backend._kv_lora_rank == 512
def test_read_gguf_metadata_qwen_mtp_has_no_kv_lora_rank(tmp_path):
# Qwen MTP header: embedded MTP but non-MLA, so kv_lora_rank stays None and
# Auto keeps it on draft-mtp.
gguf = _write_minimal_gguf(
tmp_path / "model.gguf",
arch = "qwen35moe",
nextn = 1,
extra_uint32 = {"qwen35moe.block_count": 4},
)
backend = LlamaCppBackend()
backend._read_gguf_metadata(str(gguf))
assert backend._nextn_predict_layers == 1
assert backend._kv_lora_rank is None
def test_reload_skip_auto_mla_ngram_is_idempotent():
# A GLM model resolved to ngram-mod under Auto must not churn: a duplicate
# Auto /load at the same settings is already-satisfied.
backend = _mtp_backend(
_model_identifier = _GLM_MLA_MODEL,
_speculative_type = "ngram-mod",
_requested_spec_mode = "auto",
)
assert (
backend._already_in_target_state(
gguf_path = None,
model_identifier = _GLM_MLA_MODEL,
hf_variant = "Q4_K_M",
n_ctx = 8192,
cache_type_kv = None,
speculative_type = "auto",
chat_template_override = None,
extra_args = None,
is_vision = False,
)
is True
)
def test_reload_forced_mtp_bounces_auto_mla():
# Overriding Auto (ngram-mod) with a forced mtp request must reload (to the
# slower draft-mtp route), not dedup against the running ngram-mod server.
backend = _mtp_backend(
_model_identifier = _GLM_MLA_MODEL,
_speculative_type = "ngram-mod",
_requested_spec_mode = "auto",
)
assert (
backend._already_in_target_state(
gguf_path = None,
model_identifier = _GLM_MLA_MODEL,
hf_variant = "Q4_K_M",
n_ctx = 8192,
cache_type_kv = None,
speculative_type = "mtp",
chat_template_override = None,
extra_args = None,
is_vision = False,
)
is False
)
# ── Full named-repo resolver matrix (the shipping Studio families) ─────
#
# Locks auto / off / forced-mtp routing for every Qwen3.5 (MTP + plain) and
# gemma-4 (regular + QAT) GGUF repo, including the giant MoEs that stay
# resolver-only (122B-A10B / 397B-A17B). Expectations are derived from the
# same signals load_model uses -- _extract_model_size_b (active>effective>
# total, so E2B->2, A3B->3, A10B->10, A17B->17), _is_mtp_model_name, and the
# separate-drafter flag -- so each row mirrors what the loader emits on a
# B200 (GPU default, n=2). gemma carries no -MTP marker; its MTP comes from
# the root mtp-*.gguf drafter, modelled here by passing mtp_draft_path.
#
# auto_spec: "draft-mtp" = head/drafter engaged (>=3B MTP, or any size with a
# separate drafter); "ngram-mod" = embedded sub-3B drop (zero-VRAM); None =
# non-MTP -> llama-server --spec-default.
_GEMMA_DRAFTER = "/snap/mtp-gemma-4-it.gguf" # stand-in separate drafter
_REAL_REPO_MATRIX = [
# repo, drafter, auto_spec, auto_ngram_knobs
("unsloth/Qwen3.5-0.8B-MTP-GGUF", None, "ngram-mod", True),
("unsloth/Qwen3.5-2B-MTP-GGUF", None, "ngram-mod", True),
("unsloth/Qwen3.5-4B-MTP-GGUF", None, "draft-mtp", False),
("unsloth/Qwen3.5-9B-MTP-GGUF", None, "draft-mtp", False),
("unsloth/Qwen3.5-27B-MTP-GGUF", None, "draft-mtp", False),
("unsloth/Qwen3.5-35B-A3B-MTP-GGUF", None, "draft-mtp", False),
("unsloth/Qwen3.5-122B-A10B-MTP-GGUF", None, "draft-mtp", False),
("unsloth/Qwen3.5-397B-A17B-MTP-GGUF", None, "draft-mtp", False),
("unsloth/Qwen3.5-0.8B-GGUF", None, None, False),
("unsloth/Qwen3.5-2B-GGUF", None, None, False),
("unsloth/Qwen3.5-4B-GGUF", None, None, False),
("unsloth/Qwen3.5-9B-GGUF", None, None, False),
# E2B is 2B but ships a separate drafter -> exempt from the sub-3B drop.
("unsloth/gemma-4-E2B-it-GGUF", _GEMMA_DRAFTER, "draft-mtp", False),
("unsloth/gemma-4-E4B-it-GGUF", _GEMMA_DRAFTER, "draft-mtp", False),
("unsloth/gemma-4-12b-it-GGUF", _GEMMA_DRAFTER, "draft-mtp", False),
("unsloth/gemma-4-26B-A4B-it-GGUF", _GEMMA_DRAFTER, "draft-mtp", False),
("unsloth/gemma-4-31B-it-GGUF", _GEMMA_DRAFTER, "draft-mtp", False),
("unsloth/gemma-4-E2B-it-qat-GGUF", _GEMMA_DRAFTER, "draft-mtp", False),
("unsloth/gemma-4-E4B-it-qat-GGUF", _GEMMA_DRAFTER, "draft-mtp", False),
("unsloth/gemma-4-12b-it-qat-GGUF", _GEMMA_DRAFTER, "draft-mtp", False),
("unsloth/gemma-4-26B-A4B-it-qat-GGUF", _GEMMA_DRAFTER, "draft-mtp", False),
("unsloth/gemma-4-31B-it-qat-GGUF", _GEMMA_DRAFTER, "draft-mtp", False),
]
def _resolve_real(monkeypatch, repo, drafter, mode):
backend = _resolver_backend(monkeypatch)
if "qwen" in repo.lower() and "-mtp" in repo.lower():
backend._nextn_predict_layers = 1
flags = backend._build_speculative_flags(
speculative_type = mode,
spec_draft_n_max = None,
extra_args = None,
model_identifier = repo,
model_path = None,
gpus = True, # B200 default
binary = "/fake/llama-server",
mtp_draft_path = drafter,
)
return backend, flags, _flags_dict(flags)
@pytest.mark.parametrize(
"repo, drafter, auto_spec, auto_ngram_knobs",
_REAL_REPO_MATRIX,
ids = [r[0].split("/")[-1] for r in _REAL_REPO_MATRIX],
)
def test_real_repo_auto_routing(monkeypatch, repo, drafter, auto_spec, auto_ngram_knobs):
# Auto is the default mode the dropdown ships with.
backend, flags, parsed = _resolve_real(monkeypatch, repo, drafter, "auto")
if auto_spec is None:
# Non-MTP: no draft-mtp, hand off to llama-server's own default.
assert "--spec-type" not in parsed
assert "--spec-default" in flags
assert backend.speculative_type == "default"
elif auto_spec == "draft-mtp":
assert parsed.get("--spec-type") == "draft-mtp"
assert parsed.get("--spec-draft-n-max") == "2"
assert backend.speculative_type == "draft-mtp"
# gemma ships a separate drafter; Qwen bakes the head into the GGUF.
assert (
(parsed.get("--model-draft") == drafter) if drafter else ("--model-draft" not in parsed)
)
else: # ngram-mod (sub-3B MTP drop)
assert parsed.get("--spec-type") == "ngram-mod"
assert "--model-draft" not in parsed # draft head dropped
assert backend.speculative_type == "ngram-mod"
if auto_ngram_knobs:
assert "--spec-ngram-mod-n-match" in parsed
assert backend.requested_spec_mode == "auto"
@pytest.mark.parametrize(
"repo, drafter",
[(r[0], r[1]) for r in _REAL_REPO_MATRIX],
ids = [r[0].split("/")[-1] for r in _REAL_REPO_MATRIX],
)
def test_real_repo_off_emits_nothing(monkeypatch, repo, drafter):
# Off must suppress speculative decoding for every family.
backend, flags, _ = _resolve_real(monkeypatch, repo, drafter, "off")
assert flags == []
assert backend.speculative_type is None
assert backend.requested_spec_mode == "off"
@pytest.mark.parametrize(
"repo, drafter",
[(r[0], r[1]) for r in _REAL_REPO_MATRIX],
ids = [r[0].split("/")[-1] for r in _REAL_REPO_MATRIX],
)
def test_real_repo_forced_mtp_never_aborts(monkeypatch, repo, drafter):
# Forcing MTP on the dropdown: real MTP models (name marker or separate
# drafter) engage draft-mtp even below 3B; non-MTP models default back to
# --spec-default instead of emitting a draft-mtp llama-server will abort on.
backend, flags, parsed = _resolve_real(monkeypatch, repo, drafter, "mtp")
is_real_mtp = _is_mtp_model_name(repo) or bool(drafter)
if is_real_mtp:
assert parsed.get("--spec-type") == "draft-mtp"
assert backend.speculative_type == "draft-mtp"
assert (
(parsed.get("--model-draft") == drafter) if drafter else ("--model-draft" not in parsed)
)
else:
assert "--spec-type" not in parsed
assert "--spec-default" in flags
assert backend.speculative_type == "default"
assert backend.requested_spec_mode == "mtp"
# ── Sub-3B separate-drafter exemption (Gemma) ─────────────────────────
#
# The sub-3B MTP drop is an embedded-head cost (Qwen). A separate drafter
# (Gemma's root mtp-*.gguf) is a cheap standalone model that wins below 3B
# (B200 Q4_K_XL: gemma-4-E2B draft-mtp n=2 = 1.21x vs OFF), so it is exempt.
def test_sub3b_gemma_separate_drafter_engages_mtp(monkeypatch):
backend = _resolver_backend(monkeypatch)
flags = backend._build_speculative_flags(
speculative_type = "auto",
spec_draft_n_max = None,
extra_args = None,
model_identifier = "unsloth/gemma-4-E2B-it-GGUF", # 2B
model_path = None,
gpus = True,
binary = "/fake/llama-server",
mtp_draft_path = "/snap/mtp-gemma-4-E2B-it.gguf", # separate drafter
)
parsed = _flags_dict(flags)
assert parsed.get("--spec-type") == "draft-mtp"
assert parsed.get("--model-draft") == "/snap/mtp-gemma-4-E2B-it.gguf"
assert "--spec-ngram-mod-n-match" not in parsed
assert backend.speculative_type == "draft-mtp"
def test_sub3b_qwen_embedded_head_still_drops_to_ngram(monkeypatch):
backend = _resolver_backend(monkeypatch)
flags = backend._build_speculative_flags(
speculative_type = "auto",
spec_draft_n_max = None,
extra_args = None,
model_identifier = "unsloth/Qwen3.5-2B-MTP-GGUF", # 2B, embedded head
model_path = None,
gpus = True,
binary = "/fake/llama-server",
mtp_draft_path = None, # no separate drafter
)
parsed = _flags_dict(flags)
assert parsed.get("--spec-type") == "ngram-mod"
assert "--model-draft" not in parsed
assert backend.speculative_type == "ngram-mod"
def test_auto_mode_drops_mtp_exempts_separate_drafter():
from core.inference.llama_cpp import _auto_mode_drops_mtp
assert _auto_mode_drops_mtp("auto", 2.0) is True
assert _auto_mode_drops_mtp("auto", 2.0, has_separate_drafter = True) is False
assert _auto_mode_drops_mtp("auto", 4.0) is False
assert _auto_mode_drops_mtp("mtp", 2.0) is False # forced engages regardless
# ── spec_fallback_reason (drives the "update llama.cpp" UI hint) ───────
def test_spec_fallback_reason_set_when_binary_lacks_mtp(monkeypatch):
# Outdated llama-server with no mtp token: a forced MTP request can't emit
# draft-mtp, so record the reason for the UI update affordance.
backend = _resolver_backend(monkeypatch, mtp_token = None)
backend._build_speculative_flags(
speculative_type = "mtp",
spec_draft_n_max = None,
extra_args = None,
model_identifier = _MTP_MODEL,
model_path = None,
gpus = True,
binary = "/fake/llama-server",
)
assert backend.spec_fallback_reason == "binary_no_mtp"
def test_spec_fallback_reason_none_when_mtp_engages(monkeypatch):
backend = _resolver_backend(monkeypatch)
backend._build_speculative_flags(
speculative_type = "auto",
spec_draft_n_max = None,
extra_args = None,
model_identifier = _MTP_MODEL,
model_path = None,
gpus = True,
binary = "/fake/llama-server",
)
assert backend.speculative_type == "draft-mtp"
assert backend.spec_fallback_reason is None
def test_spec_fallback_reason_reset_on_off(monkeypatch):
# A subsequent off load must clear a stale reason.
backend = _resolver_backend(monkeypatch, mtp_token = None)
backend._build_speculative_flags(
speculative_type = "mtp",
spec_draft_n_max = None,
extra_args = None,
model_identifier = _MTP_MODEL,
model_path = None,
gpus = True,
binary = "/fake/llama-server",
)
assert backend.spec_fallback_reason == "binary_no_mtp"
backend._build_speculative_flags(
speculative_type = "off",
spec_draft_n_max = None,
extra_args = None,
model_identifier = _MTP_MODEL,
model_path = None,
gpus = True,
binary = "/fake/llama-server",
)
assert backend.spec_fallback_reason is None
def test_is_gemma_mtp_family():
from core.inference.llama_cpp import _is_gemma_mtp_family
assert _is_gemma_mtp_family("unsloth/gemma-4-E4B-it-GGUF") is True
assert _is_gemma_mtp_family("unsloth/gemma-4-12b-it-GGUF") is True
# gemma-3n ships no separate drafter, so it is not a drafter family.
assert _is_gemma_mtp_family("unsloth/gemma-3n-E2B-it-GGUF") is False
assert _is_gemma_mtp_family("unsloth/Qwen3.5-35B-A3B-MTP-GGUF") is False
assert _is_gemma_mtp_family("unsloth/llama-3-8b") is False
def test_gemma_3n_without_drafter_is_not_mtp(monkeypatch):
# gemma-3n ships no drafter; it must take the normal non-MTP path, not
# drafter_not_found (which would make every reload retry a missing drafter).
backend = _resolver_backend(monkeypatch)
backend._build_speculative_flags(
speculative_type = "auto",
spec_draft_n_max = None,
extra_args = None,
model_identifier = "unsloth/gemma-3n-E4B-it-GGUF",
model_path = None,
gpus = True,
binary = "/fake/llama-server",
mtp_draft_path = None,
)
assert backend.spec_fallback_reason is None
def test_spec_fallback_reason_drafter_not_found(monkeypatch):
# Drafterless Gemma should fall back to ngram-mod + drafter_not_found.
backend = _resolver_backend(monkeypatch)
flags = backend._build_speculative_flags(
speculative_type = "auto",
spec_draft_n_max = None,
extra_args = None,
model_identifier = "unsloth/gemma-4-E4B-it-GGUF",
model_path = None,
gpus = True,
binary = "/fake/llama-server",
mtp_draft_path = None, # Drafter download failed
)
parsed = _flags_dict(flags)
assert parsed.get("--spec-type") == "ngram-mod"
assert backend.speculative_type == "ngram-mod"
assert backend.spec_fallback_reason == "drafter_not_found"
def test_is_gemma_mtp_name_none_safe():
# model_identifier=None (local load) must not raise; recognise via filename.
from core.inference.llama_cpp import _is_gemma_mtp_family, _is_gemma_mtp_name
assert _is_gemma_mtp_family(None) is False
assert _is_gemma_mtp_name(None, "/models/gemma-4-E4B-it-Q4_K_M.gguf") is True
assert _is_gemma_mtp_name("unsloth/Qwen3.5-4B-MTP-GGUF", None) is False
@pytest.mark.parametrize("mode", ["mtp", "mtp+ngram"])
def test_forced_mtp_gemma_without_drafter_falls_back(monkeypatch, mode):
# Forced MTP on a drafterless Gemma must fall back, not emit draft-mtp.
backend = _resolver_backend(monkeypatch)
flags = backend._build_speculative_flags(
speculative_type = mode,
spec_draft_n_max = None,
extra_args = None,
model_identifier = "unsloth/gemma-4-E4B-it-GGUF",
model_path = None,
gpus = True,
binary = "/fake/llama-server",
mtp_draft_path = None,
)
parsed = _flags_dict(flags)
assert parsed.get("--spec-type") == "ngram-mod"
assert "--model-draft" not in parsed
assert backend.spec_fallback_reason == "drafter_not_found"
def test_local_gemma_gguf_without_identifier_falls_back(monkeypatch):
# Local Gemma GGUF (family only in filename) must not crash; falls back.
backend = _resolver_backend(monkeypatch)
flags = backend._build_speculative_flags(
speculative_type = "auto",
spec_draft_n_max = None,
extra_args = None,
model_identifier = None,
model_path = "/models/gemma-4-E4B-it-Q4_K_M.gguf",
gpus = True,
binary = "/fake/llama-server",
mtp_draft_path = None,
)
parsed = _flags_dict(flags)
assert parsed.get("--spec-type") == "ngram-mod"
assert backend.spec_fallback_reason == "drafter_not_found"
def _drafter_not_found_kwargs():
return dict(
model_identifier = "unsloth/gemma-4-E4B-it-GGUF",
hf_variant = "Q4_K_M",
n_ctx = 8192,
cache_type_kv = None,
speculative_type = "auto",
chat_template_override = None,
extra_args = None,
is_vision = False,
gguf_path = None, # HF load: drafter resolves inside load_model
)
def test_already_in_target_state_retries_after_hf_drafter_not_found():
# Recoverable drafter_not_found must not dedupe; reload re-attempts download.
backend = _mtp_backend(
_model_identifier = "unsloth/gemma-4-E4B-it-GGUF",
_speculative_type = "ngram-mod",
_spec_fallback_reason = "drafter_not_found",
_mtp_draft_path = None,
_gguf_path = None,
)
assert backend._already_in_target_state(**_drafter_not_found_kwargs()) is False
# Sanity: with no fallback reason the same request still dedupes (matches).
ok = _mtp_backend(_model_identifier = "unsloth/gemma-4-E4B-it-GGUF", _gguf_path = None)
assert ok._already_in_target_state(**_drafter_not_found_kwargs()) is True