feat(models): add the Qwen3.8 family and fix Qwen generation parsing (#893)

Pin all four official Qwen3.8 repos in the curated catalog:

  Qwen/Qwen3.8-27B             27.78B dense, 64 layers (16 full / 48 linear)
  Qwen/Qwen3.8-27B-FP8         same weights, FP8 checkpoint
  Qwen/Qwen3.8-2.4T-A95B       2.446T total / 95B active, 512 experts (10 active),
                               92 layers (23 full / 69 linear)
  Qwen/Qwen3.8-2.4T-A95B-FP8   same, FP8 checkpoint

The automated weekly refresh in #892 picked up three of these via trending
discovery, but none were in TARGET_MODELS, so they could drop out of any
later scrape; the non-FP8 2.4T-A95B was missing entirely. Catalog entries
were merged in surgically (four entries) rather than by a full re-scrape so
this diff stays reviewable alongside #892.

infer_attention_layout_from_name now matches qwen3.8- and handles the new
-a95b 92-layer shape, and OLLAMA_MAPPINGS gains qwen3.8-27b -> qwen3.8:27b
(Ollama publishes only the 27B; the 2.4T has no library entry).

Also fixes a generation-scoring bug this surfaced. parse_generation
consulted the architecture string before the model name, but Qwen3.6 and
Qwen3.8 both ship as model_type "qwen3_5" -- so every Qwen3.6 model in the
catalog scored as generation 3.5, and the name-based 3.6 branch added in
5397a8a was dead code for anything with an architecture set. The existing
test passed None for architecture, so it never caught this.

qwen_minor_generation_from_name is now consulted first inside the Qwen arch
branch. It matches only explicit minor versions and returns None for a bare
"Qwen3", so qwen3_next still resolves from the architecture string instead
of regressing to 3.0. scripts/validate_generation_scoring.py, which
documents itself as a mirror of the Rust logic, is updated to match.

Quality bonus is (generation - 1) * 3 clamped to 9, so Qwen3.6 models gain
+0.3 and Qwen3.8 lands at +8.4.
This commit is contained in:
Alex Jones 2026-08-17 07:57:27 +01:00 • committed by GitHub
parent a9ac7ed91c
commit 6e024b2e51
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 283 additions and 18 deletions

View file

@ -1,6 +1,6 @@
# Supported Models
llmfit ships with a curated database of 106 LLM models from HuggingFace. All memory estimates assume Q4_K_M quantization (0.5 bytes per parameter) unless noted otherwise.
llmfit ships with a curated database of 108 LLM models from HuggingFace. All memory estimates assume Q4_K_M quantization (0.5 bytes per parameter) unless noted otherwise.
### 01.ai
@ -34,6 +34,7 @@ llmfit ships with a curated database of 106 LLM models from HuggingFace. All mem
| [Qwen/Qwen3-14B](https://huggingface.co/Qwen/Qwen3-14B) | 14.8B | Q4_K_M | 128k | General purpose text generation |
| [Qwen/Qwen2.5-Coder-14B-Instruct](https://huggingface.co/Qwen/Qwen2.5-Coder-14B-Instruct) | 14.8B | Q4_K_M | 32k | Code generation and completion |
| [Qwen/Qwen3.5-27B](https://huggingface.co/Qwen/Qwen3.5-27B) | 27.8B | Q4_K_M | 256k | Multimodal, vision and text |
| [Qwen/Qwen3.8-27B](https://huggingface.co/Qwen/Qwen3.8-27B) | 27.8B | Q4_K_M | 256k | Multimodal, vision and text |
| [Qwen/Qwen3-30B-A3B](https://huggingface.co/Qwen/Qwen3-30B-A3B) | 30.5B (MoE) | Q4_K_M | 40k | Efficient MoE, general purpose |
| [Qwen/Qwen3.5-35B-A3B](https://huggingface.co/Qwen/Qwen3.5-35B-A3B) | 36.0B (MoE) | Q4_K_M | 256k | Multimodal, vision and text |
| [Qwen/Qwen2.5-32B-Instruct](https://huggingface.co/Qwen/Qwen2.5-32B-Instruct) | 32.5B | Q4_K_M | 128k | Instruction following, chat |
@ -44,6 +45,7 @@ llmfit ships with a curated database of 106 LLM models from HuggingFace. All mem
| [Qwen/Qwen3-235B-A22B](https://huggingface.co/Qwen/Qwen3-235B-A22B) | 235B (MoE) | Q4_K_M | 40k | State-of-the-art, MoE architecture |
| [Qwen/Qwen3.5-397B-A17B](https://huggingface.co/Qwen/Qwen3.5-397B-A17B) | 403.4B (MoE) | Q4_K_M | 256k | Multimodal, vision and text |
| [Qwen/Qwen3-Coder-480B-A35B-Instruct](https://huggingface.co/Qwen/Qwen3-Coder-480B-A35B-Instruct) | 480B (MoE) | Q4_K_M | 256k | Code generation and completion |
| [Qwen/Qwen3.8-2.4T-A95B](https://huggingface.co/Qwen/Qwen3.8-2.4T-A95B) | 2446.2B (MoE) | Q4_K_M | 256k | General purpose text generation |
### Allen Institute

View file

@ -274430,6 +274430,88 @@
}
]
},
{
"name": "Qwen/Qwen3.8-27B",
"provider": "Alibaba",
"parameter_count": "27.8B",
"parameters_raw": 27781427952,
"min_ram_gb": 15.5,
"recommended_ram_gb": 25.9,
"min_vram_gb": 14.2,
"quantization": "Q4_K_M",
"format": "gguf",
"context_length": 262144,
"use_case": "Multimodal, vision and text",
"capabilities": [
"vision",
"tool_use"
],
"languages": [],
"pipeline_tag": "image-text-to-text",
"architecture": "qwen3_5",
"hf_downloads": 267725,
"hf_likes": 10385,
"release_date": "2026-08-05",
"num_hidden_layers": 64,
"num_attention_heads": 24,
"num_key_value_heads": 4,
"head_dim": 256,
"hidden_size": 5120,
"vocab_size": 248320,
"moe_intermediate_size": 17408,
"shared_expert_intermediate_size": null,
"license": "apache-2.0",
"gguf_sources": [
{
"repo": "unsloth/Qwen3.8-27B-GGUF",
"provider": "unsloth"
},
{
"repo": "bartowski/Qwen3.8-27B-GGUF",
"provider": "bartowski"
},
{
"repo": "ggml-org/Qwen3.8-27B-GGUF",
"provider": "ggml-org"
},
{
"repo": "mradermacher/Qwen3.8-27B-GGUF",
"provider": "mradermacher"
}
]
},
{
"name": "Qwen/Qwen3.8-27B-FP8",
"provider": "Alibaba",
"parameter_count": "27.8B",
"parameters_raw": 27781427952,
"min_ram_gb": 15.5,
"recommended_ram_gb": 25.9,
"min_vram_gb": 14.2,
"quantization": "Q4_K_M",
"format": "gguf",
"context_length": 262144,
"use_case": "Multimodal, vision and text",
"capabilities": [
"vision",
"tool_use"
],
"languages": [],
"pipeline_tag": "image-text-to-text",
"architecture": "qwen3_5",
"hf_downloads": 352971,
"hf_likes": 494,
"release_date": "2026-08-13",
"num_hidden_layers": 64,
"num_attention_heads": 24,
"num_key_value_heads": 4,
"head_dim": 256,
"hidden_size": 5120,
"vocab_size": 248320,
"moe_intermediate_size": 17408,
"shared_expert_intermediate_size": null,
"license": "apache-2.0"
},
{
"name": "cbert33/Qwen3.6-27B-Fable-Fusion-711-Uncensored-Heretic-FP8-KV-Calibrated",
"provider": "cbert33",
@ -345304,6 +345386,82 @@
"_discovered": true,
"license": "kimi-k3"
},
{
"name": "Qwen/Qwen3.8-2.4T-A95B",
"provider": "Alibaba",
"parameter_count": "2446.2B",
"parameters_raw": 2446182725504,
"min_ram_gb": 1366.9,
"recommended_ram_gb": 2278.2,
"min_vram_gb": 1253.0,
"quantization": "Q4_K_M",
"format": "gguf",
"context_length": 262144,
"use_case": "General purpose text generation",
"capabilities": [
"tool_use"
],
"languages": [],
"pipeline_tag": "text-generation",
"architecture": "qwen3_5_moe_text",
"hf_downloads": 7932,
"hf_likes": 1016,
"release_date": "2026-08-08",
"num_hidden_layers": 92,
"num_attention_heads": 64,
"num_key_value_heads": 4,
"head_dim": 256,
"hidden_size": 8192,
"vocab_size": 248320,
"moe_intermediate_size": 2048,
"shared_expert_intermediate_size": 2048,
"license": "qwen3.8-max",
"is_moe": true,
"num_experts": 512,
"active_experts": 10,
"active_parameters": 95000000000,
"gguf_sources": [
{
"repo": "unsloth/Qwen3.8-2.4T-A95B-GGUF",
"provider": "unsloth"
}
]
},
{
"name": "Qwen/Qwen3.8-2.4T-A95B-FP8",
"provider": "Alibaba",
"parameter_count": "2446.2B",
"parameters_raw": 2446182725504,
"min_ram_gb": 1366.9,
"recommended_ram_gb": 2278.2,
"min_vram_gb": 1253.0,
"quantization": "Q4_K_M",
"format": "gguf",
"context_length": 262144,
"use_case": "General purpose text generation",
"capabilities": [
"tool_use"
],
"languages": [],
"pipeline_tag": "text-generation",
"architecture": "qwen3_5_moe_text",
"hf_downloads": 11311,
"hf_likes": 210,
"release_date": "2026-08-08",
"num_hidden_layers": 92,
"num_attention_heads": 64,
"num_key_value_heads": 4,
"head_dim": 256,
"hidden_size": 8192,
"vocab_size": 248320,
"moe_intermediate_size": 2048,
"shared_expert_intermediate_size": 2048,
"license": "qwen3.8-max",
"is_moe": true,
"num_experts": 512,
"active_experts": 10,
"active_parameters": 95000000000
},
{
"name": "moonshotai/Kimi-K3",
"provider": "moonshotai",

View file

@ -114,6 +114,26 @@ pub fn quant_quality_penalty(quant: &str) -> f64 {
}
}
/// Explicit Qwen minor version spelled out in a repo name (`Qwen3.8-27B` → 3.8).
///
/// Only matches names that carry the minor version. A bare `Qwen3` returns
/// `None` so callers can fall back to the architecture string, which is what
/// distinguishes variants like `qwen3_next`.
fn qwen_minor_generation_from_name(name_lower: &str) -> Option<f64> {
const VERSIONS: &[(&str, &str, f64)] = &[
("qwen3.8", "qwen3_8", 3.8),
("qwen3.6", "qwen3_6", 3.6),
("qwen3.5", "qwen3_5", 3.5),
("qwen2.5", "qwen2_5", 2.5),
];
VERSIONS
.iter()
.find(|(dotted, underscored, _)| {
name_lower.contains(dotted) || name_lower.contains(underscored)
})
.map(|(_, _, generation)| *generation)
}
/// Parse model generation from architecture string and model name.
///
/// Returns a generation number (e.g. 2.0 for "qwen2", 3.5 for "qwen3_5_moe",
@ -135,6 +155,11 @@ pub fn parse_generation(architecture: Option<&str>, name: &str) -> Option<f64> {
}
// Qwen: qwen2, qwen3, qwen3_moe, qwen3_5, qwen3_5_moe, qwen3_next
if let Some(suffix) = arch_lower.strip_prefix("qwen") {
// Qwen3.6 and Qwen3.8 ship under the `qwen3_5` architecture string,
// so an explicit minor version in the repo name wins over the arch.
if let Some(generation) = qwen_minor_generation_from_name(&name.to_lowercase()) {
return Some(generation);
}
if suffix.starts_with("3_5") || suffix.starts_with("3.5") {
return Some(3.5);
}
@ -218,19 +243,13 @@ pub fn parse_generation(architecture: Option<&str>, name: &str) -> Option<f64> {
// Fallback: parse generation from model name
let name_lower = name.to_lowercase();
// Qwen3.6, Qwen3.5, Qwen3, Qwen2.5, Qwen2
if name_lower.contains("qwen3.6") || name_lower.contains("qwen3_6") {
return Some(3.6);
}
if name_lower.contains("qwen3.5") || name_lower.contains("qwen3_5") {
return Some(3.5);
// Qwen3.8, Qwen3.6, Qwen3.5, Qwen3, Qwen2.5, Qwen2
if let Some(generation) = qwen_minor_generation_from_name(&name_lower) {
return Some(generation);
}
if name_lower.contains("qwen3") {
return Some(3.0);
}
if name_lower.contains("qwen2.5") || name_lower.contains("qwen2_5") {
return Some(2.5);
}
if name_lower.contains("qwen2") {
return Some(2.0);
}
@ -1572,10 +1591,18 @@ pub fn infer_attention_layout_from_name(name: &str) -> Option<AttentionLayout> {
});
}
// Qwen3.5 / Qwen3.6 hybrid models use 1 full attention per 4 layers.
// Qwen3.5 / Qwen3.6 / Qwen3.8 hybrid models use 1 full attention per 4
// layers (`full_attention_interval: 4` in their configs).
// The dense 27B variants have 64 layers → 16 full + 48 linear.
// The MoE A3B variants have 40 layers → 10 full + 30 linear.
if lower.contains("qwen3.5-") || lower.contains("qwen3.6-") {
// Qwen3.8-2.4T-A95B has 92 layers → 23 full + 69 linear.
if lower.contains("qwen3.5-") || lower.contains("qwen3.6-") || lower.contains("qwen3.8-") {
if lower.contains("-a95b") {
return Some(AttentionLayout {
full: 23,
linear: 69,
});
}
if lower.contains("-a3b") || lower.contains("-a10b") || lower.contains("-a17b") {
return Some(AttentionLayout {
full: 10,
@ -3231,6 +3258,19 @@ mod tests {
assert!(layout.compressible_fraction() < 0.5);
}
/// Layer counts here come from the published `config.json` for each repo:
/// Qwen3.8-27B is 64 layers (16 full), Qwen3.8-2.4T-A95B is 92 (23 full).
#[test]
fn test_infer_attention_layout_qwen3_8() {
let dense = infer_attention_layout_from_name("Qwen/Qwen3.8-27B").unwrap();
assert_eq!(dense.full, 16);
assert_eq!(dense.linear, 48);
let moe = infer_attention_layout_from_name("Qwen/Qwen3.8-2.4T-A95B").unwrap();
assert_eq!(moe.full, 23);
assert_eq!(moe.linear, 69);
}
#[test]
fn test_infer_attention_layout_dense_returns_none() {
assert!(infer_attention_layout_from_name("meta-llama/Llama-3.1-8B").is_none());
@ -3311,6 +3351,7 @@ mod tests {
// Name-only (no architecture)
assert_eq!(parse_generation(None, "Qwen/Qwen3.6-35B-A3B"), Some(3.6));
assert_eq!(parse_generation(None, "Qwen/Qwen3.8-27B"), Some(3.8));
assert_eq!(parse_generation(None, "Qwen/Qwen2.5-72B"), Some(2.5));
assert_eq!(
parse_generation(None, "deepseek-ai/DeepSeek-V4-Flash"),
@ -3319,6 +3360,33 @@ mod tests {
assert_eq!(parse_generation(None, "google/gemma-3-12b-it"), Some(3.0));
}
/// Qwen3.6 and Qwen3.8 both ship under the `qwen3_5` architecture string,
/// so the minor version in the repo name has to win over the arch.
#[test]
fn test_parse_generation_name_beats_qwen_architecture() {
assert_eq!(
parse_generation(Some("qwen3_5"), "Qwen/Qwen3.8-27B"),
Some(3.8)
);
assert_eq!(
parse_generation(Some("qwen3_5_moe"), "Qwen/Qwen3.8-2.4T-A95B"),
Some(3.8)
);
assert_eq!(
parse_generation(Some("qwen3_5"), "Qwen/Qwen3.6-27B"),
Some(3.6)
);
// A bare Qwen3 name must not override the more specific arch string.
assert_eq!(
parse_generation(Some("qwen3_next"), "Qwen/Qwen3-Next-80B-A3B"),
Some(3.8)
);
assert_eq!(
parse_generation(Some("qwen3_5"), "Qwen/Qwen3.5-27B"),
Some(3.5)
);
}
#[test]
fn test_generation_quality_bonus_values() {
// Gen 1.0: bonus = 0

View file

@ -3810,6 +3810,9 @@ const OLLAMA_MAPPINGS: &[(&str, &str)] = &[
("qwen3.5-27b", "qwen3.5"),
("qwen3.5-35b-a3b", "qwen3.5:35b"),
("qwen3.5-122b-a10b", "qwen3.5:122b"),
// Qwen 3.8 — 27B is the only size Ollama publishes; the 2.4T-A95B MoE
// has no library entry.
("qwen3.8-27b", "qwen3.8:27b"),
// Qwen3-Coder-Next
("qwen3-coder-next", "qwen3-coder-next"),
// DeepSeek
@ -4516,6 +4519,14 @@ mod tests {
assert!(candidates.contains(&"qwen2.5:14b".to_string()));
}
#[test]
fn test_qwen3_8_resolves_to_its_ollama_tag() {
assert_eq!(
hf_name_to_ollama_candidates("Qwen/Qwen3.8-27B"),
vec!["qwen3.8:27b".to_string()]
);
}
#[test]
fn test_llama_mapping() {
let candidates = hf_name_to_ollama_candidates("meta-llama/Llama-3.1-8B-Instruct");

View file

@ -109,6 +109,11 @@ TARGET_MODELS = [
"Qwen/Qwen3.6-27B",
"Qwen/Qwen3.6-35B-A3B",
"huihui-ai/Huihui-Qwen3.6-35B-A3B-abliterated",
# Qwen 3.8 (hybrid attention, Aug 2026)
"Qwen/Qwen3.8-27B",
"Qwen/Qwen3.8-27B-FP8",
"Qwen/Qwen3.8-2.4T-A95B",
"Qwen/Qwen3.8-2.4T-A95B-FP8",
# Microsoft Phi
"microsoft/phi-3-mini-4k-instruct",
"microsoft/Phi-3-medium-14b-instruct",
@ -346,6 +351,8 @@ MOE_ACTIVE_PARAMS = {
"Qwen/Qwen3.5-397B-A17B": 17_000_000_000,
"Qwen/Qwen3.6-35B-A3B": 3_000_000_000,
"huihui-ai/Huihui-Qwen3.6-35B-A3B-abliterated": 3_000_000_000, # Qwen3.6-35B-A3B finetune
"Qwen/Qwen3.8-2.4T-A95B": 95_000_000_000,
"Qwen/Qwen3.8-2.4T-A95B-FP8": 95_000_000_000,
"meta-llama/Llama-4-Scout-17B-16E-Instruct": 17_000_000_000,
"meta-llama/Llama-4-Maverick-17B-128E-Instruct": 17_000_000_000,
"xai-org/grok-1": 86_000_000_000,

View file

@ -12,6 +12,23 @@ import sys
from collections import defaultdict
def qwen_minor_generation_from_name(name_lower: str) -> float | None:
"""Mirror of models::qwen_minor_generation_from_name in Rust.
Only matches names carrying an explicit minor version; a bare ``Qwen3``
returns ``None`` so callers can fall back to the architecture string.
"""
for dotted, underscored, generation in (
("qwen3.8", "qwen3_8", 3.8),
("qwen3.6", "qwen3_6", 3.6),
("qwen3.5", "qwen3_5", 3.5),
("qwen2.5", "qwen2_5", 2.5),
):
if dotted in name_lower or underscored in name_lower:
return generation
return None
def parse_generation(architecture: str | None, name: str) -> float | None:
"""Mirror of models::parse_generation in Rust."""
if architecture:
@ -30,6 +47,11 @@ def parse_generation(architecture: str | None, name: str) -> float | None:
# Qwen
if arch.startswith("qwen"):
suffix = arch[len("qwen"):]
# Qwen3.6 and Qwen3.8 ship under the `qwen3_5` architecture string,
# so an explicit minor version in the repo name wins over the arch.
named = qwen_minor_generation_from_name(name.lower())
if named is not None:
return named
if suffix.startswith("3_5") or suffix.startswith("3.5"):
return 3.5
if suffix.startswith("3_next") or suffix.startswith("3next"):
@ -99,14 +121,11 @@ def parse_generation(architecture: str | None, name: str) -> float | None:
# Fallback: name-based
name_lower = name.lower()
if "qwen3.6" in name_lower or "qwen3_6" in name_lower:
return 3.6
if "qwen3.5" in name_lower or "qwen3_5" in name_lower:
return 3.5
named = qwen_minor_generation_from_name(name_lower)
if named is not None:
return named
if "qwen3" in name_lower:
return 3.0
if "qwen2.5" in name_lower or "qwen2_5" in name_lower:
return 2.5
if "qwen2" in name_lower:
return 2.0