diff --git a/MODELS.md b/MODELS.md index 8f9b91fe..7f6d35f0 100644 --- a/MODELS.md +++ b/MODELS.md @@ -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 diff --git a/llmfit-core/data/hf_models.json b/llmfit-core/data/hf_models.json index c0049cf4..9195a7e3 100644 --- a/llmfit-core/data/hf_models.json +++ b/llmfit-core/data/hf_models.json @@ -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", diff --git a/llmfit-core/src/models.rs b/llmfit-core/src/models.rs index f34cc628..e0337569 100644 --- a/llmfit-core/src/models.rs +++ b/llmfit-core/src/models.rs @@ -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 { + 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 { } // 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 { // 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 { }); } - // 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 diff --git a/llmfit-core/src/providers.rs b/llmfit-core/src/providers.rs index 3fb07974..59a46075 100644 --- a/llmfit-core/src/providers.rs +++ b/llmfit-core/src/providers.rs @@ -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"); diff --git a/scripts/scrape_hf_models.py b/scripts/scrape_hf_models.py index db9db468..1aafa4e7 100644 --- a/scripts/scrape_hf_models.py +++ b/scripts/scrape_hf_models.py @@ -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, diff --git a/scripts/validate_generation_scoring.py b/scripts/validate_generation_scoring.py index 7bc16d7d..edbf13b5 100644 --- a/scripts/validate_generation_scoring.py +++ b/scripts/validate_generation_scoring.py @@ -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