Files
wehub-resource-sync 426e9eeabd
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
Chat shell gestures / Chat shell gesture + parity e2e (push) Has been cancelled
Cloud Gateway Discord / Test (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Has been cancelled
Build Agent Image / build-and-push (push) Has been cancelled
Dev Smoke / bun run dev onboarding chat (push) Has been cancelled
Dev Smoke / Vite HMR dependency-level smoke (push) Has been cancelled
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Has been cancelled
Publish @elizaos/example-code / check_npm (push) Has been cancelled
Publish @elizaos/example-code / publish_npm (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / verify_version (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / publish_npm (push) Has been cancelled
Sandbox Live Smoke / Sandbox live smoke (push) Has been cancelled
Snap Build & Test / Build Snap (amd64) (push) Has been cancelled
Snap Build & Test / Build Snap (arm64) (push) Has been cancelled
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Has been cancelled
Cloud Gateway Webhook / Test (push) Has been cancelled
Cloud Tests / lint-and-types (push) Has been cancelled
Cloud Tests / unit-tests (push) Has been cancelled
Cloud Tests / integration-tests (push) Has been cancelled
Cloud Tests / e2e-tests (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Apps Worker (Product 2) / Determine environment (push) Has been cancelled
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Has been cancelled
Deploy Eliza Provisioning Worker / Determine environment (push) Has been cancelled
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Has been cancelled
Dev Smoke / Classify changed paths (push) Has been cancelled
supply-chain / sbom (push) Has been cancelled
supply-chain / vulnerability-scan (push) Has been cancelled
Build, Push & Deploy to Phala Cloud / build-and-push (push) Has been cancelled
Test Packaging / Validate Packaging Configs (push) Has been cancelled
Test Packaging / Build & Test PyPI Package (push) Has been cancelled
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Has been cancelled
Test Packaging / Pack & Test JS Tarballs (push) Has been cancelled
UI Fixture E2E / ui-fixture-e2e (push) Has been cancelled
UI Fixture E2E / fixture-e2e (push) Has been cancelled
UI Story Gate / story-gate (push) Has been cancelled
vault-ci / test (macos-latest) (push) Has been cancelled
vault-ci / test (ubuntu-latest) (push) Has been cancelled
vault-ci / test (windows-latest) (push) Has been cancelled
vault-ci / app-core wiring tests (push) Has been cancelled
verify-patches / verify patches/CHECKSUMS.sha256 (push) Has been cancelled
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Has been cancelled
Voice Benchmark Smoke / voice bench smoke summary (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Has been cancelled
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:43:05 +08:00

328 lines
12 KiB
Python

"""Shared loaders and IO helpers for the quantization apply scripts.
The four CLI entry points in this directory (``polarquant_apply``,
``turboquant_apply``, ``fused_turboquant_apply``, ``qjl_apply``) all need
the same handful of building blocks: detect a LoRA adapter directory,
resolve its base, load model+tokenizer, save them, and write a JSON
sidecar. This module is the single source of truth for that surface.
"""
from __future__ import annotations
import argparse
import json
import logging
import os
import shutil
from pathlib import Path
from typing import TYPE_CHECKING, Mapping
import torch
import torch.nn as nn
if TYPE_CHECKING:
# Static-only import. ``PretrainedConfig`` is the upstream type for
# every HF causal-LM config (``model.config``); importing it eagerly
# would force transformers at import time even for tests that just
# want the helpers below. Behind ``TYPE_CHECKING`` mypy/pyright still
# see the strong type and reject helpers that misuse the config.
from transformers import PretrainedConfig
from transformers.tokenization_utils_base import PreTrainedTokenizerBase
log = logging.getLogger(__name__)
REPO_ROOT = Path(__file__).resolve().parents[4]
LLAMA_CPP_RELATIVE_DIR = Path("plugins/plugin-local-inference/native/llama.cpp")
DEFAULT_LLAMA_CPP_DIR = REPO_ROOT / LLAMA_CPP_RELATIVE_DIR
def llama_cpp_vendor_hint() -> str:
"""Actionable setup text for callers that need the in-repo llama.cpp fork."""
rel = LLAMA_CPP_RELATIVE_DIR.as_posix()
return (
"The llama.cpp fork submodule should already be checked out. If it's "
"missing:\n"
f" git submodule update --init {rel}\n"
"Then build the llama-quantize + llama-cli binaries from it "
"(one-shot, CPU-only is enough):\n"
f" cmake -S {rel} -B {rel}/build \\\n"
" -DCMAKE_BUILD_TYPE=Release -DLLAMA_CURL=OFF "
"-DGGML_NATIVE=OFF -DBUILD_SHARED_LIBS=OFF\n"
f" cmake --build {rel}/build --target llama-quantize llama-cli "
'-j"$(nproc)"\n'
"Or pass --llama-cpp-dir <path-to-checkout> / set LLAMA_CPP_DIR / "
"put the binaries on PATH.\n"
"(convert_hf_to_gguf.py needs the `gguf` + `mistral_common` python "
f"deps; `uv pip install -r {rel}/requirements/"
"requirements-convert_hf_to_gguf.txt`.)"
)
def _llama_cpp_dirs(llama_cpp_dir: Path | None) -> list[Path]:
"""Candidate llama.cpp checkout roots in release-safe resolution order."""
candidates: list[Path] = []
if llama_cpp_dir is not None:
candidates.append(llama_cpp_dir)
env_dir = os.environ.get("LLAMA_CPP_DIR")
if env_dir:
candidates.append(Path(env_dir))
candidates.append(DEFAULT_LLAMA_CPP_DIR)
return candidates
def find_llama_convert_script(llama_cpp_dir: Path | None) -> Path:
"""Locate ``convert_hf_to_gguf.py`` from the canonical fork or PATH."""
candidates = [d / "convert_hf_to_gguf.py" for d in _llama_cpp_dirs(llama_cpp_dir)]
which = shutil.which("convert_hf_to_gguf.py")
if which:
candidates.append(Path(which))
for candidate in candidates:
if candidate.exists():
return candidate
raise SystemExit("convert_hf_to_gguf.py not found.\n" + llama_cpp_vendor_hint())
def find_llama_quantize_binary(llama_cpp_dir: Path | None) -> Path:
"""Locate ``llama-quantize`` from the canonical fork build or PATH."""
candidates: list[Path] = []
for directory in _llama_cpp_dirs(llama_cpp_dir):
candidates.extend(
[
directory / "build" / "bin" / "llama-quantize",
directory / "llama-quantize",
]
)
which = shutil.which("llama-quantize")
if which:
candidates.append(Path(which))
for candidate in candidates:
if candidate.exists() and os.access(candidate, os.X_OK):
return candidate
raise SystemExit("llama-quantize binary not found.\n" + llama_cpp_vendor_hint())
def is_lora_dir(path: Path) -> bool:
"""True iff ``path`` is a PEFT/LoRA adapter directory."""
return (path / "adapter_config.json").exists()
def resolve_base_for_lora(adapter_dir: Path) -> str:
"""Return the base-model id recorded in a LoRA adapter's config.
Raises ``RuntimeError`` if the field is missing.
"""
cfg = json.loads((adapter_dir / "adapter_config.json").read_text(encoding="utf-8"))
base = cfg.get("base_model_name_or_path")
if not base:
raise RuntimeError(
f"adapter_config.json at {adapter_dir} has no base_model_name_or_path"
)
return base
def load_model_and_tokenizer(
model_path: str,
*,
device_map: str = "cuda",
dtype: torch.dtype = torch.bfloat16,
) -> tuple[nn.Module, PreTrainedTokenizerBase]:
"""Load a HF causal-LM checkpoint, merging a LoRA adapter if present."""
from transformers import AutoModelForCausalLM, AutoTokenizer
p = Path(model_path)
if p.exists() and is_lora_dir(p):
base = resolve_base_for_lora(p)
log.info("loading base %s + LoRA adapter %s", base, p)
from peft import PeftModel
tok = AutoTokenizer.from_pretrained(base, trust_remote_code=True)
base_model = AutoModelForCausalLM.from_pretrained(
base,
torch_dtype=dtype,
device_map=device_map,
trust_remote_code=True,
)
merged = PeftModel.from_pretrained(base_model, str(p)).merge_and_unload()
return merged, tok
log.info("loading full model %s", model_path)
tok = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)
model = AutoModelForCausalLM.from_pretrained(
model_path,
torch_dtype=dtype,
device_map=device_map,
trust_remote_code=True,
)
return model, tok
def save_model(
model: nn.Module, tokenizer: "PreTrainedTokenizerBase", output_dir: Path
) -> None:
"""Persist model + tokenizer to ``output_dir`` via safetensors."""
output_dir.mkdir(parents=True, exist_ok=True)
log.info("saving model to %s", output_dir)
model.save_pretrained(str(output_dir), safe_serialization=True)
tokenizer.save_pretrained(str(output_dir))
def write_sidecar(output_dir: Path, filename: str, payload: Mapping[str, object]) -> Path:
"""Write a sorted JSON sidecar next to the saved model. Returns its path."""
out = output_dir / filename
out.write_text(json.dumps(payload, indent=2, sort_keys=True), encoding="utf-8")
return out
# Re-export from the zero-dep _kernel_manifest module so existing recipe
# imports (`from _common import kernel_manifest_fragment`) keep working
# while unit tests can import the helper without pulling in transformers.
from _kernel_manifest import ( # noqa: E402,F401
KERNEL_BLOCK_LAYOUT_VERSIONS,
KERNEL_CODEBOOK_HASH_SOURCES,
KERNEL_CODEBOOK_HASHES,
KERNEL_PER_BLOCK_TOLERANCE,
KERNEL_RECIPE_TARGET_CLASSES,
KERNEL_TARGETS,
PINNED_KERNEL_CODEBOOK_SHA256,
kernel_manifest_fragment,
verify_kernel_codebook_hashes,
)
def get_text_config(model_config: "PretrainedConfig") -> "PretrainedConfig":
"""Return the text-decoder sub-config for hybrid VLM/decoder models, else
``model_config`` itself.
"""
getter = getattr(model_config, "get_text_config", None)
if callable(getter):
return getter(decoder=True)
return model_config
def head_dim_of(text_cfg: "PretrainedConfig") -> int:
"""Resolve head_dim from a text decoder config, falling back to
``hidden_size // num_attention_heads`` when ``head_dim`` isn't set.
"""
explicit = getattr(text_cfg, "head_dim", None)
if explicit:
return int(explicit)
# ``hidden_size`` and ``num_attention_heads`` are required fields on
# any decoder config; fall through with a clear assertion so a wrong
# config type fails fast instead of raising AttributeError downstream.
assert hasattr(text_cfg, "hidden_size") and hasattr(
text_cfg, "num_attention_heads"
), (
"head_dim_of requires a transformers PretrainedConfig with "
"hidden_size + num_attention_heads"
)
return int(text_cfg.hidden_size // text_cfg.num_attention_heads)
def full_attention_layer_indices(text_cfg: "PretrainedConfig") -> list[int]:
"""Indices of ``full_attention`` layers on hybrid decoder configs, or
``range(num_hidden_layers)`` when ``layer_types`` is absent.
"""
layer_types = getattr(text_cfg, "layer_types", None)
if layer_types:
return [i for i, t in enumerate(layer_types) if t == "full_attention"]
assert hasattr(text_cfg, "num_hidden_layers"), (
"full_attention_layer_indices requires a PretrainedConfig with "
"num_hidden_layers"
)
return list(range(int(text_cfg.num_hidden_layers)))
def add_quantization_cli_args(parser: argparse.ArgumentParser) -> None:
"""Add the CLI flags shared by every ``*_apply.py`` recipe.
The shared surface (``--model``, ``--output``, ``--calibration``,
``--calibration-samples``, ``--device``, ``--dry-run``) is identical
across turboquant, fused-turboquant, polarquant, qjl, and
abliteration. Recipe-specific flags (``--nbits``, ``--bits``,
``--no-compress-v``, …) stay in each script so the help text in
``--help`` accurately reflects which knobs that recipe accepts.
"""
parser.add_argument(
"--model",
required=True,
help=(
"HF repo id or local path. LoRA adapter dirs are merged "
"automatically."
),
)
parser.add_argument("--output", required=True, type=Path)
parser.add_argument(
"--calibration",
type=Path,
default=None,
help=(
"Optional JSONL of records with currentMessage.content for "
"calibration. Recipes that don't read it (fused-turboquant) "
"still validate the file exists when the flag is present."
),
)
parser.add_argument("--calibration-samples", type=int, default=128)
parser.add_argument("--device", default="cuda")
parser.add_argument("--dry-run", action="store_true")
def validate_quantization_args(args: argparse.Namespace) -> None:
"""Cross-cut validation for the shared CLI args.
Currently:
- ``--calibration PATH`` (when set) must point at an existing file.
- ``--device cuda`` requires CUDA on this host unless this is only a
``--dry-run`` CLI/recipe validation pass.
"""
if args.calibration is not None and not args.calibration.exists():
raise FileNotFoundError(
f"--calibration path does not exist: {args.calibration}"
)
if (
args.device == "cuda"
and not getattr(args, "dry_run", False)
and not torch.cuda.is_available()
):
raise RuntimeError("CUDA requested but not available")
def load_calibration_prompts(path: Path, n: int) -> list[str]:
"""Pull up to n user-final prompts from a JSONL file.
Supports both eliza_native_v1 format (request.messages array) and
legacy format (currentMessage.content). Lines that fail JSON parse
or carry no text are dropped silently. Raises RuntimeError if none survive.
"""
out: list[str] = []
with path.open("r", encoding="utf-8") as f:
for raw_line in f:
line = raw_line.strip()
if not line:
continue
rec = json.loads(line)
text = ""
# eliza_native_v1: find last user turn in request.messages
req = rec.get("request") or {}
msgs = req.get("messages") or []
for msg in reversed(msgs):
if msg.get("role") == "user":
c = msg.get("content") or ""
if isinstance(c, list):
c = " ".join(
p.get("text", "") for p in c
if isinstance(p, dict) and p.get("type") == "text"
)
text = c
break
# legacy schema fallback
if not text:
text = (rec.get("currentMessage") or {}).get("content") or ""
if text:
out.append(text)
if len(out) >= n:
break
if not out:
raise RuntimeError(f"No prompts read from {path}")
return out