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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 51 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,10 @@ There are **two tables**, and the split is the point.
active parameters, architecture, native context, licence, capabilities and
quant preferences. Every row was checked against the publisher's own repository
or model card, and carries the `fact_method` used, a `fact_source` URL you can
open, and the `verified_at` date. The catalog is capped at 15 rows — it is
curated by hand, and a longer list cannot be kept honest.
open, and the `verified_at` date. The catalog is capped at 17 rows — it is
curated by hand, and a longer list cannot be kept honest. The cap can be
raised, but not quietly: `CATALOG_MAX_ROWS` carries the date and reason for
every change, and past roughly twenty the answer is to retire rows instead.

`catalog_ratings()` holds *judgements*: how good a model is at coding, with a
`rating_value`, `rating_date`, `rating_method`, `rating_source` and
Expand Down Expand Up @@ -130,6 +132,53 @@ Validate the table at any time:
bash -c 'source lib/models.sh; catalog_validate || printf "%s" "$CATALOG_ERRORS"'
```

### Laguna, and what a mirror is not

Two rows were added on 2026-08-12: **Laguna XS 2.1** (33.4B total, 2.7B active,
262k context) and **Laguna S 2.1** (117.6B total, 7.8B active, 1M context),
both MoE, both `openmdw-1.1`. Four things about them are worth stating, because
each is a place the table's assumptions had not been tested.

**They are Poolside's, not Unsloth's.** Unsloth mirrors S 2.1 as GGUF and is
where most people meet the model, but `canonical_repo` names the original
publisher — and Unsloth does not mirror XS 2.1 at all, so the mirror is not
even a consistent answer. Poolside publishes GGUFs for both itself.

**The active parameter counts are computed, not copied.** The names say A3B and
A8B; `config.json` says 2.7B and 7.8B. The formula is written above
`catalog_rows()` so you can repeat it. This matters because the speed score is
built on the active count, and rounding it up claims the model is slower than
it is.

**Unsloth's `UD-` quants are not the plain quants they are named after.** They
allocate bits per tensor, so `UD-Q2_K_XL` measures 2.70 bits per weight where
plain `Q2_K` is 3.35 — a 75 GB difference on a 118B model, in the direction
that says it fits. Only the four this repo references have bits-per-weight
entries, each measured from the published files rather than estimated; an
unlisted one fails loudly instead of guessing.

**Their ratings are `unknown` even though a vendor number exists.** Poolside
ships `.eval_results/swe-bench_verified.yaml` in the model repo claiming 70.9%
resolved for XS 2.1. No other row here has a SWE-bench figure, so recording it
would not rank Laguna against the catalog — it would rank *published a number*
against *did not*, and the result would look like a quality judgement while
measuring disclosure practice.

One consequence to be aware of: with every coding rating still `unknown`, the
quality term does no discriminating work, so the ranking runs on freshness,
hardware fit, speed and features. On a 31 GB machine that makes Laguna XS 2.1
the top `medium` pick ahead of `qwen3-coder-30b`, on metadata alone. That is
the existing design behaving as designed, and it is an argument for finishing
the local benchmark, not for hand-weighting the table.

Running them here: XS 2.1 at `Q4_K_M` is 18.9 GiB and needs both cards
(`-sm layer`); S 2.1 at `UD-Q4_K_XL` is 68.4 GiB and runs with the experts in
system RAM via `-ncmoe`, which is cheap precisely because only 7.8B parameters
are active per token. Support for the architecture reached mainline llama.cpp
in [PR #25165](https://github.com/ggml-org/llama.cpp/pull/25165), merged
2026-07-22 — a build older than that will not load either model. Poolside's
DFlash speculative decoding is *not* upstream and lives only on their fork.

### Live repository metadata

[`lib/hfmeta.sh`](lib/hfmeta.sh) fetches the things that actually change —
Expand Down
67 changes: 66 additions & 1 deletion lib/catalog.sh
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,15 @@ CATALOG_RATING_CONFIDENCE=(none low medium high)
# The cap is a design constraint, not an accident: this list is curated by
# hand, and a list longer than this cannot be kept honest. Enforced by
# catalog_validate.
CATALOG_MAX_ROWS=15
#
# Raised 15 -> 17 on 2026-08-12 for the two Laguna rows. Raising it is allowed;
# raising it QUIETLY is not, which is why the reason is written down. If this
# ever needs to go past ~20, the answer is to retire rows instead: re-verifying
# every fact on every row is the work the cap exists to keep finite.
#
# lib/select.sh keeps SELECT_MAX_LISTED equal to this, so that a model in the
# catalog is always a model the menu will actually offer.
CATALOG_MAX_ROWS=17

# --- the facts table --------------------------------------------------------
# id;canonical_repo;release_date;params_b;active_params_b;arch;context;license;capabilities;quant_prefs;fact_method;fact_source;verified_at
Expand All @@ -143,6 +151,20 @@ CATALOG_MAX_ROWS=15
# one decimal. It is not the number in the model's name: Qwen3-1.7B totals
# 2.0B once embeddings are counted, and the size estimate has to weigh the
# bytes that actually get loaded.
#
# `active_params_b` for a MoE follows the same rule, and for the same reason it
# is COMPUTED from config.json rather than copied out of the model's name. The
# name rounds; the speed score does not want a rounded number. From config:
#
# per layer attention h*(nq + 2*nkv)*hd + nq*hd*h
# routed top_k * 3 * h * moe_intermediate_size
# shared 3 * h * shared_expert_intermediate_size
# router h * num_experts
# plus embeddings vocab * h, twice when tie_word_embeddings is false
#
# For Laguna XS 2.1 that gives 2.7B where the name says A3B, and for S 2.1
# 7.8B where the name says A8B. Both check out against the total the same
# formula predicts, to within 1.5% of the safetensors count.
catalog_rows() {
# Comments and blank lines are stripped so the table can be annotated.
sed -e 's/[[:space:]]*#.*$//' -e '/^[[:space:]]*$/d' <<'CATALOG'
Expand All @@ -161,6 +183,16 @@ gemma-3-12b;google/gemma-3-12b-it;2025-03-01;12.2;12.2;dense;131072;gemma;vision
llama-3.3-70b;meta-llama/Llama-3.3-70B-Instruct;2024-12-06;70.6;70.6;dense;131072;llama3.3;tools;IQ3_M|IQ3_XXS;card-stated;https://huggingface.co/meta-llama/Llama-3.3-70B-Instruct;2026-08-11
mistral-small-3.2;mistralai/Mistral-Small-3.2-24B-Instruct-2506;2025-06-19;24.0;24.0;dense;131072;apache-2.0;tools,vision;IQ4_XS|Q4_K_S;hf-api;https://huggingface.co/api/models/mistralai/Mistral-Small-3.2-24B-Instruct-2506;2026-08-11
phi-4;microsoft/phi-4;2024-12-11;14.7;14.7;dense;16384;mit;reasoning;Q4_K_M|IQ4_XS;hf-api;https://huggingface.co/api/models/microsoft/phi-4;2026-08-11
# Laguna is Poolside's, not Unsloth's. Unsloth mirrors S 2.1 as GGUF and does
# NOT mirror XS 2.1 at all, so canonical_repo points at Poolside either way --
# which is the rule anyway, but here it is easy to get wrong.
#
# Neither row carries a plain-Q4 alternative because neither publisher shipped
# one. XS has exactly two GGUFs, Q4_K_M and BF16; S is UD quants throughout.
# A fallback naming a quant that does not exist is worse than no fallback: it
# turns "this repo has no Q4" into a download of something else.
laguna-xs-2.1;poolside/Laguna-XS-2.1;2026-06-20;33.4;2.7;moe;262144;openmdw-1.1;coding,agentic,tools,reasoning;Q4_K_M;hf-api;https://huggingface.co/api/models/poolside/Laguna-XS-2.1;2026-08-12
laguna-s-2.1;poolside/Laguna-S-2.1;2026-07-13;117.6;7.8;moe;1048576;openmdw-1.1;coding,agentic,tools,reasoning;UD-Q4_K_XL|UD-Q3_K_XL;hf-api;https://huggingface.co/api/models/poolside/Laguna-S-2.1;2026-08-12
CATALOG
}

Expand Down Expand Up @@ -207,6 +239,20 @@ gemma-3-12b;unknown;-;none;-;none
llama-3.3-70b;unknown;-;none;-;none
mistral-small-3.2;unknown;-;none;-;none
phi-4;unknown;-;none;-;none
# Both Laguna rows say `unknown` even though a vendor number was available and
# would have validated: Poolside ships .eval_results/swe-bench_verified.yaml in
# the model repo, claiming 70.9% resolved for XS 2.1.
#
# It is not recorded, and the reason is the one at the top of this table. No
# other row here has a SWE-bench figure. Entering one for the only two rows
# that have it does not rank Laguna against the catalog -- it ranks "published
# a number" against "did not", and the ordering that falls out would look like
# a quality judgement while measuring disclosure practice.
#
# When lib/bench.sh has run these two on this machine, the same suite that ran
# every other row, they get a local-benchmark rating like everything else.
laguna-xs-2.1;unknown;-;none;-;none
laguna-s-2.1;unknown;-;none;-;none
RATINGS
}

Expand Down Expand Up @@ -317,6 +363,25 @@ catalog_quant_bpw_x100() {
Q6_K) printf '656' ;;
Q8_0) printf '850' ;;
F16|FP16) printf '1600' ;;
# Unsloth "UD" dynamic quants. These are NOT the same as the plain quant of
# the same name: the bits are allocated per tensor by an importance pass, so
# UD-Q2_K_XL lands at 2.70 bpw where plain Q2_K is 3.35. Guessing one from
# the other would misestimate a 118B model by 75 GB.
#
# Measured, not estimated -- summed file bytes from the published GGUF repo
# divided by the safetensors parameter count of the source model:
#
# unsloth/Laguna-S-2.1-GGUF over 117,561,977,600 params, 2026-08-12
# UD-Q2_K_XL 39,685 MB 2.701 UD-Q3_K_XL 54,094 MB 3.681
# UD-IQ3_XXS 44,283 MB 3.013 UD-Q4_K_XL 73,395 MB 4.994
#
# Only the four this repo actually references are listed. An unlisted quant
# returning status 1 is the correct outcome: it makes catalog_est_size_mb
# fail loudly rather than size a download off a number nobody measured.
UD-Q2_K_XL) printf '270' ;;
UD-IQ3_XXS) printf '301' ;;
UD-Q3_K_XL) printf '368' ;;
UD-Q4_K_XL) printf '499' ;;
*) return 1 ;;
esac
}
Expand Down
16 changes: 11 additions & 5 deletions lib/select.sh
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,17 @@ _LLMRIG_SELECT_SH=1
# shellcheck source=lib/score.sh
source "$(dirname "${BASH_SOURCE[0]}")/score.sh"

# The menu never offers more than this many models. The catalog is capped at 15
# rows, so today the two are the same number -- but a user reads a list of
# fifteen, and would not read a list of fifty, so the limit is stated here as
# well rather than inherited by accident.
SELECT_MAX_LISTED=15
# The menu never offers more than this many models. Kept EQUAL to
# CATALOG_MAX_ROWS -- a test enforces it -- because selector_build drops
# everything past this cap silently, and a model that is in the catalog but
# never reaches the menu is invisible in the one place a user would look for it.
#
# The number is still written here rather than read from the catalog: it is a
# statement about what a person will read, and if the catalog ever grows to
# fifty the right response is to paginate the menu, not to print fifty lines.
# Two limits that must agree, and a test that says so, makes that a decision
# rather than an accident.
SELECT_MAX_LISTED=17

# How many models can be chosen at once. Downloading more than three at a time
# is a request to fill the disk.
Expand Down
106 changes: 106 additions & 0 deletions tests/cases/catalog_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -749,5 +749,111 @@ test_every_row_cites_a_source_on_the_publishers_own_domain() {
return 0
}

# --- the Laguna rows --------------------------------------------------------
# These two are the first rows whose publisher is not one of the four the
# catalog started with, the first under a licence other than the usual set, and
# the first to use a quantizer's dynamic quant names. Each of those is a place
# the table's assumptions could quietly not hold.

test_laguna_is_attributed_to_poolside_not_to_its_quantizer() {
# The likeliest wrong edit here: Unsloth mirrors Laguna S 2.1 as GGUF and is
# where most people first meet the model, so `unsloth/...` reads as the
# obvious repo. canonical_repo means the ORIGINAL publisher, and Unsloth does
# not mirror XS 2.1 at all, so the mirror is not even a consistent answer.
assert_eq "$(catalog_get laguna-xs-2.1 canonical_repo)" "poolside/Laguna-XS-2.1" "XS publisher" || return 1
assert_eq "$(catalog_get laguna-s-2.1 canonical_repo)" "poolside/Laguna-S-2.1" "S publisher" || return 1

# And the fact_source with it: citing the mirror would make the row
# unverifiable against the publisher, which is what fact_source is for.
local id src
for id in laguna-xs-2.1 laguna-s-2.1; do
src="$(catalog_get "$id" fact_source)"
[[ "$src" != *unsloth* ]] \
|| { _fail "$id cites the Unsloth mirror ($src) as its source of fact"; return 1; }
done
return 0
}

test_laguna_facts_match_the_publishers_config() {
# Every number here is re-checkable with one curl against the fact_source.
assert_eq "$(catalog_get laguna-xs-2.1 params_b)" "33.4" "XS total params" || return 1
assert_eq "$(catalog_get laguna-xs-2.1 context)" "262144" "XS context" || return 1
assert_eq "$(catalog_get laguna-s-2.1 params_b)" "117.6" "S total params" || return 1
assert_eq "$(catalog_get laguna-s-2.1 context)" "1048576" "S context" || return 1
assert_eq "$(catalog_get laguna-s-2.1 license)" "openmdw-1.1" "S licence, as declared"
}

test_laguna_active_params_are_computed_not_copied_from_the_name() {
# The names say A3B and A8B. config.json says 2.7B and 7.8B, and the speed
# score is only as good as this number. A row that rounds up here claims the
# model is slower than it is.
assert_eq "$(catalog_get laguna-xs-2.1 active_params_b)" "2.7" "XS active" || return 1
assert_eq "$(catalog_get laguna-s-2.1 active_params_b)" "7.8" "S active"
}

test_laguna_size_estimates_match_the_published_gguf_bytes() {
# The estimator is only trustworthy if it agrees with reality, and these two
# rows are the check: measured totals from the GGUF repos on 2026-08-12 are
# 20,274 MB (XS Q4_K_M) and 73,395 MB (S UD-Q4_K_XL). Both must land within
# 3%, which is tighter than any decision made on the number.
local id quant want est lo hi
while read -r id quant want; do
est="$(catalog_est_size_mb "$id" "$quant")" || { _fail "no estimate for $id at $quant"; return 1; }
lo=$(( want * 97 / 100 )); hi=$(( want * 103 / 100 ))
(( est >= lo && est <= hi )) \
|| { _fail "$id at $quant estimated $est MB, measured $want MB -- outside 3%"; return 1; }
done <<'MEASURED'
laguna-xs-2.1 Q4_K_M 20274
laguna-s-2.1 UD-Q4_K_XL 73395
MEASURED
return 0
}

test_ud_quants_are_not_confused_with_their_plain_namesakes() {
# Unsloth's dynamic quants allocate bits per tensor, so UD-Q2_K_XL is 2.70
# bpw where plain Q2_K is 3.35. Reusing the plain figure would misestimate
# Laguna S by tens of gigabytes -- in the direction that says it fits.
local ud plain
ud="$(catalog_quant_bpw_x100 UD-Q2_K_XL)"
plain="$(catalog_quant_bpw_x100 Q2_K)"
assert_ne "$ud" "$plain" "UD-Q2_K_XL must not inherit Q2_K's bits-per-weight" || return 1
assert_lt "$ud" "$plain" "the dynamic quant is the smaller of the two"
}

test_an_unmeasured_ud_quant_fails_rather_than_guessing() {
# Only the four UD quants this repo references are listed. An unlisted one
# must fail, not fall through to a plausible default -- sizing a 70 GB
# download off an invented number is the failure this table exists to avoid.
if catalog_quant_bpw_x100 UD-Q5_K_XL >/dev/null 2>&1; then
_fail "UD-Q5_K_XL is not measured here and must not return a bits-per-weight"
return 1
fi
return 0
}

test_a_row_may_name_a_single_quant_with_no_fallback() {
# Poolside publishes exactly two GGUFs of XS 2.1 -- Q4_K_M and BF16 -- so the
# row has no second preference. An alternation is the norm, not a
# requirement, and inventing an IQ4_XS fallback that the repo does not carry
# would make select_quant_file match some other file instead of failing.
assert_eq "$(catalog_get laguna-xs-2.1 quant_prefs)" "Q4_K_M" "no fallback, because there is none" || return 1
assert_eq "$(catalog_preferred_quant laguna-xs-2.1)" "Q4_K_M" "and it resolves" || return 1
quant_pattern_valid "Q4_K_M" || { _fail "a single-alternative pattern must be valid"; return 1; }
return 0
}

test_laguna_ratings_stay_unknown_despite_a_published_vendor_number() {
# Poolside ships .eval_results/swe-bench_verified.yaml claiming 70.9% for XS.
# No other row in this catalog has a SWE-bench figure, so recording it would
# rank disclosure practice rather than quality. The rule has to hold when
# there IS a number available, or it is not a rule.
local id
for id in laguna-xs-2.1 laguna-s-2.1; do
assert_eq "$(catalog_rating_get "$id" rating_value)" "unknown" "$id rating" || return 1
assert_eq "$(catalog_rating_get "$id" rating_method)" "none" "$id method" || return 1
done
return 0
}

run_suite
suite_exit
53 changes: 47 additions & 6 deletions tests/cases/score_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -344,11 +344,48 @@ test_the_breakdown_shows_a_real_rating_when_one_exists() {
# --- ranking ----------------------------------------------------------------

test_ranking_groups_by_size_class_largest_first() {
local classes
# Asserted as an INVARIANT over whatever classes the catalog produces, not as
# a fixed list of three. The fixed list was really an assertion that no
# catalogued model is unrunnable at 20 GB with no offload, which was true
# until a 117.6B row existed and then failed here rather than where the
# assumption lived.
#
# The contract score_rank documents is: each class appears once, as a
# contiguous group, in this order. Unsupported last, so a user can see why a
# model they expected is missing.
local canonical=" large medium small unsupported " classes c pos prev=0 seen=" "
classes="$(score_rank 20000 | cut -f1 | uniq)"
assert_eq "$classes" "large
medium
small" "classes must appear in size order, not alphabetically"
[[ -n "$classes" ]] || { _fail "score_rank emitted nothing"; return 1; }

while IFS= read -r c; do
[[ "$seen" != *" $c "* ]] \
|| { _fail "class '$c' appears in two separate groups -- not contiguous"; return 1; }
seen+="$c "

# Position of this class within the canonical order.
pos=0
case "$c" in
large) pos=1 ;; medium) pos=2 ;; small) pos=3 ;; unsupported) pos=4 ;;
*) _fail "unknown size class '$c' (expected one of:$canonical)"; return 1 ;;
esac
(( pos > prev )) \
|| { _fail "class '$c' came after a later class -- got: $(tr '\n' ' ' <<<"$classes")"; return 1; }
prev="$pos"
done <<<"$classes"
return 0
}

test_a_model_too_big_for_ram_and_vram_is_unsupported_not_merely_large() {
# The catalog now contains a model that genuinely does not fit a 20 GB card
# with nothing to offload into, which is what makes the branch above
# reachable with real data rather than only with a synthetic table.
local no_offload with_offload
no_offload="$(score_rank 20000 0 | awk -F'\t' '$3 == "laguna-s-2.1" { print $1 }')"
assert_eq "$no_offload" "unsupported" "117.6B on a 20 GB card with no RAM to spill into" || return 1

# Same card, but with system RAM for the experts: it becomes merely large.
with_offload="$(score_rank 20000 96000 | awk -F'\t' '$3 == "laguna-s-2.1" { print $1 }')"
assert_ne "$with_offload" "unsupported" "the same model with 96 GB of offload must be runnable"
}

test_scores_descend_within_each_class() {
Expand Down Expand Up @@ -460,9 +497,13 @@ test_ties_on_score_and_popularity_fall_back_to_the_id() {
}

test_the_best_per_class_is_the_head_of_each_group() {
local best full
local best full want
best="$(score_best_per_class 20000)"
assert_eq "$(printf '%s\n' "$best" | wc -l)" "3" "one line per size class" || return 1
# One line per class the ranking actually produced -- checked against the
# ranking rather than against a hardcoded 3, which silently encoded "this
# catalog has no unrunnable model" into a test about grouping.
want="$(score_rank 20000 | cut -f1 | sort -u | wc -l)"
assert_eq "$(printf '%s\n' "$best" | wc -l)" "$want" "one line per size class" || return 1
full="$(score_rank 20000 | awk -F'\t' '$1=="medium" { print; exit }')"
assert_contains "$best" "$(printf '%s' "$full" | cut -f3)" \
"the medium winner must match the top of the medium group"
Expand Down
Loading