From e90a96653637a994d3cb9cf4fb71415039d301e6 Mon Sep 17 00:00:00 2001 From: wehub-resource-sync Date: Mon, 13 Jul 2026 12:29:01 +0800 Subject: [PATCH] chore: import upstream snapshot with attribution --- .github/FUNDING.yml | 1 + .github/ISSUE_TEMPLATE/bug_report.yml | 46 + .github/ISSUE_TEMPLATE/feature_request.yml | 27 + .github/PULL_REQUEST_TEMPLATE.md | 17 + .github/workflows/lint.yml | 25 + .github/workflows/publish.yml | 27 + .github/workflows/test.yml | 28 + .gitignore | 42 + .pre-commit-config.yaml | 7 + CHANGELOG.md | 429 +++++ CODE_OF_CONDUCT.md | 39 + CONTRIBUTING.md | 55 + LICENSE | 21 + README.md | 513 ++++++ README.wehub.md | 7 + assets/demo-run.gif | Bin 0 -> 450814 bytes assets/demo-run.tape | 45 + assets/demo.gif | Bin 0 -> 372000 bytes assets/demo.png | Bin 0 -> 130191 bytes assets/demo.tape | 37 + docs/README.ja.md | 249 +++ docs/cli.md | 270 +++ docs/hardware.md | 237 +++ docs/how-it-works.md | 193 +++ docs/run-snippet.md | 161 ++ docs/scoring.md | 216 +++ docs/troubleshooting.md | 423 +++++ pyproject.toml | 58 + scripts/import_livebench_csv.py | 116 ++ src/whichllm/__init__.py | 1 + src/whichllm/__main__.py | 7 + src/whichllm/cli.py | 1507 +++++++++++++++++ src/whichllm/constants.py | 54 + src/whichllm/data/__init__.py | 1 + src/whichllm/data/framework.py | 8 + src/whichllm/data/gpu.py | 299 ++++ src/whichllm/data/lineage.py | 161 ++ src/whichllm/data/quantization.py | 123 ++ src/whichllm/engine/__init__.py | 0 src/whichllm/engine/compatibility.py | 261 +++ src/whichllm/engine/performance.py | 284 ++++ src/whichllm/engine/quantization.py | 82 + src/whichllm/engine/ranker.py | 885 ++++++++++ src/whichllm/engine/types.py | 30 + src/whichllm/engine/vram.py | 100 ++ src/whichllm/hardware/__init__.py | 0 src/whichllm/hardware/amd.py | 286 ++++ src/whichllm/hardware/apple.py | 131 ++ src/whichllm/hardware/cpu.py | 225 +++ src/whichllm/hardware/detector.py | 56 + src/whichllm/hardware/gpu_db.py | 219 +++ src/whichllm/hardware/gpu_simulator.py | 346 ++++ src/whichllm/hardware/intel.py | 136 ++ src/whichllm/hardware/memory.py | 52 + src/whichllm/hardware/nvidia.py | 198 +++ src/whichllm/hardware/types.py | 31 + src/whichllm/hardware/windows.py | 182 ++ src/whichllm/models/__init__.py | 0 src/whichllm/models/artifacts.py | 105 ++ src/whichllm/models/benchmark.py | 90 + src/whichllm/models/benchmark_cache.py | 39 + src/whichllm/models/benchmark_fetch.py | 119 ++ src/whichllm/models/benchmark_index.py | 117 ++ src/whichllm/models/benchmark_lineage.py | 63 + src/whichllm/models/benchmark_lookup.py | 192 +++ .../models/benchmark_sources/__init__.py | 43 + .../models/benchmark_sources/aa_index.py | 386 +++++ .../models/benchmark_sources/aider.py | 135 ++ .../models/benchmark_sources/chatbot_arena.py | 140 ++ .../models/benchmark_sources/constants.py | 7 + .../models/benchmark_sources/livebench.py | 102 ++ .../benchmark_sources/open_llm_leaderboard.py | 99 ++ .../models/benchmark_sources/types.py | 5 + .../models/benchmark_sources/utils.py | 14 + .../models/benchmark_sources/vision.py | 83 + src/whichllm/models/benchmark_types.py | 23 + src/whichllm/models/cache.py | 47 + src/whichllm/models/fetcher.py | 111 ++ src/whichllm/models/gguf.py | 84 + src/whichllm/models/grouper.py | 149 ++ src/whichllm/models/hf.py | 293 ++++ src/whichllm/models/http.py | 43 + src/whichllm/models/parameters.py | 218 +++ src/whichllm/models/parser.py | 295 ++++ src/whichllm/models/serialization.py | 94 + src/whichllm/models/sliding_window.py | 95 ++ src/whichllm/models/types.py | 46 + src/whichllm/output/__init__.py | 0 src/whichllm/output/_console.py | 11 + src/whichllm/output/display.py | 40 + src/whichllm/output/formatting.py | 111 ++ src/whichllm/output/json_output.py | 210 +++ src/whichllm/output/markdown.py | 154 ++ src/whichllm/output/plan.py | 158 ++ src/whichllm/output/ranking.py | 348 ++++ src/whichllm/output/upgrade.py | 126 ++ src/whichllm/utils.py | 76 + tests/__init__.py | 0 tests/test_aa_index.py | 137 ++ tests/test_amd_detection.py | 302 ++++ tests/test_artifacts.py | 47 + tests/test_asahi_detection.py | 213 +++ tests/test_benchmark_lookup.py | 182 ++ tests/test_cache_encoding.py | 65 + tests/test_cli.py | 1381 +++++++++++++++ tests/test_compatibility.py | 502 ++++++ tests/test_fetcher.py | 651 +++++++ tests/test_gpu_db.py | 118 ++ tests/test_gpu_simulator.py | 171 ++ tests/test_grouper.py | 65 + tests/test_gtx1650_variants.py | 171 ++ tests/test_http.py | 63 + tests/test_intel_gpu.py | 130 ++ tests/test_kepler_gpus.py | 174 ++ tests/test_markdown_output.py | 167 ++ tests/test_memory.py | 40 + tests/test_nvidia_detection.py | 194 +++ tests/test_output_formatting.py | 32 + tests/test_p1_p3_regressions.py | 513 ++++++ tests/test_quantization.py | 141 ++ tests/test_r3_regressions.py | 729 ++++++++ tests/test_ranker.py | 1155 +++++++++++++ tests/test_utils.py | 81 + tests/test_vram.py | 148 ++ tests/test_windows_cpu_detection.py | 45 + tests/test_windows_gpu.py | 175 ++ uv.lock | 581 +++++++ 127 files changed, 21798 insertions(+) create mode 100644 .github/FUNDING.yml create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/workflows/lint.yml create mode 100644 .github/workflows/publish.yml create mode 100644 .github/workflows/test.yml create mode 100644 .gitignore create mode 100644 .pre-commit-config.yaml create mode 100644 CHANGELOG.md create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 README.wehub.md create mode 100644 assets/demo-run.gif create mode 100644 assets/demo-run.tape create mode 100644 assets/demo.gif create mode 100644 assets/demo.png create mode 100644 assets/demo.tape create mode 100644 docs/README.ja.md create mode 100644 docs/cli.md create mode 100644 docs/hardware.md create mode 100644 docs/how-it-works.md create mode 100644 docs/run-snippet.md create mode 100644 docs/scoring.md create mode 100644 docs/troubleshooting.md create mode 100644 pyproject.toml create mode 100644 scripts/import_livebench_csv.py create mode 100644 src/whichllm/__init__.py create mode 100644 src/whichllm/__main__.py create mode 100644 src/whichllm/cli.py create mode 100644 src/whichllm/constants.py create mode 100644 src/whichllm/data/__init__.py create mode 100644 src/whichllm/data/framework.py create mode 100644 src/whichllm/data/gpu.py create mode 100644 src/whichllm/data/lineage.py create mode 100644 src/whichllm/data/quantization.py create mode 100644 src/whichllm/engine/__init__.py create mode 100644 src/whichllm/engine/compatibility.py create mode 100644 src/whichllm/engine/performance.py create mode 100644 src/whichllm/engine/quantization.py create mode 100644 src/whichllm/engine/ranker.py create mode 100644 src/whichllm/engine/types.py create mode 100644 src/whichllm/engine/vram.py create mode 100644 src/whichllm/hardware/__init__.py create mode 100644 src/whichllm/hardware/amd.py create mode 100644 src/whichllm/hardware/apple.py create mode 100644 src/whichllm/hardware/cpu.py create mode 100644 src/whichllm/hardware/detector.py create mode 100644 src/whichllm/hardware/gpu_db.py create mode 100644 src/whichllm/hardware/gpu_simulator.py create mode 100644 src/whichllm/hardware/intel.py create mode 100644 src/whichllm/hardware/memory.py create mode 100644 src/whichllm/hardware/nvidia.py create mode 100644 src/whichllm/hardware/types.py create mode 100644 src/whichllm/hardware/windows.py create mode 100644 src/whichllm/models/__init__.py create mode 100644 src/whichllm/models/artifacts.py create mode 100644 src/whichllm/models/benchmark.py create mode 100644 src/whichllm/models/benchmark_cache.py create mode 100644 src/whichllm/models/benchmark_fetch.py create mode 100644 src/whichllm/models/benchmark_index.py create mode 100644 src/whichllm/models/benchmark_lineage.py create mode 100644 src/whichllm/models/benchmark_lookup.py create mode 100644 src/whichllm/models/benchmark_sources/__init__.py create mode 100644 src/whichllm/models/benchmark_sources/aa_index.py create mode 100644 src/whichllm/models/benchmark_sources/aider.py create mode 100644 src/whichllm/models/benchmark_sources/chatbot_arena.py create mode 100644 src/whichllm/models/benchmark_sources/constants.py create mode 100644 src/whichllm/models/benchmark_sources/livebench.py create mode 100644 src/whichllm/models/benchmark_sources/open_llm_leaderboard.py create mode 100644 src/whichllm/models/benchmark_sources/types.py create mode 100644 src/whichllm/models/benchmark_sources/utils.py create mode 100644 src/whichllm/models/benchmark_sources/vision.py create mode 100644 src/whichllm/models/benchmark_types.py create mode 100644 src/whichllm/models/cache.py create mode 100644 src/whichllm/models/fetcher.py create mode 100644 src/whichllm/models/gguf.py create mode 100644 src/whichllm/models/grouper.py create mode 100644 src/whichllm/models/hf.py create mode 100644 src/whichllm/models/http.py create mode 100644 src/whichllm/models/parameters.py create mode 100644 src/whichllm/models/parser.py create mode 100644 src/whichllm/models/serialization.py create mode 100644 src/whichllm/models/sliding_window.py create mode 100644 src/whichllm/models/types.py create mode 100644 src/whichllm/output/__init__.py create mode 100644 src/whichllm/output/_console.py create mode 100644 src/whichllm/output/display.py create mode 100644 src/whichllm/output/formatting.py create mode 100644 src/whichllm/output/json_output.py create mode 100644 src/whichllm/output/markdown.py create mode 100644 src/whichllm/output/plan.py create mode 100644 src/whichllm/output/ranking.py create mode 100644 src/whichllm/output/upgrade.py create mode 100644 src/whichllm/utils.py create mode 100644 tests/__init__.py create mode 100644 tests/test_aa_index.py create mode 100644 tests/test_amd_detection.py create mode 100644 tests/test_artifacts.py create mode 100644 tests/test_asahi_detection.py create mode 100644 tests/test_benchmark_lookup.py create mode 100644 tests/test_cache_encoding.py create mode 100644 tests/test_cli.py create mode 100644 tests/test_compatibility.py create mode 100644 tests/test_fetcher.py create mode 100644 tests/test_gpu_db.py create mode 100644 tests/test_gpu_simulator.py create mode 100644 tests/test_grouper.py create mode 100644 tests/test_gtx1650_variants.py create mode 100644 tests/test_http.py create mode 100644 tests/test_intel_gpu.py create mode 100644 tests/test_kepler_gpus.py create mode 100644 tests/test_markdown_output.py create mode 100644 tests/test_memory.py create mode 100644 tests/test_nvidia_detection.py create mode 100644 tests/test_output_formatting.py create mode 100644 tests/test_p1_p3_regressions.py create mode 100644 tests/test_quantization.py create mode 100644 tests/test_r3_regressions.py create mode 100644 tests/test_ranker.py create mode 100644 tests/test_utils.py create mode 100644 tests/test_vram.py create mode 100644 tests/test_windows_cpu_detection.py create mode 100644 tests/test_windows_gpu.py create mode 100644 uv.lock diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..998ede1 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: [Andyyyy64] diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..dd15b5e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,46 @@ +name: Bug Report +description: Report a bug or unexpected behavior +labels: ["bug"] +body: + - type: markdown + attributes: + value: | + Thanks for reporting a bug! Please fill out the information below. + - type: textarea + id: description + attributes: + label: Description + description: What happened? What did you expect to happen? + validations: + required: true + - type: textarea + id: reproduce + attributes: + label: Steps to Reproduce + description: How can we reproduce this issue? + placeholder: | + 1. Run `whichllm --gpu "RTX 4090"` + 2. ... + validations: + required: true + - type: textarea + id: hardware + attributes: + label: Hardware Info + description: Paste the output of `whichllm hardware` + render: shell + - type: input + id: python-version + attributes: + label: Python Version + placeholder: "3.11" + - type: input + id: os + attributes: + label: Operating System + placeholder: "Ubuntu 24.04 / macOS 15 / Windows 11" + - type: input + id: version + attributes: + label: whichllm Version + placeholder: "0.3.0" diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..6b2057f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,27 @@ +name: Feature Request +description: Suggest a new feature or improvement +labels: ["enhancement"] +body: + - type: markdown + attributes: + value: | + Thanks for suggesting a feature! Please describe what you'd like to see. + - type: textarea + id: problem + attributes: + label: Problem + description: What problem does this feature solve? + validations: + required: true + - type: textarea + id: solution + attributes: + label: Proposed Solution + description: How would you like this to work? + validations: + required: true + - type: textarea + id: alternatives + attributes: + label: Alternatives Considered + description: Any alternative solutions you've considered? diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..a29231d --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,17 @@ +## What + + + +## Why + + + +## Testing + +- [ ] Tests pass (`pytest`) +- [ ] New tests added (if applicable) +- [ ] Tested on real hardware (if hardware-related) + +## Notes + + diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..e564153 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,25 @@ +name: Lint + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.13" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install ruff + - name: Run Ruff linter + run: ruff check . + - name: Run Ruff formatter check + run: ruff format --check . diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..057ba70 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,27 @@ +name: Publish to PyPI + +on: + release: + types: [published] + +permissions: + id-token: write + +jobs: + publish: + runs-on: ubuntu-latest + environment: pypi + steps: + - uses: actions/checkout@v5 + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.13" + - name: Install build tools + run: | + python -m pip install --upgrade pip + pip install build + - name: Build package + run: python -m build + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..e1f2fec --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,28 @@ +name: Tests + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.11", "3.12", "3.13"] + + steps: + - uses: actions/checkout@v5 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v6 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -e . + pip install pytest pyarrow + - name: Run tests + run: pytest diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5b51978 --- /dev/null +++ b/.gitignore @@ -0,0 +1,42 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +*.egg +*.egg-info/ +dist/ +build/ +*.whl + +# Virtual environments +.venv/ +venv/ +env/ + +# Testing +.pytest_cache/ +.coverage +htmlcov/ +.mypy_cache/ +.ruff_cache/ + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Environment +.env +.env.local +.env.*.local + +# Logs +*.log +.claude/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..2e91f53 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,7 @@ +repos: + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.15.20 + hooks: + - id: ruff-check + args: [--fix] + - id: ruff-format diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..53423cf --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,429 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/), and this project adheres to [Semantic Versioning](https://semver.org/). + +## [Unreleased] + +## [0.5.15] - 2026-07-03 + +### Changed + +- Split the oversized Hugging Face model fetcher into focused modules while + keeping the existing `whichllm.models.fetcher` import surface. (#41, #140) + +### Fixed + +- Output now resolves ranked GGUF recommendations to the actual downloadable + artifact repo and filename when the ranked base model and runnable GGUF live + in different Hugging Face repos. (#137, #138) +- AA index scoring now uses retuned normalization bounds and a refreshed + fallback snapshot for the reworked Artificial Analysis scale. (#101, #139) +- Invalid ranking filters such as `--top 0`, negative `--min-speed`, and + negative `--min-params` now fail with clear errors instead of silently + producing wrong output. (#142) + +## [0.5.14] - 2026-06-29 + +### Added + +- Added sliding-window attention metadata to model fetching and KV cache + estimation, improving VRAM estimates for models that use SWA. (#124) +- Added curated Intel Arc Pro B70 / Battlemage G31 detection and simulation, + including the `0xe223` PCI device ID and 32 GB VRAM / 608 GB/s bandwidth + defaults. (#93, #136) + +### Fixed + +- Model and benchmark metadata fetches now request `gzip, deflate` instead of + brotli, avoiding broken `br` responses from mirrors or intermediate servers. + (#128, #136) + +## [0.5.13] - 2026-06-25 + +### Added + +- Added `HF_ENDPOINT` support for Hugging Face model metadata fetches, so users + behind a mirror can point whichllm at a compatible Hub endpoint. (#128, #131) +- Added manual detected-GPU overrides for usable VRAM and bandwidth, which helps + iGPU and unified-memory systems where automatic detection is too conservative. + (#132, #133) +- README now points users toward safer first-run flags when they want + full-GPU, usable-speed recommendations with extra VRAM headroom. + +### Fixed + +- Search terms such as `7B`, `0.5B`, and `500M` now match model parameter size + instead of plain substrings, so `qwen 7b` no longer returns `1.7B` or + `30B-A3B` models by accident. (#107, #126) +- GGUF sizing now treats FP16 and ternary `TQ1_0` / `TQ2_0` quant types + correctly, avoiding underestimates for those files. (#125) + +## [0.5.12] - 2026-06-18 + +### Added + +- Default ranking tables now show memory required, estimated generation speed, + fit type, and published date. Download counts are still available with + `--details`. +- Added `--speed any|usable|fast` as named generation-speed filters while + keeping `--min-speed` for exact tok/s thresholds. +- Added `--fit gpu` as a natural alias for full-GPU-only recommendations. +- Added `--markdown` / `-m` for pasteable GitHub-Flavored Markdown ranking + tables. (#111) +- Added `--vram-headroom` and `--ram-budget` so users can avoid edge VRAM fits + and cap partial-offload planning to available or fixed system RAM. + +### Changed + +- Speed color now reflects practical generation speed. `~` and `?` remain + estimate-confidence markers instead of being the primary speed color. + +## [0.5.11] - 2026-06-18 + +### Added + +- Multi-GPU simulation for repeated `--gpu` flags, comma-separated GPU specs, + and count shorthand such as `2x RTX 4090`. The fit model uses a conservative + effective VRAM budget and keeps speed confidence low for split-device + recommendations. (#113) +- `python -m whichllm` now runs the CLI entrypoint. (#116) +- `--gpu-only` and `--fit full-gpu` now filter recommendations to models that + fit fully in GPU VRAM. `--fit any` keeps the existing behavior. (#119, #122) +- T5 lineage support so T5-family models get version-aware benchmark handling. + +### Fixed + +- Fixed UTF-8 decoding for cached model and benchmark data on systems whose + default filesystem encoding is not UTF-8. (#121) +- GTX 1650 simulation now distinguishes GDDR5 and GDDR6 variants by memory + clock instead of treating every card as the slower 128 GB/s model. (#115) +- RAM reserve logic now uses a bounded reserve formula instead of a fixed 80% + usable-RAM cap, which avoids underestimating machines with more system + memory. (#103) + +## [0.5.10] - 2026-06-11 + +### Fixed + +- Strong partial-offload candidates are no longer buried below weaker full-GPU + models because the final ranking sort no longer counts full-GPU fit a second + time after runtime-fit and speed penalties have already been applied. Light + partial offload is penalized less aggressively, while heavy dense offload + remains strongly discounted. (#105, #108) +- MoE partial-offload scoring now uses the active parameter working set when it + can plausibly stay on GPU, so active-small MoE models are not penalized like + dense models with the same total parameter count. (#105, #108) + +## [0.5.9] - 2026-06-10 + +### Added + +- MXFP4 and NVFP4 4-bit quantization support across ID/filename parsing, VRAM + estimation, quality penalties, speed efficiency, and family grouping. + Repos shipping these formats were previously labeled FP16 and their VRAM + requirement overestimated about 3.5x. (#99) +- Apple M5-family entries for `--gpu` simulation. (#92) +- Kepler-era Quadro bandwidth and compute capability entries. (#75) + +### Fixed + +- AMD discrete GPU detection on Linux: rocm-smi names are read from the + correct `Card Series` key, compound lspci names such as + `Navi 22 [Radeon RX 6700/6700 XT/6750 XT ...]` resolve bandwidth, sysfs VRAM + enriches the fallback path, and discrete cards are no longer mislabeled + `shared memory`. Adds RX 6750 XT / RX 6700 / RX 6650 XT / RX 6600 series and + Radeon AI PRO R9700 to the bandwidth catalog. (#61, #68) +- Community GGUF repos without `base_model` metadata (for example + `unsloth/...-GGUF`) now inherit the official model's benchmark score by + name matching instead of falling through to no evidence. (#94) +- GPU bandwidth detection no longer depends solely on the hand-curated + catalog. When a detected card is missing from `GPU_BANDWIDTH`, bandwidth is + now resolved from the bundled TechPowerUp database (dbgpu, 2824 GPUs) using + strict name matching only: an exact normalized hit or a name plus VRAM-size + bin, never fuzzy. Laptop / Mobile / Max-Q names can no longer inherit a + desktop card's bandwidth, and VRAM bins written without a space + (`RTX A2000 12GB`) are recognized. This fixes the cluster of reports where + an uncatalogued GPU showed `BW: N/A`, was estimated at `0.0 tok/s`, and + received oversized recommendations (#74, #98). +- Artificial Analysis Intelligence Index is fetched live again. The + artificialanalysis.ai leaderboard migrated to the Next.js App Router and no + longer ships a `__NEXT_DATA__` blob, so every run logged + `AA Index fetch failed ... __NEXT_DATA__ payload not found` and silently used + the frozen snapshot. The scraper now parses the App Router RSC stream + (`self.__next_f.push(...)`), canonicalizes AA's variant-suffixed display + names (`(Reasoning)`, `(high)`, ...) for mapping, and overlays live scores on + top of the curated fallback so a successful fetch can only add coverage. The + legacy `__NEXT_DATA__` path is kept as a secondary fallback. + +## [0.5.8] - 2026-06-05 + +### Added + +- `--context-length` now accepts shorthand values such as `64k` and `128k`. +- JSON ranking output now includes benchmark source and confidence metadata. +- Asahi Linux / Apple Silicon detection now recognizes Apple CPU and GPU names. +- Added GPU catalog coverage for `NVIDIA RTX A3000 Laptop GPU`, `RTX 3050`, + `RTX 5060`, `RTX 5070 Ti`, `RX 9070`, and `RX 9070 XT`. + +### Fixed + +- A3000 Laptop 6GB systems no longer get `0.0 tok/s` / heavy partial-offload + recommendations at the top just because bandwidth was missing. +- Windows CPU detection now falls back through PowerShell/CIM when `wmic` does + not return a useful CPU name. +- Models that cannot hold the requested context are demoted instead of staying + near the top of the ranking. +- Hugging Face and benchmark fetches now retry transient failures such as 429s + before falling back or failing. +- `Error fetching models:` now includes useful detail even when the underlying + network exception message is empty. +- Upgrade tables now show `0 GB` VRAM instead of treating zero as missing. + +### Changed + +- Curated registry data was split out of `constants.py` into + `whichllm.data.*` modules. +- Troubleshooting and cache documentation now better explain disk-cache paths + and stale fetch behavior. + +## [0.5.7] - 2026-05-20 + +### Added + +- LiveBench fallback data is now kept inline so benchmark scoring remains + available without relying on a generated sidecar file. + +### Fixed + +- DGX Spark / NVIDIA GB10 is now detected as a shared-memory NVIDIA GPU when + NVIDIA reports `memory.total` as unavailable. +- `whichllm run` now provides a Transformers `offload_folder`, avoiding crashes + when large models need disk offload. +- Cache paths now respect `XDG_CACHE_HOME`, including ignoring relative values + per the XDG base directory specification. +- Apple Silicon is now treated as shared memory in fit detection. +- Benchmark score fetching now runs concurrently. + +## [0.5.6] - 2026-05-18 + +### Added + +- Speed estimates now include confidence metadata and an estimated tok/s range + in table and JSON output, so uncertain backend/model predictions are visible. +- Windows now has an AMD/Intel GPU detection fallback via + `Win32_VideoController`, including 64-bit registry memory reads for GPUs + where `AdapterRAM` is capped around 4 GB. + +### Fixed + +- MoE speed estimates now use active-parameter metadata and a + bandwidth-scaled read floor, improving shared-memory APU estimates without + over-promoting sparse models on high-bandwidth GPUs. +- Newer MoE model metadata now recognizes A3B-style active-parameter names. +- Ryzen AI / Radeon 890M-class Windows iGPUs are modeled as shared-memory AMD + GPUs instead of CPU-only or tiny-VRAM discrete GPUs. +- Mixed dedicated-GPU plus shared-memory-iGPU systems no longer sum unrelated + memory pools as one full-GPU target. +- Windows AMD GPUs no longer receive a misleading ROCm-only warning when + Vulkan or DirectML backends may be valid. + +## [0.5.5] - 2026-05-17 + +### Fixed + +- `whichllm run` now resolves auto-picked GGUF recommendations to a real + GGUF repository and file before launch, instead of falling back to the + official Transformers repository. This fixes the accidental Transformers path + for models such as `Qwen/Qwen3.6-27B`. + +## [0.5.4] - 2026-05-17 + +### Fixed + +- Strix Halo / Ryzen AI MAX systems are now modeled as AMD shared-memory APUs + instead of tiny-VRAM discrete GPUs. `STRXLGEN`, `Radeon 8050S`, + `Radeon 8060S`, and related names get a 256 GB/s bandwidth estimate and use + the system shared-memory pool for fit checks, avoiding false CPU-only, + 99%-offload, and `0 tok/s` recommendations. + +## [0.5.3] - 2026-05-17 + +### Added + +- Linux Intel integrated GPU detection via `/sys/class/drm`, so Intel iGPU + systems are no longer always treated as CPU-only. +- NVIDIA `nvidia-smi` fallback detection when pynvml is missing, NVML init + fails, or NVML reports no devices. +- Apple-prefixed Apple Silicon simulator aliases, so `--gpu "Apple M3 Max"` + resolves the same way as `--gpu "M3 Max"`. + +### Fixed + +- `whichllm run` transformers chat generation now passes tokenizer mappings + into `model.generate(**inputs)`, fixing the `KeyError: 'shape'` crash path. +- RTX 5060 Ti bandwidth lookup now reports 448 GB/s instead of `N/A`. + +### Changed + +- README install guidance now prefers `uvx` / `uv tool install`. +- Removed the old marketing note from the repository and added sponsor metadata. + +## [0.5.2] - 2026-05-15 + +### Added + +- Curated vision-language benchmark source (`benchmark_sources/vision.py`): + a 0-100 multimodal capability index (MMMU-Pro / MMBench / general + multimodal, 2026-05) covering the Qwen3-VL / Qwen2.5-VL / Qwen2-VL / + Llama-Vision / Phi-vision / Gemma-3 / Pixtral / InternVL3 lines. +- Benchmark snapshot date is now shown under every ranking so a stale + recommendation is self-evident instead of silently trusted. +- Round 3 regression suite (`tests/test_r3_regressions.py`, 20 tests), + each verified to fail when its fix is reverted. + +### Fixed + +- `--profile vision` generation inversion: text leaderboards do not + score VLMs, so the only model with a direct hit was a + two-generations-old Qwen2-VL-7B, which outranked the current + Qwen3-VL-32B even on an 80 GB GPU. Vision models now score from the + curated multimodal index (Qwen3-VL-32B leads at 73-76). +- Apple Silicon partial-offload speed was estimated ~3x too low: the + flat 0.45x PCIe penalty was applied to unified memory, where spilled + weights stay in the same high-bandwidth pool. DeepSeek-R1-class + models on M2/M3 Ultra now report a realistic 4-15 t/s instead of + ~1.7. Discrete GPUs keep the 0.45x penalty. +- Duplicate `Qwen/Qwen3-Coder-30B-A3B-Instruct` key in the LiveBench + fallback (silently scored 62 instead of the intended 58, and broke + CI lint via ruff F601). +- `ruff format` / `ruff check` are now clean across the codebase, so + the Lint CI job passes (it was red for the entire 0.5.1 release). + +### CI + +- GitHub Actions updated to the Node 24 runtime (`checkout@v5`, + `setup-python@v6`); the Node 20 actions are deprecated from 2026-06. + +## [0.5.1] - 2026-05-14 + +### Added + +- `whichllm upgrade` subcommand: side-by-side comparison of the current + machine against potential GPU upgrades, with a verdict (worth it / + meaningful / marginal / flat / downgrade). +- Apple Silicon support in `--gpu` flag (M1-M4 base / Pro / Max / Ultra) + so simulator runs no longer fuzzy-match to ATI Rage Mobility-M1 and + emit spurious AMD ROCm warnings. +- Curated LiveBench, Arena AA, and Aider benchmark source modules with + frozen 2026-Q2 fallbacks for offline operation. +- Curated entries for reasoning / thinking lines: `Qwen/QwQ-32B`, + `Qwen3-4B-Thinking-2507`, `DeepSeek-R1-Distill-Qwen-32B/14B` and + `Llama-8B`. +- Frontier-model surfacing for 2026-Q2 releases that do not auto-surface + via cardinality (Kimi-K2, MiMo, DeepSeek-V4, GLM-5, Qwen3.6/Next, + gpt-oss, Llama-4, Mistral Small/Large, Devstral, Codestral, MiniMax, + Granite 3.3/4.0, Olmo-3, Nemotron-3). +- VRAM-aware auto floor for `--profile general` so tiny GPUs surface + full-GPU 3-4B picks instead of partial-offload-only 7B+. + +### Changed + +- VRAM estimation: KV cache scaled to 3.5 MB / billion-param / Kctx (was + 0.5 MB) so 128K contexts are realistic; MoE KV uses active*4 to model + attention head sharing; activation overhead refined. +- Speed estimation: per-quant efficiency table, per-backend multiplier + (CUDA 1.0, Apple 0.82, AMD 0.78, Intel 0.65), MoE active-ratio floor, + partial-offload penalty. +- Ranking: composite family selection key replaces tier dominance; + size_score cap 20 → 35; MoE size_score uses total params; + `_knowledge_capacity_b` so `--min-params` no longer hides + Qwen3-Next-80B-A3B on its 3B active. +- Benchmark merging splits frozen (OLLB v2, Arena ELO) from current + (AA, LiveBench, Aider) with separate caps and lineage-aware recency + demotion so stale 2024-era leaderboards stop over-rewarding older + generations. +- httpx `AsyncClient` uses `follow_redirects=True` so case-mismatch HF + URLs (307) no longer silently drop frontier IDs. + +### Fixed + +- Reject benchmark inheritance when actual params differ by more than + 2x from the family's dominant member, catching draft/MTP/abliterated + forks that share a `family_id` with their much larger base + (e.g. a 6.6B "imatrix-aligned" inheriting from a 158B base). +- Family grouping prefers the upstream-referenced model as the family + base instead of the highest-downloads member, so a popular fork no + longer overrides the official base for `family_id` assignment. +- MoE active-parameter registry corrected (gpt-oss-20b 3.6B, + gpt-oss-120b 5.1B, MiniMax-M2 10B). +- Quality floor (≥ 20) and speed floor (≥ 1.5 t/s) drop junk Q1_0 / + Bonsai-class attack vectors. +- 11 non-existent HF IDs removed from curated fallbacks (Kimi K2.5/K2.6, + GLM-5-Turbo, OLMo-3-32B, Llama-3.2-8B, Codestral-25.08, Mistral-Large-3 + etc.). + +## [0.4.0] - 2026-03-09 + +### Added + +- `whichllm plan` subcommand — reverse lookup to find what GPU you need for a model +- Ollama integration examples and shell alias +- Homebrew formula for `brew install whichllm` +- VHS tape file for recording CLI demo GIF +- GitHub Actions CI/CD (tests, lint, PyPI publish) +- CONTRIBUTING.md, CODE_OF_CONDUCT.md +- Issue and PR templates +- PyPI metadata (classifiers, keywords, URLs) + +## [0.3.0] - 2026-03-09 + +### Added + +- Evidence filtering options (`--evidence`, `--direct`) in CLI and ranking logic +- A100/H100 80GB aliases to GPU simulator +- Eval benchmark integration with confidence-based score dampening +- BenchmarkEvidence with confidence-aware size interpolation +- HuggingFace evalResults as supplementary benchmark source + +## [0.2.2] + +### Added + +- `--version` option to display package version + +### Changed + +- Updated demo image asset + +## [0.2.1] + +### Added + +- Vision model support based on task profile (`--profile vision`) + +## [0.2.0] + +### Added + +- `--status` flag to show Speed/Fit columns in output +- Published date and download count columns in display +- `published_at` backfill for ranking display +- GGUF-only backend filtering for model ranking +- Task profile support (`--profile`) for general, coding, vision, math +- GPU simulation (`--gpu`, `--vram`) for testing different hardware +- JSON output mode (`--json`) +- Rich table output with color-coded scores +- GPU detection for NVIDIA, AMD, and Apple Silicon +- HuggingFace API integration for model fetching +- Quantization-aware VRAM calculation +- Cache system with TTL (6h models, 24h benchmarks) + +## [0.1.0] + +### Added + +- Initial release +- Basic hardware detection +- Simple model ranking with Typer CLI diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..3424433 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,39 @@ +# Code of Conduct + +## Our Pledge + +We are committed to making participation in this project a welcoming experience for everyone. + +## Our Standards + +Examples of positive behavior: + +- Using welcoming and inclusive language +- Being respectful of differing viewpoints +- Gracefully accepting constructive feedback +- Focusing on what is best for the community +- Owning your contributions, including code written with help from AI tools + +Examples of unacceptable behavior: + +- Trolling, insulting, or derogatory comments +- Personal or political attacks +- Publishing others' private information without permission + +## AI-assisted Contributions + +AI tools are welcome here. The rule is simple: make it work, understand what it +does, and own the result. + +If you submit code, docs, tests, or examples, you are responsible for them +whether you typed every line yourself or asked a tool to help. Review the work, +test the parts that matter, and respond to feedback or bugs as you would for +hand-written work. + +## Enforcement + +Instances of unacceptable behavior may be reported by opening a GitHub issue or contacting the maintainer. All reports will be reviewed and investigated. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org/), version 2.1. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..2867b88 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,55 @@ +# Contributing to whichllm + +Thanks for your interest in contributing! Here's how you can help. + +## Development Setup + +```bash +git clone https://github.com/Andyyyy64/whichllm.git +cd whichllm +uv sync --dev +``` + +## Running Tests + +```bash +uv run pytest +``` + +## How to Contribute + +### Bug Reports +Open an issue with: +- Your hardware (GPU model, VRAM, OS) +- Python version +- Steps to reproduce +- Expected vs actual behavior + +### Feature Requests +Open an issue describing the feature and why it would be useful. + +### Pull Requests +1. Fork the repository +2. Create a feature branch (`git checkout -b feature/your-feature`) +3. Make your changes +4. Run tests (`uv run pytest`) +5. Submit a PR + +### AI-assisted Contributions +AI-assisted code is welcome. Use the tools that help you move faster. Working +code wins. + +The bar is practical: make it work, make it fit the project, and make it +reviewable. If you wrote it or asked a tool to write it, you own it. Read it, +test the parts that matter, and be ready to explain or fix it. + +### Adding GPU Support +To add a new GPU to the bandwidth database, edit `src/whichllm/constants.py` and add the GPU specs. + +## Code Style +- Follow existing code conventions +- Use type hints +- Add tests for new functionality + +## License +By contributing, you agree that your contributions will be licensed under the MIT License. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b4416fe --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Andyyyy64 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..e38d62f --- /dev/null +++ b/README.md @@ -0,0 +1,513 @@ +# whichllm + +[![PyPI version](https://img.shields.io/pypi/v/whichllm)](https://pypi.org/project/whichllm/) +[![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +[![Tests](https://github.com/Andyyyy64/whichllm/actions/workflows/test.yml/badge.svg)](https://github.com/Andyyyy64/whichllm/actions/workflows/test.yml) +[![Sponsor](https://img.shields.io/badge/Sponsor-GitHub%20Sponsors-EA4AAA?logo=githubsponsors)](https://github.com/sponsors/Andyyyy64) + +

+ Andyyyy64%2Fwhichllm | Trendshift +

+ +**Find the best local LLM that actually runs on your hardware.** + +Auto-detects your GPU/CPU/RAM and ranks the top models from HuggingFace that fit your system. + +[日本語版はこちら](docs/README.ja.md) + +## Quick start + +Run the recommendation command once, with no project setup. + +```bash +uvx whichllm@latest +``` + +Simulate a GPU before you buy hardware. + +```bash +uvx whichllm@latest --gpu "RTX 4090" +``` + +Install it when you use it often. + +```bash +uv tool install whichllm +uv tool upgrade whichllm # update an existing install +``` + +Other install paths. + +```bash +brew install andyyyy64/whichllm/whichllm +pip install whichllm +``` + +## Want a safer pick? + +By default, whichllm is ambitious. It ranks the best model that looks runnable +on your machine, including partial RAM offload and near-edge VRAM fits when +they seem usable. + +If you want a more comfortable LM Studio-style recommendation, start with: + +```bash +uvx whichllm@latest --gpu-only --speed usable --vram-headroom 1GB +``` + +This keeps only models that fit fully in GPU VRAM, filters out slow estimates, +and leaves extra VRAM for runtime overhead. + +If LM Studio still says the model is slightly too large, increase the headroom: + +```bash +uvx whichllm@latest --gpu-only --speed usable --vram-headroom 1.5GB +``` + +## Common workflows + +After install, run `whichllm` directly. For one-off runs, replace `whichllm` +with `uvx whichllm@latest`. + +```bash +# Best models for this machine +whichllm + +# Pretend you have a specific GPU +whichllm --gpu "RTX 4090" + +# Override detected iGPU/unified-memory limits +whichllm --vram 8 --ram-bandwidth 68 + +# Only show models that fit fully in GPU VRAM +whichllm --gpu-only +whichllm --fit gpu + +# Simulate a multi-GPU workstation +whichllm --gpu "2x RTX 4090" + +# Hide models that are technically runnable but too slow +whichllm --speed usable +whichllm --speed fast + +# Pasteable GitHub / Slack / Discord output +whichllm --markdown + +# Compare upgrade candidates +whichllm upgrade "RTX 4090" "RTX 5090" "H100" + +# Find the GPU needed for a model +whichllm plan "llama 3 70b" + +# Start a chat with a model +whichllm run "qwen 2.5 1.5b gguf" + +# Print copy-paste Python +whichllm snippet "qwen 7b" + +# Return JSON for scripts +whichllm --top 1 --json +``` + +![demo](assets/demo.gif) + +## See it + +```text +$ whichllm --gpu "RTX 4090" + +#1 Qwen/Qwen3.6-27B 27.8B Q5_K_M score 92.8 27 t/s +#2 Qwen/Qwen3-32B 32.0B Q4_K_M score 83.0 31 t/s +#3 Qwen/Qwen3-30B-A3B 30.0B Q5_K_M score 82.7 102 t/s +``` + +The 32B model **fits your card fine** — whichllm still ranks the 27B #1, +because it scores higher on real benchmarks and is a newer generation. +A size-only "what fits?" tool would hand you the bigger one. That gap is +the whole point of whichllm. (Note #3: a MoE model at 102 t/s — speed is +ranked on *active* params, quality on *total*.) + +## What can I run? + +Real top picks (snapshot 2026-05 — your results track **live** HuggingFace +data, this is not a static list): + +| Hardware | VRAM | Top pick | Speed | +|---|---|---|---| +| RTX 5090 | 32 GB | `Qwen3.6-27B` · Q6_K · score 94.7 | ~40 t/s | +| RTX 4090 / 3090 | 24 GB | `Qwen3.6-27B` · Q5_K_M · score 92.8 | ~27 t/s | +| RTX 4060 | 8 GB | `Qwen3-14B` · Q3_K_M · score 71.0 | ~22 t/s | +| Apple M3 Max | 36 GB | `Qwen3.6-27B` · Q5_K_M · score 89.4 | ~9 t/s | +| CPU only | — | `gpt-oss-20b` (MoE) · Q4_K_M · score 45.2 | ~6 t/s | + +`whichllm --gpu ""` simulates any of these before you buy. +By default, rankings include full-GPU, partial-offload, and CPU-only +candidates when they are usable. Use `--gpu-only` or `--fit full-gpu` when +you only want models that fit entirely in GPU VRAM. +The default table shows memory, estimated generation speed, fit type, and +published date. Speed is colored by practical usability: under 4 tok/s is red, +4-10 is yellow, 10-30 is green, and 30+ is bright green. `~` / `?` still mark +estimate confidence. + +## Why whichllm? + +Fitting a model into your VRAM is the easy part. The hard part is knowing +**which of the models that fit is actually the best** — and that is what +whichllm is built to get right. + +- **Evidence-based ranking, not a size heuristic** — The top pick is + chosen from merged real benchmarks (LiveBench, Artificial Analysis, + Aider, multimodal/vision, Chatbot Arena ELO, Open LLM Leaderboard) — + never "the biggest model that happens to fit." +- **Recency-aware** — Stale leaderboards are demoted along each model's + lineage, so a 2024 model can't outrank a current-generation one on an + outdated score. The benchmark snapshot date is printed under every + ranking, so a stale recommendation is self-evident instead of silently + trusted. +- **Evidence-graded and guarded** — Every score is tagged + `direct` / `variant` / `base` / `interpolated` / `self-reported` and + discounted by confidence. Fabricated uploader claims and cross-family + inheritance (a small fork borrowing its much larger base's score) are + actively rejected. +- **Architecture-aware estimates** — VRAM = weights + GQA KV cache + + activation + overhead; speed is bandwidth-bound with per-quant + efficiency, per-backend factors, MoE active-vs-total split, and + unified-memory vs discrete-PCIe partial-offload modeling. +- **One command, scriptable** — `whichllm` prints the answer; add + `--json | jq` for pipelines. No TUI, no keybindings to memorize. +- **Live data** — Models fetched directly from the HuggingFace API, with + curated frozen fallbacks for offline or rate-limited use. + +## Features + +- **Auto-detect hardware** — NVIDIA, AMD, Intel, Apple Silicon, CPU-only +- **Smart ranking** — Scores models by VRAM fit, speed, and benchmark quality +- **One-command chat** — `whichllm run` downloads and starts a chat session instantly +- **Code snippets** — `whichllm snippet` prints ready-to-run Python for any model +- **Live data** — Fetches models directly from HuggingFace (cached for performance) +- **Benchmark-aware** — Integrates real eval scores with confidence-based dampening +- **Task profiles** — Filter by general, coding, vision, or math use cases +- **GPU simulation** — Test with any GPU: `whichllm --gpu "RTX 4090"` +- **Multi-GPU simulation** — Repeat `--gpu`, use commas, or write `2x RTX 4090` +- **Full-GPU filter** — `--gpu-only` / `--fit full-gpu` hides offload candidates +- **Speed-aware filtering** — `--speed usable|fast` hides slow rows by threshold +- **Markdown output** — `--markdown` / `-m` prints pasteable GFM tables +- **Runtime memory budgets** — `--vram-headroom` and `--ram-budget` avoid edge fits +- **Hardware planning** — Reverse lookup: `whichllm plan "llama 3 70b"` +- **Upgrade planning** — Compare your current machine with candidate GPUs +- **JSON output** — Pipe-friendly: `whichllm --json` + +## Run & Snippet + +Try any model with a single command. No manual installs needed — whichllm +creates an isolated environment via `uv`, installs dependencies, downloads the +model, and starts an interactive chat. + +![run demo](assets/demo-run.gif) + +```bash +# Chat with a model (auto-picks the best GGUF variant) +whichllm run "qwen 2.5 1.5b gguf" + +# Auto-pick the best model for your hardware and chat +whichllm run + +# CPU-only mode +whichllm run "phi 3 mini gguf" --cpu-only +``` + +Works with **all model formats**: +- **GGUF** — via `llama-cpp-python` (lightweight, fast) +- **AWQ / GPTQ** — via `transformers` + `autoawq` / `auto-gptq` +- **FP16 / BF16** — via `transformers` + +Get a **copy-paste Python snippet** instead: + +```bash +whichllm snippet "qwen 7b" +``` + +```python +from llama_cpp import Llama + +llm = Llama.from_pretrained( + repo_id="Qwen/Qwen2.5-7B-Instruct-GGUF", + filename="qwen2.5-7b-instruct-q4_k_m.gguf", + n_ctx=4096, + n_gpu_layers=-1, + verbose=False, +) + +output = llm.create_chat_completion( + messages=[{"role": "user", "content": "Hello!"}], +) +print(output["choices"][0]["message"]["content"]) +``` + +## Usage + +```bash +# Auto-detect hardware and show best models +whichllm + +# Simulate a GPU (e.g. planning a purchase) +whichllm --gpu "RTX 4090" +whichllm --gpu "RTX 5090" +# Specify variant +whichllm --gpu "RTX 5060 16" +# Override detected iGPU/unified-memory limits +whichllm --vram 8 --ram-bandwidth 68 +# Simulate multiple GPUs +whichllm --gpu "2x RTX 4090" +whichllm --gpu "RTX 4090" --gpu "RTX 3090" +whichllm --gpu "RTX 4090, RTX 3090" + +# Only show models that fit entirely in GPU VRAM +whichllm --gpu-only +whichllm --fit gpu +whichllm --fit full-gpu + +# Avoid edge fits and background-RAM surprises +whichllm --vram-headroom 1.5GB +whichllm --ram-budget available +whichllm --ram-budget 8GB + +# CPU-only mode +whichllm --cpu-only + +# More results / filters +whichllm --top 20 +whichllm --details # show Downloads metadata instead of runtime columns +whichllm --speed usable # minimum 10 tok/s +whichllm --speed fast # minimum 30 tok/s +whichllm --min-speed 4 # exact tok/s floor +whichllm --markdown # pasteable GitHub-Flavored Markdown table +whichllm --profile coding +whichllm --context-length 64k +whichllm --quant Q4_K_M +whichllm --min-speed 30 # exact tok/s floor +whichllm --evidence base # allow id/base-model matches +whichllm --evidence strict # id-exact only (same as --direct) +whichllm --direct + +# JSON output +whichllm --json + +# Force refresh (ignore cache) +whichllm --refresh + +# Show hardware info only +whichllm hardware + +# Plan: what GPU do I need for a specific model? +whichllm plan "llama 3 70b" +whichllm plan "Qwen2.5-72B" --quant Q8_0 +whichllm plan "mistral 7b" --context-length 32768 + +# Upgrade: compare your current machine against candidate GPUs +whichllm upgrade "RTX 4090" "RTX 5090" "H100" +whichllm upgrade "Apple M4 Max" --top 5 + +# Run: download and chat with a model instantly +whichllm run "qwen 2.5 1.5b gguf" +whichllm run # auto-pick best for your hardware + +# Snippet: print ready-to-run Python code +whichllm snippet "qwen 7b" +whichllm snippet "llama 3 8b gguf" --quant Q5_K_M +``` + +Markdown output is intended for GitHub issues, READMEs, Slack, Discord, and +blog posts: + +```bash +whichllm --markdown +whichllm -m --top 5 --gpu "RTX 4090" +``` + +JSON model rows include `fit_type`, `vram_required_bytes`, +`vram_available_bytes`, `uses_multi_gpu`, `multi_gpu_effective_vram_bytes`, +`estimated_tok_per_sec`, `speed_confidence`, `speed_range_tok_per_sec`, +`speed_notes`, `benchmark_source`, and `benchmark_confidence`. The speed range +is a planning range, not a live benchmark. + +## Integrations + +### Ollama + +Use JSON output to feed scripts that map HuggingFace IDs to your local Ollama +model names: + +```bash +# Pick the top HuggingFace model ID +whichllm --top 1 --json | jq -r '.models[0].model_id' + +# Find the best coding model ID +whichllm --profile coding --top 1 --json | jq -r '.models[0].model_id' +``` + +Ollama model names do not always match HuggingFace repo IDs, so a small mapping +step is usually needed before `ollama run`. + +### Shell alias + +Add to your `.bashrc` / `.zshrc`: + +```bash +alias bestllm='whichllm --top 1 --json | jq -r ".models[0].model_id"' +# Usage: ollama run $(bestllm) +``` + +## Scoring + +Each model gets a 0-100 score. Benchmark quality and size form the core; +evidence confidence and runtime fit then scale it, with speed, source +trust, and popularity as adjustments. + +| Factor | Effect | Description | +|--------|--------|-------------| +| Benchmark quality | core | Merged LiveBench / Artificial Analysis / Aider / Vision / Arena ELO / Open LLM Leaderboard, weighted by source confidence | +| Model size | up to 35 | `log2`-scaled world-knowledge proxy (MoE uses total params) | +| Quantization | × penalty | Lower-bit quants discounted multiplicatively | +| Evidence confidence | ×0.55–1.0 | none / self-reported ×0.55, inherited ×0.78, direct full | +| Runtime fit | ×0.50–1.0 | partial-offload ×0.72, CPU-only ×0.50 | +| Speed | -8 to +8 | Usability gate vs a fit-dependent tok/s floor; reported with confidence and range metadata | +| Source trust | -5 to +5 | Official-org bonus, known-repackager penalty | +| Popularity | tie-breaker | Downloads/likes; weight shrinks as evidence strengthens | + +Score markers: +- **`~`** (yellow) — No direct benchmark; score inherited/interpolated from the model family +- **`!sr`** (bright yellow) — Uploader-reported benchmark only, not independently verified +- **`?`** (red) — No benchmark data available + +Speed display: +- **red** — Slow generation speed (`<4 tok/s`) +- **yellow** — Marginal generation speed (`4-10 tok/s`) +- **green** — Usable generation speed (`10-30 tok/s`) +- **bright green** — Fast local generation speed (`>=30 tok/s`) +- **`~`** (yellow) — Estimated tok/s range is available +- **`?`** (red) — Low-confidence speed estimate; backend/runtime sensitivity is high + +## Documentation + +- [CLI reference](docs/cli.md) +- [How it works](docs/how-it-works.md) +- [Scoring](docs/scoring.md) +- [Hardware detection and simulation](docs/hardware.md) +- [Run and snippet](docs/run-snippet.md) +- [Troubleshooting](docs/troubleshooting.md) + +## How it works + +### Data pipeline + +1. **Model fetching** — Fetches popular models from HuggingFace API: + - Text-generation (downloads + recently updated) + - GGUF-filtered (separate query for coverage) + - Vision models (`image-text-to-text`) when `--profile vision` or `any` +2. **Benchmark sources** — *Current tier* (LiveBench, Artificial Analysis + Index, Aider) merged live when reachable, plus a curated multimodal / + vision index; *frozen tier* (Open LLM Leaderboard v2, Chatbot Arena + ELO). Tiers have separate caps and lineage-aware recency demotion so + stale leaderboards stop over-rewarding older generations. +3. **Benchmark evidence** — Five resolution levels, increasingly discounted: + - `direct` — Exact model ID match + - `variant` — Suffix-stripped or -Instruct variant + - `base_model` — Base model from cardData + - `line_interp` — Size-aware interpolation within model family + - `self_reported` — Uploader-claimed eval (heavily discounted) + + Inheritance is rejected when a model's params diverge more than 2× from + its family's dominant member, catching draft / MTP / abliterated forks + that share a `family_id` with a much larger base. +4. **Cache** — normally `~/.cache/whichllm/`, or `$XDG_CACHE_HOME/whichllm/` + when `XDG_CACHE_HOME` is set to an absolute path: + - `models.json` — 6h TTL + - `benchmark.json` — 24h TTL + +### Ranking engine + +1. **Hardware detection** — NVIDIA (nvidia-ml-py), AMD (ROCm/dbgpu), Intel, Apple Silicon (Metal), CPU cores, RAM, disk +2. **VRAM estimation** — Weights + KV cache + activation + framework overhead (~500MB) +3. **Compatibility** — Full GPU / Partial Offload / CPU-only; compute capability and OS checks +4. **Speed** — tok/s from GPU memory bandwidth, quantization, backend, fit type, and MoE active parameters +5. **Scoring** — Benchmark (with confidence dampening), size, quantization penalty, fit type, speed, popularity, source trust (official vs repackager) +6. **Backend filter** — Apple Silicon and CPU-only restrict to GGUF for stability; Linux+NVIDIA allows AWQ/GPTQ + +### Project structure + +``` +src/whichllm/ +├── cli.py # Typer CLI: main, plan, run, snippet, hardware +├── constants.py # Backward-compatible exports for registry data +├── data/ # GPU, quantization, framework, and lineage registries +├── hardware/ +│ ├── detector.py # Orchestrates GPU/CPU/RAM detection +│ ├── nvidia.py # NVIDIA GPU via nvidia-ml-py +│ ├── amd.py # AMD GPU (Linux) +│ ├── apple.py # Apple Silicon (Metal) +│ ├── cpu.py # CPU name, cores, AVX support +│ ├── memory.py # RAM and disk free +│ ├── gpu_simulator.py # --gpu flag: synthetic GPU from name +│ └── types.py # GPUInfo, HardwareInfo +├── models/ +│ ├── fetcher.py # HuggingFace API, model parsing, evalResults +│ ├── benchmark.py # Arena ELO, Leaderboard (parquet/rows API) +│ ├── grouper.py # Family grouping by base_model and name +│ ├── cache.py # JSON cache with TTL +│ └── types.py # ModelInfo, GGUFVariant, ModelFamily +├── engine/ +│ ├── vram.py # VRAM = weights + KV cache + activation + overhead +│ ├── compatibility.py# Fit type, disk check, compute/OS warnings +│ ├── performance.py # tok/s from bandwidth +│ ├── quantization.py # Bytes per weight, quality penalty, non-GGUF inference +│ ├── ranker.py # Scoring, evidence filter, profile/match +│ └── types.py # CompatibilityResult +└── output/ + ├── ranking.py # Rich hardware and recommendation tables + ├── json_output.py # Ranking, plan, and upgrade JSON + ├── plan.py # plan command display + ├── upgrade.py # upgrade comparison display + └── display.py # Compatibility re-export shim +``` + +## Development + +```bash +git clone https://github.com/Andyyyy64/whichllm.git +cd whichllm +uv sync --dev +uv run whichllm +uv run pytest +``` + +## Contributing + +Contributions are welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines. + +## Support + +If whichllm helped you find a model or avoid a bad hardware guess, +sponsoring is appreciated. It helps keep the project maintained: hardware +reports, packaging, test fixtures, benchmark updates, and support for more +machines. + +whichllm will stay open-source either way. Issues and PRs are always welcome. + +Useful? A GitHub star helps other people find it, and I'd genuinely like to +know what it picked for your rig. Drop it in [Issues](https://github.com/Andyyyy64/whichllm/issues). + +## Star History + +[![Star History Chart](https://api.star-history.com/svg?repos=Andyyyy64/whichllm&type=Date)](https://www.star-history.com/#Andyyyy64/whichllm&Date) + +## Requirements + +- Python 3.11+ +- NVIDIA GPU detection via `nvidia-ml-py` (included by default) +- AMD / Apple Silicon detected automatically + +## License + +MIT diff --git a/README.wehub.md b/README.wehub.md new file mode 100644 index 0000000..e42dfd8 --- /dev/null +++ b/README.wehub.md @@ -0,0 +1,7 @@ +# WeHub 来源说明 + +- 原始项目:`Andyyyy64/whichllm` +- 原始仓库:https://github.com/Andyyyy64/whichllm +- 导入方式:上游默认分支的最新快照 +- 原作者、版权和许可证信息以原始仓库及本仓库 LICENSE 为准 +- 本文件仅用于记录来源,不代表 WeHub 是原项目作者 diff --git a/assets/demo-run.gif b/assets/demo-run.gif new file mode 100644 index 0000000000000000000000000000000000000000..7758a667e67c3cb58bb643d8923a5695403ee669 GIT binary patch literal 450814 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

MwvVytIO?IQ<3 zhT)0hnJCYZFuki+#bZ3v5Ce`j;%ZI7Ya8J~?t}S+YFZ&^vG(t-T^EKiV`ddCm-4M~ zk_V8)&+?WSYcF;oZy0iYGs^$El)Qe4z(9EP+!W7h#Idf!)cUYu6`{%$L-v>=L$AD7{OZ`R2#x&GXBI|iB zU$GRTvlVK;wiF#`$0QbDlie><)1(zeYe0rBSS(ScaITD-r?lF#OIAtGg#mK2mQ#cr zgVUgtQ8Y!o+j?L0k^<^-tkPejp>QCo9|e#SRyHBQ$ESzpcRw|eA7Bz{530Lgw7(WG z*MPHFDykFdF995ek>SqZjRvZC$Oa`K|1;rO3Odmr-7;K4<-qH;swg$lr6(qXisfSjY3VZ|s_ zKtS!JX?Fb1CLnx>oQ?y?Q1EhroPw0t4l9BQaJK$BH~={#=W_4}|506>2Q`Bgy}E9C z>>e9oz0z~wlE+Y6#z5P+&}iuxhNlme&hL@?wJtoXEu?06 zqw6)Qqbyr04d6+r9#gK!W9rRB04G{Etx#@1)O}f^v0QW7EN%A|KHIDgI8bi3Bqc7p z7>y9iC@pMh3xy+A7BHw&MU2N$JIsWY$M$vS3XW}UzwQQ~&k<5LFccu$2)3h&hD(+W zQ-aa}#{nqa8p)XR94j>r!1#X{3YtWV#sbKWX-_NAf=`S;5UMpIaie;vH~E9$3%^kb;VC`Fu&KP}r3#P_C+@*w;l$CDa9!$;+ksyD2D>19fg!zRLpr zwztz(%sIYerv($9oF<$4sWh)0n)>@Az5$s_lmv?f^ilR~TxX#MwACzALVkY7ytNWl zu!itZmM$<*){|(0fi_O#K*ev7G)~(OCoqL#JaRL?;Q(cHAWaKaJ`7}d17?S*(lj*d zb(&h&Z}bE1F%Snnwx{bsJ8wriktkKCtkR~8_4P)H0r6LYDFOG?Om_W;I=zFi9a0Fz zl)2cP%n~ry`wFM!acBkXDADCI6w~m;{GMw;v2fyN&f0wTdYmdArL5MMcrhMbqBO7n zWc<6`vTf;^B)OPH%-dj~KC-Day7!W`>&gDI+tPa z`2D8o!eraC3|gTKKn4^m2TG?hXrI2d{mk!SQqrau-}dT->63HdTyt9 zN6f_|f-tkJijLeNJ?-nj)7Jz4{FAo*HD`k!J`f9VMk)FF*waOXst5SeS2~$ZpB|Z5 zRRftD(ht7v=j?wNB_a0tKkIr;TvUUMp}^|xu;)WtCecA%I6&P@#CQ$xi^PSVQfx!#0+%7V*p2lE<%(9b2ke3k!9sHYoUJ4(4RQI%dy zr1VolfvNWG#JBIIVV@r5+Zd$@I59=#xoH03g6Sd#-1UemA|Z2@a(tBm6^ldian-5) z7Yo#|-Vd;s$aX1Hm};ou*XeQj6p^(8m4N0g_J<$t+b&}Ilx;Wd0b;c1&3_1r2$&@b z6%V6RhdRohbxq5+XE4C&$)F!o;itBh=?PN+q>-m1Z@ftxP6y=1jPD5?t0CY5LRf_@?BiUV0{x8qt z*+ifO&E+iI$e;!>qA#3)H8uDsesJ9Nh&cP_y8^939ZQ{__H2oQI!i-Y?;Rc5oX}SoSDXmQqCEbwlvu!0MXF^p##GaOnX{gtnz3ab z1M+3S(_lem)`5kUm#I?>Z}$z!ZL!On>L{Sl1{+OTGlZ~uMJ8aMOAf@H1L;?*@_r|M_jSrTG((AbV3_)z6pK|T@RZ9k--1fze?N8%|II9*XII6Ec-%LzBd9iby z?r8a`Ox@LjiVwNs7>t1Ov?{oubgJOK^&M>A<^}MOqRssF%3(P@&EDG^MQThgaiCau za{R;drT@-cFfZ;v#*#L^l_zLqD118_t#TN8SoNr+<}*Ix4Ky5yl#D2*d}M?h^&eS` z`tSkoq4v3&l`_%lwYxY)?bJJkKxV@0qxt8%J`NCvjFEKR52yY8wfo8iZLgup8{1WB zIBX68rt@#^DhXNoT`6d$ZWt<|kU&AAye##P*%n8pOl7YtFT!c{~* zC7&$K)gu|H9R->`k2JIpxa0K&&0GJvfJ5O@Iu|xil^IjZY>M?^bP-!=-2$M)0Hed@ z5<37U6bqY5R}G-KEg#`Ddp~K3u~q}sE#^{!|38&VXH^J&yr-~KGKsocswBK@H~mp* zUdKRjMmJn=MK+{foa?^W2OPmV? z${Qy=0l~s6c)A0CugQlsRt8_D1xGec5dvmx0G8IR|6wd90Jz;`kKo^Jo!;X4wH0&X zU#$nkw}7ARCbRsA_kaAhS3o%_vU&^+P}q@C+u=) z`D=Hjr5KcW{8!t=dVlHc-Ptxq79fNmp+HAN*K~7~Zew)u-k5IT8t5q*{=@xdxnsoI zPeWfljQb+ucla;==6So;cYa&`@82f&)dlB1XkGr@>+TMZxWXZZK{lBiiVKW5X;`(D zF_roGC|7ZRyH)eX_Vyak81c{Q?rHyi3_>=GGOG!p&-Mdx+uNkac9nD=m@+YszgH8} zUzIXlYVhk&l$y<(vN4U^{ee1e=ISXH`6na4Zk_t5vPW(6`~4%iw1+NZtDF0erNsPw zMZM?s!`m}9%|I&Fynk3RrRMg$l{??R(G-9OA6>b8Dk{m^NPou?Z)`NRyTM`WwvkXk!DiL%{*omf&66}?Au+vWMxQI z^ncR{kj`PrD2~Q34lOWuchbuf7n0|IN1iw&e5*K8y*c#r-}PJR8%N#LZreESWfS)f zIIsC_L+De*gQKI)b0+^6zi2J<^gC|Fgh%HsQRa(X$yL+k-pMBJV(tADcl_pWNpqk6 z#mo->eAu(n^ut>jHc9l9SLu}GuuL#+wORY7&i&Kl`MaD7%LeCW`_kT(eQNN5B;o6X zIr~o>eY}9bavS>{_QUdGc72KC#+J(5-ge9FPXcZ)Di5fP_o&%Xgl8XiDbj+wqkl4; zw{qoC_jqMy3LrfQjfT=kgT^vgNq-O=8wg`TcC1?jO1127`?jlZzK@$9xbBA z5);!e?{sJUik8v*3u+iI#408xoW<&{Umlvf ze$O(?$In2N%3Ry$MiH_gMw9cI3cvMpMU3a`xkKR!(~SV%YwP@_bO+md;b^RbzXS_= zH22Ie;kBRei`mY_x|>0JU+*9eId0a)hwhsnGTSSO5Uv0Ce6Hee6E~6JucNN&tjq5n zFO{#ZR>KRj`@QllU2KS0^|V%=%R3Aas3Mhvw#x8J&LgQ4xBUpABVlbUU? z`X#*m&1sxEU$~S`4-&9vgpFs6unGIa1sT6Ytm{w~U(L5^Z|70G4O711FQZsVK@ zPi{TF*lB}3au+BI!$FIKdcGYl>QE*mMR*JV3@C?dT1QysFf$+HUWbmC99hQ1m8>*j zkeLKWg#hBoc^7ozoNuo3x{HY zt~9`#-B|Q|>5a&K7T9ui=DZ1R=}sr!wO%RqPjt}nLd`Rw{C}5%shpcnZfKU)SjO)2#W9^58&ZZfmgI-3Nuff@Ay z7AFgN82$ttr?-%dL#=Van8BMX=(agVp1US}mfT06xOQss`*{Y;4U5Z)m zmEE1;*bas8zg~G0q~`Q*a{6VO4#}yL`~CXR(qpS?dkm9pjcCaa=hu`cW<&NT*<{$! zLAuRZ5jgstXF6(MQFJNTVip6$(zJvvQ$lNAq=h|vlZQt5;0h7n^c{fNlDZthl&%hU z9G*<@EUbp^qy4pZ=GZ$QM@bdpsgqst+y>Fr0EuK;nhMFOv9In!(n|!Toi&|z80R&G z%bSHF&|V-m*%(StSXt%)bpSTk<9rB9zSKU`m(mwG^U%BINiGwOyOKR+7&iXS*R|iE;@<>VN=Ql;DR(tCajo>3H$+V!VoCz$60WJ_$eA>Z1Wklb=Z6gw^NB98IZRCI z9xOFEbnH}$OCECIL)_WnLV%t|0{)s)mi4Jfkhl*DCqT22up=ld?*{$~L`VK$T)Wgm z^FCb>)53yJw%l|w7s}|_K0Xe5a&7MjXrt9a-a;qQEYYtYGS;uP{7E8zNla&GNf?#F zVUkScqj_DnG={8bTwZG-(n4-6w`sHRUtuLnSeE`}ON<_BY!T5W7<-E5$!5t~(%%J~ zJhAdL)keQtH6hXs4b~|9oMC&akXSWaav_?v9UONxcCdX#cVZI5EL`q(ESciY#Igy~ z=RMsUeRQp#7wo?HYYj#p*$({|_z}tG-1Kg_^P&TZM0oH1sQdI$W;b^|biLy7I#sdU zNTsLW8hF39=mKeB!?JkJrq~CQMz%&~&{EFLpdQ8vYk!dat?3{(w&yxyINb4MPY0DT zyl7HjKY@+C<6EB?+AF~!m{hnd0%(3OQ*1g{7K&n^>?Pd~NxHjPrFIK7Jtay!hl_ zK+c(lxWEJeJtsyZ_BNQTmc%d+NmFt#8_PE|*uBA}*3Lj#PHhK;qDMj>iv~hMVJ0bz zzvE=c>&zxiHe7i;JzNVd>fQia3)vIvR3?*c+NySVmeN}jqw<8W8CL8aRPX9vuJ4i4 z+=&eSG(%4idp_LS*Il$5c<-DB+Sj&|Ws!JZPB|B`YM1-8ta&%##WP?NtJLEHw>;v? z?)lb203KhIIGtMX2Ht(f( z)-nKGtwt+zKY~;y^sQYp#`G6nF=@d+e{Sk#3@I{zRK@0#J!GuiFq*^U*=)UBVoldk zb3Pdt01jMpvl8y4`MZ6E7b_^!3?<|9mVBEGEIxaCm+E7lVYXG;{%P|_L`x$7bT z#sL{E4fB*cgCkj7tEkrb4v#bJ6LCB-+BRe zfQJ}+ZrOUDvmGZ6BUBFHDuLY@fJr+JILq$a5~gENw9#(jub9Gt{+0j`$@S25vDqD` zM${k)h8rV%-JQYFJkX!G+ujv&mKRdrMW1mKO-x<3^5$h_{ll|>$s)kiCMLqBD^eR1 zB7)f>ZnEv2I4m9*kwq1Dg%F)S(Q@B4@{n31GmG?;bUE7CIdc@zWT(SeXJoxW9J@d3 zQ-^IGzAPUIchMuY>7n|n?GBw_K7bXIwuW)Jk27vw=MciwhbInDvq|r>vt5NySUJy@ zwAkiCIuQ?Oe0&M=#?=}4=fpkRF>-eRfMq9`*L4Q9KACXtwdX}SeJ+9ODuc@G1Is(W zky=qUj=lkhC+Y(?-=lgHlI7+v$AE;p*@pzNOF3MX3xFP~oLh`f5LZSI);()Ub9KRO z4flf5)1If@zRV_|0cIuT5tRwNdvkwu>KxD;|Ng^aVx2s2Q3pkg2b6Wn{eL2^kWcH0)(BC|rUIGA|U%i^yZQL^9WMN@apSu zsPA;fGvjrH{bnAMftw6T5)i@d<|^KL_2scbN33y1|36T-$l4FvjOCD!&~K`zk6gK7gcgYqPw1r z!7vN3lEiqedIhzT!J!I=7rEA+ApE90L>l8T_f;e*M~1uouJP^<-1Ofl@A<{Xq;~v; zUk}eF8V==ylcE(4?Su)RA9NlE81kqdxt-qoZYVE8;1RaKBRr}rBvF=DALDzaewFQ0 zkw2hv5hAq?-fxMYY96Hpw=S<->`GV*7ssZ8@&6gu$7H)QWkyGHpoA-I7@$g*xMBb% z@QROng8&_)i$HpNXHW~!y!e>MyeFP-x#n8pw<{e7N!h_j+0hvT;IMFp8SdetI1)%E z<|yC4mHD)HdY`TI9s|bu6M4nAvH-5i%<$pHuTF+eZ^?((#;H?1=_)q=XKt7AGjwnA z)Enx`Lh^Ep!^ZtN!sR63+!@Y1WQpe)(=)H8TG8K-7FUS6mT|zr=8&b9G(qAf?0M?3 zGtZoL=T~PQ)d=M;f1=`&-oma1N1655asC*&Zx7U1*&Dg$qwax&)|G^8aH#(PJ<({~ z#X-zCXxcOODJ8Q!XZiWcr#%lHUo>Ppo0!}5p16DDmzSSP=20j&?sz21BqQ&vpR&9e z3ooy^vBsU3v4=F-_&++a{i*TSA)SqCxwt!p&-EC2aXqoqk@SLF@l tH+>OOD{$M zFv(pLg&>ROT6vRp?kBK9d`5{G%7$2y8{GfL)SHLJ^#1Yx_c^n#%`{U@t7*}q!L%=u zGnE!9qmr~RZGgm7lsS5uLYOpAn&B*}K#2w{>Xgo!A7_AH-wzxjT@|NQ=*>pF9t z`g~HA9QnELH;{02-pbTp@I@H z(~~i(>urspjvn1?mfU5sGdAGb-hyoR(UZ$fZv!dpoVVI(dAyao_biPlq+e#P4Vv*L zzWx|b!O`;vhYrA(56EqzWl(9SsSxM*zvpvJP&QUyHFrj0xSn5Gr$$G9M>4otlLwHF zHAT)~72VURLM(l{b^a|k(WGfB5qE>m_s05LDV*mS0Qz@C{AV_S2fev;7kI5aocEfl z5eQ2{tMm~c6d_z%p7xRQm+)HrkZ`*%oT!wpJGFa3!4R@(#ak@iOZMo!&AzhwsZUFu zSF)2{!#sHx^YL;63;>aBgsmQZ8QEGvvPODLunc|=sN8Q_1u_J{;|KH{MOPRI9$Bwp zhM}O_Z!4E)J#!yiSwITJHeRkS3$$>EmOT?^9{%6j#}dx&wFA-9BLd4+U;X@1Toc5nWBQueqYu^^HgIG$i;s<1-523|uz^UFZ2ghG5@C3G zY5ROM5U+c=J}aYIWS%FZC8KnYE)xOp%~&^Na+{c=TAB>-Sm&wLa?9w0X;&J}0leyq zKT$n%sXy_>EELtYR(nBa=`y-n#?OmlpSi!F8IU~?vxzC#$fMhhXLW3%a!Zec&YfQd|#M6(+GB4794sYh{0r#;AgH?Z#dTTuG-ROxZ3Y{qKqCQRso zqqDl~!@O*5Py3Q{BNUnXn)vts;bNU&=OPhRZ0<}x1SQKH>S3Ry)-%Xs{t=X_%|kj( z@h1IuDLmO91Ni8@LFh8p`n=?EacNP6M~B%Sy)bboQ7oLzNjRkUvw$cY^~9zHezJ4XI3 zHJ&vtBlHQyFX5Ia69d-Q;3^d6g8x2DbyCQj3De_$)N+m_;ajP(LxPvDcG2g>xRT3z z#OJxAj^bK;-!At@nXRWHW7`|<~_d;Lq4kkF|Bj< zdsNr6YjeY_p`EKXCrb`4BizGemgAkA&7DRGBAS3}&i8^_!4h8Y&I0>iX-MQuL=VrF z`R3j4gp4X>mVGr>E=}Dh_&54<11h|{ZO7&1=ez7B5b!|8oDq5kHXG@N$)s?;$YHXJ z`)|RD>1i`G;DjG|A|26vkoGY=O%d$P8edpi;B6q)9lhdZ+k`rfclpoGoW_4Bie}Le zWLk48t=%!uZ?^N0?5DHbX@Q*)7q>_cGm5Z=R9pN6DCd$_l^X&X$?aYv-*)V`-P2*K z+`w!ud65WDS0ABfXgYI;8`f;9()d;bvBNLBv@s?Ic0!OlhzK?giibi z%2C49k$gr@99b>t%Y|8}TwgvqTs$WTytL~!_CaZVE6%Ryyjz>3y()2?%NNcx@ZP9v z-46mQLS`z=d}M(p?s{YWVRXaNZi6apg<$XEm&L@uIT9~f?Yo5Sn~BB;Ba{l9%tk8AYvjG?JGOPy#ey0_Xrz^*)>8oP$Cf61L z>v>+h2z-<00Cl|6!cq3MmmB)w0pszcXtZ5#!ZlNqu@?w$Jw^Ae1gy%=wA!gGDFB8D z&P55EIpQ2AyiFej z(k^kUH=gNgx9P)eJln+c-(=~ltXy5ee>Obf%BQC0MC$|(pLJRVxanF9CBFuNir4v3 zCe)&C-pU51s{$SDmU}#Sn59(9=I3f{FG4z+UdjzqBLUSP+=$Dvd+JlG74cp9 zZJM0#`f86oGr5KUY9p>4@-3Ojsaj@$0HQ>>nx8}G_aa9jyhN!V@gEz5kMkRk91QOg zzT$}J5&VL<&8sol^aBG&(BB?pK06(;iKjOE@*vdn{=gCD%%pa8-U=^SEN?{#KS6(9 zv~*0C284zO5?~v6 z%(C2L^BfCLeeD$gQ){AsW0~|n(!AI`msum{hAjR0yZ1`X?^xL9y;+N6+E1!Au6#Q= z%=8p@?g{KeE*OL!*9;l%ye!<^wR&FTpUU_?buBpdpXuAm#mPy~Q6nGyBkvYVmdD8{ zPc~*3=EN+QcrrQ~7U8t2+H0Mc_KlA}E&GbppmT=}_+|@6E#uF7P{);yHTljjGk*s@ zeN)WQm2S_3F1cRQG4BJD3JOsIYDx?nMgd*fP-0NK3xRM{jYYb3#6>AVZ5J$0U%^?T z96gvZbL!7}SSPY#j#kE!Nd)>ioOv2Y=+s*A@5_zZFaCWSn06TQz~@6OBuRO$izZ7p zm-Bd9Y!jbK4kJNL7bM_W zh=QXLUuqpjN5UrYq=@^bnjio!kWA3GRE+VkTQ2p zdkT}k5kUS)*$ygMN#eNal6Oz$Q#EFl_5b9+d?*mq0>&0)x1L$+lo)6tG&i<)8^-!$ z>;lt59qU*6e*Rf5Z&)W+KfCV5O2T`zJ(#Jhp_JJ+65RR*e77ps0O>e!`jd0SWX**g z-?flf5njB;$Xv^+Qd1~|7JIltlK4i1$i=eD=9?Q_XZ6xNC!kPqCQaw5rM~JMX$@~B z;He9UVQK=pq;9USD&X6-k?Mf3crHhU+^=Y3tU2(K`tt*B&{B#%5!Zs#zpuIET)2z# zn2BTYdR+oz@ydij?u$HMr?wGl!D^6xaHNRjJaL(kq+b4jcir`Peoqy~(R1}TBh|MR zuaj_7^kWN!$5IwYUoy4G`PZ0nCxEgh3|F%>RX#DL=|x`iXfPu(U$?*~5Nb9W5)`fi|#8da>>|`Zn8TsW-P484cOLlG<@M6wY5FD~%&&4)(mDx+8 z{`Nn_1zDfUs;Gzf6X6%c!&us~wemk?E7~Q789`TF^YaYF$`}Kc2gJSbCKT2uhjN4y z&tE2D^ySUE#CPvypKn82MUrWJp2q*Us~MYa?~3E&7w1}_ONq{>Ah~Tr1Asx!pL#6$BO`?^w<}Sl9>5@=kJpKG% zlO^E%s8e$2oYllG!>S3H3$Dh|Y~h%}&*mK4iDKzlG@TX$jXHmNC zkH{{QCaGTYGU)EopZODB?k7u+yI9!|W$yhoK@=8#zp;VmP3m#)bW#6i_@i1H?aOM{ z?>;N}gku?v*?P897LoUt{s+{`4_OE39jf$vr|LQ-j2kkTJ&osGdmZR%(Iu3%z-#GI zog{wqHu^4I6E(Lm*LHCK1**97)aifa*VnrXR$qauMeDLkSu%276`vOI`#@hIr+mdFPySx?+6vku2c(v%+xL6k&ZC|9 zy*6;vJ!DXXsBidSk;aq7JKdTAF04UNd!{0|^%Dw513Vl5nK1kidU5~g8=^mgA9^lp zp2MHYomC1*v!u|^kVCzHG>oxJr$+SGK6ANBIW}yJZa&_hp$1R-)w(F0->#{Jg%ZM= zuh0R1;F+@=1ZkT0genBbt3+|W+T*wcA3{U8h*0f&;5Kg?9L$=3{Uy%zK!OA~{!UtL zDTNONBI>&ro~|si(ViUsxV~EtI>p#{{$hIT?|!}KkG|sCUj>419AGo#vk@@z&NP}j zHCq>ayR$po1FK}BmerrnOvL}RH-kT~)Q|vR+|9bIRZ}G!9HP5^d8o&=Kj9Z^e9lXx z8mcwnww0d(un~xr3|?EDSW3=<2gmxOrHDM&p&^-air?R!vsRG(*tPCE7ku@QI1$G- zSo3eUG3(QXPl4Z!FbRG9!&9Q4x>8!dpf1ou`V)Egr+_8((5N+8z-@+`SHt({t~&eT zdDfVp5P@9d!P%KV6J}EvZeXOIUVg8Ct-@AQ{-b$4t$ zzbhQM1N6@3!242WCW&b$VgBgGjZ8c}HJddcm1bCQHWb&*Izx)J(@jj#U42IPdM-Z! zqxU00QkCwGGis0H|E)3Obt<0u#}68@D6bV1mscZA&1>xy>D5x{R0X>pwbIW&Q@8%( z!_lLjXcxn&*r0e|UQ@~-k!f`HwoxX#-Wky69-lY7n8~m)DP;dCI72=A(YV`d+Q&?j zkBdF8*-iUhI`4Rj@j>>Vyix$y|jCVon%iN!w)6kg9a?D1_e|Cl-5@7VN@b)PLZTFln=6W>1C*n6po@K^fP zGt%BFyTzhA&hq+oerAg$VG-%Cxz*|x>->-mv?Wp;Vtw?UnaznA?D}^?)NFR4?O}V{ z?al8(w09s20$e2vJ=V4ff*q?$O$Wca_^fx>>Fo$x$g!{Hzgse<7~a0&R8cU*2C-TOpBG$UakGPaYje#9D*O=j+*|RPv?E|O?&Bfww|D}oFfYn z-1QTm;#lF2Tyn=O_nlpeKcZ*u?`e42bE;;k`mDprUN8EMqm8L}aNq6+4oN)i`LjG| zy}j3lka@Eu6n-qj=g!<81Cjqy9X_pvG_#^+K3R}BXNpljhj3(`AEH4s(1o2I8+xL6 z7`25@jFUvtlg1nPMCVMAX&$Zw5l-YvSEHm}E#_rJ6AE{2ICZuq8ftu}MDEBQqFF)y9gg;uQ#kM7Cn zIQx0qk-!9?{hd$QXzkt3*eDM&mK zXb-gUA~Ei}g_B^>QLjJ_7h);%;$n4t$P0t^dQQm+r^>xY2BU~dnt_Ox1_a;B!enLq zaKJPYkv@EzSD%LGg^72zc-t;nVD68fvQvk8|Jc6m$mfLJz0MLsExL5^*?Bj=hnSOt zcC}K!kvPd1m2Z0aXC}@OP-i8v97S5m%CHlm(`VbySy<+IB{l4qj+M2BEt#-O#he?Y z%#74%(i6hsHwBu%_Z~|WeGctLzrS|e8$HEY#BR-?-bWTH=DhzN@!LGwG;G2A3a`6? z^UN_?yQ%JI@0_#umzg^I7}n_CL`cDD8DXt1bC4iYl-;LkB(JxN3eZDhi5u_Zu4Hri zV(Do}>Wqqy&U|V*WO#o>KLouLj&Tl%jPHmju;YUG%?`)!C&W50JJ;$}{X=v$ZCavE zVCT7oUw#B`cMe)=QHIn6O^rlNiDgFXH%rIE;$CbHn|2QQd4KsX5+%}^BJGOAPORLZ z>tu(4JHRlKfOig`m(j}^714N~*8Xz{z5R2+=O1By>MaTU@K%ZAsRzqt+DrU&G`FPU zOAzlKE!^SPw938VKL)~Q-;1Iv*?l975Nzp|mSvBFVTqF0VoLDQjXsgOY}F6<3wvHV zS}|~K`WdFQYxSEt!=mbxx)wEy_oYf7Z=QdB@A~(d&>cj3CGZ;3nG==f=V%)A25|e; zS)=jLg7t4C=P$La*HDsPTc97ztY3z&M~^zZ28&)f(xt8&vy3#O-c_&ZYM9Ek;sc6_ zh$R5)8qIKcUAE$9)s&!Q1Ur?MK(l&c=R&Gq%0p0NMC_~tDDDLnfu)W)Gzk>J_xs;^ zTtOV5qJn6?rmzJ9q|I3=SnnfDZnwYbzi82xjfHX6D|zdp<#`nK&Nw7B%|72Y6Ht{! zS5`t)5L53;obe+HuLRo2-kS*!nU6z@f~A{^F3!jQ;o?;D0a;meGXW6!?B*JVffDje z%&5ErAxeB{Fl5GuiI2enM5_hey=b3CUk;jUbVD?-IX;mk0j{0;HcEdFh}AJr7f)@SmptF- z08%g?i%j28MaZ_t2aECX0?TCq=)U)6eCW7qKD&eA^CP6=Lf)-(VhKB@quu`5eE?9# zFvLEpEJ|rw>3aV8&0?ptKFzm9i}?k7ob0x2OqhY;KGE@ zm*K;Gs_Y`V52S+$L@Qdtc*x)R6_;~SqWusRpa=(u0ye_rCJS1JlG&o(H6_Cdn!=QP zS`$XsZNee6*4%|b(k^@jpI(9Jot4BMDeOVkba?fLY+k4rkSx=Y5Zos--)A-fS(YpU zx3aIzM~M?8Q<9VmQ~U7A;#Gc#4^+eGW0F1lAxkp5MHwZgv%5glmscGVN}5uV4fn&E zJ7qyh8;E7>sbAzt*LJKKZ%mKWN@h;ZS)}q^pjNUyYnYK3LCw+oqTUf{O)o0(d~Qg| z`IKA#4Gx!#3q_sc6M-E-+p?2A)mont6_#xCvcIsX6lHmtR)j#hcp0(cp?+5M*>e8%FXs9C^z_p( zVhjwHcWN4RWozAV9q0nbSY{bG_Xp1=LaaL|JFKcKCV8A6duR2soe+tl2Anrel>e9% z6HQ7D#O}dh$K1^4Bo4oZp*mpw49}WCi25~@ilKg7QPnrnWj8=3o3i^LgOCv}FP6CA z_nwc4mJnjCL?Zta`P@;m-#oY{i@P$fXyBHlwD)LW3(A($-`b-&lE45bhOj-oQMYdcdc^(9x}rx>cH<3qVQ1=FP4I+;TFJZttZSo*6Axm9{#$)>YYeMRbv#A> zlkrdoaxYOBw&>KNZP9oi0C(^QodSu&hT6PNdUPWr46_N!SYj9J9W7bF8ksgVRQbV8 zljawTz5$!JPnY~fIgT~iju7XLGAd9>a};s-P*^#z<^v|u$(~T(If$*}s_CAc*ERB# zMYfVLz&5DK&il_|OpoosTX$(XuHs^rYzK=NOI3QFeS}@#B>#~3B%_G{+7P2SQR%X9 zJu)AtfiY53q#4$#V;MUm)}=nsuNu*Bml;;nF!9*#5KShgMB(@iT0UY@F{0PeX?RoJ z)|VE3T1bu~c--#m9Gset@Ns1Eno{rF^~)-GMy_<% zZ|^P7uUe*Cne@8pU`Og$#oH8#@OaoU{7kd@$P~O6otBpJOCtI?c(QkzWYz&vTNVk(Xa|)9&hD;#Xp`oQQM%@xO(^A=*?f>-&gv zS@iMWG(%6@oMM;Iba8|XPf1%6MruI}32PN+rxs|m7bu21qjZ>0*X%vJ{S1u~><^+B3w7wXMYEaE49Yd|>gC>HhH0w>*Z%J9@)S?kMeCH(bfC z5ZTjVs+FP&rdk|uWWNdFFBZKR!mPA?OD=LOn?KG9MXr$M@JA%iaAl zl6jD0a<;yGpYXcXI)4n(SfVzXX&)wsDBx8|a6EoFzbqTi?UN9>_I%Sk%h=Id7? zHjr-DH$iip)~whhgO`K{`le>Z`sz}A76%?9YMS)32@#+?rN(X)}9aWG&n#h~jeS~P57U6v~L6}?Yz#WBrhpX-pxa%xL z7p1GPkz$w0%?O0|2OC`=d!2X88L9lQZWWQvaXEY?{xXIW-hUZrk>VKZJ$@wW0Te#D z7Qy%`;Y(o9%m>MRIa_9-fL8?@i!%#}lIHA{|ICbywie4#~z4+2-GkOT>;>{MV*A*>D#XMJ^^Hg%|uX0NU>7Xb9~ z+-`W5f+k!kVUekRbRDj2xj~IclqTIm%!8ekGLo{&T>C^E**zq7TH5z62*2j(_}8QS z!GV>~!ibz%vjA)e)X>fp&8FQfcT?bWg?~sy*JhYEQOi40>BT8yHM~&bbXO&1dzgr< z+6Kl=ZlO4%JEA?b@BT*Og-DsDDQH1(hEmsV`(ceumoFhW>-)?$j>9pN5Xg2McFq`? zzV-prU}uC(aK|(s7L4i)gC5=Vl-)5tAn;6JmIuBxThQFhPEC)cKV?CGz(T!L@_xP< zdh>zIEWx-l{zzPCz|ZfZ%-xcFmk%mKoqV$?g2qwr7E)3kJ-JZ&u56@_&rXe!;P1bz6E^hFqYo=a`a4$}`8T5UW*N3CNGs#L> z|DkS;%%sy^Daw@V^qGBl8-$Nm%_;0Z<-%-7U%<_9jHG)l4-g$utXktrr`fQOX?=EN z_ccl!P7^g@q=t2+*{07YVS+uska2W!8t=Cc%Sb9=VegF+S!nK=8VZe`g~N|~ZJ|h- z08g6-C!xSV#Q1H6mHmx|~Ai{EowHJ_Enm)-w3-P4@~tlH1yNKOUeh)5t6;qPU;5{&{POs6V| zd^?YRSuUy^u}wH5U!MMooIG*ONLdSbilXhe)V50-BT!m><=h-2Uu|tQfPMJ5x^34u zR7Lg*E{>)<(Z6%#VBY#YBH@&E%7$@kg-JjefsrS7n2;|JOY3N$Idw+Xzumw?sef+L zAx!Ac6mkEk)+x!}fPC@}* zJ}`{rrdD|Yf;drNpiD>}e>ulKkIdY5-GuCjJEYg{Xw*ElEpg5w&AYa*nH{J@Nc$j< zHpG6r{Zq>uro3mr4y>QuZkmVhDcul?ojbLP^*z${F(_8&Jc>i1#SA{AEj+UP+MWWY zcWt%lCf`{GEXW~kY4x>|Okkn-V2_sxv-8_bqhxrT<72{10O;sQvA_|7+Siwm!-Zi! z8Kmp@SQOq+JKzt_G5 z0CC?5DDht|-bh983Cm$SlfmYi9neMbF?E^xu zni8SJ&F$3rnm8j?wQ6#&+haa~he8IWXtD!r$j7ZhNgDj;dp+a22ZUv0u}uzu)H)Fo zgLBf@#xVL-jnY^XfPpd|r})ee&P9bJ-PX9%6ZCRFT$YNllZ7XVNY9j{MF@{m=60qf ze3d$U?=}9=mIQMllB{H&QbW&li`Qud~Of=7@Cl^rFl$h|WJ$mk9KtfT{9JY=j zijN>qkvH)XJ+=Nx;QhRrkl##~y@vg|$iox9p`7(XWc?ymUi>4IAqhz&0h#-fEG^7C zQ)%uKJBX$3rxlDv>YX?#-q}w3ZP{k0Fq#n1?y?9{z}TjS9aW^zjhu17IF*A>2PCGX=H$mw`EJ;|wn^8XxCB;#57t$QdtrrzQnpP;+H=I#M`B06X~15=`hfLA$HM zpRFSeV-%KpqilLU1aLpIi?U>*Ar*0H!@9K+K$mzgcLpI}X+e?=+jw|xGn^3$YR*Eo z{K8Et+rrrF6^+@sP7%dodK932Qjt$*l0+!6NRA&;q!|8^k;PL5+2X8+wPr_Jz#a~b zz%M3vnS1=yBB0b^3E`#>so%%tt_9FiXjX)4Bq0t@q-rXUQ7JXA*JTEu6EsM_sR;g1 zli@A_s{liZ0DX0CsRE_~!b>GBP0kwrvgw+a{)@_KZ)-_xH2exGtu=>T$Cwq?{wLMU zO==e&ikG4!R6=x6!w2My?b5_dev#=Cy7_DHBRiz&EA=^_U?hPJA1DN9!Q#cX+1l&_ z$R6%8-MbRpydfh#Dj1az8^YY_lFGnUGuM<-i}LYTCuosM990RD)%3Fpc%Sxab797t zGV+)@lPOu(Ji*3IP=*2N@OgTqa0&{M;l-#%SxC^3D1W=+rrtOUP_RlV)Cl2cprlDX zyXdpv?7bzW8vTS4R5;-@RJP?fLh%GSk&j!}tkh#UXaKTylEdL=Bi&y`@_`IVh4#uQ zMS!3oVjoZ>8B9iXX8TOz$s-0iL=hCOBKx!KBPCFyYh1BTrky`)=YB@B0(cI_Xk^(& zDE>A^s~=@VDB`r;YlxDBlL~zK(KsA%E5i|PVF|xqQywdEg%JJr1a$X;9UBltBId*H zD7|L55j7bWqfZxG4=ZtYe8>R8gJxL!e$G4@qe_9Vh~$i)DXDreZBU$5_B*$HpU1BB z)9b6KJiM^LU?rI00a zJgzBmh7%i1gsT^+Wj159bOoUbL*NO|7Jxe|Tyw;d{5)^kQ5G>~VucBxWqZb8T;+b% z6-;|b$r<7pu@t=u&7m4|!;)x&Dhg_#%tlg@6w(8vnb4f*_Q0|U<7!hIy7|J>&4lL@ z^acx--y6lnOaaJj69192S$bFU@vdC3x!xmHB6vPRB`E1S28o$1evf z4XT+>)I}8Kd~&Y{b$$mKc0>Xd;V_0DyGSfo5ZIXIsUOS3g%q1WiLHc;vYZxDb4G05(fNiu3IQZ*nE^eU9-usN$& zNe>aa{i_Ihl2mNMYRoj~W;MfNVSRv*RYtx+IY7AURimY(-Dcq%WXAvsx*q1lef6=E z`dG4VKu3&iou^K$kZ>)dS~G_f4!%(Sq^1Qt=Y$Z)Z$2^n;Dr0neJ4>u9RJ3m=#)w& za~q$KNOdS&oRY6TKYzSoEob2}6t8cl!vZ)lia+mY>UfWS3#B$s&~5=5Jyir@l8Cy>XOitq3=(n_cToBY2{^x)zP#K`%_8-dCQDpQhl>Wr9&FtqWnNIj z55nmtox<-Ims`r(L>EtS9&tXTvFy*|2 zfLjI#p?qZEi6{j#c*UiG(1epZ+ zrQ)MryBv5)-=M_drI$0sz$S#=r!I=Y_T>-9X-_~m!bvv7%upzXh=A%b#+Z=wxy1#3 z32S4|x$mF+cn>r4uhyQpdc=17(ObK71bXMNh79=dm`xq+($qCags)X~_z-|86NXRC zB5cJ4m(+|>6|htAU)NLOSh84!odx}nc{A__6jT9ON{EM*Bt3DqKISz0(xcCM%YsxS zKNPYos9iot*r6hFo)J!9WTFbfyFxm$y2}JYrI>2~$iehpdL?0YgQwquLHZW)F zdEz@gi7uoleoSsw5vzY|ZOO=vntZnYlC#z&7mTj~S&^H{!8N(?-2`o1tjXtgS{L0F z6-wVYqwiZQJxGHcN0}W8@Nw{{JL^r-B=jJfH|_Rw?TKfqbATz-fID-vb#Wq<&TW9F zs(hZQBkjesemtD*4NV+*8aq7At!xU~Q_fg1SCbR_Y;PCj=*2wH46m~&+GEdROA^vw zK{GX%kl(ZyfjDfg#-0UEhtl>B<73711nTM<);v4J_u-ELuGnbRV~#M@$01_PxQgUP zI>rFmwaKeila|(84ARab1W3lVm6H7pPtT_4{9IG@SVPi9{TxEfw}Hl@6-%i>$gWgMSvSKZUaUPf(vS1V)~J>4uwi6^w>x9f9P&y{^#V4m!_97Q+G0k2cq&e2Ta~r8 zqAROCR0uOsDjIa4ltE+h%f`i#fOYnq@@ggK{sfOpG{Xf+iK7aJGYY-RTCngDUKhZo zM>86_I!pDQNQNgJh8DX_VE61^jHA)ykh1z95cuvt*s&;s;%3<(d-f2 z^=iVL7GBA7?Bsd9{>F|+g`{|0!nPz_X&McWKrviO^*Q>mG~%^A9F&Ro7n^oVnHmUm zV;1!pO4e8YHJrGI!36qQ-gT!r2j9`kSNwhi)8mxj%CT@w)aX?8+|#wMzydTsA5H&W?A3{fAEB z4J4OtHe_a!B)7womB}sMB<55|bp~@se4#Em{QgU4(>yq4IKkTg=~U6dQ!7T}f^Lj= zW_}za=uO-D`c9O&WZWc1{7e??GOw)iR*U&g(NSI8jvHO_3z6+u;mSBuBum2+Q|?~h zzM4>xSTW!Q(OKl|sz@Y1alg=B^D3@YTZt8ZE$zw+mxXz=ZOg6I5ls2R+d2F5k$iv;)t(tQqrHJ?(|{9B02O& z-LPMSF!h~*Ym@p01@Cn8W07X@?sx`I{!74irH!U!P3^JtA{ocH%7_P?wew7KfJr04 zB*Fk&e`?0*tn6Uj_R)I7nQ5%@K%6dGqmzan%dT2=v(Z_jQw@;e`0mlz?Kj8YOWenf z+mbIz;$FpTj6~zNC?0^W2T<+3Sb5T0kHi$*u}F@Vc%?)5Nj)h?R?1?YD4eF$)};xBBjOKG27u zo>g}BvB-guER?WuyBqgKM!v+H31fr1_xKK}m8o%$2aUIY0Q8na|8^QLLcF|2Yh!bM z?_grH;?sqIXWb=g-;Oip1RQIi2%Yko>o!*U+#`P5gVZ(4t{Dd<4K^NS6J6u?ukc#k zYeHx77@4-Ps5~a-#(cc<;i~oN*r(UhkjBG&H9P)3fI+^2c1XFlG{@yZbz7X z&jUwIR@*1Zl$q<-WtXR-H@XOHAF?_xQN6WZb|JV+QtTcXQODqf$DedN*rCE}`yS{^ zGUxOI3wMKK4bX0(Ze={vpb7ZwpeiKzJ^RLE8HyX20bW$jZh}}bUr>BCOcjmG*oYcS zZJHp1htO>8z5=4NMDCOXb5biyCzofE1BG&;@x;wVMAJNriKGxC1;mfzUYdL@w9k6m zWzW~4Nl-Hgmx)YyOYF`W&EnHB6wio8oS0%XbyV#uNP}o30f0|}96~s*r?h8-RjEy1 zaBG`(n8x#C=pWCij0YC!6ocKBGTMyxQoG~pq3`VLvl`Pc>o#A*r6=X+SGVht`moI^ zS54%Z5N0HyFmVEQv=}uR7pv(izF!;ui;wQ`O4ewIdatKQ_ozh z($|yPuW2!S5%G%jIe{)A23LFySM|YUuY5?qRc`orfgIMw^z8zbATJ{4de0;$u@H7`M7N5OV{R4*u@T9p2anLIp?K!zELhklVupdm=807iCG;sN5tkCr zTNZ&d7M!84&|vK4NeDN0&~HXVSE1#x#FKvjLn6Q#-xYSVtaO?dDZRB8eZV9_6rVgY zgQ6LbSX3C$*BQr86B+Nqlb=}YjA6DnE5 zJ5yrD;|k%1>53;}%>sL95TUB<$c$P@yV;9HQ{C+oD%rOxVUm&hp3q;T6XsjE2`F@5 z7Am~)fv7oK7!eU_XDScx+)+bI@`yh|Oqy?XCdIaVwGx2DM%)`f_u;Ci6^-~jO&=+a zsazdGMl)S*vaST!0C?2V%Ki1%-4If9{E9G>ph1pImEc45whgV)Keq~a2LIaMKK{3s%KI=-fiQh+zl;aCT0!!u!$4sLRmN(p^Cs>hQ$7oBe z39xTQ#{>B3XY1Lq7R@LvkH38i!`3+|y~hOzWURmlt(H*yf_s3enjkFjucR4mz#BZz z+fH2&Z*n3ScZ0A|@$Djy;)7<+{fQDCn_`O-Bv%zGx$(#^BdUOJm-L{9`MVb2xRPrW zDY{#OCm^~aKvxm=FCiI3e4HA0j-939ZL*+WtR~3u3szLPCT!NLf%ABM2**`x1kFRZ zyoX@zn?L}&s+Xpje-C&QN{?~W@D`g9`pUO5NaKRsZEi=zqBX5ib-v<0LKLE;^{5=k zw=#)dGXZ45H@6wNb&|t^0v@>c4Z|QIG^psf{ z`z;`CbnX?+C!8c1u}dJE7y4#^Q914_#87#eKHgkmf9QQQ(9>)gMp7BhT8R=PEWoEhHbCpKYp7MqJi=D0Md<%TEM zmrY$*sk+!}JXcH8^cc7NV!=?X(F=U=B>5*GU7ArDKl@;9z)VNnECotnHGo--&I@Vi znhi&v_?2e4w8dzCjOhL`LuE?GpgTJEv@eX81h;Qkz6u_le0ItNfHzWR5Lp+;LN$x7 zAgssYj=b6Fu2Hk0WS^r--Abr%{@euGN&k&w{=H?F;h(&)${yQoe=48#z+}|z5xeh8 z>a2{^s%g7bTR*H6S@B^jJ+#qC_+zm5-`eB32Ja39w)djO}k^9Ka^He!fd|ERvz5hG3)<;FHz#$O6#31#pR7x zzuYk2v>!3HuiLp%OaJ((ny2J%-4-{Zt`h!eZKNM0|BQ#cR|;p6o3K8V$=FkZYjO;2WlnzSelP@RAFQ7CRnH6UO|v;BDnG)ZA>(`sU16N8 z$l?NsZ6q?a$P@e{o!T#CNxJ()uxnCIXlM1jgsXoqcH_Bpao|;#QSc+QcXgV#t~t$L ze0ZwPm(RU6c~L~EFM2}jx}a`eZQ*CxQ);}`uRMZok)9pvFN^g+2;6HPf2@+lxt;rE z&tG;<%M$&=iPAl;XK|+?j@b0pZhk#t_Rql_@0h*k+Rm5fc5Ta!P6EvMGavcHSA8{+ zGF;LSj(DTs!sKxcYV>lvJ8sYXz6+(hD(8KV@{gVxzW%TF+WPN)M*2l3ss5=4goobC z_gUnof~(+}Rid8VqjQ&PHe zC{yErA6t1#&PenV-98QfKo(z}0^Xv3lNk(its!V7EtL55C{O#bMy0 z_tWMJG;pM+D)6R;Tk-u2fq#BsH2w4OPXCu4a+L{PiiVvD9P|iE@2@BDgY<+A;(}WJ zsGvu!-A^NKYh{wFeS)6V_xnc%u`~Uw3c-o`lBfLJKOP5-)(38?xjvd4JQjV`-^_F4 zXrcd;%VU>d%DZ!|$&aB3HZHK(sU7sIhS&SS_!`Wx|2^N;q3aWl!(9|5UN^x_FIJ7%%&y2S>9fjQAC&Zfl%&w_Z*@(hoZ z8qGa!@NqG7?!dHXY#o9lD@bLsBUt++!~A25k+I!8IvJS$#uM;h3J5r$4CLcyL4Z63 zrYPL;8fC7G%$f23PpysXEZ{k;8SXn%cYQbCyTB-*rT#{_b?EWv8!i7YwYDnY>`I?Z z(zCf$_q&rpISP zFE3wG*mm;idFiX0hZdec_3UE$w|CEOoj?8JDvG0-1hxxDb~$T5otzMzd39Z3zOdS3 zzi;BrN{6-2`!A%vx}%(%*;ceaZG5mX^u+Sp9p~OZYL35E9dxnnx!sL*p_#Y*GT)i- z@=?t6UBZ#rv_-y@XK5*S0@UT~H6*J&4}umGb-X<)t#){hO33!{)^^rxx4f@(yYr1- zyDzC@=nq6j1aUv(jKYa|2pyF z#~ojce@QabE0;FyNepp+!#ZAJPUi(A{d#|Sk>D-v!s~)> z{f?h=*Jue62Hu$!`pf2PJ)hMsVbQK#_tvswF&s1IT0cuW>BV}wehVTa zc4Kpv>9@eTP3Q0Ch&vyfM*Qk}yz08tS*2BtZ6+*yy7ZKmX&~B`yYlYT|GLtg`eS63 zzXIq)Dyq(cAFvOP|&0f4Nf@>>OcND|K%7oDZn) z^d>=>bb4W0-ZeATsuz5NAac3%<(s;itlALTm;aBcbN^@Z|Nr>Tn_-&_bKK^9ijniF zZB8R;goJ8Nky8^%Qtf1OtSLlEb4V)nMv_XkIgX?$mAq9`LZup=bb9-^-M+tk|A6au zyRO^qbv>`^dR>pl{XPq9==L7T_3-&iMWm)}c#!++V7k=uHpZ9GM1l|_fot@AUrTEu zGQ4GGP2s`TH*YW55!0*sK!QevDPSSfhFS+iy}YLcr1xx`)m|12yFYBJOsmioCZ=RWw-=NS3q(mt70V2R6sqtn5%?O}!p@0Naxm+b+u zP4)0(64)Z(-6dH_tlMJIair;59((j5CFIMP&V-$+{?|Z+>k7pEQzrVT)LTt?%G+yU z{P>aS#+sX=Uie^}%7LH~yi^!A!(<>1%yO&WnR5?hx7+1Nz42WD?zE{SDC;FykCDq( zo#%3`(>lTfS-Lu(Tvr1kDnY6nFnx}+?IR8zGceaeZwo}8YNS3~IW|{dy6GL?EG$lR z#W2x?s^HpsZ7OZq`Xc9+(;&Hf+{G*zY&V^GJfE>al^6*k{h^?bVy@wr>0nf~LMeN+ zv3jWIi3dJlqCw|d3vKhI(Bo4*ra_rW$>y`@ed1#F>Ah+hVrtJw`JIKoS;fy-FWp}f zRSq7WQ4TYV$lpia6b-BA4=#=Ae*N*3xWa;yNj&qHG8laktQWih)LSSoi&Qikv@?es zCNNyPLe9FxNwCYO`r=o>hi)BV5{+pIDr$O$9z&NX1(hGq!}cmYuNIih?ndM-dTY2A z7I3%S6I zA-O0?Hv|e&&K?ID8JrRJqC4N*MgIZe)4$5#J&H-YwbP2cEM9kng5JSu#6RzbZgd+5 zn=FK*<3;_RkAN`C_20yZ;5Lrds2nzr2oHz^TFqXsHT)`vn?>?2mzKaRL~K~B@YI5- zwaWMoY@Tk_DZv3i{m0hIs^vo;uIJ13U9!D(_j?{gHt#76!!;QEX7nmQB_9u{h{a3S zaB||1!nF>(Ql>|RmoNmlcOVpDNoSk?Dp%5>|DdAEW0b}<6!@IAd~2h?+h->v5Xe&wjC_7COgKdr4~3Ht7h4ddEs%E@v-!=djy zgZy?6Gji`cr;v6^`SH*;GHOSn zWmxolQfqgeT=hMTUwZC5C!nnn|4Zh-ac%~JT?87OM0k6a0tIv%I+QVh* zlX0?ejkElMC1|R0pDYj6t0f*mnyd_|Ebn7uQvm1njM-2-|FwY*-OZuHDc3Zm40e`FqCv(^UxFo0fE+D>n; zJ+tGrht6G_T~3s(HQ7l|PKV?QCgjzh2cO10EPH%h(TpPl;gqdt|i3-J9YK z-uK)fPPOf|UAc{c7y52&DKEB5Fie9pwRT^$;T9a7i5*n?_{1q>$+0_Ln;~-<@IR=0{8(PePHdU7Fp?9 zcp}0-#geJ~m!XvV9por;G$TYOL;w$w(EVbFhAir_0!k#oCdjHOWb&y9Jtr2_!Ys*2 z9-T(Yk~fl)g$LfY={>Jk9vf@m0Q`JCmeU>1nP2$(!{(gnQ07xu4|N zeEXr?z7>4wP4)mC&0fo4Fba13*dLpu-1Z%|EL5IvM)wOr6b0zDIOh(Z-7{Hoccny( z2zYrSIw8@g3dRW^@bnUyJ7acS?d7~>#W2o$##EAanV8w|6I1-qCB9Hsx}LR<>Nf&> zqZm>^O-PqQPLlS1>w>>0EB)H7vRRs7sX%^XDtXdjqjEIN?2v-8-;#m&+ZGv2hsDbw zE||=pBvm#Oe2avxP(XD2(all_!&_w+#%eze;vj_FzHT!1ES!{-%;_d6aSQKfW+}gk z<~Bp9m?M$QL&2}X0c7yu8;E~avx{D6_{IAm!$iNo%}|Nm2adoT zl@gGj2hrh7&>?z4lR)L4lc*F0utJP_O+faG(f^SXzfs~{Sav_fYut3y{FVMR;34`I5N!#4zX~=5`SF(YNJWajJ zH~*k#wk-VlR%Ap1!(pX<&E`14j0`@w%~`nr3oC)h1Y63XpbO~xBvqDU!UO@42tako zt4qECE$OF!u~1%?NH@B`o7GsxLH#12y9iV~Mp;2tnXgdYCjzHAD*a19$%W|GOr^UC zwLU^f9&^jpP}ETtRDO*#?+Wk5_;6O54GUqmLLa$z#Gy;cGf7tb+4Z3nzHb%zAI%(V zRuPaZ78iJ0ib$`38FIB zt{>g$qWYQ+tun0dK7IMtt;?D%>>xt<^+bIa8N`%vj7yC-V}fkxu3HFOoP-b?MU|(b z)roGUa>*>Y0_+GdP$!rUg>VjL0X(H(cNT~UFi6?}bXS1(Wq~{~AR=?)fVRmiHG`xq z&xhS+Mb2R;97vAPi&!ljPXO zPP?gRAp|*MOl%eP4nhYCro*8M+)(fj!XC0FH#NQN=C()gAqTXudK#obxZ1e8=vAhZ zHSDVE2lu;QNc+B6xhX*IWapb5P8uTc;l}13o$I0Q*TYJ}D6hdw6456%xa*1Wfp1oj zW&fAN>lBk1B(p}+57ZDRv#Ppr0VIm(9?ew_U|J= z;_wcB)n8(JZtC7!cE9(vgmGJW6QZMBUZt;OZ<+MLq+ekU{9_U@*mg~eVuk^^%TGKj zp$`{>!U$D&+N6_mv^yn?lzUP~0puUcO;*HC(c=2RP z5hl)C2JGv+ZHNi=)DP-)>*@2D?hC36GtEjgKUp@-x#G9A&l04M{vq+2?hpRcPq*G+ zs@JQ~9f-&uh^!qrYd;V*J+Sl707Gw3-EuG?W^i}@VDRWba?fDe^k9Z{eafG~eI7&E zF+c|Ai)nnU@2hFE%Qa+(2djFg`*EmW~QvRW%Got7T^BQ@)g3O$A^Vup`d z4_DO=pY9pXEgi1?Gc3{@iCr9Sh#6_lAMpqpY3UhhogTSN}giw?cf8jwwr?oGR_{`K~wE^#)r#ruyu9uz@FT}!Tg=?BkS zwuL@3qAHvg+ntal7P~vHXcDgWen}jwHAL@o8n-q~%Gy3;*EtdM@cT4vwGGlPR=Lkm zVK9g{{gv(mR0kQMvuqoKb^WAA`kPqDP7%zP35LZ%Z;2mv%9zz}ZHQdiw4u2n*gi!E zZ+#1P1AyvhD0#|>-aX?=FXC*9ie87}tsd+!Sv|QPk#4^MuZ&dHOA#!}ja- ziMB6x1eGxbhZ9rpOh^{>f43q+6`+ltHq(X^AD4kNF-lH{#r59utsVF88S`O*Zbm}a z>Ekv1RT%U=X;+?f?)dlIYoN0n5bglW>fUc{VV^3~qx>GM>Fyjh-IVH*i_d`+doI*wX-CY%nv^Df=*q_y&Y1 zV6jH<507^_i?6B^oWZ~SJruxqGo36dWTE_;^a@ym4!OU>8X67Ed))b;Tz1gNId6kO zprT$~Xn@U_Q3t4e!nA2D>#baQ1>ZU2B6Lo=W-##al^GL$R1N{$k*kiqTv+$={@jg$ zkT$r;SuREeGwfU#U^9TqHxWXV@UP#IS*tJTAAf=8C;xT$4Fb!My`BbC1?Zp&@-m%z ziKVhvDBt{gdegIMSJ{O}p2VFj)ejg%besfw88jh8g^Al-K|$y|@ca9!HZL@J|0Sd4 zn0E&h?uRlE6(rbYdEJ4vg(HIUN&XGw*#R~u z;l24c*lnirC9>)w9r|qf%{CTFE`rgyv%l_sKDUf^8&vrwpWUjJ`GquXvmMOdyw&Mj z&^$@-r|b%D5p;j1U8PYn>-geHdhwpe#<8<1A6Vl9OTa?})DQXQKO!Up1KL4G{cN-Q zP1yUGMTuI)C@;!UqZO!l0<?{Y)J5h~%QpDur=bmd z^tj}$l+{Y?fg8`Ofm{-Lu%hHOV4Nxg0~gRTQqVF}Ifw~f)xP*kgbY)Fg2XfH@1cJa zP*n=|>rYiC8ou7dKcytKY8H7P>D%Ib$eJNe|SKB+v%U`7g9>~T|-&8b%{x%7Y@st|A;f=hEHFL z%1Ij~S?s*y8QlDCYxoRTYxiHqM{U>5MM%Qva>&$|R=m5TdwxqbCiUfcTie_fX?5gM z++dmhM!w$m<%E&S^}7aZw}06^cGmMKOh4*t@}u*c&-tB=`j+za%KFtW5^vvYc`2*b zJEvFbpU5$RIXuPl(c<>Jy(+`Ezxs?|473lghz;kaE~Y-Yd}`kp9wM%knxF8EdS#~w z<6ZluY4Fp1RH>y`j9tYD{np__mbnsG>u6{8PM_fq#t`DZ+&AihX*bRE3r7F7>FU$@ z!vK-x^>0gbGBBJ%^+7Vc&OIB^z*oPVGEM?J&9?wsniIV|e(s#OvYPb`>~AY)^Wj(xnx~ z;M<;LzM)k}=;qY5sycgjcivOdY+V>f^co7Y&9f;omPIy2rH-$>E}FMrMtLMH5&HKR zGt;DcamKIAwix3m@LN#wD^{;2n4a5MvB0hKd_O7Bv`TBGDGl^+#h9Gt`i4TQs+#%Z zhQ)ve&BHH+)M7l3E>X^#Z1>d&%vqY()C;Erar?354FbCo&fu!PV^u0@Az<#EKmNdv zSdbmwKC;1pIh`BZ==H;{xe%G>Fb5z+y%T7MhpC-k@B8RUl9pws27Iey&42HXwG1^m zW)5g2^<)m=1s?l`%`W`8cC;`T$cbBQ-|T-qS~9;*zXJ0bdan5T+sQX!N>rpFW-mtB zI>~Hp?8lkL!L?6Q3e{|zYlbifZ`MEC8KyYPKp$@F z^j6Dhc^Up^5cgU@vDb^h#w8`Eaxz~zapQ>v^ z0*czKYEdXw8Wq8DaH9Q*npR!s)lkkzPKCrv-dI5y3D-94=0z^_p+a+eC9QH)3ZwxO zkMq{9z*sleyxtSi2aGH3?Y*3S;YhAjU>C{+>9rOjgK!)Sfd#o%Sr2=tr0f;g0QOEZ zDzc>ccx2SWT*Yf!qjZVRx+jKaZQ!k{S!mK6(2-c!US?acCn(Wv>*=Qx&2|N4XfJw6 zZVO0-;j$~kwA|d0J_C1jIja#WXg$<+3|Y`mOKURWTlW)D;bq*#(Noo*BOhtMiRGyG zPc;Nzm1``~Avl`_uzB_yT!C1kJ2`{)ofhPMdylq!KVF2UbLfFgjyYkOj-Ld;OE7$r zCYyt=wLScy5w{vL2(Z%%U54&lfVqESLj4zw^n)k?o&@y_TY{2=sH|-)nAcy@~a(frXaxY z-FfVA3BxfYw5ffOMnaF}Dqn8}>c8&hXo?AfjdTih8)?tlXFlxkL?hCB<-U>+aoUz1 zdcqmXWI;U`AgxI;JR&F!SYabRyKdwHuL4A185%5$o_pGaa1DBr6HMG| zoYERMXJj^^W7Oexk22AlzVgj(O=`Qn%H`Pp;OacY2t2Io)q+)f^_4pZ@H-WEHKHE$ zwci9{$5K?nx*PFJ)f`L)3FbkF!*fR0tM&GF8Pc;2nn_rUal`Q>40ua0ii8o!nWMDiwUx7`D*Z`Ti^xJ=FPVd;0=r+PqBoz`oV2R^OFp| zK}accW>n+a&B6A~vCzPTHz+f?M7N`UZ(Mf%HWvofWOoSC0T=toz5D9mwJ=)Xff(i6 zI+3W{2dFr6DjXbo%3u(H3b&5cz5(b9z<#OCPWRT6cMHnVfGgq#=QIDRJ@I-$`3O(# zc;PDQza`vSrxi1!IhuQrUw4vghk5g|zuM1#Dc-rv=g?Fj}8m4%39XfvafoN z*CO2JBb22b7mU0OQgo#*Vj%AP=0Ls(3Y38boc7WJtU1ibD3?Hd6yBw(J}YJ>3|;ekw3k*|9}L(zHCw=GS+JWux|4V$j{IXIE%fz(DJL9n& z^@`s4!X|yv@ss9FDoe+*tmqmxp!9gVXz?AFPJj{hfuiUOX+I;EW2u$tBS8j zkuHv6z`)SbE;eA`JIrl9yYy|sYK3F9h8P1iw|oXy`Ua~9!b*Khs|TVcBs*pYc77cg zxv5u{Y;h{DFSO{6exY;h^w|XK!DsI}vL6qe#8CAqz1I16!e%Y|3kPGZ2VK%IFD1bC zz<;cmoUfr=JWVU5tpJ&8Nf|AS4(m^7^+^izFCjRkzIO9JD@I8R_ zGw62rrh#9{j~(gsT#fUF1F;xjysz_o-9ODBi(@>L3Z3csW}*Jt;g zl$F=%ojk`YJp@s04pTjtud<~-pmcgT21`rpFKlX+HcUv5WAC>c-|y%t6+%dtMhDZr z`n9OjcJS)wnIj{(I`v8M?Ve4z(7+Joes8P+ZYp3LL!(3dhYRnEPLxU@z60_me*Haa zSidBw(R$7=~hNJ+0|{*vyhvMdK#a24@(eZmTgu^e+7Cbi% zoOJ+U-#payeVAsb|Jx&2IsJig_#>0I1J8^rCoBBm&-MSE)zlB!^rER`k5J$VsOw*F z%fY~b^#epQFuaiO(FSGGA*ey zXNEntcoeOs#XZFuKKgPQ-!fu)E{t7#{@S3l2Mqv4OZns$_6rGHPtG=wgK_`@%!=%< z${2p~;ixw-6d~}yL78%HZ!XYA3_XN^h0+8$axmHfq~;+@L*hQVkG(i~$G|ffFW5M(U0RwGhTpG ztG9a5d3P5k`rEF$)8MIX0;c&TWAea&6o~ES#>@Dd7194H*g6DmGD)zxo0lLKBoq=% zc~{(Bcsa>}O>*$}pECU4kVi_=c@hU-Sd+GI2UZ?k^mE+NL+RsI&YRL5_gLap{K|um zg(1o#LMw0f8la+KWL)VlFf>w-O(@tTgC&SKe`mSO7QuoljOpTUB!>3?1z}xy`*8(n zGo1Kjc&H-USIFHt#kQisQgM;HQ??4OzxqA!>I`S{u`u#G5`4f3kD) z@bNpn!oFlB$EVl%`ADuD)^LBqR$i$AVThHkIPZKra_QqY!K| zW=d?EX3F?3S3w&hz_1)t%Ug~f%;+U|$}sZ9lm}hsNc6J}Agm0QO5+yUaahAUjOejC z5mMLY2^!z$nIp)eFd^`X(d?bmc-8pin={O0K{GvWE}xC3@$+a_Cf^eVJNVf`9F`7Q zcHG4z38;mlmd!Kl%`kg_fU&~)_25-P#9X3H<6o1qlK|5^4D=2e>9_#QGveb%REwP3V{M6Rv76Yg!WQ~;Mud!_swWhCXT<{%(g#oF~x4J>_gNr#KcQOPF889n0!id1_JsTXN zTrCfRMWbGMw+WVCJUjROtqE-7D!Xi#jrs1L#JL&MPp_Lbk_=Rnn|b+C_(2>j8dGpQ zu;YjncI`tK(uM7~wsBbvqYz+6M9`Q}_S@ZpNDTa_2s-3a)EdbRpb66FoDEk1Rtwc> z0EeRhI96DtW@V?QZl_%l=hXtT=?0S&HF##h{z;x`IX8hkO(icbW9laD1rC#JyGTKf zsDMG3a>xZPT^HoIKzx(o=|V78JXRMBOK$-g*>OA~1*IXcL)ru$E1ZvS8+3fxK{R;F zMNnK}=D_#o>MradlPE7a(^phI)p^y*eCglBQr(rXEKFLw^hwJgeM^w@XX`giIcQ`q z1)m&a-^RB*1ztV!Z@Bw^AR_|wkYc)XOqP36^+wgTPbZe0Nik96kcpBRynrh`bj5FJW0^JAYVQVl7D)O1XR6At;Wm2UW|0oFW?)~?{UsK!i2Ki zI$nN@lVvv|xCoku=~8Fb>{(zFCc*o0$1%y^6bu~f4@zr+yW%+ZG&IS4qiGmhLl)|b z!H5E`?3t_%S`hphpK;`9vvVG*+L>FrV_j(a~{%l|i&y2a>*a7_J#(t=CJykLgV{bv_tK5_rJ#-7*Q*6FOnSEbI+;(_nWvMYx!zhOH+~|D# zI}|pBOV`+^F@!N1p31C0;ZqoH7<5oU==~;8Yhzq0?eO2^k65*TXMEz(?k&2NW)V*s z5XAG+@O!RTs~->gj;zhC?~%%W1@$)>ei^eaye+!;g0RC1634K{m9(qETA#xyy`He= zF8wc?gSKffpMacN?hlAP>sN}+v@%Y&PcN{!edn|j1ldL1iRro#5GdOJ?a{FI_Tv*; zB>6RLDxX=c7oy*VU zsEcKd>Uv*|`}>reXCDu1`DAO=b1+g;naAn@Y`xmLEB%eg=xw8YD#%D$0WT|n^byi0@MPyx2Hn2Xq8_@fOfC-``Its6Ks7~f z2tfykg#PZP8j?O$zuDL=B<0FeLs}_3?_%G!$`pvkuzbUw^K&g~SQn)Nu`hEqGZCWw z(YOh6>c#k6AIet2@`mzHNBE$_ryF*jH zti5EjE;9SJT4sP-r6HKl@#$&Xo|GaI@sbZVbz$)5Lx?_I79Vv~aOje3U6)%0{aEC_ zisnXDXWRk*eY+r2)9T^XkAjJ-zPu9eIxmqhhDbCoPg327;XZVen>To?@&cN`IDzps z6y8q}vgLBv>qHj%BXLg0J+WQ16GgakuYrHJaLU`Hs`S>9_wn zO=sTsbbhfv%iL%x_b^$>)AdN^RV0}X-a_h{-B)6M$V07H=U>(rU-afjUj*2YV6ZDH zB5HX66HRRqA+1xb_T2YB{&#}=h-!6ZH6LJ7VV4P^q=nYWwQTen_4m<1*K}sNIunEqo!ZpyFbv0| zf0IQ3saJ!RS+oY$k6u$-#w;xJjG1pOsSMGzd~pS{UXGff@rPk2FyG3X2q!M~5)ol{ zt}T(Fwc~u9VvG*9r`|yscx+DvM$3}PR!OiM?kwf%mPrPj4U`A9WK8hWKXbT2^Wvcs zkH9u<|0Fsgw$(l2C}-*IJ+RsC6_M6nG<`?xDTs~BE+xPd&+;}Z%Bl9t+>73w-l-6W zp&Xu6xBcUJ+nVQb2%Y2`0ihiys(Oq~H(VUvyzfy)?wOD>bGA_dCN>HWfO%kJuMuPB zU}lkAxA*1zRb&iXdy(#~+9NuaO`n0dF!)~o3B^kFmf&4+L;5KICygLOgBxpVTWmer z$pole$?!Gdm2ch{22wr|D~rg%aJ(nDn!8#6jk6lq81qePbGc^8rA!s4Be5x`0+fl) z*Y9lkyB?ytVemIktc8t{=**EgzeXuq*dDRQ3`tF9TMvrR{yv)8qbSl4Kb>!ipiVgGL9 zS;|3s-p5Z6gQ`li7ZMJwv*=ao2B3WSa#-iE5ZOa6*+iOwpUq#N7H{?M^G97>U3b>= zyp)W3Y8mSqu_AOBljGSEZy=rYwiK6_==H2K_**@wn=y`#YlA9XSHN9VC@M#Typ{VZ zgy>)f3p!0TIO;-%rU;`_q+BJl$*Uixu@`*9<%++20*Sgrjd$toFp> zzfCB(5XVP-#r_wUMVE`PY~wa!}8{#VpORIJ=K^lu}ee(NNfs1t}XIsV_gKr9duVx@W zCw&n6X<^8-xgK^etOZoT6nOnnGd>*Dr)1VGF=!b_=PiP#lVU-rFd>)u77+EY7?ZwJ z!F5jt<2{Ug^-ACKf&f#9PJn}7h8*Jkv3O4gO?lU~8=eA>8Rse1=*0|CjV@DytIR7M z3@bgJ;PTOt(E>!}FyUDh9%G&^5`8|$VlJ7hcY#|L`2NOseuFM3yEvt_0Lfc*?c)SGh*cRRJQ`Pps_z-&yJMpapmWUy9<+> z^q(F8JTypXg&d8F*P%|koNvic>8k43u@4|{L^6-Q=C-S^p3x*j?yw^Ia`ES*z9liu%7Zlj{@yz~$^XU!7Zw+=a2mYxfYCTo|90XVHk}oY3DD9i64WOW!PV0S*O-`1gTE$jA z4_+1GH#jwJ@G^4nGnx{fD`5^OPXIp43egiZL@faP4e`h?2QMDesh@fC_ zRaGlu<*(%GCt}nrIjWy9nQwHCDbdANe)p}7>V9m3qo7H?=;@bAFY3Zqj&&7m-@h{8 zSXW%dt66wLRuhrc7H3^Y5~3DoBhu8sGsa%M@dehh;7?7#ZW#20*gLAgkq_8Hja_@h zYY%sUegijx)N6p?;$T9K+wKiXirLl479K(jL+9T%EW;JABmmsK%qm)Z)+?`>X zl+&E#wW{mbvH2E4qPeGtBg5;7!7dA|HBbcy@UQza88K>_KDIk=;B~NS^PXO#Z3%O? z{+RMf1zRC}!{uYl6zC-f=|5-7Hg+ZK`U`7e#>jx6v2KF>QcB%nz;Ka1{IbRS7)k@GbxZ>h`pkG z@cquC-(hVB=iQr=a)S%E5-_aYzI%(4ax>JpQBuvav%E|6tqJi)SL!{A&9+tSP5?;^ z^ri@YwB1KcOS)Q0?wXh0+ML`)%e!h0T&`31As{Cgc=;HU>0X<{UK_%g4WUn;#QrW> zEv%b-)9z?4zt~dOmc*@#h%j#9ijZ}+VwCv8B+bGt?_$|i+0oGCQ&hx(5q+1p1{HI4 zW&5BkzS;40aTQcrZO5K0FN?Zfi<*uLr>owb9(j1C!Zd(JamNaWSYXOwK($b-8nx?w<*hx_wO1FK3sLX*dADT zHt*s!>buK+R-J)XBKs2!HmP-?ufWe!y4)__h`MyM&gxdPRr1vb2kOK?Ju}K zhJ=Y_34fh*C^#Cgg%Y0Tf4THiuB$$91{yQ&`MyBvk+cd|X?^;vM@;*!^z{W@d* zB`|Ow#WWJ6-NG@9w0S=G&z~{dzhi~ZFBKx?EG=fAR~y)6nu0c`{O3e*l6n)8Eh^XS zpWWVGt#c2|=Z~D8i(r~>Yf4HxO+V0Yx;4gpSM}z#TM{97eVfO%nx(i^`=DE`J4xDQ zxxvs2n3Ly)oIk;NUe9-HPq-!b`77kS?i~g`eeubCA41ukVN&$sDy3;LaDepj$w!LI zq<2mb*n&<$|ABdoKTixr?7~SL7MOcBD?TS^^iz}%^B?~T+Bu#vocteZZx0C;-S&C( z0*6$P9&|N>oX|FG$NMv8UfNePzP&(N*8wTVUA~%nZ1=4Msmitj(b4U~&G;`4&(5*4@vH z>@%NbJBpZSuMO$H2GZg(^3n@jR?qr8G97Rdj)qZ8V^-H*<9#nn|kI~0K1^4(izUxF^&zfkf2K`Ig9^xM~a<5EpQb;P<+*4qT1 zXv8Drwe-*pd;~Cf(>m944x1kCfM&z}FK<%S18DKUR4d-Pi#TVi(u-*q%>~yZk2^j; zVP9lULrt@Y6C9VXYr`CDFO~cQWSe99Y+5+hXwyf%j~s*#qib0jg}oaVIVKE>LDU}pM)jJGS7g7!AKsnIin>l66TlOsH>U#{mQva02hAazb#^f}Aq&1MV0`~msUH^mYA(aH= zN$kJ&d5F)Lbcdt+$(XNXOy{kCwwE8v0iNa}Fm zVxq7937OQ|hYwpaFa~38^toGbK6&V;os^`!*jjzcb?U;MslBgij?>6C6vjMvJ%pp~ zPFO8DrrtGL_lLR>*Jt3->(SH~MrN6P<+@h0H73CJ1!8z9&;&=- z?pc*MOmi(4#Oo%gUj4leD{F;L(8lau_zTdx1HA^*U`MG$y&q^*!68lDCAER9x+Us= zf+O<>S^^SSl?8gGAg|U+tV>u3o5DX%eO6lh?4JrJUf;n^jLFfiu#lR%+ycXj0WD$k zH^Uvk$~vFyqY$Hx@59jgiJnvY29&Vno*wN!SdDpNJ_$noLLaSO8Ax89_Y(ri)sHpJ zAt=1=^k9l0LuSH;O^jUHY}|t=sY76)3X$qVdwjwtM?~PJ63;PKP8>eC+_+lol9-_T_nF_2wG?a!lBYxX3f&XM)b*Qdfr3)>})|%ERAt z-x!3aCHLuPQ)x2*@0AdUvNiAKV88l5z&Az2oJoq(kJD}o$C5j1E*3MVR)M7zOc0P; zMJ^Tlo^hZfO3?bDxDlKsy}M5*y+z15UQMTi1jLM$So3r-;REb>zH|m^V>81=hfZxb zhn&xEA@ZD4S?Ly*tx9kei@3|F7rFFqL-dC6PJcChI?hNt>yv|Fb&h@BYmu{e3+3Xi zDTiJw$6_&2aH?0y6s6y+0xK}6#reu?n`o5cjt2X<^ke+MN zT(iyIW;7Uy}a?9Ie{@{BM$nWAZu-t}7|WUc`2*HN2< z>Q~bK$+fRx(kT0eo#Xp(K2&_-$j1wU?i#4tG zTWu~As~;#vt4i}j4woVDyq0=CxAyed?8%Ve?Qbi$XFPv4npPb&_7N6dh23*Y&l zJG7R7twL7Gp!!Nb)cSj4f^&`VIcXuj*tkDxP?Ccz(nsM7I2z|`@cz=KHcj;os46z* z`dn~@%}t(dboEeT0lft(aga~o z-FmC6l4*xt#QqqlDJw5cbaH7# z%OqZbPkCK_Z94vIHQe2X&36fFSl8Nie!bPw|42IbcqaS*kMDe9HVn&gbI6=BB4=%L z2szC8m^NoR%^^n7W^+DGDaSO&5Va)H-C+(%2SrKJ;U1|}>J};;{q}wQ{@>&JYu9ys zuJ`-(dOo4X*9ghXm>0I{@LL&8#WGU_7jhr7+=(BpxVM95zFgU!&eu`zCp^-~aXF#C zWgh7;K7{d0Eh&Cd%>BQ+3{wd9C9NRQx7<6^g zU~Acsk+1nMX|Ad8qk*=@lw?Llu?%4c0*{CIG zE^0Z!v+Ql=JC%;$>j}4_+?zi^iHlMzO zCjw|FBHh{A)t9S9zF14Q*kw+Nh7^bTZeRL|N4*lem_E`=&Mop9-+1Qw|JK^&ShM

EyXo0^f6lm%i=hpqe&z9X@a>DJ9l5e?%U*cWw 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 /dev/null 2>&1" +Enter +Sleep 10s +Type "clear" +Enter +Sleep 500ms +Show + +# Scene 1: snippet — show the code +Type "whichllm snippet 'qwen 2.5 1.5b gguf'" +Sleep 500ms +Enter +Sleep 4s + +# Scene 2: run — download and chat +Type "whichllm run 'qwen 2.5 1.5b gguf'" +Sleep 500ms +Enter +Sleep 12s + +# Chat +Type "Explain quantum computing in one sentence." +Enter +Sleep 8s +Sleep 2s + +Type "exit" +Enter +Sleep 2s diff --git a/assets/demo.gif b/assets/demo.gif new file mode 100644 index 0000000000000000000000000000000000000000..61be4570e682e2104e539c1365b315f5352ff627 GIT binary patch literal 372000 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
8myV zFR4<{Tdme&=K$lW_|l?fBAw5zMa-e$eYR{tl%~ce~eXYM8h!4RQQ>+>Z=W;r+A=H8ZnT6hUD%yg@l`=9B zy0=pbt-o@XNBl=yZI-}4=@|~jHh~jq@hE1R% zcw%k7gxr@6njfY>#&-bDikmUiLRZO|J%J)eMqvzne;5lzX%*?FfNZJH%k>3% zEGnZK6~RI;nGk71`C%}6o&GIzmZ;Xk$8?Hq_FL}$R;0doX|l?-QlKY{!-N6#B@BTJ z-C=j`ff|M5MwGz~VfN$?bNCsyp zr(MM4G8)P(-4Qq;K%5Yn&?Ooz>JlOGt5&@06lm224N@-`Dn;aZ2+(4-yA1BL92Hr* zLuA(f^R1RRF@~w(I|vc}Z$|+inG$Unz|z}AbPcPXooxYm-2uA>19ndZ?D-v_9<+&!1H}%3 z`QCvAA%TUlfkml-#d%$8egh>nfqR<*_q7Fl7cFYgZ4WF9qe8UYH9<$3f{wNY)pQ3Pn=idjL+R@0Al-4nwZ_494#D-_!3`n7 zCu4(8r3RnQ3vMh8K2sBXwkh~rTX0i%aPwgB`KjOwzk^#y%P$%)zvQs|viI^UArCL) z<)tJn9bV84T&23Q_zH=_7?cHV%E_5Zdn@`ZOf;*_DS` zIMDmkL9Gc)CIg$v*Bhcj%x4b{G{l;t=-MJ8U#0Y%DhHU252PUf4uw z*!%uRFRwg$-xfC28JgMHGc^@9}vArX{;hz%2KM#gw zPGANG*~G(-KDM=9UxvceYp1TJ*8$#-G#(o#qI`q=@yb zqL6P`ZYmK1$MVg03+N^y{)hS9E=t-N1uw5BxzvZ!gbDtmw zW9Jj;@aOS@F`iv>_=|=|+Z#cQ06+ykS-f(^l3gp@%APDaw!;173XkR|ZhjDz6^;*l z;`L{RmqwH~x}U8R#qo*ahW2yTM)@X1`OR;M3fvVHlob_R)*pN zxA{b8hDK+tjn3ZkJX537LZ~q0gYN0kJ8wqkJ&NA-I(qlV=ska;Aq~FBgfD&_jiaM; z)!3^5GGOqFck!>mppF8O%U_9a6zwSFm%Zlid@UjX=vc>EgD!r#D*1b_4j9S^(VzL!S#_^qj9Jv`qg!4WC`FGSI~^%o6)zar6Zneo z0<@c3opv>bVE!@-5?_lYU5tqtu6f>=WR0bx!<%DTJU~Yoc3)X&bUTOxUU1W2t#J?? z-10)FWzEzibYz!p>gzQ`5qJU}b9YRq?dhwoi8WbsYr+Y!7{kGK^udnM!8>`#NC zm6u-UN@B-9vR@~y#<#}0&;MH6bBDjqF0Sw7YnI!?yDxEVLQ z*y!Wf&|q`ykja}z8gWy1s9&4ctc!p18jjM463(X-XSx?>q!h1|ic!C@L}h_=Kg6=a5rqZIUIC`rIQ?pIb}L3p zIZ|Bx_V4T0h7+%sa7S$Z#xAuP-nw`g75DO;^(g1s$j^hyU!kLWRZ!3bW*uNGOBAE} zZ&)Z09twouE2s?@Y@a}BGZ1DHoW8zzCA~N>1&qqZt$e1ubn~qxS>U5Pyt;ghkUMgZ zI*NIgblD~GmtSI58jd1C2~!FgeDHLc4si~(6~LymP-UAzIs=_eEjBqR(|~a5(P~y} z5rz6jiW@io_GZ=H^*znw`1p~nMWpO(()Ls9bC&4leoNZnpQPD{+RDQG>_7!JiXA^d zoR-3k^Eue%3g<0BsMwUFNsZ{}79=GHm+~G33oci-Oqg9+zxUhPZRGg4PZP*%NsPN= zY^V1ZJvOfIODK=S#Sd9ww_rW=C;tkdut)F3d=S&32EkDnzUWl4;Z z!7_W&dwNoB(Jt#*!-^}ZygRA;ZVWdxx$fJC0%~$`LV-69aOgLAN9 zt=h%kL_xgbmF`LO-Gd(=DfgLI$p86RGL=Wz9jWx-(KacEl_>o`h$X=C6ztn_i82zq2G_bjC(Sf*@?G-DXQrvR>(enk(`OHjY@be> z@t^v&TdE?z`}Ax>!`Qsi$GqkW{vmP?5ND!T|O>egn7DTLfO3uvyx z{j~A<^(=+@Z0m=jk7vHkWS5V`#-;1ps<@|=dvW_Rp+d$$nK%W%AqAVoFMc&z#B!h3 zsus_zF5bo{)Lf1G?xohbktVJsRu|q*B!9LVTK~2=4a-iyRhBS!^|Sq+*OsSC_2y6G z=!#(tTO8dTVZ%aL$}+70#0w?3BpG2jFkHky?w}X-dzEN;5t_U4Lpcu4P1dcgRy+usikY}Y=OvFBfZ z;we>$yGR50lGX`ns31pT&R+eTe0PynYZlIKt`rjKw$2@zsnE>JEtLLH%(v!LjZ5i&dp~@RQ07*z2pZ>lv0A~P*&AU9EKZz_-1|$s7HKnju`hkusSl5N-2j-vE`%y^$ z@nQ~8L&nzd>is+}sd2Q7_rm}qWmoAmlJD%z)$kBeaB7qn>9ZEB{f^ss899HLP#l<3sE=Ss#0)9)>(b^zp>Z^TOgK?y zPcK}jQa`ru-7p0QbU`of4V%vuM6yJM6}XM(P^IRSdEAMNScvRR9CA1DLojF&T$czbSEwpyjO`w z={g>iG^ShLD>q#xCfQ|J-QRB$UKjGC7d*55i>HfpxU%zL(qO0Gwv|xvgL2)aFMjU7 zIDT6zW}@~#oA{!TI-Dc#2IfKFKCM80646&#`R|s&^X9}%**&>dLUj`N>hQXK4limd zEofGKuN*>;*>r+nf7|vt{chM^-Or!ZwQwkbT!pH4MjQg zkDkY3v&<7y^nlAuRESTq%e&B;*3Auh-qZNEC0?1YIkA)r$LmYdCZ4f#X>mn(FsbwE zUW@aqAf4@tN1zVsq-;f)hA6!gcKOhz!>GtXvcVH=EU#)_m_-FM&!$!1ugw- z%|Ib_mws}!C~y9&l8rLsm9(=%5Wqh7jq3F?yhB~0c4Y9$Vm-VtRxMzh~Z+lL^^lX_?RYb3MJC)vnUo+o!xqJ~x|Dw!2#;W+AxV#_e>hhz9PTr|J23JZP zPml`Ps3^}89*)+~ipaDRNG%9mS6de#LBYZb>xe@GwB#e^mZCi-pO}vl52-T3+%_6l zvr&w0xC5gpgORAqiYNYuHDwztsF77*_o9t(wzLpOstqu-$;-AlXL!T2CfZ<=M9IF^ zzTBQ-)$Yx*OS7PO4|p|IFqRv(-A$Z*d%j$BgGuFDcs}EbMA&zkyBR-RgE& z+8|V5)^(3`FKmqx$u+!PVRBqYVNW(XKQ4VV`sn+RlbY?w4GkSQKOVwKadEFjo>B|0 zMi7?39*#BhFyeAxfaB7C_BjW#+^hr$r@BgvwMhXg4Oiq+&{y)nI5G1u?(U+|((y(V z9Q@yR!D+tvto55kw51W{S;BggK>-R+M&w`blBAWz+AKPnTYvM1#FEvY?=6r+TCv0S z^XhT}%{EQ(9+b#vP~Dvo2kpzQE;0!;Y$sa5`<7>Sn0$czbe00BXk&~?pRj0BFmPkB zeA-2W<>#X*hfq=uN4JGF<3I!OQSGM9j5ziDPb0BGZqg)W5w5i?O6#}KoDdkE?B8}~ zYU6{$IUbg4)$`$6bu0jYx2&-sa7t-a!99o~F z1U={-CeM`^PenW)^BXBLiV(CE-KoT$s>hfMAV&qb&ms`e|ENF^ER+vCR>L~uiVeIy zw4$xLXo6#2ZBnA7bW2mcE-ocFyw98*pU5@d>Mm+i{vfAkCZHEV^P*#;=^ERskh;Nq z5!N_eBg?zPXRNhuRZwJ$qp8FGxDc%Sq^y=AnKGkQqqj@tW!s@)l(it=Wr(Jc-cYZz zQuP=^0GIC*-lLJLId(5=$y-}34R>_`mew3ozwC&U=D7{Mm~BWa(ul$B6fWhQtdQQQ zyYRRJkKv&SuFpj3K9}5Gyve-N)Lo|imFlNm$MOnvxS~0$XfiJNRDQ8isqtQ2a$m~B zzA}cZ*-Vrt(?$DZf_sPIH=OIQYn8gja?R<$LR1V-E3Ha&ba3FVTfRagjX8!E;*i=^ z)Iz;*bF`+a&@-d5Xo0eyWIiF+sZ#FCpeGPk3Qh;l_kX=H^(_%u4p(Sa!~4?u|CiWv;H{)Yh$&Ot!j5oMv?5I<(TPzpu$bG3sN9gCi^4>!fx zEFyj-U8M61EW8tpW>^IjD{AT0RX>}}B%8eo`Y_g`b*8&ofbcL`wB8PQA=90sZ7M2{ z`KodBzlkE_Dxio-m-~ja*e!s0m`sPatOWvpuTjOi#3oAuL&&N}($LF!2d|)>Sm>SG zNu|$?s09T-`*tOG_zFON!#|X3_GzBG{E4OJN{QIz*{37wo;SlU3_qQA)?1u@oyjeQ ztJS;Jz)nl6N_%APaa#N9uipx==5&_9jf;DYIuJn4N1S4*$CYyZX5)C)T`z%%kRCDt zT+6|l>hplmZlsn5L&O+#DM9oD+OztvFr$Ks6d6J%;RZ&3a(lP0)#cFyv;P)(rOxPj zv$ZD~ekB(m(x#)bqk}o+`>wk*X{3N!=U9b1i#~+qUWp&O%`e_2lp(-@5#tQ?>@@0t zrtRgDQJWx88%i%**d;f*I`Hn$`^uD#F*%ln;#jL=@n*QUhASDOfXdp!+)S?a)#@nZ zhtiu~2X0#VF!LP;;wi7@`$4p`e*2OhT9J0OQg33QonWPgJ>Z0y-kbpIA|5pK4xv-0 z%(Y|k_hs}C18fNGL*l=b2v_pydz9}vUX*0tc;2R@b8DW1(a{Q|mY+MzX0Wd$^4KQIs%Bv+1msS81B}lh^h?EGpS3e!{>+rG?Q<_+dh55RH zfTnZ}Y^-QAAhlB;>b4*}29PP%9Lyn>eGS*1r^EwbxGU_^sNREz!4{6s1iLB~GKV8g z=q#Li*O$feb>u#Kitwa&2zM29IYv%z>AdfQ!K(`%qH5Lq(XBoRtwi#pTusLCi)H#6 z^PY3hmv(`cjggdUmXjpkI0r&`b2|8FhfFSA9WrwlORQK_wWesLo|wl zu^N%QNJmLZ2K*0Khqkcs4Ef$d*rnva9++k0fb@!rTFf8$xGqv+adKsfNFGf(eRB^UF6W#5=@(gf(!G24arJTt|V5U``G-o2?~0YjvT z>R8+&Iwaxz2!aVe_nuA1oZEG9(E!H4AEA%Bi|Jms=6y_TU%qJ`FwgDO)9$ahh*x8L zcQ5DalUhaYbF0SPf&SE2gZI^;{}-etNMG7vegCbefTNMKmr0674IUvJKM`27>SnGl z0a|W2Cmx=INKz<9BuYHowc=&x+COR}qn!w|G_#RM9zD>2_zSwYOJj!DCJkqm4Z0nv z*~xk=fMGnJqlUkBN|2u29j?`)g;|fGmT03}=K4`0<0h7Me>|$6<25R}JQ`*zfN8A= zcN7QX&awzuivUD6SGc#&$diuze-VuYh%CFh3J)ix?0px+w+N_#GY8a|np&8AqEiD6 zCcz{?j{b=yq;kFbMMey7n(uF$I-_4}BcRHlvMX??9Dz;&x+$dE}))$Rklw*yd{=@6e zg8P16jqy}p%|bw`Lh^e!2I2bh&r_~Zqr|+`SSxc>N7dfi5=MG03S5HTP$#$2&tHCD zgl~g`O|euV*R3BW?ISUL@Mmunje0NzUjIkz&qYCNwi3ougf^; z+`=`v^3NZ7hnk4Ru~_p9x?^18trr+?B!l3BhI~c=DBcTdx*vC~9Mdg2rMJ{yI|{Mj zDciLx$WWhs0?}nSWE{{IudmMKv7$Ut#~cUpO|@A7)V@^R^q2~)i{b!Z0XvBVDP_?I zI@INCNM9KA<#QzRr8X>V!*Jfnrt_P8H>RQR4Q!v^2p|yBZsNM;v4{-zlAI1gBBVK0 zx`!e6aPOF=gIoRCUOy1RB86}_EB$4iSC<^Df}8(7Iq*IqXs~Y)4!Phkn98j3PUL7M ztLsx?JWlMb;uvSuS1q*~tEc3&eeLBcBsDjXR z?!1>;s+R%oHCRL-3qfW13xJL&5#S3Usc?+?`9f&}mYT8aUa}EHM@V%(|mgh+4w2I_h~H>LU@%%n>cVYG1Xp!7>n4Bc~2Pv`8%|_gk#hoVl<*T zS77b7NUMHOt7db16k)1K0r;fhXnE4+ z=XXZDn!+F}*g+L!QUz+t1_PPnU&{;n+BsfrD1v^0OR^Z13K%M)ufB-3fF`xNkJEkS z$$=2bVa?84#)5o)3^~jOaa3WfSL4EAslLOgTbDbvG_S8>*E_N*b3L4=$ zyh$7s9fitk_a1FGs{*MqfSSrS;J435u05(*0BcJTHU(@0So|!bS-N{mrRjS-knh>w zPL~Ot5}S~XItvQq3p%Y2B}WjaXV@r_O9RKG3oK~j()eRK>Y^Tz$XKC8=v+xWEjNor zdS)7Svfq+&O8$(BJYu=F4QwMB!iM*pf0iUWk~BfUU|2ScDCXSCftI`0`1`tzO9i0ZEUH)v>+XLPtJ_3d)R$}+=j_5YzLKdsb1Y97?_Y|sy zuE^ONmsiJa^%W-5W%;)bA{Y9y-+cv)yE{B()}H;z3+BEoZae9twjKf@7e*Svoy8nL z8t1W~9LPld_m7T4ni}`Ly&g7tCIDIWe+tu*53~8K)9nRpLHp8fj(&H?qJ9AEV3|yH zIJ4wWSGEw1<<4y#dWkAQs?g}j$<_TxUT%k1CfBSDR=tS2`1i4yF^-hVX7(e)s<=Mt zY7;A&V;9%s_aPtcQ>my9FYPl+>_fY4sGP@B(dk?tiHzfV!3`dAe$jD+12`%2`QeKk zd`0+1sv)Ywvz61rP|7_?z;-ljBY2{zEZRyr+y+!7YAgQd}yj2~xZZ{G}QeOmGOhs&*?Fq~cyK5g0H82z9UkF9A*SI18-A^ClA)-lfV-QV{kb2-D7P ze4gA&+HTqZuU-L6&hyngU~H_7$h0cJwz6#n9MgU`!y}LZeTOw9wiNO>-Q9YlYztVf zRRe1k1k*?5`>9Xf@EQ3yL6qP(Lq zxZiPXlP{&;&90%{YCulS^QXdcs|K;n1e>PciK+6iqy9&$VqIJdno8s=EI!e!b}q={ zn9{e6Kf9E<&m`+Y6t)H?CIZ{H|670jjK5kc=i@mw@Qp78#VEE$DvmR##@^vDjRf2T zhLi*0_1Vq4SQM29r$K!Rz&C7jI=98odvESsc?i(&cd_U?Uy{h8^}`(-G9waUD)U^& zISf7(THm=7BmGbUqEy2eVHX4PERsb;-z@v7h(L2?@ zTI6-7YQ+ys9*d?A5`kOPmEB47dF%b>I=z;8JLkBXYi|ASe%6F1i`ixh$yvc7xme z#G-|PQRpW~RH-iWua4{7RwcWTs6nvmqFR*NPAjsAE?kUnnUP2K*VaB?x6BW+wz(Wl zYhPpcqDK4Z^LIgoe37RCEARFXjdvT5L`&%%u{<{gdvGV6ZDn&MJ+nbD^A&t4C(%h$x?8ecXqsI0ql zYxh4z#;f-;?;UiUUw1d;w|++A{R3GDonTkTv9n>d!6r?sw>@iKds}SNU4M`KAoaDy z0pYXE)FU~w7LDz9&qw4#I&97L|z>^e?|ND z<`!=~tpm~vh~CRmpFayrn_J%+AE0&7ZH@ZwqSJZ9b{{gdFIl4gTfhH0W}Ure@bQ!e zCd>FtO8idSMx6GzPp9Y-S;MsRdSsK>n(WKz~bpw8`Hq9Lj!`gn zi2sm(qh`R1W#n5u~o0MqhEcQ65*C^%jV080ilpRSF5j)WVrCW#0sur~YSO*cNWzj4E{ zb$}FjlVuJhJoW4)p4VFLAWs}FGT1#ZEHOXMAK7bnYtB8>VQ4O)+-F*Uw8D>kc(gLq z$zbfj%CN&@2iK;}8zh%l{zy8u+aT9qygKXrfM&$AGm^?|Pgx{dCu4`>(W2(-SqHZ|XP~e|oU^P0g3< zZ%W#}DE!|1`f}-m!ObuKIePwgTl2wgY@!+VB)Ow^u;`#E{$_U`W|n%|iInm&LX`lY&Vwk+@ConWA}!B|M;l4XH%xr!ubogwSC#AbkS$wl(?Wk7Qc#HU>!9~FrSlCFHwtJs)iAp`^vO?sKp+c z^aUAx8W9uJVq5JygjMjezg;X~q?(N|R`}X3rR|NV8qq$(_ruazh;ad=bA77eZK#q-T12=Y4Nfo5?XTAZ>3buU=@laFQTpjXi0O@# z7DSC&G&6w34oF~4VgsGhf=I`)*uRF^xK4S2o(x@8t3AdDS{=ONxTfPGmE40V4vxO1 zdFW!*n8U`^^UK!^Y5sG)f6OV@-~p})p`$x7#w@`HYp2#4J-2(e@bHgdOp92@Z{s`H zGpj>a6tOg3G*&LY{vtGbw>pe9x7Y354}`VE61C=M7_RG6L2*|y#>f30fweN6w_2dn z1hGzuGed-dN;CJCarW{1dvPS~g*G|d#rjlU!4RruB(|hdAVY0y_`a{o65M|GMaPrK-x-R*lL z+~#mq12T!Vpw1)eYU`ESFrWJ&?5Kzgfi}nc&!g~OTGW}{2R-~k&+d~F9-myI)3j2w zDaMQQl=44{?!}+U{{I8`bzM83=RC|@bIPeKIZUVxNkStesWzREG?H}q);4oijU=5n zNjk5RTT)#^C8;P$C5_~kRMY8pcX!`@*Y7{z;W~ML-mm9tdb!zo%G0!rrt!GO!MHie zf#c@?`NbD>iSZ#=ckvcQl{IS;P7ywkm_0mjE%ds5@vzBB`c75bs=mwA8xxW_H&u^j znO`~bBX&bzg>x6a(|+S!w~zrcx9QbEzb&-IOXkPC7-&#qZ)_4qiUqGHzMDJFiRYuu+Z<4 z)plL=UUc{SyV%~XT1b9H!I*aGaVz>da=ot_+l&c_K`=x~X68Mj_bm;@dW#v91dC$` z&L6%YjOQt5mvNt;bLptpUB$RukkGaukWmi2@mSZg*?hf>nGQg7so1eFumUJl z!1ww2RO%>2KRYPxDq;Xu-Z)bLahx)7<3|Lv|LsxQ zbOHINcIjBxfU#+zOkCjpoi$@?U&p|ZsyL21OyM{anS-KYz$u98M9gA9a)JYTY`L5m zVpl8sx#yUkB9~!IqPn*W)k3^YT|+q9KOe3fHZG~oGtD-9(1rhF+c3+UTra@8OaKD} z63Ds`T}Gao`_HLaLyPIaErv<+gcenTH~pVp$G~*59Fqdue=fZi z<#La!hUd#Of?ZbsQN8j_rRgE4dv5V`FAx8@e9E&&D33JQ7peKD(REztp1u7->3ICulOF zg}dF&@(eKklhwcwB|_{di#B`v0Vm2d`y;A3)`+zfwoepT<@GpM&VD2s#t1;0pK_XO zroFq~$C%CJ$>2z5edb4h32A+h`pjPrw_a}rLU zgzR+*_5n;fC1}OQ4`al9Ldd=ZvfDxPMu5v5#_j-Cvr8n{wVMb@YWL@!_&x?bM`$%QWbPFSZf^b0s}<2O%VeEZgU%#ffDO3@hv-2f*YdF#FqL6meZq&x-tvLh z88A*OFt5;&69Gc@Qf!LsWD5GXab0`4Hsg997_jJ-SfAtCywVx>J+raVSocb-n&hlr8P*5F z??CA78g_}1WhMNyTIO_?Yu&H0ZqTFB4DOQ5tX;^NkkeKf5$;_wX_0V#%8eYD259%59iZREnJ^ zV042f;c)9K(ft<0f&yXYac$;;q*VYR3Lv+usc32$(J%fpgVG+0j>ns2Fr`&MZAB>U z2(=Eu?$DFF^f4CCsg3Gexq8u-2x^^r$%`OrqcE!z#`2R>aWLK4@gMoktCv1@d~K*X$H zVpao~_j0Wo5Uh{PWFAN@1B@Fr>~S$B0HL$v8S7zkuYSU37~3MVY93-BYAe)x3=rV) z<}_-RneopcO9+!Bx5|}U4+?3?W`vGRt6f|)@nR(U{?;dXLG&1}ZrzM&zVlqy!1)sl=v6SiB#^WdNiYxy>t$4GL@4i19JmTsxX&)sW?wZ@jO95CFTx0HCXm z6QTos1k7>JJWW7x_ljQvQ^!EdAT_a7Y;%c=F%8DKYuF(|_Ie?SSn~n}YMuYKX^g`+ zU*|AETpD18VyY?Qdl=}pDZuV5u&^2zd_ruV4rO!-*|}V7_hy%gIBvL_Ho>)k5R6(Z zoDcs!+WYV+EU{>8vhuO&oYq@(OU%oFC2ukU4Z1qB;+(^8SnrYCmYIU}*I~YX*v)^*tDUIzf&^Ag3dA_6>gtk_R z7~~7SLSXKcX!~8m%GpF1+e`SYw!Q6>zVe$xM-Dev-5G&kP4&=Yz-pBQNP$U5BrE}f zj&ksIb8dbAY1XPE&k_UB3v!rR&=s3;RPJ~B2;CEX`XaPsF8hcu^RGv^dV%>49oZBi zYlRk{I?iQ5Fg3)^ zs&3e}{|3HO_CMPjhbgn;Fo?wnmllM+mN!#~YSRgcQB*jiEmfD&ifJ5TpVL>O09-Br z6=`f}Zoit-g^kW*o>jlHI#hX^$QxR0&O08g{>i z=H+LEy5aGlIf4?gYRs_)EB7ojw1$!+!Gr;{MxEUXjddT_LY*uA93 zPG->z8u0{`uZJvDT$P)ISo<;IGH4=}TQ^J0Tzk6y-1SFVQus1~>I~u#(P-SRp{GeW z<1nV+B`UO<&mFR;PN4TD;zoBh6Nj-^Wu|E$iu!7#;tvTLvzpfZSox}*&@t;80!L#L zFN8#T@(J#YNCn{)&*+lenx(7vhVW%-s~QbUq9e{)esY0^ePYORw~$o@T4rg8A$>48 zk$cZrvu!W2R%2VDAti`SJNKKHObc!G5?cEX%&W^a{)tv(Iqs*x{GP`49vWVfpjsrs z%5f008_a1&uQW^S3G;h$+Y&Aw*-ywp&ZKH)?iAF2+<&&&-%kCCG>dDwV~9GKDNt8fI z@Rs~7^acs5S5X=BeQ4tcRE`#PSlCHct&Efb8Ai!$VwVnhaJ_p69#K#^hZR zjol^gJ@Pd4@|qld`Dw-aUYlFY)d;v7d@TI_R}2isYdBX|;H!>up(zI+#JY-WVcKAh z$e7l8)>W=W6KJ!WOM~A&&Y;r5g>0pU*8B2y9QDp&FUN?!J7>L+<<<7<1yGfo)1U?# z5X(LdEe!Rjb)4@4007V%WZ6uVf(A_?0q_;!h`C^x02Q{)0X5zWga_YOXi~b}IrMV?-`fz=2{?L4qZu}rgi+edYz>=xGI^$didGWV|+17tkJRf>-dPmy47Jkg# zwC7w8t2$m!D#zV^Mu5Y)WuL7)HjSN$`kraV7pE3-E-g}GW zQEUghTV3Osxy)^QYY#NXSSBV3`!3Q-_6VpwJI~PjO3ZE-w((*%*)(ta->wS@`!JC6 zw)1HhOXD{#zr>wcwISpR?ilv{!@JT}{x7eZu-U)ApXy-vI|#8x2~6(Z`6XpY8*v}< zZiziUfY(Z#-!4WR|5GLd6nj7uk4gSMXCHpn0MJIC&+NgPulbI7GMyLDp>5;*?tY3j znxi~(ayPZs?pe9-fFamvNO_>l0h6SM%pQrKEp~4czatTyfLsh??Rtkc>`^aFB>xs) za=N^=7zVhHx@RLqJ43h5awxY(2PFwiO7V6gO}JnYn`qbnC^ zUFx@_B~8iu0|r9ugr^p^sG1 z{Yk83k#AP4(25k8)bqgeMN(drZ*;WXW54&ctD@%Wc#xWhjkut(A?C=|p zZ96PoI1=D*DzDreY6DIbUPm^KPFkwBQ?j-<`$JRSq?JB^B~Nfp-J`lM#20 zKt@mW3yi*Qarkd)gQCr-n{V99urXT`=9Q-O(xdYNQ4wwq!kl+cL!>b!goFVQ7c{hg zsa9WtOYIyWzWm!G}ms>za7-|gsRHE4OH zylj>;g6|K*6JwbtoV;D^9jA-(ycfLoD!1NIUKYvM;n}(1ovL8SYb==+C6W=N8qdWar{aT= z1#=TM99Dqb`Z2M9-=Hm74Qjl3d~il5`iZqmjSH0J2c~JTSXnFQJYQg*-ZOuC`zaG+ zBGzAt7S#%dEC<9adX53N9J@v-Q|Ty9;J)msB{sUEpxrr!OSz9qSASZ*==M)X-X8G6 zKKx@GhNTBDd==^K{+T#*)?|HR~ z6!<9`gI)xAIGIx=hHIuv)FcnJs3b+Ep^t>Mj~p|?6*LMd6JsIxN`$&C7Gv3JSR4yU z%GP|pcCt%)mpKB^*YbiG4|5m(y94ci;hj2pUyPrVD=#o^L9pow_xy}|WQqg<^QSf+ zm?)vI{Vu~wRE0dI9)w5iQUayeIlHXi?5=}|T>Sz+Re9-ZuZ?6*N}g}kw1gB$d}mWH z3O1Uzlp*B9V3r>5ULwWC^V|MFa0T1|!+x7eHTBPGnz{8=g=<5b1sar&J?wgn=tPJO z6e#@@xu97RsFpMx-RPqN=cwOVc{ryGnbd!ciJp`%O*(Ee2w;B&Mr z_k=cEgZ3W(mz=?QK%{TvJf*Ie_WBP-#IazVrPipFQY0e{u|f}KBW3IP63DeqXfg}N z(w*@Ho9+etbRK}2fUH(Bb-es7c|@iiI|tn&6$uE_=^_`bxZk+CqC0&4PrIdFi12`* z+i~4feL07fKTBJlIztLCneqD(k*uLDs@#Dq+Z{K@_7>X`D>ofy-Xn{})|8}E)+>+d z+()Bl>q!dhLFYVNmNFtVrWT?1c9k2IEpkv(zcVA@N@VXwo)~{U zRZj4M3v6GAaofi}Z8rN%c8X}I`D&GN3zlBqTQC{Mar~pxhZo~`0fuhp7P8k%0ZIQU zDmM(d%HrlNp@F1%DHhTwF_`)Il$nLac!#2M_}qNaS&wVYr+f^(*m`l_!^7hhdDj$h zKrnl>65tK=6fn>}svq5XSlCd!Lp)sr(NPL{t{k9XgE(3~KsD#?j~6_lNlImrauG2* zy9RsmoMP^7uDo;EAeQAPv_d8+#Xhw-Z$aqXPN`Ri9>h3n7bM`s+tJA;MW*coB>-DdMs|%R<0_ zANy%C3v((gNB3)~_UdJ~&_RJweDuEig#xI%aL@atWr|I`r-z4g?^}4nMXA}re?KHp z#RA~SY$8hqrW&Y^Rd|jbY}>G%t7dy*lCRCSr^%t^dirfW*ow_b{4-hLz;;mKzabQ= z6j~0`Ux={NsXdVIK}Z18hXK#l*_@XuTAUs;?-0RR<*Msh`voOGWgxv9-y>cZP;0x~ zT%psgGy1w<`C9Ouma$O{;(6Q>1PYXKH-=(oOEEMDj5)I46ar1o5ptRMS;@ezDrk+C z@lFe@4oOMQ!T9U(|4Jz#2FzA`%s!{sS!0ls7Mqxyx~QAlrUoq3m}V6<3Wg>^aT*>u zQwlaIG4pxU2U@I!8axNkvJr5(1^ap^@s*TB=2`mc!ChBL7$&qHc2+8Ig+as$fL133 zAK(izWwfIJWM@dfEytK3;06Qvk(eBdfd6x5+eit`0^%R08RuGEr#*eyfjpAQnwXhl zYilNLyqgv+0uaGw^qe(AMLTsLI0=FEVn&<^SfHhAfq4#6+0$*?oq){uDvE=g-UEZI zGm!7PfQ+S>Ibw(fgOt?mwpxx&A8<#&NHu`28<-Sz^xbb+zl^qoz6W2c>BGq&Dt&I% zV`wnd)DmKf$X{e6U5v0mL_VOx%>ZDYUcqC6+eM7)%&puxTi2Y~8sf)JSXPN2!pu_> zB-bGKqB(QtRNmoY=<13rEtJKieeA}gMLSk&NN!B%BCy|F3?2rS)JaG`#ktln;isPB z*hq#nxhH4f$Rn5q-5Aeq%m}0_+g+Bn1LO^Q$~jgMBHbU7bT3gy~;B*`|){ zU&@pTeYVqy=I{(L?KhLV>mDXhixD6p8wJ>nJbFswrj`6;%h?70s0+iC3+BVvEqXh5 zc;S2=;TKF}8k0ALU<0HWPZ6}#3m@#ckbTy3PZ&Eg2s2-dfq8WsU}rKD6Tzc)ti|}K zs3i`NuO7zU3}`^94{5t2gbr;%l_T}y6IVh^N6dL zxtQ`@OMG~Q@>4`$85oW*{vL`Ss0l14CzOY4KZ|`trqE%vL}a~)On?(bdj6T~D-X<}O{)*@)IKM%5bX2}a;O^>u+`{?t>Bs|fQFDzVM% z8B$!6y152LU1cOk1d}FUAR=yS6;U@DFfKbiS0`jL4^O}nKhDfL)1!MeD+s7^pU5=JZw zO1~w>k+s;2Bj@;f`~$vAfIW0hO*tdQH+NH4x0boQb}$i;hP0$bNZCs#_Qf3z=pgw6 zviepGF4s0W?gYzD%U(VR{)#b5B@B2*@7|)YBr1 z9Zd9ng#RSHeH$>k&b*X3!2EwwwVU=)wqiZg$=X0dq_mT6)E(m4|HfRN4&4eB>uF80{2fi6;8E)J)K3Vl3b`4xFxFHdtMF`03I7|cbep3d|E z9*L&KweToQ0P=l3E=&6~7?tc{T(OFlros)XNDfTw0pOV)p*|GRF0$F?Jjw^rN+=CH zDW*^L(FUdTS}}+c$>gJ*gQERy5I+4AZF<2+idb|9vjr{xx^(jca9>Yb!D}??CZmM; zhIhbOfWGSy?R7V4A+NDju4t5^7w2X@LOa`y)&>(^>-%&}XhquAmp9ptDxBhx{~ZKi zP2vUt`iSABb7=D0E3e;9LVwt;jFUpQ)2JT|c*cX*w>|;Kr1VOu7A>>u5>te{?zSt+ zUqg~kCL`GMbf*q8Lyyi?8NZn+Q)=R^E1L{b^1LHPE->z#s&!I8dGY{vQ?x!($uRIJ z?gq>;>DzbOPjpWW-}8^QL{fYSyrSxsTiqsA&AR~2@90HHsa(1&jQ zO2KE1Se-LwXb3vS9I~WCe!CoAucw{r#@YcK=vv1w9cEDUYS+rw;o^V?0YS$II0*zc z8E8}YUVLB@36$Q)s?IkClDD{Ju`jMg)%ij|Uh)z5TKu}{jrE#0Pz>DvJnhjwmDxfW zWmGiw9w1LIzHGZL)>=e<-F@h_3h&*WaactA;|)#D!=y$-s}Rblp1Ktl_$?=m>i>KJ zE4_~`4N84-^*_c`M5VI|Y%zjbOysu&m|X71~RKfSPZ!I4Wok6Y9%{Y|*)n;mhR@O%f6IZPec30>ko4AK#w%SoGe?QiNTH z7??L8lPA_*eD=)@&$wTzJ3VL;UQR=a;O!`1J&@foyfux)y5OU^Q>DJB(HySEuTz1&w~vO(ux&5Dha%Xi&yx;6!Dlm$Ma@4J!NYR(aZbSosspd@ z7CWIGTHOxUzLJC>&>H6YwVM%tDYV)Yv!WRj!h_Z_^8NMDv~}qU2 zt;d1!^yikfv6Ok}B?OUWxOn&+S&f-1Tdb)C?$kyUm0?`fE+x4??RX%}{F^K~d-;Ts zUH9KdYk=s}0IbJicrFu8=(hXW-uqx@EoQNr)+`0ta2ZXAvEu=Yc?1jQKVAY1Ycb}! z2c%00T9&aFvj6t>1PE_|j74~F*qnvl0&gb+r?wl5a1uSn!Pb)+i+@Hh|N9blooF`W zWtsQXzi&@`CocZiRvcsF_$c(pPk^h5ILLI4{~eqg1XX*u1+hbj_8|ofXQmx>GKg~2 z)tIviYx&iViee=l%)>G7!>eYlyiMhPo+gzY@{aq8mW0$j+T*tCSn!9hSgRGqucl&N zkUL2#UQS8&uLj(Kh>K6fkVRw^>8M9c+4mrFB!k0UQhgYAHgZJj=A;lP*#H5?9 zGh?2HxLP%|gd1hJ|5uny;giajenoDrA$%?k$QY0gB^T1)SZuvx{^6g3jG=%M{}&f; z=6|(u$!YJO9XPW2O^alDjjF^f?@+)hwMh8xZmC(vizf+_(0%F%anH^_rmgcS>^l$N zCM?0Tr5#W1?8$I;n_(F*8}qeHgV&h4PxEC&-c&?_>h`aR3dyY>xt>FN(>l(lUPeMZ zXZqp3kc;m&$E07uu}Dir4miJ^A29yMxRd_Mk*l9dMra8)W#`eFgIJ(3^|3IK@*&zv5h70m6w>Bm@l`*Q1c`gb3 zYvkIJ7OS1^9S7HjaqS=M6FNmND9>~%u@qi~%{G`X7_CmjzC<21uRRsnJ)&s2?Eh8} zv*qSnX2V|J0co(*?|pjvmMds@jZ^@p25AK&S+|IB?NqjE*`cUg62Zz(|7>0Mc9mX$ zHA*QmBY^Ap*W+7)*gZ=&{qb+qLeBIJoNrA<6#38!=V{kJPP?u#`+#G#YB3~}?u(1@ zCMs1MmBD#rf@idAgNu#FBL5KQF-A>99aAA1Nul*U)oIh1lqR@nwW0jkt0OJ@U;ktH zwxy4=+AjWd%*MHY->h7ks1ing+h~%MzA&_Dbl$DgFGrmYIxO4Z^5lo#>8%?(i*>EH zAChjHt?w`y*)uok)%r7ebHnO368yv3=v^IVU!)v0XNJZEePasB5n( zyVsLGw~RPF-&Ec^n6v);U#qAaZcE<%{qfFn=*Sn%YS(7GWi>tZqt$x;+|Slc1p)On zmPy>KYI7lmm^9<0Ftgbsyg5tN{y51{I=xD?#@H5Lcx!!r;9eV#y{VfAvY%SS9?Lvr zyn08rvf|#Zsk;aJ*glOLHw-Sib+nMfy>rSpE9v97Lus2S|bE8ofLmDYGhoVV*nESswb95FH(lbSD-EMJp z>-w~>P;m7cV}W`V{+pLSZS;MP&vMYTt{bDu{<3q(J|Qdy!TMKwkX>?@hp%E{10#|t z%yPNK;I_7r2jKyJAH~ezuL0cuHtkrDUh+dlte9B7X{Xx_5T7pGH}hkBY&aU z%^o~s*Gyb6f;D@Vxi3)w;%r61{*Br6&AoOMbMSS4*dax_tc_@vnC!f)RTzDBBySc; z=yVA!qmLEh&~v<*U01@kW}l+2PA>x@OvE8)#s(1T$ymBvL&ND8FwM$8U$HPf->13;g%V>-OZnPnIpx**)$d+14_TE^v4s8JrbfRS8)9nc$e3 z2rOQKD2|Qp3bmvkso1f*hL%|0?%gaYrqxPa8^a1cTGjbcSo^~xKp}fvM($QWS=ig_ z^5fFs_`K9D#RI0Neb3?_9|S;R;Fuh{dy*a$BsTjYP%ex&G*zi!nto~l$f|AH(==)G zP=$~yW&1KJfo;<&!1I+R|8Fk#av*EMqfb&u)nYOGv69tgureJ%CrJGz7i+pcWNvS+Fdq?8-E!~EkXuIjeeZFEC{bf9%`=Lz z=Ro?*t}RAM3P{*@d&dVjTW)T zJ+#ph5K@ARE*te3?adSUVjm0cSDsywLUTHFF>w`%;VIY~Pxp2pq~r&LYp}Zyzb9P~ z7xFJn67lD-_%P-IznDomT8vH#?JwJz_h#=B{5Ia-r+*F-Z47}niABYs!!=v`QVt&L zvwqREj;v6X?)Ys-4X@7+beDit4FEaCSz-KLH58YQ?ES3A+l5S0O-?R%oC#oj%Z8j) z5qHf7#P{d&yx9T^UeepPehrWyT&?W zYL;KSQ8D+_JF7ka(UDu62D!(oa6H4VwX|M(basG)rvb1&(L?uZiNVv+6A!0y5WlO2 zQ^S)}Ix8TPTK&GAOWL`3qW<-FMu73-BE=31}h zDDagSmJhAd-lkc02L4P5a4&zV@$PXmSM6o3TlnX!V?PP z0O%S9nj^*fs}u_@RSaIXfx*#j%%EJ}=iNsshrpB-cvrv{d zgGXZt&{AzebeN2ql9xp+uqwK^S_(W!!)4}T88StVVUBYV)ERzf&lKQe|HmbY?vDJ< z(TY2j>N^=X@9g2pXYvcScVJBUFbh!l8iJg1D}NN--XX&g#3YU?&!ZEtR4Edj3%#lZ zQ;Bj@Eq1%f$#zPE;JYPLdACJ2-9oPj;MN3~{Vx8P3fdD3wh6GN>Y~er^E-;LB&IA% zTNtGU(7V$Ou1`r&`u;PYIt8r0rhK*K!a8ReqaIxATo@s1tyIa@uQfcSEb33paG+QCUn8Y(sHkuX+Ww zCMl%aEM%HU>Bv{4<;MtcF$RT6zf6EiSD5_t1VU&%uwg)vs4C1TQl9%oFjiqU=#}9e zIBB&j1DKsLri>G#3~8Y|YhgK29_~ztWI<)AaxU5pTdj-$3bL4mxK_FGlw!4tkfB=0 zR>>CC7jB$VE)o=M*XK_hC`=i{E=$0>#XzP_keiiqZc#yEqP!$s9wR0M=Gq7v!cSU; zq(cu%VNm3ppCKrWPrxm10FuOoDJi&UL4Hg>RObxNW)^f`$oq7pbcq-rS*;`lqt}t( zCQlKYdto6FeOed#5Cc{R<%^kx+q(sH2if^!m=nDj%d`wOKbO?$><0j!>^Ttbd>zL`vJG6n5a z6-I0++{995<&~Hh;WL^Pmt}G*F@AekVU~)J6Q=w*RcH)g)@TV?JS;Vg=#qll!MER` zo^4_P_*#PNR>HO-ygw0S@fEQuhs-HzjzA6`21)(+%_3-ziRifB{>$c?<#z~_ORV!Q~30YEtq^ORQ zg8mkSZMg(#y^^bPs#10GKOHz18>hF{rFn{ILDQnV=x)<`XeJWJ>xOu_oqL#fTs!jS z6EXHg@}e?m<;+V(ENISDUU+?;D-VkJD);Dy(w-|^4f21;#WWy)TLZait>gL1_i<(A?JJ%uR$cGlO`X_q@zIg77Xe||nMIJ@VfjpC zS<*}er?oUTvOhrbyc$P>PM=74q+ULgb@_L>Np-c)T=^LkB_5k)Ng^$!ylMgzFv>2E zQPjs-*Ulg0c4NJP7xyaK;oP1(ylL;V7jKv+yt1$%os5+G1XXk)uzIBpzZ+{G7INg^ z@%-nP3pQQw`L+Cr9<1GDZxZ&Ziw|D@x7r(~-n6OW z@WG^`dpXBWbB;$^oVZA`IIKoXjrU$4nFn84Jj~~G-Q}46wtjr;<={-#8{A-)W@Yr%m6!i{^XDVV z(COvFn>g?CIge&qyuUtZi0c>p-EmdtdH?*X$?K$vy~{tX<9y!4`9iZW#J&A>eC3U} zS>N_P`+ogxmH4f4>)TiI)uZLBrJCut|1A)2DjsxywiKnrI5b*? zDbUkSRNz+Cd~j?cjOt5O;<(e|{ww-^oqkh~-Y942u0iYo4arO-7PE%{v9s)KQ!%IB6|;=aci!x8NOr{kyl-@{`?6zkBZj`rZYOzMJ>+-F*Cr z0R5(O8VT}Qi^~M=OJ%|9M?&kWth-rhnIntp*82TN!;AALrK<4Jk*J?*ed(Zg2Oi%) zvc%{8(tT@=xvYylu@j5}FrfGdv z{GVIb|GCl$;1v~QXZ*Rn?oSCF@J_&QzqDcNJee7j;1b`E0pM4|V+xX`z#!4E;O7D%8^^NTv9ozSFEUy4b(dX}X`cUDM@|&G{ApXO_s>Xb*TlT`D=i}>PsJvkA5f`#jWl5aAGiZ5HqI45F+Z8wejPp*N@lbjGrkO zua8e#%qN^}96x_={6Zhv|1&y%>F4-m{70?%$16@B>vEKHAoJ0vl^6xHC@oPO}$m=cp0>r2|Vtbco+J4B>waJ^`A#ZCjj^%!23K}_j$bW^T&Ii zCr(Vz5E*J?q^UAK_kEsp`ttR5#^-r}uM`hdXMD@~@_rDY803y0oV=ZkQ5jZQZ`=V| z|Ni-98gBqBqzj{_*T&Y%{^JhR|Q#|>ZIa=ua(UtJG; z#nxr*YsfmM`+BSAYv9n=S&U(iGo06(%x(%<5epxzh(Obf0_8tKNy!;G5gEGc?&HU|7E%8?E%k|cZ2>q9DMmm z$Og;4Ir3_d>-M>%@7Z*G;3T;^?bfDAvPsyFWp@Ioy7J<~tV8R*R~p1YE?|TeY1I<2 zE%4PDfVtMf*rJ$xG$ny8lIztuR$)T3F&U{l$3{kKG9oRfC{%|VuGm#z8Sh2|XTLSvJ|_A_3yVQ}{1t)s~~Ds<0f*#V7r&Y4@`jQQ4mdEAlu+*KO}O`YXsf0AO( zSS=-x_H+Uc1_kGbEa1;1PQT2eA97-LNw{gxauLRYM_#(YWX4~=Q=9k9ZE!Q ztCw%HC?4M=r%!>i)KJG6#-k@YTkh=A@NT5eU%ls*?7$0C|H~)&Q=rw@epZa!mN&d= z@~ zLLLH$QGN0O|DS75zS((z1hK+He*;sq`(mW9?l0izNyz=tmTHF8;X6eDM2x(&`+e(l zty9qcJ!A{dwN0(}{VZuSTt|#FH0RY?cS{#t-f+9M-DX)Ox7?1(IUaIcGzN;!uCC|g z%fO172Ng5Sj!Z$F@!G`Wop8$y)dTwo#GT}t{B^;MnDe6LPuHw1=(&*iN7kqDA8!5H z#F-b8aYJkROoZ;&e>ry2^EkYMSXb|Zap#q6w6Pb=(5KDDl?v*oo> zE`i?ZX$$^;vGS1oHrgX!%i83-g7c*g**>@#z3ZT90!uh{ z?A6HN+?)z{dxScxK;>Y~8`O*tA@|0Rd5DSqnnpMvq#*jTLf%`MAV=;>z!*?OUq?BEX@Z}dB zhB51Wj{_lg^`^ZASnFA9LtSbwsGdbS^@HSDx&~;GkyD4N)#0KxrfOyCF622OhmWaA zAeY1U>>Gb4m$HKq5vq5T6_>be>R8uHT17N9b&`a2WA=pV)aaV#u|B8T@+eaUe~GC} zYA_4ix^K=6_u&1!)z!%JV53c26|Cni_7T3{RLNK>C%YR8o{>JS@0ouknW@9iP(N?Q zTdQOs-XUd~5&H-6nK5HqCAxq4zQH7n;h(P8oo;;ki=l1L(9UEu?$CpH^Qm9^>t18u zo%Pq{&K{5CvCGN+JzZImb5BP64f%g1=5=}NwUApK&UzQGbyLNAuKW5vtsGP$FwojjVPFkBXr_7~UX+p7N*Bh`e zo7=yZ%t~tCP-}ftY(n>1xl6S&-jOLS_Q?$3>2BP}(dvlyWhLTG4-zyAqD|M_a*KYd zy^>gTLNC->*UQ@CqZH4F3Rg_(ANkyws)_QGPazjA9v}O**mpc2c?KE^S`#trP0P{} z;~5F6MgD`i6IADU>6D5c`)b=Fd1C>XQ1WjZ2{rI*#NR2wc2cVJ!eaL4v38>fgCdsq zj&)!Be4ZB<^g3G{nJm|t<#PA0=}oq*6$Me;d6XRyZEK!{VVSGMrj%Gan}_qRtq9FFDJTi?`hcnt!RD#$iHy%40yOou^dhF(@x_?a*0%Ny)?L zMFgkxkJzQe5LSd(0oM*0shCQ0&u5VHh=#sym(pr?qsrV=$QpLei%diIW+eoX_cIIm zB~nOO3lb@dbQZ>7zQ3i8UYe`ql!z32lxS`i9aeJ6i8I&g@iUrQt<_q{qj%WmyjW0R zRiSWcX|ooK1y;SneP(D58Z99=MRV^Ny{E`+uerQDmx%m>@7* ztx{i>F@+Grr7u^NsrGJk2B}Y+3!N3fNIeq0w?OCp&=gDSO@#6~?%dSG+|U zOmgc7X1$UZTOmO<1H@qBToo}mNk@oFD&%+~K{>J-d}tA5oV(tEC(W}?tq=0)E}t%? zb_r@cI@COSo)^UwJ|13zw; z+{0F~C!#6fT7Wz(-9oN?N%QO1P|02Eed;uY>(Jr7cL#ULqviBW_!PaynczEcw)Mm# zlN1-^%)Ctk9P_sf&r;?4{Q3zjGyK@X3j@toO^VpZMzD*tW81IVRd}~=0a637d+dt_NW(A_V_O(VYjxwElkpKg;acO$vzRfgo(P}ESJf4yH{7g z^ZUQQrstgJIiKhAdA-lNrnbH*`xm!+LoIccicgQdKil3{NQQG`{zm{J^i2k0^+;^c zfi>0X=5*Mf7tm+yrp@UE@DostZ(e!`adG6GvbkAw@pza zuv-!5Q^|sQG7GK+|6M|C>E9vls%0$lvfi0%VkGoP8L`%+lw6&fb5sDS>*IW^*Y?Cr zEV-FKxp&+46&mjW8Gm;da8mH*p8|u0(%oEGF9J%|=e&W>=(oFYtT;D;!RDJ;2Vfm0 z**UG88>z(c42dSB<_Q1h5N?9GyM%Oam~?Dk+udblJC!{M(XR_1VO45=j}^jm$4C&Y z>E0<83v7m1g&x7fW$SyT5PJj{(GF4-zDM}G^jK(|gjHn{^~gd8G`uSGUK#; z?9A4X9@)5o)`Y;>%UXolI93p{($G$<%X(&FmD7F?Q2c!Oyw)TUT zt<5I)+}63rZ0(ydgU}@YrV*LJ8&hRhuFrX9O!o~_lXd(>F9GjPkko+En-M3G)|<3< ze7gy&dv;}PC+XFjQ$bSK6H*w7USpYZuWxQH;&BxA<(Vjz=tPyAjhTI_HrxLUvQ&hh z(H2;#CK?qHE!wD|6o&?Y)_0o_EP%J77^dlDZu7l|OhBGb)S9$c_fLrN9(SW+M=>a8LgBs)U!qIGXek@h?);UA5pRx z^y$T9Jgr9tidk{I0nnw%!(~A2TqLEU5^fm0Vtgs{9zyF)3XhTp7hy|JN>4jD%kIRQ z>;kf54ApmkM5T<-gxU()>h>V)don*%=3l9e63Ig5z>(Ks51J_v5_+|0y~dhaqoU+K zzH)yW81)iolXPTK;+zQz`tu>;jHN=Ns&-Dfj$D+UjnvAj`eSAsi4~eWL~UMPC{xku z&ed?c5Y`an#?kl(!`e~gA|1Yzj$ZPX+*uLRZ2ORIkqA?3=xGR*25!pIc#GiR;8yoj z{6)2MJrBi^LU4=(1Z2sA%wUr@z*0*1I;|%SA^3v9?HWRngwtCn3YPngm#m9#r^ibJ zziA*Pk=cxT1Tm40d3N%v%2e(d~jsHccmhV%Jf`tvn*G{q+cr3G=xkx%S05prU* z-p3r$Cr|SZ6c4!eKI8LV_#w)u@T1B>X%6lM?sH-wx~?$&aJ1P z%wN6sRr}fvnh1jiCpnRGM@{~QaDxHdH_7|2BlX=cu(WbzK<8;SdqUD?$ z_WA#o<)1>#xf&Z^lf6oMdQ)b@LKLpo%(+jnD@!MKy&wmxW45F}aUpTyWr6V;C|Dg5 z|Aw!^rks0Le``&?_ykCmCv*cd{^`dXfF0x8i&o;7r<%l~Hiu*cd*Ly~CMXJJl4eXm z5NZ*Q){NdYdCL_N&-v;biAQK+K&XWE%Wlyt0cRa7q)D)D{wE6W_K8}N%gNV8IOdoN z^~$Ye3`y>sM0k=q?;5o&!uK!4!CA7&>1X{@rIoF&fsg7Zt}U>CGRw24q9v;>$XAZ7 z4xa}3I4d%Pv~>K?Q*O=F&OQ_hMjZK=gDC@Z!3n?_^9^knqQMPFBL(tcK+BGnFFgbB z^l+dK;do`RY0pGW8Ro5?nAS<@LdZ0eZ?o2?a@R7x#JflC*(0HT1n}$UR->p5&qolC zS*~#R6C5{J0xN*xGjiu#lz$Ly4vD6=AZ|r%heG-neH(Z(ZCbO(y)!S%!bCDqaYSCf zXerGU)SN_$mUx@7#tHS;kybxbYhcDQcFtaL1LWHT`Hm@DZIyu2QFww_LiX}aTeQQz zzAcCg$c+wx)p1t4+|N z^=VSt8O6T@q~%R0%ODROTGlcon3&%BbB;-*jt*D1Mq%FTh|F1MnkeeutclKt?oV0) z3lZS_ZCi#CUG$Q>?a-~b(GzFibUy3KLuKS>|8fEfrf_-&p{X9uXQCn1wjMUp2WpulYDd9D3fkzW%qjJho$*g&RXD>XS zi(u5Uzi-fyOEMR+#KEi$Q^8IWDYlcrG|;O_F2>?*5IF3%iH@p$GDD7R(6C!1ly86+ zX5tPVZ29gqp;`X;KiKB?4&$ZU1Qq6;rrA%!qfEA9wa^bo zYeJlp<)LFeQZV?(pN);7r5#Rg_sBXm7S1CAB$U+CDGjF+yHHS|jYOmbuY(096hxuW z9N3rChWyqF)=0dYwE_SIvB3+GgeoGqF9`IQ{W>iNipYe{2G5}1n?*V*A>4ayHwwe` zZV^SY&`J@nuO=r21OyV0h2T;2zwQLwxARf?ts6&o@)k&(d*BHIfR=;&5dl<0BKRzn zhL|Gxa-0FBMYn}#nzAW#N%4S3Wn0LN?7VH^;-!Dh&?8TWWnezatv{270^}+Qr3&CS zUwD3Tbx5){2Gaia1I?|l!Hul);9|!eiBkqV!Ap4ZWWcP8z$rB##rpqgW&p3OwG}ws z*djbrkL@~|_KEMN>hAw@NZmLmdqbEyWT+LNfI#st{BIs4AR?7&`xlqs2U zUk#1a6?F#p`kH)@lb{zm!S)JCu$LY!JTvBMpQZ6MgQO;4+fjsIar$~yGU%^M+79bO zZqYL&7&460n<_%yE%?LV+1LBb2bA6eI9=+gIf24Ch;6;5^10;lA1vlKlo*YWM;zU` zGx%8-_dQ#mG9n2Ygy5)svMe;HsV#^n1ps;a=OQvs7Mdov6(D4NbhBPUsgw{9&7>lX zLPrTzemK!4oT_(uY1??5glmuo$D^$L*5I+UKU~y~-aQhs1bOIrrQ$_2H9;2ct7Quo zmMc*aBKJ={>O7%+O(}M4(ndr}NdjbxbR${us@bYpf0&%JRD)g0@pMT3QNU3k4=2e0 z1M+G-B~N>0Zo_3$6?RJ_o75w-%{SSnnIu}>me)F3w8Y=sCW@BYWs04e-k!jibEPcG zUxqX4qk1%U@g}Z8&8zPf&Hds&!T0+`)UHw}G|KL8^PTbcs=mtIldEX;Lc1vXwGDa$)sCk)QI0K)t4wdcb$?7bU@YI?woNoCB-pBTjtk-EpQDZqtdq zw3SsdM)-665iIe&XbGW^^X&{48J`%p>*tLynHx`B)p00ha%y_`r(ge|jOCY(lzVgX zS1iY_TI!d%_{qXdw4k%SZ_2aUSG?73o5p&}+kCa#O?wHOhp5V%e{xBScW@n>;NC?x zg*ta9%%1T{#`+)m;M1dtP}~pp^8HH>t?{rW=cJjouX}LxG%oV^CeMA#&HIm)*o4Hl zk2&Q$M|1~Q4Ok7j393!ot5cMTC+tX3nMa(JEH4vnldcgc6po)4$7P1RujQ=iQ7>(q ziHzLRC7Js>NeiB`JxaOpM%ZD~Ug_YzO?(pqD&BXCAgZ{L$R(zD9dk)v*}v>iCiD;R zpnQ=Ma@f$^ILBG3Xzs_a>5~$=*PrUHD{y{uZ|>_Y|NQmr^)}zs0$b9uj8k~;q&2sy z=ij+P&~xTjwYp?nJ@`MzNuACo@ovq-%S!D?sWC3fCb7_5;Bwy6hU~RO>$JEeULCtj zGCo(tPY^_V>{;e3WGqklfIm>}(X1x&B2pXnrIMz~aOLqT`Q=nmtxR_;cyIsM&UC|Y z`|f3V{gBVH16L4&V}?b#+9qtOhr_HTD>}LU(YYqcaZ5$%*}c({w}+N3DPFo_7FVUF zJdR>t-52JctKQDDgZl`!(Hawe-kZYCFrSQ9_q1V`rh4q1adk*g;FkgR?as&x*Wzp| za@`$#JPdXAS#K1GR(4=G8plXYT7a8ZI2_G+DVhr9R>!R*7i`#ZoE`aB=79RphPo?X z_y1Ow+m8!AKJUcd?5C#oB0EM^h0|Y$%e6YfvK5~D7MshB8CU0rx!SD1dUEjKibs0L zp$5U>)6}6oYzTbRflVA@)d`(vkWNWVJU|G0u2=6~-5DG9W-j%kf-WCh^P97&UPf_L z>?jbvbv|37Asa-5^qwQr=0a5-Kf38Z)XE!fwFj$dcsyUEgg5nZfUA%im@;{ow2*B zjn<1+-D2B_LAx%&VzEd7^ThqUhw;Zm6G>W5*6y+DzAPy_OI_;KWWmirI)d*Y>g)}V zpO+m-!Pv6I=!GblARm z0oR|*UpU`)$Pjs{9i`jicaWYx(ukC2>ue{M9kBnVaoOW#A!TnbYh$d33 zO?6S(r~llbog*>9`bLVI4k6l&#TNt|7-|1eE`WBcVuQ~UOU3c%p6qZFTVX289ECVE zi9)Vo;nIL31RqwNY<|<-cK;(Z?njAhQ8(GkV7cV>&Z2zFRVPGkaJUJ`>5G(LO1|Cs z(?v2;PM|}I;@;HN?<9h%^3-%+IhXDANrq$|Uvl=DUu@ZX)+U7p8@L3&&p`0^O1ej-v3l z<|CXd8V)Ww1|nqvGou71P;V{Z@=^5HYE-wd4`3Fx!%n#nuSx~z9u48_BpRN^g4h_! zZhXHe8FT!@X{qbG&b%720?`Qo`SJ^b^ zZ6rviGIL$^2?Qj5Y>te11swRa|K5%U0gg6;-c}hQf~c14nv25mX07rP3Nvz1LK-W9 zmoI>9d1{L0HuvZ}0XaEWg3mB&?8Slu6N*kzD(@ADBnSK=D!ezU_9Ahf07OQC=gCUY z``aLBV_YErvkxIQ3-t6x3EpKANU9R_By9TsT!9ZusKddCiPl-{%rJ4|F8d7->!iz}!R06m-c$%8Li1mFNX2~6#{`imCs>|@nN$=KQj=b5BmUHBYgl;m++if)v z{?Js)JLUEW#^I5?>p^KS+FkGYUF$-Ce&dow^}uLnOqy!11lbMtG5 z{p_y~JvddZ$hXxe|BKHv>ZaEp`^K*|4KFmX2C0uQ{pG1344w12lS)34^Shk_|pXH&luo} zelf*L9RtA*BkjJG)-7HfsHzef=r7H5Q}&o>)CYQHrppb)C5pI^bBj-@s{Yk`|5Ax` zdge(p1UE*eDDit0qm;xucP7w<<=&UXoDN+=Pbp`Gh;~y?gH%!6!gqHUKNm;D;F8JxfkdVjq{RYWM~t}l;tS!3U=;%w<2_AUP;NVK`4x4ELN z*{Ph8$-b@xNP4A%(>Wp{5zRz#+5o4E-Bq~4flJ+omYHqCKjRDmq*mJ9U;U(HoD=oZ=E4B%}J7}v<+UF@db?B-oyIlQYQ9v4u2)$~)6@(Lpf6|kdC zoK^#gQ-?R8jQ0jck70~bX=oZ5c9U{uBm{^eUHManNOelqG}Gjj28X2pxz}JL;^=d5 z6cpt|*UIPD5e5La7ZPe6O6;43O}&T$){NWn<ynD!>6<_>ztQGBE%ltYx9H!>q!30o~U6b;5 z!Uu{m74rU?-d#p{@5Hee4$GA*gh`EY5HSm)@;42fcG9V$Ph3;-6dnU zQD%V;Z3j6f7ljFou!OFXLMa0JE6|~J-4tmhci(PN^|~3p%87STVx^L#FiZ}w!((TI zp4y8%0TUw+A0;p*2YerwomGcP4i$d83*+{6u^E8+8xUHq^soLxL~ulHlS3%{5(AQ zUC-2W#@Pi^r>!rawxxTTH*0#R3`&ynUttENkY=2o5M^Nx3GEko%^0lZ=h!;0l|qwo z$!c5bUtG~JcSg1V(wm&`sGZ{Dc_``^2jTxx+sZ8DcwwUboF2V|`>2-tNkPMg(C=v{ zt)Ig;*E&^voBMacRPDu2hng0?jZU6gp1iR;na-MHUxyn&rXDg0l<76g*15?fj5@Uw z-vs?Kg}#&Wo=a@>!c-G=|3>xgpTji^Ka^%NQy14iT_iV)iwA z*=Hcm=>S<#%a0er2E^qlM*b=By*B*!0(Pz`Z4WkBHv4z4AD>>W&s-n9D6V|b`tC(U z)?(W?(I3*Aa02`HwTuX0Rej=HyEnLYfOm%oaa%(_)Y`Eu@F@weE}hc%AoGvCPUY`{ z0)7yk0f&#Zm-(pe0LV_8Av}NSvy+eEwf}l9*ycqVFH8(HkWz$>?fuKPHxU#T zI0a04Q?j`C;_Bj)nG>J+=~K9S_Hy_C_*C-)pNBfMgDDP5+gC!)1|=~#geexX4Cr4n z0TnX`B_`E`CKr_&R46G*?EfziCPn_8`?F)oKLOc4?|?hiE_pr>CMf!XB$v;mFWA+Z zfHImSoNs{(QQ~`xv#Lq(hrwAVO+yTJ?frDsrAe!lOZ{s3^M!8WeY49iSA~Uc+dUMU z|8UCo@^Nv`KGlRwEC=%v1(2%Ng=NBTKyevGE|*Lz2k(*Qn_TAC67no-H{08b1o(Dk zKwH7KslCzH68V?QH-)}UT_bh*W*~6GGJ*t!oOMoLl!dl3m(7N>RM1)6FQ*@;bYXz7 z!1<47TekNQ4bymLko$4t!r{xh;>>N+N7e-dy2JkbZ8y+;;~&T_4@-z zsT1+cy!&smUp$!s+?nTRK3ywf&BF(w$Kt^Q-&1pJ9vZ zt8{0oIE|^?i(&7Yn`_FjTI3vO-*nP^V0i{<#}l4XtaiznGxvqbR#LEYje~Q$=?Z7b zp4$Cxzeb$kO+g}ndlj1n{)auEGwlXaeomTUTZcnY`$}794LG^^EnaW1DY?R53F4O- z2<^319m1Mbz@B}DKyG!+ko6jT8 zouHl3CZ3U3p81|9eD!bcTSxl;BIq3vce)~s4v{lPp42W?fr7`xba+>mc_{A3uS2kx}ucn%QQact@bzN(5_}MZw;QEwx*Y94v z?)~HX1@Vn5svG6?H~w01Zg?~Idv(x13r_#Hc?rm;;-jc9G6*aL6i2m9%x?>CX`3|K zCgr!e8~C!8_8Ft?iBTPM@;m0WbSxb0NRR4Vn%^1D2NY5+fOM|UzZJv>D-BK%a;qS} zE0GUGV~ez=Yft{|{iAJE0j*AWyE^~Q$(DA!fWB9H=XCzhU$?4OTM-#U^9vtdvd?+)_#W@ z{mwUDF|lfYKj3m>?2X^nS3w3!iyHL5@pjTy3sqo?(bki149xg7@ZA}{V{nXj8C>{l z5MBfgpbqn0{+xA#H2Sh`-nFy$%_y_wj7=KXp6~CJc5)nlQE7*EWKC}wa75}{hqw{-i_~fe|f(u;)8bm2S4KnKkJ7v_m3Sn zj^Cclx&O+m`&Zo)m()LQKre4V{jU}eZ2h$FYV+$apQZ+W{<-P1^x|i4>t}DV_2*J6 zdE9{yE*$n0k?oWN4mW2zzXpV_0r$g;y>5Q_Ir8PP@jyWYyCBc&D_(lQ{QYzG`5nt% zpK=QveSeP8=$*58$qwTfySP5QZaew3TQ2{A{qI+gPuT{@w*hh6ro;nFdUOp73^=pj zyZHdL0Kxh1uQJ@keau@$B{^N3`t!nJ=iNq^&^JHtPx%?S?g6n~eBES`;tPH{B#F;MhgxjouR2`VhvkU&SZZ zwYP02{%)1ju3dUt>y>m=AHL4LeZM$9D>E@^dB=X*xV@5L{Vj6^R+My`+7v_Pc=B4k zz+_>WO>(+O*X^2BYO|>L*Y5ctOHvNaKJCHx*^u?TV%*0t?aHTab1zc|zeKS^8|Lw* zbrp!KLL1+7P~=5pn*;YXcDT3%CQpTz73hw+&m9Td-kh0Kv_#A5byw_QwOe$V7C zH{H^E2eyozW*_a++mb{L7wJ=Oj276XG-}W*Z^>juNl;aznJ90Vn?#X*0|ZP?k5aeE z?u@aOIL@k)kyv#SE@c67;g7g5i$uqo5nQV#__6}prUf;KS!;=O+;1hJv|tD?8WsO0 z^YL0U)y?gr5tfP1lPpsr4btfqt5}UNGxChxqj>jzLo1M)+TV_c&5|~`Ym_m1?}A5) z3hEfM*P^4tMUp?`;9`lI9Nd*zQRZv1YMpclx-^M$zsrQ5GNjZ`kGS%UO|wy1+BtC9 z)a2Q-*c}pPdSxPRW#q6@N>b8iJdNFoXw$(>fw+K$?3f-TeX3u)7{?RFD>7sd zXQVsRtBQhONpR>rb^6?&|0XWn*zrFTxGyF>T^6M`%h{9ipRs9^1c@J6lbeUw>4i%} z7hfq3C+xpM+M^^)=*oCY^H=lri{lQdLu{DD1kK%`)?TQ9`2I%+?0>2GqSX6F^w;U* z$3=S_Ca$!$CWPGWtsq`^xsIjOcP4)=n{YVVXhYcl?Jm_jp7 zqLy^3|0dEX^ICJuhXlW`NzDZlZs6XQ`c%@uVRYt{gNiMGKe@&t$2W5lg0EyGNw(%% zmrTDuAKzc%qd--Ni}H8VKA97)_3rr0ENiqon(;{fn7X1%_=McA(u{u;A2c(dqgkrM zCQC;KrUV2|ZX;&bYhr2Pu3YSLGyVdomocITq*HjBS?`+K%^7dYVv6U+jn5dqg(=`&#}%+tN-|gPW^l?t3pT0qk4yXW>i_0Pv3rMk8XThSgmYdkw;D& z@p4OGX6?+g14mykcV1Kc@oFw-JIO!#eewtAVS_Q~$%t#oG@{f}FG^Ns630Bh$Vk0QJTYHt7$WDt;Veq;yo~SxbZC$7g?YM&coBCf6;-0L5(~Il%5YM@n#{L=ukTSn zk`g7}(`v9gKzK_#qGi3&O1h5N&3PyFFn8~mllz18PGj91G^l=-+nl&HWR8PBzQCz8x*Ni~!S=V9BbBD~*H4i!)pZ`HK|9xESB znP~upW>>DS7d3vt$~sx zn*(iP;25AAi2lTh7+C=fr5)e*Uj ztq$BeMriX}BN4w!C%a>#)L$~&K)$JXeDfe~5zyg0S)p+w)!CjA6gWt7HNN?-HiemZ z9~#(tnU?POre3Dq3^3MonMkXLV#9YTpd^tiX=#AkDJikQE*GHe&y4Y@HG{+(Aa`fI z&L=g!kYqLNT`Xy(VDjsxCe2M^Imo&p#y=dAI(J*$X?snKa06;TWQpY_A?i1UwS<_k z&dJXu(2Dm&vXK=Ry4IvSg$mje1OZ-uf~BOg*)FXlDf^LiXSNb)YdN$lGVv#qQzdnI zfMr(+`gbl(wFtz`pif`pbY$6G(jQ%&toP`S?C3gTTt=5XvXQY7b%n=vrrwHJnW!ra zJbDD(sit(+?g|J_r$0hg+Qf76k&9Lc}1a~d7&?d~Fbub{bn%;Jg*hbwr(k`LzVc0%Qscn3e4h;S* zT3K8{3}_xtKc3P`4FSqF0IoP={PwU`mc|An9;VfgsqM;!8IzPsc#o3kAu?h)t3v!Z zc34;k8Ca3xJHV@b9g({ma2FBV&;Te959GscbJi!W=(>gBX9> zf_1YpKbR@=ln`67@a349{#B8_Kovc5Vw+M={b(hRxNo7QLk@`A@+b91CG^HZ_fg>6 zzcCWcke!A0K|q|ZAU+f`SFl#Pl!F^kx)GsR^rUMW>_mobqqi7!M({!*Em1+JGBUc+ zpl`(D(&1HS<`TavsSx&vuo5O)p)e!7*1)J!(cb_RhDQAhf53+~fyT**>AMWWvu1BVGsOI)?{>8_!E6rXC?;NKQP&v&A0TT?l^fIurmlffJLx-@gMa85m}uFBFl1puOrG{M zj%#M+IVizBD#lGExr#*?Cz07F627BUyn=`^kI$^*B(B7Ff|3F8qUYUUayjH6pncR6 z=5+1F>&c%Ll*tJ6HEzr4eh|YX*CXVEW`b~x<%7A44*}|gy~IE5{j*kpSt^D}f!lxE z=8Jvt%-^=bk0H69nEIH0MvQNKD;30JMn+>HtIL!2M(=Q>f-nToCJJCbJ#|P=^f1zf#CSUa zAr1jd21Ysp92I1*HWO}I8Mz2JGj+v51@R`r*bN|l2u^LGw6dluu!&nU=m6F^^n*y5`g_8gd4%Q0V{2sf*RG#P#XAm4UD@8?Ipt43q+4+)d>$kQ5Z@nV3rw(-z5~H zk|kE*u8%p;URo$~l+fGCRjpQ9dNP5&p?bkI?(Ql-EXEx}7ay*{-$m)m5Xc|F-DT0I zq4cB8(D9;@C&uaGSmYN8G#D7I=<%=;n!~Z|%@>3&PuMpz>Hmt$*0IW(1ao$v-FcZaPmsr>7k<;*=fueiqeH49qalZ~s5`yN+aXk3H#O zW29oRs*N!!%!pfKA>&uzQ;fL9VMOj9_=OUEwHS2Or(z=I@82oY#{`h8!YdSMSf>KR zPeKKl8f?a=sK_(LI9Fq(oe(}CC0s@5IT+!_I+4JhQk)Jp2#(+Cff`cbtHGPsD=09#~+R!f*?74ThF!ZYLMNEPm$iXLk{?&}KkgcqFj zcK;@kF++Hcq1459rO=uuIv(m5@3yxyp84J9}z$gXqXAw%Qg7OAS z21Lj|tkgUM@%2B{SOm;6Fm5SuIDqhV%u4m{)U32HT8&wC!K6V8b)w4dBEr~XhKv@* zd~8uE$v#Hf6Q#l=X4YGYd3x$s0DF*t!&XK;0s#p5J3?kl+~XCLjZP&YiMS1B@$a{@=4>5d3 zz*wyy^eL#5(Y*DWFE6TwE90Pm@?Ev?E>IuTUFtGi%2*z@zl1rx6~^4i z5;JKBtE^-U#mJVE&+18zM!-!Eoy!DjEzHOcNMztfnIVRN@IDjpYK33u@HNj*StYpH z?(p9ur;C3xRR%Uh4Z}NGjD9oi5O#4VINfW&mI-0-IW0s0$@@8{hM8ej;HrUMts+#I zsjp4}fBqH+N=WQTz)1l4=)vVI24+)|t#~tw$`nA0_tB0k=Gk3AiBdRCQc=n|F&-sv z`iR>#XV1b+_IMTU5Q|X(Ktd}tPsf_KLcUhQ=riDh0C*Nke;^>e0Vu{&(xde(CnLVg z!YDRElZ3>>3NjYg_)V|jS#fy)QgZ6T;XK~ z^1r8V7Aus`XoM#Q#(FC>|9Q$v5lyLpzW&XuzRkl-;65`0>(({s8F%`0o0NBD6;eXWB%6<}s%;T4R_JY`6vRRC;d7;oT>^NUf}s&p zZ!b$U8gP?Uv>|KH+feFkRm*1unZW|9#LR;zFxCq%7C*j)(u-N3LQMT`Wt3j;eQ%+> z?|~I8(jI_1sHfa$#|>I(QGdZ#ER6IqG0e6Wu2$gJTBzSK!WbYvlQ7HfU+hy7$p*qb z7QM_$%oX(jz;Pxyk+rm+ZnRM;IVyr%C_d6kL123I9G{weYJOO)UZ zRNq7je?Nu0J8AV3gi&RlRcwL#51jaFCL%_r5j#Qb&W?(z|BF!euX0Dh5t7F~C|gB+ z*URkfYM=R$RQ>g&Dy@^tqWr6(oI92LK|?s3H#by;s}#^j&CGX2OwU$$bqjG6t&g_6 z91Xh_B!!bzgd8R1wuD4MV5#c1nRWZRo~EO*=O#iY_H?`Hp(5Uy;z{xD?(H3%!0p2l(;|vhn zl$7ZPz#r)DOyfZRqkXgzed_#PsA`_E@Mk1j+xS_t#MCbsF^xELx4?P-;cu9q5iR%| zO6FNVCdQevmBhcSv_2&@R#g)thF2=FjTTN6Y(4llbnI`&+iSgFQCgw_GI`)zB+P>f zTrW7As-XNp$X<%Ue=qu0n2y@{5jg_zmOv-8;%-^!OIgrGF|)76XEPXJt0#U}P`&i~ z`j=GSr;nR(#@|y3vQS; z-f#Gk84~zYJKun&e2Z|P8PxZqgGntIA`dq+Gf~#cls#XMWqo5N? z|5`rQM^#_S6JU!7JSQJsz``99Fdr(YR|Jef1$8IlJHN5@l>)a-!1zOf|Lobu`p(#` zz#mo;(pB`xd!@sFm5PAhcNj>kRKM5D!47fKi7^#-K!u4(W(~0wl*^)2>nA&g1C}qIYze7foI8~b1$zEJ)xShfYcvnk^Qn!j4HVP^PKRT{ z0qH#U%XP%V25LXRy8R%{BG%rTq9fbx^g6nFH-q7m?R7N1yS;iPZG86VzBhL!Y}zkF z(cy=!6W4sT+hm-0?%wuo9!OE%_UXmjb$;=05cKvTFI<9Ebtkzg(>>Vfj@f79jOmrx z&v-QNn9uXhq`kQ_I4xLSmS`*-D@4Nrj_zqmkx%nV5W1&cjo|#+GY~(lbzbiH;nR#} zlT)OdkESg|ul1th1PK@{dHQdZOUc%&276_((KRX+8$hU1($o!)-yHCzJ zw`@j$uGw?{f^+`qCYVfG@ewaKw52uVbfN%DYV(@*+gCvMq zH{cGbEE4Qs7jK+;t5ix3`mi--by0*}ZXmF$&EXc&7vT@x~oG_`PwvJ#PMxKtmFFl}FdiG%8_ENPTV1+LWsP z!ff-&K^Q!>!jr-kkEM1V94`_WIBW9%cmvbogV#*8iSimem=?i4ws*bqAE5Ag_BzXs zJYBd&CmvwO0@S>&V>L6C2UgDrd66?)NKHw{YqURHYs>QT$x7JUe1%XT7|)L7W$f*y z?4P!*o=R{#e=@d|xwf3R$1S0Lx9g$IM@IpoAk(cntt3-8k$VY<(@YP4uGXy?U7;b_ z69vJy{=4mVu1l~k&WugH0`9ihZY9ipA0>qX{&l#8u(O-f zcWgP@eX)3}RVaH}d}-(eFEvr|Z3ZDJJ!Zm<n(2fQ$qU*8(k&Z0Zv;A!N?@re^YX%v zP%HPT)P+vV^f;#oe^gD-Wm_%%?*3Cf?0hNHF1oUtN$w1W>jAHerGZIw@`670F@zT3 zwL-hR@1J8Nps4&rS$UEL=e)Sg3)LoWvZ5NhNWq{dA;~JaZSy%O{baV7jLmi)DpwXw z)d+hOIr_$bVWV>Ax;xrFh%9(>#MBtB?Hb7=L(Ux%M9*?h>Amonzn z#tCUPAKAM}wFV8fRGqsEEf`?Cmxa_NdhWO^^0Yg2aN!YQ&+wmb1c3>mtHab5~~&=9+ccFAlCBQ^@QJx+-PI5ti>{wJ^Y zyKVPgMze(E6IM&D&WydY!K|IwE!ZsjK1vIT=?9q6lMV-5e>FwHOcHv5Sw+Ns8| zz)lY^c6rGi#BBN+!4lYTw2OQRrL9pyw7S$dpD44|GdPL1v?zmDW%S>fE3g?HBDl4T zC%PFd72}LClQ5%p+NXk?xYaYrS>23eT`l8TZd{7iiVvs~Qm+_@9+hUTQ>BD<%NQ5> z3sIj)Kgs!#iC_A6UDUvE;^kAtE(;FNy*}%sn}0swK_9SdsY>o_ zP}LEk#3R9?I_GDZTH;IvKBeUd-z4sgT)76|bg5lTSh3u!U}l5?gct!n!dGoma8JN| znHwi=7w(+Q7vT)4#L%%4OUXvkTZ)%h&D)yD%pQ5;@FN~a;E6*M^z}AB1Tj#Gg0dM4 zq#86510Nx2&ZpAH^+`HI=Kn0R)p^jmhXgv7PjC^}g%a*W{z(3Dw>05oRF`f4mlQO zTh)$5ZO-E)(v1JrLGHOQe||jHE7iYqr7qpi_74y@t&y3aEW)h@Zaq8Syf5h>MNcU< zkvbc91#s`~4eT`$GHaQ+@$k6+thd~QQ7v0hH}106aaoj@s4R@&x_N=Nu>m_%zRMjS zRZX;;KXHt);+wqp?W3n<*=+x{l>XH0mCvsISgW-kGO+&U75Z0zI~L{iLw?vrV-ve{ zQ9c((y{4w(rYY?Cr*Q=syq}|9?KAXo#rl6F{KD?CMOANT3Bx4(9Yb9BW{%xFN?2R5%g#;DWi7B+$vXx+X5V^rZB)I!NBRyjj`cMlDnhtU*h(w zCk=Gdv(ZtK-E{N5Du=gXna=_)h$X0>+9hCjP9UiUx&VMkmF^N7B=IM`;a zc|UNeH_n+SU_Wtn8`A30$Iv-OTs-oZlj*NFHyd8O`t$?8G86BQsHsW4{lMYNG^fmW04#$E}f z=l4zb?$&}eab4Wh#Gfyw53P9WhZm;?ka=A+js@Gh3g*P)lUT5W^^DHz<~#(Zh858z z+Id}Cat?k0iI9d~$63^N%}@%-G{>tTL9B_}9L4QBB0e%j7p5L_v*_mK5&}+ww-bv5 z7~WPz;^_gGk0uvf<*X5$N2c0N5AjAdoMEYpFFP_79~#eoR;_bUq9sL5DbGut5p`m7 z!8pl@8iPjBJ8^zk!GctnV*xpW|KsTH<67MRKY(A?cI~#^w`$c|TX)u-rrVWptx}Vu zQdvn>DshrY6320_-Bd$T2}80HLKs4a4qJsC(utFVux=!AoZOw`IL_&JeSiP#ul=#@ zaXt3AuJ_IJg-y?g(oYikgX(3%@?}B|Qy39HShh@GChow+a6n!uHa(#{TU<7)`B=j+ zDC)qa4XT|7LEEvNdCPv%D3pE)xW%w8QL84Q$$fxI%qd%1U6v?QvGrDV7F8k#7sgO+ z36wV+1kQX8r+A$`{ImFq!Vu9B$*bFzXr zvZ!2!lGLYkZ%4qvsCbQ%G=WUBQta*YXtsya`Y@t}i5!NKkdI+#b$y{RC}hT;bDw7f zc_MJ{c~CnH*q4HKKoYZ5X#+eU=#@bn{DK6)L9g7|2wLk94};R$26SK?zG_LD@%yc& z+JpT_JYE4nQ}_1OSJK3Q(2NjEm3`<9&X;?^8-HWK`=~-t*n(lp0qfG(`}sbmu6#!1f5FT)U;kMM_7XKeC-2HU z7BO8l@i6Dv&dh%w?y?N{JsrrPKJp8Qe`vhsC%#cIUpoVJU=@T-8&Mn<6<1X`H(7F2 z{>ZJ^dZ{8#i|nZ1^%t9(zQ7iKghP9IToBuT)RM7 z{zH`XTf+SWnKe;qr&IQsDLmN&mK0={f@>FO?MlaO|P^AjxF4xWV8evdbx8?+2iJ1>V~Zi)`rwuA5&Ui5-%{8F1~5C zpn#h5`N=ZhT|+L*<~&Jh`@Li8z%Dz4s~>ZTc!f-8HqNWnCr;$>x1c4E$P?i zJP-amc`VmDWBK!-#AVm1sRKsPfBEzCXNZ&YpJr~%e0OhI(aWPmIlyABn5au`zOZD% zt>(j}x{&)BuNK*UDSN@Lj2d7(Z#e(Va`Oc{BgtMr@1KXhqrYc@g|iJ#mVx|CtSxoo z*|OLF%2uQ(C+8DgD0hHX%YB38F<k z_bl5oZ1>*^4U*+>`QcT?i^sXA$a;$Uupwv8*UZ>V zD);$LYcs%xmEyzPz|_&8v&x70AbUc|TL=u!JGK-l*@m+Lth&$S-C zx!23GGo$61mq$8oW><;9z!pf>f=e>11z%EFX3QKZF2RhQ_K?M`EyKk0L5qx<#R|wu z1$qd{C>a_Ct5@R^iH5RGrRo#2z&bYvz@QdQD8Xkt z?Ml@piZa2=+_F^a!p!_Cb52!hUZ_6at(wYFKyk94AN`~8m0gUYV#HsdP-4Sy88}O% zNj2aW3e|HOG;w{JI5ReggIV^ctfKej^6RTFe14gMAJ4EIzvMo?OABNU;`0#iO$NxB z-zh+F8{6^g3=YoPr@J`2Hz+DrwUieC<=JzTeufLH%y@L!A_~sZz)GKq@(ejfUkof8 z#IF+L6{UnYj_T?nkZ*)GPL$^>DyGv#D!VU$c#u%6C|}E{$R$F5D;B38e|fWEm;&HS z0Bq90>bvpU^6PIe<{gXCfB{DI)FFJ|r?PH$<5_SqC(BnQRQ%9~y~?K25p3ZgAp-_( zzX5|3<(uT?D^XEYr2Mf(iTcg?E%=B@P0N~wWdj8>dRLG0;u~(|9=VmPk6zYzR%?xJ z_KPt5FY2`@Ro=%~lu(h;f?3F~DCB6U2J4_He3nq^<>i+Z$_QJ0x4-xe<2$GdEX5bfo$m`lPhHvO z78YLCVT)i^NeOX6l|u_QiL=*1R8ffP(Pnj>T(!7x&&#~^*pfoOA3G+lkG;M=HtF&7 zP2Nu)Sz}%z_MZ!oTc@xfv1yADB5ExfJ}8aA`x+=U|5ki0tXL%j-G$iViE`6RB1wTS zX~FDnsANq6(N!wu9yM*yiljsJ^fG`0Q8B2Up77P6I5O;PBW4U4_)5~;y0{+ac0(uG5-@|0e?LSJfXFNF#%@v$_ae3`3_Os z6gaVdJ&NcL0^u>lIe(RJ6kS|Viv6W!-6l)9=l0OViTj@Pz3`_k_0 z%k-sJHl?|lp=B_BV}6-X_AtYO&yub~08OC{!)E$FQ~v!Ci-a>@$Cm3~F1?mv8=t>< zsc)Q}qym0XRc4^9kj*fs{n$o-B3R2hL~CkfAJ_7pn&uqEs5@!*jJBQG7ezU?6gy z`l?a%%$O8!f3D^x9P~VSJ!S=UbDweEjgCFjF?hl+h=wKo1RRF;bf~KWvYHcVemIT( zR`?AzU&8Dt#zqt4!hfR$+!#xGm?ECybOyf^f{|MXnQ6^6)9tuN>KOm+`!g5rdub>4 zd@KKre7kkeEqvsMse~U0do4HpMp{);??3oQV{AONxJbtCHtbzOPP$t8oegw;MVG!;k}R%riNwT zbR}D&tjDSkVz{E(bvNA=Me*;-QmvQ?SQ#mW*Mm-8VX5yE+$nk5l?$1E;ZETt3v7MD3y4+@ z16E5o%zI(EQOt>XA@Zra;Ttug2e-{RX9=$go0w8wQQe1lQ7#AUkom&r7lo`AGUrzX zyh)qSPVu=|FHp(w?aVMM8s0u z4H=?=8mp}KqqgZ{kT#gQb4#&nA@eSFjso1dwqvDzkx)WzX&L`o%o?_mqMbr2swIj>ZsMaSo8 z@=wBumiTQHN*jz0JXJ$oHPvZV9f2~D0;bT}tlRAQd@rCK2p#2T`lb5n97Qn6cRPs&{p?WibMF*j%9KEbu&&O5-nU>R4 zDk8g)OtHNAl!Od+{dE*FQR>{(Z}Uojr+SQ<@xB|prEmUF8kY=w|sS7;p1ZZnUL77kLeH4 z^r;u;@4dd~f5eJ98oJ?9Z^reDjb$sxqh0TbzfTRAc_|K-#7y3+o_-$a!#tcaf8Bxm z^|$UFjiK7k^JDatbQd2DTsyfG+xMFgx3O+M^*eC#?}3`~erd|jaH{i;nB>u#vpapm z)Aj`7HlG~0(b;qK2=nRS6+wF2v3u3cDc|q@J5d!{)uA#O0O_wuE?XFPLuCqLfu zcg)3~zP)`cLWagOtu8%XTTt-yALQw~%g3-%Cy!P+9^))FSrtyHUBib-H{MaUR#%-i z!^5m2;+x)u?{}^YA9Z*kzCHWh``zk{QHl)b#bJEdbF|*_Xui_+lfPPfB7EHc#Fvfs z#vLglWH{hO=&#>jSJ-R#)W_z~o*xQ79bFQ>IxOaEZ|)Q(W3HWfM1+|pXCkW8E9bmP ze-ZZR+PlwZZ`AOF#bj^8JbY%9kr`v#9JLBZW7qefS zV_4LB7~TxKq8W!!Gl~!53Sf%;FPP$s0b_ z>^{fr9$w4;ev~^doAYMbA73AJ&HeMjk$)$DO{^-ekNn*BV$L7m|MAZY?7T0X&U4@I zT=#9%b>82XuKW;M_s6$aSLXiJ{%Fpp)9e0y7I5V2qZf1kx<>#0b;yfjSdXLR25dp{NPP9pR#Rt}IY080)!SU?v!24(*I zr{&l%{=YxK-mRVW;c{HM{M+9E2Zif2)(GzeKJlQ1)MWYhAki~n1Aj)W$<=T#DKRx;(gDcZE0lZ)Z-BwGV{Uw z9X@0xZEpbISO!*~bE>9?B`{*MI1W2d5Q?{&b0+k_neD^HEfO1=;FJ@?F4Lj?J*)A| zuoG>!ec0fI)r~bL1-#6}vrGY(Rn7hC=!D5S0gSki30EJp9A-Xt#$=y-?I6Hc3;yK0 z)aso4vuBKyyY{l&z6v@vyQC$IoJkw8O-+}rZmM3N?b$h!fw=f=&#qZ8?ZRttX+75T zUf%Q`aBON%_aTqUdPck@O=+Z|jm!71&3LrE?56)&Twtb+XG2-qGb0i@D#;i{J`V?f zfP?py1)UvSn{jk)=}|;^)RR2o80)a$bJNtfkw$1#^k7uV9_2ZWQr(XF`yZt=yGBY^ zrb9X2iAU39uE9I{LrYvEn##oZn9$Dhi2sht#zs#6-Za&*a;ke}6ze&?;byqQqT(9d zaM}Lf79|lYSME4~#9W8*rN}m$CqBnzL{0a1x`%E*z>6B@TWO&3?)@>Un^r{k;$k=Y zakg_qnN>T$>_%u4Mp_>dz~YL%Tzd|r!1!kS^MKVS^TJu2E}TBQG2x#Q}sVqTIhb=mA3P1E}V* zh$bAh8(#JRp>f=~XI7^5xGf?NTN&ey4H>Pzq&y6D9p3wFM1M0beN?J@xPdYz6FV)s z?h(-pB)ogleEw$KDcj<r*I{CMjM@kTz%q_6cj2VmcS0e2K1LwI`JjY&_i^W z#b!{+l}eyWSDBnrA;#hPCN`w}@$)pyXhsO~S5}aO-nt|W9-*UXbD)Ws50KiCWEwZc z>!@*gF1-#ZNtGSb8`+f+!ct`Ewu$fF8lBi`e|8t|8<1YKRB~_xQb@Qx5>_0%jjJIi z8=-C!HVz?=J(acqkQ}YxN^0-qife`V{_ct`O1tF4OU$nw$4qt| z)08-JK0~Txjh$i(Pn>)bwSL>v^&YoLQ4&uD)(1us!)CxATo)v}%fi3Qeh#-)L6%BKFEkO8Ub%<6S z(!2m1jKOa;`6;kB(j!3m3ENV&NQxMP4!UkLvQcw&PL~(Qhav(46eyk96=>}2#OFt{ zK@k*bqldx#&HxUUGenkIjQHfvAV|%Pg3lLWQ8yQP&rGVlNvnrv$*^;c*l<~-D-mRl z_0qcG2O|?wzV8Ds-}EZjwO?p80aKb^lB&UZm62GOlI}76`)|B%nivz_9XYQr;(o93 zcDAl+N12sEIa3ztPw5g#JAom0I;#6oBK;>iy$6lBxZN@fLY6CiKBfE>H!@ zyF}d>r>U1wwNW!a0Dj3>@s!m}J>uaG2*r^hIch&4xFXFsBe_njP>VFC%mPHfjdWRy z6}Mtot#4&@fHjIw7kBweWDfO^_Ml0EqlA31}F@j*q>ehH!8Xe(p*6k}$+l#uHGUsCP2!WILYSoT-9@9{$s zOnJnu5)=+Xy5QWfhrf-+QC{QHUVXeKg!=4tt(CLy0z8}$&9C*r{!f&uXnoFgMuDkfdYnJtAYzu z)&-dVd6Gus?GYH)p$gV@+JF1#;Ul?HIX}=EMaz|7oDmwVNDB(6t%d1~%;7C($yykP zVz=L{XlzVysfwX6Qsk>@90Df%&%QKwdK4?&de(9&z`lKD@AA~XKAo}TjCy~9$rxiH}C+D^TLEi1#-ES%GfQk~AAH&(X7JUbV%3eS3;1JwirE-wHqHmY zRuy>5(C`3rq@$(#ADW6|>hvtC+mFl2A zmF^ECwHk0>M+!Bn11~x`-LO!G4gZ7Uch?jrPQ9FzYa>m#F4bjt}x+S^dOjB8cEf_)7vAd ztw@KH2Kv%DyWeCcB{^td_6(z&0&AUy!BSqW?ZlIy$k}xg;^0E&V8zYURb-)ZRx5VU zhrr;1rV6Fs|Gak?k3HSLH&{^*SQ{b&(QvC>qIB`}D^iPTR!3xDvpTF9!_HadrTI6g zv?~lyqp=6%IEkl09fowx7Dw_50Fpr|ZjKB=A>U**uMW`!sAnff2DV2AyLHBXLj0Ob zO6D!(D7yRu&GV`O>ea|VG!vi>%bOAjHUB1~es7Be{|R<4=n86n5WwyF!7qjp*Euv> zJ-a~7@3_TCP>1<+1)wsjn~?nm}`D%QL0dG2@LK0R|- zqPmTbL8tt>PSBm*ZI8YX^+=$tZ(*}O;@*o8{nX6g2Th~5s^py~+O2`UdgZ1#au6^i@R(o?VAmuyo)rHe1E7 z%x7|pdfP27_am(G`W768tP<9*vY*prIL3?q-wfiWqWck~g#)XP0jxpc3CFlL-PqxD z7i5??yHo43BM|jSHJZzS>%pR}gNp>jHJfi&*)bx3{hNIbWY>Pz_YnaThBkXt3)4@Y zu!jDtY1%OzKa_Ti?Ay>jTt?x_k-_YFGLT%@O^VzZoVk41nS(~RvjQKsH&y&7)s1lz zjeRb=e337&G{kb>eIO~;{HHUIUmeNK=@bq_QPkhZ4}Mo&(+G*oucPa{CS@}mNS&3e zVU11Z7w7+HZbuAGRpIn-Q~Pm@oA@c&TZ#P2XK-th}W&$J2u-GpB2*) z#rNG0EP~rDINrX#o3m~g0FttfsS$x6H?9xo=*Z^4`%NB<6mR<$eMD`Z+xe{R9;p-Z zOR{~k5mtWq0a)Xf^ZJ<85`7J%ftXi^HL`_U*GO6MQ4^%X4YCqqc&B@Qg1&Ba!XPJ_%mpeQcMtFA&U!T`%!4>*f zaKjRJ$4UXI8>U9fxeOK-A9Z}AiwOzKf)WqQtuD%=L+S+;4%KHvy{3VP8DoiLh+7#F z<V=`RW*+fr`o)0~HTo)0z@0@oJ z0WTDn&%R?s7-J(&rDhm{XYEd*Gy*@uG2Rh~E95rfbXW)zld3TuaTvSHjN_tJ?}>DH z*vTx)rJhZ!V$@TuC(UJCastBU9EW!E3I1RCAc!9SGZ>Lxs?fL$gNot~oAH+|R-4eI z7|#hw;PR}UtG=N#vpNjv+eWId)JVKLfuFX%)iPhrOWsP~DU+p7L5VF#+4k2k{#OMR zvC1CD->Yx?pPIVTYUJm>fp3Rx191!2pw`l0g4MrYD4B0tSJ9MGSqVdJT?x(jGDQd< zfHh7AJ!!ul=W7uJol3jszM~JY(lXRej4ZIxa@#Xd5z_PT=!uoGL-=gO&ZSaLJ)xgJ z>nlh$TPl4VAQI3Pwq`r+<3wO6>#Nc)8Cd^;2UGJ(hulpTlKtW1ysL>c zUk#kUbP6|nGO=nNwpq$95wZn;& z_R_1uX;;L>;ax(7ShzWfYlQb$)xh3EjTOB5aVVuUI(U2|viMlV-p_sAD85T5w78%Y zf)e7rrzJ#L@ZtoJosqb^q#gV4oixVVpHW^i)hUf6Mu(mPuv|FH;e!P)ik*c`z=NC* z6Vak{lZJs-_a9c@KAHG$)!IP;_DNEw*BH2yUk@+ad=(!u_k39)e-t}o>QTVlzP@zr zC>8hn`>T(iiOT##1-@dH`;dvey{MAvp*1sm078QPsBL0&T2NmiA*Udp2=62IM7^g; z+_Ig|NFxJF6QPF9=2ZbytNA`E>(%v5C&Gmi*Sh=kLsMPOOHQW?rtULu+?xhAgcxAq zHzwJ(8So=2CB!?1U@SutvPi#u-WTQW3k^YMQk9`LFLt;&aFmqwgHZ5*i^^5eOHS1~ z5v2?pPThPcOjJ2;)>HEPFWK!r4A44jr4jj~ENtJR#0k+sPy9m^ofb`+vu3VwX z=8x(w$$!I2uP+fi9f0|@lmT(7_1Fa6{5esHi0@Qq|Hy?6KNW*9@zw~i{k}VjTc5co1luZaf5maehaFHf zu}{KP2^dl&s{UI0=6M?Yhdq8&6jmKxypVXQx)uRDv##^87G9gPCA423_Dm3Qj16|rTLUlj+|KE$lYOaRsgsjn4yq7^bidHCxEWQ zbh*Ik3CPx2pawL!OGKOg4I!Zm83@2e%R+SsaqAla*vIaw zggP@Gv+BN{i_fo z#zsh-c);#(kZN0n5woYn%rWnc(mgmM+3wHh~Fl><*=T?%2$^D3H${9UYtGClXr zyH5UPHVlQk+Q*qTl{~=Z0Dk*m$70jWD8M^jPI53=cS;>xK=J&{C3@S$J@InP37?P0ytQ??>ty(9u7!N?nVc!+*k>GWZ{ zV}GOLnygIA?sD5aY=^|D)fJ=2azb%LeK*uuJ@YgF2|Ac~9iVkv7#kAFdCGJb%P(#& zw~uRW(XC#lmsLQcZ9${03Us=vv>9}RZn%XpBk)|g{q*Fpl^Y*Wn0S+N?2rcipacw% z7$unlo5}zLrNeOw8g*}quj}h^gj?XTt7RN-%zp=8t-USID zy2$O#!cnkT8XwyPWX99+s2@$^V=Cn%C?FaSbK#x+a3UIp9 zC_O54ywFH?liN-YHRce5^1Wd8n2E(eAo6PmuU^M440rVomsXSWLF8nD1)pd1OwXq5 zi*-n9QU8GWf{1D$ZJL{4HSrTsnJ%EhgI?2)ca94y;uoCd# zp9?o$3}0eQ({fFs@L`d!}ehkE1yfDVU=0odWR1-fD?EHT0y?xZuE69Z7f>)+ zj(3#Qeyq(tTSh-0%~}C{SRM#^zZ-+zK7mw|K^2Fw#AelXe;N%zWraca+EP51Ea#u}|zMo$r+IXTSvpr$$W5y(E<&|dq>=NywhmI z%3}3ifnxcTeU02G2@PrFia=~Pl`9>$3N*oPz)C;6@s5i>^V!;|_=T1uY4XU=(n|g+~75ChzFI$Gt{ezU8HS zKJGFlU>l=PSk5!nJjA)>jZa21%?LSJPCzVVvxPN~M;U<0*>dWqEbh?ziL=>)MMn3C zMrI2iA3hLyaHfwO!S}(o(+&cf(8La|^{wS|Yn6;vKD({azQ8gwThFMKhznp`$tX&Y zutxZ_vqjeM-(}XS%&uXZUO8z*2aUjvZL3IMd)a|7)FR*1YQanx&qm2FHY4)oYSpv^ z$5mR7%h4%w`FRAZ^Y@81Qp1(3L5%mE2klY~A+^Aslf~i-n77KHoM_G%VBI6(e7K8& zOdaLU}B#B zl3OQM+{mqNr1^l{eIO+QARgCm*opibW+FWQHS%pqR;=D;FzcYd$#Gz~8_~1jVcIlC zV6)~bt?3NZ4Ra`QI8P<)xmd1;Ng{ynP@h9U+w} zc^}H0P)T_1d4j*d@$9gZXv(1=i#QkA7G?UAJp!Gz*i9%CzWC5Z$X=9@ohsvP%d+Z! zJ@Y2YNK3d)Xro6Ty2Y}7#WAO=AhjLBD~3DjO{k|$yO4{ODV?6gIQ5&DQ6WAKh|QnF z=>HtIztJhy1Xczx?=)V1X|k=foVuxZ8kabk)47wt)8hv_rsw38&$&EJQJy_2ajKSZ zHtQ*WHgcOE@!ADcmk9fhvu{i{;wk~ISz;9rV0cQWUNHRA_csK5n?FHojwS0Q!c}Dv zZ~fz#duQb)1iGH(+*>33geL?e*77Fzgm)|$Cl8-12ciDYGp(}r?=dZ_sfdczzts=>Psp(u7J`o8$ceOrA43N%Hf<(y zgprsze6e~nd#3kWoqtdiY;!P+!&>M#ebqvU;=5eQ#t4=B_V6#T?FI1V&{89BQsUGw z+o@MVk^^`he{jODgZ?|9%W`J?^V3raBiI7uHD3ScJE!FIopwzNem6Rjuklo9i(>{+ zD&eJDKMczf*po@bWe=&;ZHBK9O>PP@ftRlw8{M5Hw~#9vdF>{raWm0>mLvMG)X~Vx zUVLrI(}#B@3>V|g`~2A-_)qGZi|#(vpK@rt)pUg`nA?#qFkTIH1E_ekKPjPC>yw6> zof4GHghti}D}fXt&D~-X1~N{y;g0i}7Cz+Q#T)~kA{=I{#Cj5@%la<$S(rSK@t>S& z3(~jB@fXkwN3Wm0u+ZugpZNqv?c^usD>g2TY#)%~5t6=zPhknh9|D&)V{wxfX8JIF z3_SwIobT|N30XF*Mp^=po(@nj5_W1M3l-Dd7J6^#e906rSs?2UlCT6IjltFl61tI( z%>kMz62^aec9=;UZTG^;Sw_0GjYdHP`)U6*Lpo*!!++I8LZGrMsrTxw+zG7FM`>B^>0|2KH|W`)4B2io zh&z6Cf8zEP8=UCw>k+H@MYCq_v9N>)#lu02^iXWeCk+W4eL;0ogjTsv1=?}W)RL#zqljlxm@aUyQaa* zGq9oEMOsyJ<$TEhmieLaiNTBCJ>o?S?YZ@|Yx=wMCUx3d@2&OMt{)_jtfcV;3ChuO zZ1Sg1QgKpKcl6&gME4^hcjD3prBnUV2tthck%Zu|*8t)jleH3pZ-Ip38HvOU+H6if zui_2)xR#SB)h@rfGAZjZsHo2$*5J^w(Xz!diz%ALQ)&oKrKKvIBwW&o_wBwFNwSq` zmAIg)kQEHay1vNhkU}_u0T);rOXcOB$91-?on+|MnG#o0cF#z>3$B*XWgSzu|GxDP zXPRbsZ@Jn8ZO+W=R}pND)eF4096b5TXRSed zhB3G^+rm7sCk+_KrT%6{1U|^E2EsZ6H@hf8IULCflp~#F`=Ea$c%IMa-ISNK_gK7J zt3JKJzg3RGGYf!|D?R1aolF~O{~0n*DpyfNdcKknP~CXf-KE2DC|X>HNEi!>r<9aA z$dd+=v+Q{ZF_H_#(wr9Z{6A>lSl(2OW>K&I`PkOe%~U?eTlna#2e66D8c4 zdr(~6xj+5Kd&yS0M+&zLiH+4?qmMGT{q?r3B5nM@0op(qd8rR3dQwzB^_6+x^vZAV zKPA7v8XfZ0WpUVT#}$h`Y%9}VAN#3dllsKDcl>wrj_kPhHu~w^CqMsk4zscAg#PS} zNUP`jZ``Ncxqd3>(m?bKEX_*`*HWI|HB(Waw6=fM3QS$f@n~0!P1_irn6U_Ox-d)s z4U3Jnl{#^Ol*-A=iYQ&kJ?uB{9jjWjnj&jeNu}<-nMtYk-4)}6=yLvT9N{!KZqFUr- zU_M_b3Zxqw*=uxc+ehYG!B^Ldu0;M;Qrm5sbB-uNqt8?3niDR_0fK*YIz-tlgaXS!;G}zs04nFAb>kY=7JDE?aKp zCejEsMmeS+kNO1Nf%2ICc*xQp&ka2pMACh%f3-FD+FS%IZU}(pB+NMGb5nZe0|I8|SN95Z!i=yKDXA9PSi90<7VCC%*^jm+aE*{`Vhvme^%)830yUt%WSYxs= z0!}*}g6c=Zy?(b{5P!V-!-=qe^B1l}yTdwt^ON^NDM|g{WZ~^2vDDLKN3VeksqRgA z?hf;RJ~&Xx|ERV~JI6={C*dM%A(MB2vnotzjlV>bQ{;sY^Hcw+v=BJ6hsCmfwFBrQFe|8rsG zz^UcOJ03Qze|e`BW7@HTf7JG{sFR>8Q+kFsvCl}=4nGxPe5*0GmyGK?E0HiEN}FEF zpFaaL3%dNl0{o;PIK+MhBimg#cg;9qQ#H7`w}+@w;QTd-J0g$Etqh7XU#Pyq!3QUN z>@Z<{m!I1 zCAjp?bhg!pgdKAJQJd|siMqx}7ImuFI}1!h zldggXm87|7#kBu^IlVy6YBxrS#!Xr(|B$C!LS)Fc_nc>E&Qz zfj>amq!Da9Xw$_=O(@T8#nSEhC+rup@gzOpDy|fL;1KOzp#jK0RNoGAgD;4Zo2(DF zL^*^joY3HK?j;e1GIy@xcmHc|r%Qu>NXyz8t61q|e!L4Ozh4#K53^q~Vr~+aU;^B- zt>D2-6rgmq+cR`#Sl3 zCTiTdXv}X?LQ&uYtaYgobQX36T)mG!undRYTYcNB6ClkNW?|=iK^(@Jl`i>8=DX5z zNREQYeMh-Sbh*QzE4WTEy10X|rFCOT?un9A5WB1oi_$3VN=H%$VoJlik;!8{AedHE|{F8xZNg&*B#=YayP@OVk5ML;>@zvvo6qLlN zuo(tiPXhh3ky@ZwM&MvjA2Cf={stynn(!a`s4r!-`w4i}d%8db<@Vt>BD7})ZQ)JI z8(=#qt&E+&9ayzJqh@;hAuZMufqGF9w3_;N8BGU3$8Iv+(N~ym5bYvpp_T+%^As2f zlaJ_0OQ+~pw3NGi+DIQ^j%DXCSjn-E@6y#VUOMCrsE5iFoQ=snW~RR!6D_wo798xD zxNFhG`Z=EyWjHKy31EnD7pG`9riAHJv|kf&Yx+n-eA@eF4^-9cnW}oggf>r+&Wegw zShmiXO0+O%>E!h5A_x)@KU=I=`if2lu7FRr7^ngR2Idnk>vxVM(3n5XD||_@vELqd zAl2b<0e7_xFAXO$VEy{AcNNs9f8q27TBQ+dS&x~ah+MM-yElR6QOidOkZdRMMrFv znxs}LNoper$s|g`weuud#65(SFz&dAxI@?~gd~KJtQ1LzLJqh6uJ7;vJ=%Kg^Y~o* zyg%>v^HtO9<#Cg#<;~^+N)S9?`jw2c@Aglj9jV%DF32Y}vH+)U z%rKb{tpwkHH$^DHSzObXN{qV>^`e^ysMC4Sd@O^WWmWrR)0S`5F!9N zxWtM7;j5Aj+N{o$x)ed3C~*0orx^N3C=0-SjpU18W*Y-X*e)j9}r&cF!2A^2$iZgJD3L0iP4-?l- z{XB)f$TT+8>~-+aqAzh*AK=b>4Z18fpf~AEVIY~pD}OMKUd6*r%g3rt9VmQ=u|^?h z(y%33bdCUfmr2@W03DH-n>-TCA5)ojwh(quy=_r941$`X$q2-D!eTW>32#C+h@}R2 zuR7nk1zUM zbL^la?nS1_yd?ym1mbGvr`%l+V6s^ea2ut{6d~mXzQ@ z{7NS9)dw@VB-oBVs>)9&E6r6(=Y}}V;+hSQo3?6+UzDcu)TX!>W+JFzu)^f(ud96q z993If?Q!)Jmv|mRw1GP>aZTSt#9=MsD7`?hB<66>pZqm{?dH672hG_!!UHYw6L%$_ z9GT4`ta@hlUYR}DrCIXAO!k6m-HrdnQ(o9?R?j+z#i2?BX0UPHtSisyt{o$9dpb^h z4-vSm!(zG?9hrXON*nLo<;KUCAAP-i-4ZqgO)e?hE3662SqN_?>d3h14>*qpz*p@> zaMk#0fN2qJ{gxHW@6rgTX;{@mM6$v3g0lSr*Ce@nZI;l;`=sUB1S8eF?7QQpS}q#y z>ifL_-ZO5j(q{9HqBS(aU4!X9K^h&vTvNA?97C;s;3`o>i z+x24I^nE%e6MYf7x$?K^N}jbj+$$-#r2)`!>E(-PCfB-6meV@ymDrTp_8rIZk~g7# zTx@1D;Q|lux&V1mdGp5$lT3l_(Yu?}o!ps`_nw38Yuww<&S}3kq&0RUt-Ap{vkNZ( zJb2bj9*8vn@5=!ygFzJlWU~9bb0F7%jurrK=nO2Y>w0?JgB#a9`(^ zmvf8zp6>1Y*YR<$qf?jJoo>hUhfC6XuEsw3@8zgp$n*J2`W!3oUhTIx@1GUfAD`C0 zbVGkeP5+mduLd9VKl<9goBHa{{8teLui~Vy_{Ux)w!ixGpP?5toOL&Za}58jF`z3A zvBwPYHw^JZ2C~zj!Q%yjGZ2zL@MY(~$-f7zZw$l@56rCzdF(Y9e9iG2dr-Y%5TO`+ ze|qrP-Dt&!7s-pt`stCH^+TM)L-E&!68eV{`wp|pm(Yt58tI2-1Z|ghN;dcjSz00I+Ir!(D+G(_Y z)@Vca=<%(in!}@~u8lVKkM1o*X#vw%!)R0X`h`#-#6etfg_ zHP~&|A+_S9s-@QjC$m=-Sy87uRc2&KR0@UbODi;^TlJ^7sin< z-q*kQzWU;C{K9me2%J4(?+j?TCMdz@;Oi5SM?iA`-A{ub;1e-9lb+7NF~|huO(wjW zOf-JR@i33IlSw&W)7O5%>o9j&Ul+cbq#BS_{(I+^^ycP#%iH!X|H!wz*~rm#wyTWa z1kT@!W`8fv`Mz=6_tGQZ%dUSffAwAd0mvH!w$7fCYhlXTLyqz8?MXdd=NI%J5(;`n5M| zup#L+r9U&ASa!gFQV0N%Jkn& z3x3};8aiI3-`TaXbKA=<(&=vJ#D_+1&+LdN+y49S(0?m0{I~q+f1UsRx7zNHA@~n5 z?N7=2KO@)sN7ts0%@zRq|0O#a2~II8?m(2x%yE{4nmia&L7v}oGF>|uTje-&YcX7v z4BAPXe>f5sjlV<=3>$;B3KRk2GACz6{NVoX^W0S-q4`qyt&>nT_ zpqW!hW51Fk#tEk^1jyj)m;`kENWWPCD(u=^E zrPEw_tx~fJx0O>T;`c>Zy{MUMoH!d@8}8Gp3HyOUU(mKN@aX9Ga^0Y#S7A_eQt*j`ajo3uIk#^ zywIicm6?%ZO>i6>g98GNR36&B?B_1Ujct+F_y0GwvvRR$tL>&)9X9M!-4o5G3R zGSgx;8|&@1VGx1#?~X_LYYl3w11rM?Z{8~!xpcCyJ3hs{SUG@kjA_^fQ(M z5z)uLnHKh`kjzHe#b{PtKL18U$Hyg{W&5yh4eDj?#n2P)rgpS<J;I)SralF zCD*t>7qgCkq^+BJO`kzf8KmXL8~;GW25D+uEYu zu)GROwx6@VGne*{TtvI! zu-2uo(!QwU^9$!M><{WdhV*=%)1_ZPKSBH!Nxpf= z_u9wX;*gdwtmE~?>kMsPwe-06>s!pD?`%r}Vrz>#!Y~RO9t*Mh{9W@CjzNh^M%NpL zXh2cWWPNKtr6GL4V(p#exI-yQ^42O^-)HMAed)&4L# z&%g;K34U?D?uJ&svg*o=P+^&o@t9#v-QizO+crI_)vxLE^;}gw>Ou+zyJuSEy!_>e z%UX;IZEb&2H3!tPWP*ZD5P`7jmeJdk*2 z&N5T_lpLMGue!QdY%frI5o3T?I$i2b+}r^Wei3aV6+LSllb72{ z*wrixjiH0koHqLn9c}Y&Jw&F~9<^RWjt%1_8znYPI-H(}hiicm9b(HvOSH zk8oowU>~u8hh^zR-enKal&KB$z}2lD;T)NJ<)BsHCCFb} zmpMG+^n_sWmDA=Ht)(0(skbw{!7*t*9`;e!fD#E7~{}0}b!tvc~u+}Yq zQ@dG57c~AehlvQ68r5el;ny$S#+SftMHU8fti6DJ*;}GRoYBp3>Kwn9-!Oo**2ILb zqRS&uQG}CO9r$xP({<{4;?Jvh0K8`tjfu!qbO6T zh{jrMbOHTu@}gsLiE8ZHEF?KwAaRrdND|k@BA?vm-SN3Huv|xXV<3Z?`FPIcC6W^> zFTZMk!XDo}o=!?}=qfs3%Pw?znyN%9KUW1e!nd#*xdV>2g9i<)F>39CLg_+ER-IKp z0B3?GcT!Y(sxBWL+VY?R<<@}q8MtYZwR=yYWgTW#^Ud%oEx4j-5V!w_&g~*!Mk&=9 z3D*E@Xq7ppf5rpb^BvN27JT8h4mFlN!q)rJccp8o^Z=?T=;!YTH!4yc))LskL*`Oz zY6v}YM1Q!RUTOANjWzFx3E!G*x_n{1%19I1pIQg!sI?;EtBaVG)h268kv8Vs3Q|gS zDz%12eNie-iP!C@p53+@qX)cdt4(sDW%m4xG$+T;gblw2sY0!D^w<&?y0&$xDY=DS1;E31Q@s#XF8<(gwA@5vg@E-?rpZlMn0&V5AlF!R$xgTTV~ooyMed;6x}d(BU|$w z!Nf&5?Bj`ZJN~*x)ax<>>b)o8n%87_XErCb-&OX0)RO;|TAL zdYg2z-n_WZbeh8szYqgpJS)7jqdKY-9bhL5RqadIWBNK`mYnITV2EfcFz~&skSnt4 z1@Z)PCJ)MF$T#E%Q>h~Qh|o?6ELO>9X(8N*LjnWX^G=u$q*%Hdo2!Jp4VdB#3_b(& z;K&OZp3X2PMhlWFfTc=#APdM~$V+%)vXOkO1c&0C%R)SMWH)zB}xJ_CC%OX3R!a@CD&P znif^ek*a@U;Zt$$h|*$8x&g)qn=yru$UYRLa`Yt(B%Uiy`x~x-%2%+&Bp#H+z!n=Y z%d=#`!3r30vm~Q-6iHa#JBD>2bsU_}@h&7|_f89)IU>#^%L};dK0`A!n2IaY zO|_Ee`)_VEmde6)&^E@59GD5~kmiYL%yG=F3e1L%z_>k^8UiCWkfk(rqXkdO8^IK7 zM00+~aXR!m7IsIb+#L^+a1v>ZET4rfR!LZ*Tf@-7ZKWVB162Ywob$%!X2{aX`z_xI z6Hf`lwY3|Vh>uL1tpMU_;BP<}UU~E=)3M4Rgp&n$Q_!E#2w1(cqG5B%Xn0mFfu~A1 z-^gp0<&4LK;e=PVD{i*UqK5h{P>X}qcjui1k_@xZYG4s0^H)KO8$^BQMCOJ{3+`2a zs*vq#>KrQ08%Lpamd`SUR?UDB1mVi=0>f30sjGY~=%9i={`9A&D29y7aDV3`@Zm^| z9YBOa%U2_&5TJR-KCw<{(k$RntuDeANO1r-NE;R>Ft;W^-j#KNnI&yrBq!2RPA0vbhm0>yB0#f$1;`Sezj*HlS%-FpzSpRugRrh*v8x z0j1}YSlCn+m{KaAodLIYA~7^{i3Zhm5}TnBlF6vBNFXIrzIsG#rxcfHq_|Lpb*ZRO zG2<_TFr0zqX@uMIg_$gQ3?9GDn=Y`u2hMK-S~5>^V5nl&H%a{$w?)RdB3Mke903 zi-XO@fy`r1nN#us4j9lZThlG7I_ae8c_@QG8Wptx;yU3(IUH|W7I=4iY{eY(Bt4R+ z?k&+uZpYyNb@OZPk>%xMzO>2QnL@L1)EX5MT?$;QO!V@y(pT1tLxmpXGb6=G7g>lX zooqgA%Z7^Fri7va;N3a6{YR1w$wIrW49hY}|UC ze9X^8A$1MRyp@IM-hJ7_0L;zDL;=FpdXS_*u3<`7zeD6R(Pm`fPjOcU2XotaqszGd z`rR71kq}H|FzvPbJk)!Gl5zXZg~`nN`8tFJ_lX5_nw34Fq8D10kBw1NX}y@1pm}A> zle{ceqVq-p%My(>`qc=HmWWtqR%bkg9Md;l+ z*N1oh^^NGkJ+pud=s%!sD$WeLUC)DW8p%RXE8} z4xlPc^@sks2w2*2M0`Plrxsi;c=sv+#wUd+U@>xHr+o($gp<#wLIg4r^Cb)KlX$Cu zc|k1<8&Q9HL9v0I@p|N|*RVZt_4OAhNGr?GCeCdJd=%&?6;v+NSshi70BLP10=EZ& zTmt|9O60-}Y%EUHsemH2jIfNo7Gz{lsmN8=v(e=Q4dzH??K0U_Jv zGcMbhA@wGU*TAU*>r-J*bTSV+k0ByC^*BL~Gs&224YH7osd~1PY&}px}WpbB3WJ62s zZ)X&O9vX~mAKt0kz%vYHv9Qr9;S4f%e)HRW7OIdYrE3s*p*Kv8ot$)d(7*dhJR@zs zQl7*E&M1X+9vthjB1Hf&cxe0~`|Hyl#VCE43~_aQO}sVMogs38q#HR%iwS(@P=Sr|;d(1!s7r%zMW58+=vg6xd=Eso-G z&Qf2vsGb+5fV`V8%@_x840M4%z%)oVQrE96NV5$E-5Ao1IGGjaNMO0pDPO#yS+ zl%YBP>)l8k{dot{hM6h6QkR@z5A_wYBH@Kz#To90IZDy+^4OeE^AFhN=68 znmZyYtrK?j_#bzih0b5~wt4YRlPsd?UVkcm^`#1%?o~fsY%M~;s^3^UD!2qEUuz4T zv=Rym6gCRzQ6Te}!6m#~wtxy)PKi!h!

(`T3Z9g%s?rn;_JIjaTgrCifbzEzCz) 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%0DP^}y1Z?*c;0y-%m~MVv&6;b)yGVk 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 Y&E=LCnNVfoNz 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 literal 0 HcmV?d00001 diff --git a/assets/demo.tape b/assets/demo.tape new file mode 100644 index 0000000..19ce3ff --- /dev/null +++ b/assets/demo.tape @@ -0,0 +1,37 @@ +# VHS demo recording for whichllm +# Install: https://github.com/charmbracelet/vhs +# Run: cd assets && vhs demo.tape +# Output: demo.gif + +Output demo.gif + +Set FontSize 15 +Set Width 1200 +Set Height 1100 +Set Theme "Catppuccin Mocha" +Set Padding 20 +Set TypingSpeed 50ms + +# Warm up cache invisibly +Hide +Type "uvx whichllm@latest --top 1 > /dev/null 2>&1" +Enter +Sleep 10s +Type "clear" +Enter +Sleep 500ms +Show + +# Scene 1: Just run it +Type "uvx whichllm@latest" +Sleep 500ms +Enter +Sleep 6s +Sleep 2s + +# Scene 2: What GPU do I need? +Type "uvx whichllm@latest plan 'qwen 72b'" +Sleep 500ms +Enter +Sleep 6s +Sleep 3s diff --git a/docs/README.ja.md b/docs/README.ja.md new file mode 100644 index 0000000..a2b422f --- /dev/null +++ b/docs/README.ja.md @@ -0,0 +1,249 @@ +# whichllm + +**手元のハードウェアで実際に動くローカルLLMを探すCLIです。** + +whichllm は GPU / CPU / RAM / ディスクを検出し、HuggingFace 上のモデルを +取得して、実行できる候補をランキングします。単に「VRAMに入る最大モデル」を +選ぶのではなく、ベンチマーク、量子化、速度、実行形態、モデル世代をまとめて +評価します。 + +[English version](../README.md) + +![demo](../assets/demo.gif) + +## インストール + +### uv + +一度だけ試す場合: + +```bash +uvx whichllm@latest +``` + +継続して使う場合: + +```bash +uv tool install whichllm +uv tool upgrade whichllm # 既存インストールを更新 +``` + +### Homebrew + +```bash +brew install andyyyy64/whichllm/whichllm +``` + +### pip + +```bash +pip install whichllm +``` + +## 余裕を持って動く候補だけ見たい場合 + +whichllm のデフォルトは少し攻めた推薦です。RAMへのpartial offloadや、 +VRAMぎりぎりの候補も、動きそうならランキングに入れます。 + +LM Studioなどで余裕を持って動かしたい場合は、まずこれを使ってください。 + +```bash +uvx whichllm@latest --gpu-only --speed usable --vram-headroom 1GB +``` + +GPUのVRAMに全部載る候補だけに絞り、遅い推定速度の候補を外し、実行時の +余白も1GB残します。 + +それでもLM Studio側で少しはみ出す場合は、余白を増やします。 + +```bash +uvx whichllm@latest --gpu-only --speed usable --vram-headroom 1.5GB +``` + +### 開発用 + +```bash +git clone https://github.com/Andyyyy64/whichllm.git +cd whichllm +uv sync --dev +uv run whichllm +uv run pytest +``` + +## まず使う + +```bash +# 自動検出しておすすめモデルを表示 +whichllm + +# GPUをシミュレートする +whichllm --gpu "RTX 4090" +whichllm --gpu "Apple M3 Max" + +# 複数GPUをシミュレートする +whichllm --gpu "2x RTX 4090" +whichllm --gpu "RTX 4090" --gpu "RTX 3090" + +# GPUのVRAMに全部載る候補だけを見る +whichllm --gpu-only +whichllm --fit gpu + +# 速度の最低ラインを指定する +whichllm --speed usable +whichllm --speed fast +whichllm --min-speed 4 + +# GitHubやSlackに貼りやすいMarkdown表で出力する +whichllm --markdown + +# 実行時のメモリ余白やRAM使用量を指定する +whichllm --vram-headroom 1.5GB +whichllm --ram-budget available + +# CPUのみとして評価する +whichllm --cpu-only + +# JSONで出力する +whichllm --json +``` + +JSONの各モデルには `estimated_tok_per_sec` に加えて、`fit_type`、 +`vram_required_bytes`、`vram_available_bytes`、`uses_multi_gpu`、 +`multi_gpu_effective_vram_bytes`、`speed_confidence`、 +`speed_range_tok_per_sec`、`speed_notes`、`benchmark_source`、 +`benchmark_confidence` が入ります。 +速度は実測値ではなく、ハードウェア情報とモデル情報からの推定です。 +通常の表には必要メモリ、推定生成速度、Fit種別、Published が表示されます。 +Downloads まで見たい場合は `--details` を使います。 +GitHub issue、README、Slack、Discord へ貼る場合は `--markdown` / `-m` +でMarkdown表として出力できます。 + +## 主なコマンド + +```bash +# 推薦ランキング +whichllm --top 20 +whichllm --quant Q4_K_M +whichllm --min-speed 30 +whichllm --speed usable +whichllm --speed fast +whichllm --markdown +whichllm --profile coding +whichllm --context-length 64k +whichllm --details +whichllm --gpu-only + +# ベンチ根拠の厳しさ +whichllm --evidence strict +whichllm --evidence base +whichllm --direct + +# モデルから必要GPUを逆算 +whichllm plan "llama 3 70b" +whichllm plan "Qwen2.5-72B" --quant Q8_0 +whichllm plan "mistral 7b" --context-length 32768 + +# 今のマシンと購入候補GPUを比較 +whichllm upgrade "RTX 4090" "RTX 5090" "H100" + +# モデルをダウンロードしてチャット +whichllm run "qwen 2.5 1.5b gguf" +whichllm run + +# 実行用Pythonコードを表示 +whichllm snippet "qwen 7b" + +# ハードウェア情報だけ表示 +whichllm hardware +``` + +## スコアの見方 + +各モデルには 0 から 100 のスコアが付きます。中心になるのはベンチマークと +モデルサイズですが、実行時に遅すぎる候補や、CPUオフロードが大きい候補は +下がります。 + +| 要素 | 役割 | +| --- | --- | +| ベンチマーク | LiveBench、Artificial Analysis、Aider、Vision、Arena、Open LLM Leaderboard を統合 | +| モデルサイズ | 知識量の近似。MoEは総パラメータを使う | +| 量子化 | Q4 / Q5 / Q6 / Q8 などの品質低下を反映 | +| 実行形態 | Full GPU、Partial Offload、CPU-only を区別 | +| 速度 | tok/s が実用ラインを下回ると減点。表示時は推定の信頼度と幅も出す | +| 根拠の強さ | direct、base_model、variant、line_interp、self_reported を区別 | +| 世代補正 | 古い凍結ベンチだけで新世代を上回らないよう調整 | + +スコア横のマーカー: + +- `~`: 直接ベンチではなく、系列や派生から推定したスコア +- `!sr`: アップローダー自己申告の評価値だけに基づくスコア +- `?`: 利用できるベンチマーク根拠がないスコア + +速度表示: + +- 赤: `4 tok/s` 未満の遅い生成速度 +- 黄: `4-10 tok/s` のぎりぎり使える生成速度 +- 緑: `10-30 tok/s` の実用的な生成速度 +- 明るい緑: `30 tok/s` 以上の高速なローカル生成速度 +- `~`: 速度推定の幅がある通常の推定値 +- `?`: backend や runtime の影響が大きい低信頼の推定値 + +## 仕組み + +1. ハードウェアを検出します。NVIDIA、AMD、Intel、Apple Silicon、CPU、RAM、 + ディスク空き容量を見ます。 +2. HuggingFace APIからモデルを取得します。人気モデル、GGUF、最近更新された + GGUF、trending、重要な frontier モデルを組み合わせます。 +3. ベンチマークを読み込みます。現在系の LiveBench / Artificial Analysis / + Aider / Vision と、凍結系の Arena / Open LLM Leaderboard を分けて扱います。 +4. `base_model` とモデル名からファミリーを作り、同じモデルの派生やGGUFを束ねます。 +5. 候補ごとに VRAM、互換性、速度、速度推定の信頼度、スコアを計算します。 +6. ファミリーごとに最も良い候補を残して表示します。 + +通常は full GPU、partial offload、CPU-only の候補をまとめて見ます。GPUの +VRAMに全部載るモデルだけを見たい場合は `--gpu-only` か +`--fit gpu` を使います。遅い候補を最初から除外したい場合は +`--speed usable`、`--speed fast` を使います。 +それぞれ `10 tok/s`、`30 tok/s` が最低ラインです。 +もっと低いラインを指定したい場合は `--min-speed 4` のように数値で指定します。 + +キャッシュは通常 `~/.cache/whichllm/` に保存されます。`XDG_CACHE_HOME` が +絶対パスで設定されている場合は、その配下の `whichllm/` を使います。 + +- `models.json`: 6時間 +- `benchmark.json`: 24時間 + +## プロジェクト構成 + +```text +src/whichllm/ +├── cli.py # Typer CLI: main, plan, upgrade, run, snippet, hardware +├── constants.py # 互換用のregistry再export +├── data/ # GPU、量子化、framework、lineageのregistry +├── hardware/ # ハードウェア検出とGPUシミュレーション +├── models/ # HuggingFace取得、ベンチ、キャッシュ、グルーピング +├── engine/ # VRAM、互換性、速度、ランキング +└── output/ # Rich表示、JSON、plan/upgrade表示 +``` + +## 詳細ドキュメント + +- [CLIリファレンス](cli.md) +- [仕組み](how-it-works.md) +- [スコアリング](scoring.md) +- [ハードウェア検出とシミュレーション](hardware.md) +- [run と snippet](run-snippet.md) +- [トラブルシュート](troubleshooting.md) + +## 動作環境 + +- Python 3.11+ +- NVIDIA GPU検出は `nvidia-ml-py` と `nvidia-smi` fallback +- AMD GPU検出は Linux / ROCm / sysfs / lspci と Windows fallback +- Intel GPU検出は Linux / sysfs / lspci と Windows fallback +- Strix Halo、Ryzen AI MAX、Radeon 890M 系は shared memory APU として扱う +- Apple Silicon検出は macOS / `system_profiler` + +## ライセンス + +MIT diff --git a/docs/cli.md b/docs/cli.md new file mode 100644 index 0000000..6f95e65 --- /dev/null +++ b/docs/cli.md @@ -0,0 +1,270 @@ +# CLI reference + +This page describes the commands exposed by `whichllm`. It is based on the +Typer entrypoint in `src/whichllm/cli.py`. + +## Main command + +```bash +whichllm [OPTIONS] +``` + +Detects the current machine, loads model and benchmark data, ranks compatible +models, and prints a table. + +Common options: + +| Option | Meaning | +| --- | --- | +| `--top`, `-n` | Number of ranked models to show. Default: `10` | +| `--context-length`, `-c` | Context length used for KV cache estimation. Accepts integers or `k` shorthand such as `64k`. Default: `4096` | +| `--quant`, `-q` | Keep only a quantization type such as `Q4_K_M` | +| `--min-speed` | Keep only models above an exact tok/s estimate | +| `--speed` | Named speed floor: `any`, `usable` (`10 tok/s`), or `fast` (`30 tok/s`) | +| `--fit` | Runtime fit filter: `any`, `gpu`, or `full-gpu` | +| `--gpu-only` | Alias for `--fit full-gpu`; excludes partial offload and CPU-only candidates | +| `--profile` | Ranking profile: `general`, `coding`, `vision`, `math`, `any` | +| `--evidence` | Benchmark evidence filter: `strict`, `base`, `any` | +| `--direct` | Alias for `--evidence strict` | +| `--status` | Compatibility option. Runtime columns are now shown by default | +| `--details` | Show download metadata instead of runtime columns | +| `--min-params` | Minimum model knowledge capacity in billions of parameters | +| `--json` | Print machine-readable JSON | +| `--markdown`, `-m` | Print a pasteable GitHub-Flavored Markdown table | +| `--refresh` | Ignore caches and fetch models/benchmarks again | +| `--cpu-only` | Ignore GPUs and rank for CPU-only use | +| `--gpu` | Simulate GPU(s) by name. Accepts repeated flags, comma-separated values, and count shorthand | +| `--vram` | Override simulated GPU VRAM or detected GPU usable VRAM in GB | +| `--bandwidth`, `--ram-bandwidth` | Override GPU/RAM bandwidth in GB/s | +| `--gpu-index` | Detected GPU index to override when multiple GPUs are present | +| `--vram-headroom` | Reserve per-GPU memory for runtime overhead. Default: `auto`. Accepts `none`, byte values like `1.5GB`, or percentages like `10%` | +| `--ram-budget` | Cap RAM available for partial offload. Accepts `available`, byte values like `8GB`, or percentages like `50%` | +| `--version` | Print the installed package version | + +Environment variables: + +| Variable | Meaning | +| --- | --- | +| `HF_ENDPOINT` | Hugging Face endpoint root used for whichllm's own model metadata API calls. Example: `https://huggingface.co` or a compatible mirror root | + +`--fit any` is the default. It can include full-GPU, partial-offload, and +CPU-only candidates when they are runnable. `--fit gpu`, `--fit full-gpu`, and +`--gpu-only` keep only rows whose `fit_type` is `full_gpu`. + +The default table shows memory required, estimated generation speed, fit type, +and published date. Use `--details` when you want download counts instead. +Speed colors are absolute usability hints: red is under `4 tok/s`, yellow is +`4-10 tok/s`, green is `10-30 tok/s`, and bright green is `30+ tok/s`. The `~` +and `?` markers still refer to estimate confidence, not speed quality. + +`--vram-headroom auto` subtracts a small budget from each GPU before fit +checks, so near-edge recommendations are less likely to overflow in tools such +as LM Studio. Use `--vram-headroom none` to restore the raw detected VRAM. +`--ram-budget available` caps offload planning to current available RAM. +For detected iGPU or unified-memory systems, use `--vram` and +`--bandwidth` / `--ram-bandwidth` to override the automatically detected +usable memory and bandwidth. If multiple GPUs are detected, add `--gpu-index` +with the GPU number from `whichllm hardware`. + +Examples: + +```bash +whichllm +whichllm --gpu "RTX 4090" +whichllm --gpu "RTX 5060 Ti" --vram 16 +whichllm --vram 8 --ram-bandwidth 68 +whichllm --gpu-index 1 --vram 8 --bandwidth 68 +whichllm --gpu "2x RTX 4090" +whichllm --gpu "RTX 4090" --gpu "RTX 3090" +whichllm --gpu "RTX 4090, RTX 3090" +whichllm --profile coding --top 5 +whichllm --context-length 64k +whichllm --gpu-only +whichllm --fit gpu +whichllm --speed usable +whichllm --speed fast +whichllm --min-speed 4 +whichllm --markdown +whichllm --vram-headroom 1.5GB +whichllm --ram-budget available +whichllm --details +whichllm --evidence strict +whichllm --json | jq '.models[0]' +``` + +`--markdown` is mutually exclusive with `--json`. It prints a plain Markdown +table without the Rich hardware panel, colors, or box-drawing characters. + +Ranking JSON model rows include: + +| Field | Meaning | +| --- | --- | +| `fit_type` | Runtime fit classification: `full_gpu`, `partial_offload`, or `cpu_only` | +| `vram_required_bytes` | Estimated runtime memory requirement for the candidate | +| `vram_available_bytes` | GPU memory budget used for the fit check | +| `uses_multi_gpu` | Whether the fit check used more than one GPU | +| `multi_gpu_effective_vram_bytes` | Conservative effective VRAM budget for multi-GPU fits, when applicable | +| `estimated_tok_per_sec` | Point estimate used by ranking | +| `speed_confidence` | `high`, `medium`, or `low` | +| `speed_range_tok_per_sec` | Estimated lower/upper tok/s range, when available | +| `speed_notes` | Short reasons for the confidence level | +| `benchmark_status` | Display marker category for benchmark evidence | +| `benchmark_source` | How benchmark evidence was matched: `direct`, `variant`, `base_model`, `line_interp`, `self_reported`, or `none` | +| `benchmark_confidence` | Confidence in the benchmark match, `0.0`–`1.0` | + +The top-level `hardware` object also includes `usable_vram_bytes` per GPU, +`ram_budget_bytes`, and `budget_notes` when memory budgets are active. + +## `hardware` + +```bash +whichllm hardware [OPTIONS] +``` + +Prints detected hardware without ranking models. The same simulation flags are +available here: + +```bash +whichllm hardware +whichllm hardware --cpu-only +whichllm hardware --gpu "Apple M3 Max" +whichllm hardware --gpu "RTX 3060" --vram 12 +whichllm hardware --vram 8 --bandwidth 68 +whichllm hardware --gpu "4x RTX 4090" +``` + +## `plan` + +```bash +whichllm plan MODEL_NAME [OPTIONS] +``` + +Searches for a model by HuggingFace repo ID or fuzzy terms, then estimates the +VRAM required for several quantization levels and common GPUs. + +Options: + +| Option | Meaning | +| --- | --- | +| `--context-length`, `-c` | Context length for the memory estimate. Accepts integers or `k` shorthand such as `128k`. Default: `4096` | +| `--quant`, `-q` | Target quantization. Default: `Q4_K_M` | +| `--json` | Print the plan as JSON | +| `--refresh` | Ignore model cache and fetch again | + +Examples: + +```bash +whichllm plan "llama 3 70b" +whichllm plan "Qwen2.5-72B" --quant Q8_0 +whichllm plan "mistral 7b" --context-length 32768 +whichllm plan "mistral 7b" --context-length 32k +``` + +## `upgrade` + +```bash +whichllm upgrade TARGET_GPUS... [OPTIONS] +``` + +Compares the current machine against one or more simulated GPUs. The CPU, RAM, +disk, and OS come from the current machine; only the GPU changes. + +Options: + +| Option | Meaning | +| --- | --- | +| `--context-length`, `-c` | Context length used for ranking. Accepts integers or `k` shorthand such as `64k`. Default: `8192` | +| `--top`, `-n` | Best-N models to compare per GPU. Default: `3` | +| `--profile` | Ranking profile. Default: `general` | +| `--cpu-only` | Use CPU-only as the current baseline | +| `--json` | Print comparison JSON | +| `--refresh` | Ignore caches and fetch again | + +Examples: + +```bash +whichllm upgrade "RTX 4090" "RTX 5090" "H100" +whichllm upgrade "Apple M4 Max" --top 5 +whichllm upgrade "RX 7900 XTX" --profile coding +whichllm upgrade "RTX 4090" --context-length 128k +``` + +## `run` + +```bash +whichllm run [MODEL_NAME] [OPTIONS] +``` + +Creates a temporary Python script, launches it through `uv run --no-project`, +installs the needed inference packages into that isolated run, and starts an +interactive chat. + +If `MODEL_NAME` is omitted, whichllm ranks models for the current hardware and +uses the top result. + +Options: + +| Option | Meaning | +| --- | --- | +| `--context-length`, `-c` | Context length for the generated chat script. Accepts integers or `k` shorthand such as `64k` | +| `--quant`, `-q` | Preferred GGUF quantization | +| `--refresh` | Ignore model cache and fetch again | +| `--cpu-only` | Force CPU-only execution in the generated script | + +Examples: + +```bash +whichllm run +whichllm run "qwen 2.5 1.5b gguf" +whichllm run "phi 3 mini gguf" --cpu-only +whichllm run "mistral 7b gguf" --context-length 64k +``` + +`run` requires `uv` in `PATH`. + +## `snippet` + +```bash +whichllm snippet [MODEL_NAME] [OPTIONS] +``` + +Prints a ready-to-run Python snippet for the selected model. GGUF models use +`llama-cpp-python`; non-GGUF models use `transformers`. + +Options: + +| Option | Meaning | +| --- | --- | +| `--quant`, `-q` | Preferred GGUF quantization | +| `--refresh` | Ignore model cache and fetch again | + +Examples: + +```bash +whichllm snippet "qwen 7b" +whichllm snippet "llama 3 8b gguf" --quant Q5_K_M +``` + +## Evidence filters + +`--evidence` controls which benchmark matches are allowed into the ranking. + +| Mode | Allows | +| --- | --- | +| `strict` | Exact independent benchmark matches only | +| `base` | Exact, variant, and `cardData.base_model` matches | +| `any` | All evidence levels, including line interpolation and self-reported values | + +`--direct` is kept as a shorter alias for `--evidence strict`. + +## Profiles + +The ranker detects specialization from repository names. + +| Profile | Behavior | +| --- | --- | +| `general` | Excludes coding, vision, and math-specialized names | +| `coding` | Keeps coding-specialized names | +| `vision` | Keeps vision or multimodal names and includes VLM candidates | +| `math` | Keeps math-specialized names | +| `any` | Keeps all recognized model types and includes VLM candidates | diff --git a/docs/hardware.md b/docs/hardware.md new file mode 100644 index 0000000..d781eec --- /dev/null +++ b/docs/hardware.md @@ -0,0 +1,237 @@ +# Hardware detection and simulation + +whichllm detects the current machine and can also simulate hardware for +purchase planning. + +The source of truth is the `hardware/` package plus curated registry data in +`data/gpu.py`. `constants.py` remains as a compatibility export layer for older +imports. + +## Detected data + +The ranker receives a `HardwareInfo` object with: + +- GPU list +- CPU name +- physical CPU cores +- AVX2 and AVX-512 support +- total RAM +- free disk space +- OS name + +Each GPU is represented as `GPUInfo`: + +- name +- vendor +- VRAM bytes +- usable VRAM bytes, when a runtime headroom is active +- NVIDIA compute capability, when known +- CUDA or ROCm version, when known +- memory bandwidth estimate +- whether the GPU uses shared memory + +## NVIDIA + +NVIDIA detection tries `nvidia-ml-py` first. If NVML is unavailable, fails to +initialize, or returns no devices, whichllm falls back to: + +```bash +nvidia-smi --query-gpu=name,memory.total,clocks.max.memory --format=csv,noheader,nounits +``` + +If a driver rejects `clocks.max.memory`, whichllm retries the older +`name,memory.total` query. + +For known cards, curated data and strict `dbgpu` lookups provide: + +- memory bandwidth +- compute capability + +The max memory clock is used when a marketing name covers multiple memory +types. For example, GTX 1650 GDDR5 and GDDR6 cards share the same broad driver +name, so whichllm uses the reported memory clock when available and falls back +to the conservative bandwidth when it is not. + +DGX Spark / NVIDIA GB10 uses unified system memory. When the driver reports +`memory.total` as unavailable, whichllm treats GB10 as shared memory and uses +system RAM for fit checks. + +Compute capability is used to warn when a card is below the minimum expected by +common local inference tools. + +## AMD + +On Linux, AMD detection tries `rocm-smi` first: + +- product name +- VRAM +- ROCm driver version + +If `rocm-smi` is unavailable, it falls back to `lspci` and then +`/sys/class/drm`. + +On Windows, whichllm uses `Win32_VideoController` as a fallback for AMD GPUs. +When possible, it also reads the 64-bit dedicated-memory value from the +`Control\Video` registry path because `AdapterRAM` is a 32-bit field and can +cap larger cards around 4 GB. + +AMD shared-memory APUs are treated differently from discrete GPUs. Names such +as Strix Halo, Ryzen AI MAX, Radeon 8050S, Radeon 8060S, Radeon 890M, and +Radeon 780M are modeled as shared-memory systems. If the reported VRAM is just +a small aperture, whichllm uses the system memory pool for fit checks instead +of treating it as a tiny discrete GPU. + +## Intel + +Intel integrated GPUs are detected on Linux through `lspci` or sysfs, and on +Windows through `Win32_VideoController`. They do not normally report dedicated +VRAM, so whichllm records them with `0` dedicated VRAM and labels them as +shared memory. + +Discrete Intel Arc cards are kept as dedicated-memory GPUs when the device name +and memory report look like a discrete adapter. + +The Intel backend factor is lower than NVIDIA, AMD, and Apple because local LLM +GPU inference support is less mature. + +## Apple Silicon + +On macOS, whichllm uses: + +```bash +system_profiler SPHardwareDataType -json +``` + +Apple Silicon uses unified memory, so the detected chip memory is treated as +available GPU memory. Memory bandwidth is looked up by chip family when known. + +Partial offload on Apple Silicon is not penalized like discrete PCIe offload. +Weights still live in unified memory, so the speed penalty is milder. + +## CPU and memory + +CPU detection reads: + +- `/proc/cpuinfo` on Linux +- `sysctl` on macOS +- `wmic` on Windows, then PowerShell / CIM when `wmic` is unavailable or only + returns a header + +Physical core count comes from `psutil`, with a Linux `/proc/cpuinfo` fallback. + +RAM comes from `psutil.virtual_memory()`. Disk free space is checked under the +user's home directory by default. + +## GPU simulation + +Use `--gpu` to simulate a GPU: + +```bash +whichllm --gpu "RTX 4090" +whichllm hardware --gpu "Apple M3 Max" +whichllm upgrade "RTX 4090" "RTX 5090" "H100" +``` + +Simulation uses the `dbgpu` package for a TechPowerUp-backed GPU database. +whichllm adds extra handling for common aliases and Apple Silicon chips because +those are not covered by dbgpu. + +Use `--vram` when a GPU name is ambiguous, unknown, or has multiple variants: + +```bash +whichllm --gpu "RTX 5060 Ti" --vram 16 +whichllm hardware --gpu "Unknown GPU" --vram 24 +``` + +For detected integrated or unified-memory GPUs, use `--vram` and +`--bandwidth` / `--ram-bandwidth` to override the automatically detected usable +capacity and memory bandwidth: + +```bash +whichllm --vram 8 --ram-bandwidth 68 +whichllm hardware --vram 8 --bandwidth 68 +``` + +If multiple GPUs are detected, pass `--gpu-index` to choose the GPU shown by +`whichllm hardware`: + +```bash +whichllm --gpu-index 1 --vram 8 --ram-bandwidth 68 +``` + +By default, whichllm applies a small automatic VRAM headroom before fit checks. +This avoids recommending models that only fit on paper but overflow in runtimes +that need extra graph buffers or loader overhead. Tune it with: + +```bash +whichllm --vram-headroom 1.5GB +whichllm --vram-headroom 10% +whichllm --vram-headroom none +``` + +Multi-GPU simulation accepts repeated flags, comma-separated values, and count +shorthand: + +```bash +whichllm --gpu "2x RTX 4090" +whichllm --gpu "RTX 4090" --gpu "RTX 3090" +whichllm --gpu "RTX 4090, RTX 3090" +``` + +`--vram` is only supported for a single simulated GPU. For multi-GPU +simulation, use known GPU names so whichllm can resolve each card's VRAM from +the GPU database. + +## Fit types + +Compatibility checks classify a candidate into one of three fit types: + +| Fit | Meaning | +| --- | --- | +| `full_gpu` | Required memory fits in available GPU memory | +| `partial_offload` | GPU plus usable system RAM can hold the model | +| `cpu_only` | Usable system RAM can hold the model without GPU | + +If neither GPU memory nor usable RAM can hold the model, the candidate is not +ranked. + +whichllm keeps a bounded system-RAM reserve for the OS and other processes. +Use `--ram-budget available` to cap partial-offload planning to the current +available RAM reported by the OS, or pass a fixed budget such as +`--ram-budget 8GB`. + +## Multiple GPUs + +For fit checks, whichllm uses a conservative multi-GPU budget rather than +pretending all VRAM is one perfect device. It starts from raw total VRAM, applies +a small per-GPU overhead, and then applies a utilization factor. Homogeneous +sets receive a less severe reduction than heterogeneous sets. + +If a dedicated GPU is present, low-aperture shared-memory integrated GPUs are +not added to the fit pool. This avoids treating unrelated system RAM and +dedicated VRAM as one full-GPU target. + +For speed estimates, whichllm uses the largest detected GPU as the +representative device and marks multi-GPU speed as low-confidence. This avoids +claiming ideal scaling when real performance depends on backend split mode, +PCIe/NVLink bandwidth, NCCL/RCCL support, batch size, and model architecture. + +This is a practical fit approximation. It does not model every tensor-parallel +or pipeline-parallel runtime configuration. + +## Disk checks + +The compatibility check also compares estimated model weight size with free +disk space. If the model cannot be downloaded, it is marked unrunnable. + +## Known limitations + +- GPU bandwidth is a lookup or database estimate, not a live benchmark. +- Speed estimates are planning numbers. The default table and JSON fields such + as `speed_confidence` and `speed_range_tok_per_sec` show uncertainty. +- Driver, runtime, batch size, prompt length, and thermal limits can change real + performance. +- Multi-GPU runtime behavior depends on the inference backend and is only + approximated. +- Apple and shared-memory APU behavior is modeled as unified-memory style, but + real results still depend on OS pressure and memory bandwidth. diff --git a/docs/how-it-works.md b/docs/how-it-works.md new file mode 100644 index 0000000..7ee34a3 --- /dev/null +++ b/docs/how-it-works.md @@ -0,0 +1,193 @@ +# How it works + +whichllm has one main job: start with the user's hardware, collect candidate +models, estimate what can run, and rank the results. + +The implementation is intentionally split into small packages: + +```text +src/whichllm/ +├── cli.py +├── constants.py +├── data/ +├── hardware/ +├── models/ +├── engine/ +└── output/ +``` + +## Request flow + +The default `whichllm` command follows this path: + +1. Validate CLI flags. +2. Detect hardware. +3. Load model cache or fetch from HuggingFace. +4. Load benchmark cache or fetch benchmark sources. +5. Group related model repos into families. +6. Flatten families back into rankable candidates. +7. Rank every candidate variant. +8. Backfill missing published dates for top results. +9. Print a Rich table or JSON. + +The `plan`, `upgrade`, `run`, `snippet`, and `hardware` subcommands reuse parts +of the same pipeline. + +## Hardware detection + +`hardware/detector.py` orchestrates detection. Each detector is fail-safe and +returns an empty result on failure. + +| Module | Role | +| --- | --- | +| `hardware/nvidia.py` | Uses `nvidia-ml-py`; falls back to `nvidia-smi`, including optional memory-clock data | +| `hardware/amd.py` | Uses `rocm-smi`; falls back to `lspci` and `/sys/class/drm` | +| `hardware/intel.py` | Detects Linux Intel iGPUs through `lspci` or sysfs | +| `hardware/windows.py` | Detects Windows AMD and Intel fallback GPUs through WMI and registry memory fields | +| `hardware/apple.py` | Uses `system_profiler` on macOS | +| `hardware/cpu.py` | Reads CPU name, physical cores, AVX2, and AVX-512 | +| `hardware/memory.py` | Reads RAM and disk free space | +| `hardware/gpu_simulator.py` | Builds synthetic GPUs for `--gpu` | + +The result is a `HardwareInfo` dataclass. GPUs are represented by `GPUInfo`. + +## Model fetching + +`models/fetcher.py` reads the HuggingFace API and turns responses into +`ModelInfo` objects. + +The fetcher combines several queries: + +1. Popular `text-generation` models sorted by downloads. +2. Popular GGUF repos. +3. Recently modified GGUF repos. +4. Trending text-generation repos, with and without the GGUF filter. +5. A curated list of frontier and hard-to-find model IDs. +6. Vision candidates from `image-text-to-text` when the active profile needs + them. + +For each repository, the parser extracts: + +- repo ID and display name +- parameter count +- active parameter count for known or config-detected MoE models +- architecture and context length +- license, downloads, likes, published date +- GGUF variants and file sizes +- `cardData.base_model` +- conservative HuggingFace `evalResults` values + +Parameter counts can come from safetensors metadata, GGUF metadata, config +estimation, name hints, or a curated fallback table for important models with +missing metadata. + +## Caches + +Both caches normally live under `~/.cache/whichllm/`. If `XDG_CACHE_HOME` is +set to an absolute path, whichllm uses `$XDG_CACHE_HOME/whichllm/` instead. + +| File | TTL | Contents | +| --- | --- | --- | +| `models.json` | 6 hours | Serialized `ModelInfo` data | +| `benchmark.json` | 24 hours | Combined benchmark score map | + +`--refresh` bypasses the relevant cache and writes a new one after fetching. + +## Benchmark sources + +`models/benchmark.py` builds one score map from multiple sources. Scores are +normalized to a 0-100 scale before ranking. + +whichllm separates sources into two tiers: + +| Tier | Sources | Treatment | +| --- | --- | --- | +| Current | LiveBench, Artificial Analysis, Aider, Vision | Overrides frozen scores for the same model | +| Frozen | Open LLM Leaderboard v2, Chatbot Arena ELO | Kept for older coverage, capped and recency-demoted | + +Current sources can use live scrapes when reachable and curated snapshots when +the upstream page shape changes. The snapshot month is printed below rankings. + +Frozen-only scores are demoted by model lineage. This prevents an older model +with a stale leaderboard score from outranking a newer generation simply because +the newer model was never added to that frozen leaderboard. + +## Benchmark evidence + +A model can receive benchmark evidence through several paths: + +| Evidence | Meaning | +| --- | --- | +| `direct` | Exact independent benchmark match | +| `variant` | Suffix-stripped or `-Instruct` variant match | +| `base_model` | Match through HuggingFace `cardData.base_model` | +| `line_interp` | Size-aware interpolation within the same model line | +| `self_reported` | Uploader-provided `evalResults` only | +| `none` | No usable evidence | + +Inheritance is rejected when the actual model size differs too much from the +reference. This catches small draft heads, MTP heads, and unrelated forks that +would otherwise borrow a larger base model's score. + +## Family grouping + +`models/grouper.py` groups related repos by: + +1. `cardData.base_model`, when available. +2. Normalized repository names. + +The normalizer removes common suffixes such as `-GGUF`, `-AWQ`, `-GPTQ`, +`-Instruct`, `-Chat`, `-FP16`, and date suffixes. It also handles versioned +model lines such as Qwen, Llama, Mistral, and DeepSeek. + +Within a family, the ranker evaluates all members and variants but keeps only +the best result for the final table. + +## Candidate variants + +For each `ModelInfo`, `engine/ranker.py` builds candidate variants: + +- Existing GGUF files are evaluated by quantization. +- Extreme low-bit GGUF variants are skipped unless explicitly requested with + `--quant`. +- Official safetensors-only repos can receive synthetic GGUF estimates for + common community conversions such as `Q4_K_M`, `Q5_K_M`, `Q6_K`, and `Q8_0`. +- Pre-quantized repos such as AWQ, GPTQ, FP8, and BF16 are not given synthetic + GGUF variants. + +Apple Silicon and CPU-only rankings are restricted to GGUF candidates for +runtime stability. Linux with NVIDIA can also rank non-GGUF AWQ/GPTQ/FP16/BF16 +repos. + +## Ranking + +For each candidate variant: + +1. Estimate memory. +2. Check whether it can run. +3. Estimate tok/s and attach speed confidence/range metadata. +4. Resolve benchmark evidence. +5. Compute a quality score. +6. Keep the best variant for the model family. + +The final sorting key stays close to the displayed quality score, with a small +direct-benchmark bonus and a CPU-only penalty. Full-GPU candidates are already +favored inside the score through the runtime-fit and speed adjustments, so the +sort key does not add a second full-GPU bonus. + +See [Scoring](scoring.md) for the score details. + +## Output + +Output is split by surface: + +- `output/ranking.py` renders hardware panels and recommendation tables. +- `output/json_output.py` renders ranking, `plan`, and `upgrade` JSON. +- `output/plan.py` renders `plan` tables. +- `output/upgrade.py` renders upgrade comparison tables. +- `output/display.py` re-exports those functions for older imports. + +Normal ranking tables show memory required, estimated generation speed, fit +type, and published date. `--details` switches to download-oriented metadata. +Speed color is based on absolute usability, while `~` marks estimates with a +range and `?` marks low-confidence, backend-sensitive estimates. diff --git a/docs/run-snippet.md b/docs/run-snippet.md new file mode 100644 index 0000000..6905733 --- /dev/null +++ b/docs/run-snippet.md @@ -0,0 +1,161 @@ +# Run and snippet + +whichllm can do more than print recommendations: + +- `whichllm run` starts an interactive chat with a selected model. +- `whichllm snippet` prints a Python script for manual use. + +Both commands use the same model loading helpers in `cli.py`. + +## `whichllm run` + +```bash +whichllm run [MODEL_NAME] +``` + +If `MODEL_NAME` is provided, whichllm searches the fetched model list for an +exact ID, suffix match, or term match. + +If `MODEL_NAME` is omitted, whichllm ranks models for the current hardware and +uses the top result. + +Examples: + +```bash +whichllm run +whichllm run "qwen 2.5 1.5b gguf" +whichllm run "phi 3 mini gguf" --cpu-only +whichllm run "llama 3 8b gguf" --quant Q5_K_M +``` + +## How `run` executes + +`run` requires `uv` in `PATH`. + +At runtime, whichllm: + +1. Loads models from cache or HuggingFace. +2. Selects a model and quantization. +3. Generates a temporary Python chat script. +4. Runs that script with `uv run --no-project`. +5. Adds the required dependencies with repeated `--with` flags. +6. Deletes the temporary script after the chat exits. + +This keeps the project environment clean. The temporary runtime dependencies +are not added to `pyproject.toml`. + +## Supported model paths + +### GGUF + +GGUF models use: + +- `llama-cpp-python` +- `huggingface-hub` + +The generated script downloads the selected GGUF file with `hf_hub_download` +and loads it through `llama_cpp.Llama`. + +GPU behavior: + +- default: `n_gpu_layers=-1` +- `--cpu-only`: `n_gpu_layers=0` + +### AWQ + +AWQ repos are inferred from the model ID and use: + +- `transformers` +- `torch` +- `accelerate` +- `autoawq` + +### GPTQ + +GPTQ repos are inferred from the model ID and use: + +- `transformers` +- `torch` +- `accelerate` +- `auto-gptq` + +### FP16, BF16, FP8, INT8, BNB 4-bit + +Other non-GGUF repos use the Transformers path. The generated script uses +`device_map="auto"` unless `--cpu-only` is set. + +## Quantization selection + +For GGUF repos, whichllm chooses a variant by this preference order unless +`--quant` is provided: + +```text +Q4_K_M, Q4_K_S, Q5_K_M, Q5_K_S, Q6_K, Q3_K_M, Q3_K_L, Q8_0, ... +``` + +This order favors a practical balance of memory and quality. Very low-bit +variants are available when explicitly requested but are not preferred by +default. + +If the requested quantization is not available, `run` warns and falls back to +the best available match. + +## Chat behavior + +GGUF scripts call: + +```python +llm.create_chat_completion(messages=messages, stream=True) +``` + +Transformers scripts use: + +```python +tokenizer.apply_chat_template(...) +model.generate(...) +``` + +The chat loop keeps the current conversation history in memory until the +process exits. Type `exit`, `quit`, or `q` to stop. + +## `whichllm snippet` + +```bash +whichllm snippet [MODEL_NAME] +``` + +`snippet` prints a short Python example instead of running it. + +Examples: + +```bash +whichllm snippet "qwen 7b" +whichllm snippet "llama 3 8b gguf" --quant Q5_K_M +``` + +If no model is provided, `snippet` picks the most-downloaded GGUF model from +the fetched model list. This is different from `run`, which auto-ranks for the +current hardware when no model name is provided. + +## Manual execution + +The snippet output includes a suggested `uv run --no-project` command with the +needed `--with` dependencies. + +Example shape: + +```bash +uv run --no-project --with llama-cpp-python --with huggingface-hub script.py +``` + +## Practical notes + +- First run can take time because dependencies and model weights need to + download. +- HuggingFace access rules still apply. Gated models may require local + HuggingFace authentication. +- `run` is a convenience path, not a full model manager. +- `snippet` is better when you want to adapt loading code into your own project. +- Generated Transformers scripts use `trust_remote_code=True`, matching common + HuggingFace local inference patterns. Review model repos before running code + from untrusted sources. diff --git a/docs/scoring.md b/docs/scoring.md new file mode 100644 index 0000000..da4b031 --- /dev/null +++ b/docs/scoring.md @@ -0,0 +1,216 @@ +# Scoring + +whichllm does not pick the largest model that fits. It ranks candidates by a +composite score that tries to answer a more practical question: + +> Of the models that can run here, which one is likely to be the best usable +> choice? + +The source of truth is `engine/ranker.py`. + +## Inputs + +Each candidate score uses: + +- model metadata from HuggingFace +- detected or simulated hardware +- estimated VRAM/RAM fit +- estimated tok/s +- quantization type +- benchmark evidence +- downloads and likes +- source organization +- model lineage and generation + +The score is capped to `0..100`. + +## Benchmark evidence + +Independent benchmark matches are not all treated equally. + +| Source | Weight | Meaning | +| --- | ---: | --- | +| `direct` | `0.62` | Exact independent benchmark match | +| `base_model` | `0.55` | Match through `cardData.base_model` | +| `variant` | `0.50` | Suffix-stripped variant match | +| `line_interp` | `0.40` | Size-aware model-line interpolation | +| `self_reported` | `0.30` | Uploader-provided HuggingFace eval only | +| `none` | `0.00` | No benchmark evidence | + +`self_reported` evidence is intentionally weak. HuggingFace model cards can +contain useful evaluation data, but it is not the same as an independent +leaderboard. + +## Size score + +Model size is used as a rough world-knowledge proxy: + +```text +size_score = 4.2 * log2(params_b) + 9 +``` + +The result is capped at `35`. + +For dense models, `params_b` is the parameter count. For MoE models, whichllm +uses total parameters for quality because all experts contribute to stored +knowledge. Active parameters are used later for speed. + +## Quantization penalty + +Lower-bit quantization can make a larger model fit, but it also reduces quality. +The score core is multiplied by `(1 - quant_penalty)`. + +Examples: + +| Quant | Penalty | +| --- | ---: | +| `Q8_0` | `0.01` | +| `Q6_K` | `0.02` | +| `Q5_K_M` | `0.03` | +| `Q4_K_M` | `0.05` | +| `Q3_K_M` | `0.08` | +| `Q2_K` | `0.25` | +| `IQ2_XXS` | `0.40` | +| `Q1_0` | `0.55` | + +Extreme low-bit variants are excluded by default when better candidates exist. +They can still be requested explicitly with `--quant`. + +## Evidence confidence + +After benchmark and size are combined, weak evidence is dampened: + +| Evidence state | Multiplier | +| --- | ---: | +| Direct benchmark | `1.00` | +| Inherited evidence | `0.78` | +| Self-reported evidence | `0.55` | +| No benchmark | `0.55` | + +For inherited benchmark evidence, the raw score is also scaled by confidence +before entering the scoring function. Line interpolation therefore receives a +double discount: once for its interpolation confidence and once for being +inherited evidence. + +## Runtime fit + +The candidate's runtime form matters: + +| Fit | Multiplier | +| --- | ---: | +| Full GPU | `1.00` | +| Partial offload | `0.42`-`0.88`, based on spill ratio | +| CPU-only | `0.50` | + +Light partial offload is penalized less than heavy offload. MoE models receive +a milder penalty when the active parameter working set can plausibly stay on +GPU while inactive experts spill to CPU RAM. + +The final family selection key does not add a separate full-GPU bonus. Runtime +fit is already reflected in the quality score through the multiplier above and +the speed adjustment below. CPU-only results receive a small extra sort penalty +when mixed with GPU-backed candidates. + +## Speed adjustment + +Speed is treated as a usability gate. It is not the main quality signal. + +Required speed depends on fit: + +| Fit | Required speed | +| --- | ---: | +| Full GPU | `8 tok/s` | +| Partial offload | `4 tok/s` | +| CPU-only | `1.5 tok/s` | + +Candidates below the required speed receive up to `-8` points. Candidates above +it receive up to `+8` points. + +After ranking, if any candidate is at least `5 tok/s`, whichllm drops candidates +below `1.5 tok/s`. This avoids recommending models that technically fit but are +not practical to use. + +The reported speed is a point estimate, not a live benchmark. Ranking also +exposes speed confidence: + +| Confidence | Range factor | Typical cases | +| --- | ---: | --- | +| `medium` | `0.60x`-`1.60x` | Normal GPU estimates, synthetic GGUF estimates, AMD shared-memory APU MoE estimates | +| `low` | `0.35x`-`2.00x` | CPU-only, partial offload, unknown bandwidth, Apple Silicon MoE | +| `high` | `0.85x`-`1.20x` | Reserved for future measured-speed data | + +Speed cells are colored by absolute usability: red is under `4 tok/s`, yellow +is `4-10 tok/s`, green is `10-30 tok/s`, and bright green is `30+ tok/s`. `~` +marks medium-confidence estimates with a range, and `?` marks low-confidence +estimates. JSON exposes the same uncertainty data as `speed_confidence`, +`speed_range_tok_per_sec`, and `speed_notes`. + +## Source trust + +The source organization contributes a small adjustment: + +- official model organizations receive a small bonus +- trusted GGUF converters can inherit that trust +- known repackagers receive a small penalty + +The adjustment is intentionally small. It should break ties, not replace +benchmark and fit signals. + +## Popularity + +Downloads and likes act as tie-breakers. Their weight is lower when benchmark +evidence is strong and higher when evidence is weak. + +Popularity has no effect for direct benchmark matches. + +## Generation lineage + +Some benchmark sources are frozen. A model released after a frozen leaderboard +cannot appear there, while older models can keep strong but stale scores. + +whichllm uses family-specific lineage maps to avoid that inversion. Newer +generations can receive a small bonus; older generations can receive a small +penalty. This is applied carefully so direct benchmark evidence still matters. + +Examples of tracked lineages include: + +- Qwen +- Llama +- DeepSeek +- Gemma +- Phi +- Mistral +- GLM +- Kimi +- Granite +- OLMo +- T5 (incl. Flan-T5, mT5, ByT5, T5Gemma) + +## Benchmark markers + +The table score can include a marker: + +| Marker | Meaning | +| --- | --- | +| none | Direct independent benchmark evidence | +| `~` | Estimated or inherited benchmark evidence | +| `!sr` | Self-reported HuggingFace eval only | +| `?` | No benchmark evidence | + +Top-pick confidence is computed from the score gap, benchmark status, and fit +type. Partial-offload and CPU-only top picks are reported with lower confidence +than full-GPU direct-benchmark winners. + +## Why a smaller model can win + +A smaller model can outrank a larger one when it has: + +- stronger current benchmark evidence +- a newer generation signal +- better quantization quality +- full-GPU fit instead of partial offload +- higher estimated speed +- a more trustworthy source + +That is intentional. whichllm ranks likely usable quality, not parameter count +alone. diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md new file mode 100644 index 0000000..f2941be --- /dev/null +++ b/docs/troubleshooting.md @@ -0,0 +1,423 @@ +# Troubleshooting + +This page lists common issues and the first checks to make. + +## No GPU detected + +Run: + +```bash +whichllm hardware +``` + +If an NVIDIA GPU is missing: + +- check that the driver is installed +- check `nvidia-smi` +- check that `nvidia-ml-py` can load NVML + +whichllm falls back to `nvidia-smi`, but it still needs the NVIDIA driver tools +to be working. + +If an AMD GPU is missing: + +- on Linux, check `rocm-smi`, `lspci`, and `/sys/class/drm` +- on Windows, check that PowerShell can read `Win32_VideoController` +- for Ryzen AI / Radeon integrated graphics, check whether `whichllm hardware` + shows shared memory instead of a tiny 512 MB or 4 GB adapter + +If an Intel iGPU is missing: + +- Linux detection uses `lspci` or `/sys/class/drm` +- Windows detection uses `Win32_VideoController` +- many Intel iGPUs do not expose dedicated VRAM, so they may be shown as shared + memory graphics + +## Simulate hardware instead + +If detection is unavailable or you are planning a purchase, use `--gpu`: + +```bash +whichllm --gpu "RTX 4090" +whichllm hardware --gpu "Apple M3 Max" +whichllm --gpu "RTX 5060 Ti" --vram 16 +whichllm --gpu "2x RTX 4090" +whichllm --gpu "RTX 4090" --gpu "RTX 3090" +``` + +Use `--vram` when the GPU name has multiple memory variants or is not in the +database. + +For detected iGPU or unified-memory systems, override the usable GPU memory and +bandwidth directly: + +```bash +whichllm hardware --vram 8 --ram-bandwidth 68 +whichllm --vram 8 --bandwidth 68 +``` + +If `whichllm hardware` lists multiple GPUs, add `--gpu-index` with the GPU +number from that output. + +`--vram` only applies to one simulated or detected GPU. For multi-GPU +simulation, use known GPU names and omit `--vram`. + +## `--cpu-only` conflicts with `--gpu` + +These flags are mutually exclusive: + +```bash +whichllm --cpu-only --gpu "RTX 4090" +``` + +Choose one: + +```bash +whichllm --cpu-only +whichllm --gpu "RTX 4090" +``` + +## `--vram` / `--bandwidth` needs a GPU + +These overrides need either a detected GPU or a simulated GPU: + +```bash +whichllm --vram 8 --ram-bandwidth 68 +whichllm --gpu "RTX 3060" --vram 12 +``` + +If no GPU is detected, use `--gpu` to simulate one or check the detection steps +above. + +## No compatible models found + +Try: + +```bash +whichllm +whichllm --cpu-only +whichllm --refresh +``` + +Common causes: + +- the selected `--quant` is too restrictive +- `--gpu-only` or `--fit full-gpu` filters out partial-offload and CPU-only candidates +- `--speed usable`, `--speed fast`, or `--min-speed` filters out slower candidates +- `--min-speed` is too high +- `--evidence strict` filters out all candidates +- the requested context length is too large +- available RAM is too low after reserving space for the OS +- disk free space is too low for the model weights + +For very small machines, remove optional filters first: + +```bash +whichllm --top 20 +``` + +## Recommendations use RAM or CPU offload, but I only want VRAM + +By default, whichllm includes any runnable candidate: full-GPU, partial-offload, +and CPU-only. This is useful for finding what can run at all, but it can be too +loose when you want only models that fit entirely in GPU VRAM. + +Use: + +```bash +whichllm --gpu-only +whichllm --fit gpu +whichllm --fit full-gpu +``` + +If no rows are shown, this machine has no ranked candidates that fit fully in +GPU memory under the current filters. Remove `--gpu-only`, lower the context +length, or try a smaller quantization. + +## A model fits, but it is too slow + +The default ranking table shows estimated generation speed. Slow rows are red, +marginal rows are yellow, usable rows are green, and fast rows are bright +green. The `~` and `?` markers are confidence markers for the estimate. + +Filter slow rows with: + +```bash +whichllm --speed usable # >=10 tok/s +whichllm --speed fast # >=30 tok/s +whichllm --min-speed 4 # exact floor, if you want a lower threshold +``` + +For an exact threshold: + +```bash +whichllm --min-speed 10 +``` + +## LM Studio or another runtime says the model barely does not fit + +whichllm estimates model memory, but real runtimes can need extra room for +loader overhead, graph buffers, KV cache choices, and OS pressure. By default, +whichllm reserves a small automatic VRAM headroom before fit checks. + +Tune it with: + +```bash +whichllm --vram-headroom 1.5GB +whichllm --vram-headroom 10% +whichllm --vram-headroom none +``` + +Use `none` when you want the old raw-VRAM behavior. + +## RAM offload depends on what else is running + +Partial offload uses system RAM. If Docker, Elasticsearch, a browser, or +another workload is already using a large amount of memory, cap the offload +budget: + +```bash +whichllm --ram-budget available +whichllm --ram-budget 8GB +whichllm --ram-budget 50% +``` + +`available` reads the current available RAM from the OS at startup. Fixed +values are useful when you know how much memory you want to leave for other +processes. + +## Results look stale + +whichllm caches model data for 6 hours and benchmark data for 24 hours. + +Force a refresh: + +```bash +whichllm --refresh +whichllm plan "qwen 7b" --refresh +``` + +The caches live under: + +```text +~/.cache/whichllm/ +``` + +If `XDG_CACHE_HOME` is set to an absolute path, the caches live under: + +```text +$XDG_CACHE_HOME/whichllm/ +``` + +## `uvx` fails with `realpath: command not found` + +Some older macOS versions do not include a `realpath` command. If the `uvx` +launcher fails before whichllm starts, with output like: + +```text +realpath: command not found +/Users/.../python: No such file or directory +``` + +run whichllm through Python's module entry point instead: + +```bash +uvx --from whichllm python -m whichllm +``` + +Pass normal whichllm arguments after the module name: + +```bash +uvx --from whichllm python -m whichllm --gpu "RTX 4090" +``` + +## The top pick has `~`, `!sr`, or `?` + +These markers describe benchmark evidence: + +| Marker | Meaning | +| --- | --- | +| `~` | Inherited or interpolated benchmark evidence | +| `!sr` | Uploader-reported benchmark only | +| `?` | No benchmark evidence | + +Use stricter evidence when you want only independently matched benchmark data: + +```bash +whichllm --evidence strict +whichllm --direct +``` + +Use `--evidence base` when base-model matches are acceptable but interpolation +and self-reported values are not. + +## The largest model did not win + +That is expected. whichllm scores: + +- benchmark quality +- model size +- quantization loss +- full GPU vs partial offload vs CPU-only +- estimated speed +- evidence confidence +- source trust +- generation lineage + +A smaller current-generation model with strong direct evidence can beat a +larger model that only barely fits or relies on stale benchmark data. + +## Estimated speed differs from real speed + +Speed is an estimate based on: + +- model weight size +- MoE active parameters +- GPU memory bandwidth +- quantization efficiency +- backend factor +- partial-offload penalty + +Real performance depends on the inference runtime, driver, prompt length, +batching, thermal limits, and background memory pressure. + +The default ranking table shows the speed estimate and its confidence marker. +Use `--details` only when you want download counts instead. + +Speed colors and markers: + +- red: slow generation speed, under 4 tok/s +- yellow: marginal generation speed, 4-10 tok/s +- green: usable generation speed, 10-30 tok/s +- bright green: fast local generation speed, 30+ tok/s +- `~`: estimated speed range is available +- `?`: low-confidence estimate; runtime/backend differences can be large + +JSON includes the same information as `speed_confidence`, +`speed_range_tok_per_sec`, and `speed_notes`. + +## Apple Silicon partial offload looks different + +Apple Silicon uses unified memory. Partial offload does not cross a discrete +PCIe boundary, so whichllm applies a milder speed penalty than it does for +discrete GPUs. + +The same is true for recognized AMD shared-memory APUs such as Strix Halo, +Ryzen AI MAX, and Ryzen AI / Radeon 890M-class integrated graphics. +DGX Spark / NVIDIA GB10 is handled the same way when NVIDIA reports GPU memory +as unavailable. + +On Windows, `Win32_VideoController.AdapterRAM` can cap around 4 GB. whichllm +uses the 64-bit registry memory value when it is available, and treats known +shared-memory APUs as unified-memory style devices instead of tiny discrete +GPUs. + +## `run` says `uv is required` + +Install `uv` first: + +```bash +curl -LsSf https://astral.sh/uv/install.sh | sh +``` + +Then retry: + +```bash +whichllm run +``` + +## `run` cannot download a model + +Possible causes: + +- the model is gated on HuggingFace +- local HuggingFace authentication is missing +- the selected GGUF filename no longer exists +- network access failed +- disk space is too low + +Try a known public GGUF model first: + +```bash +whichllm run "qwen 2.5 1.5b gguf" +``` + +## Hugging Face API access fails or needs a mirror + +whichllm uses the Hugging Face API to fetch model metadata. If direct access to +`huggingface.co` fails in your network, set `HF_ENDPOINT` to a compatible +endpoint root: + +```powershell +$env:HF_ENDPOINT = "https://huggingface.co" +whichllm --refresh +``` + +```bash +HF_ENDPOINT="https://huggingface.co" whichllm --refresh +``` + +Do not include `/api` in `HF_ENDPOINT`; whichllm adds that path internally. + +## How much disk space does `run` need? + +Normal ranking commands do not download model weights. They cache Hugging Face +model metadata and benchmark metadata under the whichllm cache. + +`whichllm run` downloads the selected GGUF file through `huggingface_hub`. The +required disk space is roughly the selected GGUF file size plus normal Hugging +Face cache overhead. + +By default, Hugging Face stores downloaded files under: + +```text +~/.cache/huggingface/hub +``` + +You can move that cache by setting `HF_HOME` or `HF_HUB_CACHE`. + +Cleanup is handled by the Hugging Face cache tools: + +```bash +hf cache scan +hf cache delete +``` + +whichllm does not currently delete model files automatically after a run. + +## Ollama names do not match HuggingFace IDs + +JSON output returns HuggingFace repo IDs: + +```bash +whichllm --top 1 --json | jq -r '.models[0].model_id' +``` + +Ollama model names often use a different naming scheme. Map the HuggingFace ID +to your local Ollama model name before calling `ollama run`. + +## Debugging a specific model + +Use `plan` to inspect memory requirements: + +```bash +whichllm plan "Qwen2.5-72B" --quant Q4_K_M +whichllm plan "Qwen2.5-72B" --quant Q8_0 --context-length 32768 +``` + +Use plain output when filing issues: + +```bash +whichllm --gpu "RTX 4090" --json +whichllm --gpu "RTX 4090" --markdown +whichllm hardware +``` + +Include: + +- OS +- GPU name and VRAM +- CPU and RAM +- command used +- expected result +- actual result diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..89eb955 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,58 @@ +[project] +name = "whichllm" +version = "0.5.15" +description = "Find the best LLM that runs on your hardware" +authors = [{name = "Andyyyy64"}] +readme = "README.md" +requires-python = ">=3.11" +license = "MIT" +homepage = "https://github.com/Andyyyy64/whichllm" +repository = "https://github.com/Andyyyy64/whichllm" +keywords = ["llm", "local-llm", "gpu", "vram", "huggingface", "inference", "hardware", "cli", "recommendation"] +classifiers = [ + "Development Status :: 4 - Beta", + "Environment :: Console", + "Intended Audience :: Developers", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Topic :: Scientific/Engineering :: Artificial Intelligence", + "Topic :: System :: Hardware", +] +dependencies = [ + "typer>=0.9", + "rich>=13.0", + "httpx>=0.27", + "psutil>=5.9", + "dbgpu[fuzz]>=1.0", + "nvidia-ml-py>=12.0", +] + +[project.urls] +Homepage = "https://github.com/Andyyyy64/whichllm" +Repository = "https://github.com/Andyyyy64/whichllm" +"Bug Tracker" = "https://github.com/Andyyyy64/whichllm/issues" +Changelog = "https://github.com/Andyyyy64/whichllm/blob/main/CHANGELOG.md" + +[project.scripts] +whichllm = "whichllm.cli:app" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src/whichllm"] + +[tool.pytest.ini_options] +testpaths = ["tests"] + +[dependency-groups] +dev = [ + "pytest>=8.0", + "pyarrow>=23.0.1", +] diff --git a/scripts/import_livebench_csv.py b/scripts/import_livebench_csv.py new file mode 100644 index 0000000..adba1da --- /dev/null +++ b/scripts/import_livebench_csv.py @@ -0,0 +1,116 @@ +"""Convert a LiveBench leaderboard CSV into the inlined Python dict. + +LiveBench publishes their leaderboard as a dated CSV (e.g. +``https://livebench.ai/table_2026_01_08.csv``). + +Usage: + curl https://livebench.ai/table_2026_01_08.csv | python scripts/import_livebench_csv.py + +""" + +from __future__ import annotations + +import csv +import sys + +# LiveBench CSV model name -> list of HuggingFace ids that share the score. +# When several CSV rows map onto the same HF id (e.g. thinking vs. base), +# the highest average wins. +CSV_NAME_TO_HF_IDS: dict[str, list[str]] = { + "deepseek-v3.2": ["deepseek-ai/DeepSeek-V3.2"], + "deepseek-v3.2-exp": ["deepseek-ai/DeepSeek-V3.2-Exp"], + "deepseek-v3.2-exp-thinking": ["deepseek-ai/DeepSeek-V3.2-Exp"], + "deepseek-v3.2-thinking": ["deepseek-ai/DeepSeek-V3.2"], + "deepseek-v4-flash": ["deepseek-ai/DeepSeek-V4-Flash"], + "deepseek-v4-pro": ["deepseek-ai/DeepSeek-V4-Pro"], + "devstral-2512": ["mistralai/Devstral-2512"], + "gemma-4-31b-it": ["google/gemma-4-31b-it"], + "glm-4.6": ["zai-org/GLM-4.6"], + "glm-4.6v": ["zai-org/GLM-4.6V"], + "glm-4.7": ["zai-org/GLM-4.7"], + "glm-5": ["zai-org/GLM-5"], + "glm-5.1": ["zai-org/GLM-5.1"], + "gpt-oss-120b": ["openai/gpt-oss-120b"], + "kimi-k2-instruct": ["moonshotai/Kimi-K2-Instruct"], + "kimi-k2-thinking": ["moonshotai/Kimi-K2-Thinking"], + "kimi-k2.5-thinking": ["moonshotai/Kimi-K2.5"], + "kimi-k2.6-thinking": ["moonshotai/Kimi-K2.6-Thinking"], + "mimo-v2-pro": ["XiaomiMiMo/MiMo-V2-Pro"], + "minimax-m2.5": ["MiniMaxAI/MiniMax-M2.5"], + "minimax-m2.7": ["MiniMaxAI/MiniMax-M2.7"], + "nemotron-3-super-120b-a12b": ["nvidia/Nemotron-3-Super-120B-A12B"], + "qwen3-235b-a22b-instruct-2507": ["Qwen/Qwen3-235B-A22B-Instruct-2507"], + "qwen3-235b-a22b-thinking-2507": ["Qwen/Qwen3-235B-A22B-Thinking-2507"], + "qwen3-30b-a3b-thinking": ["Qwen/Qwen3-30B-A3B-Thinking-2507"], + "qwen3-32b-thinking": ["Qwen/Qwen3-32B"], + "qwen3-next-80b-a3b-instruct": ["Qwen/Qwen3-Next-80B-A3B-Instruct"], + "qwen3-next-80b-a3b-thinking": ["Qwen/Qwen3-Next-80B-A3B-Thinking"], + "qwen3.6-27b": ["Qwen/Qwen3.6-27B"], +} + + +def row_average(row: dict[str, str]) -> float | None: + nums: list[float] = [] + for key, value in row.items(): + if key == "model" or not value: + continue + try: + nums.append(float(value)) + except ValueError: + continue + if not nums: + return None + return sum(nums) / len(nums) + + +def main(argv: list[str]) -> int: + rows = list(csv.DictReader(sys.stdin)) + + best: dict[str, float] = {} + matched: set[str] = set() + for row in rows: + name = row["model"] + hf_ids = CSV_NAME_TO_HF_IDS.get(name) + if not hf_ids: + continue + avg = row_average(row) + if avg is None: + continue + matched.add(name) + for hf_id in hf_ids: + if avg > best.get(hf_id, 0.0): + best[hf_id] = avg + + unmapped_open = [ + row["model"] + for row in rows + if row["model"] not in matched + and not any( + tok in row["model"] + for tok in ( + "claude", + "gpt-", + "gemini", + "grok", + "arcee", + "elephant", + ) + ) + ] + if unmapped_open: + print( + f"# note: {len(unmapped_open)} unmapped row(s) in CSV — extend " + "CSV_NAME_TO_HF_IDS if any are open-weight:", + " ".join(sorted(unmapped_open)), + file=sys.stderr, + ) + + print("{") + for hf_id in sorted(best): + print(f' "{hf_id}": {best[hf_id]:.1f},') + print("}") + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv)) diff --git a/src/whichllm/__init__.py b/src/whichllm/__init__.py new file mode 100644 index 0000000..c55dc64 --- /dev/null +++ b/src/whichllm/__init__.py @@ -0,0 +1 @@ +"""whichllm: Find the best LLM that runs on your hardware.""" diff --git a/src/whichllm/__main__.py b/src/whichllm/__main__.py new file mode 100644 index 0000000..53d3c5c --- /dev/null +++ b/src/whichllm/__main__.py @@ -0,0 +1,7 @@ +"""Module entry point for ``python -m whichllm``.""" + +from whichllm.cli import app + + +if __name__ == "__main__": + app() diff --git a/src/whichllm/cli.py b/src/whichllm/cli.py new file mode 100644 index 0000000..46b1b71 --- /dev/null +++ b/src/whichllm/cli.py @@ -0,0 +1,1507 @@ +"""CLI entry point using typer.""" + +from __future__ import annotations + +import asyncio +import re +import sys +from typing import Optional + +import typer +from rich.console import Console + +from whichllm.constants import _GiB +from whichllm.hardware.types import HardwareInfo +from whichllm.models.artifacts import ( + attach_resolved_artifacts, + find_gguf_variant, + resolve_ranked_gguf_artifact, +) +from whichllm.models.types import ModelInfo +from whichllm.utils import _current_version, CONTEXT_LENGTH + +app = typer.Typer( + name="llm-checker", + help="Find the best LLM that runs on your hardware.", + no_args_is_help=False, + invoke_without_command=True, +) +console = Console() + +_find_gguf_variant = find_gguf_variant +_resolve_ranked_gguf_for_run = resolve_ranked_gguf_artifact + + +def _run_async(coro): + """Run async coroutine from sync context.""" + return asyncio.run(coro) + + +def _format_fetch_error(error: Exception) -> str: + """Return a useful one-line fetch error even when str(error) is empty.""" + detail = str(error).strip() + if detail: + return detail + + response = getattr(error, "response", None) + request = getattr(error, "request", None) or getattr(response, "request", None) + status_code = getattr(response, "status_code", None) + url = getattr(request, "url", None) + if status_code and url: + return f"{type(error).__name__}: HTTP {status_code} for {url}" + if url: + return f"{type(error).__name__} while requesting {url}" + return f"{type(error).__name__} with no detail from the network layer" + + +def _print_version(value: bool) -> None: + """Print version and exit when --version is requested.""" + if value: + console.print(_current_version()) + raise typer.Exit() + + +def _validate_gpu_flags( + cpu_only: bool, + gpu: list[str] | None, + vram: float | None, + bandwidth: float | None = None, + gpu_index: int | None = None, +) -> None: + """Validate mutual exclusivity of GPU-related flags.""" + if cpu_only and gpu: + console.print("[red]Error:[/] --cpu-only and --gpu are mutually exclusive.") + raise typer.Exit(code=1) + if cpu_only and (vram is not None or bandwidth is not None): + console.print("[red]Error:[/] --cpu-only cannot be used with GPU overrides.") + raise typer.Exit(code=1) + if vram is not None and vram <= 0: + console.print("[red]Error:[/] --vram must be greater than 0.") + raise typer.Exit(code=1) + if bandwidth is not None and bandwidth <= 0: + console.print("[red]Error:[/] --bandwidth must be greater than 0.") + raise typer.Exit(code=1) + if gpu_index is not None and gpu_index < 0: + console.print("[red]Error:[/] --gpu-index must be 0 or greater.") + raise typer.Exit(code=1) + if gpu_index is not None and gpu: + console.print("[red]Error:[/] --gpu-index only applies to detected GPUs.") + raise typer.Exit(code=1) + if gpu_index is not None and vram is None and bandwidth is None: + console.print("[red]Error:[/] --gpu-index requires --vram or --bandwidth.") + raise typer.Exit(code=1) + + +def _validate_output_flags(json_output: bool, markdown_output: bool) -> None: + """Validate mutually exclusive output formats.""" + if json_output and markdown_output: + console.print("[red]Error:[/] --json and --markdown are mutually exclusive.") + raise typer.Exit(code=1) + + +def _validate_ranking_flags( + top: int, + min_speed: float | None, + min_params: float | None, +) -> None: + """Validate ranking/filter flags that otherwise silently distort output. + + Without these guards a non-positive ``--top`` reaches ``results[:top_n]`` in + :func:`whichllm.engine.ranker.rank_models`: ``--top 0`` returns no + recommendations at all, and a negative value slices from the end + (``results[:-5]``), silently returning a truncated, unrequested subset + instead of the count the user asked for. Negative ``--min-speed`` / + ``--min-params`` thresholds are likewise meaningless. Fail fast with a clear + message instead of producing misleading results. + """ + if top < 1: + console.print("[red]Error:[/] --top must be 1 or greater.") + raise typer.Exit(code=1) + if min_speed is not None and min_speed < 0: + console.print("[red]Error:[/] --min-speed must be 0 or greater.") + raise typer.Exit(code=1) + if min_params is not None and min_params < 0: + console.print("[red]Error:[/] --min-params must be 0 or greater.") + raise typer.Exit(code=1) + + +def _validate_profile(profile: str) -> str: + """Validate ranking profile option.""" + valid = {"general", "coding", "vision", "math", "any"} + p = profile.lower() + if p not in valid: + console.print( + "[red]Error:[/] --profile must be one of: general, coding, vision, math, any." + ) + raise typer.Exit(code=1) + return p + + +def _validate_evidence(evidence: str) -> str: + """Validate evidence mode option.""" + valid = {"strict", "base", "any"} + mode = evidence.lower() + if mode not in valid: + console.print("[red]Error:[/] --evidence must be one of: strict, base, any.") + raise typer.Exit(code=1) + return mode + + +def _resolve_evidence_mode(evidence: str, direct: bool) -> str: + """Resolve final evidence mode, keeping --direct as strict alias.""" + mode = _validate_evidence(evidence) + if direct: + # 互換性維持のため --direct は strict と同義に固定する。 + return "strict" + return mode + + +def _resolve_fit_filter(fit: str, gpu_only: bool) -> str: + """Resolve runtime fit filtering, keeping --gpu-only as a short alias.""" + mode = fit.lower().replace("_", "-").replace(" ", "-") + if mode not in {"any", "gpu", "full-gpu", "fullgpu"}: + console.print("[red]Error:[/] --fit must be one of: any, gpu, full-gpu.") + raise typer.Exit(code=1) + if gpu_only: + return "full_gpu" + return "full_gpu" if mode in {"gpu", "full-gpu", "fullgpu"} else "any" + + +def _resolve_speed_filter(speed: str, min_speed: float | None) -> float | None: + """Resolve named speed presets while preserving --min-speed as exact input.""" + if min_speed is not None: + return min_speed + mode = speed.lower().replace("_", "-") + presets = { + "any": None, + "usable": 10.0, + "fast": 30.0, + } + if mode not in presets: + console.print("[red]Error:[/] --speed must be one of: any, usable, fast.") + raise typer.Exit(code=1) + return presets[mode] + + +_MEMORY_RE = re.compile( + r"^(?P\d+(?:\.\d+)?)\s*(?Pgib|gb|g|mib|mb|m)?$", + re.IGNORECASE, +) + + +def _parse_memory_amount( + value: str, *, option_name: str, total_bytes: int | None = None +) -> int: + """Parse memory CLI values. Bare numbers are treated as GiB.""" + raw = value.strip() + if not raw: + console.print(f"[red]Error:[/] {option_name} cannot be empty.") + raise typer.Exit(code=1) + + if raw.endswith("%"): + if total_bytes is None: + console.print(f"[red]Error:[/] {option_name} percentage needs a base size.") + raise typer.Exit(code=1) + try: + pct = float(raw[:-1]) + except ValueError: + console.print(f"[red]Error:[/] Invalid {option_name}: {value!r}.") + raise typer.Exit(code=1) + if pct < 0: + console.print(f"[red]Error:[/] {option_name} must be non-negative.") + raise typer.Exit(code=1) + return int(total_bytes * pct / 100.0) + + match = _MEMORY_RE.match(raw) + if not match: + console.print( + f"[red]Error:[/] Invalid {option_name}: {value!r}. " + "Use values like 1.5GB, 512MB, 10%, or 8." + ) + raise typer.Exit(code=1) + + number = float(match.group("number")) + unit = (match.group("unit") or "gb").lower() + if number < 0: + console.print(f"[red]Error:[/] {option_name} must be non-negative.") + raise typer.Exit(code=1) + + if unit in {"gib", "gb", "g"}: + return int(number * _GiB) + return int(number * 1024**2) + + +def _auto_vram_headroom(vram_bytes: int) -> int: + """Default runtime headroom so near-edge fits do not over-promise.""" + if vram_bytes <= 0: + return 0 + return int(max(512 * 1024**2, min(vram_bytes * 0.05, 2 * _GiB))) + + +def _parse_vram_headroom(value: str, vram_bytes: int) -> int: + mode = value.strip().lower() + if mode == "auto": + return _auto_vram_headroom(vram_bytes) + if mode in {"none", "off", "0"}: + return 0 + return _parse_memory_amount( + value, + option_name="--vram-headroom", + total_bytes=vram_bytes, + ) + + +def _apply_memory_budgets( + hardware: HardwareInfo, + *, + vram_headroom: str, + ram_budget: str | None, +) -> HardwareInfo: + """Apply user-facing memory budgets without mutating detected raw sizes.""" + headroom_mode = vram_headroom.strip().lower() + if not hardware.gpus and headroom_mode not in {"auto", "none", "off", "0"}: + _parse_memory_amount( + vram_headroom, + option_name="--vram-headroom", + total_bytes=_GiB, + ) + + reserved_values: list[int] = [] + for gpu in hardware.gpus: + reserved = _parse_vram_headroom(vram_headroom, gpu.vram_bytes) + gpu.usable_vram_bytes = max(0, gpu.vram_bytes - reserved) + if reserved > 0: + reserved_values.append(reserved) + + if reserved_values: + unique_reserved = sorted(set(reserved_values)) + if len(unique_reserved) == 1: + note = f"VRAM headroom: {_format_budget_bytes(unique_reserved[0])} reserved per GPU" + else: + note = "VRAM headroom: auto reserve applied per GPU" + hardware.budget_notes.append(note) + + if ram_budget: + mode = ram_budget.strip().lower() + if mode == "available": + from whichllm.hardware.memory import detect_available_ram_bytes + + hardware.ram_budget_bytes = detect_available_ram_bytes() + hardware.budget_notes.append( + f"RAM budget: current available {_format_budget_bytes(hardware.ram_budget_bytes)}" + ) + elif mode not in {"auto", "none", "off"}: + hardware.ram_budget_bytes = _parse_memory_amount( + ram_budget, option_name="--ram-budget", total_bytes=hardware.ram_bytes + ) + hardware.budget_notes.append( + f"RAM budget: {_format_budget_bytes(hardware.ram_budget_bytes)}" + ) + return hardware + + +def _format_budget_bytes(value: int) -> str: + if value >= _GiB: + return f"{value / _GiB:.1f} GB" + if value >= 1024**2: + return f"{value / 1024**2:.0f} MB" + return f"{value / 1024:.0f} KB" + + +def _apply_gpu_overrides( + hardware: HardwareInfo, + cpu_only: bool, + gpu: list[str] | None, + vram: float | None, + bandwidth: float | None = None, + gpu_index: int | None = None, +) -> HardwareInfo: + """Replace hardware.gpus based on CLI flags.""" + if cpu_only: + hardware.gpus = [] + elif gpu: + from whichllm.hardware.gpu_simulator import create_synthetic_gpus + + try: + hardware.gpus = create_synthetic_gpus(gpu, vram) + except ValueError as e: + console.print(f"[red]Error:[/] {e}") + raise typer.Exit(code=1) + if bandwidth is not None: + if len(hardware.gpus) != 1: + console.print( + "[red]Error:[/] --bandwidth currently supports exactly one " + "simulated GPU." + ) + raise typer.Exit(code=1) + hardware.gpus[0].memory_bandwidth_gbps = bandwidth + elif vram is not None or bandwidth is not None: + if not hardware.gpus: + console.print( + "[red]Error:[/] --vram/--bandwidth requires a detected GPU or --gpu." + ) + raise typer.Exit(code=1) + if gpu_index is None: + if len(hardware.gpus) > 1: + console.print( + "[red]Error:[/] --gpu-index is required when overriding " + "detected hardware with multiple GPUs." + ) + raise typer.Exit(code=1) + target_gpu = hardware.gpus[0] + else: + if gpu_index >= len(hardware.gpus): + console.print( + f"[red]Error:[/] --gpu-index {gpu_index} is out of range " + f"for {len(hardware.gpus)} detected GPU(s)." + ) + raise typer.Exit(code=1) + target_gpu = hardware.gpus[gpu_index] + + if vram is not None: + target_gpu.vram_bytes = int(vram * _GiB) + target_gpu.usable_vram_bytes = None + target_gpu.vram_overridden = True + if bandwidth is not None: + target_gpu.memory_bandwidth_gbps = bandwidth + return hardware + + +def _auto_min_params_for_profile(hardware: HardwareInfo, profile: str) -> float | None: + """Pick automatic min-params threshold for strongest general ranking. + + The threshold rises with VRAM so a 24GB GPU is steered away from 3-4B + toys, but tiny GPUs (4-8GB) still see full-GPU options instead of being + forced into 7B+ partial-offload-only results. + """ + if profile != "general": + return None + if not hardware.gpus: + return 2.0 # CPU-only: tiny is the only practical choice + from whichllm.hardware.memory import effective_usable_ram + + usable_ram = effective_usable_ram(hardware.ram_bytes, hardware.ram_budget_bytes) + best_vram_gb = max( + ( + usable_ram + if g.shared_memory + and (g.vram_bytes == 0 or hardware.ram_budget_bytes is not None) + else ( + g.usable_vram_bytes if g.usable_vram_bytes is not None else g.vram_bytes + ) + ) + for g in hardware.gpus + ) / (1024**3) + if best_vram_gb >= 30: + return 12.0 + if best_vram_gb >= 20: + return 10.0 + if best_vram_gb >= 12: + return 8.0 + if best_vram_gb >= 8: + return 5.0 + if best_vram_gb >= 5: + return 3.0 + return 2.0 + + +def _include_vision_candidates(profile: str) -> bool: + """候補取得時にVLMを含めるべきプロファイルか判定する。""" + return profile.lower() in {"vision", "any"} + + +def _fill_missing_published_at( + all_models: list, + results: list, + fetch_model_published_at, +) -> bool: + """上位表示で欠けている公開日時を補完し、更新有無を返す。""" + missing_ids = [r.model.id for r in results if not r.model.published_at] + if not missing_ids: + return False + published_map = _run_async(fetch_model_published_at(missing_ids)) + if not published_map: + return False + + updated = False + for model in all_models: + published_at = published_map.get(model.id) + if published_at and not model.published_at: + model.published_at = published_at + updated = True + return updated + + +def _merge_model_eval_benchmarks( + models: list, + benchmark_scores: dict[str, float], +) -> tuple[dict[str, float], int]: + """Deprecated no-op kept for backward API compatibility. + + Previously this injected each model's uploader-reported ``hf_eval`` + value into the leaderboard scores dict under the model's id, which + caused those values to be treated as ``direct`` benchmark evidence + by the ranker. That elevated any account that wrote a high number + in their model card to the top of the rankings. + + The hf_eval value is now consumed inside ``rank_models`` via + ``BenchmarkEvidence.source == "self_reported"`` with a much lower + weight and a dedicated display tag, so we no longer need to mutate + the leaderboard dict here. Returning the input unchanged keeps any + external callers working. + """ + return benchmark_scores, 0 + + +@app.callback(invoke_without_command=True) +def main( + ctx: typer.Context, + show_version: bool = typer.Option( + False, + "--version", + help="Show version and exit", + callback=_print_version, + is_eager=True, + ), + refresh: bool = typer.Option( + False, "--refresh", help="Ignore cache and re-fetch models" + ), + top: int = typer.Option(10, "--top", "-n", help="Number of top models to show"), + context_length: int = typer.Option( + 4096, + "--context-length", + "-c", + click_type=CONTEXT_LENGTH, + help="Context length for KV cache estimation (e.g. 4096, 64k, 128k)", + ), + quant: Optional[str] = typer.Option( + None, "--quant", "-q", help="Filter by quantization type (e.g. Q4_K_M)" + ), + min_speed: Optional[float] = typer.Option( + None, "--min-speed", help="Exact minimum tok/s filter" + ), + speed: str = typer.Option( + "any", + "--speed", + help="Speed preset filter: any | usable | fast", + ), + fit: str = typer.Option( + "any", + "--fit", + help="Runtime fit filter: any | gpu | full-gpu", + ), + gpu_only: bool = typer.Option( + False, + "--gpu-only", + help="Only show models that fit fully in GPU VRAM", + ), + evidence: str = typer.Option( + "any", + "--evidence", + help="Benchmark evidence filter: strict | base | any", + ), + direct: bool = typer.Option( + False, + "--direct", + help="Alias of --evidence strict", + ), + status: bool = typer.Option( + False, + "--status", + help="Show runtime columns (default; kept for compatibility)", + ), + details: bool = typer.Option( + False, + "--details", + help="Show Downloads metadata instead of runtime columns", + ), + min_params: Optional[float] = typer.Option( + None, + "--min-params", + help="Minimum effective parameter size in billions (e.g. 7)", + ), + profile: str = typer.Option( + "general", + "--profile", + help="Ranking profile: general | coding | vision | math | any", + ), + json_output: bool = typer.Option(False, "--json", help="Output as JSON"), + markdown_output: bool = typer.Option( + False, + "--markdown", + "-m", + help="Output as GitHub-Flavored Markdown", + ), + cpu_only: bool = typer.Option( + False, "--cpu-only", help="Ignore GPU and run in CPU-only mode" + ), + gpu: Optional[list[str]] = typer.Option( + None, + "--gpu", + help="Simulate GPU(s), e.g. 'RTX 4090', '2x RTX 4090', or repeat --gpu", + ), + vram: Optional[float] = typer.Option( + None, + "--vram", + help="Override simulated GPU VRAM or detected GPU usable VRAM in GB", + ), + bandwidth: Optional[float] = typer.Option( + None, + "--bandwidth", + "--ram-bandwidth", + help="Override GPU/RAM bandwidth in GB/s", + ), + gpu_index: Optional[int] = typer.Option( + None, + "--gpu-index", + help="Detected GPU index to override when multiple GPUs are present", + ), + vram_headroom: str = typer.Option( + "auto", + "--vram-headroom", + help="Reserve GPU memory for runtime overhead: auto | none | 1GB | 10%", + ), + ram_budget: Optional[str] = typer.Option( + None, + "--ram-budget", + help="RAM budget for offload: available | 8GB | 50%", + ), +): + """Detect hardware and recommend the best local LLMs.""" + if ctx.invoked_subcommand is not None: + return + + _validate_gpu_flags(cpu_only, gpu, vram, bandwidth, gpu_index) + _validate_output_flags(json_output, markdown_output) + _validate_ranking_flags(top, min_speed, min_params) + profile = _validate_profile(profile) + evidence_mode = _resolve_evidence_mode(evidence, direct) + fit_filter = _resolve_fit_filter(fit, gpu_only) + speed_filter = _resolve_speed_filter(speed, min_speed) + + from rich.progress import Progress, SpinnerColumn, TextColumn + + from whichllm.engine.ranker import rank_models + from whichllm.hardware.detector import detect_hardware + from whichllm.models.benchmark import ( + fetch_benchmark_scores, + load_benchmark_cache, + save_benchmark_cache, + ) + from whichllm.models.cache import load_cache, save_cache + from whichllm.models.fetcher import ( + dicts_to_models, + fetch_model_published_at, + fetch_models, + models_to_dicts, + ) + from whichllm.models.grouper import group_models + from whichllm.output.display import ( + display_hardware, + display_json, + display_markdown, + display_ranking, + ) + + with Progress( + SpinnerColumn(), + TextColumn("[progress.description]{task.description}"), + console=console, + transient=True, + ) as progress: + # Step 1: Detect hardware + task = progress.add_task("Detecting hardware...", total=None) + hardware = detect_hardware() + _apply_gpu_overrides(hardware, cpu_only, gpu, vram, bandwidth, gpu_index) + _apply_memory_budgets( + hardware, vram_headroom=vram_headroom, ram_budget=ram_budget + ) + progress.update(task, description="Hardware detected") + + # Step 2: Fetch models + progress.update(task, description="Loading models...") + cached_data = None if refresh else load_cache() + if cached_data is not None: + models = dicts_to_models(cached_data) + progress.update(task, description=f"Loaded {len(models)} models from cache") + else: + progress.update(task, description="Fetching models from HuggingFace...") + try: + models = _run_async( + fetch_models(include_vision=_include_vision_candidates(profile)) + ) + save_cache(models_to_dicts(models)) + progress.update(task, description=f"Fetched {len(models)} models") + except Exception as e: + console.print( + f"[red]Error fetching models:[/] {_format_fetch_error(e)}" + ) + sys.exit(1) + + # Step 3: Fetch benchmark scores + progress.update(task, description="Loading benchmark data...") + bench_scores = None if refresh else load_benchmark_cache() + if bench_scores is None: + try: + progress.update(task, description="Fetching benchmark scores...") + bench_scores = _run_async(fetch_benchmark_scores()) + save_benchmark_cache(bench_scores) + except Exception as e: + console.print(f"[yellow]Warning:[/] Benchmark data unavailable: {e}") + bench_scores = {} + + # Step 4: Group and rank + progress.update(task, description="Ranking models...") + families = group_models(models) + + # Flatten all models with their family IDs set by grouper + all_models = [] + for family in families: + all_models.append(family.base_model) + all_models.extend(family.variants) + + # NOTE: We no longer merge uploader-reported hf_eval values into the + # leaderboard scores dict — the ranker now treats them as a separate + # "self_reported" evidence tier with much lower trust. See + # ranker.lookup_benchmark_evidence + _SOURCE_WEIGHTS. + + # general用途はGPUクラスに応じた自動しきい値で小さすぎるモデルを抑制する + auto_min_params = ( + _auto_min_params_for_profile(hardware, profile) + if min_params is None + else min_params + ) + + results = rank_models( + all_models, + hardware, + context_length=context_length, + top_n=top, + quant_filter=quant, + min_speed=speed_filter, + benchmark_scores=bench_scores, + task_profile=profile, + require_direct_top=True, + min_params_b=auto_min_params, + evidence_filter=evidence_mode, + fit_filter=fit_filter, + ) + + # 自動しきい値で候補ゼロなら緩和して表示を維持する + if not results and auto_min_params is not None and min_params is None: + results = rank_models( + all_models, + hardware, + context_length=context_length, + top_n=top, + quant_filter=quant, + min_speed=speed_filter, + benchmark_scores=bench_scores, + task_profile=profile, + require_direct_top=True, + min_params_b=None, + evidence_filter=evidence_mode, + fit_filter=fit_filter, + ) + + # 上位候補の公開日時が欠けている場合のみ補完して表示品質を上げる + if results: + attach_resolved_artifacts(results, all_models, quant_filter=quant) + try: + if _fill_missing_published_at( + all_models, results, fetch_model_published_at + ): + save_cache(models_to_dicts(models)) + except Exception as e: + progress.update( + task, description=f"Published date backfill skipped: {e}" + ) + + # Display results + empty_message = None + if fit_filter == "full_gpu": + empty_message = ( + "No full-GPU models found for this hardware. " + "Remove --gpu-only or use --fit any to include partial offload " + "and CPU-only candidates." + ) + if json_output: + display_json(results, hardware) + elif markdown_output: + display_markdown( + results, + hardware, + show_status=status or not details, + empty_message=empty_message, + ) + else: + console.print() + display_hardware(hardware) + console.print() + display_ranking( + results, + has_gpu=bool(hardware.gpus), + show_status=status or not details, + empty_message=empty_message, + ) + console.print() + + +@app.command() +def plan( + model_name: str = typer.Argument(..., help="Model name or HuggingFace repo ID"), + context_length: int = typer.Option( + 4096, + "--context-length", + "-c", + click_type=CONTEXT_LENGTH, + help="Context length for KV cache estimation (e.g. 4096, 64k, 128k)", + ), + quant: Optional[str] = typer.Option( + None, "--quant", "-q", help="Target quantization (default: Q4_K_M)" + ), + json_output: bool = typer.Option(False, "--json", help="Output as JSON"), + refresh: bool = typer.Option( + False, "--refresh", help="Ignore cache and re-fetch models" + ), +): + """Show what GPU you need to run a specific model.""" + from rich.progress import Progress, SpinnerColumn, TextColumn + + from whichllm.models.cache import load_cache, save_cache + from whichllm.models.fetcher import dicts_to_models, fetch_models, models_to_dicts + from whichllm.output.display import display_plan, display_plan_json + + with Progress( + SpinnerColumn(), + TextColumn("[progress.description]{task.description}"), + console=console, + transient=True, + ) as progress: + task = progress.add_task("Loading models...", total=None) + cached_data = None if refresh else load_cache() + if cached_data is not None: + models = dicts_to_models(cached_data) + else: + progress.update(task, description="Fetching models from HuggingFace...") + try: + models = _run_async(fetch_models(include_vision=True)) + save_cache(models_to_dicts(models)) + except Exception as e: + console.print( + f"[red]Error fetching models:[/] {_format_fetch_error(e)}" + ) + sys.exit(1) + + model = _search_model(models, model_name) + + target_quant = quant.upper() if quant else "Q4_K_M" + + if json_output: + display_plan_json(model, context_length, target_quant) + else: + console.print() + display_plan(model, context_length, target_quant) + console.print() + + +@app.command() +def upgrade( + target_gpus: list[str] = typer.Argument( + ..., + help="GPUs to compare against (e.g. 'RTX 4090' 'RTX 5090' 'H100')", + ), + context_length: int = typer.Option( + 8192, + "--context-length", + "-c", + click_type=CONTEXT_LENGTH, + help="Context length for ranking (e.g. 8192, 64k, 128k)", + ), + top: int = typer.Option(3, "--top", "-n", help="Best-N models to compare per GPU"), + profile: str = typer.Option("general", "--profile", help="Ranking profile"), + cpu_only: bool = typer.Option( + False, "--cpu-only", help="Compare against a CPU-only baseline" + ), + json_output: bool = typer.Option(False, "--json"), + refresh: bool = typer.Option(False, "--refresh"), +): + """Compare the current machine against potential GPU upgrades. + + For each GPU passed on the command line, simulate a system with the same + CPU/RAM but that GPU, run the ranker, and show the best-N models you'd + be able to run. Useful for answering "is upgrading from a 3090 to a 4090 + worth it?" — the table shows the quality jump and the speed jump for + each option. + """ + from rich.progress import Progress, SpinnerColumn, TextColumn + + from whichllm.engine.ranker import rank_models + from whichllm.hardware.detector import detect_hardware + from whichllm.hardware.gpu_simulator import create_synthetic_gpu + from whichllm.hardware.types import HardwareInfo + from whichllm.models.benchmark import ( + fetch_benchmark_scores, + load_benchmark_cache, + save_benchmark_cache, + ) + from whichllm.models.cache import load_cache, save_cache + from whichllm.models.fetcher import dicts_to_models, fetch_models, models_to_dicts + from whichllm.models.grouper import group_models + from whichllm.output.display import display_upgrade, display_upgrade_json + + profile = _validate_profile(profile) + _validate_ranking_flags(top, None, None) + + with Progress( + SpinnerColumn(), + TextColumn("[progress.description]{task.description}"), + console=console, + transient=True, + ) as progress: + task = progress.add_task("Detecting hardware...", total=None) + current_hw = detect_hardware() + if cpu_only: + current_hw.gpus = [] + + progress.update(task, description="Loading models...") + cached_data = None if refresh else load_cache() + if cached_data is not None: + models = dicts_to_models(cached_data) + else: + progress.update(task, description="Fetching models from HuggingFace...") + try: + models = _run_async(fetch_models(include_vision=False)) + save_cache(models_to_dicts(models)) + except Exception as e: + console.print( + f"[red]Error fetching models:[/] {_format_fetch_error(e)}" + ) + raise typer.Exit(code=1) + + progress.update(task, description="Loading benchmark data...") + bench_scores = None if refresh else load_benchmark_cache() + if bench_scores is None: + try: + bench_scores = _run_async(fetch_benchmark_scores()) + save_benchmark_cache(bench_scores) + except Exception: + bench_scores = {} + + all_models: list = [] + for family in group_models(models): + all_models.append(family.base_model) + all_models.extend(family.variants) + + def _rank_for(hw: HardwareInfo): + min_p = _auto_min_params_for_profile(hw, profile) + results = rank_models( + all_models, + hw, + context_length=context_length, + top_n=top, + benchmark_scores=bench_scores, + task_profile=profile, + require_direct_top=True, + min_params_b=min_p, + ) + if not results and min_p is not None: + results = rank_models( + all_models, + hw, + context_length=context_length, + top_n=top, + benchmark_scores=bench_scores, + task_profile=profile, + require_direct_top=True, + min_params_b=None, + ) + return results + + progress.update(task, description="Ranking current hardware...") + current_results = _rank_for(current_hw) + + target_results: list[tuple[str, HardwareInfo, list]] = [] + for raw_name in target_gpus: + progress.update(task, description=f"Ranking {raw_name}...") + try: + synthetic = create_synthetic_gpu(raw_name) + except ValueError as e: + console.print(f"[yellow]Skipping {raw_name}:[/] {e}") + continue + sim_hw = HardwareInfo( + gpus=[synthetic], + cpu_name=current_hw.cpu_name, + cpu_cores=current_hw.cpu_cores, + has_avx2=current_hw.has_avx2, + has_avx512=current_hw.has_avx512, + ram_bytes=current_hw.ram_bytes, + disk_free_bytes=current_hw.disk_free_bytes, + os=current_hw.os, + ) + sim_results = _rank_for(sim_hw) + target_results.append((raw_name, sim_hw, sim_results)) + + if json_output: + display_upgrade_json(current_hw, current_results, target_results) + else: + console.print() + display_upgrade(current_hw, current_results, target_results) + console.print() + + +def _load_models(refresh: bool, include_vision: bool = True): + """Load models from cache or fetch from HuggingFace.""" + from whichllm.models.cache import load_cache, save_cache + from whichllm.models.fetcher import dicts_to_models, fetch_models, models_to_dicts + + cached_data = None if refresh else load_cache() + if cached_data is not None: + return dicts_to_models(cached_data) + try: + models = _run_async(fetch_models(include_vision=include_vision)) + save_cache(models_to_dicts(models)) + return models + except Exception as e: + console.print(f"[red]Error fetching models:[/] {_format_fetch_error(e)}") + sys.exit(1) + + +_SIZE_TOKEN_RE = re.compile(r"^(\d+(?:\.\d+)?)([bm])$", re.IGNORECASE) + + +def _parse_size_tokens( + terms: list[str], +) -> tuple[list[str], float | None]: + """Split query terms into non-size terms and an optional size in billions. + + Returns (remaining_terms, size_b) where size_b is None if no size token + was found. Only the first size token is used; subsequent size tokens are + kept as plain text terms. Handles 'b' (billions) and 'm' (millions). + """ + remaining = [] + size_b: float | None = None + for t in terms: + m = _SIZE_TOKEN_RE.match(t) + if m and size_b is None: + value = float(m.group(1)) + if value <= 0: + remaining.append(t) + continue + unit = m.group(2).lower() + size_b = value if unit == "b" else value / 1000.0 + else: + remaining.append(t) + return remaining, size_b + + +_ID_SIZE_RE = re.compile(r"(?:^|[-_/])(\d+(?:\.\d+)?)(b|m)(?:[-_.]|$)", re.IGNORECASE) + + +def _extract_id_size_b(model_id: str) -> float | None: + """Extract the size label from a model ID string, in billions. + + Scans for patterns like '7B', '1.7B', '500M' at word boundaries in the + model ID. Returns the first match converted to billions, or None. + """ + for m in _ID_SIZE_RE.finditer(model_id): + value = float(m.group(1)) + if value <= 0: + continue + unit = m.group(2).lower() + return value if unit == "b" else value / 1000.0 + return None + + +def _size_compatible(model: ModelInfo, size_b: float) -> bool: + """Check whether a model's parameter count is compatible with a query size. + + Uses a tolerance band of [0.7x, 1.5x] to accommodate rounding differences + (e.g. a 7B query matching a model with 7.6B actual parameters) while + rejecting adjacent model sizes (e.g. 7B vs 4B or 12B). + """ + if model.parameter_count <= 0: + return True + actual_b = model.parameter_count / 1e9 + ratio = actual_b / size_b + return 0.7 <= ratio <= 1.5 + + +def _search_model(models: list, model_name: str): + """Search for a model by name/ID. Returns single model or exits.""" + query_lower = model_name.lower() + terms = query_lower.split() + size_b = None + + matches = [m for m in models if m.id.lower() == query_lower] + if not matches: + matches = [m for m in models if m.id.lower().endswith("/" + query_lower)] + if not matches: + text_terms, size_b = _parse_size_tokens(terms) + matches = [ + m + for m in models + if all(t in m.id.lower() for t in text_terms) + and (size_b is None or _size_compatible(m, size_b)) + ] + + if not matches: + console.print(f"[red]No model found matching '{model_name}'.[/]") + suggestions = [m for m in models if any(t in m.id.lower() for t in terms)] + if suggestions: + suggestions.sort(key=lambda m: m.downloads, reverse=True) + console.print("\n[yellow]Did you mean:[/]") + for m in suggestions[:5]: + p = ( + f"{m.parameter_count / 1e9:.1f}B" + if m.parameter_count >= 1e9 + else f"{m.parameter_count / 1e6:.0f}M" + ) + console.print(f" • {m.id} ({p})") + raise typer.Exit(code=1) + + if size_b is not None: + + def _sort_key(m: ModelInfo) -> tuple: + id_size = _extract_id_size_b(m.id) + has_id_size = id_size is not None + id_dist = abs(id_size - size_b) if has_id_size else float("inf") + pc_dist = ( + abs(m.parameter_count / 1e9 - size_b) + if m.parameter_count > 0 + else float("inf") + ) + return ( + 0 if (has_id_size or m.parameter_count > 0) else 1, + id_dist, + pc_dist, + -m.downloads, + ) + + matches.sort(key=_sort_key) + else: + matches.sort(key=lambda m: m.downloads, reverse=True) + model = matches[0] + if len(matches) > 1: + console.print(f"[dim]Found {len(matches)} matches, using: {model.id}[/]") + return model + + +def _pick_gguf_variant(model, quant_filter: str | None = None): + """Pick the best GGUF variant for a model.""" + from whichllm.constants import QUANT_PREFERENCE_ORDER + + if not model.gguf_variants: + return None + + if quant_filter: + for v in model.gguf_variants: + if v.quant_type.upper() == quant_filter.upper(): + return v + console.print( + f"[yellow]Warning:[/] {quant_filter} not available, using best match." + ) + + # Pick by preference order + variant_map = {v.quant_type.upper(): v for v in model.gguf_variants} + for qt in QUANT_PREFERENCE_ORDER: + if qt in variant_map: + return variant_map[qt] + return model.gguf_variants[0] + + +def _resolve_model_deps(model, variant) -> tuple[list[str], str]: + """Determine pip dependencies and script type for a model. + + Returns (deps, script_type) where script_type is 'gguf' or 'transformers'. + """ + if variant: + return ["llama-cpp-python", "huggingface-hub"], "gguf" + + from whichllm.engine.quantization import infer_non_gguf_quant_type + + qt = infer_non_gguf_quant_type(model.id) + base = ["transformers", "torch", "accelerate"] + if qt == "AWQ": + return [*base, "autoawq"], "transformers" + if qt == "GPTQ": + return [*base, "auto-gptq"], "transformers" + return base, "transformers" + + +def _generate_chat_script(model, variant, context_length: int, cpu_only: bool) -> str: + """Generate a self-contained Python chat script for any model type.""" + if variant: + n_gpu = 0 if cpu_only else -1 + return f'''\ +from huggingface_hub import hf_hub_download +from llama_cpp import Llama + +print("Downloading {model.id} ({variant.quant_type})...") +model_path = hf_hub_download(repo_id="{model.id}", filename="{variant.filename}") +print("Loading model...") +llm = Llama( + model_path=model_path, + n_ctx={context_length}, + n_gpu_layers={n_gpu}, + verbose=False, +) +print("Ready! Type 'exit' to quit.\\n") +messages = [] +while True: + try: + user_input = input("> ") + except (KeyboardInterrupt, EOFError): + break + if user_input.strip().lower() in ("exit", "quit", "q"): + break + if not user_input.strip(): + continue + messages.append({{"role": "user", "content": user_input}}) + response = llm.create_chat_completion(messages=messages, stream=True) + full = "" + for chunk in response: + delta = chunk["choices"][0].get("delta", {{}}) + content = delta.get("content", "") + if content: + print(content, end="", flush=True) + full += content + print() + messages.append({{"role": "assistant", "content": full}}) +print("\\nBye!") +''' + + device_map = '"cpu"' if cpu_only else '"auto"' + dtype = "torch.float32" if cpu_only else '"auto"' + return f'''\ +import shutil +import tempfile +import torch +from threading import Thread +from transformers import AutoModelForCausalLM, AutoTokenizer, TextIteratorStreamer + +model_id = "{model.id}" +offload_folder = tempfile.mkdtemp(prefix="whichllm_transformers_offload_") +try: + print(f"Loading {{model_id}}...") + tokenizer = AutoTokenizer.from_pretrained(model_id, trust_remote_code=True) + model = AutoModelForCausalLM.from_pretrained( + model_id, + device_map={device_map}, + torch_dtype={dtype}, + trust_remote_code=True, + offload_folder=offload_folder, + ) + print("Ready! Type 'exit' to quit.\\n") + messages = [] + while True: + try: + user_input = input("> ") + except (KeyboardInterrupt, EOFError): + break + if user_input.strip().lower() in ("exit", "quit", "q"): + break + if not user_input.strip(): + continue + messages.append({{"role": "user", "content": user_input}}) + inputs = tokenizer.apply_chat_template( + messages, + return_tensors="pt", + return_dict=True, + add_generation_prompt=True, + ).to(model.device) + streamer = TextIteratorStreamer( + tokenizer, skip_prompt=True, skip_special_tokens=True + ) + thread = Thread( + target=model.generate, + kwargs=dict(**inputs, max_new_tokens=512, streamer=streamer), + ) + thread.start() + full = "" + for text in streamer: + print(text, end="", flush=True) + full += text + thread.join() + print() + messages.append({{"role": "assistant", "content": full}}) + print("\\nBye!") +finally: + try: + del model + except NameError: + pass + shutil.rmtree(offload_folder, ignore_errors=True) +''' + + +@app.command() +def run( + model_name: Optional[str] = typer.Argument( + None, help="Model to run (default: auto-pick best)" + ), + context_length: int = typer.Option( + 4096, + "--context-length", + "-c", + click_type=CONTEXT_LENGTH, + help="Context length (e.g. 4096, 64k, 128k)", + ), + quant: Optional[str] = typer.Option( + None, "--quant", "-q", help="Quantization type" + ), + refresh: bool = typer.Option(False, "--refresh", help="Ignore cache"), + cpu_only: bool = typer.Option(False, "--cpu-only", help="CPU-only mode"), +): + """Download and chat with a model. Picks the best one if none specified.""" + import os + import shutil + import subprocess + import tempfile + + if not shutil.which("uv"): + console.print("[red]uv is required.[/]") + console.print( + "Install: [bold]curl -LsSf https://astral.sh/uv/install.sh | sh[/]" + ) + raise typer.Exit(code=1) + + from rich.progress import Progress, SpinnerColumn, TextColumn + + with Progress( + SpinnerColumn(), + TextColumn("[progress.description]{task.description}"), + console=console, + transient=True, + ) as progress: + task = progress.add_task("Loading models...", total=None) + models = _load_models(refresh) + progress.remove_task(task) + + variant = None + if model_name: + model = _search_model(models, model_name) + else: + from whichllm.engine.ranker import rank_models + from whichllm.hardware.detector import detect_hardware + from whichllm.models.benchmark import load_benchmark_cache + from whichllm.models.grouper import group_models + + hardware = detect_hardware() + if cpu_only: + hardware.gpus = [] + bench_scores = load_benchmark_cache() or {} + families = group_models(models) + all_models = [] + for family in families: + all_models.append(family.base_model) + all_models.extend(family.variants) + + results = rank_models( + all_models, + hardware, + context_length=context_length, + top_n=5, + quant_filter=quant, + benchmark_scores=bench_scores, + ) + if not results: + console.print("[red]No runnable model found for your hardware.[/]") + raise typer.Exit(code=1) + skipped_gguf: list[str] = [] + model = None + for ranked in results: + if ranked.gguf_variant: + resolved = resolve_ranked_gguf_artifact( + ranked.model, + ranked.gguf_variant, + all_models, + quant_filter=quant, + ) + if resolved: + resolved_model, variant = resolved + if resolved_model.id != ranked.model.id: + console.print( + "[dim]Resolved GGUF runtime: " + f"{ranked.model.id} -> {resolved_model.id} " + f"({variant.quant_type})[/]" + ) + model = resolved_model + quant = variant.quant_type + break + skipped_gguf.append(ranked.model.id) + continue + + model = ranked.model + break + + if skipped_gguf: + skipped = ", ".join(skipped_gguf[:3]) + suffix = "..." if len(skipped_gguf) > 3 else "" + console.print( + "[yellow]Warning:[/] Skipped GGUF-ranked candidate(s) without " + f"a matching runnable GGUF repo: {skipped}{suffix}" + ) + if model is None: + console.print( + "[red]Error:[/] Top recommendations require GGUF builds, " + "but no matching GGUF repos were found." + ) + console.print( + "[dim]Try specifying a GGUF model explicitly, for example " + '`whichllm run "qwen gguf"`.[/]' + ) + raise typer.Exit(code=1) + + if variant is None: + variant = _pick_gguf_variant(model, quant) + deps, script_type = _resolve_model_deps(model, variant) + script = _generate_chat_script(model, variant, context_length, cpu_only) + + fmt = variant.quant_type if variant else script_type.upper() + console.print(f"\n[bold green]Running {model.id}[/] [dim]({fmt})[/]") + console.print(f"[dim]Setting up isolated env with: {', '.join(deps)}[/]\n") + + fd, script_path = tempfile.mkstemp(suffix=".py", prefix="whichllm_run_") + try: + with os.fdopen(fd, "w") as f: + f.write(script) + cmd = ["uv", "run", "--no-project"] + for dep in deps: + cmd.extend(["--with", dep]) + cmd.append(script_path) + result = subprocess.run(cmd) + raise typer.Exit(code=result.returncode) + finally: + os.unlink(script_path) + + +@app.command() +def snippet( + model_name: Optional[str] = typer.Argument( + None, help="Model to show snippet for (default: auto-pick best)" + ), + quant: Optional[str] = typer.Option( + None, "--quant", "-q", help="Quantization type" + ), + refresh: bool = typer.Option(False, "--refresh", help="Ignore cache"), +): + """Print a ready-to-run Python script for a model.""" + from rich.progress import Progress, SpinnerColumn, TextColumn + from rich.syntax import Syntax + + with Progress( + SpinnerColumn(), + TextColumn("[progress.description]{task.description}"), + console=console, + transient=True, + ) as progress: + task = progress.add_task("Loading models...", total=None) + models = _load_models(refresh) + progress.remove_task(task) + + if model_name: + model = _search_model(models, model_name) + else: + gguf_models = [m for m in models if m.gguf_variants] + if not gguf_models: + console.print("[red]No GGUF models found.[/]") + raise typer.Exit(code=1) + gguf_models.sort(key=lambda m: m.downloads, reverse=True) + model = gguf_models[0] + + variant = _pick_gguf_variant(model, quant) + deps, _ = _resolve_model_deps(model, variant) + + if variant: + code = f'''\ +from llama_cpp import Llama + +llm = Llama.from_pretrained( + repo_id="{model.id}", + filename="{variant.filename}", + n_ctx=4096, + n_gpu_layers=-1, # -1 = all layers on GPU, 0 = CPU only + verbose=False, +) + +output = llm.create_chat_completion( + messages=[{{"role": "user", "content": "Hello!"}}], +) +print(output["choices"][0]["message"]["content"]) +''' + else: + code = f'''\ +from transformers import AutoModelForCausalLM, AutoTokenizer + +model_id = "{model.id}" +tokenizer = AutoTokenizer.from_pretrained(model_id, trust_remote_code=True) +model = AutoModelForCausalLM.from_pretrained( + model_id, device_map="auto", torch_dtype="auto", trust_remote_code=True, +) + +inputs = tokenizer("Hello!", return_tensors="pt").to(model.device) +outputs = model.generate(**inputs, max_new_tokens=256) +print(tokenizer.decode(outputs[0], skip_special_tokens=True)) +''' + + dep_str = " ".join(f"--with {d}" for d in deps) + console.print(f"\n[bold]{model.id}[/]") + console.print(f"[dim]# Run directly:[/] whichllm run '{model.id}'") + console.print(f"[dim]# Or manually:[/] uv run --no-project {dep_str} script.py\n") + console.print(Syntax(code, "python", theme="monokai")) + + +@app.command() +def hardware( + cpu_only: bool = typer.Option( + False, "--cpu-only", help="Ignore GPU and run in CPU-only mode" + ), + gpu: Optional[list[str]] = typer.Option( + None, + "--gpu", + help="Simulate GPU(s), e.g. 'RTX 4090', '2x RTX 4090', or repeat --gpu", + ), + vram: Optional[float] = typer.Option( + None, + "--vram", + help="Override simulated GPU VRAM or detected GPU usable VRAM in GB", + ), + bandwidth: Optional[float] = typer.Option( + None, + "--bandwidth", + "--ram-bandwidth", + help="Override GPU/RAM bandwidth in GB/s", + ), + gpu_index: Optional[int] = typer.Option( + None, + "--gpu-index", + help="Detected GPU index to override when multiple GPUs are present", + ), +): + """Show detected hardware information only.""" + _validate_gpu_flags(cpu_only, gpu, vram, bandwidth, gpu_index) + + from rich.progress import Progress, SpinnerColumn, TextColumn + + from whichllm.hardware.detector import detect_hardware + from whichllm.output.display import display_hardware + + with Progress( + SpinnerColumn(), + TextColumn("[progress.description]{task.description}"), + console=console, + transient=True, + ) as progress: + task = progress.add_task("Detecting hardware...", total=None) + hw = detect_hardware() + _apply_gpu_overrides(hw, cpu_only, gpu, vram, bandwidth, gpu_index) + progress.remove_task(task) + + console.print() + display_hardware(hw) + console.print() + + +if __name__ == "__main__": + app() diff --git a/src/whichllm/constants.py b/src/whichllm/constants.py new file mode 100644 index 0000000..a67354d --- /dev/null +++ b/src/whichllm/constants.py @@ -0,0 +1,54 @@ +"""Compatibility shim: curated registries now live under ``whichllm.data``. + +This module re-exports the same names so existing imports +(``from whichllm.constants import ...``) keep working. New code should import +from the specific ``whichllm.data.*`` submodule instead. +""" + +from whichllm.data.framework import ( + FRAMEWORK_OVERHEAD_BYTES, + MIN_COMPUTE_CAPABILITY_OLLAMA, + MIN_COMPUTE_CAPABILITY_VLLM, +) +from whichllm.data.gpu import ( + _GiB, + AMD_SHARED_MEMORY_APU_MARKERS, + CURATED_GPU_SPECS, + CuratedGPUSpec, + GPU_BANDWIDTH, + GPU_MEMORY_CLOCK_VARIANTS, + INTEL_PCI_DEVICE_NAMES, + NVIDIA_COMPUTE_CAPABILITY, + VULKAN_ONLY_GPUS, +) +from whichllm.data.lineage import ( + MODEL_GENERATION_BONUS_MAX, + MODEL_GENERATION_PENALTY_MAX, + MODEL_LINEAGE_VERSIONS, +) +from whichllm.data.quantization import ( + QUANT_BYTES_PER_WEIGHT, + QUANT_PREFERENCE_ORDER, + QUANT_QUALITY_PENALTY, +) + +__all__ = [ + "_GiB", + "AMD_SHARED_MEMORY_APU_MARKERS", + "CURATED_GPU_SPECS", + "CuratedGPUSpec", + "FRAMEWORK_OVERHEAD_BYTES", + "GPU_BANDWIDTH", + "GPU_MEMORY_CLOCK_VARIANTS", + "INTEL_PCI_DEVICE_NAMES", + "MIN_COMPUTE_CAPABILITY_OLLAMA", + "MIN_COMPUTE_CAPABILITY_VLLM", + "MODEL_GENERATION_BONUS_MAX", + "MODEL_GENERATION_PENALTY_MAX", + "MODEL_LINEAGE_VERSIONS", + "NVIDIA_COMPUTE_CAPABILITY", + "QUANT_BYTES_PER_WEIGHT", + "QUANT_PREFERENCE_ORDER", + "QUANT_QUALITY_PENALTY", + "VULKAN_ONLY_GPUS", +] diff --git a/src/whichllm/data/__init__.py b/src/whichllm/data/__init__.py new file mode 100644 index 0000000..64647dd --- /dev/null +++ b/src/whichllm/data/__init__.py @@ -0,0 +1 @@ +"""Curated reference data: GPU specs, quantization tiers, model lineage, framework limits.""" diff --git a/src/whichllm/data/framework.py b/src/whichllm/data/framework.py new file mode 100644 index 0000000..884a975 --- /dev/null +++ b/src/whichllm/data/framework.py @@ -0,0 +1,8 @@ +"""Framework overhead and minimum compute capability thresholds.""" + +# Framework overhead in bytes (~500MB) +FRAMEWORK_OVERHEAD_BYTES = 500_000_000 + +# Minimum compute capability for common frameworks +MIN_COMPUTE_CAPABILITY_OLLAMA = (5, 0) +MIN_COMPUTE_CAPABILITY_VLLM = (7, 0) diff --git a/src/whichllm/data/gpu.py b/src/whichllm/data/gpu.py new file mode 100644 index 0000000..ea87181 --- /dev/null +++ b/src/whichllm/data/gpu.py @@ -0,0 +1,299 @@ +"""GPU bandwidth, VRAM, NVIDIA compute capability, and GPU markers.""" + +from __future__ import annotations + +from typing import NamedTuple + +_GiB = 1024**3 + + +class CuratedGPUSpec(NamedTuple): + """Small curated spec for GPUs missing or ambiguous in dbgpu.""" + + name: str + vendor: str + vram_gb: float + memory_bandwidth_gbps: float + shared_memory: bool = False + + +AMD_SHARED_MEMORY_APU_MARKERS: tuple[str, ...] = ( + "STRIX HALO", + "STRXLGEN", + "RADEON 8050S", + "RADEON 8060S", + "RADEON 890M", + "RADEON 880M", + "RADEON 860M", + "RADEON 840M", + "RADEON 780M", + "RADEON 760M", + "RADEON 740M", + "RADEON 680M", + "RADEON 660M", + "RYZEN AI 9", + "RYZEN AI 7", + "RYZEN AI 5", + "RYZEN AI MAX", +) + +# GPU memory bandwidth in GB/s (theoretical peak) +# Key: substring matched against GPU name (case-insensitive) +GPU_BANDWIDTH: dict[str, float] = { + # NVIDIA Consumer - RTX 50 series + "RTX 5090": 1792.0, + "RTX 5080": 960.0, + "RTX 5070 Ti": 896.0, + "RTX 5070": 672.0, + "RTX 5060 Ti": 448.0, + "RTX 5060": 336.0, + "RTX 3050": 224.0, + # NVIDIA Consumer - RTX 40 series + "RTX 4090": 1008.0, + "RTX 4080 SUPER": 736.0, + "RTX 4080": 716.8, + "RTX 4070 Ti SUPER": 672.0, + "RTX 4070 Ti": 504.0, + "RTX 4070 SUPER": 504.0, + "RTX 4070": 504.0, + "RTX 4060 Ti": 288.0, + "RTX 4060": 272.0, + # NVIDIA Consumer - RTX 30 series + "RTX 3090 Ti": 1008.0, + "RTX 3090": 936.2, + "RTX 3080 Ti": 912.4, + "RTX 3080": 760.3, + "RTX 3070 Ti": 608.3, + "RTX 3070": 448.0, + "RTX 3060 Ti": 448.0, + "RTX 3060": 360.0, + # NVIDIA Consumer - RTX 20 series + "RTX 2080 Ti": 616.0, + "RTX 2080 SUPER": 496.0, + "RTX 2080": 448.0, + "RTX 2070 SUPER": 448.0, + "RTX 2070": 448.0, + "RTX 2060 SUPER": 448.0, + "RTX 2060": 336.0, + # NVIDIA Consumer - GTX 16 series + "GTX 1660 Ti": 288.0, + "GTX 1660 SUPER": 336.0, + "GTX 1660": 192.0, + "GTX 1650 SUPER": 192.0, + # GTX 1650 GDDR5 (8 Gbps x 128-bit). A later GDDR6 revision runs 192 GB/s; + # both share the TU117 die and PCI id 0x1F82, so they are disambiguated by + # memory clock via GPU_MEMORY_CLOCK_VARIANTS below. 128 is the conservative + # default used when the memory clock is unknown. + "GTX 1650": 128.0, + # NVIDIA Data Center + "H100": 3350.0, + "H200": 4800.0, + "DGX Spark": 273.0, + "GB10": 273.0, + "A100 80GB": 2039.0, + "A100 40GB": 1555.0, + "A100": 1555.0, + "RTX A3000 Laptop": 264.0, + "A6000": 768.0, + "A5000": 768.0, + "A4000": 448.0, + "L40S": 864.0, + "L40": 864.0, + "L4": 300.0, + "T4": 320.0, + "V100": 900.0, + "P100": 732.0, + # NVIDIA Kepler (legacy, Vulkan-only — no CUDA in modern llama.cpp). + # Values are theoretical peak memory bandwidth (GB/s) from NVIDIA + # datasheets. Kepler (compute capability 3.x) was dropped by CUDA 12 and + # current llama.cpp CUDA builds, so these cards run via the Vulkan backend + # on Linux. See VULKAN_ONLY_GPUS below. + "Quadro K6000": 288.0, + "Quadro K5200": 192.3, + "Quadro K4200": 173.0, + "Quadro K2200": 80.0, + "Quadro K620": 29.0, + "Quadro K420": 14.4, + "GTX 780": 288.4, + "GTX 770": 224.3, + "GTX 760": 192.2, + # AMD + "R9700": 640.0, + "RX 9070 XT": 640.0, + "RX 9070": 560.0, + "RX 9060 XT": 320.0, + "RX 7900 XTX": 960.0, + "RX 7900 XT": 800.0, + "RX 7800 XT": 624.0, + "RX 7700 XT": 432.0, + "RX 7600": 288.0, + "RX 6950 XT": 576.0, + "RX 6900 XT": 512.0, + "RX 6800 XT": 512.0, + "RX 6800": 512.0, + "RX 6750 XT": 432.0, + "RX 6700 XT": 384.0, + "RX 6700": 320.0, + "RX 6650 XT": 256.0, + "RX 6600 XT": 256.0, + "RX 6600": 224.0, + # AMD APUs / shared-memory graphics + "Ryzen AI MAX+ 395": 256.0, + "Ryzen AI MAX 395": 256.0, + "Radeon 890M": 120.0, + "Radeon 880M": 120.0, + "Radeon 860M": 90.0, + "Radeon 840M": 60.0, + "Radeon 780M": 90.0, + "Radeon 760M": 75.0, + "Radeon 740M": 60.0, + "Radeon 680M": 75.0, + "Radeon 660M": 55.0, + "Radeon 8060S": 256.0, + "Radeon 8050S": 256.0, + "Strix Halo": 256.0, + "STRXLGEN": 256.0, + "MI300X": 5300.0, + "MI250X": 3276.0, + "MI210": 1638.0, + # Intel discrete GPUs + "Arc Pro B70": 608.0, + "Battlemage G31": 608.0, + # Apple Silicon (unified memory bandwidth) + "M1 Ultra": 800.0, + "M1 Max": 400.0, + "M1 Pro": 200.0, + "M1": 68.25, + "M2 Ultra": 800.0, + "M2 Max": 400.0, + "M2 Pro": 200.0, + "M2": 100.0, + "M3 Ultra": 800.0, + "M3 Max": 400.0, + "M3 Pro": 150.0, + "M3": 100.0, + "M4 Ultra": 819.2, + "M4 Max": 546.0, + "M4 Pro": 273.0, + "M4": 120.0, + "M5 Max": 614.0, + "M5 Pro": 307.0, + "M5": 153.0, +} + +CURATED_GPU_SPECS: dict[str, CuratedGPUSpec] = { + "Arc Pro B70": CuratedGPUSpec( + name="Intel Arc Pro B70", + vendor="intel", + vram_gb=32.0, + memory_bandwidth_gbps=608.0, + ), + "Battlemage G31": CuratedGPUSpec( + name="Battlemage G31 [Intel Graphics]", + vendor="intel", + vram_gb=32.0, + memory_bandwidth_gbps=608.0, + ), +} + +INTEL_PCI_DEVICE_NAMES: dict[str, str] = { + "0xe223": "Battlemage G31 [Intel Graphics]", +} + +# NVIDIA GPU compute capability lookup (substring match, case-insensitive) +NVIDIA_COMPUTE_CAPABILITY: dict[str, tuple[int, int]] = { + # RTX 50 series (Blackwell) + "RTX 5090": (10, 0), + "RTX 5080": (10, 0), + "RTX 5070": (10, 0), + "RTX 5070 Ti": (10, 0), + "RTX 5060": (10, 0), + # RTX 40 series (Ada Lovelace) + "RTX 4090": (8, 9), + "RTX 4080": (8, 9), + "RTX 4070": (8, 9), + "RTX 4060": (8, 9), + # RTX 30 series (Ampere) + "RTX 3090": (8, 6), + "RTX 3080": (8, 6), + "RTX 3070": (8, 6), + "RTX 3060": (8, 6), + # RTX 20 series (Turing) + "RTX 2080": (7, 5), + "RTX 2070": (7, 5), + "RTX 2060": (7, 5), + # GTX 16 series (Turing) + "GTX 1660": (7, 5), + "GTX 1650 SUPER": (7, 5), + "GTX 1650": (7, 5), + # GTX 10 series (Pascal) + "GTX 1080": (6, 1), + "GTX 1070": (6, 1), + "GTX 1060": (6, 1), + # Data Center + "H100": (9, 0), + "H200": (9, 0), + "DGX Spark": (12, 1), + "GB10": (12, 1), + "A100": (8, 0), + "RTX A3000 Laptop": (8, 6), + "A6000": (8, 6), + "A5000": (8, 6), + "A4000": (8, 6), + "L40": (8, 9), + "L4": (8, 9), + "T4": (7, 5), + "V100": (7, 0), + "P100": (6, 0), + # Kepler series (compute capability 3.x) — legacy, Vulkan-only. + # CUDA 12 and current llama.cpp CUDA builds dropped Kepler support, so + # these cards only run through the Vulkan backend. See VULKAN_ONLY_GPUS. + "Quadro K6000": (3, 5), + "Quadro K5200": (3, 5), + "Quadro K4200": (3, 0), + "Quadro K2200": (3, 0), + "Quadro K620": (3, 0), + "Quadro K420": (3, 0), + "GTX 780": (3, 5), + "GTX 770": (3, 0), + "GTX 760": (3, 0), +} + +# GPUs sold under one marketing name in multiple memory configurations that the +# driver name and PCI device id cannot tell apart, resolved by max memory clock +# (MHz) at detection time. Each value is a list of (min_mem_clock_mhz, +# bandwidth_gbps), highest threshold first; the first threshold the detected +# clock meets wins. The base name stays in GPU_BANDWIDTH as the conservative +# default used when the memory clock is unknown. +# Threshold sits between the two memory-clock regimes (GDDR5 ~4 GHz vs GDDR6 +# ~6 GHz), well clear of either, so factory-OC boards do not straddle it. +GPU_MEMORY_CLOCK_VARIANTS: dict[str, list[tuple[float, float]]] = { + # GTX 1650 shipped as the original GDDR5 (8 Gbps x 128-bit = 128 GB/s) and a + # later GDDR6 revision (12 Gbps x 128-bit = 192 GB/s) on the same TU117 die + # and PCI id 0x1F82, so only the memory clock distinguishes them. Measured on + # a GDDR6 board (VBIOS 90.17.4D.00.1E): nvidia-smi reports clocks.max.memory + # = 6001 MHz, and Qwen3-1.7B Q4_K_M decodes at 75.4 tok/s, matching the + # 192 GB/s estimate (~78) and not 128's (~52). The GDDR5 board reports + # ~4001 MHz (NVIDIA spec, 8 Gbps GDDR5; not independently measured here); + # the 5500 MHz split is far from both. + "GTX 1650": [(5500.0, 192.0), (0.0, 128.0)], +} + +# Legacy NVIDIA GPUs with no CUDA support in modern llama.cpp builds. +# Kepler (compute capability 3.0/3.5) was removed in CUDA 12, so these cards +# can only be used through the Vulkan backend (Linux) in current llama.cpp. +# Entries are substrings matched case-insensitively against the GPU name, the +# same convention used by GPU_BANDWIDTH and NVIDIA_COMPUTE_CAPABILITY. +VULKAN_ONLY_GPUS: frozenset[str] = frozenset( + { + "Quadro K6000", + "Quadro K5200", + "Quadro K4200", + "Quadro K2200", + "Quadro K620", + "Quadro K420", + "GTX 780", + "GTX 770", + "GTX 760", + } +) diff --git a/src/whichllm/data/lineage.py b/src/whichllm/data/lineage.py new file mode 100644 index 0000000..95fb804 --- /dev/null +++ b/src/whichllm/data/lineage.py @@ -0,0 +1,161 @@ +"""Model lineage / generation half-order used to bonus or penalize family versions.""" + +# Generation lineage half-order. +# For each "family stem" we encode a monotone-increasing version map so that +# the ranker can apply a small bonus/penalty depending on whether a model +# represents the newest generation of its family. This avoids the situation +# where an older series with stale Open-LLM-Leaderboard data ranks above a +# newer release for which the leaderboard simply has no data yet. +# +# Each entry is a list of (regex_pattern, generation_index) tuples evaluated +# in order; first match wins. Patterns match against lowercased model_id. +# Higher index = newer. +MODEL_LINEAGE_VERSIONS: dict[str, list[tuple[str, int]]] = { + "qwen": [ + # ordered newest -> oldest so the bonus reflects the strongest claim + (r"qwen3\.6", 7), + (r"qwen3\.5", 6), + (r"qwen3-next", 6), + (r"qwen3-coder-next", 6), + (r"qwen3-omni", 5), + (r"qwen3", 5), + (r"qwq", 4), + (r"qwen2\.5", 3), + (r"qwen2(?!\.5)", 2), + (r"qwen1", 1), + (r"qwen-(7b|14b|72b)", 1), + ], + "llama": [ + (r"llama-?4\.5", 5), + (r"llama-?4", 4), + (r"llama-?3\.3", 3), + (r"llama-?3\.2", 3), + (r"llama-?3\.1", 3), + (r"meta-llama-?3(?!\.)", 2), + (r"llama-?2", 1), + ], + "deepseek": [ + (r"deepseek-v4", 5), + (r"deepseek-v3\.2", 4), + (r"deepseek-v3\.1", 4), + (r"deepseek-r1-0528", 4), + (r"deepseek-r1", 3), + (r"deepseek-v3-0324", 3), + (r"deepseek-v3(?!\.)", 3), + (r"deepseek-v2\.5", 2), + (r"deepseek-v2(?!\.5)", 1), + (r"deepseek-coder-v2", 2), + (r"deepseek-coder(?!-v2)", 1), + ], + "gemma": [ + # Avoid reading the "gemma" segment inside T5Gemma ids as a Gemma + # generation. T5Gemma is handled by the "t5" family instead. + (r"(? oldest. The bare "t5" + # fallback is boundary-guarded because "t5" is a collision-prone + # substring (e.g. "gpt5"); every named variant is matched before it. + (r"t5[-_]?gemma", 5), # T5Gemma (2025) — Gemma-adapted encoder-decoder + (r"flan-?t5", 4), # Flan-T5 instruction-tuned (2022) + (r"flan-?ul2", 4), # Flan-UL2 (2023) + (r"codet5p", 3), # CodeT5+ (2023); before codet5 since it's a superset + (r"ul2", 3), # UL2 (2022) + (r"code-?t5", 2), # CodeT5 (2021) + (r"long-?t5", 2), # LongT5 (2021) + (r"byt5", 2), # ByT5 byte-level (2021) + (r"mt5", 2), # mT5 multilingual (2020) + (r"t5-?v1[._]1", 2), # T5 v1.1 / LM-adapted (2020) + (r"(? (4*32 + 8) / 32 = 4.25 bits/weight = 0.53125 bytes. + # NVFP4: E2M1 element + one E4M3 (8-bit) scale per block of 16 weights + # (plus a negligible per-tensor FP32 scale) -> (4*16 + 8) / 16 = 4.5 bits + # = 0.5625 bytes, the same footprint as Q4_K_M. + "MXFP4": 0.53125, + "NVFP4": 0.5625, + # Sub-2-bit / ternary tiers (extremely lossy) + "Q1_0": 0.28, + "Q2_0": 0.28, + "TQ1_0": 0.21, + "TQ2_0": 0.28, + "IQ1_S": 0.21, + "IQ1_M": 0.22, + "IQ2_S": 0.275, + "IQ2_M": 0.30, + "IQ3_S": 0.40, + "IQ3_M": 0.42, + "IQ3_XS": 0.41, + "IQ4_NL": 0.5, +} + +# Quality penalty for each quantization type (fraction of quality lost) +# Sub-2-bit and ternary quants lose 30-60% of model quality - whichllm +# previously fell back to 5% which over-rewarded extreme quants. +QUANT_QUALITY_PENALTY: dict[str, float] = { + "F32": 0.0, + "F16": 0.0, + "BF16": 0.0, + "Q8_0": 0.01, + "Q6_K": 0.02, + "Q5_K_M": 0.03, + "Q5_K_S": 0.035, + "Q5_0": 0.035, + "Q4_K_M": 0.05, + "Q4_K_S": 0.055, + "Q4_0": 0.06, + # NVFP4's finer per-16 E4M3 scale recovers more accuracy than MXFP4's + # coarser per-32 E8M0 (power-of-two) scale, so NVFP4 sits on par with the + # mature 4-bit quantizers (Q4_K_M / AWQ at 0.05) while MXFP4 is slightly + # lossier. + "NVFP4": 0.05, + "MXFP4": 0.06, + "Q3_K_M": 0.08, + "Q3_K_S": 0.12, + "Q3_K_L": 0.075, + "Q2_K": 0.25, + "IQ4_XS": 0.05, + "IQ4_NL": 0.055, + "IQ3_XS": 0.16, + "IQ3_S": 0.17, + "IQ3_M": 0.16, + "IQ3_XXS": 0.18, + "IQ2_M": 0.30, + "IQ2_S": 0.32, + "IQ2_XXS": 0.40, + "IQ1_M": 0.50, + "IQ1_S": 0.55, + "Q2_0": 0.45, + "Q1_0": 0.55, + "TQ2_0": 0.45, + "TQ1_0": 0.55, +} + +# Preferred quantization types ordered from best to acceptable. +# Sub-3-bit and 1-bit ternary variants sit at the tail so they are only +# selected when nothing else is available or when explicitly requested. +QUANT_PREFERENCE_ORDER = [ + "Q4_K_M", + "Q4_K_S", + "NVFP4", + "MXFP4", + "Q5_K_M", + "Q5_K_S", + "Q6_K", + "Q3_K_M", + "Q3_K_L", + "Q8_0", + "IQ4_XS", + "IQ4_NL", + "Q4_0", + "Q5_0", + "Q3_K_S", + "F16", + "BF16", + "IQ3_M", + "IQ3_S", + "IQ3_XS", + "Q2_K", + "IQ3_XXS", + "IQ2_M", + "IQ2_S", + "IQ2_XXS", + "IQ1_M", + "IQ1_S", + "Q2_0", + "TQ2_0", + "Q1_0", + "TQ1_0", +] diff --git a/src/whichllm/engine/__init__.py b/src/whichllm/engine/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/whichllm/engine/compatibility.py b/src/whichllm/engine/compatibility.py new file mode 100644 index 0000000..7765ce1 --- /dev/null +++ b/src/whichllm/engine/compatibility.py @@ -0,0 +1,261 @@ +"""Compatibility checking: can a model run on given hardware?""" + +from __future__ import annotations + +from whichllm.constants import _GiB +from whichllm.constants import MIN_COMPUTE_CAPABILITY_OLLAMA +from whichllm.constants import VULKAN_ONLY_GPUS +from whichllm.engine.quantization import estimate_weight_bytes +from whichllm.engine.types import CompatibilityResult +from whichllm.engine.vram import estimate_vram +from whichllm.hardware.memory import effective_usable_ram +from whichllm.hardware.types import GPUInfo, HardwareInfo +from whichllm.models.types import GGUFVariant, ModelInfo + +_MULTI_GPU_FRAMEWORK_OVERHEAD_BYTES = int(0.3 * _GiB) +_MULTI_GPU_HOMOGENEOUS_UTILIZATION = 0.95 +_MULTI_GPU_HETEROGENEOUS_UTILIZATION = 0.90 + + +def _gpu_available_memory( + gpu: GPUInfo, usable_ram: int, *, ram_budget_active: bool = False +) -> int: + vram_bytes = ( + gpu.usable_vram_bytes if gpu.usable_vram_bytes is not None else gpu.vram_bytes + ) + if gpu.shared_memory and vram_bytes < 2 * _GiB and not gpu.vram_overridden: + return usable_ram + if gpu.shared_memory and ram_budget_active: + return min(vram_bytes, usable_ram) + return vram_bytes + + +def _uses_shared_system_pool(gpu: GPUInfo) -> bool: + return gpu.shared_memory and gpu.vram_bytes < 2 * _GiB and not gpu.vram_overridden + + +def _is_vulkan_only_gpu(gpu: GPUInfo) -> bool: + """Return True for legacy NVIDIA GPUs with no modern CUDA support. + + Kepler cards (compute capability 3.x) were dropped by CUDA 12 and current + llama.cpp CUDA builds, so they only run through the Vulkan backend. Matches + ``VULKAN_ONLY_GPUS`` entries as case-insensitive substrings of the GPU name, + the same convention used by the bandwidth/compute-capability lookups. + """ + if gpu.vendor != "nvidia": + return False + name_upper = gpu.name.upper() + return any(marker.upper() in name_upper for marker in VULKAN_ONLY_GPUS) + + +def _fit_candidate_gpus(gpus: list[GPUInfo]) -> list[GPUInfo]: + has_dedicated_gpu = any( + not _uses_shared_system_pool(gpu) and gpu.vram_bytes > 0 for gpu in gpus + ) + if not has_dedicated_gpu: + return gpus + return [gpu for gpu in gpus if not _uses_shared_system_pool(gpu)] + + +def _gpu_identity(gpu: GPUInfo) -> str: + name = gpu.name.lower().replace("(simulated)", "") + return " ".join(name.split()) + + +def _is_homogeneous_gpu_set(gpus: list[GPUInfo], available: list[int]) -> bool: + if not gpus: + return True + first = gpus[0] + first_identity = _gpu_identity(first) + first_available = available[0] + vram_tolerance = max(256 * 1024**2, int(first_available * 0.02)) + return all( + gpu.vendor == first.vendor + and _gpu_identity(gpu) == first_identity + and abs(gpu_available - first_available) <= vram_tolerance + for gpu, gpu_available in zip(gpus, available, strict=True) + ) + + +def _multi_gpu_effective_vram( + gpus: list[GPUInfo], + available: list[int], + warnings: list[str], +) -> tuple[int, bool, int | None]: + raw_total = sum(available) + if len(gpus) <= 1: + return raw_total, False, None + + if any(gpu.shared_memory or gpu.vendor == "apple" for gpu in gpus): + effective = max(available) + warnings.append( + "Multiple shared-memory GPUs are not pooled; using the largest " + "reported memory pool for fit checks" + ) + return effective, False, None + + homogeneous = _is_homogeneous_gpu_set(gpus, available) + utilization = ( + _MULTI_GPU_HOMOGENEOUS_UTILIZATION + if homogeneous + else _MULTI_GPU_HETEROGENEOUS_UTILIZATION + ) + overhead = min(raw_total, len(gpus) * _MULTI_GPU_FRAMEWORK_OVERHEAD_BYTES) + effective = int((raw_total - overhead) * utilization) + + warnings.append( + "Multi-GPU fit uses a conservative layer-split budget: " + f"{effective / _GiB:.1f} GB effective from {raw_total / _GiB:.1f} GB raw VRAM" + ) + if not homogeneous: + warnings.append( + "Heterogeneous multi-GPU setup: fit assumes uneven layer placement; " + "speed depends on backend split mode and interconnect" + ) + return effective, True, effective + + +def check_compatibility( + model: ModelInfo, + variant: GGUFVariant | None, + hardware: HardwareInfo, + context_length: int = 4096, +) -> CompatibilityResult: + """Check if a model+variant can run on the given hardware.""" + warnings: list[str] = [] + + vram_required = estimate_vram(model, variant, context_length) + + usable_ram = effective_usable_ram(hardware.ram_bytes, hardware.ram_budget_bytes) + + # Determine best GPU + best_gpu: GPUInfo | None = None + best_gpu_available = 0 + gpu_available_values: list[int] = [] + candidate_gpus = _fit_candidate_gpus(hardware.gpus) + ram_budget_active = hardware.ram_budget_bytes is not None + for gpu in candidate_gpus: + gpu_available = _gpu_available_memory( + gpu, usable_ram, ram_budget_active=ram_budget_active + ) + gpu_available_values.append(gpu_available) + if best_gpu is None or gpu_available > best_gpu_available: + best_gpu = gpu + best_gpu_available = gpu_available + + vram_available = sum(gpu_available_values) if gpu_available_values else 0 + fit_vram_available, uses_multi_gpu, multi_gpu_effective_vram = ( + _multi_gpu_effective_vram(candidate_gpus, gpu_available_values, warnings) + ) + if ( + len(candidate_gpus) > 1 + and not uses_multi_gpu + and any(gpu.shared_memory or gpu.vendor == "apple" for gpu in candidate_gpus) + ): + vram_available = fit_vram_available + offload_ram_available = ( + 0 + if best_gpu and (best_gpu.shared_memory or best_gpu.vendor == "apple") + else usable_ram + ) + + # Check compute capability for NVIDIA + if best_gpu and best_gpu.vendor == "nvidia" and best_gpu.compute_capability: + if best_gpu.compute_capability < MIN_COMPUTE_CAPABILITY_OLLAMA: + warnings.append( + f"Compute capability {best_gpu.compute_capability} is below " + f"minimum {MIN_COMPUTE_CAPABILITY_OLLAMA} for Ollama" + ) + + # Flag legacy Kepler GPUs that have no CUDA support in modern llama.cpp. + # They can still run, but only through the Vulkan backend on Linux. + if best_gpu and _is_vulkan_only_gpu(best_gpu): + warnings.append( + "Legacy Kepler GPU: no CUDA support in modern llama.cpp; " + "use the Vulkan backend (Linux) instead" + ) + + # Check ROCm for AMD. Windows AMD users can still use Vulkan/DirectML + # backends, so do not label the GPU path as unavailable there. + if ( + best_gpu + and best_gpu.vendor == "amd" + and hardware.os not in ("linux", "windows") + ): + warnings.append("ROCm requires Linux for AMD GPU inference") + + # Check Metal for Apple + if best_gpu and best_gpu.vendor == "apple" and hardware.os != "darwin": + warnings.append("Metal requires macOS for Apple Silicon inference") + + # Determine fit type + if fit_vram_available >= vram_required: + fit_type = "full_gpu" + can_run = True + offload_ratio = 0.0 + elif ( + fit_vram_available > 0 + and (fit_vram_available + offload_ram_available) >= vram_required + ): + fit_type = "partial_offload" + can_run = True + offload_ratio = ( + (vram_required - fit_vram_available) / vram_required + if vram_required > 0 + else 0.0 + ) + offload_pct = offload_ratio * 100 + if best_gpu and (best_gpu.shared_memory or best_gpu.vendor == "apple"): + warnings.append("Will use shared system memory") + else: + warnings.append( + f"~{offload_pct:.0f}% of layers will be offloaded to CPU RAM" + ) + elif usable_ram >= vram_required: + fit_type = "cpu_only" + can_run = True + offload_ratio = 0.0 + warnings.append("Will run on CPU only (much slower)") + else: + fit_type = "cpu_only" + can_run = False + offload_ratio = 0.0 + warnings.append("Insufficient memory (GPU VRAM + RAM) to run this model") + + # Context length warning + context_fits = not ( + model.context_length is not None and model.context_length < context_length + ) + if not context_fits: + warnings.append( + f"Model max context {model.context_length} < requested " + f"{context_length}; runtime will truncate or reject" + ) + elif ( + context_length > 8192 + and model.context_length + and model.context_length >= context_length + ): + warnings.append( + f"Large context ({context_length}) increases VRAM usage significantly" + ) + + # File size vs disk space + file_size = estimate_weight_bytes(model, variant) + if hardware.disk_free_bytes > 0 and file_size > hardware.disk_free_bytes: + warnings.append("Insufficient disk space to download this model") + can_run = False + + return CompatibilityResult( + model=model, + gguf_variant=variant, + can_run=can_run, + vram_required_bytes=vram_required, + vram_available_bytes=vram_available, + offload_ratio=offload_ratio, + uses_multi_gpu=uses_multi_gpu, + multi_gpu_effective_vram_bytes=multi_gpu_effective_vram, + warnings=warnings, + fit_type=fit_type, + context_fits=context_fits, + ) diff --git a/src/whichllm/engine/performance.py b/src/whichllm/engine/performance.py new file mode 100644 index 0000000..dc21a59 --- /dev/null +++ b/src/whichllm/engine/performance.py @@ -0,0 +1,284 @@ +"""Token generation speed estimation.""" + +from __future__ import annotations + +from whichllm.engine.quantization import estimate_weight_bytes +from whichllm.engine.quantization import effective_quant_type +from whichllm.hardware.types import GPUInfo +from whichllm.models.types import GGUFVariant, ModelInfo + + +# Per-quant efficiency factors applied to the theoretical bandwidth-bound +# tok/s. These reflect empirical llama.cpp / vLLM measurements: 4-bit GGUFs +# achieve the highest fraction of memory-bandwidth-limited theoretical +# throughput because the dequantization kernel is fast and weight reads +# dominate; 8-bit and FP16 drop because more compute is required per byte. +_QUANT_EFFICIENCY: dict[str, float] = { + "F32": 0.30, + "F16": 0.40, + "BF16": 0.40, + "Q8_0": 0.45, + "Q6_K": 0.50, + "Q5_K_M": 0.52, + "Q5_K_S": 0.52, + "Q5_0": 0.50, + "Q4_K_M": 0.55, + "Q4_K_S": 0.55, + "Q4_0": 0.53, + # 4-bit microscaling floats decode through native FP4 tensor-core paths + # (e.g. Blackwell) where weight reads dominate, so they land in the same + # high-efficiency band as the best 4-bit GGUF kernels. + "NVFP4": 0.56, + "MXFP4": 0.55, + "Q3_K_M": 0.50, + "Q3_K_S": 0.48, + "Q3_K_L": 0.50, + "Q2_K": 0.45, + "IQ4_XS": 0.52, + "IQ4_NL": 0.50, + "IQ3_S": 0.45, + "IQ3_M": 0.45, + "IQ3_XS": 0.45, + "IQ3_XXS": 0.42, + "IQ2_S": 0.40, + "IQ2_M": 0.40, + "IQ2_XXS": 0.38, + "IQ1_M": 0.35, + "IQ1_S": 0.35, + "Q2_0": 0.38, + "Q1_0": 0.32, + "TQ2_0": 0.35, + "TQ1_0": 0.32, +} + +_DEFAULT_QUANT_EFFICIENCY = 0.45 + +# Vendor / backend multiplier applied on top of quant efficiency. CUDA on +# modern data-center GPUs is the reference (1.0); Apple's Metal kernel is +# behind on dequantization; ROCm trails further; older CUDA generations +# also drop. +_BACKEND_FACTOR: dict[str, float] = { + "nvidia": 1.00, + "amd": 0.78, + "apple": 0.82, + "intel": 0.65, +} + +# MoE decode is partly bandwidth-bound and partly kernel/dispatch-bound. +# The old fixed 25% read floor matched high-bandwidth CUDA cards reasonably +# well, but badly under-estimated low-bandwidth unified-memory APUs such as +# Strix Halo where the active expert reads dominate. Model this as a floor +# that rises with bandwidth: ~5% at 256 GB/s, capped at the legacy 25%. +_MOE_REFERENCE_BANDWIDTH_GBPS = 256.0 +_MOE_MIN_READ_RATIO_AT_REFERENCE = 0.05 +_MOE_MAX_READ_RATIO_FLOOR = 0.25 + +_SPEED_CONFIDENCE_RANGE_FACTORS: dict[str, tuple[float, float]] = { + "high": (0.85, 1.20), + "medium": (0.60, 1.60), + "low": (0.35, 2.00), +} + +_SPEED_CONFIDENCE_ORDER = { + "low": 0, + "medium": 1, + "high": 2, +} + + +def _backend_factor(gpu: GPUInfo) -> float: + if gpu.vendor in _BACKEND_FACTOR: + return _BACKEND_FACTOR[gpu.vendor] + return 0.7 + + +def _quant_efficiency(model: ModelInfo, variant: GGUFVariant | None) -> float: + quant = effective_quant_type(model, variant) + if not quant: + return _DEFAULT_QUANT_EFFICIENCY + return _QUANT_EFFICIENCY.get(quant.upper(), _DEFAULT_QUANT_EFFICIENCY) + + +def _moe_effective_read_ratio(model: ModelInfo, gpu: GPUInfo) -> float: + """Return fraction of stored weights read per generated token for MoE.""" + if not model.is_moe or not model.parameter_count_active: + return 1.0 + if model.parameter_count <= 0: + return 1.0 + + active_ratio = model.parameter_count_active / model.parameter_count + if active_ratio <= 0: + return 1.0 + + bandwidth = gpu.memory_bandwidth_gbps or 0.0 + if bandwidth > 0: + floor = _MOE_MIN_READ_RATIO_AT_REFERENCE * max( + 1.0, bandwidth / _MOE_REFERENCE_BANDWIDTH_GBPS + ) + else: + floor = _MOE_MAX_READ_RATIO_FLOOR + floor = min(_MOE_MAX_READ_RATIO_FLOOR, floor) + + return min(1.0, max(active_ratio, floor)) + + +def _lower_speed_confidence(current: str, candidate: str) -> str: + if _SPEED_CONFIDENCE_ORDER[candidate] < _SPEED_CONFIDENCE_ORDER[current]: + return candidate + return current + + +def _looks_synthetic_gguf(model: ModelInfo, variant: GGUFVariant | None) -> bool: + if variant is None: + return False + if not variant.filename: + return False + expected = f"{model.name}.{variant.quant_type}.gguf" + return variant.filename == expected + + +def estimate_speed_uncertainty( + model: ModelInfo, + variant: GGUFVariant | None, + gpu: GPUInfo | None, + fit_type: str, + estimated_tok_per_sec: float | None, +) -> tuple[str, tuple[float, float] | None, list[str]]: + """Return confidence metadata for the speed point estimate. + + The tok/s estimator is intentionally hardware/model-metadata based; it + does not know the user's exact llama.cpp, Vulkan, ROCm, Metal, MLX, or + runtime kernel versions. This helper keeps that uncertainty visible + without mixing it into the ranking score itself. + """ + notes = [ + "Speed is estimated from memory bandwidth, quantization, backend, and fit type." + ] + confidence = "medium" + + if estimated_tok_per_sec is None or estimated_tok_per_sec <= 0: + return ( + "low", + None, + notes + ["No usable bandwidth estimate was available for this setup."], + ) + + if gpu is None or fit_type == "cpu_only": + confidence = "low" + notes.append( + "CPU-only speed varies heavily with memory channels and BLAS/kernel path." + ) + else: + if not gpu.memory_bandwidth_gbps: + confidence = "low" + notes.append( + "GPU memory bandwidth is unknown, so speed is especially uncertain." + ) + + if fit_type == "partial_offload": + confidence = "low" + if gpu.vendor == "apple" or gpu.shared_memory: + notes.append( + "Partial offload on unified memory is backend-sensitive but avoids a PCIe cliff." + ) + else: + notes.append( + "Partial offload on a discrete GPU depends strongly on PCIe and CPU RAM bandwidth." + ) + + if model.is_moe: + notes.append( + "MoE speed uses active parameters plus a bandwidth-scaled dispatch/read floor." + ) + if gpu.vendor == "apple": + confidence = _lower_speed_confidence(confidence, "low") + notes.append( + "Apple Silicon MoE throughput is especially sensitive to Metal/MLX runtime kernels." + ) + elif gpu.vendor == "amd" and gpu.shared_memory: + confidence = _lower_speed_confidence(confidence, "medium") + notes.append( + "AMD shared-memory APU estimates are calibrated by bandwidth, but ROCm/Vulkan kernels can differ." + ) + + if _looks_synthetic_gguf(model, variant): + confidence = _lower_speed_confidence(confidence, "medium") + notes.append( + "This is a synthetic GGUF estimate for an official repo, not a measured GGUF file." + ) + + low_factor, high_factor = _SPEED_CONFIDENCE_RANGE_FACTORS[confidence] + speed_range = ( + round(estimated_tok_per_sec * low_factor, 1), + round(estimated_tok_per_sec * high_factor, 1), + ) + return confidence, speed_range, notes + + +def estimate_tok_per_sec( + model: ModelInfo, + variant: GGUFVariant | None, + gpu: GPUInfo | None, + fit_type: str = "full_gpu", +) -> float: + """Estimate tokens per second for inference. + + Model: throughput is bounded by the time it takes to read all weights + needed per token, multiplied by quant- and backend-specific efficiency + factors. The default 0.5 efficiency factor used earlier mixed two + distinct losses (compute kernel quality and offload overhead) into one + constant — this version separates them so a Q4_K_M model on CUDA scores + differently from the same model running on Metal or with partial + offload. + """ + if gpu is None or fit_type == "cpu_only": + params_b = model.parameter_count / 1e9 + if model.is_moe and model.parameter_count_active: + params_b = model.parameter_count_active / 1e9 + if params_b <= 0: + return 0.0 + # Modern desktop CPUs sustain roughly 4-8 GB/s effective for the + # bandwidth-bound dequant+matmul loop on a single socket. Quantized + # 4-bit 7B → ~3.5 GB → ~1-2 tok/s. Approximate with an inverse-size + # heuristic that gets the right order of magnitude. + quant_factor = _quant_efficiency(model, variant) / _DEFAULT_QUANT_EFFICIENCY + return max(0.3, 18.0 / max(params_b, 0.5) * quant_factor) + + model_size = estimate_weight_bytes(model, variant) + + # MoE: use a speed-specific effective read ratio. VRAM fit still uses + # total stored weights elsewhere; this only estimates per-token reads. + if model.is_moe and model.parameter_count_active: + effective_read = model_size * _moe_effective_read_ratio(model, gpu) + else: + effective_read = model_size + + bandwidth = gpu.memory_bandwidth_gbps * 1e9 if gpu.memory_bandwidth_gbps else 0 + if bandwidth == 0: + return 0.0 + + theoretical = bandwidth / effective_read + + # Real-world efficiency depends on quant kernel and backend. + efficiency = _quant_efficiency(model, variant) * _backend_factor(gpu) + + # Partial offload penalty depends on the memory architecture: + # + # - Discrete GPU (NVIDIA/AMD/Intel): spilled weights live in CPU RAM + # and are read across PCIe at ~1/10th of VRAM bandwidth. With ~40% + # of the model offloaded the blended throughput lands near 0.45x. + # - Apple Silicon: GPU and CPU share one physical unified-memory pool. + # AMD shared-memory APUs such as Strix Halo have the same no-PCIe-cliff + # shape for model weights, even though their backend factor remains AMD. + # "Exceeding VRAM" only means exceeding the recommended working set; + # the bytes are still read from the same high-bandwidth unified RAM, + # so there is no PCIe cliff — only mild OS/cache contention. Using + # the discrete 0.45x here was the bug that made DeepSeek-R1-class + # models on M2/M3 Ultra report ~1.7 t/s when real-world is 4-15. + if fit_type == "partial_offload": + if gpu.vendor == "apple" or gpu.shared_memory: + efficiency *= 0.85 + else: + efficiency *= 0.45 + + return theoretical * efficiency diff --git a/src/whichllm/engine/quantization.py b/src/whichllm/engine/quantization.py new file mode 100644 index 0000000..e0743e8 --- /dev/null +++ b/src/whichllm/engine/quantization.py @@ -0,0 +1,82 @@ +"""Quantization helpers shared across ranking and estimators.""" + +from __future__ import annotations + +import re + +from whichllm.constants import QUANT_QUALITY_PENALTY +from whichllm.models.types import GGUFVariant, ModelInfo + +# GGUFでないリポジトリ名から量子化方式を推定する +_NON_GGUF_PATTERNS: list[tuple[str, str]] = [ + (r"(^|[-_/])awq($|[-_/])", "AWQ"), + (r"(^|[-_/])gptq($|[-_/])", "GPTQ"), + # 4-bit microscaling float formats. Anchored so they only match a distinct + # repo-name token, never a substring of an unrelated id. + (r"(^|[-_/])mxfp4($|[-_/])", "MXFP4"), + (r"(^|[-_/])nvfp4($|[-_/])", "NVFP4"), + (r"(bnb[-_/]?4bit|nf4|int4|4bit)", "BNB_4BIT"), + (r"(int8|8bit)", "INT8"), + (r"(^|[-_/])fp8($|[-_/])", "FP8"), + (r"(^|[-_/])bf16($|[-_/])", "BF16"), + (r"(^|[-_/])(fp16|f16)($|[-_/])", "FP16"), +] + +# GGUF以外の簡易推定: 重み1つあたりのバイト数 +_NON_GGUF_BYTES_PER_WEIGHT: dict[str, float] = { + "AWQ": 0.5, + "GPTQ": 0.5, + "BNB_4BIT": 0.5, + "MXFP4": 0.53125, + "NVFP4": 0.5625, + "INT8": 1.0, + "FP8": 1.0, + "BF16": 2.0, + "FP16": 2.0, +} + +# GGUF以外の簡易推定: 品質低下率 +_NON_GGUF_QUALITY_PENALTY: dict[str, float] = { + "AWQ": 0.05, + "GPTQ": 0.05, + "BNB_4BIT": 0.07, + "MXFP4": 0.06, + "NVFP4": 0.05, + "INT8": 0.02, + "FP8": 0.02, + "BF16": 0.0, + "FP16": 0.0, +} + + +def infer_non_gguf_quant_type(model_id: str) -> str: + """Infer non-GGUF quantization type from a model repo ID.""" + lower = model_id.lower() + for pattern, quant_type in _NON_GGUF_PATTERNS: + if re.search(pattern, lower): + return quant_type + return "FP16" + + +def effective_quant_type(model: ModelInfo, variant: GGUFVariant | None) -> str: + """Return effective quantization type for a model+variant pair.""" + if variant: + return variant.quant_type.upper() + return infer_non_gguf_quant_type(model.id) + + +def estimate_weight_bytes(model: ModelInfo, variant: GGUFVariant | None) -> int: + """Estimate model weight size in bytes.""" + if variant: + return variant.file_size_bytes + quant_type = infer_non_gguf_quant_type(model.id) + bytes_per_weight = _NON_GGUF_BYTES_PER_WEIGHT.get(quant_type, 2.0) + return int(model.parameter_count * bytes_per_weight) + + +def quant_quality_penalty(model: ModelInfo, variant: GGUFVariant | None) -> float: + """Return quality penalty fraction for a quantization format.""" + quant_type = effective_quant_type(model, variant).upper() + if quant_type in QUANT_QUALITY_PENALTY: + return QUANT_QUALITY_PENALTY[quant_type] + return _NON_GGUF_QUALITY_PENALTY.get(quant_type, 0.05) diff --git a/src/whichllm/engine/ranker.py b/src/whichllm/engine/ranker.py new file mode 100644 index 0000000..ccc96c7 --- /dev/null +++ b/src/whichllm/engine/ranker.py @@ -0,0 +1,885 @@ +"""Model ranking: score and select the best models for the user's hardware.""" + +from __future__ import annotations + +import math +import re + +from whichllm.constants import ( + MODEL_GENERATION_BONUS_MAX, + MODEL_GENERATION_PENALTY_MAX, + MODEL_LINEAGE_VERSIONS, + QUANT_BYTES_PER_WEIGHT, + QUANT_PREFERENCE_ORDER, +) +from whichllm.engine.compatibility import check_compatibility +from whichllm.engine.performance import estimate_speed_uncertainty, estimate_tok_per_sec +from whichllm.engine.quantization import effective_quant_type, quant_quality_penalty +from whichllm.engine.types import CompatibilityResult +from whichllm.hardware.types import HardwareInfo +from whichllm.models.benchmark import ( + BenchmarkEvidence, + build_line_bucket_index, + build_score_index, + lookup_benchmark_evidence, +) +from whichllm.models.types import GGUFVariant, ModelInfo + +# Pre-compile lineage regex tables once at import time. +_LINEAGE_REGEX: dict[str, list[tuple[re.Pattern[str], int]]] = { + family: [(re.compile(pat), idx) for pat, idx in entries] + for family, entries in MODEL_LINEAGE_VERSIONS.items() +} +_LINEAGE_FAMILY_MAX: dict[str, int] = { + family: max(idx for _, idx in entries) for family, entries in _LINEAGE_REGEX.items() +} +_MULTI_GPU_SPEED_FACTOR = 0.70 + + +def _family_selection_key( + result: CompatibilityResult, + require_direct_top: bool, +) -> tuple[float]: + """Family-level selection key — single composite score. + + ``quality_score`` already includes the runtime fit penalty and speed + adjustment. Keep final selection close to that displayed score so strong + partial-offload candidates do not get discounted again while sorting. + + - ``direct_bonus`` (+5) gives independent leaderboard evidence a + small edge at the same fit; cannot overturn a 6+ point quality gap + """ + if require_direct_top and result.benchmark_status == "direct": + direct_bonus = 5.0 + else: + direct_bonus = 0.0 + cpu_penalty = -6.0 if result.fit_type == "cpu_only" else 0.0 + ctx_penalty = -20.0 if not result.context_fits else 0.0 + return (result.quality_score + direct_bonus + cpu_penalty + ctx_penalty,) + + +def _partial_offload_quality_factor(model: ModelInfo, offload_ratio: float) -> float: + """Discount partial-offload candidates by how much leaves VRAM.""" + ratio = max(0.0, min(1.0, offload_ratio)) + if ratio >= 0.75: + factor = 0.42 + elif ratio >= 0.60: + factor = 0.52 + elif ratio >= 0.40: + factor = 0.62 + elif ratio >= 0.25: + factor = 0.76 + else: + factor = 0.86 + + # MoE offload is more nuanced: inactive experts and router/runtime + # placement do not hurt equally. If the GPU can plausibly hold the + # active expert working set, do not treat inactive-expert spill like + # dense-layer spill. + if model.is_moe and model.parameter_count_active: + active_ratio = ( + model.parameter_count_active / model.parameter_count + if model.parameter_count > 0 + else 1.0 + ) + active_ratio = max(0.0, min(1.0, active_ratio)) + active_set_fits = ratio <= max(0.0, 1.0 - active_ratio) + if active_set_fits: + if ratio >= 0.75: + factor = max(factor, 0.66) + elif ratio >= 0.60: + factor = max(factor, 0.70) + elif ratio >= 0.40: + factor = max(factor, 0.76) + elif ratio >= 0.25: + factor = max(factor, 0.82) + else: + factor = max(factor, 0.88) + else: + factor = min(0.76, factor + 0.08) + + return factor + + +# Per-source benchmark weight applied to the raw 0-100 score before it is +# combined with size, quant penalty, etc. The widest gap is between "direct" +# (independent leaderboard) and "self_reported" (uploader card claim). +_SOURCE_WEIGHTS: dict[str, float] = { + "direct": 0.62, + "base_model": 0.55, + "variant": 0.50, + "line_interp": 0.40, + "self_reported": 0.30, + "none": 0.0, +} + + +_SYNTHETIC_QUANTS = ("Q3_K_M", "Q4_K_M", "Q5_K_M", "Q6_K", "Q8_0") +_PREQUANTIZED_REPO_RE = re.compile( + r"-(awq|gptq|bnb|fp8|fp16|bf16|mxfp4|nvfp4|int4|int8|4bit|8bit|gguf)$", + re.IGNORECASE, +) + + +def _synthesize_variants_for_official_repo( + model: ModelInfo, quant_filter_upper: str | None +) -> list[GGUFVariant]: + """Return synthetic GGUF variants for popular safetensors-only repos. + + HuggingFace doesn't always index GGUF siblings for an official model + (e.g. ``Qwen/Qwen3.6-27B`` ships only safetensors), but bartowski / + lmstudio-community / QuantFactory invariably publish Q4_K_M and Q8_0 + conversions within a day of release. Without synthetic variants, we'd + score these models at BF16 file sizes (~2x larger than realistic), which + forces a partial_offload penalty on otherwise-runnable mid-size models. + + Skips repos that already advertise a specific quantization in their name + (``...-AWQ``, ``...-GPTQ``, ``...-FP8`` etc.) — those are non-GGUF formats + and synthesizing a Q4_K_M alternative would misrepresent what the repo + actually contains. + """ + org = model.id.split("/", 1)[0] if "/" in model.id else "" + if org not in _OFFICIAL_ORGS: + return [] + if _PREQUANTIZED_REPO_RE.search(model.id): + return [] + out: list[GGUFVariant] = [] + for quant in _SYNTHETIC_QUANTS: + if quant_filter_upper and quant != quant_filter_upper: + continue + bpw = QUANT_BYTES_PER_WEIGHT.get(quant, 0.5625) + out.append( + GGUFVariant( + filename=f"{model.name}.{quant}.gguf", + quant_type=quant, + file_size_bytes=int(model.parameter_count * bpw), + ) + ) + return out + + +def _iter_candidate_variants( + model: ModelInfo, + quant_filter: str | None = None, +) -> list[GGUFVariant | None]: + """Build candidate variants to evaluate for a model.""" + quant_filter_upper = quant_filter.upper() if quant_filter else None + + if not model.gguf_variants: + synthetic = _synthesize_variants_for_official_repo(model, quant_filter_upper) + if synthetic: + return list(synthetic) + quant_type = effective_quant_type(model, None) + if quant_filter_upper and quant_type != quant_filter_upper: + return [] + return [None] + + # Filter by quant type if specified + candidates: list[GGUFVariant] = model.gguf_variants + if quant_filter_upper: + candidates = [ + v for v in candidates if v.quant_type.upper() == quant_filter_upper + ] + if not candidates: + return [] + else: + # Sub-3-bit GGUFs lose 25-60% of model quality and rarely produce + # a meaningfully better candidate than a smaller model at Q4_K_M. + # Exclude them unless explicitly requested via --quant. + _EXTREME_QUANTS = { + "Q2_K", + "Q2_0", + "Q1_0", + "TQ2_0", + "TQ1_0", + "IQ3_XXS", + "IQ2_XXS", + "IQ2_S", + "IQ2_M", + "IQ1_M", + "IQ1_S", + } + filtered = [ + v for v in candidates if v.quant_type.upper() not in _EXTREME_QUANTS + ] + if filtered: + candidates = filtered + + # Sort by preference order + def variant_sort_key(v: GGUFVariant) -> int: + try: + return QUANT_PREFERENCE_ORDER.index(v.quant_type.upper()) + except ValueError: + return len(QUANT_PREFERENCE_ORDER) + + candidates = sorted(candidates, key=variant_sort_key) + + return list(candidates) + + +_OFFICIAL_ORGS = frozenset( + { + "Qwen", + "meta-llama", + "google", + "mistralai", + "deepseek-ai", + "microsoft", + "nvidia", + "01-ai", + "tiiuae", + "apple", + "CohereForAI", + "bigcode", + # 2025+ frontier open-weights labs that publish safetensors-only + # repos which the community immediately converts to GGUF. + "openai", + "zai-org", + "moonshotai", + "MiniMaxAI", + "XiaomiMiMo", + "allenai", + "ibm-granite", + "stepfun-ai", + } +) + +# Trusted GGUF converters — format converters that don't change model quality +_TRUSTED_CONVERTERS = frozenset( + { + "bartowski", + "lmstudio-community", + "QuantFactory", + "unsloth", + "ggml-org", + "Mungert", + } +) + +# Known repackagers — typically reupload others' models without added value +_REPACKAGER_ORGS = frozenset( + { + "MaziyarPanahi", + "TheBloke", + "SanctumAI", + "solidrust", + "mradermacher", + } +) + +# Orgs whose repositories ship CI fixtures, deprecated research artifacts, or +# debug binaries that are not viable production LLMs. Exclude them outright so +# they cannot occupy ranking slots regardless of download counts. +_EXCLUDED_ORGS = frozenset( + { + "openai-community", # gpt2 family, 2019 research + "distilbert", # distilgpt2 etc. + "facebook", # opt-125m research scaffolds + "EleutherAI", # pythia/gpt-neo research + "trl-internal-testing", # TRL CI fixtures + "hmellor", # random tiny test models + "HuggingFaceH4", # often staging / fixtures + "transformersbook", + "togethercomputer", # mostly inference endpoints, no GGUFs + } +) + +# Substring patterns in *names* that strongly suggest non-production usage. +_EXCLUDED_NAME_PATTERNS = ( + "tiny-", + "-tiny", + "tiny_", + "_tiny", + "test-only", + "debug-", + "playground", + "-fixture", + "for-testing", + "tiny-random", + "ci-", +) + +# Naming patterns that indicate a fine-tune / merge / "uncensoring" derivative +# of a real base model. These derivatives inherit the base model's benchmark +# score via line_interp, but the derivative itself is rarely benchmarked +# independently and frequently degrades quality. Apply a soft score penalty +# rather than full exclusion so they can still surface when nothing better is +# available. +_DUBIOUS_DERIVATIVE_PATTERNS = ( + "heretic", + "abliterat", + "uncensored", + "obliterat", + "abliter", + "horror", + "erotic", + "nsfw", + "rp-", + "-rp", + "roleplay", + "darkidol", + "darkforest", + "tiefigh", + "smaug", + "personalityengine", + "lexi", + "violence", + "violet", + "schizo", + "dark-", + "twilight", + "celeste", + "midnight-rose", + "moistral", + "stheno", + "fimbulvetr", + "wizard-vicuna", + "kunoichi", +) + + +def _derivative_name_penalty(model_id: str) -> float: + """Return a score penalty (in raw quality points) for fine-tune / + "uncensored" / merge derivatives that ride on a real base model's + benchmark line. The penalty is gentle (≤ 12pt) so a derivative can + still win when its size class has no better option. + """ + if not model_id: + return 0.0 + lower = model_id.lower() + name = lower.split("/", 1)[1] if "/" in lower else lower + for pat in _DUBIOUS_DERIVATIVE_PATTERNS: + if pat in name: + return -10.0 + return 0.0 + + +def _is_excluded_model(model_id: str) -> bool: + """Return True for CI/research/fixture models that should never rank.""" + if not model_id: + return True + org = model_id.split("/", 1)[0] if "/" in model_id else "" + if org in _EXCLUDED_ORGS: + return True + lower = model_id.lower() + name = lower.split("/", 1)[1] if "/" in lower else lower + for pat in _EXCLUDED_NAME_PATTERNS: + if pat in name: + return True + return False + + +def _generation_bonus(model_id: str) -> float: + """Return a small additive bonus reflecting how new a model's + generation is within its family. The newest version of each + recognized family gets +MODEL_GENERATION_BONUS_MAX. Older + versions get a smaller bonus (or a small penalty for the + legacy generation). Unknown families return 0. + + This is purely an additive correction to the quality score + and is small enough that strong benchmark evidence will still + dominate. + """ + if not model_id: + return 0.0 + lower = model_id.lower() + best_bonus = 0.0 + for family, patterns in _LINEAGE_REGEX.items(): + for regex, idx in patterns: + if regex.search(lower): + top = _LINEAGE_FAMILY_MAX[family] + if top <= 1: + contribution = 0.0 + else: + # Map oldest -> -PENALTY_MAX, newest -> +BONUS_MAX. + norm = (idx - 1) / (top - 1) # 0 .. 1 + span = MODEL_GENERATION_BONUS_MAX + MODEL_GENERATION_PENALTY_MAX + contribution = norm * span - MODEL_GENERATION_PENALTY_MAX + if abs(contribution) > abs(best_bonus): + best_bonus = contribution + break # first match wins for this family + return best_bonus + + +def _detect_specializations(model_id: str) -> set[str]: + """モデルIDから用途特化タグを検出する。""" + lower = model_id.lower() + tags: set[str] = set() + if re.search(r"(coder|codegen|starcoder|program|coding)", lower): + tags.add("coding") + if re.search(r"(^|[-_/])(vl|vision|multimodal|llava|image)([-_/]|$)", lower): + tags.add("vision") + if re.search(r"(^|[-_/])math([-_/]|$)", lower): + tags.add("math") + return tags + + +def _matches_profile(model: ModelInfo, task_profile: str) -> bool: + """指定プロファイルにモデルが合致するか判定する。""" + profile = task_profile.lower() + tags = _detect_specializations(model.id) + if profile == "any": + return True + if profile == "general": + return len(tags) == 0 + return profile in tags + + +def _effective_params_b(model: ModelInfo) -> float: + """Return effective parameter size in billions.""" + if model.is_moe and model.parameter_count_active: + return model.parameter_count_active / 1e9 + return model.parameter_count / 1e9 + + +def _knowledge_capacity_b(model: ModelInfo) -> float: + """Return the knowledge capacity in billions for size filtering. + + For dense models this is the parameter count. For MoE models, total + parameters (all expert weights live in VRAM and contribute to the + knowledge encoded in the model) is the right yardstick — ``min_params`` + is asking "how much does this model know?" not "how much does it + compute per token". Using *active* params here was the bug that hid + Qwen3-Next-80B-A3B from the H100 ranking — its 3B active was below the + 12B auto-floor for 30GB+ GPUs even though its 80B total clearly fits. + """ + return model.parameter_count / 1e9 + + +def _passes_evidence_filter(source: str, evidence_filter: str) -> bool: + """判定根拠フィルタに合致するかを返す。""" + mode = evidence_filter.lower() + if mode == "strict": + return source == "direct" + if mode == "base": + return source in {"direct", "variant", "base_model"} + return True + + +def _is_gguf_only_backend(hardware: HardwareInfo) -> bool: + """実行基盤の都合でGGUFのみを許可すべきか判定する。""" + # Apple Silicon(macOS/Metal)とCPU-onlyは、実運用の安定性を優先してGGUFに限定する。 + if hardware.os == "darwin": + return True + if not hardware.gpus: + return True + + # Linux + NVIDIA (CUDA) は AWQ/GPTQ 含む非GGUFも許可する。 + has_linux_nvidia = hardware.os == "linux" and any( + g.vendor == "nvidia" for g in hardware.gpus + ) + return not has_linux_nvidia + + +def _compute_quality_score( + model: ModelInfo, + variant: GGUFVariant | None, + tok_per_sec: float, + fit_type: str, + offload_ratio: float = 0.0, + family_downloads: int = 0, + family_likes: int = 0, + benchmark_avg: float | None = None, + benchmark_source: str = "none", +) -> float: + """Compute a quality score (0-100) for ranking. + + Factors: + - Benchmark score weighted by source tier + - Model size (log scale) + - Quantization penalty + - Fit type penalty (partial offload / CPU-only heavily penalized) + - Speed bonus / penalty (practical usability) + - Popularity (downloads/likes) as soft tie-breaker + - Official org bonus (vs known repackagers) + - Generation-lineage bonus (newest family member > legacy generation) + """ + params_b = model.parameter_count / 1e9 + if model.is_moe and model.parameter_count_active: + effective_b = model.parameter_count_active / 1e9 + else: + effective_b = params_b + + if effective_b <= 0: + return 0.0 + + # Benchmarks lead, but raw model size also matters: a 70B at Q4_K_M + # carries far more world knowledge than a 7B Q4_K_M even when the + # leaderboard score gap is modest. For MoE models, knowledge capacity + # tracks *total* params (every expert contributes to what the model + # knows), while routing keeps per-token compute small. Use total params + # for the size score and let the speed term separately reward MoE + # efficiency. + size_basis_b = params_b + size_score = 4.2 * math.log2(max(size_basis_b, 0.5)) + 9 + size_score = min(size_score, 35) + + has_benchmark = benchmark_avg is not None and benchmark_avg > 0 + is_direct = benchmark_source == "direct" + is_self_reported = benchmark_source == "self_reported" + is_inherited = benchmark_source in {"variant", "base_model", "line_interp"} + + bench_weight = _SOURCE_WEIGHTS.get(benchmark_source, 0.0) + benchmark_score = 0.0 + if has_benchmark: + raw = min(100.0, benchmark_avg) + benchmark_score = raw * bench_weight + + # Quantization penalty + quant_penalty = quant_quality_penalty(model, variant) + quality_core = (benchmark_score + size_score) * (1 - quant_penalty) + + # Weak / unverifiable evidence gets an extra discount. + if not has_benchmark: + quality_core *= 0.55 + elif is_self_reported: + quality_core *= 0.55 # uploader claim, easily fabricated + elif is_inherited: + quality_core *= 0.78 + + # Runtime form factor penalty + if fit_type == "partial_offload": + quality_core *= _partial_offload_quality_factor(model, offload_ratio) + elif fit_type == "cpu_only": + quality_core *= 0.50 + + # Speed acts as a usability gate rather than a ranking primary. + required_speed = ( + 8.0 + if fit_type == "full_gpu" + else (4.0 if fit_type == "partial_offload" else 1.5) + ) + if tok_per_sec > 0: + if tok_per_sec < required_speed: + speed_score = -8.0 * (1 - (tok_per_sec / required_speed)) + else: + speed_score = min(8.0, math.log2(tok_per_sec / required_speed + 1.0) * 3.2) + else: + if fit_type == "partial_offload": + if offload_ratio >= 0.70: + speed_score = -24.0 + elif offload_ratio >= 0.40: + speed_score = -18.0 + else: + speed_score = -12.0 + else: + speed_score = -8.0 + + # Popularity is a tie-breaker, never primary. + downloads = max(model.downloads, family_downloads) + likes = max(model.likes, family_likes) + pop_score_raw = 0.0 + if downloads > 0: + pop_score_raw += min(1.0, math.log10(max(downloads, 1)) / 6 * 1.0) + if likes > 0: + pop_score_raw += min(1.0, math.log10(max(likes, 1)) / 4 * 1.0) + + if is_direct: + pop_weight = 0.0 + elif is_self_reported: + pop_weight = 0.4 # uploader claim is weak — popularity acts as sanity check + elif has_benchmark: + pop_weight = 0.2 + else: + pop_weight = 0.6 + pop_score = pop_score_raw * pop_weight + + # Source-trust bonus stays small. + source_bonus_raw = 0.0 + org = model.id.split("/")[0] if "/" in model.id else "" + if org in _OFFICIAL_ORGS: + source_bonus_raw = 5.0 + elif org in _REPACKAGER_ORGS: + source_bonus_raw = -5.0 + elif model.base_model: + base_org = model.base_model.split("/")[0] if "/" in model.base_model else "" + if base_org in _OFFICIAL_ORGS: + if org in _TRUSTED_CONVERTERS: + source_bonus_raw = 5.0 + else: + source_bonus_raw = 0.0 + + if is_direct: + source_weight = 0.2 + elif is_self_reported: + source_weight = 0.5 + elif has_benchmark: + source_weight = 0.4 + else: + source_weight = 0.6 + source_bonus = source_bonus_raw * source_weight + + # Generation lineage bonus: newest in a known family gets a small boost, + # confirmed legacy versions get a small penalty. Helps surface Qwen3.6, + # DeepSeek V4, Gemma 4, etc. against accumulated download leaders. + gen_bonus = _generation_bonus(model.id) + # When benchmark evidence is missing or self-reported, the lineage signal + # carries more weight (we have less else to go on). + if not has_benchmark or is_self_reported: + gen_bonus *= 1.5 + elif is_direct: + gen_bonus *= 0.6 + + # Penalty for "uncensored / abliterated / heretic / RP" derivatives that + # ride on a base model's score without independent benchmarking. + derivative_penalty = _derivative_name_penalty(model.id) + + return max( + 0.0, + min( + 100.0, + quality_core + + speed_score + + pop_score + + source_bonus + + gen_bonus + + derivative_penalty, + ), + ) + + +def rank_models( + models: list[ModelInfo], + hardware: HardwareInfo, + context_length: int = 4096, + top_n: int = 10, + quant_filter: str | None = None, + min_speed: float | None = None, + benchmark_scores: dict[str, float] | None = None, + task_profile: str = "general", + require_direct_top: bool = True, + min_params_b: float | None = None, + evidence_filter: str = "any", + fit_filter: str = "any", +) -> list[CompatibilityResult]: + """Rank models by quality for the given hardware. Returns top N results.""" + results: list[CompatibilityResult] = [] + gguf_only_backend = _is_gguf_only_backend(hardware) + + # Pre-compute max downloads/likes per family so GGUF converters + # inherit popularity from the official base model + family_max_downloads: dict[str, int] = {} + family_max_likes: dict[str, int] = {} + # Track the parameter count of the family's dominant member (highest + # downloads). Used to detect quasi-fork uploads whose params differ + # drastically from the family proper (e.g. a 6.6B MTP-head extracted + # from a 158B base ending up tagged with the same family_id). + family_dominant_params: dict[str, int] = {} + family_dominant_downloads: dict[str, int] = {} + for m in models: + fid = m.family_id + family_max_downloads[fid] = max(family_max_downloads.get(fid, 0), m.downloads) + family_max_likes[fid] = max(family_max_likes.get(fid, 0), m.likes) + if m.parameter_count and m.downloads >= family_dominant_downloads.get(fid, -1): + family_dominant_downloads[fid] = m.downloads + family_dominant_params[fid] = m.parameter_count + + # Deduplicate by family: pick best variant per family + seen_families: set[str] = set() + + # Sort models by downloads (popular first) to process best candidates first + sorted_models = sorted(models, key=lambda m: m.downloads, reverse=True) + + # Build benchmark indices once (case-insensitive + model line) + if benchmark_scores: + bench_ci_index, bench_line_index = build_score_index(benchmark_scores) + bench_line_buckets = build_line_bucket_index(benchmark_scores) + else: + bench_ci_index, bench_line_index = {}, {} + bench_line_buckets = {} + + best_gpu = None + for gpu in hardware.gpus: + if best_gpu is None or gpu.vram_bytes > best_gpu.vram_bytes: + best_gpu = gpu + + for model in sorted_models: + if _is_excluded_model(model.id): + continue + if not _matches_profile(model, task_profile): + continue + if min_params_b is not None and _knowledge_capacity_b(model) < min_params_b: + continue + + candidates = _iter_candidate_variants(model, quant_filter) + if not candidates: + continue + + fid = model.family_id + # Uploader-reported evalResults are only ever last-resort evidence. + self_reported = None + if isinstance(model.benchmark_scores, dict): + v = model.benchmark_scores.get("hf_eval") + if isinstance(v, (int, float)) and v > 0: + self_reported = float(v) + + bench_evidence = BenchmarkEvidence(score=None, confidence=0.0, source="none") + if benchmark_scores or self_reported is not None: + actual_params_b = ( + (model.parameter_count or 0) / 1e9 if model.parameter_count else None + ) + bench_evidence = lookup_benchmark_evidence( + model.id, + model.base_model, + benchmark_scores or {}, + ci_index=bench_ci_index, + line_index=bench_line_index, + line_bucket_index=bench_line_buckets, + self_reported_score=self_reported, + actual_params_b=actual_params_b, + ) + # Family-size sanity check: if this model inherited benchmarks + # via family/base_model lookup but its own params disagree + # sharply with the family's dominant member, reject the + # inheritance. Catches MTP heads / draft / abliterated forks + # that share a family_id with their base but are effectively + # different models. + if bench_evidence.source in ("variant", "base_model", "line_interp"): + dom_params = family_dominant_params.get(model.family_id) + if dom_params and model.parameter_count and dom_params > 0: + ratio = model.parameter_count / dom_params + if ratio < 0.5 or ratio > 2.0: + bench_evidence = BenchmarkEvidence( + score=None, confidence=0.0, source="none" + ) + if not _passes_evidence_filter(bench_evidence.source, evidence_filter): + continue + + # 各variantを評価し、そのモデルで最もスコアが高いものを採用する + best_for_model: CompatibilityResult | None = None + for variant in candidates: + if gguf_only_backend and variant is None: + continue + compat = check_compatibility(model, variant, hardware, context_length) + if not compat.can_run: + continue + if fit_filter == "full_gpu" and compat.fit_type != "full_gpu": + continue + + tok_per_sec = estimate_tok_per_sec( + model, variant, best_gpu, compat.fit_type + ) + if compat.uses_multi_gpu: + tok_per_sec *= _MULTI_GPU_SPEED_FACTOR + if min_speed is not None and tok_per_sec < min_speed: + continue + + bench_avg = None + if bench_evidence.score is not None: + if bench_evidence.source in {"direct", "self_reported"}: + bench_avg = bench_evidence.score + else: + # Inherited evidence: scale by confidence so weak inheritance + # (e.g. line_interp at conf 0.22) gets discounted on top of + # the per-source weight in _compute_quality_score. + confidence = max(0.0, min(1.0, bench_evidence.confidence)) + bench_avg = bench_evidence.score * (0.75 + 0.25 * confidence) + + compat.estimated_tok_per_sec = tok_per_sec + ( + compat.speed_confidence, + compat.speed_range_tok_per_sec, + compat.speed_notes, + ) = estimate_speed_uncertainty( + model, + variant, + best_gpu, + compat.fit_type, + tok_per_sec, + ) + if compat.uses_multi_gpu: + compat.speed_confidence = "low" + if tok_per_sec > 0: + compat.speed_range_tok_per_sec = ( + round(tok_per_sec * 0.35, 1), + round(tok_per_sec * 2.0, 1), + ) + compat.speed_notes.append( + "Multi-GPU speed depends on layer/tensor split mode, " + "PCIe/NVLink bandwidth, and backend support; this estimate " + "does not assume ideal scaling." + ) + compat.quality_score = _compute_quality_score( + model, + variant, + tok_per_sec, + compat.fit_type, + offload_ratio=compat.offload_ratio, + family_downloads=family_max_downloads.get(fid, 0), + family_likes=family_max_likes.get(fid, 0), + benchmark_avg=bench_avg, + benchmark_source=bench_evidence.source, + ) + # Map evidence source to a 4-value display status. "self_reported" + # is shown distinctly so users can spot uploader-claimed numbers. + if bench_evidence.score is None: + compat.benchmark_status = "none" + elif bench_evidence.source == "direct": + compat.benchmark_status = "direct" + elif bench_evidence.source == "self_reported": + compat.benchmark_status = "self_reported" + else: + compat.benchmark_status = "estimated" + compat.benchmark_source = bench_evidence.source + compat.benchmark_confidence = bench_evidence.confidence + + if ( + best_for_model is None + or compat.quality_score > best_for_model.quality_score + ): + best_for_model = compat + + if best_for_model is None: + continue + + # Deduplicate by family: keep the one with highest quality score + family_key = model.family_id + if family_key in seen_families: + # Check if this is better than existing + existing = next( + (r for r in results if r.model.family_id == family_key), None + ) + if existing and _family_selection_key( + best_for_model, + require_direct_top, + ) > _family_selection_key(existing, require_direct_top): + results.remove(existing) + results.append(best_for_model) + continue + + seen_families.add(family_key) + results.append(best_for_model) + + if require_direct_top: + results.sort( + key=lambda r: _family_selection_key(r, require_direct_top), + reverse=True, + ) + else: + results.sort( + key=lambda r: _family_selection_key(r, require_direct_top), reverse=True + ) + + # Junk floor: when at least one candidate scores ≥ 30, drop anything + # below 20. This stops Q1_0 / Q2_0 derivatives (and other extreme-quant + # repos) from occupying ranking slots when a *real* option exists. If + # every candidate is junk (very tiny GPU + no fitting Q4) we keep the + # whole list so the user still sees what they can run. + if any(r.quality_score >= 30 for r in results): + results = [r for r in results if r.quality_score >= 20] + + # Speed floor: a model that scores well on quality but runs at <1.5 t/s + # in practice (e.g. DeepSeek-V4-Flash 158B partial-offloading 100GB to + # CPU RAM from a 4GB GTX 1650) is not actually usable. Drop these + # candidates unless every remaining option is sub-1.5 too, in which + # case the user has hardware that cannot run anything responsively + # and we still want to show what's available. + if any(r.estimated_tok_per_sec >= 5.0 for r in results): + results = [r for r in results if r.estimated_tok_per_sec >= 1.5] + + # Clamp top_n: a negative value would slice from the end + # (``results[:-5]``) and silently return a truncated, unrequested subset, + # while 0 legitimately yields an empty list. The CLI rejects non-positive + # --top, but guard here too so direct callers of this public helper never + # get a truncated ranking from a stray negative count. + return results[: max(top_n, 0)] diff --git a/src/whichllm/engine/types.py b/src/whichllm/engine/types.py new file mode 100644 index 0000000..60457e3 --- /dev/null +++ b/src/whichllm/engine/types.py @@ -0,0 +1,30 @@ +from __future__ import annotations + +from dataclasses import dataclass, field + +from whichllm.models.types import GGUFVariant, ModelInfo + + +@dataclass +class CompatibilityResult: + model: ModelInfo + gguf_variant: GGUFVariant | None + can_run: bool + vram_required_bytes: int + vram_available_bytes: int + offload_ratio: float = 0.0 # 0.0-1.0 fraction of weights spilled to CPU RAM + estimated_tok_per_sec: float | None = None + speed_confidence: str = "medium" # "high" | "medium" | "low" + speed_range_tok_per_sec: tuple[float, float] | None = None + speed_notes: list[str] = field(default_factory=list) + warnings: list[str] = field(default_factory=list) + quality_score: float = 0.0 # 0-100 for ranking + fit_type: str = "full_gpu" # "full_gpu" | "partial_offload" | "cpu_only" + benchmark_status: str = "none" # "direct" | "estimated" | "self_reported" | "none" + benchmark_source: str = "none" # granular: "direct" | "variant" | "base_model" | "line_interp" | "self_reported" | "none" + benchmark_confidence: float = 0.0 # 0.0-1.0 from BenchmarkEvidence + context_fits: bool = True # False when known model max context < requested + uses_multi_gpu: bool = False + multi_gpu_effective_vram_bytes: int | None = None + artifact_model: ModelInfo | None = None + artifact_variant: GGUFVariant | None = None diff --git a/src/whichllm/engine/vram.py b/src/whichllm/engine/vram.py new file mode 100644 index 0000000..d5427be --- /dev/null +++ b/src/whichllm/engine/vram.py @@ -0,0 +1,100 @@ +"""VRAM usage estimation.""" + +from __future__ import annotations + +from whichllm.constants import FRAMEWORK_OVERHEAD_BYTES +from whichllm.engine.quantization import estimate_weight_bytes +from whichllm.models.types import GGUFVariant, ModelInfo + +# Empirical KV-cache coefficient: bytes per B-active-param per K-context-token +# for FP16 K/V tensors. Calibrated against published llama.cpp memory reports +# for Qwen2.5-7B (0.45 GB @ 8K), Qwen3-32B (3.1 GB @ 32K), and Llama-3.1-70B +# (5.4 GB @ 32K with GQA), then bumped slightly because real llama.cpp also +# allocates a graph-compute buffer proportional to KV size. +_KV_BYTES_PER_BPARAM_PER_KCTX = 3.5 * 1024 * 1024 # 3.5 MB + +# MoE attention scales with the *attention-layer count*, which is roughly +# proportional to active_params * this multiplier. For Qwen3-Next-80B-A3B +# (3B active, 48 layers), the multiplier lands near 4. +_MOE_ATTENTION_PARAM_MULTIPLIER = 4.0 + + +def _effective_kv_context(model: ModelInfo, context_length: int) -> float: + """Context length that actually contributes to the KV cache. + + For sliding-window-attention (SWA) models, local-attention layers only keep + the last ``sliding_window`` tokens, so their KV footprint plateaus once the + request exceeds the window. Hybrid models interleave a fraction of global + (full-context) layers; that fraction is ``sliding_window_global_ratio``. + + Effective context blends the two layer types:: + + global_ratio * ctx + (1 - global_ratio) * min(ctx, window) + + This is only applied for architectures whose mainline runtimes honor SWA + (the fetcher leaves ``sliding_window`` ``None`` otherwise), and it can only + ever lower the estimate — so a model that does not advertise an honored + window keeps the full-context KV figure and stays conservative. + """ + window = model.sliding_window + ratio = model.sliding_window_global_ratio + if not window or window <= 0 or ratio is None: + return float(context_length) + ratio = min(max(ratio, 0.0), 1.0) + windowed = min(context_length, window) + return ratio * context_length + (1.0 - ratio) * windowed + + +def estimate_kv_cache(model: ModelInfo, context_length: int) -> int: + """Estimate KV cache size in bytes for a given context length. + + Dense models: KV ≈ 3 MB × params_b × ctx_k (FP16 K+V across all layers). + MoE models: scale from active params × an empirical multiplier because + attention shares across experts. + Sliding-window models cap local-layer KV at the window size (see + :func:`_effective_kv_context`). + """ + if model.is_moe and model.parameter_count_active: + # Active-params × MoE multiplier gives a reasonable proxy for the + # attention-layer footprint without needing config.num_hidden_layers. + active_b = model.parameter_count_active / 1e9 + params_b = active_b * _MOE_ATTENTION_PARAM_MULTIPLIER + else: + params_b = model.parameter_count / 1e9 + + effective_ctx = _effective_kv_context(model, context_length) + ctx_k = effective_ctx / 1024 + kv_bytes = int(params_b * ctx_k * _KV_BYTES_PER_BPARAM_PER_KCTX) + return max(kv_bytes, 0) + + +def _activation_bytes(model: ModelInfo, context_length: int) -> int: + """Activation/scratch buffer size. + + Empirically activation memory grows mildly with both model size and + context length. The prior constant-plus-linear-param formula + over-counted small models and under-counted long contexts. + """ + # Use effective (active for MoE) size as the param-dependent base + if model.is_moe and model.parameter_count_active: + effective_p = model.parameter_count_active + else: + effective_p = model.parameter_count + + base = 400_000_000 # 400 MB framework activation floor + param_term = int(effective_p * 0.08) # ~0.08 byte/param + ctx_term = int((context_length / 4096) * 150_000_000) # +150 MB per 4K + return base + param_term + ctx_term + + +def estimate_vram( + model: ModelInfo, + variant: GGUFVariant | None, + context_length: int = 4096, +) -> int: + """Estimate total VRAM required to run a model.""" + weights = estimate_weight_bytes(model, variant) + kv_cache = estimate_kv_cache(model, context_length) + activation = _activation_bytes(model, context_length) + framework = FRAMEWORK_OVERHEAD_BYTES + return weights + kv_cache + activation + framework diff --git a/src/whichllm/hardware/__init__.py b/src/whichllm/hardware/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/whichllm/hardware/amd.py b/src/whichllm/hardware/amd.py new file mode 100644 index 0000000..5122920 --- /dev/null +++ b/src/whichllm/hardware/amd.py @@ -0,0 +1,286 @@ +"""AMD GPU detection via rocm-smi with Linux fallback probes.""" + +from __future__ import annotations + +import json +import logging +import shlex +import subprocess +from pathlib import Path + +from whichllm.constants import AMD_SHARED_MEMORY_APU_MARKERS, _GiB +from whichllm.hardware.gpu_db import _static_bandwidth, resolve_detected_bandwidth +from whichllm.hardware.types import GPUInfo + +logger = logging.getLogger(__name__) + +_DISPLAY_CLASSES = ( + "vga compatible controller", + "3d controller", + "display controller", +) + + +def _lookup_bandwidth(name: str) -> float | None: + """Curated GPU_BANDWIDTH lookup, compound-lspci aware. Kept for regression + tests; live detection goes through ``resolve_detected_bandwidth``, which + also consults dbgpu.""" + return _static_bandwidth(name) + + +def _is_shared_memory_apu(name: str) -> bool: + name_upper = name.upper() + return any(marker in name_upper for marker in AMD_SHARED_MEMORY_APU_MARKERS) + + +def _normalize_apu_vram(name: str, vram_bytes: int) -> int: + if _is_shared_memory_apu(name) and vram_bytes < 2 * _GiB: + return 0 + return vram_bytes + + +def _make_gpu( + name: str, + *, + vram_bytes: int = 0, + rocm_version: str | None = None, +) -> GPUInfo: + shared_memory = _is_shared_memory_apu(name) + return GPUInfo( + name=name, + vendor="amd", + vram_bytes=_normalize_apu_vram(name, vram_bytes), + rocm_version=rocm_version, + memory_bandwidth_gbps=resolve_detected_bandwidth(name, vram_bytes), + shared_memory=shared_memory, + ) + + +_AMD_VENDOR_MARKERS = ( + "advanced micro devices", + "amd/ati", + "[amd]", + "[ati]", + "ati technologies", +) + + +def _vendor_is_amd(vendor: str) -> bool: + vendor_lower = vendor.lower() + return any(marker in vendor_lower for marker in _AMD_VENDOR_MARKERS) + + +def _detect_from_lspci() -> list[str]: + try: + result = subprocess.run( + ["lspci", "-mm"], + capture_output=True, + text=True, + timeout=5, + ) + except (FileNotFoundError, subprocess.TimeoutExpired): + logger.debug("lspci not available or timed out") + return [] + + if result.returncode != 0: + return [] + + names: list[str] = [] + seen: set[str] = set() + for line in result.stdout.splitlines(): + # `lspci -mm` is the machine-parsable format: + # "" "" "" [flags] ["" ""] + # Parse the quoted columns properly and check the vendor field + # specifically, instead of substring-matching the whole line (which + # would treat e.g. "Intel Corpor[ati]on" as AMD). + try: + tokens = shlex.split(line) + except ValueError: + continue + if len(tokens) < 4: + continue + device_class, vendor, device = tokens[1], tokens[2], tokens[3] + if device_class.lower() not in _DISPLAY_CLASSES: + continue + if not _vendor_is_amd(vendor): + continue + name = device.strip() or "AMD Graphics" + if name not in seen: + names.append(name) + seen.add(name) + return names + + +def _read_int(path: Path) -> int: + try: + text = path.read_text().strip() + except OSError: + return 0 + try: + return int(text, 0) + except ValueError: + return 0 + + +def _detect_from_sysfs(drm_path: Path = Path("/sys/class/drm")) -> list[GPUInfo]: + gpus: list[GPUInfo] = [] + seen: set[str] = set() + try: + cards = sorted(drm_path.glob("card[0-9]*")) + except OSError: + return [] + + for card in cards: + device = card / "device" + try: + vendor = (device / "vendor").read_text().strip().lower() + except OSError: + continue + if vendor != "0x1002": + continue + + name = "AMD Graphics" + try: + product_name = (device / "product_name").read_text().strip() + if product_name: + name = product_name + except OSError: + pass + + vram_bytes = _read_int(device / "mem_info_vram_total") + key = f"{name}:{vram_bytes}" + if key in seen: + continue + seen.add(key) + gpus.append(_make_gpu(name, vram_bytes=vram_bytes)) + return gpus + + +def _read_sysfs_amd_vram(drm_path: Path = Path("/sys/class/drm")) -> list[int]: + """Read VRAM for each AMD GPU from sysfs, in card order.""" + result: list[int] = [] + try: + cards = sorted(drm_path.glob("card[0-9]*")) + except OSError: + return [] + for card in cards: + device = card / "device" + try: + vendor = (device / "vendor").read_text().strip().lower() + except OSError: + continue + if vendor != "0x1002": + continue + result.append(_read_int(device / "mem_info_vram_total")) + return result + + +def _detect_amd_gpus_fallback() -> list[GPUInfo]: + # Prefer sysfs: it provides VRAM and sometimes a clean product name. + sysfs_gpus = _detect_from_sysfs() + + if sysfs_gpus: + # If sysfs names are generic ("AMD Graphics"), enrich with lspci names. + has_generic = any(g.name == "AMD Graphics" for g in sysfs_gpus) + if has_generic: + lspci_names = _detect_from_lspci() + if lspci_names and len(lspci_names) == len(sysfs_gpus): + return [ + _make_gpu( + lspci_names[i] if gpu.name == "AMD Graphics" else gpu.name, + vram_bytes=gpu.vram_bytes, + ) + for i, gpu in enumerate(sysfs_gpus) + ] + return sysfs_gpus + + # sysfs unavailable; fall back to lspci (name only), enriching with + # sysfs VRAM when possible. + names = _detect_from_lspci() + if names: + vram_list = _read_sysfs_amd_vram() + return [ + _make_gpu(name, vram_bytes=vram_list[i] if i < len(vram_list) else 0) + for i, name in enumerate(names) + ] + return [] + + +def detect_amd_gpus() -> list[GPUInfo]: + """Detect AMD GPUs. Returns empty list on failure.""" + gpus: list[GPUInfo] = [] + + # Get product names + try: + result = subprocess.run( + ["rocm-smi", "--showproductname", "--json"], + capture_output=True, + text=True, + timeout=10, + ) + if result.returncode != 0: + return _detect_amd_gpus_fallback() + product_data = json.loads(result.stdout) + except (FileNotFoundError, subprocess.TimeoutExpired, json.JSONDecodeError): + logger.debug("rocm-smi not available or failed") + return _detect_amd_gpus_fallback() + + # Get VRAM info + try: + result = subprocess.run( + ["rocm-smi", "--showmeminfo", "vram", "--json"], + capture_output=True, + text=True, + timeout=10, + ) + if result.returncode != 0: + return _detect_amd_gpus_fallback() + mem_data = json.loads(result.stdout) + except (FileNotFoundError, subprocess.TimeoutExpired, json.JSONDecodeError): + logger.debug("Failed to get AMD VRAM info") + return _detect_amd_gpus_fallback() + + # Get ROCm version + rocm_version = None + try: + result = subprocess.run( + ["rocm-smi", "--showdriverversion", "--json"], + capture_output=True, + text=True, + timeout=10, + ) + if result.returncode == 0: + driver_data = json.loads(result.stdout) + # Extract version from first card entry + for key, val in driver_data.items(): + if isinstance(val, dict) and "Driver version" in val: + rocm_version = val["Driver version"] + break + except Exception: + pass + + # Parse GPU info - rocm-smi JSON keys are like "card0", "card1" + for key in sorted(product_data.keys()): + if not key.startswith("card"): + continue + card_info = product_data[key] + # rocm-smi JSON uses "Card Series" (capital S) for the human-readable name. + # Fall back through SKU only as a last resort. + name = ( + card_info.get("Card Series") + or card_info.get("Card series") + or card_info.get("Card SKU") + or "Unknown AMD GPU" + ) + + vram_total = 0 + if key in mem_data: + vram_str = mem_data[key].get("VRAM Total Memory (B)", "0") + try: + vram_total = int(vram_str) + except (ValueError, TypeError): + pass + + gpus.append(_make_gpu(name, vram_bytes=vram_total, rocm_version=rocm_version)) + + return gpus diff --git a/src/whichllm/hardware/apple.py b/src/whichllm/hardware/apple.py new file mode 100644 index 0000000..47fbe54 --- /dev/null +++ b/src/whichllm/hardware/apple.py @@ -0,0 +1,131 @@ +"""Apple Silicon detection via system_profiler (macOS) and sysfs (Asahi Linux).""" + +from __future__ import annotations + +import json +import logging +import re +import subprocess +from pathlib import Path + +from whichllm.constants import GPU_BANDWIDTH +from whichllm.hardware.types import GPUInfo + +logger = logging.getLogger(__name__) + + +def _lookup_bandwidth(chip_name: str) -> float | None: + chip_upper = chip_name.upper() + for key in sorted(GPU_BANDWIDTH, key=len, reverse=True): + if key.upper() in chip_upper: + return GPU_BANDWIDTH[key] + return None + + +def detect_apple_gpu() -> list[GPUInfo]: + """Detect Apple Silicon GPU. Returns empty list on non-macOS or failure.""" + try: + result = subprocess.run( + ["system_profiler", "SPHardwareDataType", "-json"], + capture_output=True, + text=True, + timeout=10, + ) + if result.returncode != 0: + return [] + data = json.loads(result.stdout) + except (FileNotFoundError, subprocess.TimeoutExpired, json.JSONDecodeError): + logger.debug("system_profiler not available (not macOS)") + return [] + + try: + hw_items = data["SPHardwareDataType"] + hw = hw_items[0] + chip_name = hw.get("chip_type", "") + if not chip_name: + return [] + + # Apple Silicon uses unified memory - get total physical memory + memory_str = hw.get("physical_memory", "0 GB") + # Parse "32 GB" -> bytes + parts = memory_str.split() + mem_value = int(parts[0]) + mem_unit = parts[1].upper() if len(parts) > 1 else "GB" + multiplier = {"GB": 1024**3, "TB": 1024**4, "MB": 1024**2}.get( + mem_unit, 1024**3 + ) + unified_memory = mem_value * multiplier + + return [ + GPUInfo( + name=chip_name, + vendor="apple", + vram_bytes=unified_memory, # unified memory + memory_bandwidth_gbps=_lookup_bandwidth(chip_name), + shared_memory=True, + ) + ] + except (KeyError, IndexError, ValueError) as e: + logger.debug(f"Failed to parse Apple hardware info: {e}") + return [] + + +# ---- Asahi Linux (Apple Silicon on Linux) ---- + +_ASAHI_DRIVER_NAMES = ("asahi", "apple") + + +def _chip_name_from_devicetree() -> str | None: + """Extract Apple chip name from Linux device tree.""" + try: + raw = Path("/sys/firmware/devicetree/base/model").read_bytes() + model = raw.decode("utf-8", errors="replace").strip().rstrip("\x00") + if not model: + return None + m = re.search(r"\b(M\d+(?:\s+(?:Pro|Max|Ultra))?)\b", model) + if m: + return f"Apple {m.group(1)}" + return model + except OSError: + return None + + +def detect_apple_gpu_linux( + drm_path: Path = Path("/sys/class/drm"), +) -> list[GPUInfo]: + """Detect Apple Silicon GPU on Linux (Asahi driver). + + Returns empty list when no Asahi/Apple DRM device is found. + """ + try: + cards = sorted(drm_path.glob("card[0-9]*")) + except OSError: + return [] + + for card in cards: + driver = card / "device" / "driver" + try: + driver_name = driver.resolve().name + except OSError: + continue + if driver_name not in _ASAHI_DRIVER_NAMES: + continue + + chip_name = _chip_name_from_devicetree() or "Apple Silicon" + + # Unified memory — total system RAM is shared with the GPU. + import psutil + + unified_memory = psutil.virtual_memory().total + + return [ + GPUInfo( + name=chip_name, + vendor="apple", + vram_bytes=unified_memory, + memory_bandwidth_gbps=_lookup_bandwidth(chip_name), + shared_memory=True, + ) + ] + + return [] diff --git a/src/whichllm/hardware/cpu.py b/src/whichllm/hardware/cpu.py new file mode 100644 index 0000000..3d34b68 --- /dev/null +++ b/src/whichllm/hardware/cpu.py @@ -0,0 +1,225 @@ +"""CPU detection: name, cores, AVX2/AVX512 support.""" + +from __future__ import annotations + +import logging +import platform +import re +import subprocess +from pathlib import Path + +logger = logging.getLogger(__name__) + + +def _cpu_name_from_lscpu() -> str | None: + """Try to get CPU model name from lscpu (works on ARM/aarch64).""" + try: + result = subprocess.run(["lscpu"], capture_output=True, text=True, timeout=5) + if result.returncode == 0: + for line in result.stdout.splitlines(): + if line.strip().startswith("Model name"): + name = line.split(":", 1)[1].strip() + if name and name != "-": + return name + except (FileNotFoundError, subprocess.TimeoutExpired): + pass + return None + + +def _cpu_name_from_devicetree() -> str | None: + """Extract CPU/chip name from device tree (ARM Linux, Asahi).""" + try: + raw = Path("/sys/firmware/devicetree/base/model").read_bytes() + model = raw.decode("utf-8", errors="replace").strip().rstrip("\x00") + if not model: + return None + # "Apple MacBook Air (M2, 2022)" → "Apple M2" + m = re.search(r"\b(M\d+(?:\s+(?:Pro|Max|Ultra))?)\b", model) + if m: + return f"Apple {m.group(1)}" + return model + except OSError: + return None + + +def _clean_cpu_name(name: str | None) -> str | None: + if name is None: + return None + cleaned = name.strip() + if not cleaned or cleaned == "-" or cleaned.lower() == "name": + return None + return cleaned + + +def _cpu_name_from_wmic() -> str | None: + try: + result = subprocess.run( + ["wmic", "cpu", "get", "name"], + capture_output=True, + text=True, + timeout=5, + ) + except (FileNotFoundError, subprocess.SubprocessError, OSError): + return None + + if result.returncode != 0: + return None + + for line in result.stdout.splitlines(): + name = _clean_cpu_name(line) + if name: + return name + return None + + +def _cpu_name_from_windows_cim() -> str | None: + script = ( + "Get-CimInstance Win32_Processor | Select-Object -First 1 -ExpandProperty Name" + ) + for executable in ("powershell", "pwsh"): + try: + result = subprocess.run( + [executable, "-NoProfile", "-Command", script], + capture_output=True, + text=True, + timeout=5, + ) + except (FileNotFoundError, subprocess.SubprocessError, OSError): + continue + + if result.returncode != 0: + continue + + for line in result.stdout.splitlines(): + name = _clean_cpu_name(line) + if name: + return name + return None + + +def detect_cpu_name() -> str: + """Get CPU model name.""" + system = platform.system() + try: + if system == "Linux": + with open("/proc/cpuinfo") as f: + for line in f: + if line.startswith("model name"): + return line.split(":", 1)[1].strip() + # ARM/aarch64: /proc/cpuinfo has no model name field. + # Try lscpu, then device tree. + name = _cpu_name_from_lscpu() or _cpu_name_from_devicetree() + if name: + return name + elif system == "Darwin": + result = subprocess.run( + ["sysctl", "-n", "machdep.cpu.brand_string"], + capture_output=True, + text=True, + timeout=5, + ) + if result.returncode == 0: + name = _clean_cpu_name(result.stdout) + if name: + return name + elif system == "Windows": + name = _cpu_name_from_wmic() or _cpu_name_from_windows_cim() + if name: + return name + except Exception as e: + logger.debug(f"Failed to detect CPU name: {e}") + return "Unknown CPU" + + +def _count_physical_cores_linux() -> int | None: + """Count unique physical cores from /proc/cpuinfo (handles WSL2).""" + try: + physical_ids: set[tuple[str, str]] = set() + current_physical = "" + current_core = "" + with open("/proc/cpuinfo") as f: + for line in f: + if line.startswith("physical id"): + current_physical = line.split(":", 1)[1].strip() + elif line.startswith("core id"): + current_core = line.split(":", 1)[1].strip() + physical_ids.add((current_physical, current_core)) + if physical_ids: + return len(physical_ids) + except Exception: + pass + return None + + +def detect_cpu_cores() -> int: + """Get number of physical CPU cores.""" + import psutil + + cores = psutil.cpu_count(logical=False) + if cores: + return cores + + # Fallback for WSL2 where psutil may return None for physical cores + if platform.system() == "Linux": + linux_cores = _count_physical_cores_linux() + if linux_cores: + return linux_cores + + return psutil.cpu_count(logical=True) or 1 + + +def _detect_avx_linux() -> tuple[bool, bool]: + """Detect AVX2/AVX512 on Linux via /proc/cpuinfo.""" + has_avx2 = False + has_avx512 = False + try: + with open("/proc/cpuinfo") as f: + content = f.read() + flags_line = "" + for line in content.split("\n"): + if line.startswith("flags"): + flags_line = line + break + has_avx2 = "avx2" in flags_line + has_avx512 = "avx512f" in flags_line + except Exception: + pass + return has_avx2, has_avx512 + + +def _detect_avx_darwin() -> tuple[bool, bool]: + """Detect AVX2/AVX512 on macOS via sysctl.""" + has_avx2 = False + has_avx512 = False + try: + result = subprocess.run( + ["sysctl", "-n", "hw.optional.avx2_0"], + capture_output=True, + text=True, + timeout=5, + ) + has_avx2 = result.stdout.strip() == "1" + except Exception: + pass + try: + result = subprocess.run( + ["sysctl", "-n", "hw.optional.avx512f"], + capture_output=True, + text=True, + timeout=5, + ) + has_avx512 = result.stdout.strip() == "1" + except Exception: + pass + return has_avx2, has_avx512 + + +def detect_avx_support() -> tuple[bool, bool]: + """Detect AVX2 and AVX512 support. Returns (has_avx2, has_avx512).""" + system = platform.system() + if system == "Linux": + return _detect_avx_linux() + elif system == "Darwin": + return _detect_avx_darwin() + # Windows / fallback: assume AVX2 on modern CPUs + return True, False diff --git a/src/whichllm/hardware/detector.py b/src/whichllm/hardware/detector.py new file mode 100644 index 0000000..da4cf80 --- /dev/null +++ b/src/whichllm/hardware/detector.py @@ -0,0 +1,56 @@ +"""Unified hardware detection orchestrator.""" + +from __future__ import annotations + +import logging +import platform + +from whichllm.hardware.amd import detect_amd_gpus +from whichllm.hardware.apple import detect_apple_gpu, detect_apple_gpu_linux +from whichllm.hardware.cpu import detect_avx_support, detect_cpu_cores, detect_cpu_name +from whichllm.hardware.intel import detect_intel_gpus +from whichllm.hardware.memory import detect_disk_free_bytes, detect_ram_bytes +from whichllm.hardware.nvidia import detect_nvidia_gpus +from whichllm.hardware.types import HardwareInfo +from whichllm.hardware.windows import detect_windows_gpus + +logger = logging.getLogger(__name__) + + +def detect_hardware() -> HardwareInfo: + """Detect all hardware. Each detector is fail-safe (returns empty on error).""" + os_name = platform.system().lower() + if os_name not in ("linux", "darwin", "windows"): + os_name = "linux" + + # GPU detection + gpus = [] + gpus.extend(detect_nvidia_gpus()) + if os_name == "linux": + gpus.extend(detect_amd_gpus()) + gpus.extend(detect_intel_gpus()) + gpus.extend(detect_apple_gpu_linux()) + if os_name == "darwin": + gpus.extend(detect_apple_gpu()) + if os_name == "windows": + gpus.extend(detect_windows_gpus()) + + # CPU + cpu_name = detect_cpu_name() + cpu_cores = detect_cpu_cores() + has_avx2, has_avx512 = detect_avx_support() + + # Memory + ram_bytes = detect_ram_bytes() + disk_free = detect_disk_free_bytes() + + return HardwareInfo( + gpus=gpus, + cpu_name=cpu_name, + cpu_cores=cpu_cores, + has_avx2=has_avx2, + has_avx512=has_avx512, + ram_bytes=ram_bytes, + disk_free_bytes=disk_free, + os=os_name, + ) diff --git a/src/whichllm/hardware/gpu_db.py b/src/whichllm/hardware/gpu_db.py new file mode 100644 index 0000000..e679615 --- /dev/null +++ b/src/whichllm/hardware/gpu_db.py @@ -0,0 +1,219 @@ +"""Resolve GPU memory bandwidth for *detected* hardware. + +Detection passes the raw driver name (e.g. ``"NVIDIA GeForce RTX 5090 Laptop +GPU"``). Unlike ``--gpu`` simulation, where the user typed the name and a fuzzy +guess plus a ``(simulated)`` label is acceptable, a wrong match on real +hardware is worse than no data: giving a laptop card its desktop bandwidth +produces confidently wrong speed estimates and oversized recommendations +(issues #74, #61, #93). + +So this resolver is deliberately strict: + +* The hand-curated ``GPU_BANDWIDTH`` table stays authoritative and is tried + first, so existing behaviour for known cards is unchanged. +* The curated lookup is mobile-aware: a laptop/Max-Q driver name will not match + a desktop key (``"RTX 5090"`` no longer swallows ``"RTX 5090 Laptop GPU"``). +* dbgpu is only consulted to fill gaps, and only via an exact normalised-name + hit or a name plus a VRAM-size suffix (``"RTX 4060 Ti 16 GB"``). It never + falls back to fuzzy matching, so a variant qualifier (Ti / SUPER / Mobile / + Max-Q / XT) can never be silently dropped onto the wrong card. +* ``"Laptop GPU"`` in a driver name is normalised to dbgpu's ``"Mobile"``. + +The change is purely additive: cards already covered by ``GPU_BANDWIDTH`` keep +their exact value, and cards that previously resolved to ``None`` now get a +correct bandwidth whenever dbgpu can identify them safely. +""" + +from __future__ import annotations + +import functools +import logging +import re + +from whichllm.constants import GPU_BANDWIDTH, GPU_MEMORY_CLOCK_VARIANTS, _GiB + +logger = logging.getLogger(__name__) + +_TRADEMARK_RE = re.compile(r"\((?:tm|r)\)", re.IGNORECASE) +_VENDOR_WORD_RE = re.compile(r"\b(?:nvidia|amd|ati|intel|corporation)\b", re.IGNORECASE) +_LAPTOP_GPU_RE = re.compile(r"\blaptop gpu\b", re.IGNORECASE) +_TRAILING_GRAPHICS_RE = re.compile(r"\bgraphics\s*$", re.IGNORECASE) +_WHITESPACE_RE = re.compile(r"\s+") +_MOBILE_MARKER_RE = re.compile(r"\b(?:laptop|mobile|max-?q)\b", re.IGNORECASE) +# Driver names write VRAM bins without a space ("RTX A2000 12GB"); dbgpu +# writes "RTX A2000 12 GB" (#98). +_VRAM_NOSPACE_RE = re.compile(r"\b(\d+)GB\b", re.IGNORECASE) +_BRACKET_RE = re.compile(r"\[(.+)]") +# A dbgpu name may extend a matched query with a VRAM-size bin only +# ("RTX 4060 Ti 16 GB"). A variant word (Mobile, Ti, D, ...) is never benign. +_VRAM_SUFFIX_RE = re.compile(r"^\s+\d+\s*gb\b", re.IGNORECASE) +_VRAM_GB_RE = re.compile(r"(\d+)\s*gb", re.IGNORECASE) + + +def _normalize_detected_name(name: str) -> str: + """Reduce a raw driver name toward dbgpu's naming convention.""" + text = _TRADEMARK_RE.sub("", name) + text = _VENDOR_WORD_RE.sub("", text) + text = _LAPTOP_GPU_RE.sub("Mobile", text) + text = _TRAILING_GRAPHICS_RE.sub("", text) + text = _VRAM_NOSPACE_RE.sub(r"\1 GB", text) + return _WHITESPACE_RE.sub(" ", text).strip() + + +_SORTED_BW_KEYS = sorted(GPU_BANDWIDTH, key=len, reverse=True) + + +def _substring_bandwidth(name: str) -> float | None: + """Curated ``GPU_BANDWIDTH`` lookup (longest key first), mobile-aware. + + When the detected name is a laptop/Max-Q card, a desktop key is not allowed + to match it via substring, since the two have very different bandwidth. + """ + if not name: + return None + name_upper = name.upper() + name_is_mobile = bool(_MOBILE_MARKER_RE.search(name)) + for key in _SORTED_BW_KEYS: + if key.upper() in name_upper: + if name_is_mobile and not _MOBILE_MARKER_RE.search(key): + continue + return GPU_BANDWIDTH[key] + return None + + +def _static_bandwidth(name: str) -> float | None: + """Curated lookup that also handles compound lspci names. + + Compound names like ``"Navi 22 [Radeon RX 6700/6700 XT/6750 XT / + 6800M/6850M XT]"`` list several variants; the first segment that resolves + wins, with a ``"RX "`` prefix retry for bare segments like ``"6750 XT"``. + dbgpu is never consulted for these: the name does not identify a single + card, so the curated value for the listed family is the safest answer. + """ + if not name: + return None + if "/" not in name: + bandwidth = _substring_bandwidth(name) + if bandwidth is not None: + return bandwidth + normalized = _normalize_detected_name(name) + return _substring_bandwidth(normalized) if normalized != name else None + bracket = _BRACKET_RE.search(name) + raw = bracket.group(1) if bracket else name + for seg in raw.split("/"): + seg = seg.strip() + if not seg: + continue + bandwidth = _substring_bandwidth(seg) or _substring_bandwidth(f"RX {seg}") + if bandwidth is not None: + return bandwidth + return None + + +def _vram_gb(canonical_name: str) -> int | None: + match = _VRAM_GB_RE.search(canonical_name) + return int(match.group(1)) if match else None + + +@functools.lru_cache(maxsize=1) +def _dbgpu_index() -> tuple[object | None, dict[str, str] | None]: + """Build ``{normalized_name: canonical_dbgpu_name}``. + + Returns ``(db, index)`` or ``(None, None)`` if dbgpu is unavailable, so the + resolver degrades to static-only instead of raising. + """ + try: + from dbgpu import GPUDatabase + + db = GPUDatabase.default() + except Exception as exc: # pragma: no cover - dbgpu is a hard dependency + logger.debug("dbgpu unavailable, using static bandwidth only: %s", exc) + return None, None + index: dict[str, str] = {} + for canonical in db.names: + index.setdefault(_normalize_detected_name(canonical).lower(), canonical) + return db, index + + +def _dbgpu_bandwidth(name: str, vram_bytes: int | None) -> float | None: + """Strict dbgpu bandwidth lookup. Never fuzzy-matches a variant away.""" + db, index = _dbgpu_index() + if db is None or index is None: + return None + query = _normalize_detected_name(name).lower() + if not query: + return None + + if query in index: + candidates = [index[query]] + else: + candidates = [ + original + for normalized, original in index.items() + if normalized.startswith(query + " ") + and _VRAM_SUFFIX_RE.match(normalized[len(query) :]) + ] + if not candidates: + return None + if vram_bytes and len(candidates) > 1: + target_gb = round(vram_bytes / _GiB) + same_vram = [c for c in candidates if _vram_gb(c) == target_gb] + if same_vram: + candidates = same_vram + + # If several VRAM bins remain (VRAM unknown or no bin matched it), take the + # lowest bandwidth among them: an ambiguous match must never over-promise + # speed. Bins of the same card usually share one value anyway. + bandwidths: list[float] = [] + for canonical in candidates: + try: + spec = db[canonical] + except KeyError: # pragma: no cover - canonical comes from the index + continue + bandwidth = getattr(spec, "memory_bandwidth_gb_s", None) + if bandwidth: + bandwidths.append(float(bandwidth)) + return min(bandwidths) if bandwidths else None + + +def _memory_clock_variant_bandwidth( + name: str, mem_clock_mhz: float | None +) -> float | None: + """Disambiguate cards sold in multiple memory types by max memory clock. + + Some GPUs (e.g. GTX 1650 GDDR5 vs GDDR6) share a marketing name and PCI + device id, so only the memory clock tells them apart. Returns the matching + variant bandwidth when the name is a known dual-memory card and the clock is + known, else ``None`` (so the caller falls back to the curated default). + """ + if not name or not mem_clock_mhz or mem_clock_mhz <= 0: + return None + name_upper = name.upper() + for key in sorted(GPU_MEMORY_CLOCK_VARIANTS, key=len, reverse=True): + if key.upper() in name_upper: + for min_clock, bandwidth in GPU_MEMORY_CLOCK_VARIANTS[key]: + if mem_clock_mhz >= min_clock: + return bandwidth + return None + + +def resolve_detected_bandwidth( + name: str, + vram_bytes: int | None = None, + mem_clock_mhz: float | None = None, +) -> float | None: + """Best memory bandwidth (GB/s) for a detected GPU, or ``None`` if unknown. + + Memory-clock variant disambiguation wins first (for cards that share a name + across memory types, e.g. GTX 1650 GDDR5/GDDR6); then curated + ``GPU_BANDWIDTH``; then dbgpu fills the gaps. ``vram_bytes`` (when known) + disambiguates same-name cards that ship in multiple VRAM bins; + ``mem_clock_mhz`` (max memory clock, when known) disambiguates memory-type + variants. With both unset, behaviour is identical to before. + """ + if not name: + return None + variant = _memory_clock_variant_bandwidth(name, mem_clock_mhz) + if variant is not None: + return variant + return _static_bandwidth(name) or _dbgpu_bandwidth(name, vram_bytes) diff --git a/src/whichllm/hardware/gpu_simulator.py b/src/whichllm/hardware/gpu_simulator.py new file mode 100644 index 0000000..e45ccde --- /dev/null +++ b/src/whichllm/hardware/gpu_simulator.py @@ -0,0 +1,346 @@ +"""Create synthetic GPUInfo for GPU simulation (--gpu flag). + +Uses the dbgpu package (2000+ GPU database from TechPowerUp) for dynamic +lookup of VRAM, memory bandwidth, and compute capability. +""" + +from __future__ import annotations + +import logging +import re +from collections.abc import Sequence +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from dbgpu import GPUSpecification + +from whichllm.constants import ( + AMD_SHARED_MEMORY_APU_MARKERS, + CURATED_GPU_SPECS, + GPU_BANDWIDTH, + CuratedGPUSpec, + _GiB, +) +from whichllm.hardware.types import GPUInfo + +logger = logging.getLogger(__name__) + +_MANUFACTURER_TO_VENDOR: dict[str, str] = { + "NVIDIA": "nvidia", + "AMD": "amd", + "ATI": "amd", + "Intel": "intel", + "Apple": "apple", +} + +_MANUFACTURER_PREFIXES = ["GeForce ", "Radeon ", "Arc ", "NVIDIA ", "AMD "] +_COMMON_GPU_ALIASES: dict[str, list[str]] = { + "a10080gb": [ + "NVIDIA A100 PCIe 80 GB", + "NVIDIA A100 SXM4 80 GB", + ], + "h10080gb": [ + "NVIDIA H100 PCIe 80 GB", + "NVIDIA H100 SXM5 80 GB", + ], +} + +# Apple Silicon chips. dbgpu does not include these (it tracks discrete GPUs +# via TechPowerUp), but users routinely simulate with --gpu "M2 Max" etc. +# Without this short-circuit, "M1" fuzzy-matches the 1997 ATI Rage Mobility-M1 +# and "M3 Max" falls through to vendor=nvidia default. +# Format: chip_name -> (canonical_name, default_unified_memory_gb). +# --vram override is still respected; the default is the most common SKU. +_APPLE_SILICON_CHIPS: dict[str, tuple[str, float]] = { + "M1": ("Apple M1", 8.0), + "M1 Pro": ("Apple M1 Pro", 16.0), + "M1 Max": ("Apple M1 Max", 32.0), + "M1 Ultra": ("Apple M1 Ultra", 64.0), + "M2": ("Apple M2", 16.0), + "M2 Pro": ("Apple M2 Pro", 16.0), + "M2 Max": ("Apple M2 Max", 32.0), + "M2 Ultra": ("Apple M2 Ultra", 64.0), + "M3": ("Apple M3", 16.0), + "M3 Pro": ("Apple M3 Pro", 18.0), + "M3 Max": ("Apple M3 Max", 36.0), + "M3 Ultra": ("Apple M3 Ultra", 96.0), + "M4": ("Apple M4", 16.0), + "M4 Pro": ("Apple M4 Pro", 24.0), + "M4 Max": ("Apple M4 Max", 36.0), + "M4 Ultra": ("Apple M4 Ultra", 64.0), + "M5": ("Apple M5", 16.0), + "M5 Pro": ("Apple M5 Pro", 24.0), + "M5 Max": ("Apple M5 Max", 36.0), +} + + +def _lookup_apple_silicon( + name: str, +) -> tuple[str, str, float, float] | None: + """Match Apple Silicon chip names. Returns (canonical_name, vendor, + default_vram_gb, bandwidth_gbps) or None. + + Matches are case-insensitive and accept "M2 Max", "m2max", and + display-name forms such as "Apple M2 Max". Longest match wins so + "M2 Ultra" does not get caught by the "M2" entry. + """ + compact = re.sub(r"\s+", "", name).lower() + if compact.startswith("apple"): + compact = compact.removeprefix("apple") + + # Sort keys by length descending so "M2 Ultra" wins over "M2". + for key in sorted(_APPLE_SILICON_CHIPS, key=len, reverse=True): + key_compact = re.sub(r"\s+", "", key).lower() + if compact == key_compact: + canonical, default_vram = _APPLE_SILICON_CHIPS[key] + bandwidth = GPU_BANDWIDTH.get(key, 100.0) + return canonical, "apple", default_vram, bandwidth + return None + + +def _is_amd_shared_memory_apu(name: str) -> bool: + name_upper = name.upper() + return any(marker in name_upper for marker in AMD_SHARED_MEMORY_APU_MARKERS) + + +def _lookup_static_bandwidth(name: str) -> float | None: + name_upper = name.upper() + for key in sorted(GPU_BANDWIDTH, key=len, reverse=True): + if key.upper() in name_upper: + return GPU_BANDWIDTH[key] + return None + + +def _lookup_curated_spec(name: str) -> CuratedGPUSpec | None: + name_upper = name.upper() + for key in sorted(CURATED_GPU_SPECS, key=len, reverse=True): + if key.upper() in name_upper: + return CURATED_GPU_SPECS[key] + return None + + +def _normalize_gpu_name(name: str) -> str: + """Normalize user input: 'GTX1080' → 'GTX 1080', 'RX7900XTX' → 'RX 7900 XTX'.""" + # Insert space between letters and digits + name = re.sub(r"([A-Za-z])(\d)", r"\1 \2", name) + # Insert space between digits and letters + name = re.sub(r"(\d)([A-Za-z])", r"\1 \2", name) + # Collapse multiple spaces + return re.sub(r"\s+", " ", name).strip() + + +def _substring_search(db, name: str): + """Substring match with word-boundary filtering. + + e.g. "RTX 3060" should match "GeForce RTX 3060 12 GB" but NOT "GeForce RTX 3060 Ti". + """ + name_upper = name.upper() + candidates = [] + for db_name in db.names: + idx = db_name.upper().find(name_upper) + if idx < 0: + continue + after = db_name[idx + len(name) :] + # Accept if nothing follows, or what follows is VRAM/form-factor spec + # Reject if a variant suffix follows (Ti, SUPER, Mobile, Max-Q, etc.) + if not after or re.match(r"^(\s+(\d|GA\d|PCIe|SXM|NVL|CNX))", after): + candidates.append(db_name) + if candidates: + candidates.sort(key=len) + return db[candidates[0]] + return None + + +def _lookup_dbgpu(name: str) -> GPUSpecification | None: + """Look up GPU spec from dbgpu database. Returns GPUSpecification or None.""" + from dbgpu import GPUDatabase + + db = GPUDatabase.default() + + # Normalize input: "GTX1080" → "GTX 1080" + normalized = _normalize_gpu_name(name) + compact = re.sub(r"\s+", "", normalized.lower()) + names_to_try = [name] if normalized == name else [name, normalized] + alias_hits = _COMMON_GPU_ALIASES.get(compact) + if alias_hits: + names_to_try.extend(alias_hits) + + for n in names_to_try: + # 1) Exact key lookup + try: + return db[n] + except KeyError: + pass + + # 2) Try with common manufacturer prefixes + for prefix in _MANUFACTURER_PREFIXES: + try: + return db[prefix + n] + except KeyError: + pass + + # 3) Substring match with word-boundary filtering + result = _substring_search(db, n) + if result is not None: + return result + + # 4) Fuzzy search as last resort (use normalized name + token_set_ratio) + try: + from thefuzz import fuzz, process + + results = process.extract( + normalized, db.names, limit=3, scorer=fuzz.token_set_ratio + ) + if results and results[0][1] >= 90: + return db[results[0][0]] + # Store top suggestions for error messages + if results: + _last_suggestions[:] = [(n, s) for n, s in results if s >= 70] + except ImportError: + pass + return None + + +# Mutable list to pass suggestions from lookup to error message +_last_suggestions: list[tuple[str, int]] = [] + + +def parse_synthetic_gpu_specs(values: Sequence[str] | str) -> list[str]: + """Expand CLI GPU simulation values into individual GPU names. + + Accepts repeated options, comma-separated names, and count shorthand such + as ``2x RTX 4090``. The returned names are still looked up by + ``create_synthetic_gpu`` so existing fuzzy matching and aliases stay in + one place. + """ + raw_values = [values] if isinstance(values, str) else list(values) + gpu_names: list[str] = [] + + for raw in raw_values: + for part in raw.split(","): + spec = part.strip() + if not spec: + raise ValueError("Empty GPU entry in --gpu.") + + count_match = re.match(r"^(\d+)\s*x\s+(.+)$", spec, re.IGNORECASE) + if count_match: + count = int(count_match.group(1)) + name = count_match.group(2).strip() + if count < 1: + raise ValueError("GPU count must be at least 1.") + if not name: + raise ValueError("GPU count shorthand requires a GPU name.") + gpu_names.extend([name] * count) + else: + gpu_names.append(spec) + + if not gpu_names: + raise ValueError("At least one GPU must be specified.") + return gpu_names + + +def create_synthetic_gpus( + values: Sequence[str] | str, + vram_override_gb: float | None = None, +) -> list[GPUInfo]: + """Create one or more synthetic GPUs from CLI-style values.""" + names = parse_synthetic_gpu_specs(values) + if vram_override_gb is not None and len(names) != 1: + raise ValueError( + "--vram currently supports exactly one simulated GPU. " + "For multi-GPU simulation, specify known GPU names and omit --vram." + ) + return [create_synthetic_gpu(name, vram_override_gb) for name in names] + + +def create_synthetic_gpu(name: str, vram_override_gb: float | None = None) -> GPUInfo: + """Create a synthetic GPUInfo from a GPU name. + + Looks up specs from the dbgpu database (2000+ GPUs). + + Args: + name: GPU name (e.g. "RTX 4090", "RX 7900 XTX"). + vram_override_gb: Override VRAM in GB. Required if GPU not in database. + + Returns: + GPUInfo with ``(simulated)`` suffix in the name. + + Raises: + ValueError: If GPU is not found and no vram_override_gb given. + """ + _last_suggestions.clear() + + amd_shared_memory_apu = _is_amd_shared_memory_apu(name) + curated = _lookup_curated_spec(name) + + # Apple Silicon short-circuit: dbgpu has no Apple entries, so we check + # first to avoid fuzzy-matching "M1" against "Rage Mobility-M1". + apple_hit = _lookup_apple_silicon(name) + if apple_hit is not None: + canonical, vendor, default_vram_gb, bandwidth = apple_hit + vram_gb = vram_override_gb if vram_override_gb is not None else default_vram_gb + return GPUInfo( + name=f"{canonical} (simulated)", + vendor=vendor, + vram_bytes=int(vram_gb * _GiB), + memory_bandwidth_gbps=bandwidth, + shared_memory=True, + vram_overridden=vram_override_gb is not None, + ) + + spec = _lookup_dbgpu(name) + + # VRAM + if vram_override_gb is not None: + vram_bytes = int(vram_override_gb * _GiB) + elif spec is not None and spec.memory_size_gb: + vram_bytes = int(spec.memory_size_gb * _GiB) + elif curated is not None: + vram_bytes = int(curated.vram_gb * _GiB) + else: + msg = f"Unknown GPU '{name}'." + if _last_suggestions: + candidates = ", ".join(n for n, _ in _last_suggestions) + msg += f" Did you mean: {candidates}?" + msg += " Use --vram to specify VRAM in GB." + raise ValueError(msg) + + # Bandwidth + bandwidth: float | None = None + if spec is not None and spec.memory_bandwidth_gb_s: + bandwidth = spec.memory_bandwidth_gb_s + if bandwidth is None and curated is not None: + bandwidth = curated.memory_bandwidth_gbps + if bandwidth is None: + bandwidth = _lookup_static_bandwidth(name) + + # Compute capability (CUDA version in dbgpu = compute capability) + compute_cap: tuple[int, int] | None = None + if spec is not None and spec.cuda_major_version is not None: + compute_cap = (spec.cuda_major_version, spec.cuda_minor_version or 0) + + # Vendor + vendor = "nvidia" + if spec is not None: + vendor = _MANUFACTURER_TO_VENDOR.get(spec.manufacturer, "nvidia") + elif curated is not None: + vendor = curated.vendor + elif amd_shared_memory_apu: + vendor = "amd" + + if spec is not None: + display_name = spec.name + elif curated is not None: + display_name = curated.name + else: + display_name = name + + return GPUInfo( + name=f"{display_name} (simulated)", + vendor=vendor, + vram_bytes=vram_bytes, + compute_capability=compute_cap, + memory_bandwidth_gbps=bandwidth, + shared_memory=curated.shared_memory if curated else amd_shared_memory_apu, + vram_overridden=vram_override_gb is not None, + ) diff --git a/src/whichllm/hardware/intel.py b/src/whichllm/hardware/intel.py new file mode 100644 index 0000000..127a496 --- /dev/null +++ b/src/whichllm/hardware/intel.py @@ -0,0 +1,136 @@ +"""Intel integrated GPU detection on Linux.""" + +from __future__ import annotations + +import logging +import subprocess +from pathlib import Path + +from whichllm.constants import ( + CURATED_GPU_SPECS, + INTEL_PCI_DEVICE_NAMES, + CuratedGPUSpec, + _GiB, +) +from whichllm.hardware.types import GPUInfo + +logger = logging.getLogger(__name__) + + +_DISPLAY_CLASSES = ( + "vga compatible controller", + "3d controller", + "display controller", +) + + +def _normalize_lspci_name(line: str) -> str: + parts = [p.strip() for p in line.split('"') if p.strip() and p.strip() != "\t"] + for i, part in enumerate(parts): + if part.lower() == "intel corporation" and i + 1 < len(parts): + return parts[i + 1] + return "Intel Integrated Graphics" + + +def _detect_from_lspci() -> list[str]: + try: + result = subprocess.run( + ["lspci", "-mm"], + capture_output=True, + text=True, + timeout=5, + ) + except (FileNotFoundError, subprocess.TimeoutExpired): + logger.debug("lspci not available or timed out") + return [] + + if result.returncode != 0: + return [] + + names: list[str] = [] + seen: set[str] = set() + for line in result.stdout.splitlines(): + line_lower = line.lower() + if "intel" not in line_lower or not any( + display_class in line_lower for display_class in _DISPLAY_CLASSES + ): + continue + name = _normalize_lspci_name(line) + if name not in seen: + names.append(name) + seen.add(name) + return names + + +def _detect_from_sysfs(drm_path: Path = Path("/sys/class/drm")) -> list[str]: + names: list[str] = [] + seen: set[str] = set() + try: + cards = sorted(drm_path.glob("card[0-9]*")) + except OSError: + return [] + + for card in cards: + device = card / "device" + try: + vendor = (device / "vendor").read_text().strip().lower() + except OSError: + continue + if vendor != "0x8086": + continue + + name = "Intel Integrated Graphics" + known_device = False + try: + device_id = (device / "device").read_text().strip().lower() + mapped_name = INTEL_PCI_DEVICE_NAMES.get(device_id) + if mapped_name: + name = mapped_name + known_device = True + except OSError: + pass + + try: + product_name = (device / "product_name").read_text().strip() + if product_name and not known_device: + name = product_name + except OSError: + pass + + if name not in seen: + names.append(name) + seen.add(name) + return names + + +def _lookup_curated_spec(name: str) -> CuratedGPUSpec | None: + name_upper = name.upper() + for key in sorted(CURATED_GPU_SPECS, key=len, reverse=True): + if key.upper() in name_upper: + return CURATED_GPU_SPECS[key] + return None + + +def _gpu_info_from_name(name: str) -> GPUInfo: + curated = _lookup_curated_spec(name) + if curated is not None: + return GPUInfo( + name=name, + vendor=curated.vendor, + vram_bytes=int(curated.vram_gb * _GiB), + memory_bandwidth_gbps=curated.memory_bandwidth_gbps, + shared_memory=curated.shared_memory, + ) + return GPUInfo( + name=name, + vendor="intel", + vram_bytes=0, + shared_memory=True, + ) + + +def detect_intel_gpus() -> list[GPUInfo]: + """Detect Linux Intel iGPUs. Returns empty list on failure.""" + names = _detect_from_lspci() or _detect_from_sysfs() + + return [_gpu_info_from_name(name) for name in names] diff --git a/src/whichllm/hardware/memory.py b/src/whichllm/hardware/memory.py new file mode 100644 index 0000000..9f16ba9 --- /dev/null +++ b/src/whichllm/hardware/memory.py @@ -0,0 +1,52 @@ +"""RAM and disk space detection.""" + +from __future__ import annotations + +import os +import shutil + +import psutil + + +def detect_ram_bytes() -> int: + """Get total physical RAM in bytes.""" + return psutil.virtual_memory().total + + +def detect_available_ram_bytes() -> int: + """Get currently available RAM bytes.""" + return psutil.virtual_memory().available + + +def estimate_usable_ram(total: int) -> int: + """Estimate RAM available for model loading after OS/background reserve. + + Uses a bounded-reserve formula: total - clamp(total * 0.15, 4 GiB, 32 GiB). + """ + _GiB = 1024**3 + reserve = int(total * 0.15) + reserve = max(4 * _GiB, min(reserve, 32 * _GiB)) + return max(0, total - reserve) + + +def effective_usable_ram(total: int, budget: int | None = None) -> int: + """Estimate usable RAM, optionally capped by a user/runtime budget.""" + usable = estimate_usable_ram(total) + if budget is None: + return usable + return max(0, min(usable, budget)) + + +def detect_disk_free_bytes(path: str | None = None) -> int: + """Get free disk space in bytes at the given path. + + Defaults to the user's home directory, which is more accurate + on macOS where / may be a read-only system volume. + """ + if path is None: + path = os.path.expanduser("~") + try: + usage = shutil.disk_usage(path) + return usage.free + except OSError: + return 0 diff --git a/src/whichllm/hardware/nvidia.py b/src/whichllm/hardware/nvidia.py new file mode 100644 index 0000000..785c131 --- /dev/null +++ b/src/whichllm/hardware/nvidia.py @@ -0,0 +1,198 @@ +"""NVIDIA GPU detection via NVML with nvidia-smi fallback.""" + +from __future__ import annotations + +import logging +import re +import subprocess + +from whichllm.constants import NVIDIA_COMPUTE_CAPABILITY, _GiB +from whichllm.hardware.gpu_db import _static_bandwidth, resolve_detected_bandwidth +from whichllm.hardware.types import GPUInfo + +logger = logging.getLogger(__name__) + +_NVIDIA_UNIFIED_MEMORY_MARKERS = ("GB10", "DGX SPARK") + + +def _lookup_compute_capability(name: str) -> tuple[int, int] | None: + name_upper = name.upper() + for key, cc in NVIDIA_COMPUTE_CAPABILITY.items(): + if key.upper() in name_upper: + return cc + return None + + +def _lookup_bandwidth(name: str) -> float | None: + """Curated GPU_BANDWIDTH lookup. Kept for regression tests; live detection + goes through ``resolve_detected_bandwidth``, which also consults dbgpu.""" + return _static_bandwidth(name) + + +def _is_unified_memory_nvidia_gpu(name: str) -> bool: + name_upper = name.upper() + return any(marker in name_upper for marker in _NVIDIA_UNIFIED_MEMORY_MARKERS) + + +def _system_memory_bytes() -> int: + from whichllm.hardware.memory import detect_ram_bytes + + ram_bytes = detect_ram_bytes() + if ram_bytes > 0: + return ram_bytes + return 128 * _GiB + + +def _make_nvidia_gpu( + name: str, + vram_bytes: int | None, + cuda_version: str | None = None, + mem_clock_mhz: float | None = None, +) -> GPUInfo: + shared_memory = _is_unified_memory_nvidia_gpu(name) + if shared_memory and (vram_bytes is None or vram_bytes <= 0): + vram_bytes = _system_memory_bytes() + elif vram_bytes is None: + vram_bytes = 0 + + return GPUInfo( + name=name, + vendor="nvidia", + vram_bytes=vram_bytes, + compute_capability=_lookup_compute_capability(name), + cuda_version=cuda_version, + memory_bandwidth_gbps=resolve_detected_bandwidth( + name, vram_bytes, mem_clock_mhz + ), + shared_memory=shared_memory, + ) + + +def _run_smi_query(fields: str) -> str: + result = subprocess.run( + ["nvidia-smi", f"--query-gpu={fields}", "--format=csv,noheader,nounits"], + capture_output=True, + check=True, + text=True, + timeout=5, + ) + return result.stdout + + +def _detect_nvidia_gpus_via_smi() -> list[GPUInfo]: + """Detect NVIDIA GPUs using nvidia-smi when Python NVML cannot load. + + The max memory clock (used to disambiguate same-name memory variants such as + GTX 1650 GDDR5/GDDR6) is queried opportunistically. If the 3-field query + fails on a driver/card that rejects ``clocks.max.memory``, we retry the + original 2-field query so a missing optional field never wipes out detection. + """ + try: + stdout = _run_smi_query("name,memory.total,clocks.max.memory") + except (FileNotFoundError, subprocess.SubprocessError, OSError) as e: + logger.debug(f"nvidia-smi 3-field query failed ({e}); retrying without clock") + try: + stdout = _run_smi_query("name,memory.total") + except (FileNotFoundError, subprocess.SubprocessError, OSError) as e2: + logger.debug(f"nvidia-smi fallback failed: {e2}") + return [] + + gpus: list[GPUInfo] = [] + for line in stdout.splitlines(): + parts = [part.strip() for part in line.split(",", maxsplit=2)] + if len(parts) < 2 or not parts[0]: + continue + + name, memory_mib_text = parts[0], parts[1] + # clocks.max.memory is MHz (or "[N/A]" on some cards/drivers). + mem_clock_mhz: float | None = None + if len(parts) == 3: + clock_match = re.search(r"\d+", parts[2]) + if clock_match: + mem_clock_mhz = float(clock_match.group(0)) + + match = re.search(r"\d+", memory_mib_text) + if not match: + if not _is_unified_memory_nvidia_gpu(name): + logger.debug(f"Could not parse nvidia-smi memory value: {line!r}") + continue + gpus.append(_make_nvidia_gpu(name, None, mem_clock_mhz=mem_clock_mhz)) + continue + + memory_mib = int(match.group(0)) + gpus.append( + _make_nvidia_gpu(name, memory_mib * 1024**2, mem_clock_mhz=mem_clock_mhz) + ) + + return gpus + + +def detect_nvidia_gpus() -> list[GPUInfo]: + """Detect NVIDIA GPUs. Returns empty list on failure.""" + try: + import pynvml + except ImportError: + logger.debug("pynvml not installed, trying nvidia-smi fallback") + return _detect_nvidia_gpus_via_smi() + + try: + pynvml.nvmlInit() + except pynvml.NVMLError: + logger.debug("NVML init failed, trying nvidia-smi fallback") + return _detect_nvidia_gpus_via_smi() + + gpus: list[GPUInfo] = [] + try: + count = pynvml.nvmlDeviceGetCount() + # Get CUDA driver version + try: + pynvml.nvmlSystemGetDriverVersion() # ensure driver is accessible + cuda_version = pynvml.nvmlSystemGetCudaDriverVersion_v2() + cuda_str = f"{cuda_version // 1000}.{(cuda_version % 1000) // 10}" + except Exception: + cuda_str = None + + for i in range(count): + handle = pynvml.nvmlDeviceGetHandleByIndex(i) + name = pynvml.nvmlDeviceGetName(handle) + if isinstance(name, bytes): + name = name.decode("utf-8") + + try: + mem_info = pynvml.nvmlDeviceGetMemoryInfo(handle) + vram_bytes = mem_info.total + except pynvml.NVMLError: + if not _is_unified_memory_nvidia_gpu(name): + raise + logger.debug(f"NVML did not report dedicated memory for {name}") + vram_bytes = None + + # Max memory clock (MHz) disambiguates same-name memory variants + # (e.g. GTX 1650 GDDR5 vs GDDR6). Optional: not all drivers expose it. + try: + mem_clock_mhz: float | None = float( + pynvml.nvmlDeviceGetMaxClockInfo(handle, pynvml.NVML_CLOCK_MEM) + ) + except Exception as clock_err: + # Optional: without it, same-name memory variants fall back to the + # curated default. Log so an unexpected under-serve is diagnosable. + logger.debug( + f"max mem clock unavailable for {name} " + f"({clock_err}); bandwidth from name only" + ) + mem_clock_mhz = None + + gpus.append(_make_nvidia_gpu(name, vram_bytes, cuda_str, mem_clock_mhz)) + except pynvml.NVMLError as e: + logger.debug(f"Error enumerating NVIDIA GPUs: {e}") + finally: + try: + pynvml.nvmlShutdown() + except Exception: + pass + + if gpus: + return gpus + + logger.debug("NVML returned no NVIDIA GPUs, trying nvidia-smi fallback") + return _detect_nvidia_gpus_via_smi() diff --git a/src/whichllm/hardware/types.py b/src/whichllm/hardware/types.py new file mode 100644 index 0000000..cdca379 --- /dev/null +++ b/src/whichllm/hardware/types.py @@ -0,0 +1,31 @@ +from __future__ import annotations + +from dataclasses import dataclass, field + + +@dataclass +class GPUInfo: + name: str + vendor: str # "nvidia" | "amd" | "apple" | "intel" + vram_bytes: int + usable_vram_bytes: int | None = None + compute_capability: tuple[int, int] | None = None # NVIDIA only + cuda_version: str | None = None + rocm_version: str | None = None + memory_bandwidth_gbps: float | None = None # from lookup table + shared_memory: bool = False + vram_overridden: bool = False + + +@dataclass +class HardwareInfo: + gpus: list[GPUInfo] = field(default_factory=list) + cpu_name: str = "Unknown" + cpu_cores: int = 0 + has_avx2: bool = False + has_avx512: bool = False + ram_bytes: int = 0 + ram_budget_bytes: int | None = None + disk_free_bytes: int = 0 + os: str = "linux" # "linux" | "darwin" | "windows" + budget_notes: list[str] = field(default_factory=list) diff --git a/src/whichllm/hardware/windows.py b/src/whichllm/hardware/windows.py new file mode 100644 index 0000000..0149b01 --- /dev/null +++ b/src/whichllm/hardware/windows.py @@ -0,0 +1,182 @@ +"""Windows GPU detection via Win32_VideoController.""" + +from __future__ import annotations + +import json +import logging +import re +import subprocess + +from whichllm.constants import AMD_SHARED_MEMORY_APU_MARKERS, _GiB +from whichllm.hardware.gpu_db import resolve_detected_bandwidth +from whichllm.hardware.types import GPUInfo + +logger = logging.getLogger(__name__) + +_WINDOWS_DISCRETE_VRAM_FLOORS: tuple[tuple[str, int], ...] = ( + # AMD sells RX 9060 XT in 8 GB and 16 GB variants. WMI AdapterRAM is a + # uint32 and can report ~4 GB for larger cards, so keep a conservative + # known floor instead of trusting the capped value. + ("RX 9060 XT", 8 * _GiB), +) + + +def _vendor_from_name(name: str) -> str | None: + name_lower = name.lower() + if any(token in name_lower for token in ("amd", "radeon")): + return "amd" + if "intel" in name_lower: + return "intel" + return None + + +def _parse_memory_value(value: object) -> int: + if value is None: + return 0 + try: + ram = int(value) + except (TypeError, ValueError): + return 0 + return max(ram, 0) + + +def _is_amd_shared_memory_apu(name: str) -> bool: + name_upper = name.upper() + return any(marker in name_upper for marker in AMD_SHARED_MEMORY_APU_MARKERS) + + +def _is_intel_discrete_gpu(name: str) -> bool: + return ( + re.search( + r"\barc(?:\(tm\))?\s+(?:pro\s+)?[ab]\d{2,3}", + name, + re.IGNORECASE, + ) + is not None + ) + + +def _is_intel_shared_memory_gpu(name: str, vram_bytes: int) -> bool: + name_lower = name.lower() + if _is_intel_discrete_gpu(name): + return False + if any( + token in name_lower + for token in ( + "uhd", + "iris", + " xe", + "hd graphics", + "arc(tm) graphics", + "intel(r) graphics", + ) + ): + return True + return vram_bytes < 2 * _GiB + + +def _is_shared_memory_gpu(name: str, vendor: str, vram_bytes: int) -> bool: + if vendor == "amd": + return _is_amd_shared_memory_apu(name) + if vendor == "intel": + return _is_intel_shared_memory_gpu(name, vram_bytes) + return False + + +def _apply_discrete_vram_floor(name: str, vram_bytes: int) -> int: + name_upper = name.upper() + for marker, floor in _WINDOWS_DISCRETE_VRAM_FLOORS: + if marker in name_upper and 0 < vram_bytes < floor: + return floor + return vram_bytes + + +def _memory_from_entry(entry: dict) -> int: + dedicated = _parse_memory_value(entry.get("DedicatedVideoMemory")) + if dedicated > 0: + return dedicated + return _parse_memory_value(entry.get("AdapterRAM")) + + +def detect_windows_gpus() -> list[GPUInfo]: + """Detect non-NVIDIA Windows GPUs. Returns empty list on failure.""" + try: + result = subprocess.run( + [ + "powershell", + "-NoProfile", + "-Command", + ( + "$controllers = Get-CimInstance Win32_VideoController; " + "$controllers | ForEach-Object { " + "$dedicated = $null; " + "if ($_.PNPDeviceID) { " + "try { " + "$enumPath = 'Registry::HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Enum\\' " + "+ $_.PNPDeviceID + '\\Device Parameters'; " + "$enumProps = Get-ItemProperty -LiteralPath $enumPath -ErrorAction Stop; " + "if ($enumProps.VideoID) { " + "$videoPath = 'Registry::HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\Video\\' " + "+ $enumProps.VideoID + '\\0000'; " + "$videoProps = Get-ItemProperty -LiteralPath $videoPath -ErrorAction Stop; " + "$dedicated = $videoProps.'HardwareInformation.qwMemorySize'; " + "} " + "} catch {} " + "} " + "[PSCustomObject]@{" + "Name=$_.Name; " + "AdapterRAM=$_.AdapterRAM; " + "DedicatedVideoMemory=$dedicated" + "} " + "} | ConvertTo-Json -Depth 3" + ), + ], + capture_output=True, + text=True, + timeout=10, + ) + except (FileNotFoundError, subprocess.SubprocessError, OSError) as e: + logger.debug(f"Windows GPU detection failed: {e}") + return [] + + if result.returncode != 0 or not result.stdout.strip(): + return [] + + try: + data = json.loads(result.stdout) + except json.JSONDecodeError: + logger.debug("Failed to parse Windows GPU JSON") + return [] + + entries = data if isinstance(data, list) else [data] + gpus: list[GPUInfo] = [] + seen: set[str] = set() + for entry in entries: + if not isinstance(entry, dict): + continue + name = str(entry.get("Name") or "").strip() + if not name: + continue + vendor = _vendor_from_name(name) + if vendor is None: + continue + vram_bytes = _memory_from_entry(entry) + shared_memory = _is_shared_memory_gpu(name, vendor, vram_bytes) + if shared_memory: + vram_bytes = 0 + else: + vram_bytes = _apply_discrete_vram_floor(name, vram_bytes) + key = f"{vendor}:{name}:{vram_bytes}" + if key in seen: + continue + seen.add(key) + gpus.append( + GPUInfo( + name=name, + vendor=vendor, + vram_bytes=vram_bytes, + memory_bandwidth_gbps=resolve_detected_bandwidth(name, vram_bytes), + shared_memory=shared_memory, + ) + ) + return gpus diff --git a/src/whichllm/models/__init__.py b/src/whichllm/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/whichllm/models/artifacts.py b/src/whichllm/models/artifacts.py new file mode 100644 index 0000000..d7687bf --- /dev/null +++ b/src/whichllm/models/artifacts.py @@ -0,0 +1,105 @@ +"""Resolve runnable/downloadable model artifacts for ranked recommendations.""" + +from __future__ import annotations + +from whichllm.engine.types import CompatibilityResult +from whichllm.models.types import GGUFVariant, ModelInfo + + +def find_gguf_variant(model: ModelInfo, quant_type: str) -> GGUFVariant | None: + """Return the model's GGUF variant for a quantization type.""" + for variant in model.gguf_variants: + if variant.quant_type.upper() == quant_type.upper(): + return variant + return None + + +def is_same_model_family(candidate: ModelInfo, selected: ModelInfo) -> bool: + """Return whether two repos represent the same base model family.""" + if candidate.id == selected.id: + return True + if candidate.family_id and selected.family_id: + if candidate.family_id == selected.family_id: + return True + if candidate.base_model and candidate.base_model == selected.id: + return True + if selected.base_model and selected.base_model == candidate.id: + return True + if candidate.base_model and selected.base_model: + return candidate.base_model == selected.base_model + return False + + +def has_compatible_parameter_count(candidate: ModelInfo, selected: ModelInfo) -> bool: + """Reject artifact repos that are clearly a different model size.""" + if candidate.parameter_count <= 0 or selected.parameter_count <= 0: + return True + smaller = min(candidate.parameter_count, selected.parameter_count) + larger = max(candidate.parameter_count, selected.parameter_count) + return (larger / smaller) <= 2.0 + + +def resolve_ranked_gguf_artifact( + selected_model: ModelInfo, + selected_variant: GGUFVariant, + models: list[ModelInfo], + quant_filter: str | None = None, +) -> tuple[ModelInfo, GGUFVariant] | None: + """Resolve a ranked GGUF candidate to a real HF repo/file. + + The ranker may synthesize GGUF variants for official safetensors-only repos + so they can be scored realistically. Output surfaces and `run` need the + actual GGUF repository and filename when one exists. + """ + desired_quant = quant_filter or selected_variant.quant_type + + if selected_model.gguf_variants: + variant = find_gguf_variant(selected_model, desired_quant) + return (selected_model, variant) if variant else None + + candidates: list[tuple[bool, int, int, ModelInfo, GGUFVariant]] = [] + for model in models: + if not model.gguf_variants or not is_same_model_family(model, selected_model): + continue + if not has_compatible_parameter_count(model, selected_model): + continue + variant = find_gguf_variant(model, desired_quant) + if not variant: + continue + explicit_base = model.base_model == selected_model.id + candidates.append( + ( + explicit_base, + model.downloads, + model.likes, + model, + variant, + ) + ) + + if not candidates: + return None + + _, _, _, model, variant = max(candidates, key=lambda item: item[:3]) + return model, variant + + +def attach_resolved_artifacts( + results: list[CompatibilityResult], + models: list[ModelInfo], + quant_filter: str | None = None, +) -> None: + """Populate artifact fields on ranked results when a real artifact exists.""" + for result in results: + result.artifact_model = None + result.artifact_variant = None + if not result.gguf_variant: + continue + resolved = resolve_ranked_gguf_artifact( + result.model, + result.gguf_variant, + models, + quant_filter=quant_filter, + ) + if resolved: + result.artifact_model, result.artifact_variant = resolved diff --git a/src/whichllm/models/benchmark.py b/src/whichllm/models/benchmark.py new file mode 100644 index 0000000..15b65bb --- /dev/null +++ b/src/whichllm/models/benchmark.py @@ -0,0 +1,90 @@ +"""Compatibility shim for benchmark fetching and lookup helpers. + +The implementation is split by responsibility: + +- ``benchmark_cache`` for cache I/O +- ``benchmark_fetch`` for source fetching and layered merge policy +- ``benchmark_lineage`` for frozen-score recency demotion +- ``benchmark_index`` for score indices and model-line interpolation +- ``benchmark_lookup`` for evidence lookup and inheritance rules + +Existing imports from ``whichllm.models.benchmark`` are re-exported here. +""" + +from __future__ import annotations + +from whichllm.models import benchmark_cache as _cache_module +from whichllm.models.benchmark_cache import DEFAULT_TTL_SECONDS +from whichllm.models.benchmark_fetch import fetch_benchmark_scores +from whichllm.models.benchmark_index import ( + _extract_model_lines, + _extract_params_b_from_id, + _interpolate_line_score, + build_line_bucket_index, + build_score_index, +) +from whichllm.models.benchmark_lineage import ( + _apply_lineage_recency_demotion, + _build_lineage_regex, + _lineage_recency_factor, +) +from whichllm.models.benchmark_lookup import ( + _REPO_SUFFIXES, + _append_unique, + _generate_candidates, + _generate_score_name_candidates, + _params_compatible, + _strip_repo_suffix, + _try_lookup, + lookup_benchmark, + lookup_benchmark_evidence, +) +from whichllm.models.benchmark_types import BenchmarkEvidence + +CACHE_DIR = _cache_module.CACHE_DIR +BENCHMARK_CACHE = _cache_module.BENCHMARK_CACHE + + +def _sync_cache_module_globals() -> None: + _cache_module.CACHE_DIR = CACHE_DIR + _cache_module.BENCHMARK_CACHE = BENCHMARK_CACHE + + +def load_benchmark_cache() -> dict[str, float] | None: + """Load cached benchmark scores through the legacy shim globals.""" + _sync_cache_module_globals() + return _cache_module.load_benchmark_cache() + + +def save_benchmark_cache(scores: dict[str, float]) -> None: + """Save cached benchmark scores through the legacy shim globals.""" + _sync_cache_module_globals() + _cache_module.save_benchmark_cache(scores) + + +__all__ = [ + "BENCHMARK_CACHE", + "CACHE_DIR", + "DEFAULT_TTL_SECONDS", + "BenchmarkEvidence", + "_REPO_SUFFIXES", + "_append_unique", + "_apply_lineage_recency_demotion", + "_build_lineage_regex", + "_extract_model_lines", + "_extract_params_b_from_id", + "_generate_candidates", + "_generate_score_name_candidates", + "_interpolate_line_score", + "_lineage_recency_factor", + "_params_compatible", + "_strip_repo_suffix", + "_try_lookup", + "build_line_bucket_index", + "build_score_index", + "fetch_benchmark_scores", + "load_benchmark_cache", + "lookup_benchmark", + "lookup_benchmark_evidence", + "save_benchmark_cache", +] diff --git a/src/whichllm/models/benchmark_cache.py b/src/whichllm/models/benchmark_cache.py new file mode 100644 index 0000000..8c2bfec --- /dev/null +++ b/src/whichllm/models/benchmark_cache.py @@ -0,0 +1,39 @@ +"""Benchmark score cache helpers.""" + +from __future__ import annotations + +import json +import logging +import time + +from whichllm.utils import _cache_dir + +logger = logging.getLogger(__name__) + +CACHE_DIR = _cache_dir() +BENCHMARK_CACHE = CACHE_DIR / "benchmark.json" +DEFAULT_TTL_SECONDS = 24 * 3600 # 24 hours + + +def load_benchmark_cache() -> dict[str, float] | None: + """Load cached benchmark scores. Returns None if expired or missing.""" + if not BENCHMARK_CACHE.exists(): + return None + try: + data = json.loads(BENCHMARK_CACHE.read_text(encoding="utf-8")) + cached_at = data.get("cached_at", 0) + if time.time() - cached_at > DEFAULT_TTL_SECONDS: + logger.debug("Benchmark cache expired") + return None + return data.get("scores", {}) + except (json.JSONDecodeError, KeyError) as e: + logger.debug(f"Benchmark cache corrupted: {e}") + return None + + +def save_benchmark_cache(scores: dict[str, float]) -> None: + """Save benchmark scores to cache.""" + CACHE_DIR.mkdir(parents=True, exist_ok=True) + data = {"cached_at": time.time(), "scores": scores} + BENCHMARK_CACHE.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8") + logger.debug(f"Saved {len(scores)} benchmark scores to cache") diff --git a/src/whichllm/models/benchmark_fetch.py b/src/whichllm/models/benchmark_fetch.py new file mode 100644 index 0000000..65cede6 --- /dev/null +++ b/src/whichllm/models/benchmark_fetch.py @@ -0,0 +1,119 @@ +"""Fetch and merge benchmark scores from source adapters.""" + +from __future__ import annotations + +import asyncio +import logging + +import httpx + +from whichllm.models.benchmark_lineage import _apply_lineage_recency_demotion +from whichllm.models.http import DEFAULT_ACCEPT_ENCODING +from whichllm.utils import _current_version + +logger = logging.getLogger(__name__) + + +async def fetch_benchmark_scores() -> dict[str, float]: + """Fetch and combine benchmark scores from multiple sources. + + Sources, merged in this order (later overwrites earlier on conflict): + 1. Open LLM Leaderboard v2 (archived 2025-06) + 2. Chatbot Arena ELO (frozen 2025-07-17) + 3. LiveBench (vendored snapshot) + 4. Aider polyglot (coding-specific) + 5. Artificial Analysis Intelligence Index + 6. Vision-language capability index + + Returns dict mapping model_id -> normalized score (0-100). All network + sources are fetched concurrently; failures are logged and skipped. + """ + from whichllm.models import benchmark_sources + + async with httpx.AsyncClient( + timeout=30.0, + follow_redirects=True, + headers={ + "Accept-Encoding": DEFAULT_ACCEPT_ENCODING, + "User-Agent": f"whichllm/{_current_version()}", + }, + ) as client: + leaderboard_task = asyncio.create_task( + benchmark_sources.fetch_leaderboard_with_fallback(client) + ) + arena_task = asyncio.create_task(benchmark_sources.fetch_arena_scores(client)) + aa_task = asyncio.create_task(benchmark_sources.fetch_aa_index_scores(client)) + aider_task = asyncio.create_task( + benchmark_sources.fetch_aider_polyglot_scores(client) + ) + vision_task = asyncio.create_task(benchmark_sources.fetch_vision_scores(client)) + + ( + lb_result, + arena_result, + aa_result, + aider_result, + vision_result, + ) = await asyncio.gather( + leaderboard_task, + arena_task, + aa_task, + aider_task, + vision_task, + return_exceptions=True, + ) + + frozen: dict[str, float] = {} + current: dict[str, float] = {} + + if isinstance(lb_result, BaseException): + logger.warning(f"Leaderboard fetch failed: {lb_result}") + else: + frozen.update(lb_result) + logger.debug(f"Leaderboard: {len(lb_result)} scores (frozen)") + + if isinstance(arena_result, BaseException): + logger.warning(f"Arena fetch failed, using fallback: {arena_result}") + else: + for k, v in arena_result.items(): + if frozen.get(k, 0.0) < v: + frozen[k] = v + logger.debug(f"Arena: {len(arena_result)} scores (frozen)") + + livebench_result = benchmark_sources.get_livebench_data() + for k, v in livebench_result.items(): + if current.get(k, 0.0) < v: + current[k] = v + logger.debug(f"LiveBench: {len(livebench_result)} scores (current)") + + if isinstance(aa_result, BaseException): + logger.warning(f"AA Index fetch failed, will use fallback: {aa_result}") + aa_result = benchmark_sources.get_aa_curated_fallback() + + for k, v in aa_result.items(): + if current.get(k, 0.0) < v: + current[k] = v + logger.debug(f"AA Index: {len(aa_result)} scores (current)") + + if isinstance(aider_result, BaseException): + logger.warning(f"Aider fetch failed: {aider_result}") + else: + for k, v in aider_result.items(): + if current.get(k, 0.0) < v * 0.85: + current[k] = v * 0.85 + logger.debug(f"Aider polyglot: {len(aider_result)} scores (current, 0.85x)") + + if isinstance(vision_result, BaseException): + logger.warning(f"Vision fetch failed: {vision_result}") + else: + for k, v in vision_result.items(): + if current.get(k, 0.0) < v: + current[k] = v + logger.debug(f"Vision: {len(vision_result)} scores (current)") + + combined: dict[str, float] = dict(frozen) + combined.update(current) + combined = _apply_lineage_recency_demotion(combined, frozen, current) + + logger.debug(f"Combined: {len(combined)} benchmark scores") + return combined diff --git a/src/whichllm/models/benchmark_index.py b/src/whichllm/models/benchmark_index.py new file mode 100644 index 0000000..08049f5 --- /dev/null +++ b/src/whichllm/models/benchmark_index.py @@ -0,0 +1,117 @@ +"""Benchmark score indexing and model-line interpolation.""" + +from __future__ import annotations + +import math +import re +import statistics + + +def _extract_params_b_from_id(model_id: str) -> float | None: + """Extract parameter size in billions from model ID text.""" + lower = model_id.lower() + matches = re.findall(r"(\d+(?:\.\d+)?)b(?:-a\d+(?:\.\d+)?b)?", lower) + if not matches: + return None + try: + return max(float(v) for v in matches) + except ValueError: + return None + + +def _extract_model_lines(model_id: str) -> list[str]: + """Extract model line candidates from a model ID, most specific first.""" + if "/" not in model_id: + return [] + lower = model_id.lower() + + stripped = re.sub(r"-(gguf|awq|gptq|fp8|fp16|bf16|mxfp4|nvfp4)$", "", lower) + stripped = re.sub(r"-\d{4}(-hf)?$", "", stripped) + + lines: list[str] = [] + cleaned = re.sub( + r"-\d+(\.\d+)?b(-a\d+b)?(-[a-z][-a-z0-9]*)*$", + "", + stripped, + ) + if cleaned != stripped and "/" in cleaned: + lines.append(cleaned) + + for line in list(lines) + ([stripped] if not lines else []): + broader = re.sub(r"(\d+)\.\d+$", r"\1", line) + if broader != line and broader not in lines: + lines.append(broader) + + return lines + + +def _interpolate_line_score( + bucket: list[tuple[float | None, float]], + params_b: float | None, +) -> tuple[float, float]: + """Interpolate score from same-model-line benchmarks with confidence.""" + if not bucket: + return 0.0, 0.0 + + valid = [(p, s) for p, s in bucket if p is not None] + if not valid: + vals = [s for _, s in bucket] + return statistics.median(vals), 0.25 + + if params_b is None or params_b <= 0: + vals = [s for _, s in valid] + return statistics.median(vals), 0.30 + + weighted: list[tuple[float, float, float]] = [] + for p, s in valid: + assert p is not None + dist = abs(math.log2(max(params_b, 0.1) / max(p, 0.1))) + w = 1.0 / (0.35 + dist) + weighted.append((w, s, dist)) + + score = sum(w * s for w, s, _ in weighted) / sum(w for w, _, _ in weighted) + nearest = min(d for _, _, d in weighted) + if nearest <= 0.15: + conf = 0.45 + elif nearest <= 0.50: + conf = 0.34 + else: + conf = 0.26 + return score, conf + + +def build_score_index( + scores: dict[str, float], +) -> tuple[dict[str, float], dict[str, float]]: + """Build case-insensitive and model-line lookup indices.""" + ci_index: dict[str, float] = {} + line_index: dict[str, float] = {} + + for key, val in scores.items(): + lk = key.lower() + if lk not in ci_index or val > ci_index[lk]: + ci_index[lk] = val + + lines = _extract_model_lines(key) + if not lines and "/" in key: + lines = [lk] + for line in lines: + if line not in line_index or val > line_index[line]: + line_index[line] = val + + return ci_index, line_index + + +def build_line_bucket_index( + scores: dict[str, float], +) -> dict[str, list[tuple[float | None, float]]]: + """Build line -> [(params_b, score)] index for size-aware interpolation.""" + buckets: dict[str, list[tuple[float | None, float]]] = {} + for key, val in scores.items(): + params_b = _extract_params_b_from_id(key) + lines = _extract_model_lines(key) + if not lines and "/" in key: + lines = [key.lower()] + for line in lines: + buckets.setdefault(line, []).append((params_b, val)) + return buckets diff --git a/src/whichllm/models/benchmark_lineage.py b/src/whichllm/models/benchmark_lineage.py new file mode 100644 index 0000000..50cd357 --- /dev/null +++ b/src/whichllm/models/benchmark_lineage.py @@ -0,0 +1,63 @@ +"""Lineage-aware recency demotion for frozen benchmark scores.""" + +from __future__ import annotations + +import re + +_LINEAGE_DEMOTION_REGEX = None + + +def _build_lineage_regex(): + """Compile MODEL_LINEAGE_VERSIONS once into (family, [(re, idx)]) form.""" + global _LINEAGE_DEMOTION_REGEX + if _LINEAGE_DEMOTION_REGEX is not None: + return _LINEAGE_DEMOTION_REGEX + from whichllm.constants import MODEL_LINEAGE_VERSIONS + + out = {} + for family, entries in MODEL_LINEAGE_VERSIONS.items(): + compiled = [(re.compile(pat), idx) for pat, idx in entries] + max_idx = max(idx for _, idx in entries) + out[family] = (compiled, max_idx) + _LINEAGE_DEMOTION_REGEX = out + return out + + +def _lineage_recency_factor(model_id: str) -> float: + """Return a multiplicative recency factor for frozen-only scores. + + Newest generation in a known family -> 1.0 (no demotion). Each generation + older -> another 12% off. Unknown families -> 1.0. + """ + if not model_id: + return 1.0 + lower = model_id.lower() + families = _build_lineage_regex() + best_factor = 1.0 + for family, (patterns, max_idx) in families.items(): + for regex, idx in patterns: + if regex.search(lower): + gens_old = max(0, max_idx - idx) + factor = max(0.55, 1.0 - 0.12 * gens_old) + if factor < best_factor: + best_factor = factor + break + return best_factor + + +def _apply_lineage_recency_demotion( + combined: dict[str, float], + frozen: dict[str, float], + current: dict[str, float], +) -> dict[str, float]: + """Multiply frozen-only entries by a lineage-derived recency factor.""" + if not combined: + return combined + out: dict[str, float] = {} + for k, v in combined.items(): + if k in current: + out[k] = v + continue + factor = _lineage_recency_factor(k) + out[k] = round(v * factor, 1) + return out diff --git a/src/whichllm/models/benchmark_lookup.py b/src/whichllm/models/benchmark_lookup.py new file mode 100644 index 0000000..e9e52a8 --- /dev/null +++ b/src/whichllm/models/benchmark_lookup.py @@ -0,0 +1,192 @@ +"""Benchmark evidence lookup and inheritance rules.""" + +from __future__ import annotations + +from whichllm.models.benchmark_index import ( + _extract_model_lines, + _extract_params_b_from_id, + _interpolate_line_score, + build_line_bucket_index, + build_score_index, +) +from whichllm.models.benchmark_types import BenchmarkEvidence + + +def _try_lookup( + candidate: str, scores: dict[str, float], ci_index: dict[str, float] +) -> float | None: + """Try exact match, then case-insensitive match.""" + if candidate in scores: + return scores[candidate] + lc = candidate.lower() + if lc in ci_index: + return ci_index[lc] + return None + + +_REPO_SUFFIXES = ("-GGUF", "-gguf", "-AWQ", "-GPTQ", "-FP8", "-fp8", "-BF16", "-bf16") + + +def _generate_candidates(model_id: str) -> list[str]: + """Generate candidate IDs to look up for a model.""" + candidates = [model_id] + + for suffix in _REPO_SUFFIXES: + if model_id.endswith(suffix): + candidates.append(model_id[: -len(suffix)]) + break + + base = candidates[-1] + if base.endswith("-Instruct"): + candidates.append(base[: -len("-Instruct")]) + else: + candidates.append(base + "-Instruct") + + return candidates + + +def _append_unique(candidates: list[str], candidate: str) -> None: + if candidate and candidate not in candidates: + candidates.append(candidate) + + +def _strip_repo_suffix(model_id: str) -> str: + for suffix in _REPO_SUFFIXES: + if model_id.endswith(suffix): + return model_id[: -len(suffix)] + return model_id + + +def _generate_score_name_candidates( + model_id: str, scores: dict[str, float] +) -> list[str]: + """Match community repo names to benchmark IDs with the same model name.""" + stripped = _strip_repo_suffix(model_id) + repo_name = stripped.rsplit("/", 1)[-1] + model_names = [repo_name] + + explicit_candidates: list[str] = [] + if "_" in repo_name: + org, name = repo_name.split("_", 1) + if org and name: + _append_unique(explicit_candidates, f"{org}/{name}") + _append_unique(model_names, name) + + score_candidates: list[str] = [] + wanted_names = {name.lower() for name in model_names if name} + for score_id in scores: + score_name = score_id.rsplit("/", 1)[-1].lower() + if score_name in wanted_names: + _append_unique(score_candidates, score_id) + + return explicit_candidates + [ + candidate + for candidate in score_candidates + if candidate not in explicit_candidates + ] + + +def lookup_benchmark( + model_id: str, + base_model: str | None, + scores: dict[str, float], + ci_index: dict[str, float] | None = None, + line_index: dict[str, float] | None = None, +) -> tuple[float, bool] | None: + """Backward-compatible benchmark lookup helper.""" + evidence = lookup_benchmark_evidence( + model_id, + base_model, + scores, + ci_index=ci_index, + line_index=line_index, + ) + if evidence.score is None: + return None + return evidence.score, evidence.source == "direct" + + +def _params_compatible(actual_b: float | None, ref_id: str) -> bool: + """Reject benchmark inheritance when actual and reference sizes diverge.""" + if actual_b is None or actual_b <= 0: + return True + ref_b = _extract_params_b_from_id(ref_id) + if ref_b is None or ref_b <= 0: + return True + ratio = actual_b / ref_b + return 0.5 <= ratio <= 2.0 + + +def lookup_benchmark_evidence( + model_id: str, + base_model: str | None, + scores: dict[str, float], + ci_index: dict[str, float] | None = None, + line_index: dict[str, float] | None = None, + line_bucket_index: dict[str, list[tuple[float | None, float]]] | None = None, + self_reported_score: float | None = None, + actual_params_b: float | None = None, +) -> BenchmarkEvidence: + """Look up benchmark evidence with confidence.""" + if ci_index is None or line_index is None: + ci_index, line_index = build_score_index(scores) + if line_bucket_index is None: + line_bucket_index = build_line_bucket_index(scores) + + direct_result = _try_lookup(model_id, scores, ci_index) + if direct_result is not None: + return BenchmarkEvidence(score=direct_result, confidence=1.0, source="direct") + + variant_candidates = _generate_candidates(model_id)[1:] + for candidate in _generate_score_name_candidates(model_id, scores): + _append_unique(variant_candidates, candidate) + for candidate in variant_candidates: + result = _try_lookup(candidate, scores, ci_index) + if result is not None: + if not _params_compatible(actual_params_b, candidate): + continue + return BenchmarkEvidence(score=result, confidence=0.55, source="variant") + + if base_model: + for candidate in _generate_candidates(base_model): + result = _try_lookup(candidate, scores, ci_index) + if result is not None: + if not _params_compatible(actual_params_b, candidate): + continue + return BenchmarkEvidence( + score=result, confidence=0.60, source="base_model" + ) + + size_hint = ( + actual_params_b + or _extract_params_b_from_id(model_id) + or _extract_params_b_from_id(base_model or "") + ) + for mid in (model_id, base_model): + if mid: + for line in _extract_model_lines(mid): + if line in line_bucket_index: + score, conf = _interpolate_line_score( + line_bucket_index[line], size_hint + ) + if score > 0: + return BenchmarkEvidence( + score=score, confidence=conf, source="line_interp" + ) + if line in line_index: + return BenchmarkEvidence( + score=line_index[line], confidence=0.22, source="line_interp" + ) + + if ( + self_reported_score is not None + and isinstance(self_reported_score, (int, float)) + and self_reported_score > 0 + ): + return BenchmarkEvidence( + score=float(self_reported_score), + confidence=0.40, + source="self_reported", + ) + + return BenchmarkEvidence(score=None, confidence=0.0, source="none") diff --git a/src/whichllm/models/benchmark_sources/__init__.py b/src/whichllm/models/benchmark_sources/__init__.py new file mode 100644 index 0000000..a54a21b --- /dev/null +++ b/src/whichllm/models/benchmark_sources/__init__.py @@ -0,0 +1,43 @@ +"""External benchmark sources beyond Chatbot Arena and Open LLM Leaderboard. + +Each module here fetches an independent leaderboard / index, normalizes it to +the same 0-100 scale, and returns a ``dict[str, float]`` keyed by HuggingFace +model id (or a list of synonyms). + +The functions are intentionally defensive: if a source is unreachable or +returns malformed data, they log a warning and return an empty dict so the +main benchmark merge pipeline does not abort. +""" + +from whichllm.models.benchmark_sources.aa_index import ( + fetch_aa_index_scores, + get_aa_curated_fallback, +) +from whichllm.models.benchmark_sources.aider import fetch_aider_polyglot_scores +from whichllm.models.benchmark_sources.chatbot_arena import fetch_arena_scores +from whichllm.models.benchmark_sources.livebench import ( + get_livebench_data, +) +from whichllm.models.benchmark_sources.open_llm_leaderboard import ( + fetch_leaderboard_with_fallback, +) +from whichllm.models.benchmark_sources.vision import fetch_vision_scores + +# Newest curated-fallback date across all sources. Live scrapes are merged +# on top when reachable, but they frequently are not (the leaderboard +# spaces change their JSON shape), so the user-visible ranking is anchored +# to this snapshot. Surface it in the CLI so a stale recommendation is +# self-evident rather than silently trusted. Bump this whenever any +# *_FALLBACK_* dict is refreshed. +BENCHMARK_SNAPSHOT = "2026-05" + +__all__ = [ + "BENCHMARK_SNAPSHOT", + "fetch_aa_index_scores", + "fetch_aider_polyglot_scores", + "fetch_arena_scores", + "fetch_leaderboard_with_fallback", + "fetch_vision_scores", + "get_aa_curated_fallback", + "get_livebench_data", +] diff --git a/src/whichllm/models/benchmark_sources/aa_index.py b/src/whichllm/models/benchmark_sources/aa_index.py new file mode 100644 index 0000000..77fcdea --- /dev/null +++ b/src/whichllm/models/benchmark_sources/aa_index.py @@ -0,0 +1,386 @@ +"""Artificial Analysis Intelligence Index source. + +AA publishes a model-quality index (https://artificialanalysis.ai/) that +covers post-2025-08 frontier releases (DeepSeek V4, GLM-5, Kimi K2.6, +MiMo V2.5, Qwen3.6, etc.) that whichllm's primary sources have stopped +tracking. The index is exposed via the JSON payload embedded in +``__NEXT_DATA__`` on the leaderboard page. + +The fetcher is defensive: any failure (network, schema drift, parsing) is +caught and an empty dict is returned so it never blocks the main benchmark +pipeline. +""" + +from __future__ import annotations + +import json +import logging +import re + +import httpx + +from whichllm.models.benchmark_sources.constants import _NEXT_DATA_RE +from whichllm.models.benchmark_sources.types import ExtractionFailed +from whichllm.models.benchmark_sources.utils import _walk +from whichllm.models.http import get_with_retries + +logger = logging.getLogger(__name__) + +# Display name -> list of (org_prefix, repo_name_candidates) tuples used to +# map AA-reported labels back to HuggingFace model IDs. Only the most common +# fully-open-weights releases need entries here; anything else is dropped. +AA_NAME_TO_HF_IDS: dict[str, list[str]] = { + "Kimi K2": ["moonshotai/Kimi-K2-Instruct", "moonshotai/Kimi-K2-Base"], + "Kimi K2-Thinking": ["moonshotai/Kimi-K2-Thinking"], + "DeepSeek V3": ["deepseek-ai/DeepSeek-V3", "deepseek-ai/DeepSeek-V3-0324"], + "DeepSeek V3.1": ["deepseek-ai/DeepSeek-V3.1"], + "DeepSeek V3.2": ["deepseek-ai/DeepSeek-V3.2"], + "DeepSeek V3.2-Exp": ["deepseek-ai/DeepSeek-V3.2-Exp"], + "DeepSeek V4 Pro": ["deepseek-ai/DeepSeek-V4-Pro"], + "DeepSeek V4 Flash": ["deepseek-ai/DeepSeek-V4-Flash"], + "DeepSeek R1": ["deepseek-ai/DeepSeek-R1"], + "DeepSeek R1-0528": ["deepseek-ai/DeepSeek-R1-0528"], + "DeepSeek R1-Distill 32B": ["deepseek-ai/DeepSeek-R1-Distill-Qwen-32B"], + "DeepSeek R1-Distill 14B": ["deepseek-ai/DeepSeek-R1-Distill-Qwen-14B"], + "DeepSeek R1-Distill 8B": ["deepseek-ai/DeepSeek-R1-Distill-Llama-8B"], + "QwQ 32B": ["Qwen/QwQ-32B"], + "Qwen3 4B Thinking": ["Qwen/Qwen3-4B-Thinking-2507"], + "MiMo V2.5": ["XiaomiMiMo/MiMo-V2.5"], + "MiMo V2.5 Pro": ["XiaomiMiMo/MiMo-V2.5-Pro"], + "MiMo V2 Flash": ["XiaomiMiMo/MiMo-V2-Flash"], + "GLM-4.5": ["zai-org/GLM-4.5", "zai-org/GLM-4.5-Air"], + "GLM-4.6": ["zai-org/GLM-4.6"], + "GLM-4.7": ["zai-org/GLM-4.7"], + "GLM-4.7-Flash": ["zai-org/GLM-4.7-Flash"], + "GLM-5": ["zai-org/GLM-5", "zai-org/GLM-5-FP8"], + "GLM-5.1": ["zai-org/GLM-5.1", "zai-org/GLM-5.1-FP8"], + "gpt-oss-20b": ["openai/gpt-oss-20b"], + "gpt-oss-120b": ["openai/gpt-oss-120b"], + "Qwen3-Next 80B-A3B": ["Qwen/Qwen3-Next-80B-A3B-Instruct"], + "Qwen3.5 397B-A17B": ["Qwen/Qwen3.5-397B-A17B"], + "Qwen3 235B-A22B": ["Qwen/Qwen3-235B-A22B"], + "Qwen3 32B": ["Qwen/Qwen3-32B"], + "Qwen3 14B": ["Qwen/Qwen3-14B"], + "Qwen3 8B": ["Qwen/Qwen3-8B"], + "Qwen3-VL 235B-A22B": ["Qwen/Qwen3-VL-235B-A22B-Instruct"], + "Llama 3.3 70B": ["meta-llama/Llama-3.3-70B-Instruct"], + "Llama 4 Scout": ["meta-llama/Llama-4-Scout-17B-16E-Instruct"], + "Llama 4 Maverick": ["meta-llama/Llama-4-Maverick-17B-128E-Instruct"], + "Gemma 3 27B": ["google/gemma-3-27b-it"], + "Gemma 3 12B": ["google/gemma-3-12b-it"], + "Gemma 4 31B": ["google/gemma-4-31b-it"], + "Gemma 4 26B-A4B": ["google/gemma-4-26b-a4b-it"], + "Mistral Large 2": ["mistralai/Mistral-Large-Instruct-2411"], + "Devstral Small": ["mistralai/Devstral-Small-2505"], + "Phi-4": ["microsoft/phi-4"], + "Command A": ["CohereForAI/c4ai-command-a-03-2025"], + "Command R+": [ + "CohereForAI/c4ai-command-r-plus-08-2024", + "CohereForAI/c4ai-command-r-plus", + ], + "MiniMax-M2": ["MiniMaxAI/MiniMax-M2"], + "MiniMax-M2.5": ["MiniMaxAI/MiniMax-M2.5"], + "Nemotron 3 Super 120B-A12B": ["nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-BF16"], + "Nemotron 3 Nano 30B-A3B": [ + "nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-BF16", + "nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-FP8", + ], +} + +# Bounds used to normalize raw AA index values onto the 0-100 scale the rest +# of the ranking system uses. AA reworked their Intelligence Index in +# 2026-Q2 and the open-weights distribution compressed sharply (top open model +# ~44, 8B-class ~7, weakest mapped repos ~3). The window is anchored by the +# same two-point fit as before: top open frontier (DeepSeek-V4-Pro = 44.3) → 95 +# normalized, and 8B-class (Qwen3-8B = 7.4) → 40 normalized. This keeps a strong +# 8B model competitive with frozen-OLLB 7B scores while leaving headroom for +# frontier-tier models. On the reworked scale that fit puts the floor below +# zero; that is fine, live AA values are always positive and clamp at 0. +_AA_INDEX_MIN = -19.4 +_AA_INDEX_MAX = 47.6 + +AA_LEADERBOARD_URL = "https://artificialanalysis.ai/leaderboards/models" + +# Snapshot of the AA Intelligence Index (open-weights only), refreshed on +# 2026-06-29 from artificialanalysis.ai against their reworked index. Used as a +# fallback when the live HTML scrape returns no results (e.g. because the +# Next.js payload format changes again). Entries are raw AA index values, +# normalized through _normalize_aa_index() in get_aa_curated_fallback(). +# +# Entries marked "live" are real values from the reworked index. Entries marked +# "peer" are models AA does not track; their raw value is set so that, under the +# current bounds, they reproduce their previous normalized score (hand-estimated +# LB-equivalents). On the reworked scale several peers fall below the index +# floor and read as negative raw; that is an artifact of reusing the AA-index +# field for non-AA estimates. _normalize_aa_index() clamps them at 0, so the +# normalized output stays correct. (We could instead store this snapshot as +# already-normalized 0-100 values, which would drop the negatives and decouple +# the fallback from future bound retunes, but that is a larger change than this +# issue calls for and is left for a follow-up if you want it.) +AA_INDEX_FALLBACK_2026_06_29: dict[str, float] = { + # Frontier MoE / very large + "moonshotai/Kimi-K2-Thinking": 32.7, # live + "moonshotai/Kimi-K2-Instruct": 19.4, # live + "XiaomiMiMo/MiMo-V2.5-Pro": 42.2, # live + "XiaomiMiMo/MiMo-V2.5": 40.1, # live + "deepseek-ai/DeepSeek-V4-Pro": 44.3, # live + "deepseek-ai/DeepSeek-V4-Flash": 40.3, # live + "deepseek-ai/DeepSeek-V3.2": 33.4, # live + "deepseek-ai/DeepSeek-V3.2-Exp": 25.4, # live + "deepseek-ai/DeepSeek-V3.1": 21.0, # live + "deepseek-ai/DeepSeek-V3-0324": 10.4, # live + "deepseek-ai/DeepSeek-V3": 10.4, # live + "deepseek-ai/DeepSeek-R1-0528": 20.1, # live + "deepseek-ai/DeepSeek-R1": 12.6, # live + "deepseek-ai/DeepSeek-R1-Distill-Qwen-32B": 10.5, # peer + "deepseek-ai/DeepSeek-R1-Distill-Qwen-14B": 1.3, # peer + "deepseek-ai/DeepSeek-R1-Distill-Llama-8B": -7.9, # peer + "Qwen/QwQ-32B": 13.4, # live + "Qwen/Qwen3-4B-Thinking-2507": -4.8, # peer + "zai-org/GLM-5.1": 40.2, # live + "zai-org/GLM-5": 39.5, # live + "zai-org/GLM-5-FP8": 39.5, # live + "zai-org/GLM-5.1-FP8": 40.2, # live + "zai-org/GLM-4.7-Flash": 22.9, # live + "zai-org/GLM-4.6": 25.1, # live + "zai-org/GLM-4.5": 19.5, # live + "zai-org/GLM-4.5-Air": 19.5, # live + # Qwen family + "Qwen/Qwen3.6-27B": 32.0, # peer + "Qwen/Qwen3.5-397B-A17B": 33.7, # live + "Qwen/Qwen3-Next-80B-A3B-Instruct": 19.8, # live + "Qwen/Qwen3-235B-A22B": 13.4, # live + "Qwen/Qwen3-Coder-30B-A3B-Instruct": 19.7, # peer + "Qwen/Qwen3-32B": 11.5, # live + "Qwen/Qwen3-14B": 10.1, # live + "Qwen/Qwen3-8B": 7.4, # live + "Qwen/Qwen3-4B-Instruct-2507": 4.4, # peer + "Qwen/Qwen3-4B": 1.3, # peer + "Qwen/Qwen3-1.7B": -7.9, # peer + "Qwen/Qwen3-0.6B": -14.0, # peer + # 8B-class peers (no AA tracking but realistic LB-equivalents) + "meta-llama/Llama-3.1-8B-Instruct": -4.8, # peer + "meta-llama/Meta-Llama-3-8B-Instruct": -7.9, # peer + "google/gemma-2-9b-it": -3.3, # peer + "microsoft/Phi-4-mini-instruct": -1.8, # peer + "mistralai/Mistral-7B-Instruct-v0.3": -7.9, # peer + "Qwen/Qwen2.5-7B-Instruct": -4.8, # peer + "Qwen/Qwen2.5-14B-Instruct": 1.3, # peer + "Qwen/Qwen2.5-32B-Instruct": 7.4, # peer + "Qwen/Qwen3-30B-A3B": 10.5, # peer + # Other major open releases + "openai/gpt-oss-120b": 23.8, # live + "openai/gpt-oss-20b": 14.9, # live + "meta-llama/Llama-4-Maverick-17B-128E-Instruct": 14.3, # live + "meta-llama/Llama-4-Scout-17B-16E-Instruct": 10.0, # live + "meta-llama/Llama-3.3-70B-Instruct": 12.0, # peer + "google/gemma-4-31b-it": 29.4, # live + "google/gemma-4-26b-a4b-it": 25.7, # live + "google/gemma-3-27b-it": 12.0, # peer + "google/gemma-3-12b-it": 7.4, # peer + "microsoft/phi-4": 4.9, # live + "mistralai/Mistral-Large-Instruct-2411": 9.1, # live + "mistralai/Mistral-Small-3.2-24B-Instruct-2506": 10.5, # peer + "mistralai/Mistral-Small-3.1-24B-Instruct-2503": 7.4, # peer + "mistralai/Devstral-Small-2505": 11.8, # live + "MiniMaxAI/MiniMax-M2.5": 33.7, # live + "stepfun-ai/Step-3.5-Flash": 19.7, # peer + "nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-BF16": 16.6, # peer + "nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-BF16": 12.0, # peer + # Correct IDs for OLMo / Granite / Codestral families (the earlier + # forecast IDs like "OLMo-3-32B-Instruct" or "granite-4.1-30b-instruct" + # never shipped publicly under those names). + "allenai/Olmo-3-7B-Instruct": -4.8, # peer + "allenai/Olmo-3-1025-7B": -4.8, # peer + "ibm-granite/granite-4.0-h-small": 7.4, # peer + "ibm-granite/granite-4.0-h-tiny": -4.8, # peer + "ibm-granite/granite-3.3-8b-instruct": -3.3, # peer + "ibm-granite/granite-3.3-2b-instruct": -12.5, # peer + "mistralai/Codestral-22B-v0.1": 4.4, # peer +} + + +def _normalize_aa_index(index: float) -> float: + """Normalize a raw AA index value onto the 0-100 scale.""" + if not isinstance(index, (int, float)): + return 0.0 + span = _AA_INDEX_MAX - _AA_INDEX_MIN + normalized = (index - _AA_INDEX_MIN) / span * 100.0 + return max(0.0, min(100.0, round(normalized, 1))) + + +# --- Next.js App Router (RSC) scraping ------------------------------------- +# +# artificialanalysis.ai migrated off the classic ``__NEXT_DATA__`` blob to the +# App Router streaming format: model data arrives in ``self.__next_f.push([n, +# "…"])`` calls whose second element is a JSON-string-escaped fragment of the +# RSC payload. We concatenate + unescape those chunks, then pull every +# ``{"name": …, …, "intelligenceIndex": …}`` record out with a bounded regex +# (the payload is a flat RSC stream, not a single parseable JSON document). + +_RSC_CHUNK_RE = re.compile(r'self\.__next_f\.push\(\[\d+,(?P"(?:[^"\\]|\\.)*")\]\)') +# A model record: a "name" string followed, within the SAME object, by its +# "intelligenceIndex". The middle group forbids another '"name":"' so the +# match cannot leak across into a neighbouring record that lacks an index. +_AA_RECORD_RE = re.compile( + r'"name":"(?P(?:[^"\\]|\\.)*)"' + r'(?:(?!"name":").)*?' + r'"intelligenceIndex":(?P-?\d+(?:\.\d+)?)', + re.DOTALL, +) +# Variant qualifiers AA appends that don't change the underlying HF weights: +# "(Reasoning)", "(Non-reasoning)", "(high)", "(Reasoning, Max Effort)", etc. +_PAREN_RE = re.compile(r"\([^)]*\)") + + +def _canonical_name(name: str) -> str: + """Normalize an AA display name for fuzzy matching against the HF map. + + Drops parenthetical variant qualifiers and collapses separator/case noise + so ``"Qwen3 14B (Reasoning)"`` and ``"Qwen3-14B"`` both canonicalize to + ``"qwen3 14b"``. + """ + name = _PAREN_RE.sub("", name) + name = name.lower().replace("-", " ").replace("_", " ") + return re.sub(r"\s+", " ", name).strip() + + +# Canonical-name -> HF ids, derived once from AA_NAME_TO_HF_IDS. Several display +# names can collapse to one canonical key; we union their HF ids. +_AA_CANON_TO_HF_IDS: dict[str, list[str]] = {} +for _disp, _ids in AA_NAME_TO_HF_IDS.items(): + _AA_CANON_TO_HF_IDS.setdefault(_canonical_name(_disp), []).extend(_ids) + + +def _decode_rsc_blob(html: str) -> str: + """Concatenate and unescape the App Router RSC chunks into one string.""" + parts: list[str] = [] + for m in _RSC_CHUNK_RE.finditer(html): + try: + parts.append(json.loads(m.group("s"))) + except (ValueError, json.JSONDecodeError): + continue + return "".join(parts) + + +def _extract_aa_pairs_from_html(html: str) -> list[tuple[str, float]]: + """Extract (name, intelligence_index) pairs from the RSC stream.""" + blob = _decode_rsc_blob(html) + if not blob: + return [] + pairs: list[tuple[str, float]] = [] + for m in _AA_RECORD_RE.finditer(blob): + try: + name = json.loads('"' + m.group("name") + '"').strip() + score = float(m.group("idx")) + except (ValueError, json.JSONDecodeError): + continue + if name and score > 0: + pairs.append((name, score)) + return pairs + + +def _extract_aa_pairs(payload: dict) -> list[tuple[str, float]]: + """Walk the Next.js payload looking for {name, intelligenceIndex}-shaped + objects regardless of where they are nested.""" + pairs: list[tuple[str, float]] = [] + for node in _walk(payload): + # Look for the most common shapes AA has used in past iterations. + name = None + score = None + for name_key in ("model_name", "modelName", "name", "displayName"): + v = node.get(name_key) + if isinstance(v, str) and v.strip(): + name = v.strip() + break + for score_key in ( + "intelligence_index", + "intelligenceIndex", + "aa_index", + "aaIndex", + "score", + ): + v = node.get(score_key) + if isinstance(v, (int, float)): + score = float(v) + break + if name and score is not None and score > 0: + pairs.append((name, score)) + return pairs + + +async def fetch_aa_index_scores(client: httpx.AsyncClient) -> dict[str, float]: + """Fetch Artificial Analysis Intelligence Index scores. + + Returns ``{hf_id: normalized_score_0_100}`` for every model AA reports + that we can map back to a HuggingFace repo via :data:`AA_NAME_TO_HF_IDS`. + + Raises on HTTP / parse failure. + """ + resp = await get_with_retries(client, AA_LEADERBOARD_URL) + resp.raise_for_status() + # Primary: Next.js App Router RSC stream (current site format). + pairs = _extract_aa_pairs_from_html(resp.text) + # Legacy fallback: classic __NEXT_DATA__ JSON blob (older site format). + if not pairs: + match = _NEXT_DATA_RE.search(resp.text) + if match: + try: + pairs = _extract_aa_pairs(json.loads(match.group("json"))) + except (ValueError, json.JSONDecodeError): + pairs = [] + if not pairs: + raise ExtractionFailed( + "AA leaderboard: no (name, score) pairs found " + "(neither RSC __next_f nor __NEXT_DATA__ matched)" + ) + # When the same display name appears multiple times (different size / + # reasoning tiers), keep the maximum value — it represents the most + # capable variant available. + best_by_name: dict[str, float] = {} + for name, score in pairs: + current = best_by_name.get(name) + if current is None or score > current: + best_by_name[name] = score + + live: dict[str, float] = {} + for name, score in best_by_name.items(): + # Exact display name first, then canonicalized (variant-stripped) match. + hf_ids = AA_NAME_TO_HF_IDS.get(name) or _AA_CANON_TO_HF_IDS.get( + _canonical_name(name) + ) + if not hf_ids: + continue + normalized = _normalize_aa_index(score) + if normalized <= 0: + continue + for hf_id in hf_ids: + if normalized > live.get(hf_id, 0.0): + live[hf_id] = normalized + if not live: + raise ExtractionFailed("AA index: live fetch returned 0 mapped scores") + + # Overlay live scores on top of the curated snapshot so a successful live + # fetch can only ADD coverage, never shrink it below the fallback. Live + # numbers win wherever both exist; the snapshot fills the long tail of + # models AA labels in a way we can't map (or no longer tracks). + scores = get_aa_curated_fallback() + for hf_id, normalized in live.items(): + if normalized > scores.get(hf_id, 0.0): + scores[hf_id] = normalized + logger.debug(f"AA index: {len(live)} live + {len(scores)} merged scores") + return scores + + +def get_aa_curated_fallback() -> dict[str, float]: + """Return the 2026-06-29 curated snapshot, normalized to the 0-100 scale. + + Used whenever the live HTML scrape cannot extract data — for example + when artificialanalysis.ai changes its Next.js payload shape. + """ + result: dict[str, float] = {} + for hf_id, raw in AA_INDEX_FALLBACK_2026_06_29.items(): + normalized = _normalize_aa_index(raw) + if normalized > 0: + result[hf_id] = normalized + return result diff --git a/src/whichllm/models/benchmark_sources/aider.py b/src/whichllm/models/benchmark_sources/aider.py new file mode 100644 index 0000000..d807c74 --- /dev/null +++ b/src/whichllm/models/benchmark_sources/aider.py @@ -0,0 +1,135 @@ +"""Aider polyglot leaderboard source. + +The polyglot leaderboard ranks LLMs by how well they edit code across six +languages (C++, Go, Java, JavaScript, Python, Rust). Aider publishes the raw +data as a YAML file in its GitHub repo, which is easier to parse than the +rendered HTML and rarely changes shape. + +Used primarily for ``profile=coding`` ranking, but the score is also merged +into the general benchmark dict so coding-strong models get a small bump +elsewhere. +""" + +from __future__ import annotations + +import logging +import re + +import httpx + +from whichllm.models.http import get_with_retries + +logger = logging.getLogger(__name__) + +AIDER_POLYGLOT_YML_URL = ( + "https://raw.githubusercontent.com/Aider-AI/aider/main/" + "aider/website/_data/polyglot_leaderboard.yml" +) + +# Polyglot pass-rate is 0-100 (percent of exercises passing). Treat the +# floor/ceiling as 0..90 since the cap of practical models is ~88%. +_PG_MIN = 0.0 +_PG_MAX = 90.0 + +AIDER_NAME_TO_HF_IDS: dict[str, list[str]] = { + "deepseek-r1": ["deepseek-ai/DeepSeek-R1"], + "deepseek-r1-0528": ["deepseek-ai/DeepSeek-R1-0528"], + "deepseek-v3": ["deepseek-ai/DeepSeek-V3"], + "deepseek-v3-0324": ["deepseek-ai/DeepSeek-V3-0324"], + "deepseek-v3.1": ["deepseek-ai/DeepSeek-V3.1"], + "deepseek-v3.2": ["deepseek-ai/DeepSeek-V3.2"], + "deepseek-v4-pro": ["deepseek-ai/DeepSeek-V4-Pro"], + "deepseek-v4-flash": ["deepseek-ai/DeepSeek-V4-Flash"], + "qwen3-coder-30b-a3b-instruct": ["Qwen/Qwen3-Coder-30B-A3B-Instruct"], + "qwen3-coder-next": ["Qwen/Qwen3-Coder-Next"], + "qwen2.5-coder-32b-instruct": ["Qwen/Qwen2.5-Coder-32B-Instruct"], + "qwen3-32b": ["Qwen/Qwen3-32B"], + "qwen3.6-27b": ["Qwen/Qwen3.6-27B"], + "llama-3.3-70b-instruct": ["meta-llama/Llama-3.3-70B-Instruct"], + "llama-4-maverick": ["meta-llama/Llama-4-Maverick-17B-128E-Instruct"], + "gemma-3-27b-it": ["google/gemma-3-27b-it"], + "gemma-4-31b": ["google/gemma-4-31b-it"], + "mistral-large-2411": ["mistralai/Mistral-Large-Instruct-2411"], + "devstral-small": ["mistralai/Devstral-Small-2505"], + "gpt-oss-120b": ["openai/gpt-oss-120b"], + "gpt-oss-20b": ["openai/gpt-oss-20b"], + "glm-4.5": ["zai-org/GLM-4.5"], + "glm-4.6": ["zai-org/GLM-4.6"], + "glm-5": ["zai-org/GLM-5"], + "glm-5.1": ["zai-org/GLM-5.1"], + "kimi-k2-instruct": ["moonshotai/Kimi-K2-Instruct"], + "phi-4": ["microsoft/phi-4"], + "qwq-32b": ["Qwen/QwQ-32B"], +} + + +_PASS_RATE_RE = re.compile(r"pass_rate[_-]?2[:\s]+(\d+(?:\.\d+)?)", re.IGNORECASE) +_MODEL_RE = re.compile(r"^- model[:\s]+(.+)$", re.MULTILINE) + + +def _normalize(pass_rate: float) -> float: + if not isinstance(pass_rate, (int, float)): + return 0.0 + span = _PG_MAX - _PG_MIN + normalized = (pass_rate - _PG_MIN) / span * 100.0 + return max(0.0, min(100.0, round(normalized, 1))) + + +def _parse_yaml_lite(text: str) -> list[tuple[str, float]]: + """Tiny YAML extractor for the polyglot leaderboard format. + + We avoid pulling in PyYAML; the file shape is stable enough that two + regexes scanning each record block suffice. Each record looks like: + - dirname: 2024-12-22-blah + model: deepseek/deepseek-chat + edit_format: diff + pass_rate_2: 80.7 + ... + """ + out: list[tuple[str, float]] = [] + # Split into records starting with "- " + records = re.split(r"\n(?=-\s+\w)", text) + for rec in records: + m_model = re.search(r"^\s*model[:\s]+(.+?)$", rec, re.MULTILINE | re.IGNORECASE) + m_rate = re.search(r"pass_rate_2[:\s]+(\d+(?:\.\d+)?)", rec, re.IGNORECASE) + if not m_model or not m_rate: + continue + name = m_model.group(1).strip().strip("\"'") + # Strip any provider prefix like "deepseek/" or "openrouter/" + name = name.split("/", 1)[-1].strip().lower() + try: + rate = float(m_rate.group(1)) + except ValueError: + continue + if rate <= 0: + continue + out.append((name, rate)) + return out + + +async def fetch_aider_polyglot_scores(client: httpx.AsyncClient) -> dict[str, float]: + """Fetch Aider polyglot pass-rates. Raises on HTTP / parse failure.""" + scores: dict[str, float] = {} + resp = await get_with_retries(client, AIDER_POLYGLOT_YML_URL) + resp.raise_for_status() + pairs = _parse_yaml_lite(resp.text) + if not pairs: + logger.debug("Aider polyglot: 0 records parsed") + return {} + best_by_name: dict[str, float] = {} + for name, rate in pairs: + cur = best_by_name.get(name) + if cur is None or rate > cur: + best_by_name[name] = rate + for name, rate in best_by_name.items(): + ids = AIDER_NAME_TO_HF_IDS.get(name) + if not ids: + continue + normalized = _normalize(rate) + if normalized <= 0: + continue + for hf_id in ids: + if scores.get(hf_id, 0.0) < normalized: + scores[hf_id] = normalized + logger.debug(f"Aider polyglot: {len(scores)} mapped scores") + return scores diff --git a/src/whichllm/models/benchmark_sources/chatbot_arena.py b/src/whichllm/models/benchmark_sources/chatbot_arena.py new file mode 100644 index 0000000..c9c8cc7 --- /dev/null +++ b/src/whichllm/models/benchmark_sources/chatbot_arena.py @@ -0,0 +1,140 @@ +from __future__ import annotations + +import re + +import httpx + +from whichllm.models.http import get_with_retries + +# --- Data source URLs --- +ARENA_ROWS_URL = "https://datasets-server.huggingface.co/rows" +ARENA_DATASET = "mathewhe/chatbot-arena-elo" + +# --- Arena ELO normalization --- +# Open-source ELO range: ~1030 (worst) to ~1424 (best). Arena is frozen +# 2025-07-17 (no new models added) so the leaderboard cannot reflect any +# 2025-Q3+ release; we cap the normalized output at 82 so newer benchmark +# sources (AA Index / LiveBench, which can reach 95+) decisively win on +# conflict. +_ARENA_ELO_MIN = 1030 +_ARENA_ELO_MAX = 1430 +_ARENA_MAX_NORMALIZED = 82.0 + +# --- Arena display name -> HuggingFace org mapping --- +_ARENA_ORG_TO_HF: dict[str, list[str]] = { + "Alibaba": ["Qwen"], + "Meta": ["meta-llama"], + "DeepSeek": ["deepseek-ai"], + "DeepSeek AI": ["deepseek-ai"], + "Google": ["google"], + "Mistral": ["mistralai"], + "Microsoft": ["microsoft"], + "Nvidia": ["nvidia"], + "01 AI": ["01-ai"], + "Allen AI": ["allenai"], + "Ai2": ["allenai"], + "AllenAI/UW": ["allenai"], + "Cohere": ["CohereForAI"], + "HuggingFace": ["HuggingFaceH4", "huggingface"], + "AI21 Labs": ["ai21labs"], + "NousResearch": ["NousResearch"], + "NexusFlow": ["Nexusflow"], + "Princeton": ["princeton-nlp"], + "IBM": ["ibm-granite"], + "InternLM": ["internlm"], + "Together AI": ["togethercomputer"], + "TII": ["tiiuae"], + "MiniMax": ["MiniMaxAI"], + "MosaicML": ["mosaicml"], + "Databricks": ["databricks"], + "Moonshot": ["moonshotai"], + "UC Berkeley": ["berkeley-nest"], + "Cognitive Computations": ["cognitivecomputations"], + "Upstage AI": ["upstage"], + "UW": ["timdettmers"], + "Snowflake": ["Snowflake"], + "LMSYS": ["lmsys"], + "OpenChat": ["openchat"], +} + + +def _normalize_arena_elo(elo: float) -> float: + """Normalize Arena ELO to a frozen-source-aware 0-_ARENA_MAX_NORMALIZED scale.""" + score = ( + (elo - _ARENA_ELO_MIN) + / (_ARENA_ELO_MAX - _ARENA_ELO_MIN) + * _ARENA_MAX_NORMALIZED + ) + return max(0.0, min(_ARENA_MAX_NORMALIZED, round(score, 1))) + + +def _arena_name_to_hf_ids(model_name: str, org: str) -> list[str]: + """Convert Arena display name to potential HuggingFace model IDs.""" + hf_orgs = _ARENA_ORG_TO_HF.get(org, []) + candidates = [] + + # Clean the model name: remove date suffixes like "(03-2025)" + clean_name = re.sub(r"\s*\([\d-]+\)\s*$", "", model_name).strip() + # Remove -bf16, -fp8 suffixes for base matching + base_name = re.sub(r"-(bf16|fp8|fp16)$", "", clean_name, flags=re.IGNORECASE) + + for hf_org in hf_orgs: + candidates.append(f"{hf_org}/{clean_name}") + if base_name != clean_name: + candidates.append(f"{hf_org}/{base_name}") + # Try with -Instruct suffix stripped for base model matching + no_instruct = re.sub(r"-Instruct$", "", clean_name) + if no_instruct != clean_name: + candidates.append(f"{hf_org}/{no_instruct}") + + return candidates + + +async def fetch_arena_scores(client: httpx.AsyncClient) -> dict[str, float]: + """Fetch Chatbot Arena ELO scores via rows API.""" + scores: dict[str, float] = {} + offset = 0 + + while True: + resp = await get_with_retries( + client, + ARENA_ROWS_URL, + params={ + "dataset": ARENA_DATASET, + "config": "default", + "split": "train", + "offset": str(offset), + "length": "100", + }, + ) + resp.raise_for_status() + data = resp.json() + rows = data.get("rows", []) + if not rows: + break + + for r in rows: + row = r.get("row", {}) + model_name = str(row.get("Model", "")) + elo = row.get("Arena Score", 0) + org = str(row.get("Organization", "")) + lic = str(row.get("License", "")) + + if not model_name or not elo or elo <= 0: + continue + # Skip proprietary models (can't run locally) + if "Proprietary" in lic or "Propretary" in lic: + continue + + normalized = _normalize_arena_elo(elo) + # Map to all potential HF IDs + hf_ids = _arena_name_to_hf_ids(model_name, org) + for hf_id in hf_ids: + scores[hf_id] = normalized + + offset += len(rows) + total = data.get("num_rows_total", 0) + if total and offset >= total: + break + + return scores diff --git a/src/whichllm/models/benchmark_sources/constants.py b/src/whichllm/models/benchmark_sources/constants.py new file mode 100644 index 0000000..1e3ba11 --- /dev/null +++ b/src/whichllm/models/benchmark_sources/constants.py @@ -0,0 +1,7 @@ +from __future__ import annotations + +import re + +_NEXT_DATA_RE = re.compile( + r'', re.DOTALL +) diff --git a/src/whichllm/models/benchmark_sources/livebench.py b/src/whichllm/models/benchmark_sources/livebench.py new file mode 100644 index 0000000..0a96239 --- /dev/null +++ b/src/whichllm/models/benchmark_sources/livebench.py @@ -0,0 +1,102 @@ +"""LiveBench (livebench.ai) source — inlined snapshot. + +LiveBench publishes its leaderboard as a dated CSV (e.g. +``https://livebench.ai/table_2026_01_08.csv``). +To refresh: download the latest ``table_YYYY_MM_DD.csv`` from livebench.ai +and run ``scripts/import_livebench_csv.py`` to regenerate the dict below. +""" + +from __future__ import annotations + +# Inlined LiveBench global-average snapshot. Values are raw 0-100; the +# normalizer below rescales them onto the project's shared 0-100 axis. +# Entries from the 2026-01-08 CSV were generated by ``scripts/import_livebench_csv.py``. +LIVEBENCH_RAW_DATA: dict[str, float] = { + # --- 2026-01-08 CSV (auto-generated; see scripts/import_livebench_csv.py) + "MiniMaxAI/MiniMax-M2.5": 60.3, + "MiniMaxAI/MiniMax-M2.7": 65.0, + "Qwen/Qwen3-235B-A22B-Instruct-2507": 48.0, + "Qwen/Qwen3-235B-A22B-Thinking-2507": 52.9, + "Qwen/Qwen3-30B-A3B-Thinking-2507": 38.8, + "Qwen/Qwen3-32B": 42.7, + "Qwen/Qwen3-Next-80B-A3B-Instruct": 47.4, + "Qwen/Qwen3-Next-80B-A3B-Thinking": 51.0, + "Qwen/Qwen3.6-27B": 65.6, + "XiaomiMiMo/MiMo-V2-Pro": 58.4, + "deepseek-ai/DeepSeek-V3.2": 63.1, + "deepseek-ai/DeepSeek-V3.2-Exp": 58.9, + "deepseek-ai/DeepSeek-V4-Flash": 67.7, + "deepseek-ai/DeepSeek-V4-Pro": 74.4, + "google/gemma-4-31b-it": 62.4, + "mistralai/Devstral-2512": 38.8, + "moonshotai/Kimi-K2-Instruct": 45.9, + "moonshotai/Kimi-K2-Thinking": 62.3, + "moonshotai/Kimi-K2.5": 69.2, + "moonshotai/Kimi-K2.6-Thinking": 72.4, + "nvidia/Nemotron-3-Super-120B-A12B": 32.0, + "openai/gpt-oss-120b": 46.4, + "zai-org/GLM-4.6": 54.7, + "zai-org/GLM-4.6V": 38.9, + "zai-org/GLM-4.7": 57.3, + "zai-org/GLM-5": 68.7, + "zai-org/GLM-5.1": 70.6, + # --- 2026-04 carryover (anchors for older / smaller-class models) + "deepseek-ai/DeepSeek-R1-0528": 71.0, + "deepseek-ai/DeepSeek-R1": 65.0, + "deepseek-ai/DeepSeek-V3-0324": 57.0, + "Qwen/Qwen3-235B-A22B": 65.0, + "Qwen/Qwen3-Coder-30B-A3B-Instruct": 58.0, + "Qwen/QwQ-32B": 57.0, + "Qwen/Qwen3-4B-Thinking-2507": 50.0, + "deepseek-ai/DeepSeek-R1-Distill-Qwen-32B": 56.0, + "deepseek-ai/DeepSeek-R1-Distill-Qwen-14B": 50.0, + "deepseek-ai/DeepSeek-R1-Distill-Llama-8B": 42.0, + "meta-llama/Llama-3.3-70B-Instruct": 48.0, + "meta-llama/Llama-4-Maverick-17B-128E-Instruct": 54.0, + "meta-llama/Llama-4-Scout-17B-16E-Instruct": 49.0, + "google/gemma-3-27b-it": 50.0, + "google/gemma-4-26b-a4b-it": 54.0, + "microsoft/phi-4": 53.0, + "mistralai/Mistral-Large-Instruct-2411": 58.0, + "mistralai/Devstral-Small-2505": 50.0, + "openai/gpt-oss-20b": 52.0, + "zai-org/GLM-4.5": 58.0, + "zai-org/GLM-4.5-Air": 52.0, + # 8B-class entries to anchor the smaller-model scoring + "Qwen/Qwen3-8B": 50.0, + "Qwen/Qwen3-14B": 56.0, + "Qwen/Qwen3-4B-Instruct-2507": 45.0, + "Qwen/Qwen3-4B": 42.0, + "Qwen/Qwen3-30B-A3B": 58.0, + "Qwen/Qwen2.5-7B-Instruct": 38.0, + "Qwen/Qwen2.5-14B-Instruct": 42.0, + "Qwen/Qwen2.5-32B-Instruct": 50.0, + "meta-llama/Llama-3.1-8B-Instruct": 36.0, + "google/gemma-2-9b-it": 38.0, + "google/gemma-3-12b-it": 44.0, + "microsoft/Phi-4-mini-instruct": 40.0, + "mistralai/Mistral-Small-3.2-24B-Instruct-2506": 50.0, + "mistralai/Mistral-Small-3.1-24B-Instruct-2503": 48.0, +} + +# LiveBench global-average tops out around 72 for current frontier models +# (DeepSeek V4 Pro 72, Kimi K2.6 71, Qwen3.6-27B 66) with 8B-class around 35. +# Anchored by a two-point fit: top frontier (72) → 95, mid/8B (35) → 30 — so +# 8B models get a meaningful but not dominant share and frontier MoE clears +# the OLLB cap. +_LB_MIN = 18.0 +_LB_MAX = 75.0 + + +def _normalize_livebench(score: float) -> float: + normalized = (score - _LB_MIN) / (_LB_MAX - _LB_MIN) * 100.0 + return max(0.0, min(100.0, round(normalized, 1))) + + +def get_livebench_data() -> dict[str, float]: + """Return the inlined LiveBench snapshot, normalized to the 0-100 scale.""" + return { + hf_id: _normalize_livebench(raw) + for hf_id, raw in LIVEBENCH_RAW_DATA.items() + if _normalize_livebench(raw) > 0 + } diff --git a/src/whichllm/models/benchmark_sources/open_llm_leaderboard.py b/src/whichllm/models/benchmark_sources/open_llm_leaderboard.py new file mode 100644 index 0000000..1c616ba --- /dev/null +++ b/src/whichllm/models/benchmark_sources/open_llm_leaderboard.py @@ -0,0 +1,99 @@ +from __future__ import annotations + +import io + +import httpx + +from whichllm.models.http import get_with_retries + +LEADERBOARD_PARQUET_URL = ( + "https://huggingface.co/api/datasets/open-llm-leaderboard/contents" + "/parquet/default/train/0.parquet" +) +LEADERBOARD_ROWS_URL = "https://datasets-server.huggingface.co/rows" +LEADERBOARD_DATASET = "open-llm-leaderboard/contents" + +# --- Leaderboard normalization --- +# OLLB v2 averages range ~5 to ~52. The leaderboard is archived 2025-06 with +# the top slot held by Qwen2.5-32B (47.6 raw = 91.5 if uncapped); capping at +# 78 prevents an older generation with a strong-but-frozen OLLB score from +# dominating rankings that now have AA Index / LiveBench coverage too. +_LB_AVG_MAX = 52 +_OLLB_MAX_NORMALIZED = 78.0 + + +async def _fetch_leaderboard_parquet(client: httpx.AsyncClient) -> dict[str, float]: + """Download Open LLM Leaderboard parquet (requires pyarrow).""" + import pyarrow.parquet as pq + + resp = await get_with_retries( + client, LEADERBOARD_PARQUET_URL, follow_redirects=True + ) + resp.raise_for_status() + table = pq.read_table( + io.BytesIO(resp.content), + columns=["fullname", "Average ⬆️"], + ) + d = table.to_pydict() + scores: dict[str, float] = {} + for i in range(len(d["fullname"])): + name = d["fullname"][i] + avg = d["Average ⬆️"][i] + if name and avg and avg > 0: + scores[name] = _normalize_leaderboard_avg(avg) + return scores + + +async def _fetch_leaderboard_api(client: httpx.AsyncClient) -> dict[str, float]: + """Fetch Open LLM Leaderboard via rows API (no pyarrow needed).""" + scores: dict[str, float] = {} + offset = 0 + + while True: + resp = await get_with_retries( + client, + LEADERBOARD_ROWS_URL, + params={ + "dataset": LEADERBOARD_DATASET, + "config": "default", + "split": "train", + "offset": str(offset), + "length": "100", + }, + ) + resp.raise_for_status() + data = resp.json() + rows = data.get("rows", []) + if not rows: + break + + for r in rows: + row = r.get("row", {}) + name = row.get("fullname") + avg = row.get("Average ⬆️") + if name and avg and avg > 0: + scores[name] = _normalize_leaderboard_avg(avg) + + offset += len(rows) + total = data.get("num_rows_total", 0) + if total and offset >= total: + break + + return scores + + +def _normalize_leaderboard_avg(avg: float) -> float: + """Normalize Open LLM Leaderboard average to 0-_OLLB_MAX_NORMALIZED scale.""" + score = avg / _LB_AVG_MAX * _OLLB_MAX_NORMALIZED + return max(0.0, min(_OLLB_MAX_NORMALIZED, round(score, 1))) + + +async def fetch_leaderboard_with_fallback( + client: httpx.AsyncClient, +) -> dict[str, float]: + """Prefer the parquet path (one request, full table) and fall back to the + paginated rows API when pyarrow is unavailable.""" + try: + return await _fetch_leaderboard_parquet(client) + except ImportError: + return await _fetch_leaderboard_api(client) diff --git a/src/whichllm/models/benchmark_sources/types.py b/src/whichllm/models/benchmark_sources/types.py new file mode 100644 index 0000000..06389f0 --- /dev/null +++ b/src/whichllm/models/benchmark_sources/types.py @@ -0,0 +1,5 @@ +from __future__ import annotations + + +class ExtractionFailed(Exception): + pass diff --git a/src/whichllm/models/benchmark_sources/utils.py b/src/whichllm/models/benchmark_sources/utils.py new file mode 100644 index 0000000..cd77a98 --- /dev/null +++ b/src/whichllm/models/benchmark_sources/utils.py @@ -0,0 +1,14 @@ +from __future__ import annotations + + +def _walk(obj, depth: int = 0): + """Yield every dict encountered while recursively walking a JSON tree.""" + if depth > 12: + return + if isinstance(obj, dict): + yield obj + for v in obj.values(): + yield from _walk(v, depth + 1) + elif isinstance(obj, list): + for item in obj: + yield from _walk(item, depth + 1) diff --git a/src/whichllm/models/benchmark_sources/vision.py b/src/whichllm/models/benchmark_sources/vision.py new file mode 100644 index 0000000..e422232 --- /dev/null +++ b/src/whichllm/models/benchmark_sources/vision.py @@ -0,0 +1,83 @@ +"""Vision-language model benchmark source. + +Text leaderboards (LiveBench, AA Index, Aider) do not score VLMs, so a +``--profile vision`` ranking had only one model with a ``direct`` hit +(Qwen2-VL-7B, a two-generations-old model) and every current VLM fell +back to size/popularity heuristics — letting the 8B legacy model +outrank Qwen3-VL-32B even on an 80 GB H100. + +There is no single stable machine-readable VLM leaderboard (the MMMU / +OpenCompass spaces change their JSON shape frequently), so this source +is a curated snapshot rather than a live scrape. Values are a blended +0-100 capability index drawn from MMMU-Pro, MMBench, and general +multimodal evaluations as of 2026-05; they are already normalized and +merged into the combined benchmark dict like any other current source. +Profile filtering keeps these scores from affecting text rankings — +only models tagged ``vision`` consume them. +""" + +from __future__ import annotations + +import logging + +import httpx + +logger = logging.getLogger(__name__) + +# Curated multimodal capability index (0-100), 2026-05 snapshot. +# Anchored so the current Qwen3-VL / InternVL3 frontier sits in the +# mid-50s-to-60s and two-generations-old 7B-class VLMs sit in the low +# 30s, which restores the correct generational ordering. +VISION_FALLBACK_2026_05: dict[str, float] = { + # Qwen3-VL (current frontier) + "Qwen/Qwen3-VL-235B-A22B-Instruct": 62.0, + "Qwen/Qwen3-VL-32B-Instruct": 57.0, + "Qwen/Qwen3-VL-30B-A3B-Instruct": 53.0, + "Qwen/Qwen3-VL-8B-Instruct": 45.0, + "Qwen/Qwen3-VL-8B-Thinking": 46.0, + "Qwen/Qwen3-VL-4B-Instruct": 37.0, + "Qwen/Qwen3-VL-4B-Thinking": 38.0, + # Qwen2.5-VL (previous generation, still strong) + "Qwen/Qwen2.5-VL-72B-Instruct": 55.0, + "Qwen/Qwen2.5-VL-32B-Instruct": 49.0, + "Qwen/Qwen2.5-VL-7B-Instruct": 41.0, + "Qwen/Qwen2.5-VL-3B-Instruct": 33.0, + # Qwen2-VL (two generations old — must rank below Qwen3-VL) + "Qwen/Qwen2-VL-72B-Instruct": 45.0, + "Qwen/Qwen2-VL-7B-Instruct": 33.0, + "Qwen/Qwen2-VL-2B-Instruct": 24.0, + # Meta Llama Vision + "meta-llama/Llama-3.2-90B-Vision-Instruct": 41.0, + "meta-llama/Llama-3.2-11B-Vision-Instruct": 29.0, + # Microsoft Phi vision + "microsoft/Phi-4-reasoning-vision-15B": 46.0, + "microsoft/Phi-3.5-vision-instruct": 27.0, + # Google Gemma 3 (natively multimodal) + "google/gemma-3-27b-it": 42.0, + "google/gemma-3-12b-it": 35.0, + "google/gemma-3-4b-it": 27.0, + # Mistral Pixtral + "mistralai/Pixtral-12B-2409": 35.0, + "mistral-community/pixtral-12b": 35.0, + # OpenGVLab InternVL3 (frontier open VLM) + "OpenGVLab/InternVL3-78B": 56.0, + "OpenGVLab/InternVL3-38B": 52.0, + "OpenGVLab/InternVL3-14B": 45.0, + "OpenGVLab/InternVL3-8B": 40.0, + "OpenGVLab/InternVL2_5-78B": 50.0, + # DeepSeek-VL2 + "deepseek-ai/deepseek-vl2": 38.0, + # zhipu / GLM vision + "zai-org/GLM-4.5V": 50.0, +} + + +async def fetch_vision_scores(client: httpx.AsyncClient) -> dict[str, float]: + """Return curated VLM capability scores. + + No stable live source exists, so this returns the frozen snapshot. + The ``client`` parameter mirrors the other source functions so the + caller can treat all sources uniformly and a live scrape can be + slotted in later without changing call sites. + """ + return dict(VISION_FALLBACK_2026_05) diff --git a/src/whichllm/models/benchmark_types.py b/src/whichllm/models/benchmark_types.py new file mode 100644 index 0000000..cbaa4b5 --- /dev/null +++ b/src/whichllm/models/benchmark_types.py @@ -0,0 +1,23 @@ +"""Shared benchmark data types.""" + +from __future__ import annotations + +from dataclasses import dataclass + + +@dataclass(frozen=True) +class BenchmarkEvidence: + """Benchmark evidence with confidence. + + source values, ordered from most trusted to least: + - "direct" : independent leaderboard / Arena ELO hit on exact id + - "variant" : suffix-stripped derivative of a direct leaderboard hit + - "base_model" : cardData.base_model pointer to a direct hit + - "line_interp" : size-aware interpolation within the same model line + - "self_reported" : evalResults reported by the uploader themselves + - "none" : no usable signal + """ + + score: float | None + confidence: float + source: str diff --git a/src/whichllm/models/cache.py b/src/whichllm/models/cache.py new file mode 100644 index 0000000..8e636ab --- /dev/null +++ b/src/whichllm/models/cache.py @@ -0,0 +1,47 @@ +"""Local JSON cache with TTL for model data.""" + +from __future__ import annotations + +import json +import logging +import time + +from whichllm.utils import _cache_dir + +logger = logging.getLogger(__name__) + +CACHE_DIR = _cache_dir() +CACHE_FILE = CACHE_DIR / "models.json" +DEFAULT_TTL_SECONDS = 6 * 3600 # 6 hours + + +def _ensure_cache_dir() -> None: + CACHE_DIR.mkdir(parents=True, exist_ok=True) + + +def load_cache() -> list[dict] | None: + """Load cached model data if valid. Returns None if expired or missing.""" + if not CACHE_FILE.exists(): + return None + + try: + data = json.loads(CACHE_FILE.read_text(encoding="utf-8")) + cached_at = data.get("cached_at", 0) + if time.time() - cached_at > DEFAULT_TTL_SECONDS: + logger.debug("Cache expired") + return None + return data.get("models", []) + except (json.JSONDecodeError, KeyError) as e: + logger.debug(f"Cache corrupted: {e}") + return None + + +def save_cache(models: list[dict]) -> None: + """Save model data to cache.""" + _ensure_cache_dir() + data = { + "cached_at": time.time(), + "models": models, + } + CACHE_FILE.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8") + logger.debug(f"Saved {len(models)} models to cache") diff --git a/src/whichllm/models/fetcher.py b/src/whichllm/models/fetcher.py new file mode 100644 index 0000000..02b285d --- /dev/null +++ b/src/whichllm/models/fetcher.py @@ -0,0 +1,111 @@ +"""Compatibility shim for HuggingFace model fetching helpers. + +The implementation is split by responsibility across ``whichllm.models``: + +- ``hf`` for HuggingFace API orchestration +- ``parser`` for API payload to ModelInfo conversion +- ``parameters`` for parameter-count and MoE normalization +- ``gguf`` for GGUF filename/variant parsing +- ``sliding_window`` for SWA metadata resolution +- ``serialization`` for cache serialization + +Existing imports from ``whichllm.models.fetcher`` are re-exported here. +""" + +from __future__ import annotations + +from whichllm.models import hf as _hf_module +from whichllm.models.gguf import ( + _estimate_gguf_size, + _extract_gguf_variants, + _extract_quant_type, +) +from whichllm.models.hf import ( + _DEFAULT_HF_ENDPOINT, + _FRONTIER_MODEL_IDS, + _hf_api_url, +) +from whichllm.models.http import get_with_retries +from whichllm.models.parameters import ( + _AUTHORITATIVE_PARAM_COUNTS, + _KNOWN_MOE_ACTIVE_PARAMS, + _KNOWN_PARAM_COUNTS, + _extract_active_size_hint_from_id, + _extract_size_hint_from_id, + _is_quantized_repo_name, + _lookup_curated_count, + _normalize_param_count, + _resolve_moe_active_params, +) +from whichllm.models.parser import ( + _extract_architecture, + _extract_hf_eval_score, + _extract_param_count, + _extract_published_at, + _is_general_eval_entry, + _normalize_eval_value, + _parse_model, +) +from whichllm.models.serialization import dicts_to_models, models_to_dicts +from whichllm.models.sliding_window import ( + _SWA_ARCH_ALIASES, + _SWA_ARCH_DEFAULTS, + _resolve_sliding_window, + _swa_arch_key, + _swa_key_from_arch, +) + + +async def fetch_models(limit: int = 300, include_vision: bool = True): + """Fetch popular models from HuggingFace Hub. + + The wrapper keeps legacy monkey-patching of ``fetcher.get_with_retries`` + working while the implementation lives in ``whichllm.models.hf``. + """ + original = _hf_module.get_with_retries + _hf_module.get_with_retries = get_with_retries + try: + return await _hf_module.fetch_models(limit=limit, include_vision=include_vision) + finally: + _hf_module.get_with_retries = original + + +async def fetch_model_published_at(model_ids: list[str]): + """Fetch published timestamps for specific model IDs.""" + return await _hf_module.fetch_model_published_at(model_ids) + + +__all__ = [ + "_AUTHORITATIVE_PARAM_COUNTS", + "_DEFAULT_HF_ENDPOINT", + "_FRONTIER_MODEL_IDS", + "_KNOWN_MOE_ACTIVE_PARAMS", + "_KNOWN_PARAM_COUNTS", + "_SWA_ARCH_ALIASES", + "_SWA_ARCH_DEFAULTS", + "_estimate_gguf_size", + "_extract_active_size_hint_from_id", + "_extract_architecture", + "_extract_gguf_variants", + "_extract_hf_eval_score", + "_extract_param_count", + "_extract_published_at", + "_extract_quant_type", + "_extract_size_hint_from_id", + "_hf_api_url", + "_is_general_eval_entry", + "_is_quantized_repo_name", + "_lookup_curated_count", + "_normalize_eval_value", + "_normalize_param_count", + "_parse_model", + "_resolve_moe_active_params", + "_resolve_sliding_window", + "_swa_arch_key", + "_swa_key_from_arch", + "dicts_to_models", + "fetch_model_published_at", + "fetch_models", + "get_with_retries", + "models_to_dicts", +] diff --git a/src/whichllm/models/gguf.py b/src/whichllm/models/gguf.py new file mode 100644 index 0000000..ae8ef89 --- /dev/null +++ b/src/whichllm/models/gguf.py @@ -0,0 +1,84 @@ +"""GGUF filename parsing and variant extraction helpers.""" + +from __future__ import annotations + +import re + +from whichllm.constants import QUANT_BYTES_PER_WEIGHT +from whichllm.models.types import GGUFVariant + +_GGUF_SPLIT_RE = re.compile(r"-(\d{5})-of-(\d{5})\.gguf$", re.IGNORECASE) + +# Filename quant tokens that name the same format under a different spelling +# than the canonical key used by QUANT_BYTES_PER_WEIGHT / QUANT_QUALITY_PENALTY. +_QUANT_ALIASES = { + "FP16": "F16", + "FP32": "F32", +} + + +def _extract_quant_type(filename: str) -> str: + """Extract quantization type from a GGUF filename. + + The returned key is canonicalized to match QUANT_BYTES_PER_WEIGHT, so + callers can look it up directly. Returns "unknown" when nothing matches. + """ + patterns = [ + r"[.-](Q\d+_K_[SMLA])", + r"[.-](Q\d+_\d+)", + r"[.-](Q\d+_K)", + r"[.-](TQ\d+_\d+)", + r"[.-](IQ\d+_\w+)", + r"[.-](MXFP4|NVFP4)", + r"[.-](F16|FP16|BF16|F32|FP32)", + ] + upper = filename.upper() + for pattern in patterns: + m = re.search(pattern, upper) + if m: + quant = m.group(1) + return _QUANT_ALIASES.get(quant, quant) + return "unknown" + + +def _estimate_gguf_size(param_count: int, quant_type: str) -> int: + """Estimate GGUF file size from parameter count and quantization type.""" + bpw = QUANT_BYTES_PER_WEIGHT.get(quant_type.upper(), 0.5625) # default Q4_K_M + return int(param_count * bpw) + + +def _extract_gguf_variants(data: dict, param_count: int) -> list[GGUFVariant]: + """Extract GGUF variants from HF sibling metadata.""" + quant_sizes: dict[str, int] = {} + quant_first_filename: dict[str, str] = {} + siblings = data.get("siblings", []) or [] + for sib in siblings: + fname = sib.get("rfilename", "") + if not fname.endswith(".gguf") or fname.startswith("."): + continue + quant = _extract_quant_type(fname) + if quant == "unknown": + continue + size = sib.get("size", 0) + if not isinstance(size, int) or size < 0: + size = 0 + + # Split GGUF files are summed into one candidate per quant. + quant_sizes[quant] = quant_sizes.get(quant, 0) + size + if quant not in quant_first_filename or _GGUF_SPLIT_RE.search( + quant_first_filename[quant] + ): + quant_first_filename[quant] = fname + + gguf_variants = [] + for quant, total_size in quant_sizes.items(): + if total_size <= 0: + total_size = _estimate_gguf_size(param_count, quant) + gguf_variants.append( + GGUFVariant( + filename=quant_first_filename[quant], + quant_type=quant, + file_size_bytes=total_size, + ) + ) + return gguf_variants diff --git a/src/whichllm/models/grouper.py b/src/whichllm/models/grouper.py new file mode 100644 index 0000000..2d7d843 --- /dev/null +++ b/src/whichllm/models/grouper.py @@ -0,0 +1,149 @@ +"""Model family grouping logic.""" + +from __future__ import annotations + +import re + +from whichllm.models.types import ModelFamily, ModelInfo + + +def _normalize_name(model_id: str) -> str: + """Normalize model ID for grouping by removing org prefix and GGUF/quant/chat suffixes.""" + name = model_id.lower() + # Strip org prefix (e.g. "bartowski/Meta-Llama-3.1" -> "meta-llama-3.1") + if "/" in name: + name = name.split("/", 1)[1] + # Strip common org prefixes in model names (e.g. "qwen_qwen3-8b" -> "qwen3-8b") + name = re.sub(r"^(qwen_|meta-llama_|google_)", "", name) + # Remove common suffixes (applied repeatedly to handle stacked suffixes) + suffixes = [ + r"-gguf$", + r"-gptq$", + r"-awq$", + r"-instruct$", + r"-chat$", + r"-it$", + r"-hf$", + r"-fp8$", + r"-fp16$", + r"-bf16$", + r"-mxfp4$", + r"-nvfp4$", + r"-\d+bit$", + r"-\d{4}$", # date suffixes like -2507, -2503 + ] + for _ in range(3): # multiple passes to strip stacked suffixes + prev = name + for suffix in suffixes: + name = re.sub(suffix, "", name) + if name == prev: + break + + # Strip version-before-size: mistral-small-3.2-24b -> mistral-small-24b + # This catches patterns like MODEL-MAJOR.MINOR-SIZEb where the version + # is a separate segment (preceded by '-') before the size suffix. + # Does NOT match qwen3.5-27b because '3.5' is glued to 'qwen' without '-'. + name = re.sub(r"-\d+\.\d+(-\d+(?:\.\d+)?b(?:-a\d+b)?)$", r"\1", name) + + # Split series name from size suffix, strip minor version from series only. + # Merges qwen3.5-27b + qwen3-30b-a3b naming variants (different sizes stay separate). + m = re.match(r"^(.+?)-(\d+(?:\.\d+)?b(?:-a\d+b)?)$", name) + if m: + series, size = m.group(1), m.group(2) + series = re.sub(r"(\d+)\.\d+$", r"\1", series) + name = f"{series}-{size}" + else: + # No size suffix (e.g. deepseek-v3.2) — strip minor version directly + name = re.sub(r"(\d+)\.\d+$", r"\1", name) + + return name + + +def group_models(models: list[ModelInfo]) -> list[ModelFamily]: + """Group models into families based on base_model and name similarity.""" + # Pass 1: Group by base_model + base_model_groups: dict[str, list[ModelInfo]] = {} + ungrouped: list[ModelInfo] = [] + + for model in models: + if model.base_model: + key = model.base_model.lower() + base_model_groups.setdefault(key, []).append(model) + else: + ungrouped.append(model) + + # Pass 2: Group ungrouped by normalized name + name_groups: dict[str, list[ModelInfo]] = {} + for model in ungrouped: + key = _normalize_name(model.id) + name_groups.setdefault(key, []).append(model) + + # Merge base_model groups that share the same normalized name + merged_base: dict[str, list[ModelInfo]] = {} + for key, group in base_model_groups.items(): + norm_key = _normalize_name(key) + merged_base.setdefault(norm_key, []).extend(group) + + # Also merge with ungrouped via name matching + for norm_key, group in list(merged_base.items()): + if norm_key in name_groups: + group.extend(name_groups.pop(norm_key)) + + # Replace base_model_groups with merged version + base_model_groups = merged_base + + # Build families + families: list[ModelFamily] = [] + + for group_key, group in list(base_model_groups.items()) + list(name_groups.items()): + if not group: + continue + + # Pick the base model. Priority order: + # 1. Models that are referenced by another group member's base_model + # field — these are upstream of the others, so they are the + # true base even when a downstream fine-tune (e.g. + # prefeitura-rio/Rio-3.0-Open-Mini) has more downloads than the + # official base (Qwen/Qwen3-4B-Thinking-2507). + # 2. Models without GGUF/quant suffixes and no base_model of their + # own (the original checkpoint). + # 3. Anything left in the group. + # Within the chosen tier, pick highest downloads as a tiebreaker. + referenced_as_base: set[str] = {m.base_model for m in group if m.base_model} + upstream_candidates = [m for m in group if m.id in referenced_as_base] + if upstream_candidates: + base_candidates = upstream_candidates + else: + base_candidates = [ + m for m in group if not m.gguf_variants or m.base_model is None + ] + if not base_candidates: + base_candidates = group + + base = max(base_candidates, key=lambda m: m.downloads) + variants = [m for m in group if m.id != base.id] + + # Set family_id on all members + family_id = _normalize_name(base.id) + base.family_id = family_id + for v in variants: + v.family_id = family_id + + # Collect best benchmark scores across family + best_bench: dict[str, float] = {} + for m in group: + for k, v in m.benchmark_scores.items(): + if k not in best_bench or v > best_bench[k]: + best_bench[k] = v + + families.append( + ModelFamily( + family_id=family_id, + display_name=base.name, + base_model=base, + variants=variants, + best_benchmark=best_bench, + ) + ) + + return families diff --git a/src/whichllm/models/hf.py b/src/whichllm/models/hf.py new file mode 100644 index 0000000..3e4ae84 --- /dev/null +++ b/src/whichllm/models/hf.py @@ -0,0 +1,293 @@ +"""HuggingFace Hub client orchestration.""" + +from __future__ import annotations + +import asyncio +import logging +import os + +import httpx + +from whichllm.models.http import DEFAULT_ACCEPT_ENCODING, get_with_retries +from whichllm.models.parser import _extract_published_at, _parse_model +from whichllm.models.types import ModelInfo + +logger = logging.getLogger(__name__) + +_DEFAULT_HF_ENDPOINT = "https://huggingface.co" + +_MODEL_EXPANDS = [ + "config", + "safetensors", + "gguf", + "cardData", + "siblings", + "evalResults", +] + +_MODEL_DETAIL_EXPANDS = [ + "config", + "safetensors", + "gguf", + "cardData", + "siblings", + "evalResults", + "downloads", + "likes", + "createdAt", + "lastModified", +] + +_FRONTIER_MODEL_IDS = ( + # Newest releases that lead 2026-Q2 benchmarks + "moonshotai/Kimi-K2-Thinking", + "moonshotai/Kimi-K2-Instruct", + "moonshotai/Kimi-K2-Instruct-0905", + "XiaomiMiMo/MiMo-V2.5-Pro", + "XiaomiMiMo/MiMo-V2.5", + "XiaomiMiMo/MiMo-V2-Flash", + "deepseek-ai/DeepSeek-V4-Pro", + "deepseek-ai/DeepSeek-V4-Flash", + "deepseek-ai/DeepSeek-V3.2", + "deepseek-ai/DeepSeek-V3.2-Exp", + "deepseek-ai/DeepSeek-V3.1", + "deepseek-ai/DeepSeek-R1-0528", + "zai-org/GLM-5.1", + "zai-org/GLM-5", + "zai-org/GLM-5-FP8", + "zai-org/GLM-5.1-FP8", + "zai-org/GLM-4.7-Flash", + "zai-org/GLM-4.6", + "zai-org/GLM-4.5", + "zai-org/GLM-4.5-Air", + # Open-weight mid-size frontier + "Qwen/Qwen3.6-27B", + "Qwen/Qwen3-32B", + "Qwen/Qwen3-14B", + "Qwen/Qwen3-8B", + "Qwen/Qwen3-Coder-30B-A3B-Instruct", + "Qwen/Qwen3-Next-80B-A3B-Instruct", + "Qwen/Qwen3-235B-A22B", + "Qwen/Qwen3-4B-Instruct-2507", + # Reasoning/thinking lines that do not auto-surface via cardinality + "Qwen/QwQ-32B", + "Qwen/Qwen3-4B-Thinking-2507", + "deepseek-ai/DeepSeek-R1", + "deepseek-ai/DeepSeek-R1-Distill-Qwen-32B", + "deepseek-ai/DeepSeek-R1-Distill-Qwen-14B", + "deepseek-ai/DeepSeek-R1-Distill-Llama-8B", + # Other current open releases + "openai/gpt-oss-120b", + "openai/gpt-oss-20b", + "google/gemma-3-27b-it", + "google/gemma-3-12b-it", + "google/gemma-4-31B-it", + "google/gemma-4-26B-A4B-it", + "meta-llama/Llama-3.3-70B-Instruct", + "meta-llama/Llama-4-Maverick-17B-128E-Instruct", + "meta-llama/Llama-4-Scout-17B-16E-Instruct", + "microsoft/phi-4", + "microsoft/Phi-4-mini-instruct", + "mistralai/Mistral-Large-Instruct-2411", + "mistralai/Mistral-Small-3.2-24B-Instruct-2506", + "mistralai/Mistral-Small-3.1-24B-Instruct-2503", + "mistralai/Devstral-Small-2505", + "mistralai/Codestral-22B-v0.1", + "MiniMaxAI/MiniMax-M2", + "MiniMaxAI/MiniMax-M2.5", + # IBM Granite latest open releases + "ibm-granite/granite-4.0-h-small", + "ibm-granite/granite-4.0-h-tiny", + "ibm-granite/granite-3.3-8b-instruct", + "ibm-granite/granite-3.3-2b-instruct", + # AllenAI Olmo-3 + "allenai/Olmo-3-7B-Instruct", + "allenai/Olmo-3-1025-7B", + # Nemotron 3 series + "nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-BF16", + "nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-BF16", +) + + +def _hf_api_url(path: str) -> str: + raw_endpoint = os.environ.get("HF_ENDPOINT") + endpoint = _DEFAULT_HF_ENDPOINT if raw_endpoint is None else raw_endpoint.strip() + if not endpoint: + raise ValueError("HF_ENDPOINT must not be empty") + if not endpoint.startswith(("http://", "https://")): + raise ValueError("HF_ENDPOINT must start with http:// or https://") + endpoint = endpoint.rstrip("/") + return f"{endpoint}/api/{path.lstrip('/')}" + + +def _model_list_params(limit: int, sort: str, filter_value: str | None = None) -> dict: + params = { + "pipeline_tag": "text-generation", + "sort": sort, + "limit": str(limit), + "expand[]": _MODEL_EXPANDS, + } + if filter_value: + params["filter"] = filter_value + return params + + +def _append_new_models( + data_list: list[dict], + models: list[ModelInfo], + seen_ids: set[str], +) -> None: + for data in data_list: + if data.get("id") not in seen_ids: + model = _parse_model(data) + if model: + models.append(model) + seen_ids.add(model.id) + + +async def _fetch_model_list( + client: httpx.AsyncClient, + params: dict, +) -> list[dict]: + resp = await get_with_retries(client, _hf_api_url("models"), params=params) + resp.raise_for_status() + return resp.json() + + +async def _fetch_frontier_models( + client: httpx.AsyncClient, + models: list[ModelInfo], + seen_ids: set[str], +) -> None: + for model_id in _FRONTIER_MODEL_IDS: + if model_id in seen_ids: + continue + try: + resp = await get_with_retries( + client, + _hf_api_url(f"models/{model_id}"), + params={"expand[]": _MODEL_DETAIL_EXPANDS}, + ) + if resp.status_code >= 400: + logger.debug( + f"Frontier fetch skipped {model_id}: HTTP {resp.status_code}" + ) + continue + data = resp.json() + except (httpx.HTTPError, ValueError) as e: + logger.debug(f"Frontier fetch failed for {model_id}: {e}") + continue + model = _parse_model(data) + if model: + models.append(model) + seen_ids.add(model.id) + + +async def fetch_models( + limit: int = 300, include_vision: bool = True +) -> list[ModelInfo]: + """Fetch popular models from HuggingFace Hub.""" + models: list[ModelInfo] = [] + + async with httpx.AsyncClient( + timeout=30.0, + follow_redirects=True, + headers={"Accept-Encoding": DEFAULT_ACCEPT_ENCODING}, + ) as client: + logger.debug(f"Fetching models from HF API (limit={limit})") + data_list = await _fetch_model_list( + client, _model_list_params(limit, sort="downloads") + ) + for data in data_list: + model = _parse_model(data) + if model: + models.append(model) + + logger.debug("Fetching GGUF models from HF API") + gguf_data_list = await _fetch_model_list( + client, _model_list_params(limit, sort="downloads", filter_value="gguf") + ) + + seen_ids = {m.id for m in models} + _append_new_models(gguf_data_list, models, seen_ids) + + logger.debug("Fetching recent GGUF models from HF API") + recent_data_list = await _fetch_model_list( + client, _model_list_params(limit, sort="lastModified", filter_value="gguf") + ) + _append_new_models(recent_data_list, models, seen_ids) + + for filter_value in (None, "gguf"): + logger.debug( + f"Fetching trending {filter_value or 'all'} models from HF API" + ) + try: + trending_data_list = await _fetch_model_list( + client, + _model_list_params( + limit, sort="trending", filter_value=filter_value + ), + ) + except (httpx.HTTPError, ValueError) as e: + logger.debug(f"Trending fetch skipped: {e}") + continue + _append_new_models(trending_data_list, models, seen_ids) + + await _fetch_frontier_models(client, models, seen_ids) + + if include_vision: + for pipeline_tag in ("image-text-to-text",): + mm_params = { + "pipeline_tag": pipeline_tag, + "sort": "downloads", + "limit": str(limit), + "expand[]": _MODEL_EXPANDS, + } + logger.debug(f"Fetching {pipeline_tag} models from HF API") + mm_data_list = await _fetch_model_list(client, mm_params) + _append_new_models(mm_data_list, models, seen_ids) + + logger.debug(f"Fetched {len(models)} models total") + return models + + +async def fetch_model_published_at(model_ids: list[str]) -> dict[str, str]: + """Fetch published timestamps for specific model IDs.""" + unique_ids = sorted({m for m in model_ids if m}) + if not unique_ids: + return {} + + async with httpx.AsyncClient( + timeout=20.0, + follow_redirects=True, + headers={"Accept-Encoding": DEFAULT_ACCEPT_ENCODING}, + ) as client: + tasks = [ + client.get( + _hf_api_url(f"models/{model_id}"), + params={"expand[]": ["createdAt", "lastModified"]}, + ) + for model_id in unique_ids + ] + responses = await asyncio.gather(*tasks, return_exceptions=True) + + result: dict[str, str] = {} + for model_id, resp in zip(unique_ids, responses, strict=False): + if isinstance(resp, Exception): + logger.debug("Failed to fetch model detail for %s: %s", model_id, resp) + continue + if resp.status_code >= 400: + logger.debug( + "Failed to fetch model detail for %s: HTTP %s", + model_id, + resp.status_code, + ) + continue + try: + data = resp.json() + except ValueError: + continue + published_at = _extract_published_at(data) + if published_at: + result[model_id] = published_at + return result diff --git a/src/whichllm/models/http.py b/src/whichllm/models/http.py new file mode 100644 index 0000000..a71b076 --- /dev/null +++ b/src/whichllm/models/http.py @@ -0,0 +1,43 @@ +from __future__ import annotations + +import asyncio +import random + +import httpx + +RETRYABLE_STATUS_CODES = {408, 429, 500, 502, 503, 504} +DEFAULT_ACCEPT_ENCODING = "gzip, deflate" + + +async def get_with_retries( + client: httpx.AsyncClient, + url: str, + *, + attempts: int = 3, + base_delay: float = 0.25, + max_delay: float = 2.0, + jitter: float = 0.1, + retry_status_codes: set[int] | None = None, + **kwargs, +) -> httpx.Response: + """GET with bounded retry/backoff for transient HTTP failures.""" + retry_codes = retry_status_codes or RETRYABLE_STATUS_CODES + last_attempt = max(1, attempts) - 1 + + for attempt in range(last_attempt + 1): + try: + response = await client.get(url, **kwargs) + except (httpx.TimeoutException, httpx.TransportError): + if attempt >= last_attempt: + raise + else: + if response.status_code not in retry_codes or attempt >= last_attempt: + return response + + delay = min(max_delay, base_delay * (2**attempt)) + if jitter > 0: + delay += random.uniform(0, jitter) + if delay > 0: + await asyncio.sleep(delay) + + raise RuntimeError("unreachable retry state") diff --git a/src/whichllm/models/parameters.py b/src/whichllm/models/parameters.py new file mode 100644 index 0000000..9dd6938 --- /dev/null +++ b/src/whichllm/models/parameters.py @@ -0,0 +1,218 @@ +"""Parameter-count and MoE metadata normalization helpers.""" + +from __future__ import annotations + +import re + + +def _extract_size_hint_from_id(model_id: str | None) -> int | None: + """Extract parameter size hint (in params) from model ID like 27B or 30B-A3B.""" + if not model_id: + return None + lower = model_id.lower() + matches = re.findall(r"(\d+(?:\.\d+)?)b(?:-a\d+(?:\.\d+)?b)?", lower) + if not matches: + return None + try: + max_b = max(float(m) for m in matches) + except ValueError: + return None + if max_b <= 0: + return None + return int(max_b * 1e9) + + +def _extract_active_size_hint_from_id(model_id: str | None) -> int | None: + """Extract MoE active parameter hint from names like 35B-A3B.""" + if not model_id: + return None + lower = model_id.lower() + matches = re.findall(r"\d+(?:\.\d+)?b[-_]?a(\d+(?:\.\d+)?)b", lower) + if not matches: + return None + try: + max_b = max(float(m) for m in matches) + except ValueError: + return None + if max_b <= 0: + return None + return int(max_b * 1e9) + + +def _is_quantized_repo_name(model_id: str) -> bool: + """Detect quantized/non-base repository naming patterns.""" + lower = model_id.lower() + return bool(re.search(r"(gptq|awq|bnb|4bit|int4|int8|fp8|gguf|quant)", lower)) + + +def _lookup_curated_count(mapping: dict[str, int], model_id: str) -> int | None: + value = mapping.get(model_id) + if value is not None: + return value + + model_id_folded = model_id.casefold() + for key, value in mapping.items(): + if key.casefold() == model_id_folded: + return value + return None + + +def _resolve_moe_active_params( + total_params: int, + *model_refs: str | None, +) -> int | None: + """Resolve active params from curated data or A*B naming hints.""" + for ref in model_refs: + if not ref: + continue + active = _lookup_curated_count(_KNOWN_MOE_ACTIVE_PARAMS, ref) + if active and active > 0: + return active + + for ref in model_refs: + active = _extract_active_size_hint_from_id(ref) + if active and active > 0 and (total_params <= 0 or active < total_params): + return active + return None + + +def _normalize_param_count( + extracted: int, + model_id: str, + base_model: str | None, +) -> int: + """Normalize parameter count when metadata is inconsistent.""" + authoritative = _lookup_curated_count(_AUTHORITATIVE_PARAM_COUNTS, model_id) + if authoritative and authoritative > 0: + return authoritative + known = _lookup_curated_count(_KNOWN_PARAM_COUNTS, model_id) + if extracted <= 0: + return known or extracted + if known and extracted < int(known * 0.35): + return known + + hints = [ + h + for h in ( + _extract_size_hint_from_id(model_id), + _extract_size_hint_from_id(base_model), + ) + if h is not None + ] + if not hints: + return extracted + + hinted = max(hints) + if _is_quantized_repo_name(model_id): + if extracted < int(hinted * 0.70): + return hinted + elif extracted < int(hinted * 0.35): + return hinted + + return extracted + + +# Curated MoE active-parameter counts. Used when HF config lacks the +# `num_local_experts` / `num_experts_per_tok` keys that whichllm reads. +# Without this, frontier MoEs are scored as dense models which over-counts +# their VRAM cost and under-counts their inference speed. +_KNOWN_MOE_ACTIVE_PARAMS: dict[str, int] = { + "meta-llama/Llama-4-Scout-17B-16E-Instruct": 17_000_000_000, + "meta-llama/Llama-4-Maverick-17B-128E-Instruct": 17_000_000_000, + "Qwen/Qwen3-Next-80B-A3B-Instruct": 3_000_000_000, + "Qwen/Qwen3-30B-A3B": 3_000_000_000, + "Qwen/Qwen3-Coder-30B-A3B-Instruct": 3_000_000_000, + "Qwen/Qwen3-235B-A22B": 22_000_000_000, + "Qwen/Qwen3.5-397B-A17B": 17_000_000_000, + "deepseek-ai/DeepSeek-V3": 37_000_000_000, + "deepseek-ai/DeepSeek-V3-0324": 37_000_000_000, + "deepseek-ai/DeepSeek-V3.1": 37_000_000_000, + "deepseek-ai/DeepSeek-V3.2": 37_000_000_000, + "deepseek-ai/DeepSeek-V3.2-Exp": 37_000_000_000, + "deepseek-ai/DeepSeek-R1": 37_000_000_000, + "deepseek-ai/DeepSeek-R1-0528": 37_000_000_000, + "deepseek-ai/DeepSeek-V4-Pro": 49_000_000_000, + "deepseek-ai/DeepSeek-V4-Flash": 13_000_000_000, + "zai-org/GLM-4.5": 32_000_000_000, + "zai-org/GLM-4.5-Air": 12_000_000_000, + "zai-org/GLM-4.6": 32_000_000_000, + "zai-org/GLM-4.7": 32_000_000_000, + "zai-org/GLM-4.7-Flash": 12_000_000_000, + "zai-org/GLM-5": 40_000_000_000, + "zai-org/GLM-5-FP8": 40_000_000_000, + "zai-org/GLM-5.1": 40_000_000_000, + "zai-org/GLM-5.1-FP8": 40_000_000_000, + "moonshotai/Kimi-K2-Instruct": 32_000_000_000, + "moonshotai/Kimi-K2-Thinking": 32_000_000_000, + "MiniMaxAI/MiniMax-M2": 10_000_000_000, + "MiniMaxAI/MiniMax-M2.5": 10_000_000_000, + "XiaomiMiMo/MiMo-V2.5": 15_000_000_000, + "XiaomiMiMo/MiMo-V2.5-Pro": 42_000_000_000, + "XiaomiMiMo/MiMo-V2-Flash": 15_000_000_000, + "google/gemma-4-26B-A4B-it": 3_800_000_000, + "nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-BF16": 3_000_000_000, + "nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-FP8": 3_000_000_000, + "nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-BF16": 12_000_000_000, + # OpenAI gpt-oss MoE family - 5B active for 20b/120b. + "openai/gpt-oss-20b": 3_600_000_000, + "openai/gpt-oss-120b": 5_100_000_000, +} + +# Hardcoded parameter counts for frontier models that HF's API leaves with +# missing safetensors/gguf/config metadata. Used as a last-resort fallback +# inside _extract_param_count so these models still enter the cache and become +# rankable. Maintain only entries that lack a size hint in the model ID itself. +_KNOWN_PARAM_COUNTS: dict[str, int] = { + "microsoft/phi-4": 14_700_000_000, + "microsoft/Phi-4-mini-instruct": 3_800_000_000, + "microsoft/Phi-4-multimodal-instruct": 5_600_000_000, + "microsoft/Phi-4-reasoning": 14_700_000_000, + "microsoft/Phi-4-reasoning-plus": 14_700_000_000, + "openai/gpt-oss-20b": 20_000_000_000, + "openai/gpt-oss-120b": 120_000_000_000, + # IBM Granite 4.0 family + "ibm-granite/granite-4.0-h-small": 32_000_000_000, + "ibm-granite/granite-4.0-h-tiny": 7_000_000_000, + "ibm-granite/granite-3.3-8b-instruct": 8_000_000_000, + "ibm-granite/granite-3.3-2b-instruct": 2_000_000_000, + # AllenAI Olmo-3 + "allenai/Olmo-3-7B-Instruct": 7_000_000_000, + "allenai/Olmo-3-1025-7B": 7_000_000_000, + # Llama 4 MoE totals: repo names advertise active size, but the total + # weight footprint is much larger. + "meta-llama/Llama-4-Scout-17B-16E-Instruct": 109_000_000_000, + "meta-llama/Llama-4-Maverick-17B-128E-Instruct": 400_000_000_000, + "deepseek-ai/DeepSeek-R1": 671_000_000_000, + "deepseek-ai/DeepSeek-R1-0528": 671_000_000_000, + "deepseek-ai/DeepSeek-V3": 671_000_000_000, + "deepseek-ai/DeepSeek-V3-0324": 671_000_000_000, + "deepseek-ai/DeepSeek-V3.1": 671_000_000_000, + "deepseek-ai/DeepSeek-V3.2": 685_000_000_000, + "deepseek-ai/DeepSeek-V4-Pro": 1_600_000_000_000, + "deepseek-ai/DeepSeek-V4-Flash": 284_000_000_000, + "moonshotai/Kimi-K2-Instruct": 1_026_000_000_000, + "moonshotai/Kimi-K2-Thinking": 1_026_000_000_000, + "XiaomiMiMo/MiMo-V2.5": 310_000_000_000, + "XiaomiMiMo/MiMo-V2.5-Pro": 1_020_000_000_000, + "XiaomiMiMo/MiMo-V2-Flash": 309_000_000_000, + "zai-org/GLM-4.5": 355_000_000_000, + "zai-org/GLM-4.5-Air": 106_000_000_000, + "zai-org/GLM-4.6": 355_000_000_000, + "zai-org/GLM-4.7": 355_000_000_000, + "zai-org/GLM-4.7-Flash": 30_000_000_000, + "zai-org/GLM-5": 744_000_000_000, + "zai-org/GLM-5-FP8": 744_000_000_000, + "zai-org/GLM-5.1": 744_000_000_000, + "zai-org/GLM-5.1-FP8": 744_000_000_000, + "MiniMaxAI/MiniMax-M2": 230_000_000_000, + "MiniMaxAI/MiniMax-M2.5": 230_000_000_000, + "stepfun-ai/Step-3.5-Flash": 30_000_000_000, +} + +# Curated counts that should win even when the HF API exposes safetensors +# metadata. Some mixed-precision MoEs publish compressed checkpoint tensor +# counts that understate the model-card capacity used for ranking and planning. +_AUTHORITATIVE_PARAM_COUNTS: dict[str, int] = { + "deepseek-ai/DeepSeek-V4-Pro": 1_600_000_000_000, + "deepseek-ai/DeepSeek-V4-Flash": 284_000_000_000, +} diff --git a/src/whichllm/models/parser.py b/src/whichllm/models/parser.py new file mode 100644 index 0000000..feb2dfc --- /dev/null +++ b/src/whichllm/models/parser.py @@ -0,0 +1,295 @@ +"""Parse HuggingFace API model payloads into ModelInfo objects.""" + +from __future__ import annotations + +import statistics + +from whichllm.models.gguf import _extract_gguf_variants +from whichllm.models.parameters import ( + _AUTHORITATIVE_PARAM_COUNTS, + _KNOWN_PARAM_COUNTS, + _extract_size_hint_from_id, + _lookup_curated_count, + _normalize_param_count, + _resolve_moe_active_params, +) +from whichllm.models.sliding_window import _resolve_sliding_window +from whichllm.models.types import ModelInfo + +_GENERAL_EVAL_KEYWORDS = ( + "mmlu", + "gpqa", + "gsm8k", + "hellaswag", + "arc", + "bbh", + "ifeval", + "truthfulqa", + "ceval", + "cmmlu", +) + + +def _extract_published_at(data: dict) -> str | None: + """Extract the best published timestamp candidate from an API response.""" + created = data.get("createdAt") + if isinstance(created, str) and created: + return created + modified = data.get("lastModified") + if isinstance(modified, str) and modified: + return modified + return None + + +def _normalize_eval_value(raw: object) -> float | None: + """Convert eval value to a comparable 0-100 score.""" + if not isinstance(raw, (int, float)): + return None + value = float(raw) + if value <= 0: + return None + if value <= 1.0: + value *= 100.0 + if value > 100.0: + return None + return value + + +def _is_general_eval_entry(entry: dict) -> bool: + """Keep eval entries that are broadly useful for general chat quality.""" + data = entry.get("data") + if not isinstance(data, dict): + return False + + notes = str(data.get("notes", "")).lower() + if "with tools" in notes: + return False + + dataset = data.get("dataset") + dataset_id = "" + task_id = "" + if isinstance(dataset, dict): + dataset_id = str(dataset.get("id", "")).lower() + task_id = str(dataset.get("task_id", "")).lower() + filename = str(entry.get("filename", "")).lower() + + return any( + k in dataset_id or k in task_id or k in filename for k in _GENERAL_EVAL_KEYWORDS + ) + + +def _extract_hf_eval_score(data: dict) -> float | None: + """Extract conservative aggregate score from HF evalResults.""" + eval_results = data.get("evalResults") + if not isinstance(eval_results, list) or not eval_results: + return None + + values: list[float] = [] + for entry in eval_results: + if not isinstance(entry, dict): + continue + if not _is_general_eval_entry(entry): + continue + data_obj = entry.get("data") + if not isinstance(data_obj, dict): + continue + normalized = _normalize_eval_value(data_obj.get("value")) + if normalized is not None: + values.append(normalized) + + if not values: + return None + return round(statistics.median(values), 1) + + +def _extract_param_count(model_data: dict) -> int: + """Extract parameter count from model data. + + Resolution order: + 1. authoritative model-card overrides for known mixed-precision MoEs + 2. safetensors metadata + 3. gguf metadata + 4. config estimate + 5. curated known counts + 6. name-based size hint + + Returns 0 if none of the above succeed. + """ + model_id = model_data.get("id", "") or "" + authoritative = _lookup_curated_count(_AUTHORITATIVE_PARAM_COUNTS, model_id) + if authoritative and authoritative > 0: + return authoritative + + safetensors = model_data.get("safetensors") + if safetensors and isinstance(safetensors, dict): + params = safetensors.get("total") + if params: + return int(params) + parameters = safetensors.get("parameters") + if isinstance(parameters, dict): + total = sum(parameters.values()) + if total > 0: + return total + + gguf_meta = model_data.get("gguf", {}) or {} + if isinstance(gguf_meta, dict): + total = gguf_meta.get("total") + if total and total > 0: + return int(total) + + config = model_data.get("config", {}) or {} + hidden = config.get("hidden_size", 0) + layers = config.get("num_hidden_layers", 0) + vocab = config.get("vocab_size", 0) + if hidden and layers and vocab: + return 12 * layers * hidden * hidden + vocab * hidden * 2 + + known = _lookup_curated_count(_KNOWN_PARAM_COUNTS, model_id) + if known and known > 0: + return known + name_hint = _extract_size_hint_from_id(model_id) + if name_hint and name_hint > 0: + return name_hint + + return 0 + + +def _extract_architecture(config: dict) -> str: + """Extract architecture string from config.""" + arch_list = config.get("architectures", []) + if arch_list: + arch = arch_list[0].lower() + for name in [ + "llama", + "qwen2", + "mistral", + "mixtral", + "gemma", + "phi", + "starcoder", + "command", + "deepseek", + ]: + if name in arch: + return name + return arch.replace("forcausallm", "").replace("forconditionalgeneration", "") + model_type = config.get("model_type", "") + return model_type.lower() + + +def _extract_base_model(card_data: dict) -> str | None: + base_model_raw = card_data.get("base_model") + if isinstance(base_model_raw, str): + return base_model_raw + if isinstance(base_model_raw, list) and base_model_raw: + return base_model_raw[0] + return None + + +def _resolve_active_params( + config: dict, + param_count: int, + model_id: str, + base_model: str | None, +) -> tuple[bool, int | None]: + num_experts = 0 + for k in ( + "num_local_experts", + "num_experts", + "n_routed_experts", + "moe_num_experts", + "num_moe_experts", + "n_local_experts", + ): + v = config.get(k, 0) + if isinstance(v, int) and v > num_experts: + num_experts = v + + experts_per_tok = 0 + for k in ( + "num_experts_per_tok", + "moe_topk", + "moe_top_k", + "num_experts_per_token", + "top_k", + ): + v = config.get(k, 0) + if isinstance(v, int) and v > experts_per_tok: + experts_per_tok = v + + known_moe_active = _resolve_moe_active_params(param_count, model_id, base_model) + is_moe = num_experts > 0 or known_moe_active is not None + active_params = None + if is_moe: + if known_moe_active is not None: + active_params = known_moe_active + elif num_experts > 0: + ept = experts_per_tok if experts_per_tok > 0 else 2 + active_ratio = ept / num_experts + expert_fraction = 0.6 + active_params = int( + param_count * (1 - expert_fraction + expert_fraction * active_ratio) + ) + return is_moe, active_params + + +def _parse_model(data: dict) -> ModelInfo | None: + """Parse HF API response into ModelInfo.""" + model_id = data.get("id", "") + if not model_id: + return None + + config = data.get("config", {}) or {} + card_data = data.get("cardData", {}) or {} + base_model = _extract_base_model(card_data) + + param_count = _extract_param_count(data) + param_count = _normalize_param_count(param_count, model_id, base_model) + if param_count == 0: + return None + + is_moe, active_params = _resolve_active_params( + config, param_count, model_id, base_model + ) + gguf_variants = _extract_gguf_variants(data, param_count) + + architecture = _extract_architecture(config) + gguf_meta = data.get("gguf", {}) or {} + if not architecture and isinstance(gguf_meta, dict): + architecture = gguf_meta.get("architecture", "") + + context_length = config.get("max_position_embeddings") or config.get( + "max_sequence_length" + ) + if not context_length and isinstance(gguf_meta, dict): + context_length = gguf_meta.get("context_length") + + gguf_arch = gguf_meta.get("architecture") if isinstance(gguf_meta, dict) else None + sliding_window, swa_global_ratio = _resolve_sliding_window( + config, model_id, gguf_arch + ) + + benchmark_scores: dict[str, float] = {} + eval_score = _extract_hf_eval_score(data) + if eval_score is not None: + benchmark_scores["hf_eval"] = eval_score + + return ModelInfo( + id=model_id, + family_id=model_id, + name=model_id.split("/")[-1], + parameter_count=param_count, + parameter_count_active=active_params, + architecture=architecture, + is_moe=is_moe, + context_length=context_length, + license=card_data.get("license"), + published_at=_extract_published_at(data), + downloads=data.get("downloads", 0), + likes=data.get("likes", 0), + gguf_variants=gguf_variants, + benchmark_scores=benchmark_scores, + base_model=base_model, + sliding_window=sliding_window, + sliding_window_global_ratio=swa_global_ratio, + ) diff --git a/src/whichllm/models/serialization.py b/src/whichllm/models/serialization.py new file mode 100644 index 0000000..dd4c103 --- /dev/null +++ b/src/whichllm/models/serialization.py @@ -0,0 +1,94 @@ +"""ModelInfo cache serialization helpers.""" + +from __future__ import annotations + +from whichllm.models.parameters import ( + _normalize_param_count, + _resolve_moe_active_params, +) +from whichllm.models.types import GGUFVariant, ModelInfo + + +def models_to_dicts(models: list[ModelInfo]) -> list[dict]: + """Serialize models to dicts for caching.""" + result = [] + for m in models: + result.append( + { + "id": m.id, + "family_id": m.family_id, + "name": m.name, + "parameter_count": m.parameter_count, + "parameter_count_active": m.parameter_count_active, + "architecture": m.architecture, + "is_moe": m.is_moe, + "context_length": m.context_length, + "license": m.license, + "published_at": m.published_at, + "downloads": m.downloads, + "likes": m.likes, + "gguf_variants": [ + { + "filename": v.filename, + "quant_type": v.quant_type, + "file_size_bytes": v.file_size_bytes, + } + for v in m.gguf_variants + ], + "benchmark_scores": m.benchmark_scores, + "base_model": m.base_model, + "sliding_window": m.sliding_window, + "sliding_window_global_ratio": m.sliding_window_global_ratio, + } + ) + return result + + +def dicts_to_models(data: list[dict]) -> list[ModelInfo]: + """Deserialize models from cached dicts.""" + models = [] + for d in data: + base_model = d.get("base_model") + param_count = _normalize_param_count( + d["parameter_count"], + d["id"], + base_model, + ) + active_params = _resolve_moe_active_params( + param_count, + d["id"], + base_model, + d.get("name"), + d.get("architecture"), + ) + if active_params is None: + active_params = d.get("parameter_count_active") + models.append( + ModelInfo( + id=d["id"], + family_id=d.get("family_id", d["id"]), + name=d["name"], + parameter_count=param_count, + parameter_count_active=active_params, + architecture=d.get("architecture", ""), + is_moe=d.get("is_moe", False) or active_params is not None, + context_length=d.get("context_length"), + license=d.get("license"), + published_at=d.get("published_at"), + downloads=d.get("downloads", 0), + likes=d.get("likes", 0), + gguf_variants=[ + GGUFVariant( + filename=v["filename"], + quant_type=v["quant_type"], + file_size_bytes=v["file_size_bytes"], + ) + for v in d.get("gguf_variants", []) + ], + benchmark_scores=d.get("benchmark_scores", {}), + base_model=base_model, + sliding_window=d.get("sliding_window"), + sliding_window_global_ratio=d.get("sliding_window_global_ratio"), + ) + ) + return models diff --git a/src/whichllm/models/sliding_window.py b/src/whichllm/models/sliding_window.py new file mode 100644 index 0000000..3c7094e --- /dev/null +++ b/src/whichllm/models/sliding_window.py @@ -0,0 +1,95 @@ +"""Sliding-window-attention metadata resolution.""" + +from __future__ import annotations + + +# Sliding-window-attention (SWA) registry. We only model SWA KV-cache savings +# for architectures whose mainline runtimes actually honor interleaved SWA +# (llama.cpp's ISWA path, MLX). Each entry is (default_window_tokens, +# global_layer_ratio) where the ratio is the fraction of layers that use full +# (global) attention. Models outside this allowlist keep full-context KV so +# estimates stay conservative. +_SWA_ARCH_DEFAULTS: dict[str, tuple[int, float]] = { + "gemma2": (4096, 0.5), + "gemma3": (1024, 1.0 / 6.0), + "gpt_oss": (128, 0.5), + "cohere2": (4096, 0.25), +} + +# Map the many spellings an arch string can take (HF model_type, the +# ForCausalLM/ForConditionalGeneration class prefix, and GGUF metadata) onto a +# canonical key in _SWA_ARCH_DEFAULTS. +_SWA_ARCH_ALIASES: dict[str, str] = { + "gemma2": "gemma2", + "gemma2_text": "gemma2", + "gemma3": "gemma3", + "gemma3_text": "gemma3", + "gpt_oss": "gpt_oss", + "gptoss": "gpt_oss", + "cohere2": "cohere2", +} + + +def _swa_key_from_arch(arch: str | None) -> str | None: + """Resolve an arch string (model_type / class / gguf metadata) to a key.""" + if not arch: + return None + arch = arch.lower() + if arch in _SWA_ARCH_ALIASES: + return _SWA_ARCH_ALIASES[arch] + stripped = arch.replace("forcausallm", "").replace("forconditionalgeneration", "") + if stripped in _SWA_ARCH_ALIASES: + return _SWA_ARCH_ALIASES[stripped] + return None + + +def _swa_arch_key(config: dict, model_id: str, gguf_arch: str | None) -> str | None: + """Identify the SWA architecture key for a model, or None if not honored. + + Relies on authoritative metadata only: raw HF config model_type / + architectures and GGUF metadata architecture. When none are present the + model is left unhonored (full-context estimate), since a false positive + would under-count VRAM. + """ + model_type = config.get("model_type") + key = _swa_key_from_arch(model_type if isinstance(model_type, str) else None) + if key: + return key + + arch_list = config.get("architectures") or [] + if arch_list and isinstance(arch_list[0], str): + key = _swa_key_from_arch(arch_list[0]) + if key: + return key + + return _swa_key_from_arch(gguf_arch) + + +def _resolve_sliding_window( + config: dict, model_id: str, gguf_arch: str | None = None +) -> tuple[int | None, float | None]: + """Resolve (sliding_window, global_ratio) for honored SWA architectures. + + Returns (None, None) for every model outside the allowlist so the KV + estimate stays at full context (conservative). + """ + if config.get("use_sliding_window") is False: + return None, None + + key = _swa_arch_key(config, model_id, gguf_arch) + if key is None: + return None, None + + default_window, default_ratio = _SWA_ARCH_DEFAULTS[key] + + window = config.get("sliding_window") + if not isinstance(window, int) or window <= 0: + window = default_window + + pattern = config.get("sliding_window_pattern") + if isinstance(pattern, int) and pattern > 0: + global_ratio = 1.0 / pattern + else: + global_ratio = default_ratio + + return window, global_ratio diff --git a/src/whichllm/models/types.py b/src/whichllm/models/types.py new file mode 100644 index 0000000..56abcba --- /dev/null +++ b/src/whichllm/models/types.py @@ -0,0 +1,46 @@ +from __future__ import annotations + +from dataclasses import dataclass, field + + +@dataclass +class GGUFVariant: + filename: str + quant_type: str # "Q4_K_M", "Q8_0" etc + file_size_bytes: int + + +@dataclass +class ModelInfo: + id: str # HF repo ID + family_id: str # grouping key + name: str + parameter_count: int # total parameters + parameter_count_active: int | None = None # MoE active params + architecture: str = "" # "llama", "qwen2", "mixtral" etc + is_moe: bool = False + context_length: int | None = None + license: str | None = None + published_at: str | None = None + downloads: int = 0 + likes: int = 0 + gguf_variants: list[GGUFVariant] = field(default_factory=list) + benchmark_scores: dict[str, float] = field(default_factory=dict) + base_model: str | None = None # cardData.base_model + # Sliding-window-attention KV-cache modeling. Only populated for + # architectures whose mainline runtimes actually honor interleaved SWA + # (Gemma-2/3, gpt-oss, Cohere2); left None otherwise so VRAM estimates + # stay conservative. sliding_window is the local-attention window in + # tokens; sliding_window_global_ratio is the fraction of layers that use + # full (global) attention (0.0 = pure SWA, 1.0 = fully dense). + sliding_window: int | None = None + sliding_window_global_ratio: float | None = None + + +@dataclass +class ModelFamily: + family_id: str + display_name: str + base_model: ModelInfo + variants: list[ModelInfo] = field(default_factory=list) + best_benchmark: dict[str, float] = field(default_factory=dict) diff --git a/src/whichllm/output/__init__.py b/src/whichllm/output/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/whichllm/output/_console.py b/src/whichllm/output/_console.py new file mode 100644 index 0000000..4afc1f4 --- /dev/null +++ b/src/whichllm/output/_console.py @@ -0,0 +1,11 @@ +"""Canonical Rich Console instance shared by every output surface. + +Tests patch the ``console`` attribute on this module to capture output +(e.g. ``whichllm.output._console.console = Console(file=buf, ...)``). +Surface modules look up the console via this module so the patch +propagates without each module holding its own binding. +""" + +from rich.console import Console + +console = Console() diff --git a/src/whichllm/output/display.py b/src/whichllm/output/display.py new file mode 100644 index 0000000..36be9d1 --- /dev/null +++ b/src/whichllm/output/display.py @@ -0,0 +1,40 @@ +"""Compatibility shim: per-surface output modules now live alongside this file. + +This module re-exports the public ``display_*`` functions so existing imports +(``from whichllm.output.display import display_ranking``) keep working. New +code should import from the specific submodule: + +- ``whichllm.output.ranking`` for ranking + hardware tables +- ``whichllm.output.plan`` for the plan command +- ``whichllm.output.upgrade`` for the upgrade comparison +- ``whichllm.output.json_output`` for machine-readable JSON output +- ``whichllm.output.formatting`` for shared byte/param/date/color helpers +- ``whichllm.output._console`` for the shared Rich ``Console`` instance + +The shared ``console`` symbol is re-exported here for read access. Code that +needs to *replace* the console (e.g. test capture) should set +``whichllm.output._console.console`` so every surface picks up the change. +""" + +from whichllm.output._console import console +from whichllm.output.json_output import ( + display_json, + display_plan_json, + display_upgrade_json, +) +from whichllm.output.markdown import display_markdown +from whichllm.output.plan import display_plan +from whichllm.output.ranking import display_hardware, display_ranking +from whichllm.output.upgrade import display_upgrade + +__all__ = [ + "console", + "display_hardware", + "display_json", + "display_markdown", + "display_plan", + "display_plan_json", + "display_ranking", + "display_upgrade", + "display_upgrade_json", +] diff --git a/src/whichllm/output/formatting.py b/src/whichllm/output/formatting.py new file mode 100644 index 0000000..1444bbb --- /dev/null +++ b/src/whichllm/output/formatting.py @@ -0,0 +1,111 @@ +"""Shared low-level helpers: byte/param/date formatters and color blending.""" + +from __future__ import annotations + +from datetime import datetime +from math import log10 + +from whichllm.engine.types import CompatibilityResult + + +def _format_bytes(b: int) -> str: + """Format bytes as human-readable string.""" + if b >= 1024**3: + return f"{b / 1024**3:.1f} GB" + elif b >= 1024**2: + return f"{b / 1024**2:.0f} MB" + return f"{b / 1024:.0f} KB" + + +def _format_params(count: int) -> str: + """Format parameter count.""" + if count >= 1e9: + return f"{count / 1e9:.1f}B" + elif count >= 1e6: + return f"{count / 1e6:.0f}M" + return str(count) + + +def _format_downloads(downloads: int) -> str: + """Format download count for compact table display.""" + if downloads >= 1_000_000: + return f"{downloads / 1_000_000:.1f}M" + if downloads >= 1_000: + return f"{downloads / 1_000:.1f}K" + return str(downloads) + + +def _format_published_at(value: str | None) -> str: + """Format published datetime into YYYY-MM-DD.""" + if not value: + return "—" + try: + dt = datetime.fromisoformat(value.replace("Z", "+00:00")) + return dt.strftime("%Y-%m-%d") + except ValueError: + return value[:10] if len(value) >= 10 else value + + +def _format_speed(result: CompatibilityResult) -> str: + speed = result.estimated_tok_per_sec + if speed is None: + return "[grey50]N/A[/]" + base = f"{speed:.1f} tok/s" + if speed < 4.0: + style = "red" + elif speed < 10.0: + style = "yellow" + elif speed < 30.0: + style = "green" + else: + style = "bright_green" + + marker = "" + if result.speed_confidence == "low": + marker = " ?" + elif result.speed_confidence == "medium": + marker = " ~" + return f"[{style}]{base}{marker}[/{style}]" + + +def _parse_published_at(value: str | None) -> datetime | None: + if not value: + return None + try: + return datetime.fromisoformat(value.replace("Z", "+00:00")) + except ValueError: + return None + + +def _lerp_channel(a: int, b: int, t: float) -> int: + return int(a + (b - a) * t) + + +def _blend_hex(a: tuple[int, int, int], b: tuple[int, int, int], t: float) -> str: + t = max(0.0, min(1.0, t)) + r = _lerp_channel(a[0], b[0], t) + g = _lerp_channel(a[1], b[1], t) + bch = _lerp_channel(a[2], b[2], t) + return f"#{r:02x}{g:02x}{bch:02x}" + + +def _downloads_style(downloads: int, min_log: float, max_log: float) -> str: + if downloads <= 0: + return "grey50" + dlog = log10(max(downloads, 1)) + span = max(max_log - min_log, 1e-6) + t = (dlog - min_log) / span + return _blend_hex((145, 80, 80), (55, 190, 120), t) + + +def _published_style( + published: datetime | None, + oldest_ts: float | None, + newest_ts: float | None, +) -> str: + if published is None or oldest_ts is None or newest_ts is None: + return "grey50" + pts = published.timestamp() + span = max(newest_ts - oldest_ts, 1e-6) + t = (pts - oldest_ts) / span + return _blend_hex((190, 85, 85), (80, 190, 110), t) diff --git a/src/whichllm/output/json_output.py b/src/whichllm/output/json_output.py new file mode 100644 index 0000000..1c91d6c --- /dev/null +++ b/src/whichllm/output/json_output.py @@ -0,0 +1,210 @@ +"""Machine-readable JSON output for ranking, plan, and upgrade surfaces.""" + +from __future__ import annotations + +import json + +from whichllm.engine.quantization import effective_quant_type, estimate_weight_bytes +from whichllm.engine.types import CompatibilityResult +from whichllm.hardware.types import HardwareInfo +from whichllm.models.types import GGUFVariant, ModelInfo +from whichllm.output import _console +from whichllm.output.upgrade import _summarize_row + + +def display_json(results: list[CompatibilityResult], hardware: HardwareInfo) -> None: + """Output ranking results as JSON.""" + output = { + "hardware": { + "gpus": [ + { + "name": g.name, + "vendor": g.vendor, + "vram_bytes": g.vram_bytes, + "usable_vram_bytes": g.usable_vram_bytes, + "memory_bandwidth_gbps": g.memory_bandwidth_gbps, + "shared_memory": g.shared_memory, + } + for g in hardware.gpus + ], + "cpu": hardware.cpu_name, + "cpu_cores": hardware.cpu_cores, + "ram_bytes": hardware.ram_bytes, + "ram_budget_bytes": hardware.ram_budget_bytes, + "budget_notes": hardware.budget_notes, + "os": hardware.os, + }, + "models": [ + { + "rank": i, + "model_id": r.model.id, + "artifact_repo_id": r.artifact_model.id if r.artifact_model else None, + "artifact_filename": ( + r.artifact_variant.filename if r.artifact_variant else None + ), + "parameter_count": r.model.parameter_count, + "published_at": r.model.published_at, + "downloads": r.model.downloads, + "quant_type": effective_quant_type(r.model, r.gguf_variant), + "file_size_bytes": ( + r.gguf_variant.file_size_bytes + if r.gguf_variant + else estimate_weight_bytes(r.model, None) + ), + "vram_required_bytes": r.vram_required_bytes, + "vram_available_bytes": r.vram_available_bytes, + "uses_multi_gpu": r.uses_multi_gpu, + "multi_gpu_effective_vram_bytes": r.multi_gpu_effective_vram_bytes, + "estimated_tok_per_sec": r.estimated_tok_per_sec, + "speed_confidence": r.speed_confidence, + "speed_range_tok_per_sec": ( + list(r.speed_range_tok_per_sec) + if r.speed_range_tok_per_sec + else None + ), + "speed_notes": r.speed_notes, + "quality_score": round(r.quality_score, 2), + "benchmark_status": r.benchmark_status, + "benchmark_source": r.benchmark_source, + "benchmark_confidence": round(r.benchmark_confidence, 2), + "fit_type": r.fit_type, + "can_run": r.can_run, + "warnings": r.warnings, + "license": r.model.license, + } + for i, r in enumerate(results, 1) + ], + } + _console.console.print_json(json.dumps(output, ensure_ascii=False)) + + +def display_plan_json( + model: ModelInfo, + context_length: int, + target_quant: str, +) -> None: + """Output plan results as JSON.""" + from whichllm.constants import ( + GPU_BANDWIDTH, + QUANT_BYTES_PER_WEIGHT, + QUANT_QUALITY_PENALTY, + ) + from whichllm.engine.performance import estimate_tok_per_sec + from whichllm.engine.vram import estimate_vram + from whichllm.hardware.types import GPUInfo + + _GiB = 1024**3 + + quant_levels = ["Q2_K", "Q3_K_M", "Q4_K_M", "Q5_K_M", "Q6_K", "Q8_0", "F16"] + vram_by_quant = {} + for qt in quant_levels: + bpw = QUANT_BYTES_PER_WEIGHT.get(qt) + if bpw is None: + continue + fake_size = int(model.parameter_count * bpw) + fake_variant = GGUFVariant( + filename="", quant_type=qt, file_size_bytes=fake_size + ) + vram_bytes = estimate_vram(model, fake_variant, context_length) + vram_by_quant[qt] = { + "vram_bytes": vram_bytes, + "quality_loss": QUANT_QUALITY_PENALTY.get(qt, 0.0), + } + + target_vram = vram_by_quant.get(target_quant.upper(), {}).get("vram_bytes", 0) + if target_vram == 0: + bpw = QUANT_BYTES_PER_WEIGHT.get(target_quant.upper(), 0.5625) + fake_size = int(model.parameter_count * bpw) + fake_variant = GGUFVariant( + filename="", quant_type=target_quant, file_size_bytes=fake_size + ) + target_vram = estimate_vram(model, fake_variant, context_length) + + _PLAN_GPUS: list[tuple[str, int]] = [ + ("RTX 4060", 8), + ("RTX 3060", 12), + ("RTX 4070", 12), + ("RTX 4080", 16), + ("RTX 4090", 24), + ("RX 7900 XTX", 24), + ("RTX 5090", 32), + ("A100 40GB", 40), + ("L40S", 48), + ("A100 80GB", 80), + ("H100", 80), + ("H200", 141), + ] + + bpw = QUANT_BYTES_PER_WEIGHT.get(target_quant.upper(), 0.5625) + fake_size = int(model.parameter_count * bpw) + fake_variant = GGUFVariant( + filename="", quant_type=target_quant, file_size_bytes=fake_size + ) + + gpus = [] + for gpu_name, vram_gb in _PLAN_GPUS: + vram_bytes = int(vram_gb * _GiB) + bandwidth = GPU_BANDWIDTH.get(gpu_name) + gpu_info = GPUInfo( + name=gpu_name, + vendor="nvidia", + vram_bytes=vram_bytes, + memory_bandwidth_gbps=bandwidth, + ) + if vram_bytes >= target_vram: + fit_type = "full_gpu" + elif vram_bytes >= target_vram * 0.4: + fit_type = "partial_offload" + else: + fit_type = "too_small" + + speed = None + if fit_type != "too_small" and bandwidth: + speed = round( + estimate_tok_per_sec(model, fake_variant, gpu_info, fit_type), 1 + ) + + gpus.append( + { + "name": gpu_name, + "vram_gb": vram_gb, + "fit_type": fit_type, + "estimated_tok_per_sec": speed, + } + ) + + output = { + "model": { + "id": model.id, + "parameter_count": model.parameter_count, + "architecture": model.architecture, + "context_length": model.context_length, + "license": model.license, + }, + "target_quant": target_quant, + "context_length": context_length, + "vram_by_quant": vram_by_quant, + "gpu_compatibility": gpus, + } + _console.console.print_json(json.dumps(output, ensure_ascii=False)) + + +def display_upgrade_json( + current_hw: HardwareInfo, + current_results: list, + target_results: list[tuple[str, HardwareInfo, list]], +) -> None: + """Emit the upgrade comparison as JSON for scripting.""" + current_row = _summarize_row("Current", current_hw, current_results) + rows = [] + for name, hw, res in target_results: + row = _summarize_row(name, hw, res) + row["delta_quality"] = row["top_quality"] - current_row["top_quality"] + row["delta_tok_s"] = row["top_tok_s"] - current_row["top_tok_s"] + rows.append(row) + _console.console.print_json( + json.dumps( + {"current": current_row, "targets": rows}, + ensure_ascii=False, + ) + ) diff --git a/src/whichllm/output/markdown.py b/src/whichllm/output/markdown.py new file mode 100644 index 0000000..dec6a06 --- /dev/null +++ b/src/whichllm/output/markdown.py @@ -0,0 +1,154 @@ +"""GitHub-Flavored Markdown output for ranking results.""" + +from __future__ import annotations + +from whichllm.engine.quantization import effective_quant_type +from whichllm.engine.types import CompatibilityResult +from whichllm.hardware.types import HardwareInfo +from whichllm.output import _console +from whichllm.output.formatting import ( + _format_bytes, + _format_downloads, + _format_params, + _format_published_at, +) + + +def _escape_markdown_cell(value: object) -> str: + text = "" if value is None else str(value) + return text.replace("\\", "\\\\").replace("|", "\\|").replace("\n", "
") + + +def _format_markdown_speed(result: CompatibilityResult) -> str: + speed = result.estimated_tok_per_sec + if speed is None: + return "N/A" + marker = "" + if result.speed_confidence == "low": + marker = " ?" + elif result.speed_confidence == "medium": + marker = " ~" + return f"{speed:.1f} tok/s{marker}" + + +def _format_markdown_score(result: CompatibilityResult) -> str: + score = f"{result.quality_score:.1f}" + if result.benchmark_status == "none": + return f"{score} ?" + if result.benchmark_status == "self_reported": + return f"{score} !sr" + if result.benchmark_status == "estimated": + return f"{score} ~" + return score + + +def _format_markdown_fit(fit_type: str) -> str: + labels = { + "full_gpu": "Full GPU", + "partial_offload": "Partial", + "cpu_only": "CPU only", + } + return labels.get(fit_type, fit_type) + + +def _format_markdown_params(result: CompatibilityResult) -> str: + params = _format_params(result.model.parameter_count) + if result.model.is_moe and result.model.parameter_count_active: + params += f" ({_format_params(result.model.parameter_count_active)}a)" + return params + + +def _format_markdown_model(result: CompatibilityResult) -> str: + if not result.artifact_model: + return result.model.id + return f"[{result.model.id}](https://huggingface.co/{result.artifact_model.id})" + + +def _markdown_table(headers: list[str], rows: list[list[str]]) -> str: + lines = [ + "| " + " | ".join(headers) + " |", + "| " + " | ".join("---" for _ in headers) + " |", + ] + for row in rows: + lines.append( + "| " + " | ".join(_escape_markdown_cell(cell) for cell in row) + " |" + ) + return "\n".join(lines) + + +def _write_markdown(text: str) -> None: + _console.console.file.write(text + "\n") + _console.console.file.flush() + + +def display_markdown( + results: list[CompatibilityResult], + hardware: HardwareInfo, + *, + show_status: bool = False, + empty_message: str | None = None, +) -> None: + """Emit ranking results as a pasteable GitHub-Flavored Markdown table.""" + lines = ["## Recommended Models", ""] + + if not results: + lines.append(empty_message or "No compatible models found for your hardware.") + _write_markdown("\n".join(lines)) + return + + if show_status: + mem_label = "VRAM" if hardware.gpus else "RAM" + headers = [ + "#", + "Model", + "Params", + "Quant", + "Fit", + mem_label, + "Speed", + "Published", + "Score", + "License", + ] + rows = [ + [ + str(index), + _format_markdown_model(result), + _format_markdown_params(result), + effective_quant_type(result.model, result.gguf_variant), + _format_markdown_fit(result.fit_type), + _format_bytes(result.vram_required_bytes), + _format_markdown_speed(result), + _format_published_at(result.model.published_at), + _format_markdown_score(result), + result.model.license or "-", + ] + for index, result in enumerate(results, 1) + ] + else: + headers = [ + "#", + "Model", + "Params", + "Quant", + "Published", + "Downloads", + "Score", + "License", + ] + rows = [ + [ + str(index), + _format_markdown_model(result), + _format_markdown_params(result), + effective_quant_type(result.model, result.gguf_variant), + _format_published_at(result.model.published_at), + _format_downloads(result.model.downloads), + _format_markdown_score(result), + result.model.license or "-", + ] + for index, result in enumerate(results, 1) + ] + + lines.append(_markdown_table(headers, rows)) + _write_markdown("\n".join(lines)) diff --git a/src/whichllm/output/plan.py b/src/whichllm/output/plan.py new file mode 100644 index 0000000..5b7090c --- /dev/null +++ b/src/whichllm/output/plan.py @@ -0,0 +1,158 @@ +"""Plan-command Rich output.""" + +from __future__ import annotations + +from rich.panel import Panel +from rich.table import Table + +from whichllm.models.types import GGUFVariant, ModelInfo +from whichllm.output import _console +from whichllm.output.formatting import _format_bytes, _format_params + + +def display_plan( + model: ModelInfo, + context_length: int, + target_quant: str, +) -> None: + """Display hardware requirements for a specific model.""" + from whichllm.constants import ( + GPU_BANDWIDTH, + QUANT_BYTES_PER_WEIGHT, + QUANT_QUALITY_PENALTY, + ) + from whichllm.engine.performance import estimate_tok_per_sec + from whichllm.engine.vram import estimate_vram + from whichllm.hardware.types import GPUInfo + + _GiB = 1024**3 + + # -- Model info panel -- + params = _format_params(model.parameter_count) + active = "" + if model.is_moe and model.parameter_count_active: + active = f" ({_format_params(model.parameter_count_active)} active)" + ctx = str(model.context_length) if model.context_length else "unknown" + + lines = [ + f"[bold cyan]Model:[/] {model.id}", + f"[bold cyan]Params:[/] {params}{active} | Arch: {model.architecture} | Context: {ctx}", + ] + if model.license: + lines.append(f"[bold cyan]License:[/] {model.license}") + panel = Panel("\n".join(lines), title="[bold]Model Info[/]", border_style="cyan") + _console.console.print(panel) + + # -- VRAM requirements by quantization -- + quant_levels = ["Q2_K", "Q3_K_M", "Q4_K_M", "Q5_K_M", "Q6_K", "Q8_0", "F16"] + vram_table = Table( + title=f"VRAM Required (context: {context_length})", show_lines=True + ) + vram_table.add_column("Quant", style="bold", width=8) + vram_table.add_column("VRAM", justify="right", width=10) + vram_table.add_column("Quality Loss", justify="right", width=12) + + target_vram = 0 + for qt in quant_levels: + bpw = QUANT_BYTES_PER_WEIGHT.get(qt) + if bpw is None: + continue + fake_size = int(model.parameter_count * bpw) + fake_variant = GGUFVariant( + filename="", quant_type=qt, file_size_bytes=fake_size + ) + vram_bytes = estimate_vram(model, fake_variant, context_length) + penalty = QUANT_QUALITY_PENALTY.get(qt, 0.0) + penalty_str = f"-{penalty * 100:.0f}%" if penalty > 0 else "0%" + marker = " ★" if qt.upper() == target_quant.upper() else "" + style = "bold green" if qt.upper() == target_quant.upper() else "" + vram_table.add_row( + f"{qt}{marker}", _format_bytes(vram_bytes), penalty_str, style=style + ) + if qt.upper() == target_quant.upper(): + target_vram = vram_bytes + + _console.console.print(vram_table) + + if target_vram == 0: + bpw = QUANT_BYTES_PER_WEIGHT.get(target_quant.upper(), 0.5625) + fake_size = int(model.parameter_count * bpw) + fake_variant = GGUFVariant( + filename="", quant_type=target_quant, file_size_bytes=fake_size + ) + target_vram = estimate_vram(model, fake_variant, context_length) + + # -- GPU compatibility table -- + _PLAN_GPUS: list[tuple[str, int]] = [ + ("RTX 4060", 8), + ("RTX 3060", 12), + ("RTX 4070", 12), + ("RTX 4080", 16), + ("RTX 4090", 24), + ("RX 7900 XTX", 24), + ("RTX 5090", 32), + ("A100 40GB", 40), + ("L40S", 48), + ("A100 80GB", 80), + ("H100", 80), + ("H200", 141), + ] + + gpu_table = Table( + title=f"GPU Compatibility ({target_quant}, {_format_bytes(target_vram)} required)", + show_lines=True, + ) + gpu_table.add_column("GPU", style="bold", min_width=14) + gpu_table.add_column("VRAM", justify="right", width=8) + gpu_table.add_column("Fit", justify="center", width=12) + gpu_table.add_column("Est. Speed", justify="right", width=10) + + bpw = QUANT_BYTES_PER_WEIGHT.get(target_quant.upper(), 0.5625) + fake_size = int(model.parameter_count * bpw) + fake_variant = GGUFVariant( + filename="", quant_type=target_quant, file_size_bytes=fake_size + ) + + min_full_gpu = None + for gpu_name, vram_gb in _PLAN_GPUS: + vram_bytes = int(vram_gb * _GiB) + bandwidth = GPU_BANDWIDTH.get(gpu_name) + gpu_info = GPUInfo( + name=gpu_name, + vendor="nvidia", + vram_bytes=vram_bytes, + memory_bandwidth_gbps=bandwidth, + ) + + if vram_bytes >= target_vram: + fit = "[green]✓ Full GPU[/]" + fit_type = "full_gpu" + if min_full_gpu is None: + min_full_gpu = (gpu_name, vram_gb) + elif vram_bytes >= target_vram * 0.4: + fit = "[yellow]~ Partial[/]" + fit_type = "partial_offload" + else: + fit = "[red]✗ Too small[/]" + fit_type = None + + if fit_type and bandwidth: + speed = estimate_tok_per_sec(model, fake_variant, gpu_info, fit_type) + speed_str = f"{speed:.1f} tok/s" + else: + speed_str = "—" + + gpu_table.add_row(gpu_name, f"{vram_gb} GB", fit, speed_str) + + _console.console.print(gpu_table) + + if min_full_gpu: + _console.console.print( + f" [green]★[/] Minimum GPU for full offload: " + f"[bold]{min_full_gpu[0]}[/] ({min_full_gpu[1]} GB) at {target_quant}" + ) + else: + _console.console.print( + f" [yellow]Note:[/] No single GPU can fully load this model at {target_quant}. " + "Consider a lower quantization or multi-GPU setup." + ) diff --git a/src/whichllm/output/ranking.py b/src/whichllm/output/ranking.py new file mode 100644 index 0000000..50b1c0e --- /dev/null +++ b/src/whichllm/output/ranking.py @@ -0,0 +1,348 @@ +"""Ranking and hardware Rich output surfaces.""" + +from __future__ import annotations + +import re +from math import log10 + +from rich.panel import Panel +from rich.table import Table +from rich.text import Text + +from whichllm.engine.quantization import effective_quant_type +from whichllm.engine.types import CompatibilityResult +from whichllm.hardware.types import HardwareInfo +from whichllm.output import _console +from whichllm.output.formatting import ( + _downloads_style, + _format_bytes, + _format_downloads, + _format_params, + _format_published_at, + _format_speed, + _parse_published_at, + _published_style, +) + + +def _detect_specializations(model_id: str) -> list[str]: + """Detect task-specialized model hints from repository name.""" + lower = model_id.lower() + tags: list[str] = [] + if re.search(r"(coder|codegen|starcoder|program|coding)", lower): + tags.append("coding") + if re.search(r"(^|[-_/])(vl|vision|multimodal|llava|image)([-_/]|$)", lower): + tags.append("vision") + if re.search(r"(^|[-_/])math([-_/]|$)", lower): + tags.append("math") + return tags + + +def _artifact_model_id(result: CompatibilityResult) -> str: + if result.artifact_model: + return result.artifact_model.id + return result.model.id + + +def _top_pick_confidence(results: list[CompatibilityResult]) -> tuple[str, str]: + """Return confidence level and explanation for top pick.""" + top = results[0] + gap = (top.quality_score - results[1].quality_score) if len(results) > 1 else 999.0 + notes: list[str] = [] + if top.fit_type == "partial_offload": + notes.append("partial offload") + elif top.fit_type == "cpu_only": + notes.append("CPU-only") + if top.speed_confidence == "low": + notes.append("low-confidence speed") + risk_note = f", {', '.join(notes)}" if notes else "" + + if top.benchmark_status == "none": + return "Low", f"no benchmark data, gap +{gap:.1f}{risk_note}" + if top.benchmark_status == "self_reported": + return ( + "Low", + f"uploader-reported benchmark only (unverified), gap +{gap:.1f}{risk_note}", + ) + if top.benchmark_status == "estimated": + if gap >= 2.0: + confidence = "Medium" + else: + confidence = "Low" + if top.speed_confidence == "low" and confidence == "Medium": + confidence = "Low" + return confidence, f"estimated benchmark, gap +{gap:.1f}{risk_note}" + if gap >= 2.5: + confidence = "High" + reason = f"direct benchmark, gap +{gap:.1f}{risk_note}" + elif gap >= 1.0: + confidence = "Medium" + reason = f"direct benchmark, gap +{gap:.1f}{risk_note}" + else: + confidence = "Low" + reason = f"direct benchmark but very close (+{gap:.1f}){risk_note}" + + # オフロード/CPU-only/低信頼speedの1位は実運用で不確実性が高いため信頼度を1段階下げる + if top.fit_type != "full_gpu" or top.speed_confidence == "low": + if confidence == "High": + confidence = "Medium" + elif confidence == "Medium": + confidence = "Low" + return confidence, reason + + +def display_hardware(hw: HardwareInfo) -> None: + """Display hardware information panel.""" + lines: list[str] = [] + + if hw.gpus: + for i, gpu in enumerate(hw.gpus): + if gpu.shared_memory: + vram = ( + f"{_format_bytes(gpu.vram_bytes)} shared" + if gpu.vram_bytes > 0 + else "shared memory" + ) + else: + vram = _format_bytes(gpu.vram_bytes) + if ( + gpu.usable_vram_bytes is not None + and gpu.usable_vram_bytes < gpu.vram_bytes + ): + vram += f" (budget {_format_bytes(gpu.usable_vram_bytes)})" + bw = ( + f"{gpu.memory_bandwidth_gbps:.0f} GB/s" + if gpu.memory_bandwidth_gbps + else "N/A" + ) + cc = ( + f"CC {gpu.compute_capability[0]}.{gpu.compute_capability[1]}" + if gpu.compute_capability + else "" + ) + extra = [] + if cc: + extra.append(cc) + if gpu.cuda_version: + extra.append(f"CUDA {gpu.cuda_version}") + if gpu.rocm_version: + extra.append(f"ROCm {gpu.rocm_version}") + extra_str = f" ({', '.join(extra)})" if extra else "" + lines.append( + f"[bold green]GPU {i}:[/] {gpu.name} — {vram}{extra_str} — BW: {bw}" + ) + else: + lines.append("[yellow]No GPU detected[/] — CPU-only mode") + + avx_flags = [] + if hw.has_avx2: + avx_flags.append("AVX2") + if hw.has_avx512: + avx_flags.append("AVX-512") + avx_str = f" ({', '.join(avx_flags)})" if avx_flags else "" + lines.append(f"[bold blue]CPU:[/] {hw.cpu_name} — {hw.cpu_cores} cores{avx_str}") + + ram = _format_bytes(hw.ram_bytes) + if hw.ram_budget_bytes is not None and hw.ram_budget_bytes < hw.ram_bytes: + ram += f" (budget {_format_bytes(hw.ram_budget_bytes)})" + lines.append(f"[bold blue]RAM:[/] {ram}") + lines.append(f"[bold blue]Disk free:[/] {_format_bytes(hw.disk_free_bytes)}") + lines.append(f"[bold blue]OS:[/] {hw.os}") + for note in hw.budget_notes: + lines.append(f"[dim]{note}[/dim]") + + panel = Panel("\n".join(lines), title="[bold]Hardware Info[/]", border_style="blue") + _console.console.print(panel) + + +def display_ranking( + results: list[CompatibilityResult], + *, + has_gpu: bool = True, + show_status: bool = False, + empty_message: str | None = None, +) -> None: + """Display ranked model table.""" + if not results: + _console.console.print( + f"[yellow]{empty_message or 'No compatible models found for your hardware.'}[/]" + ) + return + + mem_label = "VRAM" if has_gpu else "RAM" + + table = Table(title="Recommended Models", show_lines=True) + table.add_column("#", style="bold", width=3, justify="right") + table.add_column("Model", style="cyan", min_width=14, overflow="fold") + table.add_column("Quant", justify="center", width=6) + if show_status: + table.add_column(f"Fit / {mem_label}", justify="center", width=8) + table.add_column("Speed", justify="right", width=12) + table.add_column("Published", justify="center", width=10) + else: + table.add_column("Params", justify="right", width=6) + table.add_column("Published", justify="center", width=10) + table.add_column("Downloads", justify="right", width=9) + table.add_column("Score", justify="right", width=5) + + download_logs = [ + log10(max(r.model.downloads, 1)) for r in results if r.model.downloads > 0 + ] + min_download_log = min(download_logs) if download_logs else 0.0 + max_download_log = max(download_logs) if download_logs else 1.0 + published_dates = [_parse_published_at(r.model.published_at) for r in results] + published_valid = [d for d in published_dates if d is not None] + oldest_ts = min((d.timestamp() for d in published_valid), default=None) + newest_ts = max((d.timestamp() for d in published_valid), default=None) + + for i, r in enumerate(results, 1): + quant = effective_quant_type(r.model, r.gguf_variant) + vram_str = _format_bytes(r.vram_required_bytes) + speed_str = _format_speed(r) + + score_val = f"{r.quality_score:.1f}" + if r.benchmark_status == "none": + score_str = f"[red]{score_val} ?[/red]" + elif r.benchmark_status == "self_reported": + score_str = f"[bright_yellow]{score_val} !sr[/bright_yellow]" + elif r.benchmark_status == "estimated": + score_str = f"[yellow]{score_val} ~[/yellow]" + else: + score_str = f"[green]{score_val}[/green]" + + fit_style = { + "full_gpu": "[green]Full GPU[/]", + "partial_offload": "[yellow]Partial[/]", + "cpu_only": "[red]CPU only[/]", + } + fit_str = fit_style.get(r.fit_type, r.fit_type) + published_dt = _parse_published_at(r.model.published_at) + published_str = Text( + _format_published_at(r.model.published_at), + style=_published_style(published_dt, oldest_ts, newest_ts), + ) + downloads_str = Text( + _format_downloads(r.model.downloads), + style=_downloads_style( + r.model.downloads, min_download_log, max_download_log + ), + ) + + params_str = _format_params(r.model.parameter_count) + if r.model.is_moe and r.model.parameter_count_active: + params_str += f" ({_format_params(r.model.parameter_count_active)}a)" + + model_link = Text(r.model.id, style="cyan") + model_link.stylize(f"link https://huggingface.co/{_artifact_model_id(r)}") + if show_status: + model_link.append(f"\n{params_str}", style="dim") + + row_cells = [ + str(i), + model_link, + quant, + ] + if show_status: + row_cells.extend( + [f"{fit_str}\n[dim]{vram_str}[/dim]", speed_str, published_str] + ) + else: + row_cells.append(params_str) + row_cells.extend([published_str, downloads_str]) + row_cells.append(score_str) + table.add_row(*row_cells) + + _console.console.print(table) + + has_estimated = any(r.benchmark_status == "estimated" for r in results) + has_self = any(r.benchmark_status == "self_reported" for r in results) + has_none = any(r.benchmark_status == "none" for r in results) + if has_estimated or has_none or has_self: + parts = [] + if has_self: + parts.append( + "[bright_yellow]!sr[/bright_yellow] = uploader-reported only (unverified)" + ) + if has_estimated: + parts.append("[yellow]Estimated / ~[/yellow] = inferred from model line") + if has_none: + parts.append("[red]None / ?[/red] = no benchmark data") + _console.console.print(f" [dim]Score:[/dim] {', '.join(parts)}") + + if show_status: + has_speed_medium = any(r.speed_confidence == "medium" for r in results) + has_speed_low = any(r.speed_confidence == "low" for r in results) + if has_speed_medium or has_speed_low: + parts = [] + if has_speed_medium: + parts.append("[yellow]~[/yellow] = estimated tok/s range") + if has_speed_low: + parts.append("[red]?[/red] = low-confidence/backend-sensitive tok/s") + _console.console.print(f" [dim]Speed:[/dim] {', '.join(parts)}") + + has_direct = any(r.benchmark_status == "direct" for r in results) + if not has_direct: + _console.console.print( + " [red]No confirmed winner:[/] direct benchmark data is missing for current candidates." + ) + + confidence, reason = _top_pick_confidence(results) + confidence_style = { + "High": "green", + "Medium": "yellow", + "Low": "red", + }[confidence] + _console.console.print( + f" Top pick confidence: [{confidence_style}]{confidence}[/{confidence_style}] ({reason})" + ) + + from whichllm.models.benchmark_sources import BENCHMARK_SNAPSHOT + + _console.console.print( + f" [dim]Benchmark reference: {BENCHMARK_SNAPSHOT} curated snapshot; " + "live AA / LiveBench / Aider merged when reachable.[/dim]" + ) + + # 上位が僅差なら「断定しすぎない」ための注意を表示する + if len(results) >= 2: + gap = results[0].quality_score - results[1].quality_score + if gap < 1.5: + _console.console.print( + f" [yellow]Note:[/] Top candidates are very close (#{1} vs #{2}: {gap:.1f} pts)." + ) + + # 上位に根拠が弱い候補がある場合は目立つ注意を出す + weak_top = [ + idx + 1 for idx, r in enumerate(results[:3]) if r.benchmark_status != "direct" + ] + if weak_top: + joined = ", ".join(f"#{i}" for i in weak_top) + _console.console.print( + f" [yellow]Caution:[/] Weaker benchmark evidence in top ranks: {joined}" + ) + + weak_speed_top = [ + idx + 1 for idx, r in enumerate(results[:3]) if r.speed_confidence == "low" + ] + if weak_speed_top: + joined = ", ".join(f"#{i}" for i in weak_speed_top) + _console.console.print( + f" [yellow]Speed caution:[/] Low-confidence speed estimates in top ranks: {joined}" + ) + + specialized: list[str] = [] + for idx, r in enumerate(results[:10], 1): + tags = _detect_specializations(r.model.id) + if tags: + joined_tags = "/".join(tags) + specialized.append(f"#{idx} {joined_tags}") + if specialized: + _console.console.print( + " [yellow]Task hint:[/] Specialized models detected in ranking: " + + ", ".join(specialized) + ) + + for i, r in enumerate(results[:3], 1): + if r.warnings: + for w in r.warnings: + _console.console.print(f" [yellow]Warning #{i} {r.model.name}:[/] {w}") diff --git a/src/whichllm/output/upgrade.py b/src/whichllm/output/upgrade.py new file mode 100644 index 0000000..6b51f47 --- /dev/null +++ b/src/whichllm/output/upgrade.py @@ -0,0 +1,126 @@ +"""Upgrade-command Rich output.""" + +from __future__ import annotations + +from rich.table import Table + +from whichllm.engine.quantization import effective_quant_type +from whichllm.hardware.types import HardwareInfo +from whichllm.output import _console + + +def _summarize_row(name: str, hw: HardwareInfo, results: list) -> dict: + """Reduce a (hardware, ranking) pair to one row for the upgrade table.""" + gpu_label = "CPU-only" + vram_gb = 0.0 + if hw.gpus: + g = max(hw.gpus, key=lambda x: x.vram_bytes) + gpu_label = g.name + vram_gb = g.vram_bytes / 1024**3 + if not results: + return { + "name": name, + "gpu": gpu_label, + "vram_gb": vram_gb, + "top_model": "—", + "top_quality": 0.0, + "top_tok_s": 0.0, + "top_speed_confidence": "low", + "top_speed_range_tok_per_sec": None, + "top_fit": "—", + "top_quant": "—", + } + r = results[0] + return { + "name": name, + "gpu": gpu_label, + "vram_gb": vram_gb, + "top_model": r.model.id, + "top_quality": float(r.quality_score), + "top_tok_s": float(r.estimated_tok_per_sec), + "top_speed_confidence": r.speed_confidence, + "top_speed_range_tok_per_sec": ( + list(r.speed_range_tok_per_sec) if r.speed_range_tok_per_sec else None + ), + "top_fit": r.fit_type, + "top_quant": ( + r.gguf_variant.quant_type + if r.gguf_variant + else effective_quant_type(r.model, None) + ), + } + + +def _upgrade_verdict(delta_q: float, delta_speed: float) -> str: + """Return a short verdict for an upgrade row.""" + if delta_q >= 12 and delta_speed >= 10: + return "[bold green]worth it[/]" + if delta_q >= 8 or delta_speed >= 20: + return "[green]meaningful[/]" + if delta_q >= 3 or delta_speed >= 5: + return "[yellow]marginal[/]" + if delta_q <= -3 or delta_speed <= -5: + return "[red]downgrade[/]" + return "[dim]flat[/]" + + +def display_upgrade( + current_hw: HardwareInfo, + current_results: list, + target_results: list[tuple[str, HardwareInfo, list]], +) -> None: + """Render the GPU-upgrade comparison table.""" + current_row = _summarize_row("Current", current_hw, current_results) + target_rows = [_summarize_row(name, hw, res) for name, hw, res in target_results] + + table = Table( + title="GPU upgrade comparison", + show_lines=False, + header_style="bold cyan", + ) + table.add_column("Setup", style="bold") + table.add_column("GPU", overflow="fold") + table.add_column("VRAM", justify="right") + table.add_column("Best model", overflow="fold") + table.add_column("Quant") + table.add_column("Quality", justify="right") + table.add_column("tok/s", justify="right") + table.add_column("ΔQ", justify="right") + table.add_column("Δtok/s", justify="right") + table.add_column("Verdict") + + table.add_row( + current_row["name"], + current_row["gpu"], + f"{current_row['vram_gb']:.0f} GB" + if current_row["vram_gb"] is not None + else "—", + current_row["top_model"], + current_row["top_quant"], + f"{current_row['top_quality']:.1f}", + f"{current_row['top_tok_s']:.0f}", + "—", + "—", + "—", + ) + for row in target_rows: + dq = row["top_quality"] - current_row["top_quality"] + ds = row["top_tok_s"] - current_row["top_tok_s"] + table.add_row( + row["name"], + row["gpu"], + f"{row['vram_gb']:.0f} GB" if row["vram_gb"] is not None else "—", + row["top_model"], + row["top_quant"], + f"{row['top_quality']:.1f}", + f"{row['top_tok_s']:.0f}", + f"{dq:+.1f}", + f"{ds:+.0f}", + _upgrade_verdict(dq, ds), + ) + + _console.console.print(table) + _console.console.print( + "[dim]Verdict: worth it (≥12pt Q & ≥10 tok/s lift) · meaningful (≥8pt Q or " + "≥20 tok/s) · marginal · flat (no change) · downgrade.[/]" + ) diff --git a/src/whichllm/utils.py b/src/whichllm/utils.py new file mode 100644 index 0000000..ca0dbc0 --- /dev/null +++ b/src/whichllm/utils.py @@ -0,0 +1,76 @@ +from __future__ import annotations + +import os +import re +from importlib.metadata import version, PackageNotFoundError +from pathlib import Path + +import click + + +def _current_version() -> str: + """Return installed package version.""" + try: + return version("whichllm") + except PackageNotFoundError: + return "unknown" + + +_SHORTHAND_RE = re.compile(r"^(\d+(?:\.\d+)?)\s*([kmb])$", re.IGNORECASE) +_MULTIPLIERS = {"k": 1024, "m": 1024 * 1024, "b": 1024 * 1024 * 1024} + + +def parse_context_length(value: str) -> int: + """Parse a context length string, supporting shorthand like 64k or 128K. + + Accepts plain integers (e.g. "4096") or shorthand with a suffix: + k/K = x1,024 (64k -> 65536) + m/M = x1,048,576 + b/B = x1,073,741,824 + + Returns the integer context length. Raises ValueError on bad input. + """ + value = value.strip() + match = _SHORTHAND_RE.match(value) + if match: + number = float(match.group(1)) + suffix = match.group(2).lower() + result = int(number * _MULTIPLIERS[suffix]) + if result <= 0: + raise ValueError(f"Context length must be positive, got {value!r}") + return result + try: + result = int(value) + except ValueError: + raise ValueError( + f"Invalid context length {value!r}. " + "Use a plain integer (4096) or shorthand (64k, 128k)." + ) + if result <= 0: + raise ValueError(f"Context length must be positive, got {result}") + return result + + +class ContextLengthType(click.ParamType): + """Click parameter type that accepts integers or shorthand like 64k.""" + + name = "context_length" + + def convert(self, value, param, ctx): + if isinstance(value, int): + return value + try: + return parse_context_length(str(value)) + except ValueError as e: + self.fail(str(e), param, ctx) + + +CONTEXT_LENGTH = ContextLengthType() + + +def _cache_dir() -> Path: + """Return the whichllm cache directory, respecting XDG_CACHE_HOME.""" + base = os.environ.get("XDG_CACHE_HOME") + if base and Path(base).is_absolute(): + return Path(base) / "whichllm" + return Path.home() / ".cache" / "whichllm" diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_aa_index.py b/tests/test_aa_index.py new file mode 100644 index 0000000..5a62ebf --- /dev/null +++ b/tests/test_aa_index.py @@ -0,0 +1,137 @@ +"""Tests for the Artificial Analysis Intelligence Index source. + +These cover the Next.js App Router (RSC) scraper that replaced the old +``__NEXT_DATA__`` extraction, the variant-stripping name canonicalization, +and the merge-over-curated-fallback behaviour of ``fetch_aa_index_scores``. +All tests are offline — network is served from an ``httpx.MockTransport``. +""" + +from __future__ import annotations + +import asyncio +import json + +import httpx +import pytest + +from whichllm.models.benchmark_sources.aa_index import ( + AA_LEADERBOARD_URL, + _canonical_name, + _decode_rsc_blob, + _extract_aa_pairs_from_html, + _normalize_aa_index, + fetch_aa_index_scores, + get_aa_curated_fallback, +) +from whichllm.models.benchmark_sources.types import ExtractionFailed + + +def _rsc_page(records: list[dict]) -> str: + """Build a minimal HTML page that embeds ``records`` the way the live + artificialanalysis.ai App Router page does: as a JSON-string-escaped + fragment inside ``self.__next_f.push([n, "..."])``.""" + # The fragment is an arbitrary slice of the RSC stream; the scraper only + # cares that it contains the "name"/"intelligenceIndex" key pairs. + fragment = ",".join( + '{"slug":"x","name":%s,"reasoningModel":false,' + '"intelligenceIndex":%s,"codingIndex":1.0}' + % (json.dumps(r["name"]), r["index"]) + for r in records + ) + chunk = json.dumps("3:[" + fragment + "]\n") + return ( + "" + "" + f"" + "" + ) + + +def test_canonical_name_strips_variants_and_separators(): + assert _canonical_name("Qwen3 14B (Reasoning)") == "qwen3 14b" + assert _canonical_name("Qwen3-14B") == "qwen3 14b" + # Separators normalize to single spaces (the table side is canonicalized + # the same way, so "GLM-5" and "GLM 5" still collide). + assert _canonical_name("GLM-5 (Non-reasoning)") == "glm 5" + assert _canonical_name("DeepSeek V4 Pro (Reasoning, Max Effort)") == ( + "deepseek v4 pro" + ) + + +def test_decode_rsc_blob_unescapes_chunks(): + page = _rsc_page([{"name": "Qwen3 14B (Reasoning)", "index": 33.0}]) + blob = _decode_rsc_blob(page) + assert '"name":"Qwen3 14B (Reasoning)"' in blob + assert '"intelligenceIndex":33.0' in blob + + +def test_extract_pairs_from_rsc_html(): + page = _rsc_page( + [ + {"name": "Qwen3 14B (Reasoning)", "index": 33.0}, + {"name": "Qwen3 14B (Non-reasoning)", "index": 30.0}, + {"name": "GLM-5 (Reasoning)", "index": 50.0}, + ] + ) + pairs = dict(_extract_aa_pairs_from_html(page)) + assert pairs["Qwen3 14B (Reasoning)"] == 33.0 + assert pairs["GLM-5 (Reasoning)"] == 50.0 + # The bounded regex must not leak one record's name into another's index. + assert len(pairs) == 3 + + +def test_extract_pairs_returns_empty_on_legacy_or_garbage_html(): + assert _extract_aa_pairs_from_html("no rsc here") == [] + + +def _run_fetch(html: str) -> dict[str, float]: + def handler(request: httpx.Request) -> httpx.Response: + assert str(request.url) == AA_LEADERBOARD_URL + return httpx.Response(200, text=html) + + async def go() -> dict[str, float]: + transport = httpx.MockTransport(handler) + async with httpx.AsyncClient(transport=transport) as client: + return await fetch_aa_index_scores(client) + + return asyncio.run(go()) + + +def test_fetch_maps_canonical_names_and_merges_over_fallback(): + # "Qwen3 14B (Reasoning)" canonicalizes onto the "Qwen3 14B" table entry + # -> Qwen/Qwen3-14B, and a high live value must override the snapshot. + page = _rsc_page([{"name": "Qwen3 14B (Reasoning)", "index": 55.0}]) + scores = _run_fetch(page) + + fallback = get_aa_curated_fallback() + # Coverage never shrinks below the curated snapshot ... + assert set(fallback).issubset(set(scores)) + # ... and the live number wins where it is higher. + assert scores["Qwen/Qwen3-14B"] > fallback["Qwen/Qwen3-14B"] + + +def test_fetch_raises_when_no_records_found(): + with pytest.raises(ExtractionFailed): + _run_fetch("nothing to see") + + +def test_live_normalization_anchors_on_reworked_scale(): + # Retuned bounds keep the calibration: the top mapped open model lands ~95 + # and an 8B-class model lands ~40, on AA's reworked (compressed) raw scale. + assert _normalize_aa_index(44.3) == pytest.approx(95, abs=0.5) # top open model + assert _normalize_aa_index(7.4) == pytest.approx(40, abs=0.5) # 8B-class + # Values below the floor clamp at 0 (raw is always positive in practice). + assert _normalize_aa_index(-100.0) == 0.0 + assert _normalize_aa_index(60.0) == 100.0 + + +def test_curated_fallback_normalizes_refreshed_snapshot(): + # The snapshot holds refreshed raw AA values; get_aa_curated_fallback maps + # them onto the 0-100 scale with the retuned bounds. + fb = get_aa_curated_fallback() + assert fb["deepseek-ai/DeepSeek-V4-Pro"] == pytest.approx(95, abs=0.5) + assert fb["Qwen/Qwen3-8B"] == 40.0 + assert fb["XiaomiMiMo/MiMo-V2.5-Pro"] == pytest.approx(92, abs=0.5) + # Reworked scale ranks the strong 8B above the small/old peers. + assert fb["Qwen/Qwen3-8B"] > fb["Qwen/Qwen3-0.6B"] + assert all(0.0 < v <= 100.0 for v in fb.values()) diff --git a/tests/test_amd_detection.py b/tests/test_amd_detection.py new file mode 100644 index 0000000..f5d528c --- /dev/null +++ b/tests/test_amd_detection.py @@ -0,0 +1,302 @@ +"""Tests for AMD GPU detection fallbacks.""" + +from __future__ import annotations + +import subprocess +from io import StringIO + +from rich.console import Console + +from whichllm.hardware import amd +from whichllm.hardware.types import GPUInfo, HardwareInfo + + +def test_detect_amd_gpu_from_lspci_when_rocm_smi_missing(monkeypatch): + output = ( + 'c1:00.0 "VGA compatible controller" "Advanced Micro Devices, Inc. ' + '[AMD/ATI]" "Strix Halo [Radeon 8060S]" -r00 "Framework" "Device 0001"\n' + ) + + def fake_run(args, **kwargs): + if args[0] == "rocm-smi": + raise FileNotFoundError + return subprocess.CompletedProcess(args, 0, stdout=output, stderr="") + + monkeypatch.setattr(amd.subprocess, "run", fake_run) + + gpus = amd.detect_amd_gpus() + + assert len(gpus) == 1 + assert gpus[0].vendor == "amd" + assert gpus[0].vram_bytes == 0 + assert gpus[0].shared_memory is True + assert gpus[0].memory_bandwidth_gbps == 256.0 + assert "Radeon 8060S" in gpus[0].name + + +def test_detect_strix_halo_rocm_smi_does_not_treat_aperture_as_vram(monkeypatch): + def fake_run(args, **kwargs): + if args[:2] == ["rocm-smi", "--showproductname"]: + return subprocess.CompletedProcess( + args, + 0, + stdout='{"card0": {"Card SKU": "STRXLGEN"}}', + stderr="", + ) + if args[:3] == ["rocm-smi", "--showmeminfo", "vram"]: + return subprocess.CompletedProcess( + args, + 0, + stdout='{"card0": {"VRAM Total Memory (B)": "536870912"}}', + stderr="", + ) + if args[:2] == ["rocm-smi", "--showdriverversion"]: + return subprocess.CompletedProcess( + args, + 0, + stdout='{"card0": {"Driver version": "7.0.3"}}', + stderr="", + ) + raise AssertionError(args) + + monkeypatch.setattr(amd.subprocess, "run", fake_run) + + gpus = amd.detect_amd_gpus() + + assert len(gpus) == 1 + assert gpus[0].name == "STRXLGEN" + assert gpus[0].vendor == "amd" + assert gpus[0].shared_memory is True + assert gpus[0].vram_bytes == 0 + assert gpus[0].rocm_version == "7.0.3" + assert gpus[0].memory_bandwidth_gbps == 256.0 + + +def test_detect_amd_gpu_ignores_intel_only_lspci(monkeypatch): + """Regression: an Intel VGA row must not be reported as AMD just + because 'Intel Corporation' contains the substring 'ati'.""" + output = ( + '00:02.0 "VGA compatible controller" "Intel Corporation" ' + '"Alder Lake-P GT1 [UHD Graphics]" -r0c -p00 ' + '"IP3 Tech (HK) Limited" "Device 8027"\n' + ) + + def fake_run(args, **kwargs): + if args[0] == "rocm-smi": + raise FileNotFoundError + return subprocess.CompletedProcess(args, 0, stdout=output, stderr="") + + monkeypatch.setattr(amd.subprocess, "run", fake_run) + # Isolate the lspci path: don't let a real sysfs probe leak in. + monkeypatch.setattr(amd, "_detect_from_sysfs", lambda: []) + + assert amd.detect_amd_gpus() == [] + + +def test_detect_amd_gpu_from_sysfs_when_lspci_missing(monkeypatch, tmp_path): + card = tmp_path / "card0" / "device" + card.mkdir(parents=True) + (card / "vendor").write_text("0x1002\n") + (card / "product_name").write_text("AMD Radeon RX 9060 XT\n") + (card / "mem_info_vram_total").write_text(str(16 * 1024**3)) + + monkeypatch.setattr(amd, "_detect_from_lspci", lambda: []) + original_sysfs = amd._detect_from_sysfs + monkeypatch.setattr(amd, "_detect_from_sysfs", lambda: original_sysfs(tmp_path)) + + gpus = amd._detect_amd_gpus_fallback() + + assert len(gpus) == 1 + assert gpus[0].vendor == "amd" + assert gpus[0].name == "AMD Radeon RX 9060 XT" + assert gpus[0].vram_bytes == 16 * 1024**3 + assert gpus[0].shared_memory is False + + +def test_display_amd_shared_memory_without_zero_kb(monkeypatch): + from whichllm.output import _console as console_mod + from whichllm.output import display as display_mod + + buf = StringIO() + monkeypatch.setattr(console_mod, "console", Console(file=buf, force_terminal=False)) + + display_mod.display_hardware( + HardwareInfo( + gpus=[ + GPUInfo( + name="Strix Halo [Radeon 8060S]", + vendor="amd", + vram_bytes=0, + shared_memory=True, + ) + ], + cpu_name="CPU", + cpu_cores=16, + ram_bytes=128 * 1024**3, + disk_free_bytes=100 * 1024**3, + os="linux", + ) + ) + + output = buf.getvalue() + assert "shared memory" in output + assert "256 GB/s" not in output + assert "0 KB" not in output + + +# ---------- Issue #61: RX 6750 XT detection ---------- + + +def test_sysfs_generic_name_enriched_by_lspci(monkeypatch, tmp_path): + """When sysfs gives 'AMD Graphics' and lspci gives a descriptive name, + the fallback should use the lspci name with sysfs VRAM.""" + _GiB = 1024**3 + + # sysfs: generic name but has VRAM + card = tmp_path / "card0" / "device" + card.mkdir(parents=True) + (card / "vendor").write_text("0x1002\n") + (card / "mem_info_vram_total").write_text(str(12 * _GiB)) + # no product_name → falls back to "AMD Graphics" + + lspci_name = "Navi 22 [Radeon RX 6700/6700 XT/6750 XT / 6800M/6850M XT]" + original_sysfs = amd._detect_from_sysfs + monkeypatch.setattr(amd, "_detect_from_sysfs", lambda: original_sysfs(tmp_path)) + monkeypatch.setattr(amd, "_detect_from_lspci", lambda: [lspci_name]) + + gpus = amd._detect_amd_gpus_fallback() + + assert len(gpus) == 1 + assert gpus[0].name == lspci_name + assert gpus[0].vram_bytes == 12 * _GiB + assert gpus[0].shared_memory is False + + +def test_sysfs_product_name_preferred_over_lspci(monkeypatch, tmp_path): + """When sysfs gives a real product name, it should be used even if + lspci is also available.""" + _GiB = 1024**3 + + card = tmp_path / "card0" / "device" + card.mkdir(parents=True) + (card / "vendor").write_text("0x1002\n") + (card / "product_name").write_text("AMD Radeon RX 6750 XT\n") + (card / "mem_info_vram_total").write_text(str(12 * _GiB)) + + original_sysfs = amd._detect_from_sysfs + monkeypatch.setattr(amd, "_detect_from_sysfs", lambda: original_sysfs(tmp_path)) + monkeypatch.setattr( + amd, + "_detect_from_lspci", + lambda: ["Navi 22 [Radeon RX 6700/6700 XT/6750 XT / 6800M/6850M XT]"], + ) + + gpus = amd._detect_amd_gpus_fallback() + + assert len(gpus) == 1 + assert gpus[0].name == "AMD Radeon RX 6750 XT" + assert gpus[0].vram_bytes == 12 * _GiB + assert gpus[0].memory_bandwidth_gbps == 432.0 + + +def test_lspci_enriched_with_sysfs_vram_when_sysfs_detection_fails( + monkeypatch, tmp_path +): + """When _detect_from_sysfs returns nothing but _read_sysfs_amd_vram + succeeds, lspci names should still get VRAM data.""" + _GiB = 1024**3 + + # _detect_from_sysfs returns nothing (e.g. product_name absent AND + # the card dir structure confuses the glob), but individual VRAM reads + # via _read_sysfs_amd_vram still work. + monkeypatch.setattr(amd, "_detect_from_sysfs", lambda: []) + monkeypatch.setattr( + amd, + "_detect_from_lspci", + lambda: ["Navi 22 [Radeon RX 6700/6700 XT/6750 XT / 6800M/6850M XT]"], + ) + monkeypatch.setattr(amd, "_read_sysfs_amd_vram", lambda: [12 * _GiB]) + + gpus = amd._detect_amd_gpus_fallback() + + assert len(gpus) == 1 + assert gpus[0].vram_bytes == 12 * _GiB + assert gpus[0].shared_memory is False + + +def test_lookup_bandwidth_compound_lspci_name(): + """The bandwidth lookup should resolve compound lspci names by + splitting on '/' and re-applying the 'RX ' prefix.""" + # Direct substring match works for clean names + assert amd._lookup_bandwidth("AMD Radeon RX 6750 XT") == 432.0 + assert amd._lookup_bandwidth("AMD Radeon RX 6700 XT") == 384.0 + + # Compound lspci name — first matching segment wins + compound = "Navi 22 [Radeon RX 6700/6700 XT/6750 XT / 6800M/6850M XT]" + bw = amd._lookup_bandwidth(compound) + assert bw is not None + assert bw > 0 + + +def test_display_amd_dgpu_does_not_say_shared_memory(monkeypatch): + """A discrete AMD GPU with VRAM must NOT display 'shared memory'.""" + from whichllm.output import _console as console_mod + from whichllm.output import display as display_mod + + buf = StringIO() + monkeypatch.setattr(console_mod, "console", Console(file=buf, force_terminal=False)) + + display_mod.display_hardware( + HardwareInfo( + gpus=[ + GPUInfo( + name="AMD Radeon RX 6750 XT", + vendor="amd", + vram_bytes=12 * 1024**3, + memory_bandwidth_gbps=432.0, + shared_memory=False, + ) + ], + cpu_name="AMD Ryzen 9 5900X", + cpu_cores=12, + ram_bytes=128 * 1024**3, + disk_free_bytes=500 * 1024**3, + os="linux", + ) + ) + + output = buf.getvalue() + assert "12.0 GB" in output + assert "432 GB/s" in output + assert "shared memory" not in output + + +def test_display_amd_dgpu_zero_vram_does_not_say_shared_memory(monkeypatch): + """An AMD dGPU with undetected VRAM should NOT be labelled + 'shared memory' — that would be a false positive.""" + from whichllm.output import _console as console_mod + from whichllm.output import display as display_mod + + buf = StringIO() + monkeypatch.setattr(console_mod, "console", Console(file=buf, force_terminal=False)) + + display_mod.display_hardware( + HardwareInfo( + gpus=[ + GPUInfo( + name="Navi 22 [Radeon RX 6750 XT]", + vendor="amd", + vram_bytes=0, + shared_memory=False, + ) + ], + cpu_name="CPU", + cpu_cores=8, + ram_bytes=32 * 1024**3, + disk_free_bytes=100 * 1024**3, + os="linux", + ) + ) + + output = buf.getvalue() + assert "shared memory" not in output diff --git a/tests/test_artifacts.py b/tests/test_artifacts.py new file mode 100644 index 0000000..396f1e9 --- /dev/null +++ b/tests/test_artifacts.py @@ -0,0 +1,47 @@ +"""Tests for resolving concrete downloadable artifacts.""" + +from whichllm.engine.types import CompatibilityResult +from whichllm.models.artifacts import attach_resolved_artifacts +from whichllm.models.types import GGUFVariant, ModelInfo + + +def test_attach_resolved_artifacts_maps_synthetic_quant_to_real_gguf_repo(): + base = ModelInfo( + id="Qwen/Qwen3-4B-Thinking-2507", + family_id="qwen3-4b-thinking", + name="Qwen3-4B-Thinking-2507", + parameter_count=4_000_000_000, + downloads=494_000, + ) + gguf_repo = ModelInfo( + id="MaziyarPanahi/Qwen3-4B-Thinking-2507-GGUF", + family_id="qwen3-4b-thinking", + name="Qwen3-4B-Thinking-2507-GGUF", + parameter_count=4_000_000_000, + downloads=26_000, + base_model="Qwen/Qwen3-4B-Thinking-2507", + gguf_variants=[ + GGUFVariant( + filename="Qwen3-4B-Thinking-2507-Q3_K_M.gguf", + quant_type="Q3_K_M", + file_size_bytes=2_000_000_000, + ) + ], + ) + synthetic = GGUFVariant( + filename="Qwen3-4B-Thinking-2507.Q3_K_M.gguf", + quant_type="Q3_K_M", + file_size_bytes=2_000_000_000, + ) + result = CompatibilityResult( + model=base, + gguf_variant=synthetic, + can_run=True, + vram_required_bytes=3_000_000_000, + vram_available_bytes=8_000_000_000, + ) + + attach_resolved_artifacts([result], [base, gguf_repo]) + + assert result.artifact_model is gguf_repo + assert result.artifact_variant is gguf_repo.gguf_variants[0] diff --git a/tests/test_asahi_detection.py b/tests/test_asahi_detection.py new file mode 100644 index 0000000..6adeee2 --- /dev/null +++ b/tests/test_asahi_detection.py @@ -0,0 +1,213 @@ +"""Tests for Asahi Linux (Apple Silicon on Linux) detection — Issue #29.""" + +from __future__ import annotations + +import subprocess +from pathlib import Path + +from whichllm.hardware import apple, cpu + + +# ---- CPU name fallback for ARM Linux ---- + + +def test_cpu_name_lscpu_fallback(monkeypatch): + """When /proc/cpuinfo has no model name (ARM), lscpu should be tried.""" + # Simulate ARM /proc/cpuinfo (no model name field) + arm_cpuinfo = ( + "processor\t: 0\n" + "BogoMIPS\t: 48.00\n" + "Features\t: fp asimd evtstrm aes\n" + "CPU implementer\t: 0x61\n" + ) + monkeypatch.setattr("builtins.open", _fake_open(arm_cpuinfo)) + monkeypatch.setattr("platform.system", lambda: "Linux") + + lscpu_output = "Architecture: aarch64\nModel name: Apple M2\n" + + def fake_run(args, **kwargs): + if args == ["lscpu"]: + return subprocess.CompletedProcess(args, 0, stdout=lscpu_output, stderr="") + raise FileNotFoundError + + monkeypatch.setattr(cpu.subprocess, "run", fake_run) + + assert cpu.detect_cpu_name() == "Apple M2" + + +def test_cpu_name_devicetree_fallback(monkeypatch, tmp_path): + """When lscpu also fails, device tree model should be used.""" + arm_cpuinfo = "processor\t: 0\nFeatures\t: fp asimd\n" + monkeypatch.setattr("builtins.open", _fake_open(arm_cpuinfo)) + monkeypatch.setattr("platform.system", lambda: "Linux") + + # lscpu not available + def fake_run(args, **kwargs): + raise FileNotFoundError + + monkeypatch.setattr(cpu.subprocess, "run", fake_run) + + # Device tree has the machine model + dt_model = tmp_path / "model" + dt_model.write_bytes(b"Apple MacBook Air (M2, 2022)\x00") + monkeypatch.setattr( + cpu, "_cpu_name_from_devicetree", lambda: _read_dt_model(dt_model) + ) + + assert cpu.detect_cpu_name() == "Apple M2" + + +def test_cpu_name_devicetree_extracts_chip_variants(): + """Verify chip name extraction from various device tree model strings.""" + assert _extract("Apple MacBook Air (M2, 2022)") == "Apple M2" + assert _extract("Apple Mac Mini (M4 Pro, 2024)") == "Apple M4 Pro" + assert _extract("Apple Mac Studio (M2 Ultra, 2023)") == "Apple M2 Ultra" + assert _extract("Apple Mac Pro (M2 Max, 2023)") == "Apple M2 Max" + assert _extract("Apple MacBook Pro (M1, 2020)") == "Apple M1" + + +def test_cpu_name_devicetree_non_apple(): + """Non-Apple device tree models should not produce an Apple chip name.""" + assert _extract("Raspberry Pi 4 Model B Rev 1.5") is None + assert _extract("Qualcomm Snapdragon 8cx Gen 3") is None + + +def test_cpu_name_lscpu_ignores_dash(monkeypatch): + """lscpu returning '-' as the model name should be ignored.""" + arm_cpuinfo = "processor\t: 0\n" + monkeypatch.setattr("builtins.open", _fake_open(arm_cpuinfo)) + monkeypatch.setattr("platform.system", lambda: "Linux") + + lscpu_output = "Architecture: aarch64\nModel name: -\n" + + def fake_run(args, **kwargs): + if args == ["lscpu"]: + return subprocess.CompletedProcess(args, 0, stdout=lscpu_output, stderr="") + raise FileNotFoundError + + monkeypatch.setattr(cpu.subprocess, "run", fake_run) + monkeypatch.setattr(cpu, "_cpu_name_from_devicetree", lambda: "Apple M2") + + assert cpu.detect_cpu_name() == "Apple M2" + + +# ---- Apple GPU detection on Asahi Linux ---- + + +def test_detect_asahi_gpu_from_sysfs(monkeypatch, tmp_path): + """Asahi DRM driver should be detected and produce an Apple GPUInfo.""" + _setup_asahi_sysfs(tmp_path) + monkeypatch.setattr( + apple, + "_chip_name_from_devicetree", + lambda: "Apple M2", + ) + monkeypatch.setattr("psutil.virtual_memory", _fake_vmem(24 * 1024**3)) + + gpus = apple.detect_apple_gpu_linux(drm_path=tmp_path) + + assert len(gpus) == 1 + assert gpus[0].vendor == "apple" + assert gpus[0].name == "Apple M2" + assert gpus[0].vram_bytes == 24 * 1024**3 + assert gpus[0].shared_memory is True + assert gpus[0].memory_bandwidth_gbps == 100.0 # M2 bandwidth + + +def test_detect_asahi_gpu_fallback_name(monkeypatch, tmp_path): + """When device tree is unavailable, GPU should be named 'Apple Silicon'.""" + _setup_asahi_sysfs(tmp_path) + monkeypatch.setattr(apple, "_chip_name_from_devicetree", lambda: None) + monkeypatch.setattr("psutil.virtual_memory", _fake_vmem(8 * 1024**3)) + + gpus = apple.detect_apple_gpu_linux(drm_path=tmp_path) + + assert len(gpus) == 1 + assert gpus[0].name == "Apple Silicon" + + +def test_detect_asahi_gpu_ignores_non_apple_drivers(tmp_path): + """Non-Apple DRM drivers should not be detected as Apple GPUs.""" + card = tmp_path / "card0" / "device" / "driver" + card.mkdir(parents=True) + # Symlink to a non-Apple driver name + target = tmp_path / "drivers" / "amdgpu" + target.mkdir(parents=True) + (tmp_path / "card0" / "device" / "driver").rmdir() + (tmp_path / "card0" / "device" / "driver").symlink_to(target) + + assert apple.detect_apple_gpu_linux(drm_path=tmp_path) == [] + + +def test_detect_asahi_gpu_no_drm(tmp_path): + """When /sys/class/drm doesn't exist, return empty.""" + nonexistent = tmp_path / "no_drm" + assert apple.detect_apple_gpu_linux(drm_path=nonexistent) == [] + + +# ---- Helpers ---- + + +def _setup_asahi_sysfs(tmp_path: Path) -> None: + """Create a minimal sysfs tree mimicking the Asahi DRM driver.""" + device_dir = tmp_path / "card0" / "device" + device_dir.mkdir(parents=True) + # Symlink driver → .../asahi + driver_target = tmp_path / "drivers" / "asahi" + driver_target.mkdir(parents=True) + (device_dir / "driver").symlink_to(driver_target) + + +def _fake_open(content: str): + """Return a context manager that yields lines from *content* + when the path is /proc/cpuinfo, and delegates otherwise.""" + import builtins + import io + + real_open = builtins.open + + def patched_open(path, *a, **kw): + if str(path) == "/proc/cpuinfo": + return io.StringIO(content) + return real_open(path, *a, **kw) + + return patched_open + + +def _fake_vmem(total: int): + """Return a callable that mimics psutil.virtual_memory().""" + from collections import namedtuple + + Vmem = namedtuple("svmem", ["total"]) + + def _inner(): + return Vmem(total=total) + + return _inner + + +def _read_dt_model(path: Path) -> str | None: + """Simulate _cpu_name_from_devicetree reading from a custom path.""" + import re + + try: + raw = path.read_bytes() + model = raw.decode("utf-8", errors="replace").strip().rstrip("\x00") + if not model: + return None + m = re.search(r"\b(M\d+(?:\s+(?:Pro|Max|Ultra))?)\b", model) + if m: + return f"Apple {m.group(1)}" + return model + except OSError: + return None + + +def _extract(model: str) -> str | None: + """Run the chip-name regex against a model string.""" + import re + + m = re.search(r"\b(M\d+(?:\s+(?:Pro|Max|Ultra))?)\b", model) + if m: + return f"Apple {m.group(1)}" + return None diff --git a/tests/test_benchmark_lookup.py b/tests/test_benchmark_lookup.py new file mode 100644 index 0000000..c531b82 --- /dev/null +++ b/tests/test_benchmark_lookup.py @@ -0,0 +1,182 @@ +"""Tests for benchmark lookup direct/inherited semantics.""" + +import asyncio + +import whichllm.models.benchmark_sources as benchmark_sources +from whichllm.models.benchmark import ( + _lineage_recency_factor, + build_line_bucket_index, + build_score_index, + fetch_benchmark_scores, + lookup_benchmark, + lookup_benchmark_evidence, +) + + +def test_fetch_benchmark_scores_disables_brotli_accept_encoding(monkeypatch): + encodings: list[str] = [] + + async def fake_source(client): + encodings.append(client.headers["accept-encoding"]) + return {} + + monkeypatch.setattr( + benchmark_sources, "fetch_leaderboard_with_fallback", fake_source + ) + monkeypatch.setattr(benchmark_sources, "fetch_arena_scores", fake_source) + monkeypatch.setattr(benchmark_sources, "fetch_aa_index_scores", fake_source) + monkeypatch.setattr(benchmark_sources, "fetch_aider_polyglot_scores", fake_source) + monkeypatch.setattr(benchmark_sources, "fetch_vision_scores", fake_source) + monkeypatch.setattr(benchmark_sources, "get_livebench_data", lambda: {}) + + assert asyncio.run(fetch_benchmark_scores()) == {} + assert set(encodings) == {"gzip, deflate"} + + +def test_lookup_benchmark_model_id_match_is_direct(): + scores = {"Qwen/Qwen2.5-7B-Instruct": 70.0} + ci, line = build_score_index(scores) + result = lookup_benchmark( + "Qwen/Qwen2.5-7B-Instruct", + None, + scores, + ci, + line, + ) + assert result == (70.0, True) + + +def test_lookup_benchmark_base_model_match_is_inherited(): + scores = {"google/gemma-3-27b-it": 82.2} + ci, line = build_score_index(scores) + result = lookup_benchmark( + "ISTA-DASLab/gemma-3-27b-it-GPTQ-4b-128g", + "google/gemma-3-27b-it", + scores, + ci, + line, + ) + assert result == (82.2, False) + + +def test_lookup_benchmark_gguf_suffix_match_is_inherited(): + scores = {"Qwen/Qwen2.5-7B-Instruct": 70.0} + ci, line = build_score_index(scores) + result = lookup_benchmark( + "Qwen/Qwen2.5-7B-Instruct-GGUF", + None, + scores, + ci, + line, + ) + assert result == (70.0, False) + + +def test_lookup_benchmark_community_gguf_without_base_model_matches_official_id(): + scores = {"Qwen/Qwen3.6-27B": 83.5} + ci, line = build_score_index(scores) + buckets = build_line_bucket_index(scores) + result = lookup_benchmark_evidence( + "unsloth/Qwen3.6-27B-GGUF", + None, + scores, + ci, + line, + buckets, + ) + assert result.source == "variant" + assert result.score == 83.5 + + +def test_lookup_benchmark_community_gguf_underscore_name_matches_official_id(): + scores = {"Qwen/Qwen3.6-35B-A3B": 86.0} + ci, line = build_score_index(scores) + buckets = build_line_bucket_index(scores) + result = lookup_benchmark_evidence( + "unsloth/Qwen_Qwen3.6-35B-A3B-GGUF", + None, + scores, + ci, + line, + buckets, + ) + assert result.source == "variant" + assert result.score == 86.0 + + +def test_lookup_benchmark_community_gguf_keeps_params_guard(): + scores = {"Qwen/Qwen3.6-27B": 83.5} + ci, line = build_score_index(scores) + buckets = build_line_bucket_index(scores) + result = lookup_benchmark_evidence( + "unsloth/Qwen3.6-27B-GGUF", + None, + scores, + ci, + line, + buckets, + actual_params_b=6.6, + ) + assert result.source != "variant" + + +def test_lookup_benchmark_community_gguf_beats_self_reported_score(): + scores = {"Qwen/Qwen3.6-27B": 83.5} + ci, line = build_score_index(scores) + buckets = build_line_bucket_index(scores) + result = lookup_benchmark_evidence( + "unsloth/Qwen3.6-27B-GGUF", + None, + scores, + ci, + line, + buckets, + self_reported_score=12.0, + ) + assert result.source == "variant" + assert result.score == 83.5 + + +def test_lookup_benchmark_evidence_direct_has_full_confidence(): + scores = {"Qwen/Qwen2.5-7B-Instruct": 70.0} + ci, line = build_score_index(scores) + buckets = build_line_bucket_index(scores) + result = lookup_benchmark_evidence( + "Qwen/Qwen2.5-7B-Instruct", + None, + scores, + ci, + line, + buckets, + ) + assert result.source == "direct" + assert result.confidence == 1.0 + assert result.score == 70.0 + + +def test_lookup_benchmark_evidence_line_uses_size_aware_interpolation(): + scores = { + "Qwen/Qwen3-8B-Instruct": 65.0, + "Qwen/Qwen3-32B-Instruct": 85.0, + } + ci, line = build_score_index(scores) + buckets = build_line_bucket_index(scores) + result = lookup_benchmark_evidence( + "Qwen/Qwen3-14B-Instruct-GGUF", + None, + scores, + ci, + line, + buckets, + ) + assert result.source == "line_interp" + assert result.score is not None + assert 65.0 < result.score < 85.0 + assert result.confidence > 0.2 + + +def test_lineage_recency_t5gemma_variants_are_not_demoted_as_old_gemma(): + assert _lineage_recency_factor("google/t5gemma-4b") == 1.0 + assert _lineage_recency_factor("google/t5-gemma-4b") == 1.0 + assert _lineage_recency_factor("google/t5_gemma-4b") == 1.0 + assert _lineage_recency_factor("google/gemma-2-2b") < 1.0 diff --git a/tests/test_cache_encoding.py b/tests/test_cache_encoding.py new file mode 100644 index 0000000..53df9db --- /dev/null +++ b/tests/test_cache_encoding.py @@ -0,0 +1,65 @@ +"""Regression tests for cross-platform cache encoding.""" + +from __future__ import annotations + +import json +import time + +import whichllm.models.benchmark as benchmark_mod +import whichllm.models.cache as cache_mod + + +class _ReadableCacheFile: + def __init__(self, payload: dict): + self.payload = payload + self.encoding = None + + def exists(self) -> bool: + return True + + def read_text(self, *, encoding: str | None = None) -> str: + self.encoding = encoding + return json.dumps(self.payload, ensure_ascii=False) + + +class _WritableCacheFile: + def __init__(self): + self.encoding = None + self.text = None + + def write_text(self, text: str, *, encoding: str | None = None) -> int: + self.encoding = encoding + self.text = text + return len(text) + + +def test_model_cache_reads_and_writes_utf8(monkeypatch, tmp_path): + reader = _ReadableCacheFile( + {"cached_at": time.time(), "models": [{"id": "test/Omega-Ω"}]} + ) + monkeypatch.setattr(cache_mod, "CACHE_FILE", reader) + assert cache_mod.load_cache() == [{"id": "test/Omega-Ω"}] + assert reader.encoding == "utf-8" + + writer = _WritableCacheFile() + monkeypatch.setattr(cache_mod, "CACHE_DIR", tmp_path) + monkeypatch.setattr(cache_mod, "CACHE_FILE", writer) + cache_mod.save_cache([{"id": "test/Omega-Ω"}]) + assert writer.encoding == "utf-8" + assert "Ω" in writer.text + + +def test_benchmark_cache_reads_and_writes_utf8(monkeypatch, tmp_path): + reader = _ReadableCacheFile( + {"cached_at": time.time(), "scores": {"test/Omega-Ω": 1.0}} + ) + monkeypatch.setattr(benchmark_mod, "BENCHMARK_CACHE", reader) + assert benchmark_mod.load_benchmark_cache() == {"test/Omega-Ω": 1.0} + assert reader.encoding == "utf-8" + + writer = _WritableCacheFile() + monkeypatch.setattr(benchmark_mod, "CACHE_DIR", tmp_path) + monkeypatch.setattr(benchmark_mod, "BENCHMARK_CACHE", writer) + benchmark_mod.save_benchmark_cache({"test/Omega-Ω": 1.0}) + assert writer.encoding == "utf-8" + assert "Ω" in writer.text diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 0000000..5ebf8fb --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,1381 @@ +"""Tests for CLI helper logic.""" + +import httpx +import pytest +from typer import Exit + +import whichllm.cli as cli_mod +import whichllm.__main__ as main_mod +from whichllm.cli import ( + _apply_memory_budgets, + _apply_gpu_overrides, + _auto_min_params_for_profile, + _extract_id_size_b, + _fill_missing_published_at, + _format_fetch_error, + _generate_chat_script, + _include_vision_candidates, + _merge_model_eval_benchmarks, + _parse_memory_amount, + _pick_gguf_variant, + _resolve_ranked_gguf_for_run, + _resolve_evidence_mode, + _resolve_fit_filter, + _resolve_speed_filter, + _search_model, + _validate_evidence, + _validate_gpu_flags, + app, +) +from whichllm.utils import _current_version +from whichllm.engine.types import CompatibilityResult +from whichllm.hardware.types import GPUInfo, HardwareInfo +from whichllm.models.types import GGUFVariant, ModelInfo +from typer.testing import CliRunner + + +def _hw_with_gpu(vram_gb: int) -> HardwareInfo: + return HardwareInfo( + gpus=[ + GPUInfo( + name="GPU", + vendor="nvidia", + vram_bytes=vram_gb * 1024**3, + memory_bandwidth_gbps=1.0, + ) + ], + cpu_name="CPU", + cpu_cores=1, + ram_bytes=16 * 1024**3, + disk_free_bytes=100 * 1024**3, + os="linux", + ) + + +def test_auto_min_params_general_by_vram(): + # Updated thresholds: tiny GPUs (4-8GB) get a lower floor so they can + # surface full-GPU 3-4B models instead of being forced into 7B+ + # partial-offload-only candidates. + assert _auto_min_params_for_profile(_hw_with_gpu(4), "general") == 2.0 + assert _auto_min_params_for_profile(_hw_with_gpu(6), "general") == 3.0 + assert _auto_min_params_for_profile(_hw_with_gpu(8), "general") == 5.0 + assert _auto_min_params_for_profile(_hw_with_gpu(12), "general") == 8.0 + assert _auto_min_params_for_profile(_hw_with_gpu(24), "general") == 10.0 + assert _auto_min_params_for_profile(_hw_with_gpu(32), "general") == 12.0 + + +def test_auto_min_params_non_general_disabled(): + assert _auto_min_params_for_profile(_hw_with_gpu(24), "coding") is None + + +def test_auto_min_params_uses_usable_vram_budget(): + hw = _hw_with_gpu(20) + hw.gpus[0].usable_vram_bytes = int(19.0 * 1024**3) + + assert _auto_min_params_for_profile(hw, "general") == 8.0 + + +def test_auto_min_params_uses_ram_budget_for_shared_memory_gpu(): + hw = HardwareInfo( + gpus=[ + GPUInfo( + name="Apple M2", + vendor="apple", + vram_bytes=16 * 1024**3, + usable_vram_bytes=15 * 1024**3, + shared_memory=True, + ) + ], + ram_bytes=16 * 1024**3, + ram_budget_bytes=4 * 1024**3, + ) + + assert _auto_min_params_for_profile(hw, "general") == 2.0 + + +def test_apply_gpu_overrides_accepts_multiple_simulated_gpus(): + hw = HardwareInfo(gpus=[], ram_bytes=64 * 1024**3, os="linux") + + _apply_gpu_overrides(hw, cpu_only=False, gpu=["2x RTX 4090"], vram=None) + + assert len(hw.gpus) == 2 + assert all(gpu.vendor == "nvidia" for gpu in hw.gpus) + assert all(gpu.vram_bytes == 24 * 1024**3 for gpu in hw.gpus) + + +def test_validate_gpu_flags_allows_detected_vram_override(): + _validate_gpu_flags(cpu_only=False, gpu=None, vram=8.0, bandwidth=None) + + +def test_validate_gpu_flags_rejects_non_positive_overrides(): + with pytest.raises(Exit): + _validate_gpu_flags(cpu_only=False, gpu=None, vram=0, bandwidth=None) + with pytest.raises(Exit): + _validate_gpu_flags(cpu_only=False, gpu=None, vram=None, bandwidth=-1) + + +def test_validate_gpu_flags_rejects_gpu_index_with_simulated_gpu(): + with pytest.raises(Exit): + _validate_gpu_flags( + cpu_only=False, + gpu=["RTX 4090"], + vram=24.0, + bandwidth=None, + gpu_index=0, + ) + + +def test_apply_gpu_overrides_updates_detected_shared_memory_gpu(): + hw = HardwareInfo( + gpus=[ + GPUInfo( + name="Intel UHD Graphics", + vendor="intel", + vram_bytes=0, + shared_memory=True, + memory_bandwidth_gbps=None, + ) + ], + cpu_name="CPU", + cpu_cores=8, + ram_bytes=32 * 1024**3, + disk_free_bytes=100 * 1024**3, + os="linux", + ) + + _apply_gpu_overrides(hw, cpu_only=False, gpu=None, vram=6.0, bandwidth=88.5) + + assert hw.gpus[0].vram_bytes == 6 * 1024**3 + assert hw.gpus[0].usable_vram_bytes is None + assert hw.gpus[0].memory_bandwidth_gbps == 88.5 + assert hw.gpus[0].shared_memory is True + assert hw.gpus[0].vram_overridden is True + + +def test_apply_gpu_overrides_updates_selected_detected_gpu_only(): + hw = HardwareInfo( + gpus=[ + GPUInfo( + name="NVIDIA RTX 4060", + vendor="nvidia", + vram_bytes=8 * 1024**3, + memory_bandwidth_gbps=272.0, + ), + GPUInfo( + name="Intel UHD Graphics", + vendor="intel", + vram_bytes=0, + shared_memory=True, + ), + ], + cpu_name="CPU", + cpu_cores=8, + ram_bytes=32 * 1024**3, + disk_free_bytes=100 * 1024**3, + os="linux", + ) + + _apply_gpu_overrides( + hw, cpu_only=False, gpu=None, vram=4.0, bandwidth=60.0, gpu_index=1 + ) + + assert hw.gpus[0].vram_bytes == 8 * 1024**3 + assert hw.gpus[0].memory_bandwidth_gbps == 272.0 + assert hw.gpus[1].vram_bytes == 4 * 1024**3 + assert hw.gpus[1].memory_bandwidth_gbps == 60.0 + assert hw.gpus[1].vram_overridden is True + + +def test_apply_gpu_overrides_requires_gpu_index_for_multiple_detected_gpus(): + hw = HardwareInfo( + gpus=[ + GPUInfo(name="GPU 0", vendor="nvidia", vram_bytes=8 * 1024**3), + GPUInfo(name="GPU 1", vendor="intel", vram_bytes=0, shared_memory=True), + ], + cpu_name="CPU", + cpu_cores=8, + ram_bytes=32 * 1024**3, + disk_free_bytes=100 * 1024**3, + os="linux", + ) + + with pytest.raises(Exit): + _apply_gpu_overrides(hw, cpu_only=False, gpu=None, vram=4.0) + + +def test_apply_gpu_overrides_updates_simulated_gpu_bandwidth(): + hw = HardwareInfo(gpus=[], ram_bytes=32 * 1024**3, os="linux") + + _apply_gpu_overrides( + hw, cpu_only=False, gpu=["Unknown GPU"], vram=4.0, bandwidth=72.0 + ) + + assert len(hw.gpus) == 1 + assert hw.gpus[0].vram_bytes == 4 * 1024**3 + assert hw.gpus[0].memory_bandwidth_gbps == 72.0 + + +def test_apply_gpu_overrides_rejects_override_without_gpu(): + hw = HardwareInfo(gpus=[], ram_bytes=32 * 1024**3, os="linux") + + with pytest.raises(Exit): + _apply_gpu_overrides(hw, cpu_only=False, gpu=None, vram=4.0) + + +def test_include_vision_candidates_by_profile(): + assert _include_vision_candidates("vision") is True + assert _include_vision_candidates("any") is True + assert _include_vision_candidates("general") is False + assert _include_vision_candidates("coding") is False + + +def test_fill_missing_published_at_updates_models(): + model = ModelInfo( + id="Qwen/Qwen3-8B-AWQ", + family_id="qwen3-8b", + name="Qwen3-8B-AWQ", + parameter_count=8_000_000_000, + downloads=1, + likes=1, + ) + result = CompatibilityResult( + model=model, + gguf_variant=None, + can_run=True, + vram_required_bytes=0, + vram_available_bytes=0, + ) + + async def _fake_fetch(ids: list[str]) -> dict[str, str]: + assert ids == ["Qwen/Qwen3-8B-AWQ"] + return {"Qwen/Qwen3-8B-AWQ": "2026-03-05T08:00:00.000Z"} + + updated = _fill_missing_published_at([model], [result], _fake_fetch) + assert updated is True + assert model.published_at == "2026-03-05T08:00:00.000Z" + + +def test_version_option_prints_version_and_exits(): + runner = CliRunner() + result = runner.invoke(app, ["--version"]) + assert result.exit_code == 0 + assert _current_version() in result.stdout + + +def test_module_entrypoint_uses_cli_app(): + assert main_mod.app is app + + +def test_format_fetch_error_uses_exception_class_when_message_is_empty(): + class EmptyNetworkError(Exception): + def __str__(self) -> str: + return "" + + assert _format_fetch_error(EmptyNetworkError()) == ( + "EmptyNetworkError with no detail from the network layer" + ) + + +def test_format_fetch_error_includes_status_and_url_for_empty_http_error(): + request = httpx.Request("GET", "https://huggingface.co/api/models") + response = httpx.Response(429, request=request) + error = httpx.HTTPStatusError("", request=request, response=response) + + assert _format_fetch_error(error) == ( + "HTTPStatusError: HTTP 429 for https://huggingface.co/api/models" + ) + + +def test_merge_model_eval_benchmarks_is_now_a_noop(): + """As of the self_reported evidence tier, _merge_model_eval_benchmarks + must NOT mutate the leaderboard scores. Uploader-reported hf_eval values + are consumed directly by the ranker as a separate, low-trust source. + """ + model_direct_missing = ModelInfo( + id="meta-llama/Llama-3.1-8B-Instruct", + family_id="llama-3.1-8b", + name="Llama-3.1-8B-Instruct", + parameter_count=8_000_000_000, + downloads=1, + likes=1, + benchmark_scores={"hf_eval": 66.4}, + ) + model_already_present = ModelInfo( + id="Qwen/Qwen2.5-7B-Instruct", + family_id="qwen2.5-7b", + name="Qwen2.5-7B-Instruct", + parameter_count=7_000_000_000, + downloads=1, + likes=1, + benchmark_scores={"hf_eval": 70.0}, + ) + original = {"Qwen/Qwen2.5-7B-Instruct": 71.2} + merged, injected = _merge_model_eval_benchmarks( + [model_direct_missing, model_already_present], + original, + ) + # Function is a deprecation no-op now. + assert injected == 0 + assert merged is original or merged == original + # Critically, the uploader-reported value MUST NOT have been injected + # under the model id, because doing so would make it appear as a + # direct leaderboard hit. + assert "meta-llama/Llama-3.1-8B-Instruct" not in merged + + +def test_validate_evidence_accepts_all_modes(): + assert _validate_evidence("strict") == "strict" + assert _validate_evidence("base") == "base" + assert _validate_evidence("any") == "any" + + +def test_validate_evidence_rejects_unknown_mode(): + with pytest.raises(Exit): + _validate_evidence("foo") + + +def test_resolve_evidence_mode_direct_alias_wins(): + assert _resolve_evidence_mode("base", direct=True) == "strict" + + +def test_resolve_fit_filter_accepts_gpu_only_alias(): + assert _resolve_fit_filter("any", gpu_only=False) == "any" + assert _resolve_fit_filter("gpu", gpu_only=False) == "full_gpu" + assert _resolve_fit_filter("full-gpu", gpu_only=False) == "full_gpu" + assert _resolve_fit_filter("full_gpu", gpu_only=False) == "full_gpu" + assert _resolve_fit_filter("any", gpu_only=True) == "full_gpu" + + +def test_resolve_fit_filter_rejects_unknown_mode(): + with pytest.raises(Exit): + _resolve_fit_filter("partial", gpu_only=False) + + +def test_resolve_speed_filter_presets_and_min_speed_override(): + assert _resolve_speed_filter("any", min_speed=None) is None + assert _resolve_speed_filter("usable", min_speed=None) == 10.0 + assert _resolve_speed_filter("fast", min_speed=None) == 30.0 + assert _resolve_speed_filter("fast", min_speed=2.5) == 2.5 + + +def test_resolve_speed_filter_rejects_unknown_mode(): + with pytest.raises(Exit): + _resolve_speed_filter("slowish", min_speed=None) + + +def test_parse_memory_amount_supports_gb_mb_and_percent(): + assert _parse_memory_amount("1.5GB", option_name="--x") == int(1.5 * 1024**3) + assert _parse_memory_amount("512MB", option_name="--x") == 512 * 1024**2 + assert _parse_memory_amount("8", option_name="--x") == 8 * 1024**3 + assert ( + _parse_memory_amount("10%", option_name="--x", total_bytes=20 * 1024**3) + == 2 * 1024**3 + ) + + +def test_apply_memory_budgets_sets_vram_headroom_and_ram_budget(): + hw = _hw_with_gpu(16) + + _apply_memory_budgets(hw, vram_headroom="1GB", ram_budget="8GB") + + assert hw.gpus[0].vram_bytes == 16 * 1024**3 + assert hw.gpus[0].usable_vram_bytes == 15 * 1024**3 + assert hw.ram_budget_bytes == 8 * 1024**3 + assert any("VRAM headroom" in note for note in hw.budget_notes) + assert any("RAM budget" in note for note in hw.budget_notes) + + +def test_apply_memory_budgets_validates_vram_headroom_without_gpus(): + hw = HardwareInfo(gpus=[], ram_bytes=16 * 1024**3) + + with pytest.raises(Exit): + _apply_memory_budgets(hw, vram_headroom="nope", ram_budget=None) + + +def test_apply_memory_budgets_accepts_valid_noop_vram_headroom_without_gpus(): + hw = HardwareInfo(gpus=[], ram_bytes=16 * 1024**3) + + _apply_memory_budgets(hw, vram_headroom="10%", ram_budget=None) + + assert hw.gpus == [] + assert hw.ram_budget_bytes is None + + +def test_main_passes_gpu_only_fit_filter(monkeypatch): + model = ModelInfo( + id="org/Test-7B", + family_id="test-7b", + name="Test-7B", + parameter_count=7_000_000_000, + downloads=1, + likes=1, + published_at="2026-01-01T00:00:00.000Z", + ) + captured: dict[str, object] = {} + + def fake_rank_models(models, hardware, **kwargs): + captured["fit_filter"] = kwargs.get("fit_filter") + return [ + CompatibilityResult( + model=model, + gguf_variant=None, + can_run=True, + vram_required_bytes=4 * 1024**3, + vram_available_bytes=8 * 1024**3, + fit_type="full_gpu", + quality_score=80.0, + ) + ] + + monkeypatch.setattr( + "whichllm.hardware.detector.detect_hardware", lambda: _hw_with_gpu(8) + ) + monkeypatch.setattr("whichllm.models.cache.load_cache", lambda: []) + monkeypatch.setattr("whichllm.models.benchmark.load_benchmark_cache", lambda: {}) + monkeypatch.setattr("whichllm.engine.ranker.rank_models", fake_rank_models) + monkeypatch.setattr( + "whichllm.output.display.display_hardware", lambda hardware: None + ) + monkeypatch.setattr( + "whichllm.output.display.display_ranking", + lambda results, **kwargs: None, + ) + + result = CliRunner().invoke(app, ["--gpu-only"]) + + assert result.exit_code == 0 + assert captured["fit_filter"] == "full_gpu" + + +def test_main_passes_speed_preset_and_default_runtime_columns(monkeypatch): + model = ModelInfo( + id="org/Test-7B", + family_id="test-7b", + name="Test-7B", + parameter_count=7_000_000_000, + downloads=1, + likes=1, + published_at="2026-01-01T00:00:00.000Z", + ) + captured: dict[str, object] = {} + + def fake_rank_models(models, hardware, **kwargs): + captured["min_speed"] = kwargs.get("min_speed") + return [ + CompatibilityResult( + model=model, + gguf_variant=None, + can_run=True, + vram_required_bytes=4 * 1024**3, + vram_available_bytes=8 * 1024**3, + fit_type="full_gpu", + estimated_tok_per_sec=8.0, + quality_score=80.0, + ) + ] + + def fake_display_ranking(results, **kwargs): + captured["show_status"] = kwargs.get("show_status") + + monkeypatch.setattr( + "whichllm.hardware.detector.detect_hardware", lambda: _hw_with_gpu(8) + ) + monkeypatch.setattr("whichllm.models.cache.load_cache", lambda: []) + monkeypatch.setattr("whichllm.models.benchmark.load_benchmark_cache", lambda: {}) + monkeypatch.setattr("whichllm.engine.ranker.rank_models", fake_rank_models) + monkeypatch.setattr( + "whichllm.output.display.display_hardware", lambda hardware: None + ) + monkeypatch.setattr("whichllm.output.display.display_ranking", fake_display_ranking) + + result = CliRunner().invoke(app, ["--speed", "usable"]) + + assert result.exit_code == 0 + assert captured["min_speed"] == 10.0 + assert captured["show_status"] is True + + +def test_main_details_flag_restores_metadata_columns(monkeypatch): + captured: dict[str, object] = {} + + def fake_rank_models(models, hardware, **kwargs): + return [] + + def fake_display_ranking(results, **kwargs): + captured["show_status"] = kwargs.get("show_status") + + monkeypatch.setattr( + "whichllm.hardware.detector.detect_hardware", lambda: _hw_with_gpu(8) + ) + monkeypatch.setattr("whichllm.models.cache.load_cache", lambda: []) + monkeypatch.setattr("whichllm.models.benchmark.load_benchmark_cache", lambda: {}) + monkeypatch.setattr("whichllm.engine.ranker.rank_models", fake_rank_models) + monkeypatch.setattr( + "whichllm.output.display.display_hardware", lambda hardware: None + ) + monkeypatch.setattr("whichllm.output.display.display_ranking", fake_display_ranking) + + result = CliRunner().invoke(app, ["--details", "--min-params", "1"]) + + assert result.exit_code == 0 + assert captured["show_status"] is False + + +def test_main_markdown_alias_dispatches_markdown_output(monkeypatch): + model = ModelInfo( + id="org/Test-7B", + family_id="test-7b", + name="Test-7B", + parameter_count=7_000_000_000, + downloads=1, + likes=1, + published_at="2026-01-01T00:00:00.000Z", + ) + captured: dict[str, object] = {} + + def fake_rank_models(models, hardware, **kwargs): + return [ + CompatibilityResult( + model=model, + gguf_variant=None, + can_run=True, + vram_required_bytes=4 * 1024**3, + vram_available_bytes=8 * 1024**3, + fit_type="full_gpu", + estimated_tok_per_sec=12.0, + quality_score=80.0, + ) + ] + + def fake_display_markdown(results, hardware, **kwargs): + captured["called"] = True + captured["show_status"] = kwargs.get("show_status") + + def fail_display_hardware(hardware): + raise AssertionError("markdown output should not render Rich hardware panel") + + monkeypatch.setattr( + "whichllm.hardware.detector.detect_hardware", lambda: _hw_with_gpu(8) + ) + monkeypatch.setattr("whichllm.models.cache.load_cache", lambda: []) + monkeypatch.setattr("whichllm.models.benchmark.load_benchmark_cache", lambda: {}) + monkeypatch.setattr("whichllm.engine.ranker.rank_models", fake_rank_models) + monkeypatch.setattr( + "whichllm.output.display.display_markdown", fake_display_markdown + ) + monkeypatch.setattr( + "whichllm.output.display.display_hardware", fail_display_hardware + ) + + result = CliRunner().invoke(app, ["-m"]) + + assert result.exit_code == 0 + assert captured["called"] is True + assert captured["show_status"] is True + + +def test_main_json_and_markdown_are_mutually_exclusive(): + result = CliRunner().invoke(app, ["--json", "--markdown"]) + + assert result.exit_code == 1 + assert "--json and --markdown are mutually exclusive" in result.stdout + + +def test_main_top_zero_rejected(): + result = CliRunner().invoke(app, ["--top", "0"]) + + assert result.exit_code == 1 + assert "--top must be 1 or greater" in result.stdout + + +def test_main_top_negative_rejected(): + # ``results[:-5]`` would silently drop the best-ranked models. + result = CliRunner().invoke(app, ["--top=-5"]) + + assert result.exit_code == 1 + assert "--top must be 1 or greater" in result.stdout + + +def test_main_negative_min_speed_rejected(): + result = CliRunner().invoke(app, ["--min-speed=-1"]) + + assert result.exit_code == 1 + assert "--min-speed must be 0 or greater" in result.stdout + + +def test_main_negative_min_params_rejected(): + result = CliRunner().invoke(app, ["--min-params=-2"]) + + assert result.exit_code == 1 + assert "--min-params must be 0 or greater" in result.stdout + + +def test_validate_ranking_flags_accepts_valid_values(): + # A valid combination must not raise (no recommendations are dropped). + cli_mod._validate_ranking_flags(top=10, min_speed=0.0, min_params=None) + cli_mod._validate_ranking_flags(top=1, min_speed=None, min_params=7.0) + + +def test_upgrade_top_zero_rejected(): + # The upgrade command shares the same --top -> results[:top_n] hazard. + result = CliRunner().invoke(app, ["upgrade", "RTX 4090", "--top", "0"]) + + assert result.exit_code == 1 + assert "--top must be 1 or greater" in result.stdout + + +def test_main_empty_gpu_only_result_shows_fit_message(monkeypatch): + captured: dict[str, object] = {} + + def fake_rank_models(models, hardware, **kwargs): + return [] + + def fake_display_ranking(results, **kwargs): + captured["empty_message"] = kwargs.get("empty_message") + + monkeypatch.setattr( + "whichllm.hardware.detector.detect_hardware", lambda: _hw_with_gpu(8) + ) + monkeypatch.setattr("whichllm.models.cache.load_cache", lambda: []) + monkeypatch.setattr("whichllm.models.benchmark.load_benchmark_cache", lambda: {}) + monkeypatch.setattr("whichllm.engine.ranker.rank_models", fake_rank_models) + monkeypatch.setattr( + "whichllm.output.display.display_hardware", lambda hardware: None + ) + monkeypatch.setattr("whichllm.output.display.display_ranking", fake_display_ranking) + + result = CliRunner().invoke(app, ["--fit", "full-gpu", "--min-params", "1"]) + + assert result.exit_code == 0 + assert "No full-GPU models found" in captured["empty_message"] + + +# --------------- plan command tests --------------- + + +def test_plan_no_model_found_shows_error(monkeypatch): + monkeypatch.setattr("whichllm.models.cache.load_cache", lambda: []) + runner = CliRunner() + result = runner.invoke(app, ["plan", "nonexistent_model_xyz_999"]) + assert result.exit_code != 0 + assert "No model found" in result.stdout + + +def test_plan_display_plan_renders_tables(): + """display_plan should render model info, VRAM table, and GPU table.""" + from whichllm.output.display import display_plan + + model = ModelInfo( + id="test-org/Test-Model-7B-GGUF", + family_id="test-7b", + name="Test-Model-7B", + parameter_count=7_000_000_000, + architecture="llama", + context_length=4096, + license="mit", + downloads=100, + likes=10, + ) + # Should not raise + display_plan(model, context_length=4096, target_quant="Q4_K_M") + + +def test_plan_display_plan_json_outputs_valid_json(): + """display_plan_json should output valid JSON.""" + import json as json_mod + from io import StringIO + + from rich.console import Console + + from whichllm.output.display import display_plan_json + + model = ModelInfo( + id="test-org/Test-Model-7B-GGUF", + family_id="test-7b", + name="Test-Model-7B", + parameter_count=7_000_000_000, + architecture="llama", + context_length=4096, + license="mit", + downloads=100, + likes=10, + ) + # Capture output + buf = StringIO() + import whichllm.output._console as console_mod + + orig_console = console_mod.console + console_mod.console = Console(file=buf, force_terminal=False) + try: + display_plan_json(model, context_length=4096, target_quant="Q4_K_M") + finally: + console_mod.console = orig_console + raw = buf.getvalue().strip() + data = json_mod.loads(raw) + assert data["model"]["id"] == "test-org/Test-Model-7B-GGUF" + assert "vram_by_quant" in data + assert "gpu_compatibility" in data + assert data["target_quant"] == "Q4_K_M" + + +# --------------- helper tests --------------- + + +def _make_model( + model_id="org/Test-7B-GGUF", + downloads=100, + gguf_variants=None, + parameter_count=7_000_000_000, +): + return ModelInfo( + id=model_id, + family_id="test-7b", + name="Test-7B", + parameter_count=parameter_count, + downloads=downloads, + likes=10, + gguf_variants=gguf_variants or [], + ) + + +def test_search_model_exact_match(): + models = [_make_model("org/Llama-8B"), _make_model("org/Qwen-7B")] + result = _search_model(models, "org/Llama-8B") + assert result.id == "org/Llama-8B" + + +def test_search_model_endswith_match(): + models = [_make_model("org/Llama-8B"), _make_model("org/Qwen-7B")] + result = _search_model(models, "Llama-8B") + assert result.id == "org/Llama-8B" + + +def test_search_model_term_match(): + models = [_make_model("org/Llama-3.1-8B-GGUF"), _make_model("org/Qwen-7B")] + result = _search_model(models, "llama 8b") + assert result.id == "org/Llama-3.1-8B-GGUF" + + +def test_search_model_not_found(): + models = [_make_model("org/Llama-8B")] + with pytest.raises(Exit): + _search_model(models, "nonexistent_xyz") + + +# --- regression tests for size-token substring matching (#107) --- + + +def test_search_model_7b_does_not_match_1_7b(): + """'qwen 7b' should NOT match Qwen3-1.7B (issue #107).""" + models = [ + _make_model( + "org/Qwen3-1.7B-GGUF", downloads=9999, parameter_count=1_700_000_000 + ), + _make_model("org/Qwen3-7B-GGUF", downloads=100, parameter_count=7_000_000_000), + ] + result = _search_model(models, "qwen 7b") + assert result.id == "org/Qwen3-7B-GGUF" + + +def test_search_model_2b_does_not_match_12b(): + """'gemma 2b' should NOT match gemma-3-12b-it (issue #107).""" + models = [ + _make_model( + "google/gemma-3-12b-it", downloads=5000, parameter_count=12_000_000_000 + ), + _make_model("google/gemma-2b", downloads=100, parameter_count=2_000_000_000), + ] + result = _search_model(models, "gemma 2b") + assert result.id == "google/gemma-2b" + + +def test_search_model_3b_does_not_match_30b_a3b(): + """'qwen 3b' should NOT match Qwen3-30B-A3B (issue #107).""" + models = [ + _make_model( + "org/Qwen3-30B-A3B-GGUF", downloads=8000, parameter_count=30_000_000_000 + ), + _make_model("org/Qwen3-3B-GGUF", downloads=50, parameter_count=3_000_000_000), + ] + result = _search_model(models, "qwen 3b") + assert result.id == "org/Qwen3-3B-GGUF" + + +def test_search_model_no_size_token_still_works(): + """Queries without a size token should still use plain substring matching.""" + models = [ + _make_model("org/Llama-3.1-8B-GGUF", parameter_count=8_000_000_000), + _make_model("org/Qwen-7B", parameter_count=7_000_000_000), + ] + result = _search_model(models, "llama gguf") + assert result.id == "org/Llama-3.1-8B-GGUF" + + +def test_search_model_millions_size_token(): + """'500m' size token should match a ~500M parameter model.""" + models = [ + _make_model("org/SmolLM-500M", downloads=200, parameter_count=500_000_000), + _make_model("org/SmolLM-1.7B", downloads=300, parameter_count=1_700_000_000), + ] + result = _search_model(models, "smollm 500m") + assert result.id == "org/SmolLM-500M" + + +def test_search_model_decimal_size_token(): + """'0.5b' size token should match a ~500M parameter model.""" + models = [ + _make_model("org/TinyModel-0.5B", downloads=100, parameter_count=500_000_000), + _make_model("org/TinyModel-3B", downloads=500, parameter_count=3_000_000_000), + ] + result = _search_model(models, "tinymodel 0.5b") + assert result.id == "org/TinyModel-0.5B" + + +def test_search_model_zero_param_count_passes_through(): + """Models with parameter_count=0 (missing metadata) should not be excluded.""" + models = [ + _make_model("org/Mystery-7B-GGUF", downloads=500, parameter_count=0), + ] + result = _search_model(models, "mystery 7b") + assert result.id == "org/Mystery-7B-GGUF" + + +def test_search_model_first_size_token_wins(): + """When multiple size tokens appear, only the first is used as a filter.""" + models = [ + _make_model( + "org/Qwen3-30B-A3B-GGUF", downloads=8000, parameter_count=30_000_000_000 + ), + _make_model( + "org/Qwen3-7B-3B-GGUF", downloads=100, parameter_count=7_000_000_000 + ), + ] + # "7b" is the first size token and filters by ~7B; "3b" becomes a text term + result = _search_model(models, "qwen 7b 3b") + assert result.id == "org/Qwen3-7B-3B-GGUF" + + +def test_search_model_7b_prefers_closest_size_over_downloads(): + """'qwen 7b' should pick 8B (closest to 7B) over 4B even if 4B has more downloads.""" + models = [ + _make_model("org/Qwen3-4B-GGUF", downloads=9000, parameter_count=4_000_000_000), + _make_model("org/Qwen3-8B-GGUF", downloads=100, parameter_count=8_000_000_000), + ] + result = _search_model(models, "qwen 7b") + assert result.id == "org/Qwen3-8B-GGUF" + + +def test_search_model_3b_prefers_closest_size_over_downloads(): + """'qwen 3b' should pick 3B (exact) over 4B even if 4B has more downloads.""" + models = [ + _make_model("org/Qwen3-4B-GGUF", downloads=9000, parameter_count=4_000_000_000), + _make_model("org/Qwen3-3B-GGUF", downloads=50, parameter_count=3_000_000_000), + ] + result = _search_model(models, "qwen 3b") + assert result.id == "org/Qwen3-3B-GGUF" + + +def test_search_model_size_tiebreak_falls_back_to_downloads(): + """When two models are equally close in size, prefer the one with more downloads.""" + models = [ + _make_model( + "org/Qwen3-8B-A-GGUF", downloads=100, parameter_count=8_000_000_000 + ), + _make_model( + "org/Qwen3-8B-B-GGUF", downloads=500, parameter_count=8_000_000_000 + ), + ] + result = _search_model(models, "qwen 7b") + assert result.id == "org/Qwen3-8B-B-GGUF" + + +def test_search_model_unknown_param_count_ranks_after_known(): + """Models with parameter_count=0 should rank after models with known sizes.""" + models = [ + _make_model("org/Qwen3-Unknown-GGUF", downloads=9999, parameter_count=0), + _make_model("org/Qwen3-7B-GGUF", downloads=10, parameter_count=7_000_000_000), + ] + result = _search_model(models, "qwen 7b") + assert result.id == "org/Qwen3-7B-GGUF" + + +@pytest.mark.parametrize( + "model_id, expected", + [ + ("org/Qwen3-8B-GGUF", 8.0), + ("org/Qwen3.5-9B-NVFP4", 9.0), + ("org/Qwen3-30B-A3B-GGUF", 30.0), + ("org/SmolLM-500M", 0.5), + ("org/TinyModel-1.7B-Chat", 1.7), + ("org/Llama-3.1-8B-Instruct", 8.0), + ("org/NoSizeLabel-GGUF", None), + ], +) +def test_extract_id_size_b(model_id, expected): + assert _extract_id_size_b(model_id) == expected + + +def test_search_model_7b_prefers_id_label_over_param_count(): + """'qwen 7b' should not pick a 9B-labeled repo even if its param count is closer.""" + models = [ + _make_model( + "org/Qwen3.5-9B-NVFP4", downloads=500, parameter_count=6_600_000_000 + ), + _make_model("org/Qwen3-8B-GGUF", downloads=100, parameter_count=8_000_000_000), + ] + result = _search_model(models, "qwen 7b") + assert result.id == "org/Qwen3-8B-GGUF" + + +def test_pick_gguf_variant_by_preference(): + variants = [ + GGUFVariant(filename="q2.gguf", quant_type="Q2_K", file_size_bytes=1000), + GGUFVariant(filename="q4km.gguf", quant_type="Q4_K_M", file_size_bytes=2000), + ] + model = _make_model(gguf_variants=variants) + result = _pick_gguf_variant(model) + assert result.quant_type == "Q4_K_M" + + +def test_pick_gguf_variant_with_filter(): + variants = [ + GGUFVariant(filename="q2.gguf", quant_type="Q2_K", file_size_bytes=1000), + GGUFVariant(filename="q4km.gguf", quant_type="Q4_K_M", file_size_bytes=2000), + ] + model = _make_model(gguf_variants=variants) + result = _pick_gguf_variant(model, quant_filter="Q2_K") + assert result.quant_type == "Q2_K" + + +def test_pick_gguf_variant_no_variants(): + model = _make_model(gguf_variants=[]) + result = _pick_gguf_variant(model) + assert result is None + + +def test_resolve_ranked_synthetic_gguf_to_real_repo(): + selected = ModelInfo( + id="Qwen/Qwen3.6-27B", + family_id="qwen3-27b", + name="Qwen3.6-27B", + parameter_count=27_000_000_000, + downloads=50_000, + ) + real_gguf = ModelInfo( + id="unsloth/Qwen3.6-27B-GGUF", + family_id="qwen3-27b", + name="Qwen3.6-27B-GGUF", + parameter_count=27_000_000_000, + downloads=200_000, + base_model="Qwen/Qwen3.6-27B", + gguf_variants=[ + GGUFVariant( + filename="Qwen3.6-27B-Q4_K_M.gguf", + quant_type="Q4_K_M", + file_size_bytes=16_000_000_000, + ) + ], + ) + synthetic = GGUFVariant( + filename="Qwen3.6-27B.Q4_K_M.gguf", + quant_type="Q4_K_M", + file_size_bytes=16_000_000_000, + ) + + resolved = _resolve_ranked_gguf_for_run(selected, synthetic, [selected, real_gguf]) + + assert resolved is not None + model, variant = resolved + assert model.id == "unsloth/Qwen3.6-27B-GGUF" + assert variant.filename == "Qwen3.6-27B-Q4_K_M.gguf" + + +def test_resolve_ranked_synthetic_gguf_prefers_exact_quant(): + selected = ModelInfo( + id="Qwen/Qwen3.6-27B", + family_id="qwen3-27b", + name="Qwen3.6-27B", + parameter_count=27_000_000_000, + ) + q5_only = ModelInfo( + id="converter/Qwen3.6-27B-GGUF", + family_id="qwen3-27b", + name="Qwen3.6-27B-GGUF", + parameter_count=27_000_000_000, + downloads=1_000_000, + gguf_variants=[ + GGUFVariant( + filename="q5.gguf", + quant_type="Q5_K_M", + file_size_bytes=18_000_000_000, + ) + ], + ) + q4_match = ModelInfo( + id="smaller/Qwen3.6-27B-GGUF", + family_id="qwen3-27b", + name="Qwen3.6-27B-GGUF", + parameter_count=27_000_000_000, + downloads=10, + gguf_variants=[ + GGUFVariant( + filename="q4.gguf", + quant_type="Q4_K_M", + file_size_bytes=16_000_000_000, + ) + ], + ) + synthetic = GGUFVariant( + filename="Qwen3.6-27B.Q4_K_M.gguf", + quant_type="Q4_K_M", + file_size_bytes=16_000_000_000, + ) + + resolved = _resolve_ranked_gguf_for_run( + selected, + synthetic, + [selected, q5_only, q4_match], + ) + + assert resolved is not None + model, variant = resolved + assert model.id == "smaller/Qwen3.6-27B-GGUF" + assert variant.quant_type == "Q4_K_M" + + +def test_resolve_ranked_synthetic_gguf_rejects_quant_mismatch(): + selected = ModelInfo( + id="Qwen/Qwen3.6-27B", + family_id="qwen3-27b", + name="Qwen3.6-27B", + parameter_count=27_000_000_000, + ) + q5_only = ModelInfo( + id="converter/Qwen3.6-27B-GGUF", + family_id="qwen3-27b", + name="Qwen3.6-27B-GGUF", + parameter_count=27_000_000_000, + downloads=1_000_000, + gguf_variants=[ + GGUFVariant( + filename="q5.gguf", + quant_type="Q5_K_M", + file_size_bytes=18_000_000_000, + ) + ], + ) + synthetic = GGUFVariant( + filename="Qwen3.6-27B.Q4_K_M.gguf", + quant_type="Q4_K_M", + file_size_bytes=16_000_000_000, + ) + + resolved = _resolve_ranked_gguf_for_run(selected, synthetic, [selected, q5_only]) + + assert resolved is None + + +def test_resolve_ranked_synthetic_gguf_without_real_repo_returns_none(): + selected = ModelInfo( + id="Qwen/Qwen3.6-27B", + family_id="qwen3-27b", + name="Qwen3.6-27B", + parameter_count=27_000_000_000, + ) + unrelated = ModelInfo( + id="other/Model-7B-GGUF", + family_id="model-7b", + name="Model-7B-GGUF", + parameter_count=7_000_000_000, + gguf_variants=[ + GGUFVariant( + filename="other.gguf", + quant_type="Q4_K_M", + file_size_bytes=4_000_000_000, + ) + ], + ) + synthetic = GGUFVariant( + filename="Qwen3.6-27B.Q4_K_M.gguf", + quant_type="Q4_K_M", + file_size_bytes=16_000_000_000, + ) + + assert ( + _resolve_ranked_gguf_for_run(selected, synthetic, [selected, unrelated]) is None + ) + + +def test_resolve_ranked_synthetic_gguf_rejects_size_mismatch(): + selected = ModelInfo( + id="deepseek-ai/DeepSeek-V4-Flash", + family_id="deepseek-v4-flash", + name="DeepSeek-V4-Flash", + parameter_count=158_000_000_000, + ) + mtp_head = ModelInfo( + id="converter/deepseek-v4-flash-mtp-gguf", + family_id="deepseek-v4-flash", + name="DeepSeek-V4-Flash-MTP-GGUF", + parameter_count=6_600_000_000, + gguf_variants=[ + GGUFVariant( + filename="mtp.gguf", + quant_type="Q4_K_M", + file_size_bytes=4_000_000_000, + ) + ], + ) + synthetic = GGUFVariant( + filename="DeepSeek-V4-Flash.Q4_K_M.gguf", + quant_type="Q4_K_M", + file_size_bytes=90_000_000_000, + ) + + resolved = _resolve_ranked_gguf_for_run(selected, synthetic, [selected, mtp_head]) + + assert resolved is None + + +# --------------- run/snippet command tests --------------- + + +def test_run_exits_gracefully(): + """run should fail gracefully (uv missing, or no model found).""" + runner = CliRunner() + result = runner.invoke(app, ["run", "some-model"]) + if result.exit_code != 0: + assert any( + msg in result.stdout + for msg in ("uv is required", "No model found", "llama-cpp-python") + ) + + +def test_transformers_chat_script_passes_tokenizer_mapping_to_generate(): + model = _make_model(model_id="org/Test-7B") + + script = _generate_chat_script( + model, variant=None, context_length=4096, cpu_only=False + ) + + assert "return_dict=True" in script + assert "kwargs=dict(**inputs, max_new_tokens=512, streamer=streamer)" in script + assert "kwargs=dict(input_ids=inputs" not in script + + +def test_transformers_chat_script_provides_disk_offload_folder(): + model = _make_model(model_id="org/Test-7B") + + script = _generate_chat_script( + model, variant=None, context_length=4096, cpu_only=False + ) + + assert 'tempfile.mkdtemp(prefix="whichllm_transformers_offload_")' in script + assert "offload_folder=offload_folder" in script + assert "shutil.rmtree(offload_folder, ignore_errors=True)" in script + + +def test_run_auto_pick_resolves_ranked_gguf_before_launch(monkeypatch): + selected = ModelInfo( + id="Qwen/Qwen3.6-27B", + family_id="qwen3-27b", + name="Qwen3.6-27B", + parameter_count=27_000_000_000, + downloads=50_000, + ) + real_gguf = ModelInfo( + id="unsloth/Qwen3.6-27B-GGUF", + family_id="qwen3-27b", + name="Qwen3.6-27B-GGUF", + parameter_count=27_000_000_000, + downloads=200_000, + base_model="Qwen/Qwen3.6-27B", + gguf_variants=[ + GGUFVariant( + filename="q4.gguf", + quant_type="Q4_K_M", + file_size_bytes=16_000_000_000, + ) + ], + ) + synthetic = GGUFVariant( + filename="Qwen3.6-27B.Q4_K_M.gguf", + quant_type="Q4_K_M", + file_size_bytes=16_000_000_000, + ) + captured: dict[str, object] = {} + + def fake_rank_models(models, hardware, **kwargs): + captured["quant_filter"] = kwargs.get("quant_filter") + return [ + CompatibilityResult( + model=selected, + gguf_variant=synthetic, + can_run=True, + vram_required_bytes=0, + vram_available_bytes=0, + quality_score=90.0, + ) + ] + + def fake_generate_chat_script(model, variant, context_length, cpu_only): + captured["model_id"] = model.id + captured["variant"] = variant + return "print('ok')" + + class Completed: + returncode = 0 + + def fake_run(cmd): + captured["cmd"] = cmd + return Completed() + + monkeypatch.setattr("shutil.which", lambda _: "/usr/bin/uv") + monkeypatch.setattr(cli_mod, "_load_models", lambda refresh: [selected, real_gguf]) + monkeypatch.setattr( + "whichllm.hardware.detector.detect_hardware", lambda: _hw_with_gpu(8) + ) + monkeypatch.setattr("whichllm.models.benchmark.load_benchmark_cache", lambda: {}) + monkeypatch.setattr("whichllm.engine.ranker.rank_models", fake_rank_models) + monkeypatch.setattr(cli_mod, "_generate_chat_script", fake_generate_chat_script) + monkeypatch.setattr("subprocess.run", fake_run) + + result = CliRunner().invoke(app, ["run", "--quant", "Q4_K_M"]) + + assert result.exit_code == 0 + assert captured["quant_filter"] == "Q4_K_M" + assert captured["model_id"] == "unsloth/Qwen3.6-27B-GGUF" + assert captured["variant"].filename == "q4.gguf" + assert "llama-cpp-python" in captured["cmd"] + assert "transformers" not in captured["cmd"] + + +def test_snippet_no_model_found(monkeypatch): + monkeypatch.setattr(cli_mod, "_load_models", lambda refresh: []) + runner = CliRunner() + result = runner.invoke(app, ["snippet", "nonexistent_model_xyz_999"]) + assert result.exit_code != 0 + assert "No model found" in result.stdout + + +def test_json_output_includes_benchmark_source_and_confidence(): + """display_json should include benchmark_source and benchmark_confidence.""" + import json as json_mod + from io import StringIO + + from rich.console import Console + + from whichllm.output.display import display_json + + model = ModelInfo( + id="test-org/Test-7B", + family_id="test-7b", + name="Test-7B", + parameter_count=7_000_000_000, + downloads=100, + likes=10, + ) + result = CompatibilityResult( + model=model, + gguf_variant=None, + can_run=True, + vram_required_bytes=8_000_000_000, + vram_available_bytes=24_000_000_000, + quality_score=55.0, + benchmark_status="estimated", + benchmark_source="line_interp", + benchmark_confidence=0.34, + ) + hw = HardwareInfo( + gpus=[], + cpu_name="Test CPU", + cpu_cores=8, + ram_budget_bytes=32 * 1024**3, + ram_bytes=64 * 1024**3, + disk_free_bytes=500 * 1024**3, + os="linux", + budget_notes=["RAM budget: 32.0 GB"], + ) + + buf = StringIO() + import whichllm.output._console as console_mod + + orig_console = console_mod.console + console_mod.console = Console(file=buf, force_terminal=False) + try: + display_json([result], hw) + finally: + console_mod.console = orig_console + + data = json_mod.loads(buf.getvalue().strip()) + entry = data["models"][0] + assert data["hardware"]["ram_budget_bytes"] == 32 * 1024**3 + assert data["hardware"]["budget_notes"] == ["RAM budget: 32.0 GB"] + assert entry["artifact_repo_id"] is None + assert entry["artifact_filename"] is None + assert entry["benchmark_status"] == "estimated" + assert entry["benchmark_source"] == "line_interp" + assert entry["benchmark_confidence"] == 0.34 + + +def test_json_output_includes_resolved_artifact_fields(): + """display_json should expose the actual GGUF repo/file when resolved.""" + import json as json_mod + from io import StringIO + + from rich.console import Console + + from whichllm.output.display import display_json + + model = ModelInfo( + id="Qwen/Qwen3-4B-Thinking-2507", + family_id="qwen3-4b-thinking", + name="Qwen3-4B-Thinking-2507", + parameter_count=4_000_000_000, + ) + artifact = ModelInfo( + id="MaziyarPanahi/Qwen3-4B-Thinking-2507-GGUF", + family_id="qwen3-4b-thinking", + name="Qwen3-4B-Thinking-2507-GGUF", + parameter_count=4_000_000_000, + ) + result = CompatibilityResult( + model=model, + gguf_variant=GGUFVariant( + filename="Qwen3-4B-Thinking-2507.Q3_K_M.gguf", + quant_type="Q3_K_M", + file_size_bytes=2_000_000_000, + ), + artifact_model=artifact, + artifact_variant=GGUFVariant( + filename="Qwen3-4B-Thinking-2507-Q3_K_M.gguf", + quant_type="Q3_K_M", + file_size_bytes=2_000_000_000, + ), + can_run=True, + vram_required_bytes=3_000_000_000, + vram_available_bytes=8_000_000_000, + ) + hw = HardwareInfo( + gpus=[], + cpu_name="Test CPU", + cpu_cores=8, + ram_bytes=64 * 1024**3, + disk_free_bytes=500 * 1024**3, + os="linux", + ) + + buf = StringIO() + import whichllm.output._console as console_mod + + orig_console = console_mod.console + console_mod.console = Console(file=buf, force_terminal=False) + try: + display_json([result], hw) + finally: + console_mod.console = orig_console + + entry = json_mod.loads(buf.getvalue().strip())["models"][0] + assert entry["model_id"] == "Qwen/Qwen3-4B-Thinking-2507" + assert entry["artifact_repo_id"] == "MaziyarPanahi/Qwen3-4B-Thinking-2507-GGUF" + assert entry["artifact_filename"] == "Qwen3-4B-Thinking-2507-Q3_K_M.gguf" diff --git a/tests/test_compatibility.py b/tests/test_compatibility.py new file mode 100644 index 0000000..6919fc8 --- /dev/null +++ b/tests/test_compatibility.py @@ -0,0 +1,502 @@ +"""Tests for compatibility checking.""" + +from whichllm.constants import _GiB +from whichllm.engine.compatibility import check_compatibility +from whichllm.hardware.memory import estimate_usable_ram +from whichllm.hardware.types import GPUInfo, HardwareInfo +from whichllm.models.types import GGUFVariant, ModelInfo + + +def _make_model( + params: int = 7_000_000_000, context_length: int | None = None +) -> ModelInfo: + return ModelInfo( + id="test/model", + family_id="test/model", + name="model", + parameter_count=params, + context_length=context_length, + ) + + +def _make_variant(size: int = 4_000_000_000) -> GGUFVariant: + return GGUFVariant( + filename="model-Q4_K_M.gguf", quant_type="Q4_K_M", file_size_bytes=size + ) + + +def _make_hardware( + vram: int = 0, ram: int = 16 * 1024**3, disk: int = 100 * 1024**3, **gpu_kwargs +) -> HardwareInfo: + gpus = [] + if vram > 0: + gpus.append( + GPUInfo( + name="Test GPU", + vendor=gpu_kwargs.get("vendor", "nvidia"), + vram_bytes=vram, + compute_capability=gpu_kwargs.get("cc", (8, 6)), + memory_bandwidth_gbps=gpu_kwargs.get("bw", 500.0), + ) + ) + return HardwareInfo( + gpus=gpus, + cpu_name="Test CPU", + cpu_cores=8, + has_avx2=True, + ram_bytes=ram, + disk_free_bytes=disk, + os="linux", + ) + + +def test_full_gpu_fit(): + model = _make_model() + variant = _make_variant(4_000_000_000) + hw = _make_hardware(vram=24 * 1024**3) # 24GB VRAM + result = check_compatibility(model, variant, hw) + assert result.can_run is True + assert result.fit_type == "full_gpu" + + +def test_partial_offload(): + model = _make_model() + variant = _make_variant(20_000_000_000) # 20GB model + hw = _make_hardware(vram=8 * 1024**3, ram=64 * 1024**3) # 8GB VRAM, 64GB RAM + result = check_compatibility(model, variant, hw) + assert result.can_run is True + assert result.fit_type == "partial_offload" + assert 0.0 < result.offload_ratio < 1.0 + assert any("offload" in w.lower() for w in result.warnings) + + +def test_usable_vram_budget_can_turn_full_gpu_into_partial_offload(): + model = _make_model() + variant = _make_variant(7_000_000_000) + hw = _make_hardware(vram=8 * _GiB, ram=64 * _GiB) + hw.gpus[0].usable_vram_bytes = 6 * _GiB + + result = check_compatibility(model, variant, hw) + + assert result.can_run is True + assert result.fit_type == "partial_offload" + assert result.vram_available_bytes == 6 * _GiB + + +def test_ram_budget_limits_partial_offload_pool(): + model = _make_model() + variant = _make_variant(20_000_000_000) + hw = _make_hardware(vram=8 * _GiB, ram=64 * _GiB) + hw.ram_budget_bytes = 4 * _GiB + + result = check_compatibility(model, variant, hw) + + assert result.can_run is False + assert "Insufficient memory" in result.warnings[-1] + + +def test_ram_budget_caps_shared_memory_gpu_fit_pool(): + model = _make_model() + variant = _make_variant(12_000_000_000) + hw = HardwareInfo( + gpus=[ + GPUInfo( + name="Apple M2", + vendor="apple", + vram_bytes=16 * _GiB, + usable_vram_bytes=15 * _GiB, + memory_bandwidth_gbps=100.0, + shared_memory=True, + ) + ], + cpu_name="Apple M2", + cpu_cores=8, + ram_bytes=16 * _GiB, + ram_budget_bytes=8 * _GiB, + disk_free_bytes=100 * _GiB, + os="darwin", + ) + + result = check_compatibility(model, variant, hw) + + assert result.can_run is False + assert result.vram_available_bytes == 8 * _GiB + + +def test_shared_memory_manual_vram_override_caps_available_gpu_memory(): + model = _make_model(8_000_000_000) + variant = _make_variant(4_000_000_000) + hw = HardwareInfo( + gpus=[ + GPUInfo( + name="Intel UHD Graphics", + vendor="intel", + vram_bytes=1 * _GiB, + shared_memory=True, + vram_overridden=True, + ) + ], + cpu_name="Intel CPU", + cpu_cores=8, + ram_bytes=32 * _GiB, + disk_free_bytes=100 * _GiB, + os="linux", + ) + + result = check_compatibility(model, variant, hw) + + assert result.can_run is True + assert result.vram_available_bytes == 1 * _GiB + assert result.fit_type == "cpu_only" + + +def test_shared_memory_amd_apu_uses_system_memory_pool(): + model = _make_model(120_000_000_000) + variant = _make_variant(55_000_000_000) + hw = HardwareInfo( + gpus=[ + GPUInfo( + name="STRXLGEN", + vendor="amd", + vram_bytes=512 * 1024**2, + memory_bandwidth_gbps=256.0, + shared_memory=True, + ) + ], + cpu_name="AMD Ryzen AI MAX+ 395", + cpu_cores=16, + ram_bytes=128 * 1024**3, + disk_free_bytes=200 * 1024**3, + os="linux", + ) + + result = check_compatibility(model, variant, hw) + + assert result.can_run is True + assert result.fit_type == "full_gpu" + assert result.vram_available_bytes == estimate_usable_ram(hw.ram_bytes) + assert not any("offload" in w.lower() for w in result.warnings) + assert not any("cpu only" in w.lower() for w in result.warnings) + + +def test_windows_shared_memory_amd_apu_does_not_emit_rocm_warning(): + model = _make_model(8_000_000_000) + variant = _make_variant(6_000_000_000) + hw = HardwareInfo( + gpus=[ + GPUInfo( + name="AMD Ryzen AI 9 HX 370 w/ Radeon 890M", + vendor="amd", + vram_bytes=0, + memory_bandwidth_gbps=120.0, + shared_memory=True, + ) + ], + cpu_name="AMD Ryzen AI 9 HX 370", + cpu_cores=12, + ram_bytes=16 * 1024**3, + disk_free_bytes=100 * 1024**3, + os="windows", + ) + + result = check_compatibility(model, variant, hw) + + assert result.can_run is True + assert result.fit_type == "full_gpu" + assert result.vram_available_bytes == estimate_usable_ram(hw.ram_bytes) + assert not any("rocm" in w.lower() for w in result.warnings) + assert not any("offload" in w.lower() for w in result.warnings) + + +def test_shared_memory_igpu_is_not_summed_with_dedicated_gpu(): + model = _make_model(20_000_000_000) + variant = _make_variant(14 * 1024**3) + hw = HardwareInfo( + gpus=[ + GPUInfo( + name="NVIDIA GeForce RTX 4060", + vendor="nvidia", + vram_bytes=8 * 1024**3, + memory_bandwidth_gbps=272.0, + ), + GPUInfo( + name="Intel(R) Arc(TM) Graphics", + vendor="intel", + vram_bytes=0, + shared_memory=True, + ), + ], + cpu_name="Intel CPU", + cpu_cores=12, + ram_bytes=32 * 1024**3, + disk_free_bytes=100 * 1024**3, + os="windows", + ) + + result = check_compatibility(model, variant, hw) + + assert result.can_run is True + assert result.fit_type == "partial_offload" + assert result.vram_available_bytes == 8 * 1024**3 + assert any("offloaded to CPU RAM" in w for w in result.warnings) + + +def test_homogeneous_multi_gpu_uses_conservative_fit_budget(): + model = _make_model(1_000_000_000) + variant = _make_variant(int(46 * _GiB)) + hw = HardwareInfo( + gpus=[ + GPUInfo( + name="NVIDIA GeForce RTX 4090", + vendor="nvidia", + vram_bytes=24 * _GiB, + compute_capability=(8, 9), + memory_bandwidth_gbps=1008.0, + ), + GPUInfo( + name="NVIDIA GeForce RTX 4090", + vendor="nvidia", + vram_bytes=24 * _GiB, + compute_capability=(8, 9), + memory_bandwidth_gbps=1008.0, + ), + ], + cpu_name="Test CPU", + cpu_cores=16, + ram_bytes=128 * _GiB, + disk_free_bytes=200 * _GiB, + os="linux", + ) + + result = check_compatibility(model, variant, hw) + + assert result.can_run is True + assert result.fit_type == "partial_offload" + assert result.uses_multi_gpu is True + assert result.vram_available_bytes == 48 * _GiB + assert result.multi_gpu_effective_vram_bytes is not None + assert result.multi_gpu_effective_vram_bytes < result.vram_available_bytes + assert any("conservative layer-split budget" in w for w in result.warnings) + + +def test_heterogeneous_multi_gpu_warns_about_split_assumptions(): + model = _make_model() + variant = _make_variant(20 * _GiB) + hw = HardwareInfo( + gpus=[ + GPUInfo( + name="NVIDIA GeForce RTX 4090", + vendor="nvidia", + vram_bytes=24 * _GiB, + compute_capability=(8, 9), + memory_bandwidth_gbps=1008.0, + ), + GPUInfo( + name="NVIDIA GeForce RTX 3060", + vendor="nvidia", + vram_bytes=12 * _GiB, + compute_capability=(8, 6), + memory_bandwidth_gbps=360.0, + ), + ], + cpu_name="Test CPU", + cpu_cores=16, + ram_bytes=64 * _GiB, + disk_free_bytes=200 * _GiB, + os="linux", + ) + + result = check_compatibility(model, variant, hw) + + assert result.can_run is True + assert result.uses_multi_gpu is True + assert result.multi_gpu_effective_vram_bytes is not None + assert result.multi_gpu_effective_vram_bytes < 36 * _GiB + assert any("Heterogeneous multi-GPU" in w for w in result.warnings) + + +def test_multiple_shared_memory_gpus_are_not_summed(): + model = _make_model(120_000_000_000) + variant = _make_variant(70 * _GiB) + hw = HardwareInfo( + gpus=[ + GPUInfo( + name="Integrated GPU A", + vendor="amd", + vram_bytes=0, + memory_bandwidth_gbps=120.0, + shared_memory=True, + ), + GPUInfo( + name="Integrated GPU B", + vendor="intel", + vram_bytes=0, + shared_memory=True, + ), + ], + cpu_name="Test CPU", + cpu_cores=16, + ram_bytes=64 * _GiB, + disk_free_bytes=200 * _GiB, + os="linux", + ) + + result = check_compatibility(model, variant, hw) + + assert result.vram_available_bytes == estimate_usable_ram(hw.ram_bytes) + assert result.multi_gpu_effective_vram_bytes is None + assert result.fit_type == "cpu_only" + assert any("shared-memory GPUs are not pooled" in w for w in result.warnings) + + +def test_apple_silicon_does_not_double_count_unified_memory(): + """Apple Silicon uses unified memory: vram_bytes IS the system RAM. + The fit checker must not add a separate offload pool on top.""" + model = _make_model(70_000_000_000) + variant = _make_variant(40_000_000_000) # 40 GB model + hw = HardwareInfo( + gpus=[ + GPUInfo( + name="Apple M2 Max", + vendor="apple", + vram_bytes=32 * 1024**3, # 32 GB unified memory + memory_bandwidth_gbps=400.0, + shared_memory=True, + ) + ], + cpu_name="Apple M2 Max", + cpu_cores=12, + ram_bytes=32 * 1024**3, + disk_free_bytes=200 * 1024**3, + os="darwin", + ) + + result = check_compatibility(model, variant, hw) + + # Model (40 GB) exceeds unified memory (32 GB). There is no separate + # CPU RAM pool to spill into, so this must NOT be partial_offload. + assert result.fit_type != "partial_offload", ( + "Apple Silicon should not get partial_offload — unified memory " + "cannot be double-counted as GPU VRAM + CPU RAM offload pool" + ) + assert not any("offloaded to CPU RAM" in w for w in result.warnings) + + +def test_apple_silicon_full_gpu_fit(): + """A model that fits within unified memory should be full_gpu.""" + model = _make_model(7_000_000_000) + variant = _make_variant(4_000_000_000) # 4 GB model + hw = HardwareInfo( + gpus=[ + GPUInfo( + name="Apple M4 Pro", + vendor="apple", + vram_bytes=24 * 1024**3, + memory_bandwidth_gbps=273.0, + shared_memory=True, + ) + ], + cpu_name="Apple M4 Pro", + cpu_cores=14, + ram_bytes=24 * 1024**3, + disk_free_bytes=200 * 1024**3, + os="darwin", + ) + + result = check_compatibility(model, variant, hw) + + assert result.can_run is True + assert result.fit_type == "full_gpu" + assert not any("offload" in w.lower() for w in result.warnings) + + +def test_apple_silicon_vendor_guard_handles_legacy_shared_memory_false(): + """Even if a cached/older GPUInfo has shared_memory=False, the + vendor=='apple' guard should still prevent double-counting.""" + model = _make_model(70_000_000_000) + variant = _make_variant(40_000_000_000) # 40 GB model + hw = HardwareInfo( + gpus=[ + GPUInfo( + name="Apple M2 Max", + vendor="apple", + vram_bytes=32 * 1024**3, + memory_bandwidth_gbps=400.0, + shared_memory=False, # legacy/cached object + ) + ], + cpu_name="Apple M2 Max", + cpu_cores=12, + ram_bytes=32 * 1024**3, + disk_free_bytes=200 * 1024**3, + os="darwin", + ) + + result = check_compatibility(model, variant, hw) + + assert result.fit_type != "partial_offload", ( + "vendor='apple' guard must prevent double-counting even when " + "shared_memory=False (cached/older GPUInfo)" + ) + assert not any("offloaded to CPU RAM" in w for w in result.warnings) + + +def test_cpu_only(): + model = _make_model(1_000_000_000) + variant = _make_variant(600_000_000) + hw = _make_hardware(vram=0, ram=16 * 1024**3) # No GPU + result = check_compatibility(model, variant, hw) + assert result.can_run is True + assert result.fit_type == "cpu_only" + + +def test_insufficient_memory(): + model = _make_model(70_000_000_000) + variant = _make_variant(40_000_000_000) + hw = _make_hardware(vram=0, ram=8 * 1024**3) # Only 8GB RAM, no GPU + result = check_compatibility(model, variant, hw) + assert result.can_run is False + + +def test_low_compute_capability(): + model = _make_model() + variant = _make_variant(4_000_000_000) + hw = _make_hardware(vram=24 * 1024**3, cc=(4, 0)) # Very old GPU + result = check_compatibility(model, variant, hw) + assert result.can_run is True # Still runs, just with warning + assert any("compute capability" in w.lower() for w in result.warnings) + + +def test_insufficient_disk(): + model = _make_model() + variant = _make_variant(50_000_000_000) # 50GB file + hw = _make_hardware(vram=80 * 1024**3, disk=10 * 1024**3) # Only 10GB disk + result = check_compatibility(model, variant, hw) + assert result.can_run is False + assert any("disk" in w.lower() for w in result.warnings) + + +def test_context_fits_true_when_model_supports(): + model = _make_model(context_length=131072) + variant = _make_variant() + hw = _make_hardware(vram=24 * 1024**3) + result = check_compatibility(model, variant, hw, context_length=32768) + assert result.context_fits is True + + +def test_context_fits_false_when_model_too_small(): + model = _make_model(context_length=8192) + variant = _make_variant() + hw = _make_hardware(vram=24 * 1024**3) + result = check_compatibility(model, variant, hw, context_length=32768) + assert result.context_fits is False + assert any("max context" in w.lower() for w in result.warnings) + + +def test_context_fits_unknown_is_true(): + model = _make_model(context_length=None) + variant = _make_variant() + hw = _make_hardware(vram=24 * 1024**3) + result = check_compatibility(model, variant, hw, context_length=32768) + assert result.context_fits is True + assert not any("max context" in w.lower() for w in result.warnings) diff --git a/tests/test_fetcher.py b/tests/test_fetcher.py new file mode 100644 index 0000000..0984e03 --- /dev/null +++ b/tests/test_fetcher.py @@ -0,0 +1,651 @@ +"""Tests for model metadata normalization in fetcher.""" + +import asyncio + +import httpx + +import whichllm.models.fetcher as fetcher +from whichllm.models.fetcher import ( + _extract_hf_eval_score, + _extract_published_at, + _hf_api_url, + _normalize_param_count, + _parse_model, + dicts_to_models, + models_to_dicts, +) +from whichllm.models.types import ModelInfo + + +def test_hf_api_url_uses_default_endpoint(monkeypatch): + monkeypatch.delenv("HF_ENDPOINT", raising=False) + + assert _hf_api_url("models") == "https://huggingface.co/api/models" + + +def test_hf_api_url_respects_hf_endpoint(monkeypatch): + monkeypatch.setenv("HF_ENDPOINT", "https://hf-mirror.example") + + assert _hf_api_url("models/Qwen/Qwen3-8B") == ( + "https://hf-mirror.example/api/models/Qwen/Qwen3-8B" + ) + + +def test_hf_api_url_strips_trailing_slash(monkeypatch): + monkeypatch.setenv("HF_ENDPOINT", "https://hf-mirror.example/") + + assert _hf_api_url("/models") == "https://hf-mirror.example/api/models" + + +def test_hf_api_url_rejects_empty_endpoint(monkeypatch): + monkeypatch.setenv("HF_ENDPOINT", " ") + + try: + _hf_api_url("models") + except ValueError as exc: + assert "HF_ENDPOINT must not be empty" in str(exc) + else: + raise AssertionError("expected empty HF_ENDPOINT to raise") + + +def test_hf_api_url_rejects_endpoint_without_scheme(monkeypatch): + monkeypatch.setenv("HF_ENDPOINT", "hf-mirror.example") + + try: + _hf_api_url("models") + except ValueError as exc: + assert "HF_ENDPOINT must start with http:// or https://" in str(exc) + else: + raise AssertionError("expected invalid HF_ENDPOINT to raise") + + +def test_fetch_models_respects_hf_endpoint(monkeypatch): + monkeypatch.setenv("HF_ENDPOINT", "https://hf-mirror.example") + urls: list[str] = [] + encodings: list[str] = [] + + async def fake_get_with_retries(client, url: str, **kwargs): + urls.append(url) + encodings.append(client.headers["accept-encoding"]) + request = httpx.Request("GET", url) + if "/models/" in url: + return httpx.Response(404, request=request) + return httpx.Response(200, json=[], request=request) + + monkeypatch.setattr(fetcher, "get_with_retries", fake_get_with_retries) + + models = asyncio.run(fetcher.fetch_models(limit=1, include_vision=False)) + + assert models == [] + assert urls + assert all(url.startswith("https://hf-mirror.example/api/") for url in urls) + assert "https://hf-mirror.example/api/models" in urls + assert not any(url.startswith("https://huggingface.co/api/") for url in urls) + assert set(encodings) == {"gzip, deflate"} + + +def test_fetch_model_published_at_respects_hf_endpoint(monkeypatch): + monkeypatch.setenv("HF_ENDPOINT", "https://hf-mirror.example") + urls: list[str] = [] + encodings: list[str] = [] + + async def fake_get(self, url: str, **kwargs): + urls.append(url) + encodings.append(self.headers["accept-encoding"]) + return httpx.Response( + 200, + json={"createdAt": "2026-06-22T00:00:00.000Z"}, + request=httpx.Request("GET", url), + ) + + monkeypatch.setattr(httpx.AsyncClient, "get", fake_get) + + result = asyncio.run(fetcher.fetch_model_published_at(["Qwen/Qwen3-8B"])) + + assert result == {"Qwen/Qwen3-8B": "2026-06-22T00:00:00.000Z"} + assert urls == ["https://hf-mirror.example/api/models/Qwen/Qwen3-8B"] + assert encodings == ["gzip, deflate"] + + +def test_normalize_param_count_for_quantized_repo_uses_size_hint(): + corrected = _normalize_param_count( + extracted=5_233_828_308, + model_id="ISTA-DASLab/gemma-3-27b-it-GPTQ-4b-128g", + base_model="google/gemma-3-27b-it", + ) + assert corrected == 27_000_000_000 + + +def test_normalize_param_count_keeps_reasonable_value(): + kept = _normalize_param_count( + extracted=11_765_788_416, + model_id="MaziyarPanahi/gemma-3-12b-it-GGUF", + base_model="google/gemma-3-12b-it", + ) + assert kept == 11_765_788_416 + + +def test_normalize_param_count_with_no_hint_keeps_original(): + kept = _normalize_param_count( + extracted=3_820_000_000, + model_id="microsoft/Phi-3-mini-4k-instruct-gguf", + base_model=None, + ) + assert kept == 3_820_000_000 + + +def test_dicts_to_models_normalizes_cached_parameter_count(): + models = dicts_to_models( + [ + { + "id": "ISTA-DASLab/gemma-3-27b-it-GPTQ-4b-128g", + "family_id": "gemma-3-27b", + "name": "gemma-3-27b-it-GPTQ-4b-128g", + "parameter_count": 5_233_828_308, + "downloads": 1, + "likes": 1, + "gguf_variants": [], + "benchmark_scores": {}, + "base_model": "google/gemma-3-27b-it", + } + ] + ) + assert len(models) == 1 + assert models[0].parameter_count == 27_000_000_000 + + +def test_dicts_to_models_refreshes_cached_deepseek_v4_flash_counts(): + models = dicts_to_models( + [ + { + "id": "deepseek-ai/DeepSeek-V4-Flash", + "family_id": "deepseek-v4-flash", + "name": "DeepSeek-V4-Flash", + "parameter_count": 158_069_433_298, + "parameter_count_active": 10_000_000_000, + "downloads": 1, + "likes": 1, + "gguf_variants": [], + "benchmark_scores": {}, + } + ] + ) + + assert len(models) == 1 + assert models[0].parameter_count == 284_000_000_000 + assert models[0].parameter_count_active == 13_000_000_000 + assert models[0].is_moe is True + + +def test_dicts_to_models_uses_case_insensitive_curated_active_params(): + models = dicts_to_models( + [ + { + "id": "google/gemma-4-26B-A4B-it", + "family_id": "gemma-4-26b-a4b", + "name": "gemma-4-26B-A4B-it", + "parameter_count": 26_544_131_376, + "parameter_count_active": None, + "downloads": 1, + "likes": 1, + "gguf_variants": [], + "benchmark_scores": {}, + } + ] + ) + + assert len(models) == 1 + assert models[0].parameter_count_active == 3_800_000_000 + assert models[0].is_moe is True + + +def test_dicts_to_models_recovers_a3b_active_params_from_cached_qwen_model(): + models = dicts_to_models( + [ + { + "id": "Qwen/Qwen3.6-35B-A3B", + "family_id": "qwen3.6-35b-a3b", + "name": "Qwen3.6-35B-A3B", + "parameter_count": 35_951_822_704, + "parameter_count_active": None, + "architecture": "qwen3_5moe", + "downloads": 1, + "likes": 1, + "gguf_variants": [], + "benchmark_scores": {}, + } + ] + ) + + assert len(models) == 1 + assert models[0].parameter_count_active == 3_000_000_000 + assert models[0].is_moe is True + + +def test_dicts_to_models_recovers_a3b_active_params_from_base_model(): + models = dicts_to_models( + [ + { + "id": "local/Qwen36-GGUF", + "family_id": "qwen36-gguf", + "name": "Qwen36-GGUF", + "parameter_count": 34_660_610_688, + "parameter_count_active": None, + "architecture": "qwen35moe", + "downloads": 1, + "likes": 1, + "gguf_variants": [], + "benchmark_scores": {}, + "base_model": "Qwen/Qwen3.6-35B-A3B", + } + ] + ) + + assert len(models) == 1 + assert models[0].parameter_count_active == 3_000_000_000 + assert models[0].is_moe is True + + +def test_dicts_to_models_refreshes_stale_xiaomi_moe_cache_counts(): + models = dicts_to_models( + [ + { + "id": "XiaomiMiMo/MiMo-V2.5-Pro", + "family_id": "mimo-v2.5-pro", + "name": "MiMo-V2.5-Pro", + "parameter_count": 58_000_000_000, + "parameter_count_active": 11_000_000_000, + "downloads": 1, + "likes": 1, + "gguf_variants": [], + "benchmark_scores": {}, + } + ] + ) + + assert len(models) == 1 + assert models[0].parameter_count == 1_020_000_000_000 + assert models[0].parameter_count_active == 42_000_000_000 + assert models[0].is_moe is True + + +def test_dicts_to_models_recovers_missing_known_parameter_count(): + models = dicts_to_models( + [ + { + "id": "zai-org/GLM-5", + "family_id": "glm-5", + "name": "GLM-5", + "parameter_count": 0, + "parameter_count_active": None, + "downloads": 1, + "likes": 1, + "gguf_variants": [], + "benchmark_scores": {}, + } + ] + ) + + assert len(models) == 1 + assert models[0].parameter_count == 744_000_000_000 + assert models[0].parameter_count_active == 40_000_000_000 + assert models[0].is_moe is True + + +def test_parse_model_uses_current_glm5_and_xiaomi_active_counts(): + glm = _parse_model( + { + "id": "zai-org/GLM-5", + "config": {"architectures": ["GlmForCausalLM"]}, + "safetensors": {"total": 753_864_139_008}, + "siblings": [], + "cardData": {}, + } + ) + mimo = _parse_model( + { + "id": "XiaomiMiMo/MiMo-V2-Flash", + "config": {"architectures": ["LlamaForCausalLM"]}, + "safetensors": {"total": 309_785_318_400}, + "siblings": [], + "cardData": {}, + } + ) + + assert glm is not None + assert glm.parameter_count_active == 40_000_000_000 + assert mimo is not None + assert mimo.parameter_count_active == 15_000_000_000 + + +def test_parse_model_recovers_qwen36_a3b_active_params_from_name(): + parsed = _parse_model( + { + "id": "Qwen/Qwen3.6-35B-A3B", + "config": { + "architectures": ["Qwen3_5MoeForConditionalGeneration"], + "model_type": "qwen3_5_moe", + }, + "safetensors": {"total": 35_951_822_704}, + "siblings": [], + "cardData": {}, + } + ) + + assert parsed is not None + assert parsed.parameter_count == 35_951_822_704 + assert parsed.parameter_count_active == 3_000_000_000 + assert parsed.is_moe is True + + +def test_models_cache_roundtrip_keeps_published_at(): + models = [ + ModelInfo( + id="Qwen/Qwen3-8B-AWQ", + family_id="qwen3-8b", + name="Qwen3-8B-AWQ", + parameter_count=8_000_000_000, + published_at="2025-09-17T12:34:56.000Z", + downloads=123_456, + likes=789, + ) + ] + cached = models_to_dicts(models) + restored = dicts_to_models(cached) + assert len(restored) == 1 + assert restored[0].published_at == "2025-09-17T12:34:56.000Z" + assert restored[0].downloads == 123_456 + + +def test_extract_published_at_prefers_created_at(): + value = _extract_published_at( + { + "createdAt": "2025-01-01T00:00:00.000Z", + "lastModified": "2026-01-01T00:00:00.000Z", + } + ) + assert value == "2025-01-01T00:00:00.000Z" + + +def test_extract_published_at_falls_back_to_last_modified(): + value = _extract_published_at( + { + "lastModified": "2026-01-01T00:00:00.000Z", + } + ) + assert value == "2026-01-01T00:00:00.000Z" + + +def test_parse_model_keeps_split_gguf_as_single_variant(): + parsed = _parse_model( + { + "id": "org/Test-8B-GGUF", + "config": { + "architectures": ["LlamaForCausalLM"], + }, + "safetensors": {"total": 8_000_000_000}, + "siblings": [ + { + "rfilename": "model-Q4_K_M-00001-of-00002.gguf", + "size": 2_000_000_000, + }, + { + "rfilename": "model-Q4_K_M-00002-of-00002.gguf", + "size": 2_500_000_000, + }, + {"rfilename": "model-Q8_0.gguf", "size": 8_000_000_000}, + ], + "cardData": {}, + } + ) + assert parsed is not None + q4 = [v for v in parsed.gguf_variants if v.quant_type == "Q4_K_M"] + q8 = [v for v in parsed.gguf_variants if v.quant_type == "Q8_0"] + assert len(q4) == 1 + assert len(q8) == 1 + assert q4[0].file_size_bytes == 4_500_000_000 + + +def test_extract_hf_eval_score_uses_general_datasets_and_median(): + score = _extract_hf_eval_score( + { + "evalResults": [ + { + "filename": ".eval_results/mmlu-pro.yaml", + "data": {"dataset": {"id": "TIGER-Lab/MMLU-Pro"}, "value": 48.3}, + }, + { + "filename": ".eval_results/gsm8k.yaml", + "data": {"dataset": {"id": "openai/gsm8k"}, "value": 84.5}, + }, + { + "filename": ".eval_results/hle_medium_with_tools.yaml", + "data": { + "dataset": {"id": "cais/hle"}, + "value": 99.0, + "notes": "Reasoning: medium, With tools", + }, + }, + { + "filename": ".eval_results/swe_bench.yaml", + "data": { + "dataset": {"id": "SWE-bench/SWE-bench_Verified"}, + "value": 53.2, + }, + }, + ] + } + ) + # general対象(mmlu/gsm8k)のみ集計し、中央値を使う。 + assert score == 66.4 + + +def test_parse_model_extracts_hf_eval_benchmark_score(): + parsed = _parse_model( + { + "id": "meta-llama/Llama-3.1-8B-Instruct", + "config": {"architectures": ["LlamaForCausalLM"]}, + "safetensors": {"total": 8_000_000_000}, + "siblings": [], + "cardData": {}, + "evalResults": [ + { + "filename": ".eval_results/mmlu-pro.yaml", + "data": {"dataset": {"id": "TIGER-Lab/MMLU-Pro"}, "value": 48.3}, + }, + { + "filename": ".eval_results/gsm8k.yaml", + "data": {"dataset": {"id": "openai/gsm8k"}, "value": 84.5}, + }, + ], + } + ) + assert parsed is not None + assert parsed.benchmark_scores.get("hf_eval") == 66.4 + + +def test_deepseek_v4_flash_uses_model_card_counts_over_hf_tensor_metadata(): + """DeepSeek V4 Flash's mixed-precision HF tensor metadata reports a + smaller stored tensor count than the model-card total. Ranking and GGUF + synthesis must use the published model capacity instead.""" + + parsed = _parse_model( + { + "id": "deepseek-ai/DeepSeek-V4-Flash", + "config": { + "architectures": ["DeepseekV4ForCausalLM"], + "model_type": "deepseek_v4", + "quantization_config": {"quant_method": "fp8"}, + }, + "safetensors": { + "total": 158_069_433_298, + "parameters": { + "BF16": 1_415_259_264, + "F8_E8M0": 8_858_737_664, + "F8_E4M3": 6_023_020_544, + "I8": 141_733_920_768, + }, + }, + "siblings": [], + "cardData": {}, + } + ) + + assert parsed is not None + assert parsed.parameter_count == 284_000_000_000 + assert parsed.parameter_count_active == 13_000_000_000 + + +def test_parse_model_resolves_gemma3_sliding_window(): + parsed = _parse_model( + { + "id": "google/gemma-3-27b-it", + "config": { + "architectures": ["Gemma3ForConditionalGeneration"], + "model_type": "gemma3", + "sliding_window": 1024, + "sliding_window_pattern": 6, + "max_position_embeddings": 131072, + }, + "safetensors": {"total": 27_000_000_000}, + "siblings": [], + "cardData": {}, + } + ) + assert parsed is not None + assert parsed.sliding_window == 1024 + assert parsed.sliding_window_global_ratio == 1.0 / 6.0 + + +def test_parse_model_resolves_gpt_oss_sliding_window(): + parsed = _parse_model( + { + "id": "openai/gpt-oss-20b", + "config": { + "architectures": ["GptOssForCausalLM"], + "model_type": "gpt_oss", + "sliding_window": 128, + "max_position_embeddings": 131072, + }, + "safetensors": {"total": 20_000_000_000}, + "siblings": [], + "cardData": {}, + } + ) + assert parsed is not None + assert parsed.sliding_window == 128 + assert parsed.sliding_window_global_ratio == 0.5 + + +def test_parse_model_does_not_honor_mistral_sliding_window(): + """Mistral declares sliding_window in config but runtimes ignore it.""" + parsed = _parse_model( + { + "id": "mistralai/Mistral-7B-v0.1", + "config": { + "architectures": ["MistralForCausalLM"], + "model_type": "mistral", + "sliding_window": 4096, + "max_position_embeddings": 32768, + }, + "safetensors": {"total": 7_000_000_000}, + "siblings": [], + "cardData": {}, + } + ) + assert parsed is not None + assert parsed.sliding_window is None + assert parsed.sliding_window_global_ratio is None + + +def test_parse_model_respects_disabled_sliding_window(): + parsed = _parse_model( + { + "id": "google/gemma-3-4b-it", + "config": { + "architectures": ["Gemma3ForConditionalGeneration"], + "model_type": "gemma3", + "sliding_window": 1024, + "use_sliding_window": False, + }, + "safetensors": {"total": 4_000_000_000}, + "siblings": [], + "cardData": {}, + } + ) + assert parsed is not None + assert parsed.sliding_window is None + assert parsed.sliding_window_global_ratio is None + + +def test_parse_model_does_not_honor_gemma1_window(): + """Gemma-1 has no sliding window; the gemma2/gemma3 keys must not match it.""" + parsed = _parse_model( + { + "id": "google/gemma-7b", + "config": { + "architectures": ["GemmaForCausalLM"], + "model_type": "gemma", + }, + "safetensors": {"total": 7_000_000_000}, + "siblings": [], + "cardData": {}, + } + ) + assert parsed is not None + assert parsed.sliding_window is None + + +def test_parse_model_sliding_window_from_gguf_metadata(): + """GGUF-only repos: trust the authoritative GGUF architecture metadata.""" + parsed = _parse_model( + { + "id": "unsloth/gemma-3-12b-it-GGUF", + "config": {}, + "gguf": {"architecture": "gemma3"}, + "safetensors": {"total": 12_000_000_000}, + "siblings": [], + "cardData": {}, + } + ) + assert parsed is not None + assert parsed.sliding_window == 1024 + assert parsed.sliding_window_global_ratio == 1.0 / 6.0 + + +def test_parse_model_sliding_window_unset_without_metadata(): + """With neither HF config nor GGUF arch metadata, SWA stays unhonored. + + Detection is limited to authoritative metadata; a GGUF-only repo whose id + merely names an SWA family keeps the conservative full-context estimate + rather than being guessed at from the id. + """ + parsed = _parse_model( + { + "id": "TheBloke/gemma-2-9b-it-GGUF", + "config": {}, + "gguf": {}, + "safetensors": {"total": 9_000_000_000}, + "siblings": [], + "cardData": {}, + } + ) + assert parsed is not None + assert parsed.sliding_window is None + assert parsed.sliding_window_global_ratio is None + + +def test_models_cache_roundtrip_keeps_sliding_window(): + models = [ + ModelInfo( + id="google/gemma-3-27b-it", + family_id="gemma-3-27b", + name="gemma-3-27b-it", + parameter_count=27_000_000_000, + sliding_window=1024, + sliding_window_global_ratio=1.0 / 6.0, + ) + ] + restored = dicts_to_models(models_to_dicts(models)) + assert restored[0].sliding_window == 1024 + assert restored[0].sliding_window_global_ratio == 1.0 / 6.0 diff --git a/tests/test_gpu_db.py b/tests/test_gpu_db.py new file mode 100644 index 0000000..1063091 --- /dev/null +++ b/tests/test_gpu_db.py @@ -0,0 +1,118 @@ +"""Tests for dbgpu-backed bandwidth resolution on detected hardware. + +The detection path must never give a laptop/mobile card its desktop bandwidth +(issues #74, #61, #93). These tests pin the strict matching rules: curated +values win, the curated lookup is mobile-aware, dbgpu fills gaps, and an +unknown card resolves to None rather than a wrong guess. +""" + +from whichllm.hardware.gpu_db import ( + _normalize_detected_name, + _static_bandwidth, + resolve_detected_bandwidth, +) + +_GiB = 1024**3 + + +# --- normalization --------------------------------------------------------- + + +def test_normalize_strips_vendor_trademark_and_maps_laptop_to_mobile(): + assert ( + _normalize_detected_name("NVIDIA GeForce RTX 5090 Laptop GPU") + == "GeForce RTX 5090 Mobile" + ) + assert _normalize_detected_name("Intel(R) Arc(TM) A770 Graphics") == "Arc A770" + assert _normalize_detected_name("AMD Radeon RX 6750 XT") == "Radeon RX 6750 XT" + + +def test_normalize_empty_or_vendor_only(): + assert _normalize_detected_name("") == "" + assert _normalize_detected_name("AMD") == "" + + +def test_normalize_adds_space_to_vram_bin_suffix(): + # #98: drivers write "12GB", dbgpu writes "12 GB". + assert _normalize_detected_name("NVIDIA RTX A2000 12GB") == "RTX A2000 12 GB" + + +# --- curated lookup is mobile-aware --------------------------------------- + + +def test_static_bandwidth_desktop_key_does_not_claim_laptop_card(): + # "RTX 5090" desktop key (1792) must not match a Laptop driver name. + assert _static_bandwidth("NVIDIA GeForce RTX 5090 Laptop GPU") is None + # The desktop card itself still resolves from the curated table. + assert _static_bandwidth("NVIDIA GeForce RTX 5090") == 1792.0 + + +def test_static_bandwidth_keeps_curated_laptop_entry(): + # A curated key that is itself a laptop entry stays authoritative. + assert _static_bandwidth("NVIDIA RTX A3000 Laptop GPU") == 264.0 + + +# --- end-to-end resolution ------------------------------------------------- + + +def test_resolve_desktop_uses_curated_value(): + assert resolve_detected_bandwidth("NVIDIA GeForce RTX 5090") == 1792.0 + + +def test_resolve_laptop_5090_is_mobile_not_desktop(): + # The bug in #74: a laptop 5090 must not inherit the 1792 desktop value. + bw = resolve_detected_bandwidth("NVIDIA GeForce RTX 5090 Laptop GPU", 24 * _GiB) + assert bw is not None + assert bw < 1500.0 # mobile 5090 is ~896 GB/s, nowhere near desktop 1792 + + +def test_resolve_a3000_laptop_preserves_curated_264(): + assert resolve_detected_bandwidth("NVIDIA RTX A3000 Laptop GPU", 6 * _GiB) == 264.0 + + +def test_resolve_rx_6750_xt(): + # #61: originally None. The curated table now carries it (#68); dbgpu + # would agree (432 GB/s) if the curated entry ever went away. + bw = resolve_detected_bandwidth("AMD Radeon RX 6750 XT", 12 * _GiB) + assert bw == 432.0 + + +def test_resolve_variant_qualifier_is_preserved(): + # "RTX 4060 Ti" must not collapse to the non-Ti "RTX 4060". + bw = resolve_detected_bandwidth("NVIDIA GeForce RTX 4060 Ti", 16 * _GiB) + assert bw is not None + # 4060 (non-Ti) is 272 GB/s; the Ti is 288. Either way it must be a real, + # positive value and not desktop-flagship sized. + assert 200 < bw < 400 + + +def test_resolve_arc_pro_b70_uses_curated_value(): + assert resolve_detected_bandwidth("Intel(R) Arc(TM) Pro B70 Graphics") == 608.0 + assert resolve_detected_bandwidth("Battlemage G31 [Intel Graphics]") == 608.0 + + +def test_resolve_empty_name_returns_none(): + assert resolve_detected_bandwidth("") is None + assert resolve_detected_bandwidth("Some Totally Made Up GPU 9xZ") is None + + +def test_resolve_known_amd_desktop_from_curated_table(): + assert resolve_detected_bandwidth("AMD Radeon RX 9070 XT") == 640.0 + + +def test_resolve_a2000_12gb_vram_bin_from_dbgpu(): + # #98: "NVIDIA RTX A2000 12GB" reported BW: N/A. dbgpu names the bin + # "RTX A2000 12 GB" (288 GB/s); the normalizer must bridge the spacing. + assert resolve_detected_bandwidth("NVIDIA RTX A2000 12GB", 12 * _GiB) == 288.0 + + +def test_static_bandwidth_compound_lspci_name(): + # Ported from amd.py (#68): compound lspci names resolve via segment + # splitting, first matching segment wins, "RX " prefix retried for bare + # segments. + compound = "Navi 22 [Radeon RX 6700/6700 XT/6750 XT / 6800M/6850M XT]" + bw = _static_bandwidth(compound) + assert bw is not None + assert bw > 0 + # The same answer flows through the public resolver. + assert resolve_detected_bandwidth(compound, 12 * _GiB) == bw diff --git a/tests/test_gpu_simulator.py b/tests/test_gpu_simulator.py new file mode 100644 index 0000000..56864af --- /dev/null +++ b/tests/test_gpu_simulator.py @@ -0,0 +1,171 @@ +"""Tests for GPU simulator (--gpu flag) using dbgpu database.""" + +import pytest + +from whichllm.constants import _GiB +from whichllm.hardware.gpu_simulator import ( + create_synthetic_gpu, + create_synthetic_gpus, + parse_synthetic_gpu_specs, +) + + +class TestMultiGPUSpecParsing: + def test_comma_separated_gpu_specs(self): + assert parse_synthetic_gpu_specs(["RTX 4090, RTX 3090"]) == [ + "RTX 4090", + "RTX 3090", + ] + + def test_repeated_gpu_specs(self): + assert parse_synthetic_gpu_specs(["RTX 4090", "RTX 3090"]) == [ + "RTX 4090", + "RTX 3090", + ] + + def test_count_shorthand(self): + assert parse_synthetic_gpu_specs(["2x RTX 4090, 1x RTX 3090"]) == [ + "RTX 4090", + "RTX 4090", + "RTX 3090", + ] + + def test_empty_entry_raises(self): + with pytest.raises(ValueError, match="Empty GPU entry"): + parse_synthetic_gpu_specs(["RTX 4090,"]) + + def test_create_synthetic_gpus_expands_count(self): + gpus = create_synthetic_gpus(["2x RTX 4090"]) + assert len(gpus) == 2 + assert all(gpu.vendor == "nvidia" for gpu in gpus) + assert all(gpu.vram_bytes == 24 * _GiB for gpu in gpus) + + def test_multi_gpu_vram_override_is_rejected(self): + with pytest.raises(ValueError, match="exactly one simulated GPU"): + create_synthetic_gpus(["2x RTX 4090"], vram_override_gb=24) + + +class TestKnownGPULookup: + def test_nvidia_rtx_4090(self): + gpu = create_synthetic_gpu("RTX 4090") + assert gpu.vram_bytes == 24 * _GiB + assert gpu.vendor == "nvidia" + assert gpu.memory_bandwidth_gbps is not None + assert gpu.compute_capability is not None + assert gpu.compute_capability[0] >= 8 + assert "(simulated)" in gpu.name + + def test_nvidia_rtx_3060(self): + gpu = create_synthetic_gpu("RTX 3060") + assert "RTX 3060" in gpu.name + assert "Ti" not in gpu.name # Should NOT match RTX 3060 Ti + assert gpu.vendor == "nvidia" + assert gpu.compute_capability is not None + + def test_nvidia_rtx_3060_ti(self): + gpu = create_synthetic_gpu("RTX 3060 Ti") + assert "RTX 3060 Ti" in gpu.name + assert gpu.vram_bytes == 8 * _GiB + assert gpu.vendor == "nvidia" + + def test_amd_rx_7900_xtx(self): + gpu = create_synthetic_gpu("RX 7900 XTX") + assert gpu.vram_bytes == 24 * _GiB + assert gpu.vendor == "amd" + assert gpu.memory_bandwidth_gbps is not None + + def test_amd_strix_halo_with_vram_override(self): + gpu = create_synthetic_gpu("Radeon 8060S", vram_override_gb=96) + assert gpu.vram_bytes == 96 * _GiB + assert gpu.vendor == "amd" + assert gpu.shared_memory is True + assert gpu.memory_bandwidth_gbps == 256.0 + + def test_nvidia_gtx_1080(self): + gpu = create_synthetic_gpu("GTX 1080") + assert gpu.vram_bytes == 8 * _GiB + assert gpu.vendor == "nvidia" + assert gpu.compute_capability == (6, 1) + + def test_a100_80gb_alias(self): + gpu = create_synthetic_gpu("A100 80GB") + assert gpu.vram_bytes == 80 * _GiB + assert gpu.vendor == "nvidia" + assert "(simulated)" in gpu.name + + def test_h100_80gb_alias(self): + gpu = create_synthetic_gpu("H100 80GB") + assert gpu.vram_bytes == 80 * _GiB + assert gpu.vendor == "nvidia" + assert "(simulated)" in gpu.name + + def test_intel_arc_pro_b70_curated_spec(self): + gpu = create_synthetic_gpu("Arc Pro B70") + assert gpu.name == "Intel Arc Pro B70 (simulated)" + assert gpu.vram_bytes == 32 * _GiB + assert gpu.vendor == "intel" + assert gpu.memory_bandwidth_gbps == 608.0 + assert gpu.shared_memory is False + + +class TestAppleSiliconAliases: + @pytest.mark.parametrize( + "chip", + [ + "M1", + "M1 Max", + "M1 Ultra", + "M2", + "M2 Max", + "M2 Ultra", + "M3", + "M3 Max", + "M3 Ultra", + "M4", + "M4 Max", + "M4 Ultra", + ], + ) + def test_apple_prefixed_alias_matches_plain_chip_name(self, chip): + plain = create_synthetic_gpu(chip) + prefixed = create_synthetic_gpu(f"Apple {chip}") + + assert prefixed.name == plain.name + assert prefixed.vendor == "apple" + assert prefixed.vram_bytes == plain.vram_bytes + assert prefixed.memory_bandwidth_gbps == plain.memory_bandwidth_gbps + + @pytest.mark.parametrize("chip", ["M1", "M2 Max", "M3 Ultra", "M4 Pro"]) + def test_apple_silicon_has_shared_memory(self, chip): + gpu = create_synthetic_gpu(chip) + assert gpu.shared_memory is True + + +class TestVRAMOverride: + def test_override_known_gpu(self): + gpu = create_synthetic_gpu("RTX 4060 Ti", vram_override_gb=16) + assert gpu.vram_bytes == 16 * _GiB + assert gpu.memory_bandwidth_gbps is not None + + def test_override_unknown_gpu(self): + gpu = create_synthetic_gpu("Nonexistent GPU 9999", vram_override_gb=48) + assert gpu.vram_bytes == 48 * _GiB + assert "(simulated)" in gpu.name + + +class TestUnknownGPU: + def test_unknown_without_vram_raises(self): + with pytest.raises(ValueError, match="Unknown GPU"): + create_synthetic_gpu("Nonexistent GPU 9999") + + def test_unknown_with_vram_succeeds(self): + gpu = create_synthetic_gpu("Nonexistent GPU 9999", vram_override_gb=24) + assert gpu.vram_bytes == 24 * _GiB + + +class TestFuzzySearch: + def test_partial_name(self): + """dbgpu fuzzy search should find GPU from partial name.""" + gpu = create_synthetic_gpu("GTX 1080") + assert "1080" in gpu.name + assert gpu.vendor == "nvidia" diff --git a/tests/test_grouper.py b/tests/test_grouper.py new file mode 100644 index 0000000..ca3df07 --- /dev/null +++ b/tests/test_grouper.py @@ -0,0 +1,65 @@ +"""Tests for model grouping logic.""" + +from whichllm.models.grouper import _normalize_name, group_models +from whichllm.models.types import ModelInfo + + +def _make_model( + id: str, base_model: str | None = None, downloads: int = 100 +) -> ModelInfo: + return ModelInfo( + id=id, + family_id=id, + name=id.split("/")[-1], + parameter_count=7_000_000_000, + downloads=downloads, + base_model=base_model, + ) + + +def test_group_by_base_model(): + base = _make_model("meta/Llama-3-8B", downloads=1000) + gguf = _make_model( + "user/Llama-3-8B-GGUF", base_model="meta/Llama-3-8B", downloads=500 + ) + families = group_models([base, gguf]) + assert len(families) == 1 + assert families[0].base_model.id in ("meta/Llama-3-8B", "user/Llama-3-8B-GGUF") + + +def test_group_by_name_normalization(): + base = _make_model("org/model-v1", downloads=1000) + gguf = _make_model("org/model-v1-GGUF", downloads=200) + families = group_models([base, gguf]) + assert len(families) <= 2 # may or may not merge depending on normalization + + +def test_fp4_suffixes_normalize_to_base_family(): + # MXFP4 and NVFP4 derivatives must collapse onto the base family the same + # way the older quant suffixes do, instead of orphaning into their own. + base = _normalize_name("openai/gpt-oss-20b") + assert _normalize_name("openai/gpt-oss-20b-MXFP4") == base + assert _normalize_name("openai/gpt-oss-20b-NVFP4") == base + + +def test_ungrouped_models_separate(): + m1 = _make_model("org/alpha", downloads=100) + m2 = _make_model("org/beta", downloads=200) + families = group_models([m1, m2]) + assert len(families) == 2 + + +def test_empty_input(): + families = group_models([]) + assert families == [] + + +def test_family_id_set(): + base = _make_model("meta/Llama-3-8B", downloads=1000) + gguf = _make_model( + "user/Llama-3-8B-GGUF", base_model="meta/Llama-3-8B", downloads=500 + ) + families = group_models([base, gguf]) + for family in families: + assert family.family_id + assert family.base_model.family_id == family.family_id diff --git a/tests/test_gtx1650_variants.py b/tests/test_gtx1650_variants.py new file mode 100644 index 0000000..debcc20 --- /dev/null +++ b/tests/test_gtx1650_variants.py @@ -0,0 +1,171 @@ +"""GTX 1650 GDDR5 vs GDDR6 memory-variant disambiguation. + +The GTX 1650 shipped in two memory configurations the driver name and PCI +device id (0x1F82) cannot tell apart: the original GDDR5 (8 Gbps x 128-bit = +128 GB/s) and a later GDDR6 revision (12 Gbps x 128-bit = 192 GB/s). They are +resolved by the max memory clock reported at detection time. + +Bandwidth evidence (measured on a GDDR6 board, VBIOS 90.17.4D.00.1E): Qwen3-1.7B +Q4_K_M decodes at 75.4 tok/s clock-locked, matching the 192 GB/s estimate (~78) +and not the GDDR5 128 GB/s estimate (~52). +""" + +import subprocess + +import pytest + +from whichllm.constants import GPU_BANDWIDTH, GPU_MEMORY_CLOCK_VARIANTS +from whichllm.data.gpu import ( + GPU_MEMORY_CLOCK_VARIANTS as DATA_GPU_MEMORY_CLOCK_VARIANTS, +) +from whichllm.hardware import nvidia +from whichllm.hardware.gpu_db import resolve_detected_bandwidth +from whichllm.hardware.types import GPUInfo +from whichllm.engine.performance import estimate_tok_per_sec +from whichllm.models.types import GGUFVariant, ModelInfo + +_NAME = "NVIDIA GeForce GTX 1650" +GDDR6_CLOCK = 6001.0 # measured on the GDDR6 board +GDDR5_CLOCK = 4001.0 # typical GDDR5 1650 + + +def test_variant_table_present_and_reexported(): + assert "GTX 1650" in GPU_MEMORY_CLOCK_VARIANTS + # constants is a shim over whichllm.data.gpu. + assert GPU_MEMORY_CLOCK_VARIANTS is DATA_GPU_MEMORY_CLOCK_VARIANTS + # Highest threshold first, descending — required for first-match resolution. + thresholds = [t for t, _bw in GPU_MEMORY_CLOCK_VARIANTS["GTX 1650"]] + assert thresholds == sorted(thresholds, reverse=True) + + +def test_curated_default_is_gddr5(): + # The base key stays the conservative GDDR5 value for unknown-clock cases. + assert GPU_BANDWIDTH["GTX 1650"] == 128.0 + + +def test_gddr6_clock_resolves_to_192(): + assert resolve_detected_bandwidth(_NAME, 4 * 1024**3, GDDR6_CLOCK) == 192.0 + + +def test_gddr5_clock_resolves_to_128(): + assert resolve_detected_bandwidth(_NAME, 4 * 1024**3, GDDR5_CLOCK) == 128.0 + + +def test_unknown_clock_falls_back_to_curated_default(): + # No clock => identical to pre-change behaviour (GDDR5 default). + assert resolve_detected_bandwidth(_NAME, 4 * 1024**3) == 128.0 + assert resolve_detected_bandwidth(_NAME, 4 * 1024**3, None) == 128.0 + assert resolve_detected_bandwidth(_NAME, 4 * 1024**3, 0.0) == 128.0 + + +@pytest.mark.parametrize( + "clock, expected", [(5499.0, 128.0), (5500.0, 192.0), (12000.0, 192.0)] +) +def test_threshold_boundary(clock, expected): + assert resolve_detected_bandwidth(_NAME, 4 * 1024**3, clock) == expected + + +def test_non_variant_card_ignores_memory_clock(): + # A single-memory-type card must resolve to its curated value regardless of + # any clock passed in (clock only disambiguates listed variants). + assert ( + resolve_detected_bandwidth("NVIDIA GeForce GTX 1660", 6 * 1024**3, 9999.0) + == 192.0 + ) + assert ( + resolve_detected_bandwidth("NVIDIA GeForce GTX 1660", 6 * 1024**3, 100.0) + == 192.0 + ) + + +def test_gtx1650_super_does_not_fall_through_to_base_1650(): + assert ( + resolve_detected_bandwidth("NVIDIA GeForce GTX 1650 SUPER", 4 * 1024**3) + == 192.0 + ) + assert ( + resolve_detected_bandwidth("NVIDIA GeForce GTX 1650 SUPER", 4 * 1024**3, 0.0) + == 192.0 + ) + + +def test_gddr6_estimate_scales_with_bandwidth_and_matches_measured(): + # Bandwidth flows linearly into the tok/s estimate; the GDDR6 estimate must + # exceed the GDDR5 one by the bandwidth ratio, and land near the measured + # 75.4 tok/s for Qwen3-1.7B Q4_K_M. + model = ModelInfo( + id="Qwen/Qwen3-1.7B", + family_id="Qwen/Qwen3-1.7B", + name="Qwen3-1.7B", + parameter_count=1_720_000_000, + ) + variant = GGUFVariant( + filename="Qwen3-1.7B-Q4_K_M.gguf", + quant_type="Q4_K_M", + file_size_bytes=1_353_000_000, + ) + gpu6 = GPUInfo( + "NVIDIA GeForce GTX 1650", + "nvidia", + 4 * 1024**3, + compute_capability=(7, 5), + memory_bandwidth_gbps=192.0, + ) + gpu5 = GPUInfo( + "NVIDIA GeForce GTX 1650", + "nvidia", + 4 * 1024**3, + compute_capability=(7, 5), + memory_bandwidth_gbps=128.0, + ) + est6 = estimate_tok_per_sec(model, variant, gpu6) + est5 = estimate_tok_per_sec(model, variant, gpu5) + assert est6 > est5 + assert est6 / est5 == pytest.approx(192.0 / 128.0, rel=1e-3) + # Measured 75.4 tok/s; estimate should be within the "high"-confidence band. + assert 60.0 <= est6 <= 95.0 + + +# --- nvidia-smi detection-path tests (the plumbing that feeds the resolver) --- + + +def _smi_bw(monkeypatch, stdout: str) -> float | None: + monkeypatch.setattr(nvidia, "_run_smi_query", lambda fields: stdout) + gpus = nvidia._detect_nvidia_gpus_via_smi() + assert len(gpus) == 1 + return gpus[0].memory_bandwidth_gbps + + +def test_smi_gddr6_clock_resolves_192(monkeypatch): + assert _smi_bw(monkeypatch, "NVIDIA GeForce GTX 1650, 4096, 6001\n") == 192.0 + + +def test_smi_gddr5_clock_resolves_128(monkeypatch): + assert _smi_bw(monkeypatch, "NVIDIA GeForce GTX 1650, 4096, 4001\n") == 128.0 + + +def test_smi_na_clock_falls_back_to_curated(monkeypatch): + # Cards/drivers that don't report the clock emit "[N/A]" (still exit 0). + assert _smi_bw(monkeypatch, "NVIDIA GeForce GTX 1650, 4096, [N/A]\n") == 128.0 + + +def test_smi_3field_query_failure_retries_without_clock(monkeypatch): + # If clocks.max.memory makes the 3-field query fail, detection must retry the + # 2-field query rather than returning zero GPUs (regression guard). + def fake_query(fields: str) -> str: + if "clocks" in fields: + raise subprocess.CalledProcessError(6, "nvidia-smi") + return "NVIDIA GeForce GTX 1650, 4096\n" + + monkeypatch.setattr(nvidia, "_run_smi_query", fake_query) + gpus = nvidia._detect_nvidia_gpus_via_smi() + assert len(gpus) == 1 # not wiped out + assert gpus[0].memory_bandwidth_gbps == 128.0 # no clock => curated default + + +def test_smi_both_queries_fail_returns_empty(monkeypatch): + def always_fail(fields: str) -> str: + raise FileNotFoundError("nvidia-smi") + + monkeypatch.setattr(nvidia, "_run_smi_query", always_fail) + assert nvidia._detect_nvidia_gpus_via_smi() == [] diff --git a/tests/test_http.py b/tests/test_http.py new file mode 100644 index 0000000..1210621 --- /dev/null +++ b/tests/test_http.py @@ -0,0 +1,63 @@ +import asyncio + +import httpx +import pytest + +from whichllm.models.benchmark_sources.chatbot_arena import fetch_arena_scores +from whichllm.models.http import get_with_retries + + +def test_get_with_retries_retries_429_then_returns_response(monkeypatch): + calls = 0 + sleeps: list[float] = [] + + async def fake_sleep(delay: float) -> None: + sleeps.append(delay) + + def handler(request: httpx.Request) -> httpx.Response: + nonlocal calls + calls += 1 + if calls < 3: + return httpx.Response(429, request=request) + return httpx.Response(200, json={"ok": True}, request=request) + + async def run() -> httpx.Response: + monkeypatch.setattr("whichllm.models.http.asyncio.sleep", fake_sleep) + transport = httpx.MockTransport(handler) + async with httpx.AsyncClient(transport=transport) as client: + return await get_with_retries( + client, + "https://example.test/models", + base_delay=0.01, + jitter=0, + ) + + response = asyncio.run(run()) + + assert response.status_code == 200 + assert response.json() == {"ok": True} + assert calls == 3 + assert sleeps == [0.01, 0.02] + + +def test_benchmark_source_retries_429_before_final_failure(monkeypatch): + calls = 0 + + async def fake_sleep(delay: float) -> None: + return None + + def handler(request: httpx.Request) -> httpx.Response: + nonlocal calls + calls += 1 + return httpx.Response(429, request=request) + + async def run() -> None: + monkeypatch.setattr("whichllm.models.http.asyncio.sleep", fake_sleep) + transport = httpx.MockTransport(handler) + async with httpx.AsyncClient(transport=transport) as client: + with pytest.raises(httpx.HTTPStatusError): + await fetch_arena_scores(client) + + asyncio.run(run()) + + assert calls == 3 diff --git a/tests/test_intel_gpu.py b/tests/test_intel_gpu.py new file mode 100644 index 0000000..fae340c --- /dev/null +++ b/tests/test_intel_gpu.py @@ -0,0 +1,130 @@ +"""Tests for Linux Intel integrated GPU detection.""" + +from __future__ import annotations + +import subprocess +from io import StringIO + +from rich.console import Console + +from whichllm.hardware import intel +from whichllm.hardware.types import GPUInfo, HardwareInfo + + +def test_detect_intel_gpu_from_lspci(monkeypatch): + output = ( + '00:02.0 "VGA compatible controller" "Intel Corporation" ' + '"Alder Lake-P GT1 [UHD Graphics]" -r0c "Dell" "Device 0b19"\n' + ) + + def fake_run(*args, **kwargs): + return subprocess.CompletedProcess(args[0], 0, stdout=output, stderr="") + + monkeypatch.setattr(intel.subprocess, "run", fake_run) + + gpus = intel.detect_intel_gpus() + + assert len(gpus) == 1 + assert gpus[0].vendor == "intel" + assert gpus[0].vram_bytes == 0 + assert "UHD Graphics" in gpus[0].name + + +def test_detect_intel_arc_pro_b70_from_battlemage_g31_lspci(monkeypatch): + output = ( + '12:00.0 "VGA compatible controller" "Intel Corporation" ' + '"Battlemage G31 [Intel Graphics]"\n' + ) + + def fake_run(*args, **kwargs): + return subprocess.CompletedProcess(args[0], 0, stdout=output, stderr="") + + monkeypatch.setattr(intel.subprocess, "run", fake_run) + + gpus = intel.detect_intel_gpus() + + assert len(gpus) == 1 + assert gpus[0].name == "Battlemage G31 [Intel Graphics]" + assert gpus[0].vendor == "intel" + assert gpus[0].vram_bytes == 32 * 1024**3 + assert gpus[0].memory_bandwidth_gbps == 608.0 + assert gpus[0].shared_memory is False + + +def test_detect_intel_gpu_ignores_non_display_lspci(monkeypatch): + output = '00:00.0 "Host bridge" "Intel Corporation" "Device 4621"\n' + + def fake_run(*args, **kwargs): + return subprocess.CompletedProcess(args[0], 0, stdout=output, stderr="") + + monkeypatch.setattr(intel.subprocess, "run", fake_run) + monkeypatch.setattr(intel, "_detect_from_sysfs", lambda: []) + + assert intel.detect_intel_gpus() == [] + + +def test_detect_intel_gpu_from_sysfs_when_lspci_missing(monkeypatch, tmp_path): + card = tmp_path / "card0" / "device" + card.mkdir(parents=True) + (card / "vendor").write_text("0x8086\n") + (card / "uevent").write_text("PCI_SLOT_NAME=0000:00:02.0\n") + + monkeypatch.setattr(intel, "_detect_from_lspci", lambda: []) + original_sysfs = intel._detect_from_sysfs + monkeypatch.setattr(intel, "_detect_from_sysfs", lambda: original_sysfs(tmp_path)) + + gpus = intel.detect_intel_gpus() + + assert len(gpus) == 1 + assert gpus[0].vendor == "intel" + assert gpus[0].vram_bytes == 0 + assert gpus[0].name == "Intel Integrated Graphics" + + +def test_detect_intel_arc_pro_b70_from_sysfs_device_id(monkeypatch, tmp_path): + card = tmp_path / "card0" / "device" + card.mkdir(parents=True) + (card / "vendor").write_text("0x8086\n") + (card / "device").write_text("0xe223\n") + + monkeypatch.setattr(intel, "_detect_from_lspci", lambda: []) + original_sysfs = intel._detect_from_sysfs + monkeypatch.setattr(intel, "_detect_from_sysfs", lambda: original_sysfs(tmp_path)) + + gpus = intel.detect_intel_gpus() + + assert len(gpus) == 1 + assert gpus[0].name == "Battlemage G31 [Intel Graphics]" + assert gpus[0].vram_bytes == 32 * 1024**3 + assert gpus[0].memory_bandwidth_gbps == 608.0 + assert gpus[0].shared_memory is False + + +def test_display_intel_shared_memory_without_zero_kb(monkeypatch): + from whichllm.output import _console as console_mod + from whichllm.output import display as display_mod + + buf = StringIO() + monkeypatch.setattr(console_mod, "console", Console(file=buf, force_terminal=False)) + + display_mod.display_hardware( + HardwareInfo( + gpus=[ + GPUInfo( + name="Alder Lake-P GT1 [UHD Graphics]", + vendor="intel", + vram_bytes=0, + shared_memory=True, + ) + ], + cpu_name="CPU", + cpu_cores=8, + ram_bytes=16 * 1024**3, + disk_free_bytes=100 * 1024**3, + os="linux", + ) + ) + + output = buf.getvalue() + assert "shared memory" in output + assert "0 KB" not in output diff --git a/tests/test_kepler_gpus.py b/tests/test_kepler_gpus.py new file mode 100644 index 0000000..7db9739 --- /dev/null +++ b/tests/test_kepler_gpus.py @@ -0,0 +1,174 @@ +"""Tests for legacy Kepler GPU entries and their Vulkan-only handling. + +Kepler cards (compute capability 3.0/3.5) have no CUDA support in modern +llama.cpp, so whichllm carries bandwidth + compute-capability data for them +and flags them as Vulkan-only via ``VULKAN_ONLY_GPUS``. +""" + +import pytest + +from whichllm.constants import ( + GPU_BANDWIDTH, + NVIDIA_COMPUTE_CAPABILITY, + VULKAN_ONLY_GPUS, +) +from whichllm.data.gpu import ( + GPU_BANDWIDTH as DATA_GPU_BANDWIDTH, + VULKAN_ONLY_GPUS as DATA_VULKAN_ONLY_GPUS, +) +from whichllm.engine.compatibility import check_compatibility +from whichllm.hardware.nvidia import ( + _lookup_bandwidth, + _lookup_compute_capability, +) +from whichllm.hardware.types import GPUInfo, HardwareInfo +from whichllm.models.types import GGUFVariant, ModelInfo + +# (GPU name, expected bandwidth GB/s, expected compute capability) +KEPLER_GPUS: list[tuple[str, float, tuple[int, int]]] = [ + ("Quadro K6000", 288.0, (3, 5)), + ("Quadro K5200", 192.3, (3, 5)), + ("Quadro K4200", 173.0, (3, 0)), + ("Quadro K2200", 80.0, (3, 0)), + ("Quadro K620", 29.0, (3, 0)), + ("Quadro K420", 14.4, (3, 0)), + ("GTX 780", 288.4, (3, 5)), + ("GTX 770", 224.3, (3, 0)), + ("GTX 760", 192.2, (3, 0)), +] + + +@pytest.mark.parametrize("name, bandwidth, _cc", KEPLER_GPUS) +def test_kepler_gpu_present_in_bandwidth(name, bandwidth, _cc): + assert name in GPU_BANDWIDTH + assert GPU_BANDWIDTH[name] == bandwidth + + +@pytest.mark.parametrize("name, _bandwidth, cc", KEPLER_GPUS) +def test_kepler_gpu_present_in_compute_capability(name, _bandwidth, cc): + assert name in NVIDIA_COMPUTE_CAPABILITY + assert NVIDIA_COMPUTE_CAPABILITY[name] == cc + + +@pytest.mark.parametrize("name, _bandwidth, cc", KEPLER_GPUS) +def test_kepler_compute_capability_is_kepler_era(name, _bandwidth, cc): + # Kepler is always compute capability 3.x. + assert cc[0] == 3 + + +@pytest.mark.parametrize("name, _bandwidth, _cc", KEPLER_GPUS) +def test_kepler_gpu_marked_vulkan_only(name, _bandwidth, _cc): + assert name in VULKAN_ONLY_GPUS + + +def test_vulkan_only_set_matches_kepler_list(): + expected = {name for name, _bw, _cc in KEPLER_GPUS} + assert set(VULKAN_ONLY_GPUS) == expected + + +def test_vulkan_only_gpus_is_immutable(): + assert isinstance(VULKAN_ONLY_GPUS, frozenset) + + +def test_constants_shim_reexports_data_module(): + # The constants module is a compatibility shim over whichllm.data.gpu. + assert GPU_BANDWIDTH is DATA_GPU_BANDWIDTH + assert VULKAN_ONLY_GPUS is DATA_VULKAN_ONLY_GPUS + + +def test_every_vulkan_only_gpu_has_bandwidth_and_compute_capability(): + # Each flagged card must be fully described so downstream lookups work. + for name in VULKAN_ONLY_GPUS: + assert name in GPU_BANDWIDTH + assert name in NVIDIA_COMPUTE_CAPABILITY + + +@pytest.mark.parametrize("name, bandwidth, cc", KEPLER_GPUS) +def test_nvidia_name_lookup_resolves_kepler_specs(name, bandwidth, cc): + # Detection matches the registry substrings against the full marketing + # name reported by the driver (e.g. "NVIDIA Quadro K4200"). + full_name = f"NVIDIA {name}" + assert _lookup_bandwidth(full_name) == bandwidth + assert _lookup_compute_capability(full_name) == cc + + +def test_k4200_lookup_not_shadowed_by_k420(): + # "Quadro K420" is a substring of "Quadro K4200"; the length-sorted lookup + # must still resolve the longer name to its own bandwidth. + assert _lookup_bandwidth("NVIDIA Quadro K4200") == 173.0 + assert _lookup_bandwidth("NVIDIA Quadro K420") == 14.4 + + +def _kepler_hardware(name: str, cc: tuple[int, int], bandwidth: float) -> HardwareInfo: + return HardwareInfo( + gpus=[ + GPUInfo( + name=f"NVIDIA {name}", + vendor="nvidia", + vram_bytes=4 * 1024**3, + compute_capability=cc, + memory_bandwidth_gbps=bandwidth, + ) + ], + cpu_name="Test CPU", + cpu_cores=8, + ram_bytes=16 * 1024**3, + disk_free_bytes=200 * 1024**3, + os="linux", + ) + + +@pytest.mark.parametrize("name, bandwidth, cc", KEPLER_GPUS) +def test_compatibility_flags_kepler_as_vulkan_only(name, bandwidth, cc): + model = ModelInfo( + id="test/model", + family_id="test/model", + name="model", + parameter_count=3_000_000_000, + ) + variant = GGUFVariant( + filename="model-Q4_K_M.gguf", + quant_type="Q4_K_M", + file_size_bytes=2_000_000_000, + ) + hw = _kepler_hardware(name, cc, bandwidth) + + result = check_compatibility(model, variant, hw) + + assert any("vulkan" in w.lower() for w in result.warnings), ( + f"{name} should be flagged as Vulkan-only" + ) + + +def test_compatibility_does_not_flag_modern_nvidia_as_vulkan_only(): + model = ModelInfo( + id="test/model", + family_id="test/model", + name="model", + parameter_count=3_000_000_000, + ) + variant = GGUFVariant( + filename="model-Q4_K_M.gguf", + quant_type="Q4_K_M", + file_size_bytes=2_000_000_000, + ) + hw = HardwareInfo( + gpus=[ + GPUInfo( + name="NVIDIA GeForce RTX 4090", + vendor="nvidia", + vram_bytes=24 * 1024**3, + compute_capability=(8, 9), + memory_bandwidth_gbps=1008.0, + ) + ], + cpu_name="Test CPU", + cpu_cores=8, + ram_bytes=32 * 1024**3, + disk_free_bytes=200 * 1024**3, + os="linux", + ) + + result = check_compatibility(model, variant, hw) + + assert not any("vulkan" in w.lower() for w in result.warnings) diff --git a/tests/test_markdown_output.py b/tests/test_markdown_output.py new file mode 100644 index 0000000..13fae9c --- /dev/null +++ b/tests/test_markdown_output.py @@ -0,0 +1,167 @@ +"""Tests for pasteable Markdown ranking output.""" + +from io import StringIO + +from rich.console import Console + +from whichllm.engine.types import CompatibilityResult +from whichllm.hardware.types import GPUInfo, HardwareInfo +from whichllm.models.types import GGUFVariant, ModelInfo +from whichllm.output.markdown import display_markdown + + +def _capture_markdown( + results: list[CompatibilityResult], + hardware: HardwareInfo, + *, + show_status: bool, + empty_message: str | None = None, +) -> str: + import whichllm.output._console as console_mod + + buf = StringIO() + orig_console = console_mod.console + console_mod.console = Console(file=buf, force_terminal=False) + try: + display_markdown( + results, + hardware, + show_status=show_status, + empty_message=empty_message, + ) + finally: + console_mod.console = orig_console + return buf.getvalue().strip() + + +def _hardware() -> HardwareInfo: + return HardwareInfo( + gpus=[ + GPUInfo( + name="RTX 4090", + vendor="nvidia", + vram_bytes=24 * 1024**3, + memory_bandwidth_gbps=1008.0, + ) + ], + cpu_name="Test CPU", + cpu_cores=16, + ram_bytes=64 * 1024**3, + disk_free_bytes=500 * 1024**3, + os="linux", + ) + + +def _result( + index: int, + *, + benchmark_status: str = "direct", + speed_confidence: str = "medium", +) -> CompatibilityResult: + model = ModelInfo( + id=f"org/Test-{index}|Model", + family_id=f"test-{index}", + name=f"Test-{index}", + parameter_count=7_000_000_000 + index, + downloads=1_500 * index, + likes=index, + license="apache-2.0", + published_at=f"2026-01-0{index}T00:00:00Z", + ) + return CompatibilityResult( + model=model, + gguf_variant=GGUFVariant( + filename=f"test-{index}.gguf", + quant_type="Q4_K_M", + file_size_bytes=4 * 1024**3, + ), + can_run=True, + vram_required_bytes=(4 + index) * 1024**3, + vram_available_bytes=24 * 1024**3, + estimated_tok_per_sec=10.0 * index, + speed_confidence=speed_confidence, + quality_score=80.0 - index, + fit_type="full_gpu", + benchmark_status=benchmark_status, + benchmark_source=benchmark_status, + benchmark_confidence=1.0, + ) + + +def test_display_markdown_runtime_table_top_three(): + output = _capture_markdown( + [ + _result(1, speed_confidence="medium"), + _result(2, benchmark_status="estimated", speed_confidence="low"), + _result(3, benchmark_status="none", speed_confidence="high"), + ], + _hardware(), + show_status=True, + ) + + assert output.startswith("## Recommended Models") + assert ( + "| # | Model | Params | Quant | Fit | VRAM | Speed | Published | Score | License |" + in output + ) + assert ( + "| 1 | org/Test-1\\|Model | 7.0B | Q4_K_M | Full GPU | 5.0 GB | 10.0 tok/s ~ | 2026-01-01 | 79.0 | apache-2.0 |" + in output + ) + assert "20.0 tok/s ?" in output + assert "78.0 ~" in output + assert "77.0 ?" in output + + +def test_display_markdown_details_table_uses_metadata_columns(): + output = _capture_markdown([_result(1)], _hardware(), show_status=False) + + assert ( + "| # | Model | Params | Quant | Published | Downloads | Score | License |" + in output + ) + assert "Fit | VRAM | Speed" not in output + assert ( + "| 1 | org/Test-1\\|Model | 7.0B | Q4_K_M | 2026-01-01 | 1.5K | 79.0 | apache-2.0 |" + in output + ) + + +def test_display_markdown_links_to_resolved_artifact_repo(): + result = _result(1) + result.model.id = "Qwen/Qwen3-4B-Thinking-2507" + result.gguf_variant = GGUFVariant( + filename="Qwen3-4B-Thinking-2507.Q3_K_M.gguf", + quant_type="Q3_K_M", + file_size_bytes=2 * 1024**3, + ) + result.artifact_model = ModelInfo( + id="MaziyarPanahi/Qwen3-4B-Thinking-2507-GGUF", + family_id=result.model.family_id, + name="Qwen3-4B-Thinking-2507-GGUF", + parameter_count=result.model.parameter_count, + ) + result.artifact_variant = GGUFVariant( + filename="Qwen3-4B-Thinking-2507-Q3_K_M.gguf", + quant_type="Q3_K_M", + file_size_bytes=2 * 1024**3, + ) + + output = _capture_markdown([result], _hardware(), show_status=False) + + assert ( + "[Qwen/Qwen3-4B-Thinking-2507]" + "(https://huggingface.co/MaziyarPanahi/Qwen3-4B-Thinking-2507-GGUF)" in output + ) + assert "Q3_K_M" in output + + +def test_display_markdown_empty_results(): + output = _capture_markdown( + [], + _hardware(), + show_status=True, + empty_message="Nothing matched.", + ) + + assert output == "## Recommended Models\n\nNothing matched." diff --git a/tests/test_memory.py b/tests/test_memory.py new file mode 100644 index 0000000..879acf9 --- /dev/null +++ b/tests/test_memory.py @@ -0,0 +1,40 @@ +"""Tests for hardware.memory — estimate_usable_ram bounded-reserve formula.""" + +import pytest + +from whichllm.hardware.memory import estimate_usable_ram + +_GiB = 1024**3 + + +def _expected_usable(total: int) -> int: + reserve = int(total * 0.15) + reserve = max(4 * _GiB, min(reserve, 32 * _GiB)) + return total - reserve + + +@pytest.mark.parametrize( + "total_gb", + [16, 32, 64, 128, 1024], + ids=["16GB", "32GB", "64GB", "128GB", "1TB"], +) +def test_estimate_usable_ram(total_gb): + total = total_gb * _GiB + assert estimate_usable_ram(total) == _expected_usable(total) + + +def test_16gb_hits_min_reserve(): + total = 16 * _GiB + assert estimate_usable_ram(total) == total - 4 * _GiB + + +def test_1tb_hits_max_reserve(): + total = 1024 * _GiB + assert estimate_usable_ram(total) == total - 32 * _GiB + + +def test_midrange_uses_percentage(): + total = 64 * _GiB + expected_reserve = int(total * 0.15) + assert 4 * _GiB < expected_reserve < 32 * _GiB + assert estimate_usable_ram(total) == total - expected_reserve diff --git a/tests/test_nvidia_detection.py b/tests/test_nvidia_detection.py new file mode 100644 index 0000000..9a955e9 --- /dev/null +++ b/tests/test_nvidia_detection.py @@ -0,0 +1,194 @@ +import builtins +import subprocess +from types import SimpleNamespace +from unittest.mock import Mock + +from whichllm.hardware.nvidia import detect_nvidia_gpus + + +def test_nvidia_smi_fallback_when_pynvml_missing(monkeypatch): + real_import = builtins.__import__ + + def fake_import(name, *args, **kwargs): + if name == "pynvml": + raise ImportError + return real_import(name, *args, **kwargs) + + def fake_run(*args, **kwargs): + return SimpleNamespace(stdout="NVIDIA GeForce RTX 5060 Ti, 16303\n") + + monkeypatch.setattr(builtins, "__import__", fake_import) + monkeypatch.setattr(subprocess, "run", fake_run) + + gpus = detect_nvidia_gpus() + + assert len(gpus) == 1 + assert gpus[0].name == "NVIDIA GeForce RTX 5060 Ti" + assert gpus[0].vendor == "nvidia" + assert gpus[0].vram_bytes == 16303 * 1024**2 + + +def test_nvidia_smi_fallback_applies_rtx_a3000_laptop_catalog(monkeypatch): + real_import = builtins.__import__ + + def fake_import(name, *args, **kwargs): + if name == "pynvml": + raise ImportError + return real_import(name, *args, **kwargs) + + def fake_run(*args, **kwargs): + return SimpleNamespace(stdout="NVIDIA RTX A3000 Laptop GPU, 6144 MiB\n") + + monkeypatch.setattr(builtins, "__import__", fake_import) + monkeypatch.setattr(subprocess, "run", fake_run) + + gpus = detect_nvidia_gpus() + + assert len(gpus) == 1 + assert gpus[0].name == "NVIDIA RTX A3000 Laptop GPU" + assert gpus[0].compute_capability == (8, 6) + assert gpus[0].memory_bandwidth_gbps == 264.0 + + +def test_nvidia_smi_fallback_resolves_laptop_5090_via_dbgpu(monkeypatch): + # Regression for #74: a laptop RTX 5090 must not inherit the desktop + # 1792 GB/s bandwidth. dbgpu carries "GeForce RTX 5090 Mobile" (~896). + real_import = builtins.__import__ + + def fake_import(name, *args, **kwargs): + if name == "pynvml": + raise ImportError + return real_import(name, *args, **kwargs) + + def fake_run(*args, **kwargs): + return SimpleNamespace(stdout="NVIDIA GeForce RTX 5090 Laptop GPU, 24564 MiB\n") + + monkeypatch.setattr(builtins, "__import__", fake_import) + monkeypatch.setattr(subprocess, "run", fake_run) + + gpus = detect_nvidia_gpus() + + assert len(gpus) == 1 + assert gpus[0].name == "NVIDIA GeForce RTX 5090 Laptop GPU" + bw = gpus[0].memory_bandwidth_gbps + assert bw is not None + assert bw < 1500.0 # mobile ~896, not the 1792 desktop value + + +def test_nvidia_smi_fallback_when_nvml_init_fails(monkeypatch): + class FakeNVMLError(Exception): + pass + + fake_pynvml = SimpleNamespace( + NVMLError=FakeNVMLError, + nvmlInit=Mock(side_effect=FakeNVMLError("NVML unavailable")), + ) + real_import = builtins.__import__ + + def fake_import(name, *args, **kwargs): + if name == "pynvml": + return fake_pynvml + return real_import(name, *args, **kwargs) + + def fake_run(*args, **kwargs): + return SimpleNamespace(stdout="NVIDIA DGX Spark, 128000 MiB\n") + + monkeypatch.setattr(builtins, "__import__", fake_import) + monkeypatch.setattr(subprocess, "run", fake_run) + + gpus = detect_nvidia_gpus() + + assert len(gpus) == 1 + assert gpus[0].name == "NVIDIA DGX Spark" + assert gpus[0].vendor == "nvidia" + assert gpus[0].vram_bytes == 128000 * 1024**2 + assert gpus[0].shared_memory is True + + +def test_nvidia_smi_fallback_detects_gb10_with_na_memory(monkeypatch): + real_import = builtins.__import__ + + def fake_import(name, *args, **kwargs): + if name == "pynvml": + raise ImportError + return real_import(name, *args, **kwargs) + + def fake_run(*args, **kwargs): + return SimpleNamespace(stdout="NVIDIA GB10, [N/A]\n") + + monkeypatch.setattr(builtins, "__import__", fake_import) + monkeypatch.setattr(subprocess, "run", fake_run) + monkeypatch.setattr( + "whichllm.hardware.memory.detect_ram_bytes", lambda: 128 * 1024**3 + ) + + gpus = detect_nvidia_gpus() + + assert len(gpus) == 1 + assert gpus[0].name == "NVIDIA GB10" + assert gpus[0].vendor == "nvidia" + assert gpus[0].vram_bytes == 128 * 1024**3 + assert gpus[0].shared_memory is True + assert gpus[0].compute_capability == (12, 1) + assert gpus[0].memory_bandwidth_gbps == 273.0 + + +def test_nvml_detects_gb10_when_memory_info_is_unavailable(monkeypatch): + class FakeNVMLError(Exception): + pass + + handle = object() + fake_pynvml = SimpleNamespace( + NVMLError=FakeNVMLError, + nvmlInit=Mock(), + nvmlDeviceGetCount=Mock(return_value=1), + nvmlSystemGetDriverVersion=Mock(return_value=b"580.142"), + nvmlSystemGetCudaDriverVersion_v2=Mock(return_value=12010), + nvmlDeviceGetHandleByIndex=Mock(return_value=handle), + nvmlDeviceGetName=Mock(return_value="NVIDIA GB10"), + nvmlDeviceGetMemoryInfo=Mock(side_effect=FakeNVMLError("not supported")), + nvmlShutdown=Mock(), + ) + real_import = builtins.__import__ + + def fake_import(name, *args, **kwargs): + if name == "pynvml": + return fake_pynvml + return real_import(name, *args, **kwargs) + + def fake_run(*args, **kwargs): + raise AssertionError("nvidia-smi fallback should not be used") + + monkeypatch.setattr(builtins, "__import__", fake_import) + monkeypatch.setattr(subprocess, "run", fake_run) + monkeypatch.setattr( + "whichllm.hardware.memory.detect_ram_bytes", lambda: 128 * 1024**3 + ) + + gpus = detect_nvidia_gpus() + + assert len(gpus) == 1 + assert gpus[0].name == "NVIDIA GB10" + assert gpus[0].vendor == "nvidia" + assert gpus[0].vram_bytes == 128 * 1024**3 + assert gpus[0].shared_memory is True + assert gpus[0].compute_capability == (12, 1) + assert gpus[0].cuda_version == "12.1" + assert gpus[0].memory_bandwidth_gbps == 273.0 + + +def test_nvidia_smi_fallback_returns_empty_on_command_failure(monkeypatch): + real_import = builtins.__import__ + + def fake_import(name, *args, **kwargs): + if name == "pynvml": + raise ImportError + return real_import(name, *args, **kwargs) + + def fake_run(*args, **kwargs): + raise FileNotFoundError + + monkeypatch.setattr(builtins, "__import__", fake_import) + monkeypatch.setattr(subprocess, "run", fake_run) + + assert detect_nvidia_gpus() == [] diff --git a/tests/test_output_formatting.py b/tests/test_output_formatting.py new file mode 100644 index 0000000..36a916f --- /dev/null +++ b/tests/test_output_formatting.py @@ -0,0 +1,32 @@ +"""Tests for output formatting helpers.""" + +from whichllm.engine.types import CompatibilityResult +from whichllm.models.types import ModelInfo +from whichllm.output.formatting import _format_speed + + +def _result(speed: float, confidence: str) -> CompatibilityResult: + return CompatibilityResult( + model=ModelInfo( + id="org/model", + family_id="org/model", + name="model", + parameter_count=1_000_000_000, + ), + gguf_variant=None, + can_run=True, + vram_required_bytes=0, + vram_available_bytes=0, + estimated_tok_per_sec=speed, + speed_confidence=confidence, + ) + + +def test_format_speed_colors_by_runtime_speed_not_confidence(): + assert _format_speed(_result(2.5, "medium")) == "[red]2.5 tok/s ~[/red]" + assert _format_speed(_result(6.0, "low")) == "[yellow]6.0 tok/s ?[/yellow]" + assert _format_speed(_result(12.0, "medium")) == "[green]12.0 tok/s ~[/green]" + assert ( + _format_speed(_result(30.0, "low")) + == "[bright_green]30.0 tok/s ?[/bright_green]" + ) diff --git a/tests/test_p1_p3_regressions.py b/tests/test_p1_p3_regressions.py new file mode 100644 index 0000000..aa99b28 --- /dev/null +++ b/tests/test_p1_p3_regressions.py @@ -0,0 +1,513 @@ +"""Regression tests for the Phase 1-3 ranking-quality fixes. + +Each test reproduces a specific bug class that the original whichllm 0.5.0 +ranker exhibited on 2026-05-14: + +- Q1_0 / Q2_0 derivatives must not surface as full GPU candidates by + default (P1: extreme-quant exclusion). +- Self-reported (hf_eval) values must not beat independent leaderboard + evidence (P3-1: self_reported tier). +- CI / research orgs (gpt2, opt-125m, tiny-Qwen2ForCausalLM) must never + occupy a ranking slot (P2-2: excluded orgs). +- Newer generation in the same family should beat the older one when + benchmark data is unavailable or weak (P2-1: lineage bonus). +- Heretic / abliterated / uncensored derivatives should rank below their + clean base/converter counterparts (P2-3: derivative penalty). +""" + +from __future__ import annotations + +from whichllm.engine.ranker import ( + _SOURCE_WEIGHTS, + _derivative_name_penalty, + _generation_bonus, + _is_excluded_model, + rank_models, +) +from whichllm.hardware.types import GPUInfo, HardwareInfo +from whichllm.models.types import GGUFVariant, ModelInfo + + +def _hw( + vram_gb: int = 24, + bandwidth_gbps: float = 1000.0, + vendor: str = "nvidia", + os_name: str = "linux", + with_gpu: bool = True, +) -> HardwareInfo: + gpus = [] + if with_gpu: + gpus = [ + GPUInfo( + name="Test GPU", + vendor=vendor, + vram_bytes=vram_gb * 1024**3, + compute_capability=(8, 9) if vendor == "nvidia" else None, + memory_bandwidth_gbps=bandwidth_gbps, + ) + ] + return HardwareInfo( + gpus=gpus, + cpu_name="Test CPU", + cpu_cores=8, + has_avx2=True, + ram_bytes=64 * 1024**3, + disk_free_bytes=500 * 1024**3, + os=os_name, + ) + + +def _gguf(quant: str, size_gb: float) -> GGUFVariant: + return GGUFVariant( + filename=f"model-{quant}.gguf", + quant_type=quant, + file_size_bytes=int(size_gb * 1e9), + ) + + +# ---------------------------------------------------------------------- P1 + + +def test_p1_q1_0_only_repo_is_severely_penalized_when_no_quant_filter(): + """An 8B repo that only ships Q1_0 may still appear as a fallback (since + it's the only thing the repo offers) but its quality score must be + crushed by the combined Q1_0 (-55%) + self_reported (-45%) penalties so + it cannot beat a normal Q4_K_M alternative.""" + bonsai = ModelInfo( + id="prism-ml/Bonsai-8B-gguf", + family_id="bonsai-8b", + name="Bonsai-8B", + parameter_count=8_000_000_000, + downloads=48_000, + likes=0, + gguf_variants=[_gguf("Q1_0", 4.6)], + benchmark_scores={"hf_eval": 88.0}, + ) + normal = ModelInfo( + id="bartowski/Qwen3-8B-GGUF", + family_id="qwen3-8b", + name="Qwen3-8B", + parameter_count=8_000_000_000, + downloads=58_000, + likes=20, + base_model="Qwen/Qwen3-8B", + gguf_variants=[_gguf("Q4_K_M", 4.7)], + ) + results = rank_models( + [bonsai, normal], + _hw(), + top_n=5, + benchmark_scores={"Qwen/Qwen3-8B-Instruct": 65.0}, + ) + ids = [r.model.id for r in results] + assert "bartowski/Qwen3-8B-GGUF" in ids + # The Q1_0 + self_reported combo must end up below the normal Q4_K_M. + sc_bonsai = next( + (r.quality_score for r in results if r.model.id == "prism-ml/Bonsai-8B-gguf"), + 0.0, + ) + sc_normal = next( + (r.quality_score for r in results if r.model.id == "bartowski/Qwen3-8B-GGUF"), + 0.0, + ) + assert sc_normal > sc_bonsai + # And the absolute Bonsai score must be in the "obviously broken" band. + assert sc_bonsai < 25 + + +def test_p1_q1_0_returned_when_explicitly_requested_via_quant_filter(): + """Users can still ask for Q1_0 with --quant Q1_0 when they really mean it.""" + model = ModelInfo( + id="prism-ml/Bonsai-8B-gguf", + family_id="bonsai-8b", + name="Bonsai-8B", + parameter_count=8_000_000_000, + downloads=48_000, + likes=0, + gguf_variants=[_gguf("Q1_0", 4.6)], + ) + results = rank_models( + [model], _hw(), top_n=5, benchmark_scores={}, quant_filter="Q1_0" + ) + assert len(results) == 1 + assert results[0].gguf_variant is not None + assert results[0].gguf_variant.quant_type == "Q1_0" + + +def test_p1_q1_q2_quality_penalty_is_severe(): + """Sub-2-bit quants must carry 40-60% quality penalty, not the + legacy 5% fallback. We assert the constant directly to lock this in.""" + from whichllm.constants import QUANT_QUALITY_PENALTY + + assert QUANT_QUALITY_PENALTY["Q1_0"] >= 0.50 + assert QUANT_QUALITY_PENALTY["Q2_0"] >= 0.40 + assert QUANT_QUALITY_PENALTY["TQ1_0"] >= 0.50 + assert QUANT_QUALITY_PENALTY["IQ1_S"] >= 0.50 + assert QUANT_QUALITY_PENALTY["IQ1_M"] >= 0.45 + assert QUANT_QUALITY_PENALTY["IQ2_M"] >= 0.25 + + +# ---------------------------------------------------------------------- P2-2 + + +def test_p2_excluded_orgs_never_rank(): + """gpt2 / opt-125m / TRL fixtures must be skipped regardless of DL.""" + gpt2 = ModelInfo( + id="openai-community/gpt2", + family_id="gpt2", + name="gpt2", + parameter_count=124_000_000, + downloads=16_000_000, + likes=1000, + ) + opt = ModelInfo( + id="facebook/opt-125m", + family_id="opt-125m", + name="opt-125m", + parameter_count=125_000_000, + downloads=9_000_000, + ) + tiny = ModelInfo( + id="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", + family_id="tiny-q2", + name="tiny", + parameter_count=1_000_000, + downloads=6_000_000, + ) + # also a normal model so the result list is non-empty + real = ModelInfo( + id="bartowski/gemma-3-12b-it-GGUF", + family_id="gemma-3-12b", + name="gemma-3-12b", + parameter_count=12_200_000_000, + downloads=70_000, + likes=10, + gguf_variants=[_gguf("Q4_K_M", 7.0)], + ) + results = rank_models([gpt2, opt, tiny, real], _hw(), top_n=10, benchmark_scores={}) + ids = [r.model.id for r in results] + assert "openai-community/gpt2" not in ids + assert "facebook/opt-125m" not in ids + assert "trl-internal-testing/tiny-Qwen2ForCausalLM-2.5" not in ids + assert "bartowski/gemma-3-12b-it-GGUF" in ids + + +def test_p2_is_excluded_model_helper(): + assert _is_excluded_model("openai-community/gpt2") + assert _is_excluded_model("facebook/opt-125m") + assert _is_excluded_model("EleutherAI/pythia-70m-deduped") + assert _is_excluded_model("trl-internal-testing/tiny-Qwen2ForCausalLM-2.5") + assert _is_excluded_model("hmellor/tiny-random-LlamaForCausalLM") + assert not _is_excluded_model("Qwen/Qwen3-8B") + assert not _is_excluded_model("meta-llama/Llama-4-Maverick-17B-128E-Instruct") + + +# ---------------------------------------------------------------------- P2-1 + + +def test_p2_generation_bonus_newer_wins_over_legacy_in_same_family(): + new = _generation_bonus("Qwen/Qwen3.6-27B") + mid = _generation_bonus("Qwen/Qwen3-8B") + old = _generation_bonus("Qwen/Qwen2.5-7B-Instruct") + ancient = _generation_bonus("Qwen/Qwen-7B") + assert new > mid > old > ancient + + +def test_p2_lineage_covers_llama_deepseek_gemma_phi(): + assert _generation_bonus("meta-llama/Llama-4-Scout") > _generation_bonus( + "meta-llama/Llama-3.1-8B-Instruct" + ) + assert _generation_bonus("deepseek-ai/DeepSeek-V4-Pro") > _generation_bonus( + "deepseek-ai/DeepSeek-V2.5" + ) + assert _generation_bonus("google/gemma-4-31b-it") > _generation_bonus( + "google/gemma-2-27b-it" + ) + assert _generation_bonus("microsoft/phi-4") > _generation_bonus( + "microsoft/Phi-3-mini-4k-instruct" + ) + + +def test_p2_lineage_covers_t5_variants_without_gemma_collision(): + assert _generation_bonus("google/t5gemma-4b") > _generation_bonus( + "google/flan-t5-xl" + ) + assert _generation_bonus("google/t5-gemma-4b") == _generation_bonus( + "google/t5gemma-4b" + ) + assert _generation_bonus("google/t5_gemma-4b") == _generation_bonus( + "google/t5gemma-4b" + ) + assert _generation_bonus("openai/gpt5-test") == 0.0 + + +def test_p2_unknown_family_gets_zero_bonus(): + assert _generation_bonus("random-org/random-model-7b") == 0.0 + + +# ---------------------------------------------------------------------- P2-3 + + +def test_p2_derivative_penalty_for_heretic_uncensored(): + assert ( + _derivative_name_penalty("diffusionmodels1254ani/gemma-3-12b-it-heretic-v2") < 0 + ) + assert ( + _derivative_name_penalty( + "Youssofal/Qwen3.6-27B-Abliterated-Heretic-Uncensored-GGUF" + ) + < 0 + ) + assert _derivative_name_penalty("OBLITERATUS/gemma-4-E4B-it-OBLITERATED") < 0 + assert _derivative_name_penalty("bartowski/Qwen3-32B-GGUF") == 0.0 + + +# ---------------------------------------------------------------------- P3-1 + + +def test_p3_self_reported_evidence_does_not_outrank_direct_leaderboard(): + """A self-reported eval (e.g. prism-ml/Bonsai claiming 91) must NOT + outrank an independent-leaderboard hit on a comparable model.""" + self_reported = ModelInfo( + id="prism-ml/Self-Reported-8B", + family_id="self-reported-8b", + name="Self-Reported", + parameter_count=8_000_000_000, + downloads=20_000, + likes=10, + gguf_variants=[_gguf("Q4_K_M", 4.5)], + benchmark_scores={"hf_eval": 91.0}, + ) + direct_hit = ModelInfo( + id="trusted-org/Real-Bench-8B", + family_id="real-bench-8b", + name="Real-Bench", + parameter_count=8_000_000_000, + downloads=50_000, + likes=200, + gguf_variants=[_gguf("Q4_K_M", 4.5)], + ) + results = rank_models( + [self_reported, direct_hit], + _hw(), + top_n=5, + benchmark_scores={"trusted-org/Real-Bench-8B": 70.0}, + ) + assert len(results) == 2 + assert results[0].model.id == "trusted-org/Real-Bench-8B" + assert results[0].benchmark_status == "direct" + assert results[1].benchmark_status == "self_reported" + + +def test_p3_self_reported_outranks_only_when_there_is_nothing_else(): + """When no other evidence exists, self_reported should still produce a + score so the candidate isn't filtered out — just at a lower weight.""" + only_self_reported = ModelInfo( + id="some-org/Only-Self-Reported-8B", + family_id="only-sr-8b", + name="Only-SR", + parameter_count=8_000_000_000, + downloads=20_000, + gguf_variants=[_gguf("Q4_K_M", 4.5)], + benchmark_scores={"hf_eval": 90.0}, + ) + results = rank_models( + [only_self_reported], + _hw(), + top_n=5, + benchmark_scores={}, + ) + assert len(results) == 1 + assert results[0].benchmark_status == "self_reported" + # Score should be positive but well below what a 90 direct-leaderboard + # would have produced (which would push past 60 with this size class). + assert 0 < results[0].quality_score < 60 + + +def test_p3_source_weights_ordering(): + """Direct must weight the most, self_reported the least among + benchmark-producing sources.""" + assert _SOURCE_WEIGHTS["direct"] > _SOURCE_WEIGHTS["base_model"] + assert _SOURCE_WEIGHTS["base_model"] > _SOURCE_WEIGHTS["variant"] + assert _SOURCE_WEIGHTS["variant"] > _SOURCE_WEIGHTS["line_interp"] + assert _SOURCE_WEIGHTS["line_interp"] > _SOURCE_WEIGHTS["self_reported"] + assert _SOURCE_WEIGHTS["self_reported"] > 0 + assert _SOURCE_WEIGHTS["none"] == 0.0 + + +def test_p3_strict_evidence_filter_excludes_self_reported(): + """`--evidence strict` should keep only direct hits, dropping + self_reported (the exact bug that surfaced 5 prism-ml/squ11z1 + models as 'direct') as well as inherited evidence.""" + self_reported = ModelInfo( + id="some-org/Self-Reported-8B", + family_id="sr-8b", + name="SR", + parameter_count=8_000_000_000, + downloads=20_000, + gguf_variants=[_gguf("Q4_K_M", 4.5)], + benchmark_scores={"hf_eval": 95.0}, + ) + direct_hit = ModelInfo( + id="trusted-org/Real-Bench-8B", + family_id="real-bench-8b", + name="Real-Bench", + parameter_count=8_000_000_000, + downloads=50_000, + gguf_variants=[_gguf("Q4_K_M", 4.5)], + ) + results = rank_models( + [self_reported, direct_hit], + _hw(), + top_n=5, + benchmark_scores={"trusted-org/Real-Bench-8B": 70.0}, + evidence_filter="strict", + ) + ids = [r.model.id for r in results] + assert ids == ["trusted-org/Real-Bench-8B"] + + +# -------------------- Rating-push regressions (Phase 2) -------------------- + + +def test_official_org_safetensors_gets_q4km_synthesis(): + """A Qwen/Meta/Google safetensors-only repo should produce realistic + Q4_K_M candidates rather than being scored as bf16 (which forces + partial_offload on consumer GPUs).""" + model = ModelInfo( + id="Qwen/Qwen3.6-27B", + family_id="qwen3.6-27b", + name="Qwen3.6-27B", + parameter_count=27_000_000_000, + downloads=50_000, + gguf_variants=[], # safetensors only — synthesis should kick in + ) + results = rank_models( + [model], + _hw(vram_gb=24), + top_n=1, + benchmark_scores={"Qwen/Qwen3.6-27B": 84.0}, + ) + assert results + chosen = results[0] + assert chosen.fit_type == "full_gpu" + assert chosen.gguf_variant is not None + # Synthesis offers a range of K-quants — any of them is acceptable so + # long as a GGUF candidate is constructed (i.e. the safetensors-only + # repo is rankable at a realistic quant). + assert chosen.gguf_variant.quant_type in { + "Q3_K_M", + "Q4_K_M", + "Q5_K_M", + "Q6_K", + "Q8_0", + } + + +def test_prequantized_repo_skips_synthesis(): + """An -AWQ / -GPTQ repo must not get synthetic Q4_K_M candidates — those + repos can't actually serve a GGUF variant.""" + model = ModelInfo( + id="Qwen/Qwen2.5-14B-Instruct-AWQ", + family_id="qwen2.5-14b-awq", + name="Qwen2.5-14B-Instruct-AWQ", + parameter_count=14_000_000_000, + downloads=10_000, + ) + q4_filtered = rank_models( + [model], + _hw(vram_gb=24), + top_n=5, + benchmark_scores={"Qwen/Qwen2.5-14B-Instruct-AWQ": 70.0}, + quant_filter="Q4_K_M", + ) + assert q4_filtered == [] + + +def test_newer_generation_beats_older_at_same_size(): + """With the realigned AA/LB normalizers and stronger generation lineage + bonus, a current-gen 8B model should outrank a frozen-OLLB-favored + previous-gen 7B.""" + new_gen = ModelInfo( + id="Qwen/Qwen3-8B", + family_id="qwen3-8b", + name="Qwen3-8B", + parameter_count=8_000_000_000, + downloads=10_000_000, + gguf_variants=[_gguf("Q4_K_M", 4.5)], + ) + old_gen = ModelInfo( + id="Qwen/Qwen2.5-7B-Instruct", + family_id="qwen2.5-7b", + name="Qwen2.5-7B-Instruct", + parameter_count=7_000_000_000, + downloads=12_000_000, + gguf_variants=[_gguf("Q4_K_M", 4.0)], + ) + results = rank_models( + [new_gen, old_gen], + _hw(vram_gb=24), + top_n=2, + benchmark_scores={ + "Qwen/Qwen3-8B": 56.0, # current AA/LB-derived + "Qwen/Qwen2.5-7B-Instruct": 35.0, # current source supersedes OLLB + }, + ) + assert [r.model.id for r in results][0] == "Qwen/Qwen3-8B" + + +def test_speed_estimator_differs_by_quant_and_backend(): + """Q4_K_M should run faster than F16 on the same hardware, and CUDA + should be faster than Metal at equal bandwidth.""" + from whichllm.engine.performance import estimate_tok_per_sec + from whichllm.hardware.types import GPUInfo + + model = ModelInfo( + id="t/x", + family_id="t/x", + name="x", + parameter_count=8_000_000_000, + downloads=0, + ) + q4 = GGUFVariant( + filename="x.Q4_K_M.gguf", quant_type="Q4_K_M", file_size_bytes=int(8e9 * 0.5625) + ) + f16 = GGUFVariant( + filename="x.F16.gguf", quant_type="F16", file_size_bytes=int(8e9 * 2.0) + ) + cuda = GPUInfo( + name="t-nv", + vendor="nvidia", + vram_bytes=24 * 1024**3, + memory_bandwidth_gbps=1000.0, + ) + metal = GPUInfo( + name="t-apple", + vendor="apple", + vram_bytes=24 * 1024**3, + memory_bandwidth_gbps=1000.0, + ) + q4_cuda = estimate_tok_per_sec(model, q4, cuda, "full_gpu") + f16_cuda = estimate_tok_per_sec(model, f16, cuda, "full_gpu") + q4_metal = estimate_tok_per_sec(model, q4, metal, "full_gpu") + assert q4_cuda > f16_cuda # Q4 dequant is faster than F16 matmul read + assert q4_cuda > q4_metal # CUDA beats Metal at equal bandwidth + + +def test_vram_kv_cache_scales_with_context(): + """KV-cache contribution should grow linearly with context length so + longer contexts surface as a real VRAM cost in `plan`.""" + from whichllm.engine.vram import estimate_kv_cache + + model = ModelInfo( + id="t/x", + family_id="t/x", + name="x", + parameter_count=32_000_000_000, + downloads=0, + ) + kv_4k = estimate_kv_cache(model, 4096) + kv_32k = estimate_kv_cache(model, 32768) + assert kv_32k > kv_4k * 7 # near-linear in context length + # and the absolute size at 32K should be in the gigabyte range + assert kv_32k > 2 * 1024**3 diff --git a/tests/test_quantization.py b/tests/test_quantization.py new file mode 100644 index 0000000..cc6ba7d --- /dev/null +++ b/tests/test_quantization.py @@ -0,0 +1,141 @@ +"""Tests for non-GGUF quantization inference.""" + +from whichllm.engine.quantization import ( + effective_quant_type, + estimate_weight_bytes, + infer_non_gguf_quant_type, +) +from whichllm.engine.vram import estimate_vram +from whichllm.models.types import ModelInfo + + +def _make_model(model_id: str, params: int = 14_000_000_000) -> ModelInfo: + return ModelInfo( + id=model_id, + family_id=model_id, + name=model_id.split("/")[-1], + parameter_count=params, + ) + + +def test_infer_non_gguf_awq(): + model = _make_model("Qwen/Qwen2.5-14B-Instruct-AWQ") + assert infer_non_gguf_quant_type(model.id) == "AWQ" + assert effective_quant_type(model, None) == "AWQ" + + +def test_estimate_weight_bytes_for_awq(): + model = _make_model("Qwen/Qwen2.5-14B-Instruct-AWQ", params=10_000_000_000) + assert estimate_weight_bytes(model, None) == 5_000_000_000 + + +def test_awq_vram_is_lower_than_fp16_fallback(): + awq = _make_model("Qwen/Qwen2.5-14B-Instruct-AWQ") + fp16 = _make_model("Qwen/Qwen2.5-14B-Instruct") + assert estimate_vram(awq, None, context_length=4096) < estimate_vram( + fp16, None, context_length=4096 + ) + + +def test_infer_mxfp4(): + model = _make_model("openai/gpt-oss-20b-MXFP4") + assert infer_non_gguf_quant_type(model.id) == "MXFP4" + assert effective_quant_type(model, None) == "MXFP4" + + +def test_infer_nvfp4(): + model = _make_model("nvidia/Llama-3.1-8B-Instruct-NVFP4") + assert infer_non_gguf_quant_type(model.id) == "NVFP4" + assert effective_quant_type(model, None) == "NVFP4" + + +def test_fp4_patterns_do_not_false_match_plain_ids(): + # A bare id with no fp4 token must not be mislabeled as a microscaling float. + plain = _make_model("meta-llama/Llama-3.1-8B-Instruct") + assert infer_non_gguf_quant_type(plain.id) == "FP16" + + +def test_estimate_weight_bytes_for_fp4_formats(): + mxfp4 = _make_model("openai/gpt-oss-20b-MXFP4", params=20_000_000_000) + nvfp4 = _make_model("nvidia/model-NVFP4", params=20_000_000_000) + assert estimate_weight_bytes(mxfp4, None) == int(20_000_000_000 * 0.53125) + assert estimate_weight_bytes(nvfp4, None) == int(20_000_000_000 * 0.5625) + + +def test_fp4_vram_is_lower_than_fp16_fallback(): + mxfp4 = _make_model("openai/gpt-oss-20b-MXFP4") + fp16 = _make_model("openai/gpt-oss-20b") + assert estimate_vram(mxfp4, None, context_length=4096) < estimate_vram( + fp16, None, context_length=4096 + ) + + +def test_extract_quant_type_parses_fp4_gguf_filenames(): + from whichllm.models.fetcher import _extract_quant_type + + assert _extract_quant_type("gpt-oss-20b-MXFP4.gguf") == "MXFP4" + assert _extract_quant_type("model.NVFP4.gguf") == "NVFP4" + + +def test_extract_quant_type_canonicalizes_full_precision_aliases(): + # llama.cpp publishes full-precision GGUFs as *-FP16/*-FP32; the byte and + # penalty tables key these as F16/F32, so the extractor must canonicalize. + from whichllm.models.fetcher import _extract_quant_type + + assert _extract_quant_type("Meta-Llama-3-8B-FP16.gguf") == "F16" + assert _extract_quant_type("model.FP32.gguf") == "F32" + # Canonical spellings still pass through unchanged. + assert _extract_quant_type("model-F16.gguf") == "F16" + assert _extract_quant_type("model.BF16.gguf") == "BF16" + + +def test_extract_quant_type_recognizes_ternary_gguf(): + # BitNet-class ternary GGUFs (TQ1_0/TQ2_0) are fully priced in the tables + # but were previously extracted as "unknown" and dropped at fetch. + from whichllm.models.fetcher import _extract_quant_type + + assert _extract_quant_type("BitNet-b1.58-2B-4T-TQ1_0.gguf") == "TQ1_0" + assert _extract_quant_type("model.TQ2_0.gguf") == "TQ2_0" + + +def test_estimate_gguf_size_does_not_undersize_fp16(): + # An FP16 GGUF must size at full precision (2.0 bytes/weight), not collapse + # to the Q4_K_M 0.5625 default that an unrecognized token falls back to. + from whichllm.models.fetcher import _estimate_gguf_size, _extract_quant_type + + params = 7_000_000_000 + quant = _extract_quant_type("model-FP16.gguf") + size = _estimate_gguf_size(params, quant) + assert size == params * 2 # 14 GB, not the ~3.94 GB default + assert size == _estimate_gguf_size(params, "F16") + + +def test_extract_quant_type_keys_resolve_in_byte_table(): + # Drift guard: every quant the extractor surfaces from a real GGUF filename + # must resolve in QUANT_BYTES_PER_WEIGHT, otherwise it is silently mis-sized + # by the default or dropped at fetch. Keeps the extractor and tables aligned. + from whichllm.data.quantization import QUANT_BYTES_PER_WEIGHT + from whichllm.models.fetcher import _extract_quant_type + + filenames = [ + "model-Q4_K_M.gguf", + "model-Q8_0.gguf", + "model-Q6_K.gguf", + "model-IQ4_NL.gguf", + "model-IQ3_XXS.gguf", + "model-TQ1_0.gguf", + "model-TQ2_0.gguf", + "model-F16.gguf", + "model-FP16.gguf", + "model-BF16.gguf", + "model-F32.gguf", + "model-FP32.gguf", + "model-MXFP4.gguf", + "model-NVFP4.gguf", + ] + for fname in filenames: + quant = _extract_quant_type(fname) + assert quant != "unknown", f"{fname} not recognized by extractor" + assert quant in QUANT_BYTES_PER_WEIGHT, ( + f"{fname} -> {quant!r} missing from QUANT_BYTES_PER_WEIGHT" + ) diff --git a/tests/test_r3_regressions.py b/tests/test_r3_regressions.py new file mode 100644 index 0000000..472ddf0 --- /dev/null +++ b/tests/test_r3_regressions.py @@ -0,0 +1,729 @@ +"""Regression tests for the Round 3 fixes (whichllm 0.5.1). + +Each test reproduces a specific bug class that the 0.5.1-dev ranker +exhibited and that was found by stress-testing previously unexercised +axes (Apple --gpu, family inheritance order, grouper base selection, +reasoning-model surfacing): + +- R3-1: ``--gpu "M1"`` fuzzy-matched the 1997 ATI Rage Mobility-M1 + (vendor=amd); ``--gpu "M3 Max"`` fell through to vendor=nvidia. +- R3-2: a 6.6B "imatrix-aligned" / MTP-head fork inherited its 158B + base's benchmark via family/base_model lookup. +- R3-3: a high-download downstream fork overrode the official upstream + model as the family base, corrupting ``family_id``. +- R3-4/R3-5: reasoning lines (QwQ-32B, DeepSeek-R1-Distill) had no + curated benchmark entry and never surfaced in the ranking. +""" + +from __future__ import annotations + +from whichllm.engine.ranker import rank_models +from whichllm.hardware.gpu_simulator import create_synthetic_gpu +from whichllm.hardware.types import GPUInfo, HardwareInfo +from whichllm.models.benchmark import _params_compatible +from whichllm.models.benchmark_sources.aa_index import AA_INDEX_FALLBACK_2026_06_29 +from whichllm.models.benchmark_sources.livebench import LIVEBENCH_RAW_DATA +from whichllm.models.benchmark_sources.vision import VISION_FALLBACK_2026_05 +from whichllm.models.grouper import group_models +from whichllm.models.types import GGUFVariant, ModelInfo + + +def _hw( + vram_gb: int = 24, + bandwidth_gbps: float = 1000.0, + vendor: str = "nvidia", + os_name: str = "linux", + with_gpu: bool = True, +) -> HardwareInfo: + gpus = [] + if with_gpu: + gpus = [ + GPUInfo( + name="Test GPU", + vendor=vendor, + vram_bytes=vram_gb * 1024**3, + compute_capability=(8, 9) if vendor == "nvidia" else None, + memory_bandwidth_gbps=bandwidth_gbps, + ) + ] + return HardwareInfo( + gpus=gpus, + cpu_name="Test CPU", + cpu_cores=8, + has_avx2=True, + ram_bytes=64 * 1024**3, + disk_free_bytes=500 * 1024**3, + os=os_name, + ) + + +def _gguf(quant: str, size_gb: float) -> GGUFVariant: + return GGUFVariant( + filename=f"model-{quant}.gguf", + quant_type=quant, + file_size_bytes=int(size_gb * 1e9), + ) + + +# ------------------------------------------------------------------ R3-1 + + +class TestAppleSiliconSimulator: + """``--gpu`` must recognize Apple Silicon instead of fuzzy-matching + discrete-GPU database entries.""" + + def test_m1_default_is_apple_not_ati_rage_mobility(self): + gpu = create_synthetic_gpu("M1") + assert gpu.vendor == "apple", ( + f"M1 must be Apple, got vendor={gpu.vendor!r} " + f"name={gpu.name!r} (regression: fuzzy-matched ATI Rage " + "Mobility-M1)" + ) + assert "rage" not in gpu.name.lower() + # Default unified memory for the base M1 is 8 GB. + assert gpu.vram_bytes == 8 * 1024**3 + assert gpu.memory_bandwidth_gbps == 68.25 + + def test_m3_max_vram_override_apple_400gbps(self): + gpu = create_synthetic_gpu("M3 Max", vram_override_gb=64) + assert gpu.vendor == "apple", ( + f"M3 Max must be Apple, got vendor={gpu.vendor!r} " + "(regression: fell through to nvidia default)" + ) + assert gpu.vram_bytes == 64 * 1024**3 + assert gpu.memory_bandwidth_gbps == 400.0 + + def test_m2_ultra_192gb_apple_800gbps(self): + gpu = create_synthetic_gpu("M2 Ultra", vram_override_gb=192) + assert gpu.vendor == "apple" + assert gpu.vram_bytes == 192 * 1024**3 + assert gpu.memory_bandwidth_gbps == 800.0 + + def test_apple_chip_compact_form_is_recognized(self): + # Users type "M2Max" / "m2 max" / "M2 MAX" interchangeably. + for name in ("M2Max", "m2 max", "M2 MAX"): + gpu = create_synthetic_gpu(name, vram_override_gb=32) + assert gpu.vendor == "apple", f"{name!r} not recognized as Apple" + assert gpu.memory_bandwidth_gbps == 400.0 + + def test_longest_match_wins_m2_ultra_not_m2(self): + # "M2 Ultra" must not be swallowed by the "M2" entry (100 GB/s). + gpu = create_synthetic_gpu("M2 Ultra", vram_override_gb=128) + assert gpu.memory_bandwidth_gbps == 800.0 + assert gpu.memory_bandwidth_gbps != 100.0 + + +# ------------------------------------------------------------------ R3-2 + + +class TestFamilySizeInheritance: + """A small fork must not inherit a much larger base model's + benchmark score.""" + + def test_params_compatible_rejects_25x_mismatch(self): + # 6.6B vs an id that encodes 158B → ratio 0.04, must reject. + assert _params_compatible(6.6, "org/Some-Model-158B") is False + + def test_params_compatible_accepts_same_size_quant(self): + # A Q4 repack of an 8B model is still 8B → must inherit. + assert _params_compatible(7.8, "org/Llama-3-8B-GGUF") is True + + def test_params_compatible_permissive_when_no_actual_size(self): + # No actual size → cannot judge, must not block (avoids + # false-negatives that would erase legitimate inheritance). + assert _params_compatible(None, "org/Model-70B") is True + + def test_params_compatible_permissive_when_ref_has_no_size(self): + # Base id without a parseable size (e.g. DeepSeek-V4-Flash) → + # the function alone cannot guard; the ranker's + # family_dominant_params check is the backstop (next test). + assert _params_compatible(6.6, "deepseek-ai/DeepSeek-V4-Flash") is True + + def test_ranker_drops_tiny_fork_inheriting_huge_base(self): + """The real bug: jedisct1/DeepSeek-V4-Flash-imatrix-aligned + (6.6B) shared family_id with the 158B base and inherited its + leaderboard score, landing in the CPU-only top 5.""" + base = ModelInfo( + id="org/DeepSeek-Vx-Flash", + family_id="deepseek-vx-flash", + name="DeepSeek-Vx-Flash", + parameter_count=158_000_000_000, + downloads=1_000_000, + likes=1000, + gguf_variants=[], # safetensors-only official + ) + tiny_fork = ModelInfo( + id="fork/DeepSeek-Vx-Flash-mtp-aligned", + family_id="deepseek-vx-flash", + name="DeepSeek-Vx-Flash-mtp-aligned", + parameter_count=6_600_000_000, + downloads=0, + likes=0, + base_model="org/DeepSeek-Vx-Flash", + gguf_variants=[_gguf("Q8_0", 7.0)], + ) + # External leaderboard only knows the 158B base. + scores = {"org/DeepSeek-Vx-Flash": 92.0} + # Tiny VRAM: the 158B base cannot run; only the 6.6B fork fits. + hw = _hw(vram_gb=12) + ranked = rank_models( + [base, tiny_fork], + hw, + benchmark_scores=scores, + require_direct_top=False, + ) + # With the guard the fork's family/base inheritance is rejected + # entirely (status=none, score≈20). Without it the fork inherits + # the 158B base at confidence 0.6 (status=estimated, score≈59). + # The thresholds below sit firmly between those two regimes so + # the test goes red the instant the guard is removed. + tiny_res = next((r for r in ranked if r.model.id == tiny_fork.id), None) + assert tiny_res is not None, "tiny fork should still be listed" + assert tiny_res.benchmark_status == "none", ( + "tiny fork inherited a benchmark from its 158B base " + f"(status={tiny_res.benchmark_status!r}); the " + "family_dominant_params guard is not working" + ) + assert tiny_res.quality_score < 30.0, ( + "tiny fork's score reflects inherited 158B evidence " + f"(got {tiny_res.quality_score:.1f}, expected <30)" + ) + + +# ------------------------------------------------------------------ R3-3 + + +class TestGrouperUpstreamBase: + """Family base selection must follow the base_model graph, not raw + download counts.""" + + def test_official_upstream_wins_over_more_downloaded_fork(self): + official = ModelInfo( + id="Qwen/Qwen3-4B-Thinking-2507", + family_id="", + name="Qwen3-4B-Thinking-2507", + parameter_count=4_000_000_000, + downloads=494_000, + base_model=None, + ) + popular_fork = ModelInfo( + id="prefeitura-rio/Rio-3.0-Open-Mini", + family_id="", + name="Rio-3.0-Open-Mini", + parameter_count=4_000_000_000, + downloads=1_300_000, # more than the official base + base_model="Qwen/Qwen3-4B-Thinking-2507", + ) + gguf_fork = ModelInfo( + id="MaziyarPanahi/Qwen3-4B-Thinking-2507-GGUF", + family_id="", + name="Qwen3-4B-Thinking-2507-GGUF", + parameter_count=4_000_000_000, + downloads=26_000, + base_model="Qwen/Qwen3-4B-Thinking-2507", + gguf_variants=[_gguf("Q4_K_M", 2.4)], + ) + families = group_models([popular_fork, official, gguf_fork]) + # All three collapse into one family. + assert len(families) == 1 + fam = families[0] + assert fam.base_model.id == "Qwen/Qwen3-4B-Thinking-2507", ( + f"family base is {fam.base_model.id!r}; the popular fork " + "overrode the official upstream (regression R3-3)" + ) + # Every member must carry the upstream-derived family_id. + all_ids = {fam.base_model.id} | {v.id for v in fam.variants} + assert "Qwen/Qwen3-4B-Thinking-2507" in all_ids + for m in [official, popular_fork, gguf_fork]: + assert m.family_id == official.family_id + assert "rio" not in m.family_id + + def test_falls_back_to_downloads_without_upstream_reference(self): + # No member references another's base_model → keep the prior + # "most downloads, no GGUF" behaviour. + a = ModelInfo( + id="orgA/Model-7B", + family_id="", + name="Model-7B", + parameter_count=7_000_000_000, + downloads=500, + ) + b = ModelInfo( + id="orgB/Model-7B", + family_id="", + name="Model-7B", + parameter_count=7_000_000_000, + downloads=5000, + ) + families = group_models([a, b]) + assert len(families) == 1 + assert families[0].base_model.id == "orgB/Model-7B" + + +# --------------------------------------------------------------- R3-4/5 + + +class TestReasoningSurface: + """Reasoning/thinking lines must have curated benchmark anchors and + surface in the ranking.""" + + def test_qwq32b_has_curated_benchmarks(self): + assert "Qwen/QwQ-32B" in LIVEBENCH_RAW_DATA + assert "Qwen/QwQ-32B" in AA_INDEX_FALLBACK_2026_06_29 + + def test_r1_distill_family_has_curated_benchmarks(self): + for mid in ( + "deepseek-ai/DeepSeek-R1-Distill-Qwen-32B", + "deepseek-ai/DeepSeek-R1-Distill-Qwen-14B", + "deepseek-ai/DeepSeek-R1-Distill-Llama-8B", + ): + assert mid in LIVEBENCH_RAW_DATA, f"{mid} missing in LB" + assert mid in AA_INDEX_FALLBACK_2026_06_29, f"{mid} missing in AA" + + def test_qwq32b_surfaces_with_curated_score(self): + qwq = ModelInfo( + id="Qwen/QwQ-32B", + family_id="qwq-32b", + name="QwQ-32B", + parameter_count=32_800_000_000, + downloads=64_000, + gguf_variants=[_gguf("Q4_K_M", 20.0)], + ) + filler = ModelInfo( + id="org/Generic-7B", + family_id="generic-7b", + name="Generic-7B", + parameter_count=7_000_000_000, + downloads=10, + gguf_variants=[_gguf("Q4_K_M", 4.5)], + ) + scores = {"Qwen/QwQ-32B": 57.0} + hw = _hw(vram_gb=48) + ranked = rank_models( + [qwq, filler], hw, benchmark_scores=scores, require_direct_top=False + ) + ids = [r.model.id for r in ranked] + assert "Qwen/QwQ-32B" in ids, "QwQ-32B did not surface in ranking" + qwq_res = next(r for r in ranked if r.model.id == "Qwen/QwQ-32B") + assert qwq_res.benchmark_status == "direct" + assert qwq_res.quality_score > 0 + + +# ----------------------------------------------------------------- R3-6 + + +class TestVisionGenerationOrder: + """``--profile vision`` had no VLM benchmark source, so only a + two-generations-old Qwen2-VL-7B had a direct hit and outranked the + current Qwen3-VL-32B even on an 80 GB GPU.""" + + def test_curated_vision_scores_respect_generation(self): + v = VISION_FALLBACK_2026_05 + # The whole point: newest generation must outscore the oldest. + assert ( + v["Qwen/Qwen3-VL-32B-Instruct"] + > v["Qwen/Qwen2.5-VL-32B-Instruct"] + > v["Qwen/Qwen2-VL-7B-Instruct"] + ) + # Two-generations-old 7B must sit in the low band so it cannot + # win a vision ranking by virtue of being the only direct hit. + assert v["Qwen/Qwen2-VL-7B-Instruct"] <= 35.0 + + def test_qwen3_vl_outranks_legacy_qwen2_vl_on_vision_profile(self): + new_vlm = ModelInfo( + id="Qwen/Qwen3-VL-32B-Instruct", + family_id="qwen3-vl-32b", + name="Qwen3-VL-32B-Instruct", + parameter_count=33_400_000_000, + downloads=1_500_000, + gguf_variants=[_gguf("Q4_K_M", 20.0)], + ) + legacy_vlm = ModelInfo( + id="Qwen/Qwen2-VL-7B-Instruct", + family_id="qwen2-vl-7b", + name="Qwen2-VL-7B-Instruct", + parameter_count=8_300_000_000, + downloads=2_000_000, # more downloads — must NOT win + gguf_variants=[_gguf("Q4_K_M", 5.0)], + ) + # Feed the curated vision scores exactly as the merge would. + scores = { + "Qwen/Qwen3-VL-32B-Instruct": VISION_FALLBACK_2026_05[ + "Qwen/Qwen3-VL-32B-Instruct" + ], + "Qwen/Qwen2-VL-7B-Instruct": VISION_FALLBACK_2026_05[ + "Qwen/Qwen2-VL-7B-Instruct" + ], + } + hw = _hw(vram_gb=80) + ranked = rank_models( + [legacy_vlm, new_vlm], + hw, + benchmark_scores=scores, + task_profile="vision", + require_direct_top=False, + ) + assert ranked, "no vision models ranked" + assert ranked[0].model.id == "Qwen/Qwen3-VL-32B-Instruct", ( + "legacy Qwen2-VL-7B outranked the current Qwen3-VL-32B on " + f"the vision profile (got {ranked[0].model.id})" + ) + + +# ----------------------------------------------------------------- R3-7 + + +class TestApplePartialOffloadPenalty: + """Partial-offload penalty must respect memory architecture: Apple + unified memory has no PCIe cliff, so the discrete 0.45x penalty made + DeepSeek-R1-class models on M2/M3 Ultra report ~1.7 t/s vs a + real-world 4-15.""" + + def _model_variant(self): + m = ModelInfo( + id="deepseek-ai/DeepSeek-R1-Distill-Qwen-32B", + family_id="deepseek-r1-distill-qwen-32b", + name="DeepSeek-R1-Distill-Qwen-32B", + parameter_count=32_800_000_000, + ) + v = _gguf("Q4_K_M", 20.0) + return m, v + + def test_apple_partial_offload_keeps_most_of_full_speed(self): + from whichllm.engine.performance import estimate_tok_per_sec + + m, v = self._model_variant() + apple = GPUInfo( + name="M2 Ultra", + vendor="apple", + vram_bytes=192 * 1024**3, + memory_bandwidth_gbps=800.0, + ) + full = estimate_tok_per_sec(m, v, apple, "full_gpu") + partial = estimate_tok_per_sec(m, v, apple, "partial_offload") + assert full > 0 + ratio = partial / full + # 0.85 with the fix; 0.45 if the vendor branch is removed. The + # 0.7 threshold sits between the two regimes. + assert ratio > 0.7, ( + f"Apple partial-offload ratio {ratio:.2f} — the discrete " + "0.45x PCIe penalty is being wrongly applied to unified memory" + ) + + def test_discrete_partial_offload_still_takes_pcie_penalty(self): + from whichllm.engine.performance import estimate_tok_per_sec + + m, v = self._model_variant() + nvidia = GPUInfo( + name="RTX 4090", + vendor="nvidia", + vram_bytes=24 * 1024**3, + compute_capability=(8, 9), + memory_bandwidth_gbps=1008.0, + ) + full = estimate_tok_per_sec(m, v, nvidia, "full_gpu") + partial = estimate_tok_per_sec(m, v, nvidia, "partial_offload") + assert full > 0 + ratio = partial / full + # Discrete GPUs must keep the steep PCIe penalty (~0.45). + assert ratio < 0.6, ( + f"discrete partial-offload ratio {ratio:.2f} — the PCIe " + "penalty is too weak; offload should hurt on NVIDIA/AMD" + ) + + def test_apple_partial_faster_than_discrete_partial_same_bandwidth(self): + from whichllm.engine.performance import estimate_tok_per_sec + + m, v = self._model_variant() + # Same nominal bandwidth so the only difference is the + # architecture-aware offload penalty. + apple = GPUInfo( + name="Apple", + vendor="apple", + vram_bytes=192 * 1024**3, + memory_bandwidth_gbps=900.0, + ) + nvidia = GPUInfo( + name="NVIDIA", + vendor="nvidia", + vram_bytes=24 * 1024**3, + compute_capability=(8, 9), + memory_bandwidth_gbps=900.0, + ) + a = estimate_tok_per_sec(m, v, apple, "partial_offload") + n = estimate_tok_per_sec(m, v, nvidia, "partial_offload") + assert a > n, ( + "Apple unified-memory partial offload should beat discrete " + f"PCIe-bound partial offload at equal bandwidth ({a:.1f} vs " + f"{n:.1f})" + ) + + +# ----------------------------------------------------------------- R3-8 + + +class TestMoESpeedEstimation: + """MoE speed estimation should use active params without letting + high-bandwidth GPUs turn sparse models into unrealistic outliers.""" + + def test_qwen3_next_strix_halo_matches_reported_generation_speed(self): + from whichllm.engine.performance import estimate_tok_per_sec + + model = ModelInfo( + id="Qwen/Qwen3-Next-80B-A3B-Instruct", + family_id="qwen3-next-80b-a3b", + name="Qwen3-Next-80B-A3B-Instruct", + parameter_count=79_670_000_000, + parameter_count_active=3_000_000_000, + is_moe=True, + ) + variant = GGUFVariant( + filename="qwen3next-q4_k_m.gguf", + quant_type="Q4_K_M", + file_size_bytes=int(45.17 * 1024**3), + ) + strix_halo = GPUInfo( + name="STRXLGEN", + vendor="amd", + vram_bytes=0, + memory_bandwidth_gbps=256.0, + shared_memory=True, + ) + + speed = estimate_tok_per_sec(model, variant, strix_halo, "full_gpu") + + assert 40.0 <= speed <= 50.0 + + def test_unknown_ultra_sparse_moe_uses_active_params_on_strix_halo(self): + from whichllm.engine.performance import estimate_tok_per_sec + + model = ModelInfo( + id="unknown/Experimental-80B-A3B", + family_id="experimental-80b-a3b", + name="Experimental-80B-A3B", + parameter_count=79_670_000_000, + parameter_count_active=3_000_000_000, + is_moe=True, + ) + variant = GGUFVariant( + filename="experimental-q4_k_m.gguf", + quant_type="Q4_K_M", + file_size_bytes=int(45.17 * 1024**3), + ) + strix_halo = GPUInfo( + name="STRXLGEN", + vendor="amd", + vram_bytes=0, + memory_bandwidth_gbps=256.0, + shared_memory=True, + ) + + speed = estimate_tok_per_sec(model, variant, strix_halo, "full_gpu") + + assert 40.0 <= speed <= 50.0 + + def test_qwen3_30b_a3b_strix_halo_no_longer_uses_legacy_floor(self): + from whichllm.engine.performance import estimate_tok_per_sec + + model = ModelInfo( + id="Qwen/Qwen3-30B-A3B", + family_id="qwen3-30b-a3b", + name="Qwen3-30B-A3B", + parameter_count=30_000_000_000, + parameter_count_active=3_000_000_000, + is_moe=True, + ) + variant = GGUFVariant( + filename="qwen3-30b-q4_k_m.gguf", + quant_type="Q4_K_M", + file_size_bytes=int(17.1 * 1024**3), + ) + strix_halo = GPUInfo( + name="STRXLGEN", + vendor="amd", + vram_bytes=0, + memory_bandwidth_gbps=256.0, + shared_memory=True, + ) + + speed = estimate_tok_per_sec(model, variant, strix_halo, "full_gpu") + + assert 50.0 <= speed <= 70.0 + + def test_high_bandwidth_gpu_keeps_moe_kernel_floor(self): + from whichllm.engine.performance import estimate_tok_per_sec + + model = ModelInfo( + id="Qwen/Qwen3-30B-A3B", + family_id="qwen3-30b-a3b", + name="Qwen3-30B-A3B", + parameter_count=30_000_000_000, + parameter_count_active=3_000_000_000, + is_moe=True, + ) + variant = _gguf("Q5_K_M", 20.6) + rtx_4090 = GPUInfo( + name="RTX 4090", + vendor="nvidia", + vram_bytes=24 * 1024**3, + compute_capability=(8, 9), + memory_bandwidth_gbps=1008.0, + ) + + speed = estimate_tok_per_sec(model, variant, rtx_4090, "full_gpu") + + assert 100.0 <= speed <= 150.0 + + def test_deepseek_v4_flash_synthetic_q4_does_not_fit_strix_halo_96gb(self): + deepseek = ModelInfo( + id="deepseek-ai/DeepSeek-V4-Flash", + family_id="deepseek-v4-flash", + name="DeepSeek-V4-Flash", + parameter_count=284_000_000_000, + parameter_count_active=13_000_000_000, + is_moe=True, + downloads=1_000_000, + gguf_variants=[], + ) + qwen_dense = ModelInfo( + id="Qwen/Qwen3.6-27B", + family_id="qwen3.6-27b", + name="Qwen3.6-27B", + parameter_count=27_000_000_000, + downloads=100_000, + gguf_variants=[], + ) + hardware = HardwareInfo( + gpus=[ + GPUInfo( + name="Strix Halo", + vendor="amd", + vram_bytes=96 * 1024**3, + memory_bandwidth_gbps=256.0, + shared_memory=True, + ) + ], + cpu_name="Ryzen AI MAX+ 395", + cpu_cores=16, + ram_bytes=128 * 1024**3, + disk_free_bytes=500 * 1024**3, + os="linux", + ) + + results = rank_models( + [deepseek, qwen_dense], + hardware, + top_n=5, + quant_filter="Q4_K_M", + benchmark_scores={ + "deepseek-ai/DeepSeek-V4-Flash": 87.0, + "Qwen/Qwen3.6-27B": 84.0, + }, + ) + + ids = [r.model.id for r in results] + assert "deepseek-ai/DeepSeek-V4-Flash" not in ids + assert "Qwen/Qwen3.6-27B" in ids + + +class TestSpeedUncertainty: + def test_strix_halo_moe_speed_is_medium_confidence_with_range(self): + from whichllm.engine.performance import estimate_speed_uncertainty + + model = ModelInfo( + id="unknown/Experimental-80B-A3B", + family_id="experimental-80b-a3b", + name="Experimental-80B-A3B", + parameter_count=79_670_000_000, + parameter_count_active=3_000_000_000, + is_moe=True, + ) + variant = GGUFVariant( + filename="experimental-q4_k_m.gguf", + quant_type="Q4_K_M", + file_size_bytes=int(45.17 * 1024**3), + ) + strix_halo = GPUInfo( + name="Strix Halo", + vendor="amd", + vram_bytes=96 * 1024**3, + memory_bandwidth_gbps=256.0, + shared_memory=True, + ) + + confidence, speed_range, notes = estimate_speed_uncertainty( + model, variant, strix_halo, "full_gpu", 48.0 + ) + + assert confidence == "medium" + assert speed_range == (28.8, 76.8) + assert any("shared-memory APU" in note for note in notes) + + def test_apple_silicon_moe_speed_is_low_confidence(self): + from whichllm.engine.performance import estimate_speed_uncertainty + + model = ModelInfo( + id="google/gemma-4-26B-A4B-it", + family_id="gemma-4-26b-a4b-it", + name="gemma-4-26B-A4B-it", + parameter_count=26_000_000_000, + parameter_count_active=3_800_000_000, + is_moe=True, + ) + variant = _gguf("Q4_K_M", 15.0) + apple = GPUInfo( + name="M3 Max", + vendor="apple", + vram_bytes=96 * 1024**3, + memory_bandwidth_gbps=400.0, + shared_memory=True, + ) + + confidence, speed_range, notes = estimate_speed_uncertainty( + model, variant, apple, "full_gpu", 30.0 + ) + + assert confidence == "low" + assert speed_range == (10.5, 60.0) + assert any("Metal/MLX" in note for note in notes) + + def test_synthetic_gguf_rank_result_exposes_speed_uncertainty(self): + model = ModelInfo( + id="Qwen/Qwen3-30B-A3B", + family_id="qwen3-30b-a3b", + name="Qwen3-30B-A3B", + parameter_count=30_000_000_000, + parameter_count_active=3_000_000_000, + is_moe=True, + downloads=1_000_000, + ) + hardware = HardwareInfo( + gpus=[ + GPUInfo( + name="Strix Halo", + vendor="amd", + vram_bytes=96 * 1024**3, + memory_bandwidth_gbps=256.0, + shared_memory=True, + ) + ], + cpu_name="Ryzen AI MAX+ 395", + cpu_cores=16, + ram_bytes=128 * 1024**3, + disk_free_bytes=500 * 1024**3, + os="linux", + ) + + result = rank_models( + [model], + hardware, + top_n=1, + quant_filter="Q4_K_M", + benchmark_scores={"Qwen/Qwen3-30B-A3B": 80.0}, + )[0] + + assert result.speed_confidence == "medium" + assert result.speed_range_tok_per_sec is not None + assert result.speed_range_tok_per_sec[0] < result.estimated_tok_per_sec + assert result.speed_range_tok_per_sec[1] > result.estimated_tok_per_sec + assert any("synthetic GGUF" in note for note in result.speed_notes) diff --git a/tests/test_ranker.py b/tests/test_ranker.py new file mode 100644 index 0000000..3480cef --- /dev/null +++ b/tests/test_ranker.py @@ -0,0 +1,1155 @@ +"""Tests for ranking behavior.""" + +from whichllm.engine.quantization import effective_quant_type +from whichllm.engine.ranker import _partial_offload_quality_factor, rank_models +from whichllm.hardware.types import GPUInfo, HardwareInfo +from whichllm.models.types import GGUFVariant, ModelInfo + + +def _make_hardware( + vram_gb: int = 24, + bandwidth_gbps: float = 80.0, + vendor: str = "nvidia", + os_name: str = "linux", + with_gpu: bool = True, +) -> HardwareInfo: + gpus = [] + if with_gpu: + gpus = [ + GPUInfo( + name="Test GPU", + vendor=vendor, + vram_bytes=vram_gb * 1024**3, + compute_capability=(8, 9) if vendor == "nvidia" else None, + memory_bandwidth_gbps=bandwidth_gbps, + ), + ] + return HardwareInfo( + gpus=gpus, + cpu_name="Test CPU", + cpu_cores=8, + has_avx2=True, + ram_bytes=64 * 1024**3, + disk_free_bytes=500 * 1024**3, + os=os_name, + ) + + +def test_ranker_picks_highest_scoring_variant(): + # On a fast GPU (≥800 GB/s) both quants run well above the comfort + # threshold, so the F16 quality bonus dominates and F16 wins. On a slow + # GPU the speed gap flips the choice — that's exercised separately. + model = ModelInfo( + id="org/Test-8B-GGUF", + family_id="org/Test-8B-GGUF", + name="Test-8B-GGUF", + parameter_count=8_000_000_000, + downloads=1000, + likes=100, + gguf_variants=[ + GGUFVariant( + filename="test-Q4_K_M.gguf", + quant_type="Q4_K_M", + file_size_bytes=4_500_000_000, + ), + GGUFVariant( + filename="test-F16.gguf", + quant_type="F16", + file_size_bytes=5_000_000_000, + ), + ], + ) + hw = _make_hardware(bandwidth_gbps=900.0) + results = rank_models( + [model], + hw, + top_n=1, + benchmark_scores={"org/Test-8B-GGUF": 70.0}, + ) + assert results + assert results[0].gguf_variant is not None + assert results[0].gguf_variant.quant_type == "F16" + + +def test_quant_filter_applies_to_non_gguf_models(): + model = ModelInfo( + id="Qwen/Qwen2.5-14B-Instruct-AWQ", + family_id="qwen2.5-14b", + name="Qwen2.5-14B-Instruct-AWQ", + parameter_count=14_000_000_000, + downloads=1000, + likes=100, + ) + hw = _make_hardware(vram_gb=24, bandwidth_gbps=300.0) + + awq_only = rank_models([model], hw, top_n=5, quant_filter="AWQ") + q4_only = rank_models([model], hw, top_n=5, quant_filter="Q4_K_M") + + assert len(awq_only) == 1 + assert q4_only == [] + + +def test_quant_filter_matches_mxfp4_non_gguf_model(): + model = ModelInfo( + id="openai/gpt-oss-20b-MXFP4", + family_id="gpt-oss-20b", + name="gpt-oss-20b-MXFP4", + parameter_count=20_000_000_000, + downloads=1000, + likes=100, + ) + # Linux + NVIDIA: non-GGUF formats are runnable, so the filter resolves. + hw = _make_hardware(vram_gb=24, bandwidth_gbps=900.0) + + mxfp4_only = rank_models([model], hw, top_n=5, quant_filter="MXFP4") + nvfp4_only = rank_models([model], hw, top_n=5, quant_filter="NVFP4") + + assert len(mxfp4_only) == 1 + # The label surfaced in the output table (display.py uses the same call). + assert ( + effective_quant_type(mxfp4_only[0].model, mxfp4_only[0].gguf_variant) == "MXFP4" + ) + assert nvfp4_only == [] + + +def test_darwin_backend_filters_out_fp4_non_gguf_models(): + mxfp4_model = ModelInfo( + id="openai/gpt-oss-20b-MXFP4", + family_id="gpt-oss-20b", + name="gpt-oss-20b-MXFP4", + parameter_count=20_000_000_000, + downloads=1000, + likes=100, + ) + hw = _make_hardware( + vram_gb=64, bandwidth_gbps=400.0, vendor="apple", os_name="darwin" + ) + results = rank_models([mxfp4_model], hw, top_n=10) + assert results == [] + + +def test_darwin_backend_filters_out_non_gguf_models(): + awq_model = ModelInfo( + id="Qwen/Qwen3-8B-AWQ", + family_id="qwen3-8b-awq", + name="Qwen3-8B-AWQ", + parameter_count=8_000_000_000, + downloads=1000, + likes=100, + ) + gguf_model = ModelInfo( + id="Qwen/Qwen3-8B-GGUF", + family_id="qwen3-8b-gguf", + name="Qwen3-8B-GGUF", + parameter_count=8_000_000_000, + downloads=1000, + likes=100, + gguf_variants=[ + GGUFVariant( + filename="a-Q4_K_M.gguf", + quant_type="Q4_K_M", + file_size_bytes=4_000_000_000, + ), + ], + ) + hw = _make_hardware( + vram_gb=16, bandwidth_gbps=200.0, vendor="apple", os_name="darwin" + ) + results = rank_models([awq_model, gguf_model], hw, top_n=10) + assert len(results) == 1 + assert results[0].model.id == "Qwen/Qwen3-8B-GGUF" + + +def test_cpu_only_backend_filters_out_non_gguf_models(): + awq_model = ModelInfo( + id="Qwen/Qwen3-8B-AWQ", + family_id="qwen3-8b-awq", + name="Qwen3-8B-AWQ", + parameter_count=8_000_000_000, + downloads=1000, + likes=100, + ) + gguf_model = ModelInfo( + id="Qwen/Qwen3-8B-GGUF", + family_id="qwen3-8b-gguf", + name="Qwen3-8B-GGUF", + parameter_count=8_000_000_000, + downloads=1000, + likes=100, + gguf_variants=[ + GGUFVariant( + filename="a-Q4_K_M.gguf", + quant_type="Q4_K_M", + file_size_bytes=4_000_000_000, + ), + ], + ) + hw = _make_hardware(with_gpu=False, os_name="linux") + results = rank_models([awq_model, gguf_model], hw, top_n=10) + assert len(results) == 1 + assert results[0].model.id == "Qwen/Qwen3-8B-GGUF" + + +def _gguf_model(model_id: str, family_id: str, downloads: int) -> ModelInfo: + return ModelInfo( + id=model_id, + family_id=family_id, + name=model_id.split("/")[-1], + parameter_count=7_000_000_000, + downloads=downloads, + likes=downloads // 10, + gguf_variants=[ + GGUFVariant( + filename=f"{family_id}-Q4_K_M.gguf", + quant_type="Q4_K_M", + file_size_bytes=4_500_000_000, + ), + ], + ) + + +def test_rank_models_clamps_non_positive_top_n(): + # Several distinct families so the full ranking has multiple entries; only + # then is the slice hazard observable. + hw = _make_hardware(vram_gb=24, bandwidth_gbps=300.0) + models = [ + _gguf_model("org/Alpha-7B-GGUF", "alpha-7b", 1000), + _gguf_model("org/Beta-7B-GGUF", "beta-7b", 900), + _gguf_model("org/Gamma-7B-GGUF", "gamma-7b", 800), + ] + + full = rank_models(models, hw, top_n=10) + assert len(full) >= 2 # guard is only meaningful with several results + + # 0 and negative requests must yield nothing, never a slice-from-the-end + # subset: ``results[:-1]`` would otherwise return all-but-last. + assert rank_models(models, hw, top_n=0) == [] + assert rank_models(models, hw, top_n=-1) == [] + # Positive requests are unaffected. + assert len(rank_models(models, hw, top_n=2)) == 2 + + +def test_popularity_has_no_effect_with_direct_benchmark(): + model_low_pop = ModelInfo( + id="Qwen/test-8b-lowpop", + family_id="qwen-test-8b-lowpop", + name="test-8b-lowpop", + parameter_count=8_000_000_000, + downloads=100, + likes=5, + gguf_variants=[ + GGUFVariant( + filename="test-Q4_K_M.gguf", + quant_type="Q4_K_M", + file_size_bytes=4_500_000_000, + ), + ], + ) + model_high_pop = ModelInfo( + id="Qwen/test-8b-highpop", + family_id="qwen-test-8b-highpop", + name="test-8b-highpop", + parameter_count=8_000_000_000, + downloads=1_000_000, + likes=10_000, + gguf_variants=[ + GGUFVariant( + filename="test-Q4_K_M.gguf", + quant_type="Q4_K_M", + file_size_bytes=4_500_000_000, + ), + ], + ) + hw = _make_hardware() + results = rank_models( + [model_low_pop, model_high_pop], + hw, + top_n=2, + benchmark_scores={ + "Qwen/test-8b-lowpop": 70.0, + "Qwen/test-8b-highpop": 70.0, + }, + ) + assert len(results) == 2 + assert abs(results[0].quality_score - results[1].quality_score) < 1e-9 + + +def test_general_profile_excludes_specialized_models(): + general_model = ModelInfo( + id="Qwen/Qwen2.5-7B-Instruct", + family_id="qwen2.5-7b", + name="Qwen2.5-7B-Instruct", + parameter_count=7_000_000_000, + downloads=1000, + likes=100, + gguf_variants=[ + GGUFVariant( + filename="a-Q4_K_M.gguf", + quant_type="Q4_K_M", + file_size_bytes=4_000_000_000, + ), + ], + ) + coding_model = ModelInfo( + id="Qwen/Qwen2.5-Coder-7B-Instruct", + family_id="qwen2.5-coder-7b", + name="Qwen2.5-Coder-7B-Instruct", + parameter_count=7_000_000_000, + downloads=1000, + likes=100, + gguf_variants=[ + GGUFVariant( + filename="b-Q4_K_M.gguf", + quant_type="Q4_K_M", + file_size_bytes=4_000_000_000, + ), + ], + ) + hw = _make_hardware() + results = rank_models( + [general_model, coding_model], + hw, + top_n=10, + benchmark_scores={ + "Qwen/Qwen2.5-7B-Instruct": 70.0, + "Qwen/Qwen2.5-Coder-7B-Instruct": 75.0, + }, + task_profile="general", + ) + assert len(results) == 1 + assert "Coder" not in results[0].model.id + + +def test_require_direct_top_prioritizes_direct_benchmark(): + direct_model = ModelInfo( + id="Qwen/direct-7b", + family_id="qwen-direct-7b", + name="direct-7b", + parameter_count=7_000_000_000, + downloads=1000, + likes=100, + gguf_variants=[ + GGUFVariant( + filename="d-Q4_K_M.gguf", + quant_type="Q4_K_M", + file_size_bytes=4_000_000_000, + ), + ], + ) + estimated_model = ModelInfo( + id="Qwen/Qwen3-9B", + family_id="qwen3-9b", + name="Qwen3-9B", + parameter_count=9_000_000_000, + downloads=1000, + likes=100, + gguf_variants=[ + GGUFVariant( + filename="e-Q4_K_M.gguf", + quant_type="Q4_K_M", + file_size_bytes=5_000_000_000, + ), + ], + ) + hw = _make_hardware() + results = rank_models( + [direct_model, estimated_model], + hw, + top_n=10, + benchmark_scores={ + "Qwen/direct-7b": 65.0, + # Qwen3のラインスコアだけ与えてestimatedを作る + "Qwen/Qwen3-32B": 80.0, + }, + task_profile="any", + require_direct_top=True, + ) + assert len(results) == 2 + assert results[0].benchmark_status == "direct" + + +def test_min_params_filter_excludes_small_models(): + small = ModelInfo( + id="Qwen/Qwen2.5-3B-Instruct", + family_id="qwen2.5-3b", + name="Qwen2.5-3B-Instruct", + parameter_count=3_000_000_000, + downloads=1000, + likes=100, + gguf_variants=[ + GGUFVariant( + filename="s-Q4_K_M.gguf", + quant_type="Q4_K_M", + file_size_bytes=1_700_000_000, + ), + ], + ) + large = ModelInfo( + id="Qwen/Qwen2.5-7B-Instruct", + family_id="qwen2.5-7b", + name="Qwen2.5-7B-Instruct", + parameter_count=7_000_000_000, + downloads=1000, + likes=100, + gguf_variants=[ + GGUFVariant( + filename="l-Q4_K_M.gguf", + quant_type="Q4_K_M", + file_size_bytes=4_000_000_000, + ), + ], + ) + hw = _make_hardware() + results = rank_models( + [small, large], + hw, + top_n=10, + benchmark_scores={ + "Qwen/Qwen2.5-3B-Instruct": 90.0, + "Qwen/Qwen2.5-7B-Instruct": 70.0, + }, + task_profile="any", + min_params_b=7.0, + ) + assert len(results) == 1 + assert results[0].model.id == "Qwen/Qwen2.5-7B-Instruct" + + +def test_general_profile_prefers_full_gpu_when_direct_is_partial(): + # Direct-evidence model that won't fit on 8GB even after Q4_K_M synthesis + # (72B * 0.56 ≈ 40GB → partial_offload). + partial_direct = ModelInfo( + id="Qwen/Qwen2.5-72B-Instruct", + family_id="qwen2.5-72b", + name="Qwen2.5-72B-Instruct", + parameter_count=72_000_000_000, + downloads=1000, + likes=100, + ) + full_gpu_estimated = ModelInfo( + id="Qwen/Qwen3-9B-AWQ", + family_id="qwen3-9b", + name="Qwen3-9B-AWQ", + parameter_count=9_000_000_000, + downloads=1000, + likes=100, + ) + hw = _make_hardware(vram_gb=8, bandwidth_gbps=272.0) + results = rank_models( + [partial_direct, full_gpu_estimated], + hw, + top_n=10, + benchmark_scores={ + "Qwen/Qwen2.5-72B-Instruct": 80.0, # direct + "Qwen/Qwen3-32B": 85.0, # line inherited for Qwen3-9B + }, + task_profile="general", + require_direct_top=True, + ) + assert results + assert results[0].fit_type == "full_gpu" + assert results[0].model.id == "Qwen/Qwen3-9B-AWQ" + + +def test_family_dedup_prefers_direct_when_enabled(): + # 同一family内でfit条件が同等なら、directを優先する + direct_base = ModelInfo( + id="Qwen/Qwen2.5-7B-Instruct", + family_id="qwen2.5-7b", + name="Qwen2.5-7B-Instruct", + parameter_count=7_000_000_000, + downloads=1000, + likes=100, + ) + estimated_variant = ModelInfo( + id="Qwen/Qwen2.5-7B-Instruct-GGUF", + family_id="qwen2.5-7b", + name="Qwen2.5-7B-Instruct-GGUF", + parameter_count=7_000_000_000, + downloads=1000, + likes=100, + gguf_variants=[ + GGUFVariant( + filename="x-Q4_K_M.gguf", + quant_type="Q4_K_M", + file_size_bytes=4_000_000_000, + ), + ], + ) + hw = _make_hardware(vram_gb=16, bandwidth_gbps=272.0) + results = rank_models( + [direct_base, estimated_variant], + hw, + top_n=10, + benchmark_scores={"Qwen/Qwen2.5-7B-Instruct": 75.0}, + task_profile="general", + require_direct_top=True, + min_params_b=7.0, + ) + assert len(results) == 1 + assert results[0].model.id == "Qwen/Qwen2.5-7B-Instruct" + assert results[0].benchmark_status == "direct" + + +def test_full_gpu_estimated_ranks_above_partial_direct(): + # Use 72B model so Q4_K_M synthesis still doesn't fit 8GB — preserves the + # "direct evidence, but model is too big" half of this scenario. + partial_direct = ModelInfo( + id="Qwen/Qwen2.5-72B-Instruct", + family_id="qwen2.5-72b", + name="Qwen2.5-72B-Instruct", + parameter_count=72_000_000_000, + downloads=1000, + likes=100, + ) + full_gpu_estimated = ModelInfo( + id="Qwen/Qwen3-8B-AWQ", + family_id="qwen3-8b", + name="Qwen3-8B-AWQ", + parameter_count=8_000_000_000, + downloads=1000, + likes=100, + ) + hw = _make_hardware(vram_gb=8, bandwidth_gbps=272.0) + results = rank_models( + [partial_direct, full_gpu_estimated], + hw, + top_n=10, + benchmark_scores={ + "Qwen/Qwen2.5-72B-Instruct": 75.0, # direct but partial + "Qwen/Qwen3-32B": 85.0, # estimated but full gpu + }, + task_profile="general", + require_direct_top=True, + min_params_b=7.0, + ) + # The full-GPU candidate must always win over a partial-offload one of + # comparable quality. The partial 72B may or may not be retained + # depending on whether its sub-2 t/s estimate trips the speed floor — + # either way the full-GPU 8B should be #1. + assert results + assert results[0].fit_type == "full_gpu" + assert results[0].model.id == "Qwen/Qwen3-8B-AWQ" + + +def test_strong_partial_offload_not_buried_below_weaker_full_gpu(): + strong_partial = ModelInfo( + id="Qwen/Qwen3.6-27B", + family_id="qwen3.6-27b", + name="Qwen3.6-27B", + parameter_count=27_800_000_000, + downloads=5_300_000, + likes=10_000, + gguf_variants=[ + GGUFVariant( + filename="qwen3.6-27b-q4_k_m.gguf", + quant_type="Q4_K_M", + file_size_bytes=15 * 1024**3, + ) + ], + ) + full_gpu_14b = ModelInfo( + id="Qwen/Qwen3-14B", + family_id="qwen3-14b", + name="Qwen3-14B", + parameter_count=14_800_000_000, + downloads=1_600_000, + likes=5_000, + gguf_variants=[ + GGUFVariant( + filename="qwen3-14b-q5_k_m.gguf", + quant_type="Q5_K_M", + file_size_bytes=9 * 1024**3, + ) + ], + ) + full_gpu_8b = ModelInfo( + id="Qwen/Qwen3-8B", + family_id="qwen3-8b", + name="Qwen3-8B", + parameter_count=8_200_000_000, + downloads=11_000_000, + likes=5_000, + gguf_variants=[ + GGUFVariant( + filename="qwen3-8b-q5_k_m.gguf", + quant_type="Q5_K_M", + file_size_bytes=5 * 1024**3, + ) + ], + ) + old_full_gpu = ModelInfo( + id="google/gemma-2-9b-it", + family_id="gemma-2-9b-it", + name="gemma-2-9b-it", + parameter_count=9_200_000_000, + downloads=400_000, + likes=1_000, + gguf_variants=[ + GGUFVariant( + filename="gemma-2-9b-q5_k_m.gguf", + quant_type="Q5_K_M", + file_size_bytes=5_500_000_000, + ) + ], + ) + hardware = HardwareInfo( + gpus=[ + GPUInfo( + name="RTX 3060", + vendor="nvidia", + vram_bytes=12 * 1024**3, + compute_capability=(8, 6), + memory_bandwidth_gbps=360.0, + ) + ], + cpu_name="Test CPU", + cpu_cores=6, + has_avx2=True, + ram_bytes=32 * 1024**3, + disk_free_bytes=500 * 1024**3, + os="windows", + ) + + results = rank_models( + [strong_partial, full_gpu_14b, full_gpu_8b, old_full_gpu], + hardware, + top_n=10, + benchmark_scores={ + "Qwen/Qwen3.6-27B": 83.5, + "Qwen/Qwen3-14B": 66.7, + "Qwen/Qwen3-8B": 56.1, + "google/gemma-2-9b-it": 35.1, + }, + task_profile="any", + ) + + ids = [r.model.id for r in results] + assert ids.index("Qwen/Qwen3.6-27B") < ids.index("Qwen/Qwen3-8B") + assert ids.index("Qwen/Qwen3.6-27B") < ids.index("google/gemma-2-9b-it") + strong = next(r for r in results if r.model.id == "Qwen/Qwen3.6-27B") + assert strong.fit_type == "partial_offload" + assert ( + strong.quality_score + > next(r for r in results if r.model.id == "Qwen/Qwen3-8B").quality_score + ) + + +def test_moe_partial_offload_penalty_uses_active_working_set(): + dense = ModelInfo( + id="example/Dense-30B", + family_id="dense-30b", + name="Dense-30B", + parameter_count=30_000_000_000, + ) + moe = ModelInfo( + id="example/MoE-30B-A3B", + family_id="moe-30b-a3b", + name="MoE-30B-A3B", + parameter_count=30_000_000_000, + parameter_count_active=3_000_000_000, + is_moe=True, + ) + + assert _partial_offload_quality_factor(dense, 0.80) == 0.42 + assert _partial_offload_quality_factor(moe, 0.80) >= 0.66 + + +def test_evidence_strict_filters_out_estimated_models(): + direct_model = ModelInfo( + id="Qwen/Qwen2.5-7B-Instruct", + family_id="qwen2.5-7b", + name="Qwen2.5-7B-Instruct", + parameter_count=7_000_000_000, + downloads=1000, + likes=100, + gguf_variants=[ + GGUFVariant( + filename="d-Q4_K_M.gguf", + quant_type="Q4_K_M", + file_size_bytes=4_000_000_000, + ), + ], + ) + estimated_model = ModelInfo( + id="Qwen/Qwen3-14B-Instruct-GGUF", + family_id="qwen3-14b", + name="Qwen3-14B-Instruct-GGUF", + parameter_count=14_000_000_000, + downloads=1000, + likes=100, + gguf_variants=[ + GGUFVariant( + filename="e-Q4_K_M.gguf", + quant_type="Q4_K_M", + file_size_bytes=8_000_000_000, + ), + ], + ) + hw = _make_hardware(vram_gb=24, bandwidth_gbps=300.0) + results = rank_models( + [direct_model, estimated_model], + hw, + top_n=10, + benchmark_scores={ + "Qwen/Qwen2.5-7B-Instruct": 70.0, + "Qwen/Qwen3-32B-Instruct": 85.0, # Qwen3-14B には line 推定が入る + }, + task_profile="any", + evidence_filter="strict", + ) + assert len(results) == 1 + assert results[0].model.id == "Qwen/Qwen2.5-7B-Instruct" + assert results[0].benchmark_status == "direct" + + +def test_evidence_base_keeps_base_model_match_and_drops_line_interp(): + direct_model = ModelInfo( + id="Qwen/Qwen2.5-7B-Instruct", + family_id="qwen2.5-7b", + name="Qwen2.5-7B-Instruct", + parameter_count=7_000_000_000, + downloads=1000, + likes=100, + ) + base_match_model = ModelInfo( + id="ISTA-DASLab/gemma-3-27b-it-GPTQ-4b-128g", + family_id="gemma-3-27b", + name="gemma-3-27b-it-GPTQ-4b-128g", + parameter_count=27_000_000_000, + downloads=1000, + likes=100, + base_model="google/gemma-3-27b-it", + ) + line_interp_model = ModelInfo( + id="Qwen/Qwen3-14B-Instruct-GGUF", + family_id="qwen3-14b", + name="Qwen3-14B-Instruct-GGUF", + parameter_count=14_000_000_000, + downloads=1000, + likes=100, + gguf_variants=[ + GGUFVariant( + filename="f-Q4_K_M.gguf", + quant_type="Q4_K_M", + file_size_bytes=8_000_000_000, + ), + ], + ) + hw = _make_hardware(vram_gb=24, bandwidth_gbps=300.0) + results = rank_models( + [direct_model, base_match_model, line_interp_model], + hw, + top_n=10, + benchmark_scores={ + "Qwen/Qwen2.5-7B-Instruct": 70.0, + "google/gemma-3-27b-it": 82.0, + "Qwen/Qwen3-32B-Instruct": 85.0, + }, + task_profile="any", + evidence_filter="base", + ) + ids = {r.model.id for r in results} + assert "Qwen/Qwen2.5-7B-Instruct" in ids + assert "ISTA-DASLab/gemma-3-27b-it-GPTQ-4b-128g" in ids + assert "Qwen/Qwen3-14B-Instruct-GGUF" not in ids + + +def test_unknown_speed_heavy_partial_offload_does_not_top_rank(): + heavy_partial = ModelInfo( + id="Qwen/Qwen3.6-27B", + family_id="qwen3.6-27b", + name="Qwen3.6-27B", + parameter_count=27_800_000_000, + downloads=1_000_000, + likes=10_000, + gguf_variants=[ + GGUFVariant( + filename="qwen3.6-27b-q8_0.gguf", + quant_type="Q8_0", + file_size_bytes=29_500_000_000, + ) + ], + ) + full_gpu = ModelInfo( + id="Qwen/Qwen3-8B", + family_id="qwen3-8b", + name="Qwen3-8B", + parameter_count=8_000_000_000, + downloads=500_000, + likes=5_000, + gguf_variants=[ + GGUFVariant( + filename="qwen3-8b-q4_k_m.gguf", + quant_type="Q4_K_M", + file_size_bytes=4_000_000_000, + ) + ], + ) + hardware = HardwareInfo( + gpus=[ + GPUInfo( + name="Unknown 6GB NVIDIA GPU", + vendor="nvidia", + vram_bytes=6 * 1024**3, + compute_capability=(8, 6), + memory_bandwidth_gbps=None, + ) + ], + cpu_name="Test CPU", + cpu_cores=8, + has_avx2=True, + ram_bytes=32 * 1024**3, + disk_free_bytes=500 * 1024**3, + os="windows", + ) + + results = rank_models( + [heavy_partial, full_gpu], + hardware, + top_n=2, + benchmark_scores={ + "Qwen/Qwen3.6-27B": 84.0, + "Qwen/Qwen3-8B": 62.0, + }, + ) + + assert results + assert results[0].model.id == "Qwen/Qwen3-8B" + assert results[0].fit_type == "full_gpu" + heavy = next((r for r in results if r.model.id == "Qwen/Qwen3.6-27B"), None) + if heavy is not None: + assert heavy.fit_type == "partial_offload" + assert heavy.offload_ratio >= 0.70 + assert heavy.estimated_tok_per_sec == 0.0 + + +def test_fit_filter_full_gpu_excludes_partial_offload_and_cpu_only(): + partial = ModelInfo( + id="org/Test-30B-GGUF", + family_id="test-30b", + name="Test-30B-GGUF", + parameter_count=30_000_000_000, + downloads=1000, + likes=100, + gguf_variants=[ + GGUFVariant( + filename="test-30b-Q4_K_M.gguf", + quant_type="Q4_K_M", + file_size_bytes=18_000_000_000, + ) + ], + ) + full = ModelInfo( + id="org/Test-7B-GGUF", + family_id="test-7b", + name="Test-7B-GGUF", + parameter_count=7_000_000_000, + downloads=900, + likes=90, + gguf_variants=[ + GGUFVariant( + filename="test-7b-Q4_K_M.gguf", + quant_type="Q4_K_M", + file_size_bytes=4_000_000_000, + ) + ], + ) + cpu_only = ModelInfo( + id="org/Test-60B-GGUF", + family_id="test-60b", + name="Test-60B-GGUF", + parameter_count=60_000_000_000, + downloads=800, + likes=80, + gguf_variants=[ + GGUFVariant( + filename="test-60b-Q4_K_M.gguf", + quant_type="Q4_K_M", + file_size_bytes=36_000_000_000, + ) + ], + ) + hw = _make_hardware(vram_gb=8, bandwidth_gbps=300.0) + results = rank_models( + [partial, full, cpu_only], + hw, + top_n=10, + fit_filter="full_gpu", + task_profile="any", + require_direct_top=False, + ) + + assert [r.model.id for r in results] == ["org/Test-7B-GGUF"] + assert results[0].fit_type == "full_gpu" + + +def test_fit_filter_full_gpu_returns_empty_when_no_full_gpu_candidate(): + partial = ModelInfo( + id="org/Test-30B-GGUF", + family_id="test-30b", + name="Test-30B-GGUF", + parameter_count=30_000_000_000, + downloads=1000, + likes=100, + gguf_variants=[ + GGUFVariant( + filename="test-30b-Q4_K_M.gguf", + quant_type="Q4_K_M", + file_size_bytes=18_000_000_000, + ) + ], + ) + hw = _make_hardware(vram_gb=8, bandwidth_gbps=300.0) + results = rank_models( + [partial], + hw, + top_n=10, + fit_filter="full_gpu", + task_profile="any", + require_direct_top=False, + ) + + assert results == [] + + +def test_multi_gpu_speed_confidence_is_low(): + from whichllm.engine.performance import estimate_tok_per_sec + + model = ModelInfo( + id="org/Test-34B-GGUF", + family_id="org/Test-34B-GGUF", + name="Test-34B-GGUF", + parameter_count=34_000_000_000, + downloads=1000, + likes=100, + gguf_variants=[ + GGUFVariant( + filename="test-34b-Q4_K_M.gguf", + quant_type="Q4_K_M", + file_size_bytes=22 * 1024**3, + ) + ], + ) + hw = HardwareInfo( + gpus=[ + GPUInfo( + name="NVIDIA GeForce RTX 4090", + vendor="nvidia", + vram_bytes=24 * 1024**3, + compute_capability=(8, 9), + memory_bandwidth_gbps=1008.0, + ), + GPUInfo( + name="NVIDIA GeForce RTX 4090", + vendor="nvidia", + vram_bytes=24 * 1024**3, + compute_capability=(8, 9), + memory_bandwidth_gbps=1008.0, + ), + ], + cpu_name="Test CPU", + cpu_cores=16, + has_avx2=True, + ram_bytes=128 * 1024**3, + disk_free_bytes=500 * 1024**3, + os="linux", + ) + + results = rank_models( + [model], + hw, + top_n=1, + benchmark_scores={"org/Test-34B-GGUF": 70.0}, + ) + + assert results + assert results[0].fit_type == "full_gpu" + assert results[0].uses_multi_gpu is True + assert results[0].speed_confidence == "low" + single_gpu_speed = estimate_tok_per_sec( + model, + model.gguf_variants[0], + hw.gpus[0], + "full_gpu", + ) + assert results[0].estimated_tok_per_sec == single_gpu_speed * 0.70 + assert any("Multi-GPU speed depends" in note for note in results[0].speed_notes) + + +def test_benchmark_source_and_confidence_exposed_for_direct(): + model = ModelInfo( + id="Qwen/Qwen2.5-7B-Instruct", + family_id="qwen2.5-7b", + name="Qwen2.5-7B-Instruct", + parameter_count=7_000_000_000, + downloads=1000, + likes=100, + gguf_variants=[ + GGUFVariant( + filename="a-Q4_K_M.gguf", + quant_type="Q4_K_M", + file_size_bytes=4_000_000_000, + ), + ], + ) + hw = _make_hardware() + results = rank_models( + [model], + hw, + top_n=1, + benchmark_scores={"Qwen/Qwen2.5-7B-Instruct": 75.0}, + task_profile="any", + ) + assert results + assert results[0].benchmark_status == "direct" + assert results[0].benchmark_source == "direct" + assert results[0].benchmark_confidence == 1.0 + + +def test_benchmark_source_and_confidence_exposed_for_estimated(): + model = ModelInfo( + id="Qwen/Qwen3-14B-Instruct-GGUF", + family_id="qwen3-14b", + name="Qwen3-14B-Instruct-GGUF", + parameter_count=14_000_000_000, + downloads=1000, + likes=100, + gguf_variants=[ + GGUFVariant( + filename="e-Q4_K_M.gguf", + quant_type="Q4_K_M", + file_size_bytes=8_000_000_000, + ), + ], + ) + hw = _make_hardware() + results = rank_models( + [model], + hw, + top_n=1, + benchmark_scores={"Qwen/Qwen3-32B-Instruct": 85.0}, + task_profile="any", + ) + assert results + assert results[0].benchmark_status == "estimated" + assert results[0].benchmark_source == "line_interp" + assert 0.0 < results[0].benchmark_confidence < 1.0 + + +def test_benchmark_source_and_confidence_exposed_for_self_reported(): + model = ModelInfo( + id="someorg/mystery-7B", + family_id="mystery-7b", + name="mystery-7B", + parameter_count=7_000_000_000, + downloads=1000, + likes=100, + benchmark_scores={"hf_eval": 72.0}, + gguf_variants=[ + GGUFVariant( + filename="m-Q4_K_M.gguf", + quant_type="Q4_K_M", + file_size_bytes=4_000_000_000, + ), + ], + ) + hw = _make_hardware() + results = rank_models( + [model], + hw, + top_n=1, + benchmark_scores={}, + task_profile="any", + ) + assert results + assert results[0].benchmark_status == "self_reported" + assert results[0].benchmark_source == "self_reported" + assert results[0].benchmark_confidence > 0.0 + + +def test_benchmark_source_and_confidence_exposed_for_none(): + model = ModelInfo( + id="someorg/unknown-7B", + family_id="unknown-7b", + name="unknown-7B", + parameter_count=7_000_000_000, + downloads=1000, + likes=100, + gguf_variants=[ + GGUFVariant( + filename="u-Q4_K_M.gguf", + quant_type="Q4_K_M", + file_size_bytes=4_000_000_000, + ), + ], + ) + hw = _make_hardware() + results = rank_models( + [model], + hw, + top_n=1, + benchmark_scores={}, + task_profile="any", + ) + assert results + assert results[0].benchmark_status == "none" + assert results[0].benchmark_source == "none" + assert results[0].benchmark_confidence == 0.0 + + +def test_ctx_penalty_demotes_non_fitting(): + models = [ + ModelInfo( + id="org/LongCtx-8B", + family_id="longctx-8b", + name="LongCtx-8B", + parameter_count=8_000_000_000, + context_length=131072, + downloads=900, + likes=90, + gguf_variants=[ + GGUFVariant( + filename="long-Q4_K_M.gguf", + quant_type="Q4_K_M", + file_size_bytes=4_500_000_000, + ), + ], + ), + ModelInfo( + id="org/ShortCtx-8B", + family_id="shortctx-8b", + name="ShortCtx-8B", + parameter_count=8_000_000_000, + context_length=8192, + downloads=1000, + likes=100, + gguf_variants=[ + GGUFVariant( + filename="short-Q4_K_M.gguf", + quant_type="Q4_K_M", + file_size_bytes=4_500_000_000, + ), + ], + ), + ] + scores = { + "org/LongCtx-8B": 74.0, + "org/ShortCtx-8B": 76.0, + } + hw = _make_hardware(bandwidth_gbps=900.0) + + results = rank_models( + models, + hw, + context_length=32768, + top_n=2, + benchmark_scores=scores, + require_direct_top=False, + task_profile="any", + ) + + assert len(results) == 2 + assert results[0].model.family_id == "longctx-8b" + assert results[0].context_fits is True + assert results[1].context_fits is False diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 0000000..f68376c --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,81 @@ +"""Tests for shared utilities.""" + +from pathlib import Path + +import pytest + +from whichllm.utils import _cache_dir, parse_context_length, CONTEXT_LENGTH + + +def test_cache_dir_defaults_to_dot_cache(monkeypatch): + monkeypatch.delenv("XDG_CACHE_HOME", raising=False) + result = _cache_dir() + assert result == Path.home() / ".cache" / "whichllm" + + +def test_cache_dir_respects_xdg_cache_home(monkeypatch): + monkeypatch.setenv("XDG_CACHE_HOME", "/tmp/custom-cache") + result = _cache_dir() + assert result == Path("/tmp/custom-cache/whichllm") + + +def test_cache_dir_falls_back_on_empty_xdg(monkeypatch): + monkeypatch.setenv("XDG_CACHE_HOME", "") + result = _cache_dir() + assert result == Path.home() / ".cache" / "whichllm" + + +def test_cache_dir_ignores_relative_xdg(monkeypatch): + monkeypatch.setenv("XDG_CACHE_HOME", "relative/path") + result = _cache_dir() + assert result == Path.home() / ".cache" / "whichllm" + + +# --- parse_context_length tests --- + + +def test_parse_plain_integer(): + assert parse_context_length("4096") == 4096 + assert parse_context_length("131072") == 131072 + + +def test_parse_k_suffix(): + assert parse_context_length("64k") == 64 * 1024 + assert parse_context_length("128K") == 128 * 1024 + + +def test_parse_m_suffix(): + assert parse_context_length("1m") == 1024 * 1024 + assert parse_context_length("2M") == 2 * 1024 * 1024 + + +def test_parse_fractional_suffix(): + assert parse_context_length("1.5k") == int(1.5 * 1024) + assert parse_context_length("0.5m") == int(0.5 * 1024 * 1024) + + +def test_parse_whitespace_is_stripped(): + assert parse_context_length(" 64k ") == 64 * 1024 + + +def test_parse_rejects_invalid_string(): + with pytest.raises(ValueError, match="Invalid context length"): + parse_context_length("abc") + + +def test_parse_rejects_zero(): + with pytest.raises(ValueError, match="must be positive"): + parse_context_length("0") + + +def test_parse_rejects_negative(): + with pytest.raises(ValueError, match="must be positive"): + parse_context_length("-1") + + +def test_click_type_passes_int_through(): + assert CONTEXT_LENGTH.convert(4096, None, None) == 4096 + + +def test_click_type_parses_shorthand(): + assert CONTEXT_LENGTH.convert("64k", None, None) == 64 * 1024 diff --git a/tests/test_vram.py b/tests/test_vram.py new file mode 100644 index 0000000..74b54fd --- /dev/null +++ b/tests/test_vram.py @@ -0,0 +1,148 @@ +"""Tests for VRAM estimation.""" + +from whichllm.engine.vram import estimate_kv_cache, estimate_vram +from whichllm.models.types import GGUFVariant, ModelInfo + + +def _make_model(params: int, **kwargs) -> ModelInfo: + return ModelInfo( + id="test/model", + family_id="test/model", + name="model", + parameter_count=params, + **kwargs, + ) + + +def test_estimate_vram_gguf_variant(): + model = _make_model(7_000_000_000) + variant = GGUFVariant( + filename="model-Q4_K_M.gguf", quant_type="Q4_K_M", file_size_bytes=4_000_000_000 + ) + vram = estimate_vram(model, variant, context_length=4096) + # Should be: 4GB weights + KV cache + activation + framework overhead + assert vram > 4_000_000_000 + assert vram < 7_000_000_000 # should be well under FP16 size + + +def test_estimate_vram_fp16_fallback(): + model = _make_model(7_000_000_000) + vram = estimate_vram(model, None, context_length=4096) + # FP16: 7B * 2 = 14GB + overhead + assert vram > 14_000_000_000 + assert vram < 20_000_000_000 + + +def test_estimate_vram_increases_with_context(): + model = _make_model(7_000_000_000) + variant = GGUFVariant( + filename="model-Q4_K_M.gguf", quant_type="Q4_K_M", file_size_bytes=4_000_000_000 + ) + vram_4k = estimate_vram(model, variant, context_length=4096) + vram_32k = estimate_vram(model, variant, context_length=32768) + assert vram_32k > vram_4k + + +def test_estimate_kv_cache_scales_with_params(): + small = _make_model(1_000_000_000) + large = _make_model(70_000_000_000) + kv_small = estimate_kv_cache(small, 4096) + kv_large = estimate_kv_cache(large, 4096) + assert kv_large > kv_small + + +def test_estimate_vram_small_model(): + model = _make_model(500_000_000) # 0.5B + variant = GGUFVariant( + filename="model-Q4_K_M.gguf", quant_type="Q4_K_M", file_size_bytes=300_000_000 + ) + vram = estimate_vram(model, variant, context_length=4096) + # Should be reasonable for a tiny model + assert vram > 300_000_000 + assert vram < 3_000_000_000 + + +def test_kv_cache_unchanged_when_no_sliding_window(): + """Models without an honored sliding window keep the full-context KV figure. + + This is the conservative-default guarantee: the SWA change must be a no-op + for every model that does not advertise an honored window. Expected values + are pinned literals (the current 3.5 MB/B/Kctx coefficient) so the test + fails if either the formula or the coefficient drifts. + """ + dense = _make_model(7_000_000_000) + expected = { + 4096: 102_760_448, + 32768: 822_083_584, + 131072: 3_288_334_336, + } + for ctx, want in expected.items(): + assert estimate_kv_cache(dense, ctx) == want + + +def test_kv_cache_mistral_window_in_config_is_ignored(): + """A declared window is NOT honored unless the architecture is allowlisted. + + Mistral-7B-v0.1 ships sliding_window=4096 in its config but mainline + runtimes ignore it, so whichllm must stay at full-context KV. We model this + by leaving sliding_window=None on the model; the estimate must equal dense. + """ + mistral = _make_model(7_000_000_000, architecture="mistral") + dense = _make_model(7_000_000_000) + for ctx in (4096, 131072): + assert estimate_kv_cache(mistral, ctx) == estimate_kv_cache(dense, ctx) + + +def test_kv_cache_pure_swa_plateaus_beyond_window(): + """Pure sliding-window models (global_ratio=0) plateau at the window size.""" + swa = _make_model( + 7_000_000_000, sliding_window=4096, sliding_window_global_ratio=0.0 + ) + at_window = estimate_kv_cache(swa, 4096) + far_beyond = estimate_kv_cache(swa, 131072) + # KV is flat once context exceeds the window. + assert far_beyond == at_window + # And it is far below the dense estimate at the same long context. + dense = estimate_kv_cache(_make_model(7_000_000_000), 131072) + assert far_beyond < dense / 10 + + +def test_kv_cache_hybrid_grows_slower_than_dense(): + """Hybrid SWA models grow with context but much slower than dense.""" + # Gemma-3-like: 1/6 global layers, 1024-token window. + hybrid = _make_model( + 27_000_000_000, + sliding_window=1024, + sliding_window_global_ratio=1.0 / 6.0, + ) + dense = _make_model(27_000_000_000) + short = estimate_kv_cache(hybrid, 4096) + long_hybrid = estimate_kv_cache(hybrid, 131072) + long_dense = estimate_kv_cache(dense, 131072) + # Still grows with context (global layers keep scaling)... + assert long_hybrid > short + # ...but well under the dense estimate (roughly the global ratio). + assert long_hybrid < long_dense + assert long_hybrid < long_dense * 0.30 + + +def test_kv_cache_never_exceeds_dense_estimate(): + """The SWA reduction can only ever lower the estimate, never raise it.""" + for ratio in (0.0, 1.0 / 6.0, 0.25, 0.5, 1.0): + swa = _make_model( + 13_000_000_000, + sliding_window=2048, + sliding_window_global_ratio=ratio, + ) + dense = _make_model(13_000_000_000) + for ctx in (1024, 4096, 65536): + assert estimate_kv_cache(swa, ctx) <= estimate_kv_cache(dense, ctx) + + +def test_kv_cache_below_window_matches_dense(): + """When context fits inside the window, SWA and dense agree.""" + swa = _make_model( + 7_000_000_000, sliding_window=8192, sliding_window_global_ratio=0.0 + ) + dense = _make_model(7_000_000_000) + assert estimate_kv_cache(swa, 4096) == estimate_kv_cache(dense, 4096) diff --git a/tests/test_windows_cpu_detection.py b/tests/test_windows_cpu_detection.py new file mode 100644 index 0000000..c9f35ea --- /dev/null +++ b/tests/test_windows_cpu_detection.py @@ -0,0 +1,45 @@ +"""Tests for Windows CPU name fallback detection.""" + +from __future__ import annotations + +import subprocess + +from whichllm.hardware import cpu + + +def test_windows_cpu_name_uses_cim_when_wmic_is_empty(monkeypatch): + monkeypatch.setattr(cpu.platform, "system", lambda: "Windows") + + def fake_run(args, **kwargs): + if args == ["wmic", "cpu", "get", "name"]: + return subprocess.CompletedProcess(args, 0, stdout="Name\n\n", stderr="") + if args[0] == "powershell": + return subprocess.CompletedProcess( + args, + 0, + stdout="Intel(R) Xeon(R) W-11955M CPU @ 2.60GHz\n", + stderr="", + ) + raise FileNotFoundError + + monkeypatch.setattr(cpu.subprocess, "run", fake_run) + + assert cpu.detect_cpu_name() == "Intel(R) Xeon(R) W-11955M CPU @ 2.60GHz" + + +def test_windows_cpu_name_uses_wmic_when_available(monkeypatch): + monkeypatch.setattr(cpu.platform, "system", lambda: "Windows") + + def fake_run(args, **kwargs): + if args == ["wmic", "cpu", "get", "name"]: + return subprocess.CompletedProcess( + args, + 0, + stdout="Name\nIntel(R) Core(TM) i9-11900H @ 2.50GHz\n", + stderr="", + ) + raise AssertionError("CIM fallback should not run when wmic returns a name") + + monkeypatch.setattr(cpu.subprocess, "run", fake_run) + + assert cpu.detect_cpu_name() == "Intel(R) Core(TM) i9-11900H @ 2.50GHz" diff --git a/tests/test_windows_gpu.py b/tests/test_windows_gpu.py new file mode 100644 index 0000000..1fe3b86 --- /dev/null +++ b/tests/test_windows_gpu.py @@ -0,0 +1,175 @@ +"""Tests for Windows GPU detection.""" + +from __future__ import annotations + +import json +import subprocess + +from whichllm.hardware import windows + + +def test_detect_windows_amd_gpu(monkeypatch): + output = json.dumps( + { + "Name": "AMD Radeon RX 9060 XT", + "AdapterRAM": 16 * 1024**3, + } + ) + + def fake_run(*args, **kwargs): + return subprocess.CompletedProcess(args[0], 0, stdout=output, stderr="") + + monkeypatch.setattr(windows.subprocess, "run", fake_run) + + gpus = windows.detect_windows_gpus() + + assert len(gpus) == 1 + assert gpus[0].name == "AMD Radeon RX 9060 XT" + assert gpus[0].vendor == "amd" + assert gpus[0].vram_bytes == 16 * 1024**3 + assert gpus[0].shared_memory is False + assert gpus[0].memory_bandwidth_gbps == 320.0 + + +def test_detect_windows_amd_gpu_prefers_64_bit_dedicated_memory(monkeypatch): + output = json.dumps( + { + "Name": "AMD Radeon RX 9060 XT", + "AdapterRAM": 4 * 1024**3, + "DedicatedVideoMemory": 16 * 1024**3, + } + ) + + def fake_run(*args, **kwargs): + return subprocess.CompletedProcess(args[0], 0, stdout=output, stderr="") + + monkeypatch.setattr(windows.subprocess, "run", fake_run) + + gpus = windows.detect_windows_gpus() + + assert len(gpus) == 1 + assert gpus[0].vram_bytes == 16 * 1024**3 + assert gpus[0].shared_memory is False + + +def test_detect_windows_gpu_queries_control_video_qword_memory(monkeypatch): + captured: dict[str, str] = {} + + def fake_run(*args, **kwargs): + command = args[0] + captured["script"] = command[-1] + return subprocess.CompletedProcess(command, 0, stdout="[]", stderr="") + + monkeypatch.setattr(windows.subprocess, "run", fake_run) + + assert windows.detect_windows_gpus() == [] + assert "Control\\Video" in captured["script"] + assert "VideoID" in captured["script"] + assert "HardwareInformation.qwMemorySize" in captured["script"] + + +def test_detect_windows_amd_gpu_applies_known_floor_for_capped_adapter_ram( + monkeypatch, +): + output = json.dumps( + { + "Name": "AMD Radeon RX 9060 XT", + "AdapterRAM": 4 * 1024**3, + "DedicatedVideoMemory": None, + } + ) + + def fake_run(*args, **kwargs): + return subprocess.CompletedProcess(args[0], 0, stdout=output, stderr="") + + monkeypatch.setattr(windows.subprocess, "run", fake_run) + + gpus = windows.detect_windows_gpus() + + assert len(gpus) == 1 + assert gpus[0].vram_bytes == 8 * 1024**3 + assert gpus[0].shared_memory is False + + +def test_detect_windows_ryzen_ai_radeon_890m_as_shared_memory(monkeypatch): + output = json.dumps( + { + "Name": "AMD Ryzen AI 9 HX 370 w/ Radeon 890M", + "AdapterRAM": 512 * 1024**2, + "DedicatedVideoMemory": None, + } + ) + + def fake_run(*args, **kwargs): + return subprocess.CompletedProcess(args[0], 0, stdout=output, stderr="") + + monkeypatch.setattr(windows.subprocess, "run", fake_run) + + gpus = windows.detect_windows_gpus() + + assert len(gpus) == 1 + assert gpus[0].name == "AMD Ryzen AI 9 HX 370 w/ Radeon 890M" + assert gpus[0].vendor == "amd" + assert gpus[0].vram_bytes == 0 + assert gpus[0].shared_memory is True + assert gpus[0].memory_bandwidth_gbps == 120.0 + + +def test_detect_windows_intel_gpu_list(monkeypatch): + output = json.dumps( + [ + { + "Name": "Intel(R) Arc(TM) Graphics", + "AdapterRAM": None, + }, + { + "Name": "Microsoft Basic Display Adapter", + "AdapterRAM": 0, + }, + ] + ) + + def fake_run(*args, **kwargs): + return subprocess.CompletedProcess(args[0], 0, stdout=output, stderr="") + + monkeypatch.setattr(windows.subprocess, "run", fake_run) + + gpus = windows.detect_windows_gpus() + + assert len(gpus) == 1 + assert gpus[0].name == "Intel(R) Arc(TM) Graphics" + assert gpus[0].vendor == "intel" + assert gpus[0].vram_bytes == 0 + assert gpus[0].shared_memory is True + + +def test_detect_windows_intel_arc_discrete_is_not_shared_memory(monkeypatch): + output = json.dumps( + { + "Name": "Intel(R) Arc(TM) B580 Graphics", + "AdapterRAM": 4 * 1024**3, + "DedicatedVideoMemory": 12 * 1024**3, + } + ) + + def fake_run(*args, **kwargs): + return subprocess.CompletedProcess(args[0], 0, stdout=output, stderr="") + + monkeypatch.setattr(windows.subprocess, "run", fake_run) + + gpus = windows.detect_windows_gpus() + + assert len(gpus) == 1 + assert gpus[0].name == "Intel(R) Arc(TM) B580 Graphics" + assert gpus[0].vendor == "intel" + assert gpus[0].vram_bytes == 12 * 1024**3 + assert gpus[0].shared_memory is False + + +def test_detect_windows_gpu_returns_empty_on_command_failure(monkeypatch): + def fake_run(*args, **kwargs): + raise FileNotFoundError + + monkeypatch.setattr(windows.subprocess, "run", fake_run) + + assert windows.detect_windows_gpus() == [] diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..da5cc41 --- /dev/null +++ b/uv.lock @@ -0,0 +1,581 @@ +version = 1 +revision = 3 +requires-python = ">=3.11" + +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, +] + +[[package]] +name = "certifi" +version = "2026.2.25" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, +] + +[[package]] +name = "click" +version = "8.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "dbgpu" +version = "2025.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d3/89/3f020bdd2236ba974f0c4f1b31ce67c54bd127ca0d2296ef48b8e94c3bc3/dbgpu-2025.12.tar.gz", hash = "sha256:d4a2fdc36ff5ff2af37e8fd8a3e0740ab2af73cc5e0fda199fdff3d6f1686f4e", size = 225653, upload-time = "2025-12-15T16:57:20.958Z" } + +[package.optional-dependencies] +fuzz = [ + { name = "thefuzz" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "nvidia-ml-py" +version = "13.590.48" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/a0/f4fc18cf72f06821a9a665085435b901449986855519d5b3843532db35e9/nvidia_ml_py-13.590.48.tar.gz", hash = "sha256:8184d1be52914ac7f0991cd1c0d946c65dc88a840c754cd12c274b77b88760dd", size = 49732, upload-time = "2026-01-22T01:14:56.456Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/72/fb2af0d259a651affdce65fd6a495f0e07a685a0136baf585c5065204ee7/nvidia_ml_py-13.590.48-py3-none-any.whl", hash = "sha256:fd43d30ee9cd0b7940f5f9f9220b68d42722975e3992b6c21d14144c48760e43", size = 50680, upload-time = "2026-01-22T01:14:55.281Z" }, +] + +[[package]] +name = "packaging" +version = "26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "psutil" +version = "7.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/08/510cbdb69c25a96f4ae523f733cdc963ae654904e8db864c07585ef99875/psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b", size = 130595, upload-time = "2026-01-28T18:14:57.293Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f5/97baea3fe7a5a9af7436301f85490905379b1c6f2dd51fe3ecf24b4c5fbf/psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea", size = 131082, upload-time = "2026-01-28T18:14:59.732Z" }, + { url = "https://files.pythonhosted.org/packages/37/d6/246513fbf9fa174af531f28412297dd05241d97a75911ac8febefa1a53c6/psutil-7.2.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63", size = 181476, upload-time = "2026-01-28T18:15:01.884Z" }, + { url = "https://files.pythonhosted.org/packages/b8/b5/9182c9af3836cca61696dabe4fd1304e17bc56cb62f17439e1154f225dd3/psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312", size = 184062, upload-time = "2026-01-28T18:15:04.436Z" }, + { url = "https://files.pythonhosted.org/packages/16/ba/0756dca669f5a9300d0cbcbfae9a4c30e446dfc7440ffe43ded5724bfd93/psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b", size = 139893, upload-time = "2026-01-28T18:15:06.378Z" }, + { url = "https://files.pythonhosted.org/packages/1c/61/8fa0e26f33623b49949346de05ec1ddaad02ed8ba64af45f40a147dbfa97/psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9", size = 135589, upload-time = "2026-01-28T18:15:08.03Z" }, + { url = "https://files.pythonhosted.org/packages/81/69/ef179ab5ca24f32acc1dac0c247fd6a13b501fd5534dbae0e05a1c48b66d/psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00", size = 130664, upload-time = "2026-01-28T18:15:09.469Z" }, + { url = "https://files.pythonhosted.org/packages/7b/64/665248b557a236d3fa9efc378d60d95ef56dd0a490c2cd37dafc7660d4a9/psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9", size = 131087, upload-time = "2026-01-28T18:15:11.724Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2e/e6782744700d6759ebce3043dcfa661fb61e2fb752b91cdeae9af12c2178/psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a", size = 182383, upload-time = "2026-01-28T18:15:13.445Z" }, + { url = "https://files.pythonhosted.org/packages/57/49/0a41cefd10cb7505cdc04dab3eacf24c0c2cb158a998b8c7b1d27ee2c1f5/psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf", size = 185210, upload-time = "2026-01-28T18:15:16.002Z" }, + { url = "https://files.pythonhosted.org/packages/dd/2c/ff9bfb544f283ba5f83ba725a3c5fec6d6b10b8f27ac1dc641c473dc390d/psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1", size = 141228, upload-time = "2026-01-28T18:15:18.385Z" }, + { url = "https://files.pythonhosted.org/packages/f2/fc/f8d9c31db14fcec13748d373e668bc3bed94d9077dbc17fb0eebc073233c/psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841", size = 136284, upload-time = "2026-01-28T18:15:19.912Z" }, + { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" }, + { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" }, + { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" }, + { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" }, + { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" }, + { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" }, + { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, +] + +[[package]] +name = "pyarrow" +version = "23.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/88/22/134986a4cc224d593c1afde5494d18ff629393d74cc2eddb176669f234a4/pyarrow-23.0.1.tar.gz", hash = "sha256:b8c5873e33440b2bc2f4a79d2b47017a89c5a24116c055625e6f2ee50523f019", size = 1167336, upload-time = "2026-02-16T10:14:12.39Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/41/8e6b6ef7e225d4ceead8459427a52afdc23379768f54dd3566014d7618c1/pyarrow-23.0.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:6f0147ee9e0386f519c952cc670eb4a8b05caa594eeffe01af0e25f699e4e9bb", size = 34302230, upload-time = "2026-02-16T10:09:03.859Z" }, + { url = "https://files.pythonhosted.org/packages/bf/4a/1472c00392f521fea03ae93408bf445cc7bfa1ab81683faf9bc188e36629/pyarrow-23.0.1-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:0ae6e17c828455b6265d590100c295193f93cc5675eb0af59e49dbd00d2de350", size = 35850050, upload-time = "2026-02-16T10:09:11.877Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b2/bd1f2f05ded56af7f54d702c8364c9c43cd6abb91b0e9933f3d77b4f4132/pyarrow-23.0.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:fed7020203e9ef273360b9e45be52a2a47d3103caf156a30ace5247ffb51bdbd", size = 44491918, upload-time = "2026-02-16T10:09:18.144Z" }, + { url = "https://files.pythonhosted.org/packages/0b/62/96459ef5b67957eac38a90f541d1c28833d1b367f014a482cb63f3b7cd2d/pyarrow-23.0.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:26d50dee49d741ac0e82185033488d28d35be4d763ae6f321f97d1140eb7a0e9", size = 47562811, upload-time = "2026-02-16T10:09:25.792Z" }, + { url = "https://files.pythonhosted.org/packages/7d/94/1170e235add1f5f45a954e26cd0e906e7e74e23392dcb560de471f7366ec/pyarrow-23.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3c30143b17161310f151f4a2bcfe41b5ff744238c1039338779424e38579d701", size = 48183766, upload-time = "2026-02-16T10:09:34.645Z" }, + { url = "https://files.pythonhosted.org/packages/0e/2d/39a42af4570377b99774cdb47f63ee6c7da7616bd55b3d5001aa18edfe4f/pyarrow-23.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db2190fa79c80a23fdd29fef4b8992893f024ae7c17d2f5f4db7171fa30c2c78", size = 50607669, upload-time = "2026-02-16T10:09:44.153Z" }, + { url = "https://files.pythonhosted.org/packages/00/ca/db94101c187f3df742133ac837e93b1f269ebdac49427f8310ee40b6a58f/pyarrow-23.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:f00f993a8179e0e1c9713bcc0baf6d6c01326a406a9c23495ec1ba9c9ebf2919", size = 27527698, upload-time = "2026-02-16T10:09:50.263Z" }, + { url = "https://files.pythonhosted.org/packages/9a/4b/4166bb5abbfe6f750fc60ad337c43ecf61340fa52ab386da6e8dbf9e63c4/pyarrow-23.0.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:f4b0dbfa124c0bb161f8b5ebb40f1a680b70279aa0c9901d44a2b5a20806039f", size = 34214575, upload-time = "2026-02-16T10:09:56.225Z" }, + { url = "https://files.pythonhosted.org/packages/e1/da/3f941e3734ac8088ea588b53e860baeddac8323ea40ce22e3d0baa865cc9/pyarrow-23.0.1-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:7707d2b6673f7de054e2e83d59f9e805939038eebe1763fe811ee8fa5c0cd1a7", size = 35832540, upload-time = "2026-02-16T10:10:03.428Z" }, + { url = "https://files.pythonhosted.org/packages/88/7c/3d841c366620e906d54430817531b877ba646310296df42ef697308c2705/pyarrow-23.0.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:86ff03fb9f1a320266e0de855dee4b17da6794c595d207f89bba40d16b5c78b9", size = 44470940, upload-time = "2026-02-16T10:10:10.704Z" }, + { url = "https://files.pythonhosted.org/packages/2c/a5/da83046273d990f256cb79796a190bbf7ec999269705ddc609403f8c6b06/pyarrow-23.0.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:813d99f31275919c383aab17f0f455a04f5a429c261cc411b1e9a8f5e4aaaa05", size = 47586063, upload-time = "2026-02-16T10:10:17.95Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3c/b7d2ebcff47a514f47f9da1e74b7949138c58cfeb108cdd4ee62f43f0cf3/pyarrow-23.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bf5842f960cddd2ef757d486041d57c96483efc295a8c4a0e20e704cbbf39c67", size = 48173045, upload-time = "2026-02-16T10:10:25.363Z" }, + { url = "https://files.pythonhosted.org/packages/43/b2/b40961262213beaba6acfc88698eb773dfce32ecdf34d19291db94c2bd73/pyarrow-23.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:564baf97c858ecc03ec01a41062e8f4698abc3e6e2acd79c01c2e97880a19730", size = 50621741, upload-time = "2026-02-16T10:10:33.477Z" }, + { url = "https://files.pythonhosted.org/packages/f6/70/1fdda42d65b28b078e93d75d371b2185a61da89dda4def8ba6ba41ebdeb4/pyarrow-23.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:07deae7783782ac7250989a7b2ecde9b3c343a643f82e8a4df03d93b633006f0", size = 27620678, upload-time = "2026-02-16T10:10:39.31Z" }, + { url = "https://files.pythonhosted.org/packages/47/10/2cbe4c6f0fb83d2de37249567373d64327a5e4d8db72f486db42875b08f6/pyarrow-23.0.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:6b8fda694640b00e8af3c824f99f789e836720aa8c9379fb435d4c4953a756b8", size = 34210066, upload-time = "2026-02-16T10:10:45.487Z" }, + { url = "https://files.pythonhosted.org/packages/cb/4f/679fa7e84dadbaca7a65f7cdba8d6c83febbd93ca12fa4adf40ba3b6362b/pyarrow-23.0.1-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:8ff51b1addc469b9444b7c6f3548e19dc931b172ab234e995a60aea9f6e6025f", size = 35825526, upload-time = "2026-02-16T10:10:52.266Z" }, + { url = "https://files.pythonhosted.org/packages/f9/63/d2747d930882c9d661e9398eefc54f15696547b8983aaaf11d4a2e8b5426/pyarrow-23.0.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:71c5be5cbf1e1cb6169d2a0980850bccb558ddc9b747b6206435313c47c37677", size = 44473279, upload-time = "2026-02-16T10:11:01.557Z" }, + { url = "https://files.pythonhosted.org/packages/b3/93/10a48b5e238de6d562a411af6467e71e7aedbc9b87f8d3a35f1560ae30fb/pyarrow-23.0.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:9b6f4f17b43bc39d56fec96e53fe89d94bac3eb134137964371b45352d40d0c2", size = 47585798, upload-time = "2026-02-16T10:11:09.401Z" }, + { url = "https://files.pythonhosted.org/packages/5c/20/476943001c54ef078dbf9542280e22741219a184a0632862bca4feccd666/pyarrow-23.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9fc13fc6c403d1337acab46a2c4346ca6c9dec5780c3c697cf8abfd5e19b6b37", size = 48179446, upload-time = "2026-02-16T10:11:17.781Z" }, + { url = "https://files.pythonhosted.org/packages/4b/b6/5dd0c47b335fcd8edba9bfab78ad961bd0fd55ebe53468cc393f45e0be60/pyarrow-23.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5c16ed4f53247fa3ffb12a14d236de4213a4415d127fe9cebed33d51671113e2", size = 50623972, upload-time = "2026-02-16T10:11:26.185Z" }, + { url = "https://files.pythonhosted.org/packages/d5/09/a532297c9591a727d67760e2e756b83905dd89adb365a7f6e9c72578bcc1/pyarrow-23.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:cecfb12ef629cf6be0b1887f9f86463b0dd3dc3195ae6224e74006be4736035a", size = 27540749, upload-time = "2026-02-16T10:12:23.297Z" }, + { url = "https://files.pythonhosted.org/packages/a5/8e/38749c4b1303e6ae76b3c80618f84861ae0c55dd3c2273842ea6f8258233/pyarrow-23.0.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:29f7f7419a0e30264ea261fdc0e5fe63ce5a6095003db2945d7cd78df391a7e1", size = 34471544, upload-time = "2026-02-16T10:11:32.535Z" }, + { url = "https://files.pythonhosted.org/packages/a3/73/f237b2bc8c669212f842bcfd842b04fc8d936bfc9d471630569132dc920d/pyarrow-23.0.1-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:33d648dc25b51fd8055c19e4261e813dfc4d2427f068bcecc8b53d01b81b0500", size = 35949911, upload-time = "2026-02-16T10:11:39.813Z" }, + { url = "https://files.pythonhosted.org/packages/0c/86/b912195eee0903b5611bf596833def7d146ab2d301afeb4b722c57ffc966/pyarrow-23.0.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:cd395abf8f91c673dd3589cadc8cc1ee4e8674fa61b2e923c8dd215d9c7d1f41", size = 44520337, upload-time = "2026-02-16T10:11:47.764Z" }, + { url = "https://files.pythonhosted.org/packages/69/c2/f2a717fb824f62d0be952ea724b4f6f9372a17eed6f704b5c9526f12f2f1/pyarrow-23.0.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:00be9576d970c31defb5c32eb72ef585bf600ef6d0a82d5eccaae96639cf9d07", size = 47548944, upload-time = "2026-02-16T10:11:56.607Z" }, + { url = "https://files.pythonhosted.org/packages/84/a7/90007d476b9f0dc308e3bc57b832d004f848fd6c0da601375d20d92d1519/pyarrow-23.0.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c2139549494445609f35a5cda4eb94e2c9e4d704ce60a095b342f82460c73a83", size = 48236269, upload-time = "2026-02-16T10:12:04.47Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3f/b16fab3e77709856eb6ac328ce35f57a6d4a18462c7ca5186ef31b45e0e0/pyarrow-23.0.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7044b442f184d84e2351e5084600f0d7343d6117aabcbc1ac78eb1ae11eb4125", size = 50604794, upload-time = "2026-02-16T10:12:11.797Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a1/22df0620a9fac31d68397a75465c344e83c3dfe521f7612aea33e27ab6c0/pyarrow-23.0.1-cp313-cp313t-win_amd64.whl", hash = "sha256:a35581e856a2fafa12f3f54fce4331862b1cfb0bef5758347a858a4aa9d6bae8", size = 27660642, upload-time = "2026-02-16T10:12:17.746Z" }, + { url = "https://files.pythonhosted.org/packages/8d/1b/6da9a89583ce7b23ac611f183ae4843cd3a6cf54f079549b0e8c14031e73/pyarrow-23.0.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:5df1161da23636a70838099d4aaa65142777185cc0cdba4037a18cee7d8db9ca", size = 34238755, upload-time = "2026-02-16T10:12:32.819Z" }, + { url = "https://files.pythonhosted.org/packages/ae/b5/d58a241fbe324dbaeb8df07be6af8752c846192d78d2272e551098f74e88/pyarrow-23.0.1-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:fa8e51cb04b9f8c9c5ace6bab63af9a1f88d35c0d6cbf53e8c17c098552285e1", size = 35847826, upload-time = "2026-02-16T10:12:38.949Z" }, + { url = "https://files.pythonhosted.org/packages/54/a5/8cbc83f04aba433ca7b331b38f39e000efd9f0c7ce47128670e737542996/pyarrow-23.0.1-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:0b95a3994f015be13c63148fef8832e8a23938128c185ee951c98908a696e0eb", size = 44536859, upload-time = "2026-02-16T10:12:45.467Z" }, + { url = "https://files.pythonhosted.org/packages/36/2e/c0f017c405fcdc252dbccafbe05e36b0d0eb1ea9a958f081e01c6972927f/pyarrow-23.0.1-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:4982d71350b1a6e5cfe1af742c53dfb759b11ce14141870d05d9e540d13bc5d1", size = 47614443, upload-time = "2026-02-16T10:12:55.525Z" }, + { url = "https://files.pythonhosted.org/packages/af/6b/2314a78057912f5627afa13ba43809d9d653e6630859618b0fd81a4e0759/pyarrow-23.0.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c250248f1fe266db627921c89b47b7c06fee0489ad95b04d50353537d74d6886", size = 48232991, upload-time = "2026-02-16T10:13:04.729Z" }, + { url = "https://files.pythonhosted.org/packages/40/f2/1bcb1d3be3460832ef3370d621142216e15a2c7c62602a4ea19ec240dd64/pyarrow-23.0.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5f4763b83c11c16e5f4c15601ba6dfa849e20723b46aa2617cb4bffe8768479f", size = 50645077, upload-time = "2026-02-16T10:13:14.147Z" }, + { url = "https://files.pythonhosted.org/packages/eb/3f/b1da7b61cd66566a4d4c8383d376c606d1c34a906c3f1cb35c479f59d1aa/pyarrow-23.0.1-cp314-cp314-win_amd64.whl", hash = "sha256:3a4c85ef66c134161987c17b147d6bffdca4566f9a4c1d81a0a01cdf08414ea5", size = 28234271, upload-time = "2026-02-16T10:14:09.397Z" }, + { url = "https://files.pythonhosted.org/packages/b5/78/07f67434e910a0f7323269be7bfbf58699bd0c1d080b18a1ab49ba943fe8/pyarrow-23.0.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:17cd28e906c18af486a499422740298c52d7c6795344ea5002a7720b4eadf16d", size = 34488692, upload-time = "2026-02-16T10:13:21.541Z" }, + { url = "https://files.pythonhosted.org/packages/50/76/34cf7ae93ece1f740a04910d9f7e80ba166b9b4ab9596a953e9e62b90fe1/pyarrow-23.0.1-cp314-cp314t-macosx_12_0_x86_64.whl", hash = "sha256:76e823d0e86b4fb5e1cf4a58d293036e678b5a4b03539be933d3b31f9406859f", size = 35964383, upload-time = "2026-02-16T10:13:28.63Z" }, + { url = "https://files.pythonhosted.org/packages/46/90/459b827238936d4244214be7c684e1b366a63f8c78c380807ae25ed92199/pyarrow-23.0.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:a62e1899e3078bf65943078b3ad2a6ddcacf2373bc06379aac61b1e548a75814", size = 44538119, upload-time = "2026-02-16T10:13:35.506Z" }, + { url = "https://files.pythonhosted.org/packages/28/a1/93a71ae5881e99d1f9de1d4554a87be37da11cd6b152239fb5bd924fdc64/pyarrow-23.0.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:df088e8f640c9fae3b1f495b3c64755c4e719091caf250f3a74d095ddf3c836d", size = 47571199, upload-time = "2026-02-16T10:13:42.504Z" }, + { url = "https://files.pythonhosted.org/packages/88/a3/d2c462d4ef313521eaf2eff04d204ac60775263f1fb08c374b543f79f610/pyarrow-23.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:46718a220d64677c93bc243af1d44b55998255427588e400677d7192671845c7", size = 48259435, upload-time = "2026-02-16T10:13:49.226Z" }, + { url = "https://files.pythonhosted.org/packages/cc/f1/11a544b8c3d38a759eb3fbb022039117fd633e9a7b19e4841cc3da091915/pyarrow-23.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a09f3876e87f48bc2f13583ab551f0379e5dfb83210391e68ace404181a20690", size = 50629149, upload-time = "2026-02-16T10:13:57.238Z" }, + { url = "https://files.pythonhosted.org/packages/50/f2/c0e76a0b451ffdf0cf788932e182758eb7558953f4f27f1aff8e2518b653/pyarrow-23.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:527e8d899f14bd15b740cd5a54ad56b7f98044955373a17179d5956ddb93d9ce", size = 28365807, upload-time = "2026-02-16T10:14:03.892Z" }, +] + +[[package]] +name = "pydantic" +version = "2.12.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, + { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, + { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, + { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, + { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, + { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, + { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, + { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, + { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, + { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, + { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, + { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, + { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, + { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, + { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, + { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, + { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, + { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + +[[package]] +name = "rapidfuzz" +version = "3.14.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/28/9d808fe62375b9aab5ba92fa9b29371297b067c2790b2d7cda648b1e2f8d/rapidfuzz-3.14.3.tar.gz", hash = "sha256:2491937177868bc4b1e469087601d53f925e8d270ccc21e07404b4b5814b7b5f", size = 57863900, upload-time = "2025-11-01T11:54:52.321Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/25/5b0a33ad3332ee1213068c66f7c14e9e221be90bab434f0cb4defa9d6660/rapidfuzz-3.14.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:dea2d113e260a5da0c4003e0a5e9fdf24a9dc2bb9eaa43abd030a1e46ce7837d", size = 1953885, upload-time = "2025-11-01T11:52:47.75Z" }, + { url = "https://files.pythonhosted.org/packages/2d/ab/f1181f500c32c8fcf7c966f5920c7e56b9b1d03193386d19c956505c312d/rapidfuzz-3.14.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e6c31a4aa68cfa75d7eede8b0ed24b9e458447db604c2db53f358be9843d81d3", size = 1390200, upload-time = "2025-11-01T11:52:49.491Z" }, + { url = "https://files.pythonhosted.org/packages/14/2a/0f2de974ececad873865c6bb3ea3ad07c976ac293d5025b2d73325aac1d4/rapidfuzz-3.14.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02821366d928e68ddcb567fed8723dad7ea3a979fada6283e6914d5858674850", size = 1389319, upload-time = "2025-11-01T11:52:51.224Z" }, + { url = "https://files.pythonhosted.org/packages/ed/69/309d8f3a0bb3031fd9b667174cc4af56000645298af7c2931be5c3d14bb4/rapidfuzz-3.14.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cfe8df315ab4e6db4e1be72c5170f8e66021acde22cd2f9d04d2058a9fd8162e", size = 3178495, upload-time = "2025-11-01T11:52:53.005Z" }, + { url = "https://files.pythonhosted.org/packages/10/b7/f9c44a99269ea5bf6fd6a40b84e858414b6e241288b9f2b74af470d222b1/rapidfuzz-3.14.3-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:769f31c60cd79420188fcdb3c823227fc4a6deb35cafec9d14045c7f6743acae", size = 1228443, upload-time = "2025-11-01T11:52:54.991Z" }, + { url = "https://files.pythonhosted.org/packages/f2/0a/3b3137abac7f19c9220e14cd7ce993e35071a7655e7ef697785a3edfea1a/rapidfuzz-3.14.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:54fa03062124e73086dae66a3451c553c1e20a39c077fd704dc7154092c34c63", size = 2411998, upload-time = "2025-11-01T11:52:56.629Z" }, + { url = "https://files.pythonhosted.org/packages/f3/b6/983805a844d44670eaae63831024cdc97ada4e9c62abc6b20703e81e7f9b/rapidfuzz-3.14.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:834d1e818005ed0d4ae38f6b87b86fad9b0a74085467ece0727d20e15077c094", size = 2530120, upload-time = "2025-11-01T11:52:58.298Z" }, + { url = "https://files.pythonhosted.org/packages/b4/cc/2c97beb2b1be2d7595d805682472f1b1b844111027d5ad89b65e16bdbaaa/rapidfuzz-3.14.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:948b00e8476a91f510dd1ec07272efc7d78c275d83b630455559671d4e33b678", size = 4283129, upload-time = "2025-11-01T11:53:00.188Z" }, + { url = "https://files.pythonhosted.org/packages/4d/03/2f0e5e94941045aefe7eafab72320e61285c07b752df9884ce88d6b8b835/rapidfuzz-3.14.3-cp311-cp311-win32.whl", hash = "sha256:43d0305c36f504232f18ea04e55f2059bb89f169d3119c4ea96a0e15b59e2a91", size = 1724224, upload-time = "2025-11-01T11:53:02.149Z" }, + { url = "https://files.pythonhosted.org/packages/cf/99/5fa23e204435803875daefda73fd61baeabc3c36b8fc0e34c1705aab8c7b/rapidfuzz-3.14.3-cp311-cp311-win_amd64.whl", hash = "sha256:ef6bf930b947bd0735c550683939a032090f1d688dfd8861d6b45307b96fd5c5", size = 1544259, upload-time = "2025-11-01T11:53:03.66Z" }, + { url = "https://files.pythonhosted.org/packages/48/35/d657b85fcc615a42661b98ac90ce8e95bd32af474603a105643963749886/rapidfuzz-3.14.3-cp311-cp311-win_arm64.whl", hash = "sha256:f3eb0ff3b75d6fdccd40b55e7414bb859a1cda77c52762c9c82b85569f5088e7", size = 814734, upload-time = "2025-11-01T11:53:05.008Z" }, + { url = "https://files.pythonhosted.org/packages/fa/8e/3c215e860b458cfbedb3ed73bc72e98eb7e0ed72f6b48099604a7a3260c2/rapidfuzz-3.14.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:685c93ea961d135893b5984a5a9851637d23767feabe414ec974f43babbd8226", size = 1945306, upload-time = "2025-11-01T11:53:06.452Z" }, + { url = "https://files.pythonhosted.org/packages/36/d9/31b33512015c899f4a6e6af64df8dfe8acddf4c8b40a4b3e0e6e1bcd00e5/rapidfuzz-3.14.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fa7c8f26f009f8c673fbfb443792f0cf8cf50c4e18121ff1e285b5e08a94fbdb", size = 1390788, upload-time = "2025-11-01T11:53:08.721Z" }, + { url = "https://files.pythonhosted.org/packages/a9/67/2ee6f8de6e2081ccd560a571d9c9063184fe467f484a17fa90311a7f4a2e/rapidfuzz-3.14.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:57f878330c8d361b2ce76cebb8e3e1dc827293b6abf404e67d53260d27b5d941", size = 1374580, upload-time = "2025-11-01T11:53:10.164Z" }, + { url = "https://files.pythonhosted.org/packages/30/83/80d22997acd928eda7deadc19ccd15883904622396d6571e935993e0453a/rapidfuzz-3.14.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c5f545f454871e6af05753a0172849c82feaf0f521c5ca62ba09e1b382d6382", size = 3154947, upload-time = "2025-11-01T11:53:12.093Z" }, + { url = "https://files.pythonhosted.org/packages/5b/cf/9f49831085a16384695f9fb096b99662f589e30b89b4a589a1ebc1a19d34/rapidfuzz-3.14.3-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:07aa0b5d8863e3151e05026a28e0d924accf0a7a3b605da978f0359bb804df43", size = 1223872, upload-time = "2025-11-01T11:53:13.664Z" }, + { url = "https://files.pythonhosted.org/packages/c8/0f/41ee8034e744b871c2e071ef0d360686f5ccfe5659f4fd96c3ec406b3c8b/rapidfuzz-3.14.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73b07566bc7e010e7b5bd490fb04bb312e820970180df6b5655e9e6224c137db", size = 2392512, upload-time = "2025-11-01T11:53:15.109Z" }, + { url = "https://files.pythonhosted.org/packages/da/86/280038b6b0c2ccec54fb957c732ad6b41cc1fd03b288d76545b9cf98343f/rapidfuzz-3.14.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6de00eb84c71476af7d3110cf25d8fe7c792d7f5fa86764ef0b4ca97e78ca3ed", size = 2521398, upload-time = "2025-11-01T11:53:17.146Z" }, + { url = "https://files.pythonhosted.org/packages/fa/7b/05c26f939607dca0006505e3216248ae2de631e39ef94dd63dbbf0860021/rapidfuzz-3.14.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d7843a1abf0091773a530636fdd2a49a41bcae22f9910b86b4f903e76ddc82dc", size = 4259416, upload-time = "2025-11-01T11:53:19.34Z" }, + { url = "https://files.pythonhosted.org/packages/40/eb/9e3af4103d91788f81111af1b54a28de347cdbed8eaa6c91d5e98a889aab/rapidfuzz-3.14.3-cp312-cp312-win32.whl", hash = "sha256:dea97ac3ca18cd3ba8f3d04b5c1fe4aa60e58e8d9b7793d3bd595fdb04128d7a", size = 1709527, upload-time = "2025-11-01T11:53:20.949Z" }, + { url = "https://files.pythonhosted.org/packages/b8/63/d06ecce90e2cf1747e29aeab9f823d21e5877a4c51b79720b2d3be7848f8/rapidfuzz-3.14.3-cp312-cp312-win_amd64.whl", hash = "sha256:b5100fd6bcee4d27f28f4e0a1c6b5127bc8ba7c2a9959cad9eab0bf4a7ab3329", size = 1538989, upload-time = "2025-11-01T11:53:22.428Z" }, + { url = "https://files.pythonhosted.org/packages/fc/6d/beee32dcda64af8128aab3ace2ccb33d797ed58c434c6419eea015fec779/rapidfuzz-3.14.3-cp312-cp312-win_arm64.whl", hash = "sha256:4e49c9e992bc5fc873bd0fff7ef16a4405130ec42f2ce3d2b735ba5d3d4eb70f", size = 811161, upload-time = "2025-11-01T11:53:23.811Z" }, + { url = "https://files.pythonhosted.org/packages/e4/4f/0d94d09646853bd26978cb3a7541b6233c5760687777fa97da8de0d9a6ac/rapidfuzz-3.14.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dbcb726064b12f356bf10fffdb6db4b6dce5390b23627c08652b3f6e49aa56ae", size = 1939646, upload-time = "2025-11-01T11:53:25.292Z" }, + { url = "https://files.pythonhosted.org/packages/b6/eb/f96aefc00f3bbdbab9c0657363ea8437a207d7545ac1c3789673e05d80bd/rapidfuzz-3.14.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1704fc70d214294e554a2421b473779bcdeef715881c5e927dc0f11e1692a0ff", size = 1385512, upload-time = "2025-11-01T11:53:27.594Z" }, + { url = "https://files.pythonhosted.org/packages/26/34/71c4f7749c12ee223dba90017a5947e8f03731a7cc9f489b662a8e9e643d/rapidfuzz-3.14.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc65e72790ddfd310c2c8912b45106e3800fefe160b0c2ef4d6b6fec4e826457", size = 1373571, upload-time = "2025-11-01T11:53:29.096Z" }, + { url = "https://files.pythonhosted.org/packages/32/00/ec8597a64f2be301ce1ee3290d067f49f6a7afb226b67d5f15b56d772ba5/rapidfuzz-3.14.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43e38c1305cffae8472572a0584d4ffc2f130865586a81038ca3965301f7c97c", size = 3156759, upload-time = "2025-11-01T11:53:30.777Z" }, + { url = "https://files.pythonhosted.org/packages/61/d5/b41eeb4930501cc899d5a9a7b5c9a33d85a670200d7e81658626dcc0ecc0/rapidfuzz-3.14.3-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:e195a77d06c03c98b3fc06b8a28576ba824392ce40de8c708f96ce04849a052e", size = 1222067, upload-time = "2025-11-01T11:53:32.334Z" }, + { url = "https://files.pythonhosted.org/packages/2a/7d/6d9abb4ffd1027c6ed837b425834f3bed8344472eb3a503ab55b3407c721/rapidfuzz-3.14.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1b7ef2f4b8583a744338a18f12c69693c194fb6777c0e9ada98cd4d9e8f09d10", size = 2394775, upload-time = "2025-11-01T11:53:34.24Z" }, + { url = "https://files.pythonhosted.org/packages/15/ce/4f3ab4c401c5a55364da1ffff8cc879fc97b4e5f4fa96033827da491a973/rapidfuzz-3.14.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a2135b138bcdcb4c3742d417f215ac2d8c2b87bde15b0feede231ae95f09ec41", size = 2526123, upload-time = "2025-11-01T11:53:35.779Z" }, + { url = "https://files.pythonhosted.org/packages/c1/4b/54f804975376a328f57293bd817c12c9036171d15cf7292032e3f5820b2d/rapidfuzz-3.14.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:33a325ed0e8e1aa20c3e75f8ab057a7b248fdea7843c2a19ade0008906c14af0", size = 4262874, upload-time = "2025-11-01T11:53:37.866Z" }, + { url = "https://files.pythonhosted.org/packages/e9/b6/958db27d8a29a50ee6edd45d33debd3ce732e7209183a72f57544cd5fe22/rapidfuzz-3.14.3-cp313-cp313-win32.whl", hash = "sha256:8383b6d0d92f6cd008f3c9216535be215a064b2cc890398a678b56e6d280cb63", size = 1707972, upload-time = "2025-11-01T11:53:39.442Z" }, + { url = "https://files.pythonhosted.org/packages/07/75/fde1f334b0cec15b5946d9f84d73250fbfcc73c236b4bc1b25129d90876b/rapidfuzz-3.14.3-cp313-cp313-win_amd64.whl", hash = "sha256:e6b5e3036976f0fde888687d91be86d81f9ac5f7b02e218913c38285b756be6c", size = 1537011, upload-time = "2025-11-01T11:53:40.92Z" }, + { url = "https://files.pythonhosted.org/packages/2e/d7/d83fe001ce599dc7ead57ba1debf923dc961b6bdce522b741e6b8c82f55c/rapidfuzz-3.14.3-cp313-cp313-win_arm64.whl", hash = "sha256:7ba009977601d8b0828bfac9a110b195b3e4e79b350dcfa48c11269a9f1918a0", size = 810744, upload-time = "2025-11-01T11:53:42.723Z" }, + { url = "https://files.pythonhosted.org/packages/92/13/a486369e63ff3c1a58444d16b15c5feb943edd0e6c28a1d7d67cb8946b8f/rapidfuzz-3.14.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a0a28add871425c2fe94358c6300bbeb0bc2ed828ca003420ac6825408f5a424", size = 1967702, upload-time = "2025-11-01T11:53:44.554Z" }, + { url = "https://files.pythonhosted.org/packages/f1/82/efad25e260b7810f01d6b69122685e355bed78c94a12784bac4e0beb2afb/rapidfuzz-3.14.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:010e12e2411a4854b0434f920e72b717c43f8ec48d57e7affe5c42ecfa05dd0e", size = 1410702, upload-time = "2025-11-01T11:53:46.066Z" }, + { url = "https://files.pythonhosted.org/packages/ba/1a/34c977b860cde91082eae4a97ae503f43e0d84d4af301d857679b66f9869/rapidfuzz-3.14.3-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cfc3d57abd83c734d1714ec39c88a34dd69c85474918ebc21296f1e61eb5ca8", size = 1382337, upload-time = "2025-11-01T11:53:47.62Z" }, + { url = "https://files.pythonhosted.org/packages/88/74/f50ea0e24a5880a9159e8fd256b84d8f4634c2f6b4f98028bdd31891d907/rapidfuzz-3.14.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:89acb8cbb52904f763e5ac238083b9fc193bed8d1f03c80568b20e4cef43a519", size = 3165563, upload-time = "2025-11-01T11:53:49.216Z" }, + { url = "https://files.pythonhosted.org/packages/e8/7a/e744359404d7737049c26099423fc54bcbf303de5d870d07d2fb1410f567/rapidfuzz-3.14.3-cp313-cp313t-manylinux_2_31_armv7l.whl", hash = "sha256:7d9af908c2f371bfb9c985bd134e295038e3031e666e4b2ade1e7cb7f5af2f1a", size = 1214727, upload-time = "2025-11-01T11:53:50.883Z" }, + { url = "https://files.pythonhosted.org/packages/d3/2e/87adfe14ce75768ec6c2b8acd0e05e85e84be4be5e3d283cdae360afc4fe/rapidfuzz-3.14.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:1f1925619627f8798f8c3a391d81071336942e5fe8467bc3c567f982e7ce2897", size = 2403349, upload-time = "2025-11-01T11:53:52.322Z" }, + { url = "https://files.pythonhosted.org/packages/70/17/6c0b2b2bff9c8b12e12624c07aa22e922b0c72a490f180fa9183d1ef2c75/rapidfuzz-3.14.3-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:152555187360978119e98ce3e8263d70dd0c40c7541193fc302e9b7125cf8f58", size = 2507596, upload-time = "2025-11-01T11:53:53.835Z" }, + { url = "https://files.pythonhosted.org/packages/c3/d1/87852a7cbe4da7b962174c749a47433881a63a817d04f3e385ea9babcd9e/rapidfuzz-3.14.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:52619d25a09546b8db078981ca88939d72caa6b8701edd8b22e16482a38e799f", size = 4273595, upload-time = "2025-11-01T11:53:55.961Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ab/1d0354b7d1771a28fa7fe089bc23acec2bdd3756efa2419f463e3ed80e16/rapidfuzz-3.14.3-cp313-cp313t-win32.whl", hash = "sha256:489ce98a895c98cad284f0a47960c3e264c724cb4cfd47a1430fa091c0c25204", size = 1757773, upload-time = "2025-11-01T11:53:57.628Z" }, + { url = "https://files.pythonhosted.org/packages/0b/0c/71ef356adc29e2bdf74cd284317b34a16b80258fa0e7e242dd92cc1e6d10/rapidfuzz-3.14.3-cp313-cp313t-win_amd64.whl", hash = "sha256:656e52b054d5b5c2524169240e50cfa080b04b1c613c5f90a2465e84888d6f15", size = 1576797, upload-time = "2025-11-01T11:53:59.455Z" }, + { url = "https://files.pythonhosted.org/packages/fe/d2/0e64fc27bb08d4304aa3d11154eb5480bcf5d62d60140a7ee984dc07468a/rapidfuzz-3.14.3-cp313-cp313t-win_arm64.whl", hash = "sha256:c7e40c0a0af02ad6e57e89f62bef8604f55a04ecae90b0ceeda591bbf5923317", size = 829940, upload-time = "2025-11-01T11:54:01.1Z" }, + { url = "https://files.pythonhosted.org/packages/32/6f/1b88aaeade83abc5418788f9e6b01efefcd1a69d65ded37d89cd1662be41/rapidfuzz-3.14.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:442125473b247227d3f2de807a11da6c08ccf536572d1be943f8e262bae7e4ea", size = 1942086, upload-time = "2025-11-01T11:54:02.592Z" }, + { url = "https://files.pythonhosted.org/packages/a0/2c/b23861347436cb10f46c2bd425489ec462790faaa360a54a7ede5f78de88/rapidfuzz-3.14.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1ec0c8c0c3d4f97ced46b2e191e883f8c82dbbf6d5ebc1842366d7eff13cd5a6", size = 1386993, upload-time = "2025-11-01T11:54:04.12Z" }, + { url = "https://files.pythonhosted.org/packages/83/86/5d72e2c060aa1fbdc1f7362d938f6b237dff91f5b9fc5dd7cc297e112250/rapidfuzz-3.14.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2dc37bc20272f388b8c3a4eba4febc6e77e50a8f450c472def4751e7678f55e4", size = 1379126, upload-time = "2025-11-01T11:54:05.777Z" }, + { url = "https://files.pythonhosted.org/packages/c9/bc/ef2cee3e4d8b3fc22705ff519f0d487eecc756abdc7c25d53686689d6cf2/rapidfuzz-3.14.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dee362e7e79bae940a5e2b3f6d09c6554db6a4e301cc68343886c08be99844f1", size = 3159304, upload-time = "2025-11-01T11:54:07.351Z" }, + { url = "https://files.pythonhosted.org/packages/a0/36/dc5f2f62bbc7bc90be1f75eeaf49ed9502094bb19290dfb4747317b17f12/rapidfuzz-3.14.3-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:4b39921df948388a863f0e267edf2c36302983459b021ab928d4b801cbe6a421", size = 1218207, upload-time = "2025-11-01T11:54:09.641Z" }, + { url = "https://files.pythonhosted.org/packages/df/7e/8f4be75c1bc62f47edf2bbbe2370ee482fae655ebcc4718ac3827ead3904/rapidfuzz-3.14.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:beda6aa9bc44d1d81242e7b291b446be352d3451f8217fcb068fc2933927d53b", size = 2401245, upload-time = "2025-11-01T11:54:11.543Z" }, + { url = "https://files.pythonhosted.org/packages/05/38/f7c92759e1bb188dd05b80d11c630ba59b8d7856657baf454ff56059c2ab/rapidfuzz-3.14.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:6a014ba09657abfcfeed64b7d09407acb29af436d7fc075b23a298a7e4a6b41c", size = 2518308, upload-time = "2025-11-01T11:54:13.134Z" }, + { url = "https://files.pythonhosted.org/packages/c7/ac/85820f70fed5ecb5f1d9a55f1e1e2090ef62985ef41db289b5ac5ec56e28/rapidfuzz-3.14.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:32eeafa3abce138bb725550c0e228fc7eaeec7059aa8093d9cbbec2b58c2371a", size = 4265011, upload-time = "2025-11-01T11:54:15.087Z" }, + { url = "https://files.pythonhosted.org/packages/46/a9/616930721ea9835c918af7cde22bff17f9db3639b0c1a7f96684be7f5630/rapidfuzz-3.14.3-cp314-cp314-win32.whl", hash = "sha256:adb44d996fc610c7da8c5048775b21db60dd63b1548f078e95858c05c86876a3", size = 1742245, upload-time = "2025-11-01T11:54:17.19Z" }, + { url = "https://files.pythonhosted.org/packages/06/8a/f2fa5e9635b1ccafda4accf0e38246003f69982d7c81f2faa150014525a4/rapidfuzz-3.14.3-cp314-cp314-win_amd64.whl", hash = "sha256:f3d15d8527e2b293e38ce6e437631af0708df29eafd7c9fc48210854c94472f9", size = 1584856, upload-time = "2025-11-01T11:54:18.764Z" }, + { url = "https://files.pythonhosted.org/packages/ef/97/09e20663917678a6d60d8e0e29796db175b1165e2079830430342d5298be/rapidfuzz-3.14.3-cp314-cp314-win_arm64.whl", hash = "sha256:576e4b9012a67e0bf54fccb69a7b6c94d4e86a9540a62f1a5144977359133583", size = 833490, upload-time = "2025-11-01T11:54:20.753Z" }, + { url = "https://files.pythonhosted.org/packages/03/1b/6b6084576ba87bf21877c77218a0c97ba98cb285b0c02eaaee3acd7c4513/rapidfuzz-3.14.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:cec3c0da88562727dd5a5a364bd9efeb535400ff0bfb1443156dd139a1dd7b50", size = 1968658, upload-time = "2025-11-01T11:54:22.25Z" }, + { url = "https://files.pythonhosted.org/packages/38/c0/fb02a0db80d95704b0a6469cc394e8c38501abf7e1c0b2afe3261d1510c2/rapidfuzz-3.14.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d1fa009f8b1100e4880868137e7bf0501422898f7674f2adcd85d5a67f041296", size = 1410742, upload-time = "2025-11-01T11:54:23.863Z" }, + { url = "https://files.pythonhosted.org/packages/a4/72/3fbf12819fc6afc8ec75a45204013b40979d068971e535a7f3512b05e765/rapidfuzz-3.14.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b86daa7419b5e8b180690efd1fdbac43ff19230803282521c5b5a9c83977655", size = 1382810, upload-time = "2025-11-01T11:54:25.571Z" }, + { url = "https://files.pythonhosted.org/packages/0f/18/0f1991d59bb7eee28922a00f79d83eafa8c7bfb4e8edebf4af2a160e7196/rapidfuzz-3.14.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7bd1816db05d6c5ffb3a4df0a2b7b56fb8c81ef584d08e37058afa217da91b1", size = 3166349, upload-time = "2025-11-01T11:54:27.195Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f0/baa958b1989c8f88c78bbb329e969440cf330b5a01a982669986495bb980/rapidfuzz-3.14.3-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:33da4bbaf44e9755b0ce192597f3bde7372fe2e381ab305f41b707a95ac57aa7", size = 1214994, upload-time = "2025-11-01T11:54:28.821Z" }, + { url = "https://files.pythonhosted.org/packages/e4/a0/cd12ec71f9b2519a3954febc5740291cceabc64c87bc6433afcb36259f3b/rapidfuzz-3.14.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3fecce764cf5a991ee2195a844196da840aba72029b2612f95ac68a8b74946bf", size = 2403919, upload-time = "2025-11-01T11:54:30.393Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ce/019bd2176c1644098eced4f0595cb4b3ef52e4941ac9a5854f209d0a6e16/rapidfuzz-3.14.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:ecd7453e02cf072258c3a6b8e930230d789d5d46cc849503729f9ce475d0e785", size = 2508346, upload-time = "2025-11-01T11:54:32.048Z" }, + { url = "https://files.pythonhosted.org/packages/23/f8/be16c68e2c9e6c4f23e8f4adbb7bccc9483200087ed28ff76c5312da9b14/rapidfuzz-3.14.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ea188aa00e9bcae8c8411f006a5f2f06c4607a02f24eab0d8dc58566aa911f35", size = 4274105, upload-time = "2025-11-01T11:54:33.701Z" }, + { url = "https://files.pythonhosted.org/packages/a1/d1/5ab148e03f7e6ec8cd220ccf7af74d3aaa4de26dd96df58936beb7cba820/rapidfuzz-3.14.3-cp314-cp314t-win32.whl", hash = "sha256:7ccbf68100c170e9a0581accbe9291850936711548c6688ce3bfb897b8c589ad", size = 1793465, upload-time = "2025-11-01T11:54:35.331Z" }, + { url = "https://files.pythonhosted.org/packages/cd/97/433b2d98e97abd9fff1c470a109b311669f44cdec8d0d5aa250aceaed1fb/rapidfuzz-3.14.3-cp314-cp314t-win_amd64.whl", hash = "sha256:9ec02e62ae765a318d6de38df609c57fc6dacc65c0ed1fd489036834fd8a620c", size = 1623491, upload-time = "2025-11-01T11:54:38.085Z" }, + { url = "https://files.pythonhosted.org/packages/e2/f6/e2176eb94f94892441bce3ddc514c179facb65db245e7ce3356965595b19/rapidfuzz-3.14.3-cp314-cp314t-win_arm64.whl", hash = "sha256:e805e52322ae29aa945baf7168b6c898120fbc16d2b8f940b658a5e9e3999253", size = 851487, upload-time = "2025-11-01T11:54:40.176Z" }, + { url = "https://files.pythonhosted.org/packages/c9/33/b5bd6475c7c27164b5becc9b0e3eb978f1e3640fea590dd3dced6006ee83/rapidfuzz-3.14.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7cf174b52cb3ef5d49e45d0a1133b7e7d0ecf770ed01f97ae9962c5c91d97d23", size = 1888499, upload-time = "2025-11-01T11:54:42.094Z" }, + { url = "https://files.pythonhosted.org/packages/30/d2/89d65d4db4bb931beade9121bc71ad916b5fa9396e807d11b33731494e8e/rapidfuzz-3.14.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:442cba39957a008dfc5bdef21a9c3f4379e30ffb4e41b8555dbaf4887eca9300", size = 1336747, upload-time = "2025-11-01T11:54:43.957Z" }, + { url = "https://files.pythonhosted.org/packages/85/33/cd87d92b23f0b06e8914a61cea6850c6d495ca027f669fab7a379041827a/rapidfuzz-3.14.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1faa0f8f76ba75fd7b142c984947c280ef6558b5067af2ae9b8729b0a0f99ede", size = 1352187, upload-time = "2025-11-01T11:54:45.518Z" }, + { url = "https://files.pythonhosted.org/packages/22/20/9d30b4a1ab26aac22fff17d21dec7e9089ccddfe25151d0a8bb57001dc3d/rapidfuzz-3.14.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1e6eefec45625c634926a9fd46c9e4f31118ac8f3156fff9494422cee45207e6", size = 3101472, upload-time = "2025-11-01T11:54:47.255Z" }, + { url = "https://files.pythonhosted.org/packages/b1/ad/fa2d3e5c29a04ead7eaa731c7cd1f30f9ec3c77b3a578fdf90280797cbcb/rapidfuzz-3.14.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56fefb4382bb12250f164250240b9dd7772e41c5c8ae976fd598a32292449cc5", size = 1511361, upload-time = "2025-11-01T11:54:49.057Z" }, +] + +[[package]] +name = "rich" +version = "14.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/c6/f3b320c27991c46f43ee9d856302c70dc2d0fb2dba4842ff739d5f46b393/rich-14.3.3.tar.gz", hash = "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b", size = 230582, upload-time = "2026-02-19T17:23:12.474Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[package]] +name = "thefuzz" +version = "0.22.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "rapidfuzz" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/81/4b/d3eb25831590d6d7d38c2f2e3561d3ba41d490dc89cd91d9e65e7c812508/thefuzz-0.22.1.tar.gz", hash = "sha256:7138039a7ecf540da323792d8592ef9902b1d79eb78c147d4f20664de79f3680", size = 19993, upload-time = "2024-01-19T19:18:23.135Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/4f/1695e70ceb3604f19eda9908e289c687ea81c4fecef4d90a9d1d0f2f7ae9/thefuzz-0.22.1-py3-none-any.whl", hash = "sha256:59729b33556850b90e1093c4cf9e618af6f2e4c985df193fdf3c5b5cf02ca481", size = 8245, upload-time = "2024-01-19T19:18:20.362Z" }, +] + +[[package]] +name = "typer" +version = "0.24.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/24/cb09efec5cc954f7f9b930bf8279447d24618bb6758d4f6adf2574c41780/typer-0.24.1.tar.gz", hash = "sha256:e39b4732d65fbdcde189ae76cf7cd48aeae72919dea1fdfc16593be016256b45", size = 118613, upload-time = "2026-02-21T16:54:40.609Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/91/48db081e7a63bb37284f9fbcefda7c44c277b18b0e13fbc36ea2335b71e6/typer-0.24.1-py3-none-any.whl", hash = "sha256:112c1f0ce578bfb4cab9ffdabc68f031416ebcc216536611ba21f04e9aa84c9e", size = 56085, upload-time = "2026-02-21T16:54:41.616Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "whichllm" +version = "0.5.15" +source = { editable = "." } +dependencies = [ + { name = "dbgpu", extra = ["fuzz"] }, + { name = "httpx" }, + { name = "nvidia-ml-py" }, + { name = "psutil" }, + { name = "rich" }, + { name = "typer" }, +] + +[package.dev-dependencies] +dev = [ + { name = "pyarrow" }, + { name = "pytest" }, +] + +[package.metadata] +requires-dist = [ + { name = "dbgpu", extras = ["fuzz"], specifier = ">=1.0" }, + { name = "httpx", specifier = ">=0.27" }, + { name = "nvidia-ml-py", specifier = ">=12.0" }, + { name = "psutil", specifier = ">=5.9" }, + { name = "rich", specifier = ">=13.0" }, + { name = "typer", specifier = ">=0.9" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "pyarrow", specifier = ">=23.0.1" }, + { name = "pytest", specifier = ">=8.0" }, +]