#!/usr/bin/env python3 """Select a pre-download ODS model from config/model-library.json. This script is intentionally offline and deterministic. It only uses the installer's detected hardware envelope plus the versioned model catalog; it does not download GGUF metadata and it never treats catalog tok/s estimates as measured performance. """ from __future__ import annotations import argparse import json import re import sys from pathlib import Path from typing import Any VRAM_FIT_TOLERANCE_GB = 0.25 POLICY = "context-aware-largest-capable-general-v1" SPARK_AARCH64_POLICY = "spark-aarch64-nv-ultra-a3b-v1" SPARK_AARCH64_MODEL_ID = "qwen3.6-35b-a3b-ud-q4" # Unified-memory hosts (Strix Halo SH_LARGE, future AMD/NV unified-memory # tiers) hit the same coder-next correctness pathology as Spark aarch64. # Until upstream fixes coder-next on unified-memory backends, route the # qwen profile to the same 35B-A3B substitution used for Spark — same # model id, separate policy tag so the recommendation_reason is honest # about why the substitution fired. UNIFIED_MEMORY_POLICY = "unified-memory-coder-next-a3b-v1" UNIFIED_MEMORY_MODEL_ID = SPARK_AARCH64_MODEL_ID def normalize_key(value: Any) -> str: return re.sub(r"[^a-z0-9]+", "-", str(value or "").lower()).strip("-") def normalize_profile(value: str | None) -> str: key = normalize_key(value or "qwen") if key in {"gemma", "gemma4", "gemma-4"}: return "gemma4" if key == "auto": return "auto" return "qwen" def normalize_host_arch(value: str | None) -> str: key = normalize_key(value or "unknown") if key in {"aarch64", "arm64"}: return "arm64" if key in {"x86-64", "x86_64", "amd64", "x64"}: return "amd64" return key or "unknown" def list_value(value: Any) -> list[str]: if value is None: return [] if isinstance(value, list): return [str(item) for item in value] return [str(value)] def value_enabled(value: Any) -> bool: return normalize_key(value) not in {"", "0", "false", "off", "no"} def effective_profile(profile: str, backend: str, tier: str) -> str: if profile != "auto": return profile if normalize_key(tier) in {"cloud", "0", "t0"}: return "qwen" return "gemma4" if normalize_key(backend) in {"apple", "nvidia", "sycl"} else "qwen" def normalize_model(raw: dict[str, Any]) -> dict[str, Any] | None: gguf_parts = raw.get("gguf_parts") if isinstance(raw.get("gguf_parts"), list) else [] gguf = raw.get("gguf") or raw.get("gguf_file") if not gguf and gguf_parts and isinstance(gguf_parts[0], dict): gguf = gguf_parts[0].get("file") model_id = raw.get("id") or raw.get("llm_model_name") or raw.get("name") or gguf if not model_id or not gguf: return None try: size_mb = float(raw.get("size_mb") or 0) except (TypeError, ValueError): size_mb = 0.0 try: vram_required = float(raw.get("vram_required_gb") or 0) except (TypeError, ValueError): vram_required = 0.0 try: context_length = int(raw.get("context_length") or 0) except (TypeError, ValueError): context_length = 0 return { "id": str(model_id), "name": raw.get("name") or str(model_id), "family": raw.get("family") or "", "llm_model_name": raw.get("llm_model_name") or str(model_id), "gguf_file": str(gguf), "gguf_url": raw.get("gguf_url") or "", "gguf_sha256": raw.get("gguf_sha256") or "", "gguf_parts": gguf_parts, "size_mb": size_mb, "vram_required_gb": vram_required, "context_length": context_length, "quantization": raw.get("quantization") or "", "specialty": raw.get("specialty") or "General", "llama_server_image": raw.get("llama_server_image") or "", "runtime_profiles": raw.get("runtime_profiles") if isinstance(raw.get("runtime_profiles"), list) else [], } def load_catalog(path: Path) -> list[dict[str, Any]]: with path.open("r", encoding="utf-8") as fh: data = json.load(fh) return [ model for model in (normalize_model(raw) for raw in data.get("models", [])) if model is not None ] def usable_memory_gb(backend: str, memory_type: str, vram_mb: int, ram_gb: int) -> tuple[float, str]: backend_key = normalize_key(backend) memory_key = normalize_key(memory_type) if backend_key == "apple" or memory_key == "unified": # Unified-memory machines share RAM with the OS, Docker services, and # KV cache. Use only a bounded share for the model pick so 32GB-class # Macs/APUs are not handed a model that technically fits but thrashes. return max(float(ram_gb) * 0.55, 2.0), "unified system memory" if backend_key in {"cpu", "none", "unknown"} or vram_mb <= 0: return min(max(float(ram_gb) * 0.35, 3.0), 8.0), "system RAM" return float(vram_mb) / 1024.0, "GPU VRAM" def fits(required_gb: float, capacity_gb: float) -> bool: return required_gb <= capacity_gb + VRAM_FIT_TOLERANCE_GB def estimated_param_billions(model: dict[str, Any]) -> float: for key in ("total_params_b", "params_b"): try: value = float(model.get(key) or 0) if value > 0: return value except (TypeError, ValueError): pass numbers: list[float] = [] for text in (model.get("id"), model.get("name"), model.get("llm_model_name"), model.get("gguf_file")): numbers.extend(float(match) for match in re.findall(r"(\d+(?:\.\d+)?)\s*b", str(text or ""), re.I)) if numbers: return max(numbers) size_mb = float(model.get("size_mb") or 0) if size_mb > 0: return max(size_mb / 600.0, 1.0) return 4.0 def estimated_context_kv_gb(model: dict[str, Any]) -> float: context = max(int(model.get("context_length") or 0), 8192) params_b = estimated_param_billions(model) kv_per_32k_gb = min(max(params_b * 0.12, 0.35), 3.5) return round(kv_per_32k_gb * (context / 32768.0), 2) def selector_required_memory_gb(model: dict[str, Any]) -> float: declared = float(model.get("vram_required_gb") or 0) size_gb = float(model.get("size_mb") or 0) / 1024.0 if size_gb <= 0: return round(declared, 2) return round(max(declared, size_gb + estimated_context_kv_gb(model)), 2) def matching_runtime_profile(model: dict[str, Any], backend: str, memory_type: str, vram_mb: int, ram_gb: int, host_arch: str) -> dict[str, Any] | None: backend_key = normalize_key(backend) memory_key = normalize_key(memory_type) arch_key = normalize_host_arch(host_arch) vram_gb = float(vram_mb or 0) / 1024.0 for profile in model.get("runtime_profiles", []) or []: if not isinstance(profile, dict): continue if normalize_key(profile.get("backend")) not in {"", backend_key}: continue allowed_arches = {normalize_host_arch(item) for item in list_value(profile.get("host_arch"))} if allowed_arches and arch_key not in allowed_arches: continue required_memory_type = normalize_key(profile.get("memory_type")) if required_memory_type and required_memory_type != memory_key: continue try: if profile.get("vram_min_gb") is not None and vram_gb < float(profile["vram_min_gb"]): continue if profile.get("vram_max_gb") is not None and vram_gb > float(profile["vram_max_gb"]): continue if profile.get("system_ram_min_gb") is not None and float(ram_gb or 0) < float(profile["system_ram_min_gb"]): continue except (TypeError, ValueError): continue return profile return None def effective_context_length(model: dict[str, Any], runtime_profile: dict[str, Any] | None = None) -> int: if runtime_profile and runtime_profile.get("context_length"): return int(runtime_profile["context_length"]) return int(model.get("context_length") or 0) def effective_required_memory_gb(model: dict[str, Any], runtime_profile: dict[str, Any] | None = None) -> float: if runtime_profile and runtime_profile.get("estimated_required_gb") is not None: return round(float(runtime_profile["estimated_required_gb"]), 2) if runtime_profile and runtime_profile.get("context_length"): model = {**model, "context_length": int(runtime_profile["context_length"])} return selector_required_memory_gb(model) def family_allowed(model: dict[str, Any], profile: str) -> bool: family = normalize_key(model.get("family")) if profile == "gemma4": return family == "gemma4" or model.get("id") == "qwen3.5-2b-q4" return family != "gemma4" def score_model(model: dict[str, Any], capacity_gb: float, profile: str) -> float: runtime_profile = model.get("_runtime_profile") if isinstance(model.get("_runtime_profile"), dict) else None required = effective_required_memory_gb(model, runtime_profile) size_mb = max(float(model.get("size_mb") or 1), 1.0) context = max(effective_context_length(model, runtime_profile), 8192) specialty = str(model.get("specialty") or "General") family = normalize_key(model.get("family")) specialty_weight = { "Code": 4.4, "Quality": 4.1, "General": 3.8, "Balanced": 3.5, "Reasoning": 3.3, "Fast": 2.0, "Bootstrap": 1.0, }.get(specialty, 2.5) family_bonus = 0.35 if profile == "gemma4" and family == "gemma4" else 0.0 family_bonus += 0.25 if profile in {"qwen", "auto"} and family == "qwen" else 0.0 context_bonus = min(context / 32768, 4.0) * 0.18 capability = min(size_mb / 1024, 48.0) * 0.24 fit_ratio = required / max(capacity_gb, 1.0) headroom_penalty = 0.35 if fit_ratio > 0.98 else 0.15 if fit_ratio > 0.92 else 0.0 return specialty_weight + family_bonus + context_bonus + capability - headroom_penalty def rank_models(catalog: list[dict[str, Any]], capacity_gb: float, profile: str, installable_only: bool, backend: str, memory_type: str, vram_mb: int, ram_gb: int, host_arch: str) -> list[dict[str, Any]]: candidates = [] for model in catalog: if installable_only and not model.get("gguf_url"): continue if not family_allowed(model, profile): continue runtime_profile = matching_runtime_profile(model, backend, memory_type, vram_mb, ram_gb, host_arch) candidate_model = {**model, "_runtime_profile": runtime_profile} if runtime_profile else model required = effective_required_memory_gb(candidate_model, runtime_profile) if not fits(required, capacity_gb): continue candidates.append((score_model(candidate_model, capacity_gb, profile), candidate_model)) if not candidates: fallback_pool = [ model for model in catalog if (not installable_only or model.get("gguf_url")) and family_allowed(model, profile) ] or catalog fallback = min(fallback_pool, key=lambda m: float(m.get("vram_required_gb") or 999)) return [fallback] candidates.sort( key=lambda item: ( item[0], effective_required_memory_gb(item[1], item[1].get("_runtime_profile")), effective_context_length(item[1], item[1].get("_runtime_profile")), ), reverse=True, ) return [model for _, model in candidates] def arch_policy_model(catalog: list[dict[str, Any]], tier: str, profile: str, host_arch: str, memory_type: str, installable_only: bool, selected_model: dict[str, Any] | None = None) -> tuple[dict[str, Any] | None, str | None]: """Return (model, policy_tag) for an architecture-specific override, or (None, None). Two routes both substitute coder-next → Qwen3.6-35B-A3B-UD on unified memory hosts where the qwen profile would otherwise pick coder-next (which produces all-`?` tokens on those backends — see in-source notes in installers/lib/tier-map.sh NV_ULTRA + SH_LARGE blocks): - nv-ultra + qwen + arm64: Spark / GB10 Grace Blackwell. - any-tier + qwen + memory_type=unified: Strix Halo + future unified-memory NV/AMD tiers. Memory-type is the authoritative signal (not arch or tier alone) because that's the actual characteristic that triggers the pathology. """ if profile != "qwen": return None, None is_spark_aarch64 = ( normalize_key(tier) == "nv-ultra" and normalize_host_arch(host_arch) == "arm64" ) is_unified_coder_next = ( normalize_key(memory_type) == "unified" and selected_model is not None and is_spark_aarch64_excluded_model(selected_model) ) if not (is_spark_aarch64 or is_unified_coder_next): return None, None for model in catalog: if installable_only and not model.get("gguf_url"): continue if normalize_key(model.get("id")) == normalize_key(SPARK_AARCH64_MODEL_ID): policy = SPARK_AARCH64_POLICY if is_spark_aarch64 else UNIFIED_MEMORY_POLICY return model, policy return None, None def is_spark_aarch64_excluded_model(model: dict[str, Any]) -> bool: """True if `model` is the coder-next entry that we route around on unified-memory backends. Function name preserved for backwards compat with existing callers; the broader semantic is "excluded on unified memory" (see arch_policy_model).""" return normalize_key(model.get("llm_model_name")) == "qwen3-coder-next" def shell_value(value: Any) -> str: text = str(value or "") text = ( text.replace("\\", "\\\\") .replace('"', '\\"') .replace("$", "\\$") .replace("`", "\\`") ) return f'"{text}"' def recommendation_reason(model: dict[str, Any], capacity_gb: float, memory_label: str, backend: str, confidence: str) -> str: runtime_profile = model.get("_runtime_profile") if isinstance(model.get("_runtime_profile"), dict) else None context_k = int(effective_context_length(model, runtime_profile) / 1024) required = effective_required_memory_gb(model, runtime_profile) if runtime_profile: label = runtime_profile.get("label") or runtime_profile.get("id") or "advanced runtime profile" runtime = runtime_profile.get("runtime") or "llama.cpp" return ( f"Catalog runtime fit ({POLICY}): {model['name']} uses {label} " f"via {runtime}, needs about {required:g}GB GPU headroom plus " f"{runtime_profile.get('system_ram_min_gb', 'documented')}GB system RAM, " f"fits {capacity_gb:.1f}GB {memory_label} on {backend}, and gives " f"{context_k}K context. Throughput still requires a local benchmark after first launch." ) return ( f"Catalog fit ({POLICY}): {model['name']} needs " f"about {required:g}GB including context/KV, fits {capacity_gb:.1f}GB " f"{memory_label} on {backend}, and gives {context_k}K context. " f"Throughput requires a local benchmark after first launch." ) def arch_policy_reason(model: dict[str, Any], capacity_gb: float, memory_label: str, policy_tag: str) -> str: context_k = int((model.get("context_length") or 0) / 1024) required = selector_required_memory_gb(model) if policy_tag == UNIFIED_MEMORY_POLICY: rationale = ( "is selected for unified-memory hosts (e.g. Strix Halo, future " "AMD/NV unified-memory tiers) because qwen3-coder-next produces " "all-`?` tokens on unified-memory backends" ) else: rationale = ( "is selected for arm64 NV_ULTRA Spark-class NVIDIA hosts because " "qwen3-coder-next is excluded on this architecture by the tier map" ) return ( f"Arch-aware catalog policy ({policy_tag}): {model['name']} " f"{rationale}. It needs about {required:g}GB including context/KV, " f"fits {capacity_gb:.1f}GB {memory_label}, and gives " f"{context_k}K context. Throughput requires a local benchmark after " f"first launch." ) def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--catalog", required=True, type=Path) parser.add_argument("--backend", default="unknown") parser.add_argument("--memory-type", default="discrete") parser.add_argument("--vram-mb", type=int, default=0) parser.add_argument("--ram-gb", type=int, default=0) parser.add_argument("--profile", default="qwen") parser.add_argument("--tier", default="1") parser.add_argument("--host-arch", default="unknown") parser.add_argument("--installable-only", action="store_true") parser.add_argument("--env", action="store_true", help="print shell assignments") args = parser.parse_args() catalog = load_catalog(args.catalog) profile = effective_profile(normalize_profile(args.profile), args.backend, args.tier) capacity_gb, memory_label = usable_memory_gb(args.backend, args.memory_type, args.vram_mb, args.ram_gb) confidence = "high" if args.backend not in {"unknown", "none"} and capacity_gb > 0 else "medium" ranked = rank_models( catalog, capacity_gb, profile, args.installable_only, args.backend, args.memory_type, args.vram_mb, args.ram_gb, args.host_arch, ) arch_selected, arch_policy_tag = arch_policy_model( catalog, args.tier, profile, args.host_arch, args.memory_type, args.installable_only, ranked[0], ) if arch_selected: selected = arch_selected alternatives = [selected] + [ model for model in ranked if model["id"] != selected["id"] and not is_spark_aarch64_excluded_model(model) ][:2] policy = f"{POLICY}+{arch_policy_tag}" source = "catalog_arch_policy_pre_download" reason = arch_policy_reason(selected, capacity_gb, memory_label, arch_policy_tag) else: selected = ranked[0] alternatives = ranked[:3] policy = POLICY source = "catalog_runtime_profile_pre_download" if selected.get("_runtime_profile") else "catalog_fit_pre_download" reason = recommendation_reason(selected, capacity_gb, memory_label, args.backend, confidence) selected_public = {key: value for key, value in selected.items() if key != "_runtime_profile"} payload = { "policy": policy, "source": source, "confidence": confidence, "profile": profile, "host_arch": normalize_host_arch(args.host_arch), "memory_capacity_gb": round(capacity_gb, 1), "memory_label": memory_label, "selected": selected_public, "reason": reason, "alternatives": [ { "id": model["id"], "name": model["name"], "gguf": model["gguf_file"], "vram_required_gb": model["vram_required_gb"], "estimated_required_gb": effective_required_memory_gb(model, model.get("_runtime_profile")), "context_length": effective_context_length(model, model.get("_runtime_profile")), "specialty": model["specialty"], "runtime_profile": (model.get("_runtime_profile") or {}).get("id"), } for model in alternatives ], } if not args.env: print(json.dumps(payload, indent=2)) return 0 alt_value = ";".join( f"{m['id']}:{effective_context_length(m, m.get('_runtime_profile'))}:{effective_required_memory_gb(m, m.get('_runtime_profile')):g}" for m in alternatives ) runtime_profile = selected.get("_runtime_profile") if isinstance(selected.get("_runtime_profile"), dict) else None env = { "LLM_MODEL": selected["llm_model_name"], "GGUF_FILE": selected["gguf_file"], "GGUF_URL": selected["gguf_url"], "GGUF_SHA256": selected["gguf_sha256"], "MAX_CONTEXT": effective_context_length(selected, runtime_profile), "LLM_MODEL_SIZE_MB": int(round(float(selected["size_mb"]))), "MODEL_RECOMMENDATION_SOURCE": payload["source"], "MODEL_RECOMMENDATION_POLICY": payload["policy"], "MODEL_RECOMMENDATION_CONFIDENCE": payload["confidence"], "MODEL_RECOMMENDATION_REASON": payload["reason"], "MODEL_RECOMMENDED_ALTERNATIVES": alt_value, } if runtime_profile: env["MODEL_RUNTIME_PROFILE"] = runtime_profile.get("id", "") env["MODEL_RUNTIME_PROFILE_LABEL"] = runtime_profile.get("label", "") env["MODEL_RUNTIME_PROFILE_SOURCE"] = runtime_profile.get("source_url", "") if runtime_profile.get("llama_server_image"): env["LLAMA_SERVER_IMAGE"] = runtime_profile["llama_server_image"] for key, value in (runtime_profile.get("env") or {}).items(): if value is not None: env[str(key)] = value elif selected.get("llama_server_image"): env["LLAMA_SERVER_IMAGE"] = selected["llama_server_image"] for key, value in env.items(): print(f"{key}={shell_value(value)}") return 0 if __name__ == "__main__": sys.exit(main())