mirror of https://github.com/razor-ai/soup.git
293 lines
11 KiB
Python
293 lines
11 KiB
Python
"""v0.53.0 Parts A+B — UD GGUF + IQ / Apple-ARM quant schema helpers.
|
|
|
|
Schema-only support for Unsloth Dynamic 2.0 GGUF ladder (``UD-Q8_K_XL`` …
|
|
``UD-IQ1_M``), the IQ1/IQ2/IQ3 family, the Apple/ARM-friendly Q4_NL /
|
|
Q5.x / Q4.x variants, and the existing TQ1_0 1.58-bit GGUF flavour (from
|
|
v0.52.0 Part D, re-exposed here for ``soup export --format gguf-iq``).
|
|
|
|
Live llama.cpp ``imatrix`` calibration + actual GGUF write are deferred
|
|
to v0.53.1 (mirrors v0.50.0 stub-then-live pattern).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from types import MappingProxyType
|
|
from typing import Mapping
|
|
|
|
# --- Part A: Unsloth Dynamic 2.0 GGUF ladder ---------------------------------
|
|
UD_GGUF_FORMATS: frozenset[str] = frozenset({
|
|
"UD-Q8_K_XL",
|
|
"UD-Q6_K_XL",
|
|
"UD-Q5_K_XL",
|
|
"UD-Q4_K_XL",
|
|
"UD-Q3_K_XL",
|
|
"UD-Q2_K_XL",
|
|
"UD-IQ4_XS",
|
|
"UD-IQ3_M",
|
|
"UD-IQ3_XXS",
|
|
"UD-IQ2_M",
|
|
"UD-IQ2_XS",
|
|
"UD-IQ2_XXS",
|
|
"UD-IQ1_M",
|
|
"UD-IQ1_S",
|
|
})
|
|
|
|
# --- Part B: IQ + Apple/ARM quant flavours -----------------------------------
|
|
IQ_GGUF_FORMATS: frozenset[str] = frozenset({
|
|
"IQ1_S",
|
|
"IQ1_M",
|
|
"IQ2_XXS",
|
|
"IQ2_XS",
|
|
"IQ2_S",
|
|
"IQ2_M",
|
|
"IQ3_XXS",
|
|
"IQ3_XS",
|
|
"IQ3_S",
|
|
"IQ3_M",
|
|
"IQ4_XS",
|
|
"IQ4_NL",
|
|
})
|
|
APPLE_ARM_GGUF_FORMATS: frozenset[str] = frozenset({
|
|
"Q4_0_4_4",
|
|
"Q4_0_4_8",
|
|
"Q4_0_8_8",
|
|
"Q4_NL",
|
|
"Q5_0",
|
|
"Q5_1",
|
|
"Q5_K_S",
|
|
"Q5_K_M",
|
|
"Q4_K_S",
|
|
"Q4_K_M",
|
|
})
|
|
|
|
# Union of all v0.53.0 schema-only GGUF flavours (excludes TQ1_0 which is owned
|
|
# by v0.52.0 Part D ``utils/bitnet.py``; ``is_advanced_gguf_format`` returns
|
|
# True for the BitNet family too via the helper below for export-CLI parity).
|
|
ALL_ADVANCED_GGUF_FORMATS: frozenset[str] = (
|
|
UD_GGUF_FORMATS | IQ_GGUF_FORMATS | APPLE_ARM_GGUF_FORMATS
|
|
)
|
|
|
|
_MAX_FORMAT_LEN: int = 32
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class GGUFQuantSpec:
|
|
"""Frozen metadata for a v0.53.0 advanced GGUF flavour."""
|
|
|
|
name: str
|
|
family: str # "ud" | "iq" | "apple_arm"
|
|
bits: float
|
|
description: str
|
|
live_wired: bool
|
|
|
|
|
|
def _spec(name: str, family: str, bits: float, description: str) -> GGUFQuantSpec:
|
|
return GGUFQuantSpec(
|
|
name=name, family=family, bits=bits,
|
|
description=description, live_wired=False,
|
|
)
|
|
|
|
|
|
_GGUF_METADATA: Mapping[str, GGUFQuantSpec] = MappingProxyType({
|
|
# Unsloth Dynamic 2.0 ladder
|
|
"UD-Q8_K_XL": _spec("UD-Q8_K_XL", "ud", 8.0, "UD Q8_K_XL (Unsloth Dynamic 2.0)"),
|
|
"UD-Q6_K_XL": _spec("UD-Q6_K_XL", "ud", 6.0, "UD Q6_K_XL"),
|
|
"UD-Q5_K_XL": _spec("UD-Q5_K_XL", "ud", 5.0, "UD Q5_K_XL"),
|
|
"UD-Q4_K_XL": _spec("UD-Q4_K_XL", "ud", 4.0, "UD Q4_K_XL"),
|
|
"UD-Q3_K_XL": _spec("UD-Q3_K_XL", "ud", 3.0, "UD Q3_K_XL"),
|
|
"UD-Q2_K_XL": _spec("UD-Q2_K_XL", "ud", 2.0, "UD Q2_K_XL"),
|
|
"UD-IQ4_XS": _spec("UD-IQ4_XS", "ud", 4.0, "UD IQ4_XS"),
|
|
"UD-IQ3_M": _spec("UD-IQ3_M", "ud", 3.0, "UD IQ3_M"),
|
|
"UD-IQ3_XXS": _spec("UD-IQ3_XXS", "ud", 3.0, "UD IQ3_XXS"),
|
|
"UD-IQ2_M": _spec("UD-IQ2_M", "ud", 2.0, "UD IQ2_M"),
|
|
"UD-IQ2_XS": _spec("UD-IQ2_XS", "ud", 2.0, "UD IQ2_XS"),
|
|
"UD-IQ2_XXS": _spec("UD-IQ2_XXS", "ud", 2.0, "UD IQ2_XXS"),
|
|
"UD-IQ1_M": _spec("UD-IQ1_M", "ud", 1.0, "UD IQ1_M (smallest UD)"),
|
|
"UD-IQ1_S": _spec("UD-IQ1_S", "ud", 1.0, "UD IQ1_S"),
|
|
# IQ family (non-UD)
|
|
"IQ1_S": _spec("IQ1_S", "iq", 1.0, "IQ1_S 1-bit"),
|
|
"IQ1_M": _spec("IQ1_M", "iq", 1.0, "IQ1_M 1-bit"),
|
|
"IQ2_XXS": _spec("IQ2_XXS", "iq", 2.0, "IQ2_XXS 2-bit"),
|
|
"IQ2_XS": _spec("IQ2_XS", "iq", 2.0, "IQ2_XS 2-bit"),
|
|
"IQ2_S": _spec("IQ2_S", "iq", 2.0, "IQ2_S 2-bit"),
|
|
"IQ2_M": _spec("IQ2_M", "iq", 2.0, "IQ2_M 2-bit"),
|
|
"IQ3_XXS": _spec("IQ3_XXS", "iq", 3.0, "IQ3_XXS 3-bit"),
|
|
"IQ3_XS": _spec("IQ3_XS", "iq", 3.0, "IQ3_XS 3-bit"),
|
|
"IQ3_S": _spec("IQ3_S", "iq", 3.0, "IQ3_S 3-bit"),
|
|
"IQ3_M": _spec("IQ3_M", "iq", 3.0, "IQ3_M 3-bit"),
|
|
"IQ4_XS": _spec("IQ4_XS", "iq", 4.0, "IQ4_XS 4-bit"),
|
|
"IQ4_NL": _spec("IQ4_NL", "iq", 4.0, "IQ4_NL 4-bit (non-linear)"),
|
|
# Apple/ARM neural-engine-friendly
|
|
"Q4_0_4_4": _spec("Q4_0_4_4", "apple_arm", 4.0, "Apple/ARM Q4_0_4_4"),
|
|
"Q4_0_4_8": _spec("Q4_0_4_8", "apple_arm", 4.0, "Apple/ARM Q4_0_4_8"),
|
|
"Q4_0_8_8": _spec("Q4_0_8_8", "apple_arm", 4.0, "Apple/ARM Q4_0_8_8"),
|
|
"Q4_NL": _spec("Q4_NL", "apple_arm", 4.0, "Apple/ARM Q4_NL"),
|
|
"Q5_0": _spec("Q5_0", "apple_arm", 5.0, "Apple/ARM Q5_0"),
|
|
"Q5_1": _spec("Q5_1", "apple_arm", 5.0, "Apple/ARM Q5_1"),
|
|
"Q5_K_S": _spec("Q5_K_S", "apple_arm", 5.0, "Apple/ARM Q5_K_S"),
|
|
"Q5_K_M": _spec("Q5_K_M", "apple_arm", 5.0, "Apple/ARM Q5_K_M"),
|
|
"Q4_K_S": _spec("Q4_K_S", "apple_arm", 4.0, "Apple/ARM Q4_K_S"),
|
|
"Q4_K_M": _spec("Q4_K_M", "apple_arm", 4.0, "Apple/ARM Q4_K_M"),
|
|
})
|
|
|
|
|
|
def _basic_validate(value: object, field: str) -> str:
|
|
if isinstance(value, bool):
|
|
raise TypeError(f"{field} must not be bool, got {value!r}")
|
|
if not isinstance(value, str):
|
|
raise TypeError(f"{field} must be str, got {type(value).__name__}")
|
|
if not value:
|
|
raise ValueError(f"{field} must be non-empty")
|
|
if "\x00" in value:
|
|
raise ValueError(f"{field} must not contain null bytes")
|
|
if len(value) > _MAX_FORMAT_LEN:
|
|
raise ValueError(f"{field} too long (max {_MAX_FORMAT_LEN} chars)")
|
|
return value
|
|
|
|
|
|
# Lowercase index built once at module load — O(1) lookup vs O(N) walk
|
|
# (code-review MEDIUM fix). Mirrors v0.32.0 ``pick_mixed_precision`` quirk
|
|
# ordering policy where sorting / indexing is precomputed.
|
|
_LOWER_INDEX: Mapping[str, str] = MappingProxyType({
|
|
name.lower(): name for name in ALL_ADVANCED_GGUF_FORMATS
|
|
})
|
|
|
|
|
|
def _resolve_canonical(value: str) -> str | None:
|
|
"""Match ``value`` against the union allowlist case-insensitively.
|
|
|
|
Returns the canonical (original-case) entry from the allowlist or
|
|
``None`` if no match.
|
|
"""
|
|
return _LOWER_INDEX.get(value.lower())
|
|
|
|
|
|
def validate_ud_gguf_format(value: object) -> str:
|
|
"""Validate ``value`` is a UD GGUF format string. Returns canonical form."""
|
|
_basic_validate(value, "ud_gguf_format")
|
|
canonical = _resolve_canonical(value) # type: ignore[arg-type]
|
|
if canonical is None or canonical not in UD_GGUF_FORMATS:
|
|
supported = ", ".join(sorted(UD_GGUF_FORMATS))
|
|
raise ValueError(
|
|
f"ud_gguf_format {value!r} not supported. Supported: {supported}"
|
|
)
|
|
return canonical
|
|
|
|
|
|
def validate_iq_gguf_format(value: object) -> str:
|
|
"""Validate ``value`` is an IQ GGUF format string. Returns canonical form."""
|
|
_basic_validate(value, "iq_gguf_format")
|
|
canonical = _resolve_canonical(value) # type: ignore[arg-type]
|
|
if canonical is None or canonical not in IQ_GGUF_FORMATS:
|
|
supported = ", ".join(sorted(IQ_GGUF_FORMATS))
|
|
raise ValueError(
|
|
f"iq_gguf_format {value!r} not supported. Supported: {supported}"
|
|
)
|
|
return canonical
|
|
|
|
|
|
def validate_apple_arm_gguf_format(value: object) -> str:
|
|
"""Validate ``value`` is an Apple/ARM GGUF format string."""
|
|
_basic_validate(value, "apple_arm_gguf_format")
|
|
canonical = _resolve_canonical(value) # type: ignore[arg-type]
|
|
if canonical is None or canonical not in APPLE_ARM_GGUF_FORMATS:
|
|
supported = ", ".join(sorted(APPLE_ARM_GGUF_FORMATS))
|
|
raise ValueError(
|
|
f"apple_arm_gguf_format {value!r} not supported. "
|
|
f"Supported: {supported}"
|
|
)
|
|
return canonical
|
|
|
|
|
|
def is_ud_gguf_format(value: object) -> bool:
|
|
"""Return True iff ``value`` is one of the UD GGUF ladder entries."""
|
|
if isinstance(value, bool) or not isinstance(value, str):
|
|
return False
|
|
canonical = _resolve_canonical(value)
|
|
return canonical is not None and canonical in UD_GGUF_FORMATS
|
|
|
|
|
|
def is_iq_gguf_format(value: object) -> bool:
|
|
"""Return True iff ``value`` is one of the IQ GGUF flavours."""
|
|
if isinstance(value, bool) or not isinstance(value, str):
|
|
return False
|
|
canonical = _resolve_canonical(value)
|
|
return canonical is not None and canonical in IQ_GGUF_FORMATS
|
|
|
|
|
|
def is_apple_arm_gguf_format(value: object) -> bool:
|
|
"""Return True iff ``value`` is one of the Apple/ARM GGUF flavours."""
|
|
if isinstance(value, bool) or not isinstance(value, str):
|
|
return False
|
|
canonical = _resolve_canonical(value)
|
|
return canonical is not None and canonical in APPLE_ARM_GGUF_FORMATS
|
|
|
|
|
|
def is_advanced_gguf_format(value: object) -> bool:
|
|
"""Return True iff ``value`` is any v0.53.0 advanced GGUF format."""
|
|
return (
|
|
is_ud_gguf_format(value)
|
|
or is_iq_gguf_format(value)
|
|
or is_apple_arm_gguf_format(value)
|
|
)
|
|
|
|
|
|
def get_gguf_spec(name: str) -> GGUFQuantSpec:
|
|
"""Return the frozen :class:`GGUFQuantSpec` for ``name`` (case-insensitive)."""
|
|
if isinstance(name, bool) or not isinstance(name, str):
|
|
raise TypeError(f"name must be str, got {type(name).__name__}")
|
|
canonical = _resolve_canonical(name)
|
|
if canonical is None:
|
|
supported_n = len(ALL_ADVANCED_GGUF_FORMATS)
|
|
raise ValueError(
|
|
f"GGUF format {name!r} not in v0.53.0 catalog "
|
|
f"({supported_n} known)"
|
|
)
|
|
return _GGUF_METADATA[canonical]
|
|
|
|
|
|
def validate_calibration_data_path(path: object) -> str:
|
|
"""Validate ``--calibration-data <jsonl>`` argument shape.
|
|
|
|
Boundary contract — what's enforced HERE vs at CLI dispatch (v0.53.1):
|
|
|
|
THIS helper enforces:
|
|
* non-empty ``str`` (bool / None / other types rejected with ``TypeError``)
|
|
* no null bytes
|
|
* length <= 4096 chars
|
|
|
|
CLI dispatch in v0.53.1 MUST additionally apply (mirrors v0.43.0 /
|
|
v0.46.0 / v0.47.0 TOCTOU policy):
|
|
* ``os.path.realpath`` + ``os.path.commonpath`` cwd containment
|
|
* ``os.lstat`` + ``stat.S_ISLNK`` rejection BEFORE any ``open()``
|
|
* existence check via ``os.path.isfile``
|
|
|
|
Do NOT skip the dispatch-time controls — this helper is shape-only.
|
|
"""
|
|
if isinstance(path, bool):
|
|
raise TypeError(f"calibration_data must not be bool, got {path!r}")
|
|
if not isinstance(path, str):
|
|
raise TypeError(
|
|
f"calibration_data must be str, got {type(path).__name__}"
|
|
)
|
|
if not path:
|
|
raise ValueError("calibration_data must be non-empty")
|
|
if "\x00" in path:
|
|
raise ValueError("calibration_data must not contain null bytes")
|
|
if len(path) > 4096:
|
|
raise ValueError("calibration_data path too long (max 4096 chars)")
|
|
return path
|
|
|
|
|
|
def export_advanced_gguf() -> None:
|
|
"""Live UD/IQ/Apple-ARM GGUF export — deferred to v0.53.1.
|
|
|
|
Mirrors v0.52.0 ``export_bitnet_gguf`` stub-then-live pattern.
|
|
"""
|
|
raise NotImplementedError(
|
|
"UD / IQ / Apple-ARM GGUF export live wiring deferred to v0.53.1. "
|
|
"Schema accepts every format in ALL_ADVANCED_GGUF_FORMATS but no "
|
|
"llama.cpp imatrix invocation is registered yet."
|
|
)
|