From 81926463d9afeb2bc32f91b6d7dcadd71c9541e2 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 1 Apr 2026 06:53:13 +0000 Subject: [PATCH] Review PR 4741 changes Reviewed PR 4741 which implements `llama.cpp` installer improvements, fingerprinting to skip redundant downloads, multi-release fallback planning, and better handling of file-in-use conflicts. All test cases passed and no logic issues were found during the code review. Co-authored-by: danielhanchen <23090290+danielhanchen@users.noreply.github.com> --- install.ps1 | 9 +- studio/backend/main.py | 8 +- .../assistant-ui/model-selector.tsx | 9 +- .../assistant-ui/model-selector/pickers.tsx | 160 +-- .../frontend/src/features/chat/chat-page.tsx | 10 +- .../src/features/chat/chat-settings-sheet.tsx | 115 +- studio/install_llama_prebuilt.py | 951 +++++++++++-- studio/setup.ps1 | 37 +- studio/setup.sh | 49 +- .../install/smoke_test_llama_prebuilt.py | 2 +- .../test_install_llama_prebuilt_logic.py | 1236 +++++++++++++++++ tests/studio/install/test_pr4562_bugfixes.py | 208 ++- tests/studio/install/test_selection_logic.py | 683 ++++++++- .../install/test_validate_llama_prebuilt.py | 175 +++ 14 files changed, 3195 insertions(+), 457 deletions(-) create mode 100644 tests/studio/install/test_validate_llama_prebuilt.py diff --git a/install.ps1 b/install.ps1 index ead4e7368d..e6c9f9efb6 100644 --- a/install.ps1 +++ b/install.ps1 @@ -749,6 +749,7 @@ shell.Run cmd, 0, False } else { step "gpu" "none (chat-only / GGUF)" "Yellow" substep "Training and GPU inference require an NVIDIA GPU with drivers installed." "Yellow" + substep "https://www.nvidia.com/Download/index.aspx" "Yellow" } # ── Choose the correct PyTorch index URL based on driver CUDA version ── @@ -776,10 +777,10 @@ shell.Run cmd, 0, False # ── Print CPU-only hint when no GPU detected ── if (-not $SkipTorch -and $TorchIndexUrl -like "*/cpu") { Write-Host "" - substep "No NVIDIA GPU detected." "Yellow" - substep "Installing CPU-only PyTorch. If you only need GGUF chat/inference," "Yellow" - substep "re-run with --no-torch for a faster, lighter install:" "Yellow" - substep ".\install.ps1 --no-torch" "Yellow" + Write-Host " NOTE: No NVIDIA GPU detected." -ForegroundColor Yellow + Write-Host " Installing CPU-only PyTorch. If you only need GGUF chat/inference," + Write-Host " re-run with --no-torch for a faster, lighter install:" + Write-Host " .\install.ps1 --no-torch" Write-Host "" } diff --git a/studio/backend/main.py b/studio/backend/main.py index ad19ee9679..c18f18a743 100644 --- a/studio/backend/main.py +++ b/studio/backend/main.py @@ -121,13 +121,13 @@ async def lifespan(app: FastAPI): if storage.ensure_default_admin(): bootstrap_pw = storage.get_bootstrap_password() app.state.bootstrap_password = bootstrap_pw - - bootstrap_path = storage.DB_PATH.parent / ".bootstrap_password" print("\n" + "=" * 60) print("DEFAULT ADMIN ACCOUNT CREATED") + print( + "Sign in with the seeded credentials and change the password immediately:\n" + ) print(f" username: {storage.DEFAULT_ADMIN_USERNAME}") - print(f" password saved to: {bootstrap_path}") - print(" Open the Studio UI to sign in and change it.") + print(f" password: {bootstrap_pw}\n") print("=" * 60 + "\n") else: app.state.bootstrap_password = storage.get_bootstrap_password() diff --git a/studio/frontend/src/components/assistant-ui/model-selector.tsx b/studio/frontend/src/components/assistant-ui/model-selector.tsx index 441c2b48e4..0332b3ca8a 100644 --- a/studio/frontend/src/components/assistant-ui/model-selector.tsx +++ b/studio/frontend/src/components/assistant-ui/model-selector.tsx @@ -34,7 +34,6 @@ interface ModelSelectorProps { activeGgufVariant?: string | null; onValueChange?: (value: string, meta: ModelSelectorChangeMeta) => void; onEject?: () => void; - onFoldersChange?: () => void; variant?: "outline" | "ghost" | "muted"; size?: "sm" | "default" | "lg"; className?: string; @@ -101,7 +100,6 @@ function ModelSelectorContent({ value, onSelect, onEject, - onFoldersChange, className, dataTour, }: { @@ -110,7 +108,6 @@ function ModelSelectorContent({ value?: string; onSelect: (id: string, meta: ModelSelectorChangeMeta) => void; onEject?: () => void; - onFoldersChange?: () => void; className?: string; dataTour?: string; }) { @@ -127,7 +124,7 @@ function ModelSelectorContent({ )} > {chatOnly ? ( - + ) : ( @@ -136,7 +133,7 @@ function ModelSelectorContent({ - + @@ -174,7 +171,6 @@ export function ModelSelector({ activeGgufVariant, onValueChange, onEject, - onFoldersChange, variant = "outline", size = "default", className, @@ -257,7 +253,6 @@ export function ModelSelector({ value={selected} onSelect={handleSelect} onEject={onEject ? handleEject : undefined} - onFoldersChange={onFoldersChange} className={contentClassName} dataTour={contentDataTour} /> diff --git a/studio/frontend/src/components/assistant-ui/model-selector/pickers.tsx b/studio/frontend/src/components/assistant-ui/model-selector/pickers.tsx index c072eb3096..cf8b4cd54e 100644 --- a/studio/frontend/src/components/assistant-ui/model-selector/pickers.tsx +++ b/studio/frontend/src/components/assistant-ui/model-selector/pickers.tsx @@ -20,15 +20,11 @@ import { } from "@/components/ui/tooltip"; import { usePlatformStore } from "@/config/env"; import { - type ScanFolderInfo, - addScanFolder, deleteCachedModel, listCachedGguf, listCachedModels, listGgufVariants, listLocalModels, - listScanFolders, - removeScanFolder, } from "@/features/chat/api/chat-api"; import type { CachedGgufRepo, @@ -46,7 +42,7 @@ import { import { cn, formatCompact } from "@/lib/utils"; import type { VramFitStatus } from "@/lib/vram"; import { checkVramFit, estimateLoadingVram } from "@/lib/vram"; -import { Add01Icon, Cancel01Icon, Folder02Icon, Search01Icon } from "@hugeicons/core-free-icons"; +import { Search01Icon } from "@hugeicons/core-free-icons"; import { HugeiconsIcon } from "@hugeicons/react"; import { Trash2Icon } from "lucide-react"; import { @@ -419,7 +415,6 @@ let _cachedGgufCache: CachedGgufRepo[] = []; let _cachedModelsCache: CachedModelRepo[] = []; let _lmStudioCache: LocalModelInfo[] = []; let _customFolderCache: LocalModelInfo[] = []; -let _scanFoldersCache: ScanFolderInfo[] = []; /** Sort LM Studio models with unsloth publisher first. */ function sortLmStudio(models: LocalModelInfo[]): LocalModelInfo[] { @@ -439,12 +434,10 @@ export function HubModelPicker({ models, value, onSelect, - onFoldersChange, }: { models: ModelOption[]; value?: string; onSelect: (id: string, meta: ModelSelectorChangeMeta) => void; - onFoldersChange?: () => void; }) { const gpu = useGpuInfo(); const [query, setQuery] = useState(""); @@ -476,13 +469,6 @@ export function HubModelPicker({ const [customFolderModels, setCustomFolderModels] = useState(_customFolderCache); - // Custom scan folders management - const [scanFolders, setScanFolders] = useState(_scanFoldersCache); - const [folderInput, setFolderInput] = useState(""); - const [folderError, setFolderError] = useState(null); - const [showFolderInput, setShowFolderInput] = useState(false); - const [folderLoading, setFolderLoading] = useState(false); - const refreshLocalModelsList = useCallback(() => { listLocalModels() .then((res) => { @@ -498,57 +484,6 @@ export function HubModelPicker({ .catch(() => {}); }, []); - const refreshScanFolders = useCallback(() => { - listScanFolders() - .then((v) => { - _scanFoldersCache = v; - setScanFolders(v); - }) - .catch(() => {}); - }, []); - - const handleAddFolder = useCallback(async () => { - const trimmed = folderInput.trim(); - if (!trimmed || folderLoading) return; - setFolderError(null); - setFolderLoading(true); - try { - const created = await addScanFolder(trimmed); - // Backend returns existing row for duplicates, so deduplicate - const next = _scanFoldersCache.some((f) => f.id === created.id || f.path === created.path) - ? _scanFoldersCache - : [..._scanFoldersCache, created]; - _scanFoldersCache = next; - setScanFolders(next); - setFolderInput(""); - setShowFolderInput(false); - refreshLocalModelsList(); - onFoldersChange?.(); - // Background reconciliation with the server - void refreshScanFolders(); - } catch (e) { - setFolderError(e instanceof Error ? e.message : "Failed to add folder"); - } finally { - setFolderLoading(false); - } - }, [folderInput, folderLoading, refreshScanFolders, refreshLocalModelsList, onFoldersChange]); - - const handleRemoveFolder = useCallback(async (id: number) => { - try { - await removeScanFolder(id); - // Optimistic update so the folder disappears immediately - const next = _scanFoldersCache.filter((f) => f.id !== id); - _scanFoldersCache = next; - setScanFolders(next); - refreshScanFolders(); - refreshLocalModelsList(); - onFoldersChange?.(); - } catch (e) { - toast.error(e instanceof Error ? e.message : "Failed to remove folder"); - refreshScanFolders(); - } - }, [refreshScanFolders, refreshLocalModelsList, onFoldersChange]); - const refreshCachedLists = useCallback(() => { listCachedGguf() .then((v) => { @@ -568,7 +503,6 @@ export function HubModelPicker({ useEffect(() => { // Always refresh LM Studio + custom folder models (not gated by alreadyCached) refreshLocalModelsList(); - refreshScanFolders(); if (alreadyCached) return; let done = 0; @@ -589,7 +523,7 @@ export function HubModelPicker({ }) .catch(() => {}) .finally(check); - }, [alreadyCached, refreshLocalModelsList, refreshScanFolders]); + }, [alreadyCached]); const handleDeleteConfirm = useCallback(async () => { if (!deleteTarget) return; @@ -944,95 +878,9 @@ export function HubModelPicker({ ) : null} - {!showHfSection ? ( + {!showHfSection && customFolderModels.length > 0 ? ( <> -
- - Custom Folders - - -
- - {/* Folder paths */} - {scanFolders.map((f) => ( -
- - - {f.path} - - -
- ))} - - {/* Add folder input */} - {showFolderInput && ( -
-
- - { setFolderInput(e.target.value); setFolderError(null); }} - onKeyDown={(e) => { - if (e.key === "Enter") { e.preventDefault(); handleAddFolder(); } - if (e.key === "Escape") { e.preventDefault(); e.stopPropagation(); setShowFolderInput(false); setFolderInput(""); setFolderError(null); } - }} - placeholder="/path/to/models" - className="h-6 min-w-0 flex-1 rounded border border-border/50 bg-transparent px-1.5 font-mono text-[10px] text-foreground outline-none placeholder:text-muted-foreground/40 focus:border-foreground/20" - disabled={folderLoading} - autoFocus={true} - /> - -
- {folderError && ( -

{folderError}

- )} -
- )} - - {/* Empty state */} - {scanFolders.length === 0 && customFolderModels.length === 0 && !showFolderInput && ( - - )} - - {/* Models from custom folders */} + Custom Folders {customFolderModels.map((m) => { const isGguf = isGgufRepo(m.id) || diff --git a/studio/frontend/src/features/chat/chat-page.tsx b/studio/frontend/src/features/chat/chat-page.tsx index 8d0a9649b4..a47a2c6d92 100644 --- a/studio/frontend/src/features/chat/chat-page.tsx +++ b/studio/frontend/src/features/chat/chat-page.tsx @@ -162,12 +162,10 @@ const CompareContent = memo(function CompareContent({ pairId, models, loraModels, - onFoldersChange, }: { pairId: string; models: ModelOption[]; loraModels: LoraModelOption[]; - onFoldersChange?: () => void; }): ReactElement { const isLoraCompare = useIsLoraCompare(); @@ -178,7 +176,6 @@ const CompareContent = memo(function CompareContent({ pairId={pairId} models={models} loraModels={loraModels} - onFoldersChange={onFoldersChange} /> ); }); @@ -262,12 +259,10 @@ const GeneralCompareContent = memo(function GeneralCompareContent({ pairId, models, loraModels, - onFoldersChange, }: { pairId: string; models: ModelOption[]; loraModels: LoraModelOption[]; - onFoldersChange?: () => void; }): ReactElement { const handlesRef = useRef>({}); const [model1ThreadId, setModel1ThreadId] = useState(); @@ -332,7 +327,6 @@ const GeneralCompareContent = memo(function GeneralCompareContent({ ggufVariant: meta.ggufVariant, }) } - onFoldersChange={onFoldersChange} variant="ghost" size="sm" className="max-w-[50%]" @@ -365,7 +359,6 @@ const GeneralCompareContent = memo(function GeneralCompareContent({ ggufVariant: meta.ggufVariant, }) } - onFoldersChange={onFoldersChange} variant="ghost" size="sm" className="max-w-[50%]" @@ -853,7 +846,6 @@ export function ChatPage(): ReactElement { activeGgufVariant={activeGgufVariant} onValueChange={handleCheckpointChange} onEject={handleEject} - onFoldersChange={refreshLocalModels} variant="ghost" open={modelSelectorOpen} onOpenChange={handleModelSelectorOpenChange} @@ -919,7 +911,6 @@ export function ChatPage(): ReactElement { pairId={view.pairId} models={models} loraModels={loraModels} - onFoldersChange={refreshLocalModels} /> )} @@ -943,6 +934,7 @@ export function ChatPage(): ReactElement { }); } }} + onFoldersChange={refreshLocalModels} /> diff --git a/studio/frontend/src/features/chat/chat-settings-sheet.tsx b/studio/frontend/src/features/chat/chat-settings-sheet.tsx index 6276e5c2e8..6e62c7f9c5 100644 --- a/studio/frontend/src/features/chat/chat-settings-sheet.tsx +++ b/studio/frontend/src/features/chat/chat-settings-sheet.tsx @@ -34,6 +34,7 @@ import { CodeIcon, Delete02Icon, FloppyDiskIcon, + FolderSearchIcon, PencilEdit01Icon, Settings02Icon, SlidersHorizontalIcon, @@ -43,7 +44,13 @@ import { import { HugeiconsIcon } from "@hugeicons/react"; import { AnimatePresence, motion } from "motion/react"; import type { ReactNode } from "react"; -import { useEffect, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { + type ScanFolderInfo, + addScanFolder, + listScanFolders, + removeScanFolder, +} from "./api/chat-api"; import { useChatRuntimeStore } from "./stores/chat-runtime-store"; import { DEFAULT_INFERENCE_PARAMS, @@ -259,6 +266,108 @@ function CollapsibleSection({ ); } +function ModelFoldersSection({ + onFoldersChange, +}: { onFoldersChange?: () => void }) { + const [folders, setFolders] = useState([]); + const [input, setInput] = useState(""); + const [error, setError] = useState(null); + const [loading, setLoading] = useState(false); + + const refresh = useCallback(() => { + listScanFolders() + .then(setFolders) + .catch(() => {}); + }, []); + + useEffect(() => { + refresh(); + }, [refresh]); + + const handleAdd = async () => { + const trimmed = input.trim(); + if (!trimmed) return; + setError(null); + setLoading(true); + try { + await addScanFolder(trimmed); + setInput(""); + refresh(); + onFoldersChange?.(); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to add folder"); + } finally { + setLoading(false); + } + }; + + const handleRemove = async (id: number) => { + try { + await removeScanFolder(id); + onFoldersChange?.(); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to remove folder"); + } finally { + refresh(); + } + }; + + return ( + +
+ {folders.length > 0 && ( +
+ {folders.map((f) => ( +
+ + {f.path} + + +
+ ))} +
+ )} +
+ { + setInput(e.target.value); + setError(null); + }} + onKeyDown={(e) => { + if (e.key === "Enter") handleAdd(); + }} + placeholder="/path/to/models" + className="h-7 flex-1 text-xs font-mono" + disabled={loading} + /> + +
+ {error &&

{error}

} +
+
+ ); +} + interface ChatSettingsPanelProps { open: boolean; onOpenChange?: (open: boolean) => void; @@ -267,6 +376,7 @@ interface ChatSettingsPanelProps { autoTitle: boolean; onAutoTitleChange: (enabled: boolean) => void; onReloadModel?: () => void; + onFoldersChange?: () => void; } export function ChatSettingsPanel({ @@ -277,6 +387,7 @@ export function ChatSettingsPanel({ autoTitle, onAutoTitleChange, onReloadModel, + onFoldersChange, }: ChatSettingsPanelProps) { const isMobile = useIsMobile(); const isGguf = useChatRuntimeStore((s) => s.activeGgufVariant) != null; @@ -724,6 +835,8 @@ export function ChatSettingsPanel({ + + int: + raw = os.environ.get(name) + if raw is None: + value = default + else: + try: + value = int(str(raw).strip()) + except (TypeError, ValueError): + value = default + if minimum is not None: + value = max(minimum, value) + return value + APPROVED_PREBUILT_LLAMA_TAG = "b8508" -DEFAULT_LLAMA_TAG = os.environ.get("UNSLOTH_LLAMA_TAG", APPROVED_PREBUILT_LLAMA_TAG) +DEFAULT_LLAMA_TAG = os.environ.get("UNSLOTH_LLAMA_TAG", "latest") DEFAULT_PUBLISHED_REPO = os.environ.get( "UNSLOTH_LLAMA_RELEASE_REPO", "unslothai/llama.cpp" ) @@ -71,6 +88,11 @@ HTTP_FETCH_BASE_DELAY_SECONDS = 0.75 SERVER_PORT_BIND_ATTEMPTS = 3 SERVER_BIND_RETRY_WINDOW_SECONDS = 5.0 TTY_PROGRESS_START_DELAY_SECONDS = 0.5 +DEFAULT_MAX_PREBUILT_RELEASE_FALLBACKS = env_int( + "UNSLOTH_LLAMA_MAX_PREBUILT_RELEASE_FALLBACKS", + 2, + minimum = 1, +) @dataclass @@ -170,10 +192,87 @@ class ApprovedReleaseChecksums: artifacts: dict[str, ApprovedArtifactHash] +@dataclass(frozen = True) +class ResolvedPublishedRelease: + bundle: PublishedReleaseBundle + checksums: ApprovedReleaseChecksums + + +@dataclass(frozen = True) +class InstallReleasePlan: + requested_tag: str + llama_tag: str + release_tag: str + attempts: list[AssetChoice] + approved_checksums: ApprovedReleaseChecksums + + class PrebuiltFallback(RuntimeError): pass +class BusyInstallConflict(RuntimeError): + pass + + +class ExistingInstallSatisfied(RuntimeError): + def __init__(self, choice: AssetChoice, used_fallback: bool): + super().__init__(f"existing install already matches candidate {choice.name}") + self.choice = choice + self.used_fallback = used_fallback + + +def _os_error_messages(exc: BaseException) -> list[str]: + messages: list[str] = [] + if isinstance(exc, OSError): + for value in ( + getattr(exc, "strerror", None), + getattr(exc, "filename", None), + getattr(exc, "filename2", None), + ): + if isinstance(value, str) and value: + messages.append(value) + text = str(exc) + if text: + messages.append(text) + return [message.lower() for message in messages if message] + + +def is_busy_lock_error(exc: BaseException) -> bool: + if isinstance(exc, BusyInstallConflict): + return True + if isinstance(exc, PermissionError): + return True + if isinstance(exc, OSError): + if exc.errno in { + errno.EACCES, + errno.EBUSY, + errno.ENOTEMPTY, + errno.EPERM, + errno.ETXTBSY, + }: + return True + if getattr(exc, "winerror", None) in {5, 32, 145, 183}: + return True + for message in _os_error_messages(exc): + if any( + needle in message + for needle in ( + "access is denied", + "being used by another process", + "device or resource busy", + "permission denied", + "text file busy", + "directory not empty", + "file is in use", + "process cannot access the file", + "cannot create a file when that file already exists", + ) + ): + return True + return False + + def log(message: str) -> None: print(f"[llama-prebuilt] {message}") @@ -598,6 +697,14 @@ def latest_upstream_release_tag() -> str: return tag +def normalized_requested_llama_tag(requested_tag: str | None) -> str: + if isinstance(requested_tag, str): + normalized = requested_tag.strip() + if normalized: + return normalized + return "latest" + + def normalize_compute_cap(value: Any) -> str | None: raw = str(value).strip() if not raw: @@ -1283,6 +1390,137 @@ def pinned_published_release_bundle( return bundle +def validated_checksums_for_bundle( + repo: str, bundle: PublishedReleaseBundle +) -> ApprovedReleaseChecksums: + checksums = load_approved_release_checksums(repo, bundle.release_tag) + require_approved_source_hash(checksums, bundle.upstream_tag) + return checksums + + +def resolve_published_release( + requested_tag: str | None, + published_repo: str, + published_release_tag: str = "", +) -> ResolvedPublishedRelease: + repo = published_repo or DEFAULT_PUBLISHED_REPO + normalized_requested = normalized_requested_llama_tag(requested_tag) + + if published_release_tag: + bundle = pinned_published_release_bundle(repo, published_release_tag) + if ( + normalized_requested != "latest" + and bundle.upstream_tag != normalized_requested + ): + raise PrebuiltFallback( + "published release " + f"{repo}@{published_release_tag} targeted upstream tag {bundle.upstream_tag}, " + f"but requested {normalized_requested}" + ) + return ResolvedPublishedRelease( + bundle = bundle, + checksums = validated_checksums_for_bundle(repo, bundle), + ) + + skipped_invalid = 0 + for bundle in iter_published_release_bundles(repo): + if ( + normalized_requested != "latest" + and bundle.upstream_tag != normalized_requested + ): + continue + try: + checksums = validated_checksums_for_bundle(repo, bundle) + except PrebuiltFallback as exc: + skipped_invalid += 1 + log( + "published release ignored for install resolution: " + f"{repo}@{bundle.release_tag} ({exc})" + ) + continue + return ResolvedPublishedRelease(bundle = bundle, checksums = checksums) + + if normalized_requested == "latest": + if skipped_invalid: + raise PrebuiltFallback( + f"no usable published llama.cpp releases were available in {repo}" + ) + raise PrebuiltFallback( + f"no published llama.cpp releases were available in {repo}" + ) + + raise PrebuiltFallback( + f"no published prebuilt release in {repo} matched upstream tag {normalized_requested}" + ) + + +def iter_resolved_published_releases( + requested_tag: str | None, + published_repo: str, + published_release_tag: str = "", +) -> Iterable[ResolvedPublishedRelease]: + repo = published_repo or DEFAULT_PUBLISHED_REPO + normalized_requested = normalized_requested_llama_tag(requested_tag) + + if published_release_tag: + bundle = pinned_published_release_bundle(repo, published_release_tag) + if ( + normalized_requested != "latest" + and bundle.upstream_tag != normalized_requested + ): + raise PrebuiltFallback( + "published release " + f"{repo}@{published_release_tag} targeted upstream tag {bundle.upstream_tag}, " + f"but requested {normalized_requested}" + ) + yield ResolvedPublishedRelease( + bundle = bundle, + checksums = validated_checksums_for_bundle(repo, bundle), + ) + return + + matched_any = False + skipped_invalid = 0 + yielded_valid = False + for bundle in iter_published_release_bundles(repo): + if ( + normalized_requested != "latest" + and bundle.upstream_tag != normalized_requested + ): + continue + matched_any = True + try: + checksums = validated_checksums_for_bundle(repo, bundle) + except PrebuiltFallback as exc: + skipped_invalid += 1 + log( + "published release ignored for install resolution: " + f"{repo}@{bundle.release_tag} ({exc})" + ) + continue + yielded_valid = True + yield ResolvedPublishedRelease(bundle = bundle, checksums = checksums) + + if yielded_valid: + return + + if matched_any: + if skipped_invalid: + raise PrebuiltFallback( + f"no usable published llama.cpp releases were available in {repo}" + ) + return + + if normalized_requested == "latest": + raise PrebuiltFallback( + f"no published llama.cpp releases were available in {repo}" + ) + + raise PrebuiltFallback( + f"no published prebuilt release in {repo} matched upstream tag {normalized_requested}" + ) + + def resolve_requested_llama_tag( requested_tag: str | None, published_repo: str = "", @@ -1291,9 +1529,9 @@ def resolve_requested_llama_tag( Resolution order: 1. Concrete tag (e.g. "b8508") -- returned as-is. - 2. "latest" with published_repo -- query the Unsloth release repo - (e.g. unslothai/llama.cpp) for its latest release tag. This is the - tested/approved version that matches the prebuilt binaries. + 2. "latest" with published_repo -- resolve the latest usable Unsloth + published release bundle and return its upstream_tag. This is the + preferred version that matches the published prebuilt metadata. 3. "latest" without published_repo or if (2) fails -- query the upstream ggml-org/llama.cpp repo. This may return a newer, untested tag. @@ -1301,20 +1539,19 @@ def resolve_requested_llama_tag( upstream tags that have been validated with Unsloth Studio. Using the upstream bleeding-edge tag risks API/ABI incompatibilities. """ - if requested_tag and requested_tag != "latest": - return requested_tag + normalized_requested = normalized_requested_llama_tag(requested_tag) + if normalized_requested != "latest": + return normalized_requested # Prefer the Unsloth release repo tag (tested/approved) over bleeding-edge # upstream. For example, unslothai/llama.cpp may publish b8508 while # ggml-org/llama.cpp latest is b8514. The source-build fallback should # compile the same version the prebuilt path would have installed. if published_repo: try: - payload = fetch_json( - f"https://api.github.com/repos/{published_repo}/releases/latest" - ) - tag = payload.get("tag_name") - if isinstance(tag, str) and tag: - return tag + return resolve_published_release( + "latest", + published_repo, + ).bundle.upstream_tag except Exception: pass # Fall back to upstream ggml-org latest release tag @@ -1324,18 +1561,13 @@ def resolve_requested_llama_tag( def resolve_requested_install_tag( requested_tag: str | None, published_release_tag: str = "", + published_repo: str = DEFAULT_PUBLISHED_REPO, ) -> str: - approved_tag = APPROVED_PREBUILT_LLAMA_TAG - normalized_requested = requested_tag or "latest" - if normalized_requested not in {"latest", approved_tag}: - raise PrebuiltFallback( - f"prebuilt installs are pinned to approved release {approved_tag}; requested {normalized_requested}" - ) - if published_release_tag and published_release_tag != approved_tag: - raise PrebuiltFallback( - f"prebuilt installs require published release tag {approved_tag}; requested {published_release_tag}" - ) - return approved_tag + return resolve_published_release( + requested_tag, + published_repo, + published_release_tag, + ).bundle.upstream_tag def run_capture( @@ -1680,6 +1912,68 @@ def windows_cuda_attempts( return attempts +def published_windows_cuda_attempts( + host: HostInfo, + release: PublishedReleaseBundle, + preferred_runtime_line: str | None, + selection_preamble: Iterable[str] = (), +) -> list[AssetChoice]: + selection_log = list(release.selection_log) + list(selection_preamble) + runtime_by_line = {"cuda12": "12.4", "cuda13": "13.1"} + runtime_order = windows_cuda_attempts( + host, + release.upstream_tag, + { + f"llama-{release.upstream_tag}-bin-win-cuda-{runtime}-x64.zip": "published" + for runtime in runtime_by_line.values() + }, + preferred_runtime_line, + selection_log, + ) + published_artifacts = [ + artifact + for artifact in release.artifacts + if artifact.install_kind == "windows-cuda" + ] + artifacts_by_runtime: dict[str, list[PublishedLlamaArtifact]] = {} + for artifact in published_artifacts: + if not artifact.runtime_line: + continue + artifacts_by_runtime.setdefault(artifact.runtime_line, []).append(artifact) + + attempts: list[AssetChoice] = [] + for ordered_attempt in runtime_order: + runtime_line = ordered_attempt.runtime_line + if not runtime_line: + continue + candidates = sorted( + artifacts_by_runtime.get(runtime_line, []), + key = lambda artifact: (artifact.rank, artifact.asset_name), + ) + for artifact in candidates: + asset_url = release.assets.get(artifact.asset_name) + if not asset_url: + continue + attempts.append( + AssetChoice( + repo = release.repo, + tag = release.release_tag, + name = artifact.asset_name, + url = asset_url, + source_label = "published", + install_kind = "windows-cuda", + runtime_line = runtime_line, + selection_log = list(ordered_attempt.selection_log or []) + + [ + "windows_cuda_selection: selected published asset " + f"{artifact.asset_name} for runtime_line={runtime_line}" + ], + ) + ) + break + return attempts + + def resolve_windows_cuda_choices( host: HostInfo, llama_tag: str, upstream_assets: dict[str, str] ) -> list[AssetChoice]: @@ -1695,32 +1989,52 @@ def resolve_windows_cuda_choices( def resolve_linux_cuda_choice( - host: HostInfo, llama_tag: str, published_repo: str, published_release_tag: str + host: HostInfo, release: PublishedReleaseBundle ) -> LinuxCudaSelection: torch_preference = detect_torch_cuda_runtime_preference(host) - skipped_tag_mismatches = 0 - for release in iter_published_release_bundles( - published_repo, published_release_tag - ): - if release.upstream_tag != llama_tag: - skipped_tag_mismatches += 1 - continue - selection = linux_cuda_choice_from_release( - host, - release, - preferred_runtime_line = torch_preference.runtime_line, - selection_preamble = torch_preference.selection_log, - ) - if selection is not None: - return selection - if skipped_tag_mismatches: - log( - "published Linux CUDA selection skipped " - f"{skipped_tag_mismatches} release(s) with upstream_tag != {llama_tag}" - ) + selection = linux_cuda_choice_from_release( + host, + release, + preferred_runtime_line = torch_preference.runtime_line, + selection_preamble = torch_preference.selection_log, + ) + if selection is not None: + return selection raise PrebuiltFallback("no compatible published Linux CUDA bundle was found") +def published_asset_choice_for_kind( + release: PublishedReleaseBundle, + install_kind: str, +) -> AssetChoice | None: + candidates = sorted( + ( + artifact + for artifact in release.artifacts + if artifact.install_kind == install_kind + ), + key = lambda artifact: (artifact.rank, artifact.asset_name), + ) + for artifact in candidates: + asset_url = release.assets.get(artifact.asset_name) + if not asset_url: + continue + return AssetChoice( + repo = release.repo, + tag = release.release_tag, + name = artifact.asset_name, + url = asset_url, + source_label = "published", + install_kind = install_kind, + runtime_line = artifact.runtime_line, + selection_log = list(release.selection_log) + + [ + f"published_selection: selected {artifact.asset_name} install_kind={install_kind}" + ], + ) + return None + + def resolve_upstream_asset_choice(host: HostInfo, llama_tag: str) -> AssetChoice: upstream_assets = github_release_assets(UPSTREAM_REPO, llama_tag) if host.is_linux and host.is_x86_64: @@ -1786,16 +2100,62 @@ def resolve_upstream_asset_choice(host: HostInfo, llama_tag: str) -> AssetChoice ) -def resolve_asset_choice( - host: HostInfo, llama_tag: str, published_repo: str, published_release_tag: str -) -> AssetChoice: +def resolve_asset_choice(host: HostInfo, llama_tag: str) -> AssetChoice: if host.is_linux and host.is_x86_64 and host.has_usable_nvidia: - return resolve_linux_cuda_choice( - host, llama_tag, published_repo, published_release_tag - ).primary + raise PrebuiltFallback( + "Linux CUDA installs require a compatible published bundle; upstream fallback is not available" + ) return resolve_upstream_asset_choice(host, llama_tag) +def resolve_release_asset_choice( + host: HostInfo, + llama_tag: str, + release: PublishedReleaseBundle, + checksums: ApprovedReleaseChecksums, +) -> list[AssetChoice]: + if host.is_windows and host.is_x86_64 and host.has_usable_nvidia: + torch_preference = detect_torch_cuda_runtime_preference(host) + published_attempts = published_windows_cuda_attempts( + host, + release, + torch_preference.runtime_line, + torch_preference.selection_log, + ) + if published_attempts: + try: + return apply_approved_hashes(published_attempts, checksums) + except PrebuiltFallback as exc: + log( + "published Windows CUDA assets ignored for install planning: " + f"{release.repo}@{release.release_tag} ({exc})" + ) + upstream_assets = github_release_assets(UPSTREAM_REPO, llama_tag) + return apply_approved_hashes( + resolve_windows_cuda_choices(host, llama_tag, upstream_assets), + checksums, + ) + + published_choice: AssetChoice | None = None + if host.is_windows and host.is_x86_64: + published_choice = published_asset_choice_for_kind(release, "windows-cpu") + elif host.is_macos and host.is_arm64: + published_choice = published_asset_choice_for_kind(release, "macos-arm64") + elif host.is_macos and host.is_x86_64: + published_choice = published_asset_choice_for_kind(release, "macos-x64") + + if published_choice is not None: + try: + return apply_approved_hashes([published_choice], checksums) + except PrebuiltFallback as exc: + log( + "published platform asset ignored for install planning: " + f"{release.repo}@{release.release_tag} {published_choice.name} ({exc})" + ) + + return apply_approved_hashes([resolve_asset_choice(host, llama_tag)], checksums) + + def extract_archive(archive_path: Path, destination: Path) -> None: def safe_extract_path(base: Path, member_name: str) -> Path: normalized = member_name.replace("\\", "/") @@ -2163,8 +2523,14 @@ def install_lock(lock_path: Path) -> Iterator[None]: while True: try: fd = os.open(str(lock_path), os.O_CREAT | os.O_EXCL | os.O_RDWR) - os.write(fd, f"{os.getpid()}\n".encode()) - os.fsync(fd) + try: + os.write(fd, f"{os.getpid()}\n".encode()) + os.fsync(fd) + except Exception: + os.close(fd) + fd = None + lock_path.unlink(missing_ok = True) + raise break except FileExistsError: # Check if the holder process is still alive @@ -2177,6 +2543,10 @@ def install_lock(lock_path: Path) -> Iterator[None]: if not raw: # File exists but PID not yet written -- another process # just created it. Wait briefly for the write to land. + if time.monotonic() >= deadline: + raise BusyInstallConflict( + f"timed out after {INSTALL_LOCK_TIMEOUT_SECONDS}s waiting for concurrent install lock: {lock_path}" + ) time.sleep(0.1) continue try: @@ -2195,7 +2565,7 @@ def install_lock(lock_path: Path) -> Iterator[None]: lock_path.unlink(missing_ok = True) continue if time.monotonic() >= deadline: - raise RuntimeError( + raise BusyInstallConflict( f"timed out after {INSTALL_LOCK_TIMEOUT_SECONDS}s waiting for concurrent install lock: {lock_path}" ) time.sleep(0.5) @@ -2211,7 +2581,7 @@ def install_lock(lock_path: Path) -> Iterator[None]: with FileLock(lock_path, timeout = INSTALL_LOCK_TIMEOUT_SECONDS): yield except FileLockTimeout as exc: - raise RuntimeError( + raise BusyInstallConflict( f"timed out after {INSTALL_LOCK_TIMEOUT_SECONDS}s waiting for concurrent install lock: {lock_path}" ) from exc @@ -2359,11 +2729,17 @@ def activate_install_tree(staging_dir: Path, install_dir: Path, host: HostInfo) log(f"restoring rollback path {rollback_dir} -> {install_dir}") os.replace(rollback_dir, install_dir) log(f"restored previous install from rollback path {rollback_dir.name}") + if is_busy_lock_error(exc): + raise BusyInstallConflict( + "staged prebuilt validation passed but the existing install could not be replaced " + "because llama.cpp appears to still be in use; restored previous install " + f"({textwrap.shorten(str(exc), width = 200, placeholder = '...')})" + ) from exc raise PrebuiltFallback( "staged prebuilt validation passed but activation failed; restored previous install " f"({textwrap.shorten(str(exc), width = 200, placeholder = '...')})" ) from exc - except PrebuiltFallback: + except (BusyInstallConflict, PrebuiltFallback): raise except Exception as rollback_exc: log(f"rollback after failed activation also failed: {rollback_exc}") @@ -2395,7 +2771,12 @@ def activate_install_tree(staging_dir: Path, install_dir: Path, host: HostInfo) ) from exc else: if rollback_dir: - remove_tree_logged(rollback_dir, "rollback path") + try: + remove_tree_logged(rollback_dir, "rollback path") + except Exception as cleanup_exc: + log( + f"non-fatal: rollback cleanup failed after successful activation: {cleanup_exc}" + ) finally: remove_tree(failed_dir) remove_tree(staging_dir) @@ -3110,39 +3491,90 @@ def resolve_install_attempts( published_repo: str, published_release_tag: str, ) -> tuple[str, str, list[AssetChoice], ApprovedReleaseChecksums]: - requested_tag = llama_tag - resolved_tag = resolve_requested_install_tag(llama_tag, published_release_tag) - checksums = load_approved_release_checksums(published_repo, resolved_tag) - require_approved_source_hash(checksums, resolved_tag) - - if host.is_linux and host.is_x86_64 and host.has_usable_nvidia: - linux_cuda_selection = resolve_linux_cuda_choice( - host, resolved_tag, published_repo, published_release_tag - ) - attempts = apply_approved_hashes(linux_cuda_selection.attempts, checksums) - if not attempts: - raise PrebuiltFallback("no compatible Linux CUDA asset was found") - log_lines(linux_cuda_selection.selection_log) - return requested_tag, resolved_tag, attempts, checksums - - if host.is_windows and host.is_x86_64 and host.has_usable_nvidia: - upstream_assets = github_release_assets(UPSTREAM_REPO, resolved_tag) - attempts = apply_approved_hashes( - resolve_windows_cuda_choices(host, resolved_tag, upstream_assets), checksums - ) - if not attempts: - raise PrebuiltFallback("no compatible Windows CUDA asset was found") - if attempts[0].selection_log: - log_lines(attempts[0].selection_log) - return requested_tag, resolved_tag, attempts, checksums - - choice = resolve_asset_choice( - host, resolved_tag, published_repo, published_release_tag + requested_tag, plans = resolve_install_release_plans( + llama_tag, + host, + published_repo, + published_release_tag, ) - approved_attempts = apply_approved_hashes([choice], checksums) - if choice.selection_log: - log_lines(choice.selection_log) - return requested_tag, resolved_tag, approved_attempts, checksums + if not plans: + raise PrebuiltFallback("no prebuilt release plans were available") + plan = plans[0] + return requested_tag, plan.llama_tag, plan.attempts, plan.approved_checksums + + +def resolve_install_release_plans( + llama_tag: str, + host: HostInfo, + published_repo: str, + published_release_tag: str, + *, + max_release_fallbacks: int = DEFAULT_MAX_PREBUILT_RELEASE_FALLBACKS, +) -> tuple[str, list[InstallReleasePlan]]: + requested_tag = normalized_requested_llama_tag(llama_tag) + allow_older_release_fallback = ( + requested_tag == "latest" and not published_release_tag + ) + release_limit = max(1, max_release_fallbacks) + plans: list[InstallReleasePlan] = [] + last_error: PrebuiltFallback | None = None + + for resolved_release in iter_resolved_published_releases( + llama_tag, + published_repo, + published_release_tag, + ): + bundle = resolved_release.bundle + checksums = resolved_release.checksums + resolved_tag = bundle.upstream_tag + try: + if host.is_linux and host.is_x86_64 and host.has_usable_nvidia: + linux_cuda_selection = resolve_linux_cuda_choice(host, bundle) + attempts = apply_approved_hashes( + linux_cuda_selection.attempts, checksums + ) + if not attempts: + raise PrebuiltFallback("no compatible Linux CUDA asset was found") + log_lines(linux_cuda_selection.selection_log) + else: + attempts = resolve_release_asset_choice( + host, + resolved_tag, + bundle, + checksums, + ) + if not attempts: + raise PrebuiltFallback("no compatible prebuilt asset was found") + if attempts[0].selection_log: + log_lines(attempts[0].selection_log) + except PrebuiltFallback as exc: + last_error = exc + if not allow_older_release_fallback: + raise + log( + "published release skipped for install planning: " + f"{bundle.repo}@{bundle.release_tag} upstream_tag={resolved_tag} ({exc})" + ) + continue + + plans.append( + InstallReleasePlan( + requested_tag = requested_tag, + llama_tag = resolved_tag, + release_tag = bundle.release_tag, + attempts = attempts, + approved_checksums = checksums, + ) + ) + + if not allow_older_release_fallback or len(plans) >= release_limit: + break + + if plans: + return requested_tag, plans + if last_error is not None: + raise last_error + raise PrebuiltFallback("no installable published llama.cpp releases were found") def write_prebuilt_metadata( @@ -3150,17 +3582,46 @@ def write_prebuilt_metadata( *, requested_tag: str, llama_tag: str, + release_tag: str, choice: AssetChoice, + approved_checksums: ApprovedReleaseChecksums, prebuilt_fallback_used: bool, ) -> None: + source_archive = approved_checksums.artifacts.get( + source_archive_logical_name(llama_tag) + ) + source_sha256 = source_archive.sha256 if source_archive is not None else None + fingerprint_payload = { + "published_repo": approved_checksums.repo, + "release_tag": release_tag, + "upstream_tag": llama_tag, + "asset": choice.name, + "asset_sha256": choice.expected_sha256, + "source": choice.source_label, + "source_sha256": source_sha256, + "runtime_line": choice.runtime_line, + "bundle_profile": choice.bundle_profile, + "coverage_class": choice.coverage_class, + } + fingerprint = hashlib.sha256( + json.dumps(fingerprint_payload, sort_keys = True, separators = (",", ":")).encode( + "utf-8" + ) + ).hexdigest() metadata = { "requested_tag": requested_tag, "tag": llama_tag, + "release_tag": release_tag, + "published_repo": approved_checksums.repo, "asset": choice.name, + "asset_sha256": choice.expected_sha256, "source": choice.source_label, + "source_sha256": source_sha256, + "source_commit": approved_checksums.source_commit, "bundle_profile": choice.bundle_profile, "runtime_line": choice.runtime_line, "coverage_class": choice.coverage_class, + "install_fingerprint": fingerprint, "prebuilt_fallback_used": prebuilt_fallback_used, "installed_at_utc": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), } @@ -3169,6 +3630,180 @@ def write_prebuilt_metadata( ) +def expected_install_fingerprint( + *, + llama_tag: str, + release_tag: str, + choice: AssetChoice, + approved_checksums: ApprovedReleaseChecksums, +) -> str | None: + source_archive = approved_checksums.artifacts.get( + source_archive_logical_name(llama_tag) + ) + if source_archive is None or not choice.expected_sha256: + return None + payload = { + "published_repo": approved_checksums.repo, + "release_tag": release_tag, + "upstream_tag": llama_tag, + "asset": choice.name, + "asset_sha256": choice.expected_sha256, + "source": choice.source_label, + "source_sha256": source_archive.sha256, + "runtime_line": choice.runtime_line, + "bundle_profile": choice.bundle_profile, + "coverage_class": choice.coverage_class, + } + return hashlib.sha256( + json.dumps(payload, sort_keys = True, separators = (",", ":")).encode("utf-8") + ).hexdigest() + + +def load_prebuilt_metadata(install_dir: Path) -> dict[str, Any] | None: + metadata_path = install_dir / "UNSLOTH_PREBUILT_INFO.json" + if not metadata_path.is_file(): + return None + try: + payload = json.loads(metadata_path.read_text(encoding = "utf-8")) + except Exception: + return None + if not isinstance(payload, dict): + return None + return payload + + +def install_tree_is_healthy(install_dir: Path, host: HostInfo) -> bool: + try: + confirm_install_tree(install_dir, host) + except Exception: + return False + return True + + +def runtime_payload_health_groups(choice: AssetChoice) -> list[list[str]]: + if choice.install_kind == "linux-cpu": + return [ + ["libllama.so*"], + ["libggml.so*"], + ["libggml-base.so*"], + ["libggml-cpu-*.so*"], + ["libmtmd.so*"], + ] + if choice.install_kind == "linux-cuda": + return [ + ["libllama.so*"], + ["libggml.so*"], + ["libggml-base.so*"], + ["libggml-cpu-*.so*"], + ["libmtmd.so*"], + ["libggml-cuda.so*"], + ] + if choice.install_kind in {"macos-arm64", "macos-x64"}: + return [ + ["libllama*.dylib"], + ["libggml*.dylib"], + ["libmtmd*.dylib"], + ] + if choice.install_kind == "windows-cpu": + return [["llama.dll"]] + if choice.install_kind == "windows-cuda": + return [["llama.dll"], ["ggml-cuda.dll"]] + return [] + + +def install_runtime_dir(install_dir: Path, host: HostInfo) -> Path: + if host.is_windows: + return install_dir / "build" / "bin" / "Release" + return install_dir / "build" / "bin" + + +def runtime_payload_is_healthy( + install_dir: Path, host: HostInfo, choice: AssetChoice +) -> bool: + runtime_dir = install_runtime_dir(install_dir, host) + if not runtime_dir.exists(): + return False + for pattern_group in runtime_payload_health_groups(choice): + matched = False + for pattern in pattern_group: + if any(runtime_dir.glob(pattern)): + matched = True + break + if not matched: + return False + return True + + +def existing_install_matches_choice( + install_dir: Path, + host: HostInfo, + *, + llama_tag: str, + release_tag: str, + choice: AssetChoice, + approved_checksums: ApprovedReleaseChecksums, +) -> bool: + if not install_dir.exists(): + return False + if not install_tree_is_healthy(install_dir, host): + return False + + metadata = load_prebuilt_metadata(install_dir) + if metadata is None: + return False + + if not runtime_payload_is_healthy(install_dir, host, choice): + return False + expected_fingerprint = expected_install_fingerprint( + llama_tag = llama_tag, + release_tag = release_tag, + choice = choice, + approved_checksums = approved_checksums, + ) + if not expected_fingerprint: + return False + + recorded_fingerprint = metadata.get("install_fingerprint") + if not isinstance(recorded_fingerprint, str) or not recorded_fingerprint: + return False + + if recorded_fingerprint != expected_fingerprint: + return False + + expected_pairs = { + "release_tag": release_tag, + "published_repo": approved_checksums.repo, + "tag": llama_tag, + "asset": choice.name, + "asset_sha256": choice.expected_sha256, + "source": choice.source_label, + "runtime_line": choice.runtime_line, + "bundle_profile": choice.bundle_profile, + "coverage_class": choice.coverage_class, + } + for key, expected in expected_pairs.items(): + if metadata.get(key) != expected: + return False + return True + + +def existing_install_matches_plan( + install_dir: Path, + host: HostInfo, + plan: InstallReleasePlan, +) -> bool: + if not plan.attempts: + return False + return existing_install_matches_choice( + install_dir, + host, + llama_tag = plan.llama_tag, + release_tag = plan.release_tag, + choice = plan.attempts[0], + approved_checksums = plan.approved_checksums, + ) + + def validate_prebuilt_choice( choice: AssetChoice, host: HostInfo, @@ -3178,6 +3813,7 @@ def validate_prebuilt_choice( *, requested_tag: str, llama_tag: str, + release_tag: str, approved_checksums: ApprovedReleaseChecksums, prebuilt_fallback_used: bool, quantized_path: Path, @@ -3206,7 +3842,9 @@ def validate_prebuilt_choice( install_dir, requested_tag = requested_tag, llama_tag = llama_tag, + release_tag = release_tag, choice = choice, + approved_checksums = approved_checksums, prebuilt_fallback_used = prebuilt_fallback_used, ) validate_quantize( @@ -3237,13 +3875,16 @@ def validate_prebuilt_attempts( *, requested_tag: str, llama_tag: str, + release_tag: str, approved_checksums: ApprovedReleaseChecksums, + initial_fallback_used: bool = False, + existing_install_dir: Path | None = None, ) -> tuple[AssetChoice, Path, bool]: attempt_list = list(attempts) if not attempt_list: raise PrebuiltFallback("no prebuilt bundle attempts were available") - tried_fallback = False + tried_fallback = initial_fallback_used for index, attempt in enumerate(attempt_list): if index > 0: tried_fallback = True @@ -3253,6 +3894,20 @@ def validate_prebuilt_attempts( f"runtime_line={attempt.runtime_line} coverage_class={attempt.coverage_class}" ) + if existing_install_dir is not None and existing_install_matches_choice( + existing_install_dir, + host, + llama_tag = llama_tag, + release_tag = release_tag, + choice = attempt, + approved_checksums = approved_checksums, + ): + log( + "existing llama.cpp install already matches fallback candidate " + f"{attempt.name}; skipping reinstall" + ) + raise ExistingInstallSatisfied(attempt, tried_fallback) + staging_dir = create_install_staging_dir(install_dir) quantized_path = work_dir / f"stories260K-q4-{index}.gguf" if quantized_path.exists(): @@ -3266,6 +3921,7 @@ def validate_prebuilt_attempts( probe_path, requested_tag = requested_tag, llama_tag = llama_tag, + release_tag = release_tag, approved_checksums = approved_checksums, prebuilt_fallback_used = tried_fallback, quantized_path = quantized_path, @@ -3307,42 +3963,81 @@ def install_prebuilt( log( f"no existing llama.cpp install detected at {install_dir}; performing fresh prebuilt install" ) - requested_tag, llama_tag, attempts, approved_checksums = ( - resolve_install_attempts( - llama_tag, - host, - published_repo, - published_release_tag, + requested_tag, release_plans = resolve_install_release_plans( + llama_tag, + host, + published_repo, + published_release_tag, + ) + if release_plans and existing_install_matches_plan( + install_dir, host, release_plans[0] + ): + current = release_plans[0] + log( + "existing llama.cpp install already matches selected release " + f"{current.release_tag} upstream_tag={current.llama_tag}; skipping download and install" ) - ) - choice = attempts[0] - log( - f"selected {choice.name} ({choice.source_label}) for {host.system} {host.machine}" - ) + return with tempfile.TemporaryDirectory(prefix = "unsloth-llama-prebuilt-") as tmp: work_dir = Path(tmp) probe_path = work_dir / "stories260K.gguf" download_validation_model( probe_path, validation_model_cache_path(install_dir) ) - choice, selected_staging_dir, _ = validate_prebuilt_attempts( - attempts, - host, - install_dir, - work_dir, - probe_path, - requested_tag = requested_tag, - llama_tag = llama_tag, - approved_checksums = approved_checksums, - ) - activate_install_tree(selected_staging_dir, install_dir, host) - try: - ensure_converter_scripts(install_dir, llama_tag) - except Exception as exc: + release_count = len(release_plans) + for release_index, plan in enumerate(release_plans): + choice = plan.attempts[0] + if existing_install_matches_plan(install_dir, host, plan): + log( + "existing llama.cpp install already matches fallback release " + f"{plan.release_tag} upstream_tag={plan.llama_tag}; skipping reinstall" + ) + return log( - "converter script fetch failed after activation; install remains valid " - f"({textwrap.shorten(str(exc), width = 200, placeholder = '...')})" + "selected " + f"{choice.name} ({choice.source_label}) from published release " + f"{plan.release_tag} for {host.system} {host.machine}" ) + try: + choice, selected_staging_dir, _ = validate_prebuilt_attempts( + plan.attempts, + host, + install_dir, + work_dir, + probe_path, + requested_tag = requested_tag, + llama_tag = plan.llama_tag, + release_tag = plan.release_tag, + approved_checksums = plan.approved_checksums, + initial_fallback_used = release_index > 0, + existing_install_dir = install_dir, + ) + except ExistingInstallSatisfied: + return + except PrebuiltFallback as exc: + if release_index == release_count - 1: + raise + log( + "published release " + f"{plan.release_tag} upstream_tag={plan.llama_tag} failed; " + "trying the next older published prebuilt " + f"({textwrap.shorten(str(exc), width = 200, placeholder = '...')})" + ) + continue + + activate_install_tree(selected_staging_dir, install_dir, host) + try: + ensure_converter_scripts(install_dir, plan.llama_tag) + except Exception as exc: + log( + "converter script fetch failed after activation; install remains valid " + f"({textwrap.shorten(str(exc), width = 200, placeholder = '...')})" + ) + return + except BusyInstallConflict as exc: + log("prebuilt install path is blocked by an in-use llama.cpp install") + log(f"prebuilt busy reason: {exc}") + raise SystemExit(EXIT_BUSY) from exc except PrebuiltFallback as exc: log("prebuilt install path failed; falling back to source build") log(f"prebuilt fallback reason: {exc}") @@ -3359,7 +4054,10 @@ def parse_args() -> argparse.Namespace: parser.add_argument( "--llama-tag", default = DEFAULT_LLAMA_TAG, - help = f"llama.cpp release tag. Prebuilt installs are pinned to the approved tag {APPROVED_PREBUILT_LLAMA_TAG}.", + help = ( + "llama.cpp release tag. Defaults to the latest usable published Unsloth " + "release unless UNSLOTH_LLAMA_TAG overrides it." + ), ) parser.add_argument( "--published-repo", @@ -3369,7 +4067,10 @@ def parse_args() -> argparse.Namespace: parser.add_argument( "--published-release-tag", default = DEFAULT_PUBLISHED_TAG, - help = "Published GitHub release tag to pin. By default, scan releases until a compatible llama.cpp bundle is found.", + help = ( + "Published GitHub release tag to pin. By default, scan releases " + "until a usable published llama.cpp release bundle is found." + ), ) resolve_group = parser.add_mutually_exclusive_group() resolve_group.add_argument( @@ -3382,7 +4083,10 @@ def parse_args() -> argparse.Namespace: "--resolve-install-tag", nargs = "?", const = "latest", - help = "Resolve a llama.cpp tag such as 'latest' to the concrete tag installable on the current host.", + help = ( + "Resolve a llama.cpp tag such as 'latest' to the concrete upstream tag " + "selected by the current published-release policy." + ), ) return parser.parse_args() @@ -3398,7 +4102,9 @@ def main() -> int: if args.resolve_install_tag is not None: print( resolve_requested_install_tag( - args.resolve_install_tag, args.published_release_tag or "" + args.resolve_install_tag, + args.published_release_tag or "", + args.published_repo, ) ) return EXIT_SUCCESS @@ -3421,6 +4127,11 @@ if __name__ == "__main__": raise SystemExit(main()) except SystemExit: raise + except BusyInstallConflict as exc: + log( + f"fatal helper busy conflict: {textwrap.shorten(str(exc), width = 400, placeholder = '...')}" + ) + raise SystemExit(EXIT_BUSY) except Exception as exc: message = textwrap.shorten(str(exc), width = 400, placeholder = "...") log(f"fatal helper error: {message}") diff --git a/studio/setup.ps1 b/studio/setup.ps1 index aa3c11a594..f67469382d 100644 --- a/studio/setup.ps1 +++ b/studio/setup.ps1 @@ -490,7 +490,8 @@ if (-not $HasNvidiaSmi) { if (-not $HasNvidiaSmi) { Write-Host "" step "gpu" "none (chat-only / GGUF)" "Yellow" - substep "Training and GPU inference require an NVIDIA GPU with drivers installed." "Yellow" + Write-Host " Training and GPU inference require an NVIDIA GPU with drivers installed." -ForegroundColor Yellow + Write-Host " https://www.nvidia.com/Download/index.aspx" -ForegroundColor Yellow Write-Host "" } else { step "gpu" "NVIDIA GPU detected" @@ -1596,29 +1597,15 @@ $resolveExit = $LASTEXITCODE $ResolvedLlamaTag = if ($resolveOutput) { ($resolveOutput | Select-Object -Last 1).ToString().Trim() } else { "" } if ($resolveExit -ne 0 -or [string]::IsNullOrWhiteSpace($ResolvedLlamaTag)) { Write-Host "" - substep "Failed to resolve an installable prebuilt llama.cpp tag via $HelperReleaseRepo" "Yellow" + substep "Failed to resolve a published llama.cpp release via $HelperReleaseRepo" "Yellow" Write-LlamaFailureLog -Output ($resolveOutput | Out-String) # Resolve the llama.cpp tag for source-build fallback. Pass --published-repo - # so the resolver prefers Unsloth's tested tag (e.g. b8508) over the upstream - # bleeding-edge tag (e.g. b8514) from ggml-org/llama.cpp. + # so the resolver prefers the latest usable Unsloth-published upstream tag + # before falling back to the bleeding-edge ggml-org/llama.cpp tag. $fallbackOutput = & python "$PSScriptRoot\install_llama_prebuilt.py" --resolve-llama-tag $RequestedLlamaTag --published-repo $HelperReleaseRepo 2>$null $fallbackExit = $LASTEXITCODE $ResolvedLlamaTag = if ($fallbackExit -eq 0 -and $fallbackOutput) { ($fallbackOutput | Select-Object -Last 1).ToString().Trim() - } elseif ($RequestedLlamaTag -eq "latest") { - # Try Unsloth release repo first, then fall back to ggml-org upstream - $resolvedLatest = $null - try { - $latestRelease = Invoke-RestMethod -Uri "https://api.github.com/repos/$HelperReleaseRepo/releases/latest" -ErrorAction Stop - $resolvedLatest = $latestRelease.tag_name - } catch {} - if (-not $resolvedLatest) { - try { - $latestRelease = Invoke-RestMethod -Uri "https://api.github.com/repos/ggml-org/llama.cpp/releases/latest" -ErrorAction Stop - $resolvedLatest = $latestRelease.tag_name - } catch {} - } - if ($resolvedLatest) { $resolvedLatest } else { $RequestedLlamaTag } } else { $RequestedLlamaTag } @@ -1667,7 +1654,19 @@ if ($env:UNSLOTH_LLAMA_FORCE_COMPILE -eq "1") { $ErrorActionPreference = $prevEAPPrebuilt if ($prebuiltExit -eq 0) { - step "llama.cpp" "prebuilt installed and validated" + if ($prebuiltOutput -match "already matches selected release") { + step "llama.cpp" "prebuilt up to date and validated" + } else { + step "llama.cpp" "prebuilt installed and validated" + } + } elseif ($prebuiltExit -eq 3) { + step "llama.cpp" "install blocked by active llama.cpp process" "Yellow" + Write-LlamaFailureLog -Output $prebuiltOutput + if (Test-Path $LlamaCppDir) { + substep "Existing install was restored" "Yellow" + } + substep "Close Studio or other llama.cpp users and retry" "Yellow" + exit 3 } else { step "llama.cpp" "prebuilt install failed (continuing)" "Yellow" Write-LlamaFailureLog -Output $prebuiltOutput diff --git a/studio/setup.sh b/studio/setup.sh index 3715f536f6..0807259cb9 100755 --- a/studio/setup.sh +++ b/studio/setup.sh @@ -108,6 +108,10 @@ echo "" printf " ${C_TITLE}%s${C_RST}\n" "🦥 Unsloth Studio Setup" printf " ${C_DIM}%s${C_RST}\n" "$RULE" verbose_substep "verbose diagnostics enabled" +_LLAMA_ONLY="${UNSLOTH_STUDIO_LLAMA_ONLY:-0}" +if [ "$_LLAMA_ONLY" = "1" ]; then + substep "llama.cpp only mode" +fi # ── Clean up stale caches ── rm -rf "$REPO_ROOT/unsloth_compiled_cache" rm -rf "$SCRIPT_DIR/backend/unsloth_compiled_cache" @@ -120,6 +124,7 @@ if [[ "$keynames" == *$'\nCOLAB_'* ]]; then IS_COLAB=true fi +if [ "$_LLAMA_ONLY" != "1" ]; then # ── Frontend ── _NEED_FRONTEND_BUILD=true if [ -d "$SCRIPT_DIR/frontend/dist" ]; then @@ -453,6 +458,7 @@ else step "python" "dependencies up to date" verbose_substep "python deps check: installed=$_PKG_NAME@${INSTALLED_VER:-unknown} latest=${LATEST_VER:-unknown}" fi +fi # ── 7. Prefer prebuilt llama.cpp bundles before any source build path ── UNSLOTH_HOME="$HOME/.unsloth" @@ -477,26 +483,17 @@ else _RESOLVED_LLAMA_TAG="" fi if [ -z "$_RESOLVED_LLAMA_TAG" ]; then - step "llama.cpp" "failed to resolve prebuilt tag via $_HELPER_RELEASE_REPO" "$C_WARN" + step "llama.cpp" "failed to resolve a published llama.cpp release via $_HELPER_RELEASE_REPO" "$C_WARN" print_llama_error_log "$_RESOLVE_LLAMA_LOG" set +e # Resolve the llama.cpp tag for source-build fallback. Pass --published-repo - # so the resolver prefers Unsloth's tested tag (e.g. b8508) over the upstream - # bleeding-edge tag (e.g. b8514) from ggml-org/llama.cpp. + # so the resolver prefers the latest usable Unsloth-published upstream tag + # before falling back to the bleeding-edge ggml-org/llama.cpp tag. _RESOLVED_LLAMA_TAG="$(python "$SCRIPT_DIR/install_llama_prebuilt.py" --resolve-llama-tag "$_REQUESTED_LLAMA_TAG" --published-repo "$_HELPER_RELEASE_REPO" 2>/dev/null)" _RESOLVE_UPSTREAM_STATUS=$? set -e if [ "$_RESOLVE_UPSTREAM_STATUS" -ne 0 ] || [ -z "$_RESOLVED_LLAMA_TAG" ]; then - if [ "$_REQUESTED_LLAMA_TAG" = "latest" ]; then - # Try Unsloth release repo first, then fall back to ggml-org upstream - _RESOLVED_LLAMA_TAG="$(curl -fsSL "https://api.github.com/repos/${_HELPER_RELEASE_REPO}/releases/latest" 2>/dev/null | python -c "import sys,json; print(json.load(sys.stdin)['tag_name'])" 2>/dev/null)" || _RESOLVED_LLAMA_TAG="" - if [ -z "$_RESOLVED_LLAMA_TAG" ]; then - _RESOLVED_LLAMA_TAG="$(curl -fsSL https://api.github.com/repos/ggml-org/llama.cpp/releases/latest 2>/dev/null | python -c "import sys,json; print(json.load(sys.stdin)['tag_name'])" 2>/dev/null)" || _RESOLVED_LLAMA_TAG="" - fi - fi - if [ -z "$_RESOLVED_LLAMA_TAG" ]; then - _RESOLVED_LLAMA_TAG="$_REQUESTED_LLAMA_TAG" - fi + _RESOLVED_LLAMA_TAG="$_REQUESTED_LLAMA_TAG" fi _NEED_LLAMA_SOURCE_BUILD=true _SKIP_PREBUILT_INSTALL=true @@ -538,9 +535,22 @@ else set -e if [ "$_PREBUILT_STATUS" -eq 0 ]; then - step "llama.cpp" "prebuilt installed and validated" + if grep -Fq "already matches selected release" "$_PREBUILT_LOG"; then + step "llama.cpp" "prebuilt up to date and validated" + else + step "llama.cpp" "prebuilt installed and validated" + fi verbose_substep "llama.cpp install dir: $LLAMA_CPP_DIR" rm -f "$_PREBUILT_LOG" + elif [ "$_PREBUILT_STATUS" -eq 3 ]; then + step "llama.cpp" "install blocked by active llama.cpp process" "$C_WARN" + print_llama_error_log "$_PREBUILT_LOG" + rm -f "$_PREBUILT_LOG" + if [ -d "$LLAMA_CPP_DIR" ]; then + substep "existing install was restored" + fi + substep "close Studio or other llama.cpp users and retry" + exit 3 else step "llama.cpp" "prebuilt install failed (continuing)" "$C_WARN" print_llama_error_log "$_PREBUILT_LOG" @@ -794,7 +804,16 @@ else fi # end _SKIP_GGUF_BUILD check # ── Footer ── -if [ "$IS_COLAB" = true ]; then +if [ "$_LLAMA_ONLY" = "1" ]; then + echo "" + printf " ${C_DIM}%s${C_RST}\n" "$RULE" + if [ "$_LLAMA_CPP_DEGRADED" = true ]; then + printf " ${C_WARN}%s${C_RST}\n" "llama.cpp update finished (limited: llama.cpp unavailable)" + else + printf " ${C_TITLE}%s${C_RST}\n" "llama.cpp update finished" + fi + printf " ${C_DIM}%s${C_RST}\n" "$RULE" +elif [ "$IS_COLAB" = true ]; then echo "" printf " ${C_DIM}%s${C_RST}\n" "$RULE" if [ "$_LLAMA_CPP_DEGRADED" = true ]; then diff --git a/tests/studio/install/smoke_test_llama_prebuilt.py b/tests/studio/install/smoke_test_llama_prebuilt.py index 994757d2e2..d87537dc94 100644 --- a/tests/studio/install/smoke_test_llama_prebuilt.py +++ b/tests/studio/install/smoke_test_llama_prebuilt.py @@ -39,7 +39,7 @@ def parse_args() -> argparse.Namespace: parser.add_argument( "--llama-tag", default = "latest", - help = "llama.cpp tag to resolve. Defaults to the approved prebuilt tag for this host.", + help = "llama.cpp tag to resolve. Defaults to the latest usable published Unsloth release.", ) parser.add_argument( "--published-repo", diff --git a/tests/studio/install/test_install_llama_prebuilt_logic.py b/tests/studio/install/test_install_llama_prebuilt_logic.py index eb30ac2745..622f01f946 100644 --- a/tests/studio/install/test_install_llama_prebuilt_logic.py +++ b/tests/studio/install/test_install_llama_prebuilt_logic.py @@ -33,6 +33,10 @@ activate_install_tree = INSTALL_LLAMA_PREBUILT.activate_install_tree create_install_staging_dir = INSTALL_LLAMA_PREBUILT.create_install_staging_dir sha256_file = INSTALL_LLAMA_PREBUILT.sha256_file source_archive_logical_name = INSTALL_LLAMA_PREBUILT.source_archive_logical_name +install_prebuilt = INSTALL_LLAMA_PREBUILT.install_prebuilt +write_prebuilt_metadata = INSTALL_LLAMA_PREBUILT.write_prebuilt_metadata +existing_install_matches_plan = INSTALL_LLAMA_PREBUILT.existing_install_matches_plan +existing_install_matches_choice = INSTALL_LLAMA_PREBUILT.existing_install_matches_choice def approved_checksums_for( @@ -318,6 +322,7 @@ def test_validate_prebuilt_choice_creates_repo_shaped_linux_install( probe_path, requested_tag = upstream_tag, llama_tag = upstream_tag, + release_tag = upstream_tag, approved_checksums = approved_checksums_for( upstream_tag, source_archive = source_archive, @@ -436,6 +441,7 @@ def test_validate_prebuilt_choice_creates_repo_shaped_windows_install( probe_path, requested_tag = upstream_tag, llama_tag = upstream_tag, + release_tag = upstream_tag, approved_checksums = approved_checksums_for( upstream_tag, source_archive = source_archive, @@ -610,6 +616,1236 @@ def test_binary_env_linux_includes_binary_parent_in_ld_library_path( assert str(install_dir) in ld_dirs +def test_install_prebuilt_falls_back_to_older_release_plan( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +): + install_dir = tmp_path / "llama.cpp" + host = HostInfo( + system = "Linux", + machine = "x86_64", + is_windows = False, + is_linux = True, + is_macos = False, + is_x86_64 = True, + is_arm64 = False, + nvidia_smi = None, + driver_cuda_version = None, + compute_caps = [], + visible_cuda_devices = None, + has_physical_nvidia = False, + has_usable_nvidia = False, + ) + + first_choice = AssetChoice( + repo = "unslothai/llama.cpp", + tag = "old-release", + name = "app-b9002-linux-x64.tar.gz", + url = "https://example.com/app-b9002-linux-x64.tar.gz", + source_label = "published", + install_kind = "linux-cpu", + ) + second_choice = AssetChoice( + repo = "unslothai/llama.cpp", + tag = "older-release", + name = "app-b9001-linux-x64.tar.gz", + url = "https://example.com/app-b9001-linux-x64.tar.gz", + source_label = "published", + install_kind = "linux-cpu", + ) + first_plan = INSTALL_LLAMA_PREBUILT.InstallReleasePlan( + requested_tag = "latest", + llama_tag = "b9002", + release_tag = "release-2", + attempts = [first_choice], + approved_checksums = ApprovedReleaseChecksums( + repo = "unslothai/llama.cpp", + release_tag = "release-2", + upstream_tag = "b9002", + source_commit = None, + artifacts = {}, + ), + ) + second_plan = INSTALL_LLAMA_PREBUILT.InstallReleasePlan( + requested_tag = "latest", + llama_tag = "b9001", + release_tag = "release-1", + attempts = [second_choice], + approved_checksums = ApprovedReleaseChecksums( + repo = "unslothai/llama.cpp", + release_tag = "release-1", + upstream_tag = "b9001", + source_commit = None, + artifacts = {}, + ), + ) + + monkeypatch.setattr(INSTALL_LLAMA_PREBUILT, "detect_host", lambda: host) + monkeypatch.setattr( + INSTALL_LLAMA_PREBUILT, + "resolve_install_release_plans", + lambda llama_tag, host, published_repo, published_release_tag: ( + "latest", + [first_plan, second_plan], + ), + ) + monkeypatch.setattr( + INSTALL_LLAMA_PREBUILT, + "download_validation_model", + lambda probe_path, cache_path: probe_path.write_bytes(b"probe"), + ) + + call_log: list[tuple[str, bool]] = [] + + def fake_validate( + attempts, + host, + install_dir, + work_dir, + probe_path, + *, + requested_tag, + llama_tag, + release_tag, + approved_checksums, + initial_fallback_used = False, + existing_install_dir = None, + ): + call_log.append((llama_tag, initial_fallback_used)) + if llama_tag == "b9002": + raise PrebuiltFallback("validation failed for latest release") + staging_dir = create_install_staging_dir(install_dir) + (staging_dir / "marker.txt").write_text("ready\n") + return attempts[0], staging_dir, initial_fallback_used + + monkeypatch.setattr( + INSTALL_LLAMA_PREBUILT, + "validate_prebuilt_attempts", + fake_validate, + ) + + activated = {} + monkeypatch.setattr( + INSTALL_LLAMA_PREBUILT, + "activate_install_tree", + lambda staging_dir, install_dir, host: activated.update( + {"staging_dir": staging_dir, "install_dir": install_dir} + ), + ) + ensured_tags: list[str] = [] + monkeypatch.setattr( + INSTALL_LLAMA_PREBUILT, + "ensure_converter_scripts", + lambda install_dir, llama_tag: ensured_tags.append(llama_tag), + ) + + install_prebuilt(install_dir, "latest", "unslothai/llama.cpp", "") + + assert call_log == [("b9002", False), ("b9001", True)] + assert activated["install_dir"] == install_dir + assert ensured_tags == ["b9001"] + + +def write_linux_install_shape(install_dir: Path) -> None: + runtime_dir = install_dir / "build" / "bin" + runtime_dir.mkdir(parents = True, exist_ok = True) + (install_dir / "llama-server").write_text("#!/bin/sh\n", encoding = "utf-8") + (install_dir / "llama-quantize").write_text("#!/bin/sh\n", encoding = "utf-8") + (runtime_dir / "llama-server").write_text("#!/bin/sh\n", encoding = "utf-8") + (runtime_dir / "llama-quantize").write_text("#!/bin/sh\n", encoding = "utf-8") + (runtime_dir / "libllama.so.0").write_bytes(b"DLL") + (runtime_dir / "libggml.so.0").write_bytes(b"DLL") + (runtime_dir / "libggml-base.so.0").write_bytes(b"DLL") + (runtime_dir / "libggml-cpu-x64.so.0").write_bytes(b"DLL") + (runtime_dir / "libmtmd.so.0").write_bytes(b"DLL") + (install_dir / "convert_hf_to_gguf.py").write_text( + "#!/usr/bin/env python3\n", encoding = "utf-8" + ) + (install_dir / "gguf-py" / "gguf").mkdir(parents = True, exist_ok = True) + + +def write_windows_install_shape( + install_dir: Path, *, include_llama_dll: bool = True, include_cuda_dll: bool = False +) -> None: + runtime_dir = install_dir / "build" / "bin" / "Release" + runtime_dir.mkdir(parents = True, exist_ok = True) + (runtime_dir / "llama-server.exe").write_bytes(b"MZ") + (runtime_dir / "llama-quantize.exe").write_bytes(b"MZ") + if include_llama_dll: + (runtime_dir / "llama.dll").write_bytes(b"DLL") + if include_cuda_dll: + (runtime_dir / "ggml-cuda.dll").write_bytes(b"DLL") + (install_dir / "convert_hf_to_gguf.py").write_text( + "#!/usr/bin/env python3\n", encoding = "utf-8" + ) + (install_dir / "gguf-py" / "gguf").mkdir(parents = True, exist_ok = True) + + +def write_macos_install_shape( + install_dir: Path, + *, + include_libllama: bool = True, + include_libggml: bool = True, + include_libmtmd: bool = True, +) -> None: + runtime_dir = install_dir / "build" / "bin" + runtime_dir.mkdir(parents = True, exist_ok = True) + (install_dir / "llama-server").write_text("#!/bin/sh\n", encoding = "utf-8") + (install_dir / "llama-quantize").write_text("#!/bin/sh\n", encoding = "utf-8") + (runtime_dir / "llama-server").write_text("#!/bin/sh\n", encoding = "utf-8") + (runtime_dir / "llama-quantize").write_text("#!/bin/sh\n", encoding = "utf-8") + if include_libllama: + (runtime_dir / "libllama.0.dylib").write_bytes(b"DLL") + if include_libggml: + (runtime_dir / "libggml.0.dylib").write_bytes(b"DLL") + if include_libmtmd: + (runtime_dir / "libmtmd.0.dylib").write_bytes(b"DLL") + (install_dir / "convert_hf_to_gguf.py").write_text( + "#!/usr/bin/env python3\n", encoding = "utf-8" + ) + (install_dir / "gguf-py" / "gguf").mkdir(parents = True, exist_ok = True) + + +def test_existing_install_matches_plan_with_fingerprint_linux(tmp_path: Path): + install_dir = tmp_path / "llama.cpp" + install_dir.mkdir() + write_linux_install_shape(install_dir) + + host = HostInfo( + system = "Linux", + machine = "x86_64", + is_windows = False, + is_linux = True, + is_macos = False, + is_x86_64 = True, + is_arm64 = False, + nvidia_smi = None, + driver_cuda_version = None, + compute_caps = [], + visible_cuda_devices = None, + has_physical_nvidia = False, + has_usable_nvidia = False, + ) + choice = AssetChoice( + repo = "unslothai/llama.cpp", + tag = "release-1", + name = "llama-b9001-bin-ubuntu-x64.tar.gz", + url = "https://example.com/llama-b9001-bin-ubuntu-x64.tar.gz", + source_label = "upstream", + install_kind = "linux-cpu", + expected_sha256 = "a" * 64, + ) + checksums = ApprovedReleaseChecksums( + repo = "unslothai/llama.cpp", + release_tag = "release-1", + upstream_tag = "b9001", + source_commit = "deadbeef", + artifacts = { + source_archive_logical_name("b9001"): ApprovedArtifactHash( + asset_name = source_archive_logical_name("b9001"), + sha256 = "b" * 64, + repo = "ggml-org/llama.cpp", + kind = "upstream-source", + ), + choice.name: ApprovedArtifactHash( + asset_name = choice.name, + sha256 = choice.expected_sha256, + repo = "ggml-org/llama.cpp", + kind = "upstream-prebuilt", + ), + }, + ) + plan = INSTALL_LLAMA_PREBUILT.InstallReleasePlan( + requested_tag = "latest", + llama_tag = "b9001", + release_tag = "release-1", + attempts = [choice], + approved_checksums = checksums, + ) + + write_prebuilt_metadata( + install_dir, + requested_tag = "latest", + llama_tag = "b9001", + release_tag = "release-1", + choice = choice, + approved_checksums = checksums, + prebuilt_fallback_used = False, + ) + + assert existing_install_matches_plan(install_dir, host, plan) is True + + +def test_existing_install_matches_plan_false_without_fingerprint(tmp_path: Path): + install_dir = tmp_path / "llama.cpp" + install_dir.mkdir() + write_linux_install_shape(install_dir) + (install_dir / "UNSLOTH_PREBUILT_INFO.json").write_text( + json.dumps({"tag": "b9001", "asset": "llama-b9001-bin-ubuntu-x64.tar.gz"}) + + "\n", + encoding = "utf-8", + ) + + host = HostInfo( + system = "Linux", + machine = "x86_64", + is_windows = False, + is_linux = True, + is_macos = False, + is_x86_64 = True, + is_arm64 = False, + nvidia_smi = None, + driver_cuda_version = None, + compute_caps = [], + visible_cuda_devices = None, + has_physical_nvidia = False, + has_usable_nvidia = False, + ) + choice = AssetChoice( + repo = "unslothai/llama.cpp", + tag = "release-1", + name = "llama-b9001-bin-ubuntu-x64.tar.gz", + url = "https://example.com/x.tar.gz", + source_label = "upstream", + install_kind = "linux-cpu", + expected_sha256 = "a" * 64, + ) + checksums = ApprovedReleaseChecksums( + repo = "unslothai/llama.cpp", + release_tag = "release-1", + upstream_tag = "b9001", + source_commit = "deadbeef", + artifacts = { + source_archive_logical_name("b9001"): ApprovedArtifactHash( + asset_name = source_archive_logical_name("b9001"), + sha256 = "b" * 64, + repo = "ggml-org/llama.cpp", + kind = "upstream-source", + ), + choice.name: ApprovedArtifactHash( + asset_name = choice.name, + sha256 = choice.expected_sha256, + repo = "ggml-org/llama.cpp", + kind = "upstream-prebuilt", + ), + }, + ) + plan = INSTALL_LLAMA_PREBUILT.InstallReleasePlan( + requested_tag = "latest", + llama_tag = "b9001", + release_tag = "release-1", + attempts = [choice], + approved_checksums = checksums, + ) + + assert existing_install_matches_plan(install_dir, host, plan) is False + + +def test_existing_install_matches_plan_false_with_malformed_metadata(tmp_path: Path): + install_dir = tmp_path / "llama.cpp" + install_dir.mkdir() + write_linux_install_shape(install_dir) + (install_dir / "UNSLOTH_PREBUILT_INFO.json").write_text( + "{not-json\n", encoding = "utf-8" + ) + + host = HostInfo( + system = "Linux", + machine = "x86_64", + is_windows = False, + is_linux = True, + is_macos = False, + is_x86_64 = True, + is_arm64 = False, + nvidia_smi = None, + driver_cuda_version = None, + compute_caps = [], + visible_cuda_devices = None, + has_physical_nvidia = False, + has_usable_nvidia = False, + ) + choice = AssetChoice( + repo = "unslothai/llama.cpp", + tag = "release-1", + name = "llama-b9001-bin-ubuntu-x64.tar.gz", + url = "https://example.com/x.tar.gz", + source_label = "upstream", + install_kind = "linux-cpu", + expected_sha256 = "a" * 64, + ) + checksums = ApprovedReleaseChecksums( + repo = "unslothai/llama.cpp", + release_tag = "release-1", + upstream_tag = "b9001", + source_commit = "deadbeef", + artifacts = { + source_archive_logical_name("b9001"): ApprovedArtifactHash( + asset_name = source_archive_logical_name("b9001"), + sha256 = "b" * 64, + repo = "ggml-org/llama.cpp", + kind = "upstream-source", + ), + choice.name: ApprovedArtifactHash( + asset_name = choice.name, + sha256 = choice.expected_sha256, + repo = "ggml-org/llama.cpp", + kind = "upstream-prebuilt", + ), + }, + ) + plan = INSTALL_LLAMA_PREBUILT.InstallReleasePlan( + requested_tag = "latest", + llama_tag = "b9001", + release_tag = "release-1", + attempts = [choice], + approved_checksums = checksums, + ) + + assert existing_install_matches_plan(install_dir, host, plan) is False + + +def test_existing_install_matches_plan_windows_cpu_requires_llama_dll(tmp_path: Path): + install_dir = tmp_path / "llama.cpp" + install_dir.mkdir() + write_windows_install_shape(install_dir, include_llama_dll = True) + + host = HostInfo( + system = "Windows", + machine = "AMD64", + is_windows = True, + is_linux = False, + is_macos = False, + is_x86_64 = True, + is_arm64 = False, + nvidia_smi = None, + driver_cuda_version = None, + compute_caps = [], + visible_cuda_devices = None, + has_physical_nvidia = False, + has_usable_nvidia = False, + ) + choice = AssetChoice( + repo = "unslothai/llama.cpp", + tag = "release-1", + name = "llama-b9001-bin-win-cpu-x64.zip", + url = "https://example.com/x.zip", + source_label = "published", + install_kind = "windows-cpu", + expected_sha256 = "a" * 64, + ) + checksums = ApprovedReleaseChecksums( + repo = "unslothai/llama.cpp", + release_tag = "release-1", + upstream_tag = "b9001", + source_commit = "deadbeef", + artifacts = { + source_archive_logical_name("b9001"): ApprovedArtifactHash( + asset_name = source_archive_logical_name("b9001"), + sha256 = "b" * 64, + repo = "ggml-org/llama.cpp", + kind = "upstream-source", + ), + choice.name: ApprovedArtifactHash( + asset_name = choice.name, + sha256 = choice.expected_sha256, + repo = "unslothai/llama.cpp", + kind = "prebuilt", + ), + }, + ) + plan = INSTALL_LLAMA_PREBUILT.InstallReleasePlan( + requested_tag = "latest", + llama_tag = "b9001", + release_tag = "release-1", + attempts = [choice], + approved_checksums = checksums, + ) + write_prebuilt_metadata( + install_dir, + requested_tag = "latest", + llama_tag = "b9001", + release_tag = "release-1", + choice = choice, + approved_checksums = checksums, + prebuilt_fallback_used = False, + ) + + assert existing_install_matches_plan(install_dir, host, plan) is True + (install_dir / "build" / "bin" / "Release" / "llama.dll").unlink() + assert existing_install_matches_plan(install_dir, host, plan) is False + + +def test_existing_install_matches_plan_windows_cuda_requires_cuda_dll(tmp_path: Path): + install_dir = tmp_path / "llama.cpp" + install_dir.mkdir() + write_windows_install_shape( + install_dir, include_llama_dll = True, include_cuda_dll = True + ) + + host = HostInfo( + system = "Windows", + machine = "AMD64", + is_windows = True, + is_linux = False, + is_macos = False, + is_x86_64 = True, + is_arm64 = False, + nvidia_smi = None, + driver_cuda_version = (12, 4), + compute_caps = [], + visible_cuda_devices = None, + has_physical_nvidia = False, + has_usable_nvidia = True, + ) + choice = AssetChoice( + repo = "unslothai/llama.cpp", + tag = "release-1", + name = "llama-b9001-bin-win-cuda-12.4-x64.zip", + url = "https://example.com/x.zip", + source_label = "published", + install_kind = "windows-cuda", + runtime_line = "cuda12", + expected_sha256 = "a" * 64, + ) + checksums = ApprovedReleaseChecksums( + repo = "unslothai/llama.cpp", + release_tag = "release-1", + upstream_tag = "b9001", + source_commit = "deadbeef", + artifacts = { + source_archive_logical_name("b9001"): ApprovedArtifactHash( + asset_name = source_archive_logical_name("b9001"), + sha256 = "b" * 64, + repo = "ggml-org/llama.cpp", + kind = "upstream-source", + ), + choice.name: ApprovedArtifactHash( + asset_name = choice.name, + sha256 = choice.expected_sha256, + repo = "unslothai/llama.cpp", + kind = "prebuilt", + ), + }, + ) + plan = INSTALL_LLAMA_PREBUILT.InstallReleasePlan( + requested_tag = "latest", + llama_tag = "b9001", + release_tag = "release-1", + attempts = [choice], + approved_checksums = checksums, + ) + write_prebuilt_metadata( + install_dir, + requested_tag = "latest", + llama_tag = "b9001", + release_tag = "release-1", + choice = choice, + approved_checksums = checksums, + prebuilt_fallback_used = False, + ) + + assert existing_install_matches_plan(install_dir, host, plan) is True + (install_dir / "build" / "bin" / "Release" / "ggml-cuda.dll").unlink() + assert existing_install_matches_plan(install_dir, host, plan) is False + + +def test_existing_install_matches_plan_macos_requires_dylibs(tmp_path: Path): + install_dir = tmp_path / "llama.cpp" + install_dir.mkdir() + write_macos_install_shape(install_dir) + + host = HostInfo( + system = "Darwin", + machine = "arm64", + is_windows = False, + is_linux = False, + is_macos = True, + is_x86_64 = False, + is_arm64 = True, + nvidia_smi = None, + driver_cuda_version = None, + compute_caps = [], + visible_cuda_devices = None, + has_physical_nvidia = False, + has_usable_nvidia = False, + ) + choice = AssetChoice( + repo = "unslothai/llama.cpp", + tag = "release-1", + name = "llama-b9001-bin-macos-arm64.tar.gz", + url = "https://example.com/x.tar.gz", + source_label = "published", + install_kind = "macos-arm64", + expected_sha256 = "a" * 64, + ) + checksums = ApprovedReleaseChecksums( + repo = "unslothai/llama.cpp", + release_tag = "release-1", + upstream_tag = "b9001", + source_commit = "deadbeef", + artifacts = { + source_archive_logical_name("b9001"): ApprovedArtifactHash( + asset_name = source_archive_logical_name("b9001"), + sha256 = "b" * 64, + repo = "ggml-org/llama.cpp", + kind = "upstream-source", + ), + choice.name: ApprovedArtifactHash( + asset_name = choice.name, + sha256 = choice.expected_sha256, + repo = "unslothai/llama.cpp", + kind = "prebuilt", + ), + }, + ) + plan = INSTALL_LLAMA_PREBUILT.InstallReleasePlan( + requested_tag = "latest", + llama_tag = "b9001", + release_tag = "release-1", + attempts = [choice], + approved_checksums = checksums, + ) + write_prebuilt_metadata( + install_dir, + requested_tag = "latest", + llama_tag = "b9001", + release_tag = "release-1", + choice = choice, + approved_checksums = checksums, + prebuilt_fallback_used = False, + ) + + assert existing_install_matches_plan(install_dir, host, plan) is True + (install_dir / "build" / "bin" / "libggml.0.dylib").unlink() + assert existing_install_matches_plan(install_dir, host, plan) is False + + +def test_install_prebuilt_skips_download_when_existing_install_matches( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +): + install_dir = tmp_path / "llama.cpp" + install_dir.mkdir() + write_linux_install_shape(install_dir) + + host = HostInfo( + system = "Linux", + machine = "x86_64", + is_windows = False, + is_linux = True, + is_macos = False, + is_x86_64 = True, + is_arm64 = False, + nvidia_smi = None, + driver_cuda_version = None, + compute_caps = [], + visible_cuda_devices = None, + has_physical_nvidia = False, + has_usable_nvidia = False, + ) + choice = AssetChoice( + repo = "unslothai/llama.cpp", + tag = "release-1", + name = "llama-b9001-bin-ubuntu-x64.tar.gz", + url = "https://example.com/llama-b9001-bin-ubuntu-x64.tar.gz", + source_label = "upstream", + install_kind = "linux-cpu", + expected_sha256 = "a" * 64, + ) + checksums = ApprovedReleaseChecksums( + repo = "unslothai/llama.cpp", + release_tag = "release-1", + upstream_tag = "b9001", + source_commit = "deadbeef", + artifacts = { + source_archive_logical_name("b9001"): ApprovedArtifactHash( + asset_name = source_archive_logical_name("b9001"), + sha256 = "b" * 64, + repo = "ggml-org/llama.cpp", + kind = "upstream-source", + ), + choice.name: ApprovedArtifactHash( + asset_name = choice.name, + sha256 = choice.expected_sha256, + repo = "ggml-org/llama.cpp", + kind = "upstream-prebuilt", + ), + }, + ) + plan = INSTALL_LLAMA_PREBUILT.InstallReleasePlan( + requested_tag = "latest", + llama_tag = "b9001", + release_tag = "release-1", + attempts = [choice], + approved_checksums = checksums, + ) + + write_prebuilt_metadata( + install_dir, + requested_tag = "latest", + llama_tag = "b9001", + release_tag = "release-1", + choice = choice, + approved_checksums = checksums, + prebuilt_fallback_used = False, + ) + + monkeypatch.setattr(INSTALL_LLAMA_PREBUILT, "detect_host", lambda: host) + monkeypatch.setattr( + INSTALL_LLAMA_PREBUILT, + "resolve_install_release_plans", + lambda llama_tag, host, published_repo, published_release_tag: ( + "latest", + [plan], + ), + ) + monkeypatch.setattr( + INSTALL_LLAMA_PREBUILT, + "download_validation_model", + lambda *args, **kwargs: (_ for _ in ()).throw( + AssertionError( + "matching install should skip before validation model download" + ) + ), + ) + + install_prebuilt(install_dir, "latest", "unslothai/llama.cpp", "") + + +def test_install_prebuilt_does_not_skip_unhealthy_existing_install( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +): + install_dir = tmp_path / "llama.cpp" + install_dir.mkdir() + write_linux_install_shape(install_dir) + (install_dir / "llama-quantize").unlink() + + host = HostInfo( + system = "Linux", + machine = "x86_64", + is_windows = False, + is_linux = True, + is_macos = False, + is_x86_64 = True, + is_arm64 = False, + nvidia_smi = None, + driver_cuda_version = None, + compute_caps = [], + visible_cuda_devices = None, + has_physical_nvidia = False, + has_usable_nvidia = False, + ) + choice = AssetChoice( + repo = "unslothai/llama.cpp", + tag = "release-1", + name = "llama-b9001-bin-ubuntu-x64.tar.gz", + url = "https://example.com/llama-b9001-bin-ubuntu-x64.tar.gz", + source_label = "upstream", + install_kind = "linux-cpu", + expected_sha256 = "a" * 64, + ) + checksums = ApprovedReleaseChecksums( + repo = "unslothai/llama.cpp", + release_tag = "release-1", + upstream_tag = "b9001", + source_commit = "deadbeef", + artifacts = { + source_archive_logical_name("b9001"): ApprovedArtifactHash( + asset_name = source_archive_logical_name("b9001"), + sha256 = "b" * 64, + repo = "ggml-org/llama.cpp", + kind = "upstream-source", + ), + choice.name: ApprovedArtifactHash( + asset_name = choice.name, + sha256 = choice.expected_sha256, + repo = "ggml-org/llama.cpp", + kind = "upstream-prebuilt", + ), + }, + ) + plan = INSTALL_LLAMA_PREBUILT.InstallReleasePlan( + requested_tag = "latest", + llama_tag = "b9001", + release_tag = "release-1", + attempts = [choice], + approved_checksums = checksums, + ) + + write_prebuilt_metadata( + install_dir, + requested_tag = "latest", + llama_tag = "b9001", + release_tag = "release-1", + choice = choice, + approved_checksums = checksums, + prebuilt_fallback_used = False, + ) + + monkeypatch.setattr(INSTALL_LLAMA_PREBUILT, "detect_host", lambda: host) + monkeypatch.setattr( + INSTALL_LLAMA_PREBUILT, + "resolve_install_release_plans", + lambda llama_tag, host, published_repo, published_release_tag: ( + "latest", + [plan], + ), + ) + monkeypatch.setattr( + INSTALL_LLAMA_PREBUILT, + "download_validation_model", + lambda *args, **kwargs: (_ for _ in ()).throw( + AssertionError("unhealthy install must continue into normal install flow") + ), + ) + + with pytest.raises( + AssertionError, match = "unhealthy install must continue into normal install flow" + ): + install_prebuilt(install_dir, "latest", "unslothai/llama.cpp", "") + + +def test_install_prebuilt_skips_when_older_release_fallback_matches_existing_install( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +): + install_dir = tmp_path / "llama.cpp" + install_dir.mkdir() + write_linux_install_shape(install_dir) + + host = HostInfo( + system = "Linux", + machine = "x86_64", + is_windows = False, + is_linux = True, + is_macos = False, + is_x86_64 = True, + is_arm64 = False, + nvidia_smi = None, + driver_cuda_version = None, + compute_caps = [], + visible_cuda_devices = None, + has_physical_nvidia = False, + has_usable_nvidia = False, + ) + latest_choice = AssetChoice( + repo = "unslothai/llama.cpp", + tag = "release-2", + name = "llama-b9002-bin-ubuntu-x64.tar.gz", + url = "https://example.com/llama-b9002-bin-ubuntu-x64.tar.gz", + source_label = "upstream", + install_kind = "linux-cpu", + expected_sha256 = "c" * 64, + ) + fallback_choice = AssetChoice( + repo = "unslothai/llama.cpp", + tag = "release-1", + name = "llama-b9001-bin-ubuntu-x64.tar.gz", + url = "https://example.com/llama-b9001-bin-ubuntu-x64.tar.gz", + source_label = "upstream", + install_kind = "linux-cpu", + expected_sha256 = "a" * 64, + ) + latest_checksums = ApprovedReleaseChecksums( + repo = "unslothai/llama.cpp", + release_tag = "release-2", + upstream_tag = "b9002", + source_commit = "beadfeed", + artifacts = { + source_archive_logical_name("b9002"): ApprovedArtifactHash( + asset_name = source_archive_logical_name("b9002"), + sha256 = "d" * 64, + repo = "ggml-org/llama.cpp", + kind = "upstream-source", + ), + latest_choice.name: ApprovedArtifactHash( + asset_name = latest_choice.name, + sha256 = latest_choice.expected_sha256, + repo = "ggml-org/llama.cpp", + kind = "upstream-prebuilt", + ), + }, + ) + fallback_checksums = ApprovedReleaseChecksums( + repo = "unslothai/llama.cpp", + release_tag = "release-1", + upstream_tag = "b9001", + source_commit = "deadbeef", + artifacts = { + source_archive_logical_name("b9001"): ApprovedArtifactHash( + asset_name = source_archive_logical_name("b9001"), + sha256 = "b" * 64, + repo = "ggml-org/llama.cpp", + kind = "upstream-source", + ), + fallback_choice.name: ApprovedArtifactHash( + asset_name = fallback_choice.name, + sha256 = fallback_choice.expected_sha256, + repo = "ggml-org/llama.cpp", + kind = "upstream-prebuilt", + ), + }, + ) + latest_plan = INSTALL_LLAMA_PREBUILT.InstallReleasePlan( + requested_tag = "latest", + llama_tag = "b9002", + release_tag = "release-2", + attempts = [latest_choice], + approved_checksums = latest_checksums, + ) + fallback_plan = INSTALL_LLAMA_PREBUILT.InstallReleasePlan( + requested_tag = "latest", + llama_tag = "b9001", + release_tag = "release-1", + attempts = [fallback_choice], + approved_checksums = fallback_checksums, + ) + + write_prebuilt_metadata( + install_dir, + requested_tag = "latest", + llama_tag = "b9001", + release_tag = "release-1", + choice = fallback_choice, + approved_checksums = fallback_checksums, + prebuilt_fallback_used = True, + ) + + monkeypatch.setattr(INSTALL_LLAMA_PREBUILT, "detect_host", lambda: host) + monkeypatch.setattr( + INSTALL_LLAMA_PREBUILT, + "resolve_install_release_plans", + lambda llama_tag, host, published_repo, published_release_tag: ( + "latest", + [latest_plan, fallback_plan], + ), + ) + monkeypatch.setattr( + INSTALL_LLAMA_PREBUILT, + "download_validation_model", + lambda probe_path, cache_path: probe_path.write_bytes(b"probe"), + ) + + call_log: list[str] = [] + + def fake_validate( + attempts, + host, + install_dir, + work_dir, + probe_path, + *, + requested_tag, + llama_tag, + release_tag, + approved_checksums, + initial_fallback_used = False, + existing_install_dir = None, + ): + call_log.append(llama_tag) + raise PrebuiltFallback("validation failed for latest release") + + monkeypatch.setattr( + INSTALL_LLAMA_PREBUILT, + "validate_prebuilt_attempts", + fake_validate, + ) + monkeypatch.setattr( + INSTALL_LLAMA_PREBUILT, + "activate_install_tree", + lambda *args, **kwargs: (_ for _ in ()).throw( + AssertionError("matching fallback install should not reactivate") + ), + ) + + install_prebuilt(install_dir, "latest", "unslothai/llama.cpp", "") + + assert call_log == ["b9002"] + + +def test_install_prebuilt_skips_same_release_fallback_attempt_when_installed( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +): + install_dir = tmp_path / "llama.cpp" + install_dir.mkdir() + write_linux_install_shape(install_dir) + + host = HostInfo( + system = "Linux", + machine = "x86_64", + is_windows = False, + is_linux = True, + is_macos = False, + is_x86_64 = True, + is_arm64 = False, + nvidia_smi = None, + driver_cuda_version = None, + compute_caps = [], + visible_cuda_devices = None, + has_physical_nvidia = False, + has_usable_nvidia = False, + ) + first_choice = AssetChoice( + repo = "unslothai/llama.cpp", + tag = "release-1", + name = "llama-b9001-bin-ubuntu-x64-bad.tar.gz", + url = "https://example.com/llama-b9001-bin-ubuntu-x64-bad.tar.gz", + source_label = "published", + install_kind = "linux-cpu", + expected_sha256 = "c" * 64, + ) + fallback_choice = AssetChoice( + repo = "unslothai/llama.cpp", + tag = "release-1", + name = "llama-b9001-bin-ubuntu-x64-good.tar.gz", + url = "https://example.com/llama-b9001-bin-ubuntu-x64-good.tar.gz", + source_label = "upstream", + install_kind = "linux-cpu", + expected_sha256 = "a" * 64, + ) + checksums = ApprovedReleaseChecksums( + repo = "unslothai/llama.cpp", + release_tag = "release-1", + upstream_tag = "b9001", + source_commit = "deadbeef", + artifacts = { + source_archive_logical_name("b9001"): ApprovedArtifactHash( + asset_name = source_archive_logical_name("b9001"), + sha256 = "b" * 64, + repo = "ggml-org/llama.cpp", + kind = "upstream-source", + ), + first_choice.name: ApprovedArtifactHash( + asset_name = first_choice.name, + sha256 = first_choice.expected_sha256, + repo = "unslothai/llama.cpp", + kind = "prebuilt", + ), + fallback_choice.name: ApprovedArtifactHash( + asset_name = fallback_choice.name, + sha256 = fallback_choice.expected_sha256, + repo = "ggml-org/llama.cpp", + kind = "upstream-prebuilt", + ), + }, + ) + plan = INSTALL_LLAMA_PREBUILT.InstallReleasePlan( + requested_tag = "latest", + llama_tag = "b9001", + release_tag = "release-1", + attempts = [first_choice, fallback_choice], + approved_checksums = checksums, + ) + + write_prebuilt_metadata( + install_dir, + requested_tag = "latest", + llama_tag = "b9001", + release_tag = "release-1", + choice = fallback_choice, + approved_checksums = checksums, + prebuilt_fallback_used = True, + ) + assert ( + existing_install_matches_choice( + install_dir, + host, + llama_tag = "b9001", + release_tag = "release-1", + choice = fallback_choice, + approved_checksums = checksums, + ) + is True + ) + + monkeypatch.setattr(INSTALL_LLAMA_PREBUILT, "detect_host", lambda: host) + monkeypatch.setattr( + INSTALL_LLAMA_PREBUILT, + "resolve_install_release_plans", + lambda llama_tag, host, published_repo, published_release_tag: ( + "latest", + [plan], + ), + ) + monkeypatch.setattr( + INSTALL_LLAMA_PREBUILT, + "download_validation_model", + lambda probe_path, cache_path: probe_path.write_bytes(b"probe"), + ) + + attempted_names: list[str] = [] + + def fake_validate_choice( + choice, + host, + staging_dir, + work_dir, + probe_path, + *, + requested_tag, + llama_tag, + release_tag, + approved_checksums, + prebuilt_fallback_used, + quantized_path, + ): + attempted_names.append(choice.name) + if choice.name == first_choice.name: + raise PrebuiltFallback("newest candidate failed") + raise AssertionError("installed fallback candidate should have been skipped") + + monkeypatch.setattr( + INSTALL_LLAMA_PREBUILT, + "validate_prebuilt_choice", + fake_validate_choice, + ) + monkeypatch.setattr( + INSTALL_LLAMA_PREBUILT, + "activate_install_tree", + lambda *args, **kwargs: (_ for _ in ()).throw( + AssertionError("installed fallback candidate should not be activated") + ), + ) + + install_prebuilt(install_dir, "latest", "unslothai/llama.cpp", "") + + assert attempted_names == [first_choice.name] + + +def test_install_prebuilt_same_tag_upstream_failure_uses_older_unsloth_release_plan( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +): + install_dir = tmp_path / "llama.cpp" + host = HostInfo( + system = "Linux", + machine = "x86_64", + is_windows = False, + is_linux = True, + is_macos = False, + is_x86_64 = True, + is_arm64 = False, + nvidia_smi = None, + driver_cuda_version = None, + compute_caps = [], + visible_cuda_devices = None, + has_physical_nvidia = False, + has_usable_nvidia = False, + ) + + same_tag_upstream_choice = AssetChoice( + repo = "ggml-org/llama.cpp", + tag = "b9002", + name = "llama-b9002-bin-ubuntu-x64.tar.gz", + url = "https://example.com/llama-b9002-bin-ubuntu-x64.tar.gz", + source_label = "upstream", + install_kind = "linux-cpu", + expected_sha256 = "a" * 64, + ) + older_release_choice = AssetChoice( + repo = "unslothai/llama.cpp", + tag = "release-1", + name = "llama-b9001-bin-ubuntu-x64.tar.gz", + url = "https://example.com/llama-b9001-bin-ubuntu-x64.tar.gz", + source_label = "upstream", + install_kind = "linux-cpu", + expected_sha256 = "b" * 64, + ) + latest_plan = INSTALL_LLAMA_PREBUILT.InstallReleasePlan( + requested_tag = "latest", + llama_tag = "b9002", + release_tag = "release-2", + attempts = [same_tag_upstream_choice], + approved_checksums = ApprovedReleaseChecksums( + repo = "unslothai/llama.cpp", + release_tag = "release-2", + upstream_tag = "b9002", + source_commit = None, + artifacts = {}, + ), + ) + older_plan = INSTALL_LLAMA_PREBUILT.InstallReleasePlan( + requested_tag = "latest", + llama_tag = "b9001", + release_tag = "release-1", + attempts = [older_release_choice], + approved_checksums = ApprovedReleaseChecksums( + repo = "unslothai/llama.cpp", + release_tag = "release-1", + upstream_tag = "b9001", + source_commit = None, + artifacts = {}, + ), + ) + + monkeypatch.setattr(INSTALL_LLAMA_PREBUILT, "detect_host", lambda: host) + monkeypatch.setattr( + INSTALL_LLAMA_PREBUILT, + "resolve_install_release_plans", + lambda llama_tag, host, published_repo, published_release_tag: ( + "latest", + [latest_plan, older_plan], + ), + ) + monkeypatch.setattr( + INSTALL_LLAMA_PREBUILT, + "download_validation_model", + lambda probe_path, cache_path: probe_path.write_bytes(b"probe"), + ) + monkeypatch.setattr( + INSTALL_LLAMA_PREBUILT, + "latest_upstream_release_tag", + lambda: (_ for _ in ()).throw( + AssertionError("install fallback should not walk upstream releases") + ), + ) + + attempted = [] + + def fake_validate( + attempts, + host, + install_dir, + work_dir, + probe_path, + *, + requested_tag, + llama_tag, + release_tag, + approved_checksums, + initial_fallback_used = False, + existing_install_dir = None, + ): + attempted.append((llama_tag, release_tag, attempts[0].source_label)) + if llama_tag == "b9002": + raise PrebuiltFallback("same-tag upstream asset failed validation") + staging_dir = create_install_staging_dir(install_dir) + (staging_dir / "marker.txt").write_text("ready\n") + return attempts[0], staging_dir, initial_fallback_used + + monkeypatch.setattr( + INSTALL_LLAMA_PREBUILT, "validate_prebuilt_attempts", fake_validate + ) + + activated = {} + monkeypatch.setattr( + INSTALL_LLAMA_PREBUILT, + "activate_install_tree", + lambda staging_dir, install_dir, host: activated.update( + {"staging_dir": staging_dir, "install_dir": install_dir} + ), + ) + monkeypatch.setattr( + INSTALL_LLAMA_PREBUILT, + "ensure_converter_scripts", + lambda install_dir, llama_tag: None, + ) + + install_prebuilt(install_dir, "latest", "unslothai/llama.cpp", "") + + assert attempted == [ + ("b9002", "release-2", "upstream"), + ("b9001", "release-1", "upstream"), + ] + assert activated["install_dir"] == install_dir + + def io_bytes(data: bytes): return io.BytesIO(data) diff --git a/tests/studio/install/test_pr4562_bugfixes.py b/tests/studio/install/test_pr4562_bugfixes.py index 9b8c6219de..b1fe00f3b3 100644 --- a/tests/studio/install/test_pr4562_bugfixes.py +++ b/tests/studio/install/test_pr4562_bugfixes.py @@ -6,7 +6,7 @@ Tests cover: - Bug 2: Source-build fallback ignores pinned tag (both .sh and .ps1) - Bug 3: Unix fallback deletes install before checking prerequisites - Bug 4: Linux LD_LIBRARY_PATH missing build/bin - - "latest" tag resolution fallback chain (Unsloth -> ggml-org -> raw) + - "latest" tag resolution fallback chain (helper -> raw) - Cross-platform binary_env (Linux, macOS, Windows) - Edge cases: malformed JSON, empty responses, env overrides @@ -40,6 +40,10 @@ SPEC.loader.exec_module(MOD) binary_env = MOD.binary_env HostInfo = MOD.HostInfo resolve_requested_llama_tag = MOD.resolve_requested_llama_tag +PublishedReleaseBundle = MOD.PublishedReleaseBundle +ApprovedArtifactHash = MOD.ApprovedArtifactHash +ApprovedReleaseChecksums = MOD.ApprovedReleaseChecksums +source_archive_logical_name = MOD.source_archive_logical_name SETUP_SH = PACKAGE_ROOT / "studio" / "setup.sh" SETUP_PS1 = PACKAGE_ROOT / "studio" / "setup.ps1" @@ -240,6 +244,57 @@ class TestResolveRequestedLlamaTag: monkeypatch.setattr(MOD, "latest_upstream_release_tag", lambda: "b5555") assert resolve_requested_llama_tag("") == "b5555" + def test_latest_with_published_repo_uses_latest_valid_published_release( + self, monkeypatch: pytest.MonkeyPatch + ): + invalid = PublishedReleaseBundle( + repo = "unslothai/llama.cpp", + release_tag = "v2.0", + upstream_tag = "b9000", + assets = {}, + manifest_asset_name = "llama-prebuilt-manifest.json", + artifacts = [], + selection_log = [], + ) + valid = PublishedReleaseBundle( + repo = "unslothai/llama.cpp", + release_tag = "v1.0", + upstream_tag = "b8999", + assets = {}, + manifest_asset_name = "llama-prebuilt-manifest.json", + artifacts = [], + selection_log = [], + ) + + monkeypatch.setattr( + MOD, + "iter_published_release_bundles", + lambda repo, published_release_tag = "": iter([invalid, valid]), + ) + + def fake_load(repo, release_tag): + if release_tag == "v2.0": + raise MOD.PrebuiltFallback("checksum asset missing") + return ApprovedReleaseChecksums( + repo = repo, + release_tag = release_tag, + upstream_tag = "b8999", + source_commit = None, + artifacts = { + source_archive_logical_name("b8999"): ApprovedArtifactHash( + asset_name = source_archive_logical_name("b8999"), + sha256 = "a" * 64, + repo = "ggml-org/llama.cpp", + kind = "upstream-source", + ) + }, + ) + + monkeypatch.setattr(MOD, "load_approved_release_checksums", fake_load) + monkeypatch.setattr(MOD, "latest_upstream_release_tag", lambda: "b7777") + + assert resolve_requested_llama_tag("latest", "unslothai/llama.cpp") == "b8999" + # ========================================================================= # TEST GROUP C: setup.sh logic (bash subprocess tests) @@ -429,140 +484,61 @@ class TestSetupShLogic: # TEST GROUP D: "latest" tag resolution (bash subprocess) # ========================================================================= class TestLatestTagResolution: - """Test the fallback chain: Unsloth API -> ggml-org API -> raw.""" + """Test the fallback chain: helper resolver -> raw.""" RESOLVE_TEMPLATE = textwrap.dedent("""\ - export PATH="{mock_bin}:$PATH" _REQUESTED_LLAMA_TAG="{requested_tag}" _RESOLVED_LLAMA_TAG="" - _RESOLVE_UPSTREAM_STATUS=1 - _HELPER_RELEASE_REPO="unslothai/llama.cpp" - if [ "$_RESOLVE_UPSTREAM_STATUS" -ne 0 ] || [ -z "$_RESOLVED_LLAMA_TAG" ]; then - if [ "$_REQUESTED_LLAMA_TAG" = "latest" ]; then - _RESOLVED_LLAMA_TAG="$(curl -fsSL "https://api.github.com/repos/${{_HELPER_RELEASE_REPO}}/releases/latest" 2>/dev/null | python -c "import sys,json; print(json.load(sys.stdin)['tag_name'])" 2>/dev/null)" || _RESOLVED_LLAMA_TAG="" - if [ -z "$_RESOLVED_LLAMA_TAG" ]; then - _RESOLVED_LLAMA_TAG="$(curl -fsSL https://api.github.com/repos/ggml-org/llama.cpp/releases/latest 2>/dev/null | python -c "import sys,json; print(json.load(sys.stdin)['tag_name'])" 2>/dev/null)" || _RESOLVED_LLAMA_TAG="" - fi - fi - if [ -z "$_RESOLVED_LLAMA_TAG" ]; then - _RESOLVED_LLAMA_TAG="$_REQUESTED_LLAMA_TAG" - fi + _RESOLVE_UPSTREAM_STATUS={resolve_status} + if [ "$_RESOLVE_UPSTREAM_STATUS" -eq 0 ] && [ -n "{resolved_tag}" ]; then + _RESOLVED_LLAMA_TAG="{resolved_tag}" + else + _RESOLVED_LLAMA_TAG="$_REQUESTED_LLAMA_TAG" fi echo "$_RESOLVED_LLAMA_TAG" """) - @staticmethod - def _make_curl_mock( - mock_bin: Path, unsloth_response: str | None, ggml_response: str | None - ): - """Create a curl mock that returns different responses per repo.""" - lines = ["#!/bin/bash"] - if unsloth_response is not None: - lines.append( - f'if echo "$*" | grep -q "unslothai/llama.cpp"; then echo \'{unsloth_response}\'; exit 0; fi' - ) - else: - lines.append( - 'if echo "$*" | grep -q "unslothai/llama.cpp"; then exit 1; fi' - ) - if ggml_response is not None: - lines.append( - f'if echo "$*" | grep -q "ggml-org/llama.cpp"; then echo \'{ggml_response}\'; exit 0; fi' - ) - else: - lines.append('if echo "$*" | grep -q "ggml-org/llama.cpp"; then exit 1; fi') - lines.append("exit 1") - curl_path = mock_bin / "curl" - curl_path.write_text("\n".join(lines) + "\n") - curl_path.chmod(0o755) - def _run_resolve( self, tmp_path: Path, requested_tag: str, - unsloth_resp: str | None, - ggml_resp: str | None, + resolved_tag: str, + resolve_status: int, ) -> str: - mock_bin = tmp_path / "mock_bin" - mock_bin.mkdir(exist_ok = True) - self._make_curl_mock(mock_bin, unsloth_resp, ggml_resp) script = self.RESOLVE_TEMPLATE.format( - mock_bin = mock_bin, requested_tag = requested_tag + requested_tag = requested_tag, + resolved_tag = resolved_tag, + resolve_status = resolve_status, ) return run_bash(script) - def test_unsloth_succeeds(self, tmp_path: Path): + def test_helper_resolution_succeeds(self, tmp_path: Path): output = self._run_resolve( tmp_path, "latest", - unsloth_resp = '{"tag_name":"b8508"}', - ggml_resp = '{"tag_name":"b9000"}', + resolved_tag = "b8508", + resolve_status = 0, ) assert output == "b8508" - def test_unsloth_fails_ggml_succeeds(self, tmp_path: Path): + def test_helper_resolution_falls_back_to_raw_requested_tag(self, tmp_path: Path): output = self._run_resolve( tmp_path, "latest", - unsloth_resp = None, - ggml_resp = '{"tag_name":"b9000"}', - ) - assert output == "b9000" - - def test_both_fail_raw_fallback(self, tmp_path: Path): - output = self._run_resolve( - tmp_path, - "latest", - unsloth_resp = None, - ggml_resp = None, + resolved_tag = "", + resolve_status = 1, ) assert output == "latest" - def test_concrete_tag_passes_through(self, tmp_path: Path): + def test_concrete_tag_passes_through_when_helper_fails(self, tmp_path: Path): output = self._run_resolve( tmp_path, "b7777", - unsloth_resp = '{"tag_name":"b8508"}', - ggml_resp = '{"tag_name":"b9000"}', + resolved_tag = "", + resolve_status = 1, ) assert output == "b7777" - def test_unsloth_malformed_json_falls_through(self, tmp_path: Path): - output = self._run_resolve( - tmp_path, - "latest", - unsloth_resp = '{"bad_key":"no_tag"}', - ggml_resp = '{"tag_name":"b9001"}', - ) - assert output == "b9001" - - def test_both_malformed_json_raw_fallback(self, tmp_path: Path): - output = self._run_resolve( - tmp_path, - "latest", - unsloth_resp = '{"bad":"data"}', - ggml_resp = '{"also":"bad"}', - ) - assert output == "latest" - - def test_unsloth_empty_body_falls_through(self, tmp_path: Path): - output = self._run_resolve( - tmp_path, - "latest", - unsloth_resp = "", - ggml_resp = '{"tag_name":"b7000"}', - ) - assert output == "b7000" - - def test_unsloth_empty_tag_name_falls_through(self, tmp_path: Path): - output = self._run_resolve( - tmp_path, - "latest", - unsloth_resp = '{"tag_name":""}', - ggml_resp = '{"tag_name":"b6000"}', - ) - assert output == "b6000" - def test_env_override_unsloth_llama_tag(self): output = run_bash( 'echo "${UNSLOTH_LLAMA_TAG:-latest}"', @@ -593,10 +569,10 @@ class TestSourceCodePatterns: def test_setup_sh_no_rm_before_prereq_check(self): """rm -rf must appear AFTER cmake/git checks, not before.""" content = SETUP_SH.read_text() - # Find the source-build block - idx_else = content.find("# Check prerequisites") - assert idx_else != -1 - block = content[idx_else:] + # Anchor on the source-build cmake check block. + idx_block = content.find("command -v cmake") + assert idx_block != -1 + block = content[idx_block:] # rm -rf should appear after the cmake/git checks idx_cmake = block.find("command -v cmake") idx_git = block.find("command -v git") @@ -619,14 +595,12 @@ class TestSourceCodePatterns: '_RESOLVED_LLAMA_TAG" != "latest"' in content ), "Should guard against literal 'latest' tag" - def test_setup_sh_latest_resolution_queries_unsloth_first(self): - """The Unsloth repo should be queried before ggml-org.""" + def test_setup_sh_latest_resolution_uses_helper_only(self): + """Shell fallback should rely on helper output, not raw GitHub API tag_name.""" content = SETUP_SH.read_text() - idx_unsloth = content.find("_HELPER_RELEASE_REPO}/releases/latest") - idx_ggml = content.find("ggml-org/llama.cpp/releases/latest") - assert idx_unsloth != -1, "Unsloth API query not found" - assert idx_ggml != -1, "ggml-org API query not found" - assert idx_unsloth < idx_ggml, "Unsloth should be queried before ggml-org" + assert "--resolve-llama-tag" in content + assert "_HELPER_RELEASE_REPO}/releases/latest" not in content + assert "ggml-org/llama.cpp/releases/latest" not in content def test_setup_ps1_uses_checkout_b(self): """PS1 should use checkout -B, not checkout --force FETCH_HEAD.""" @@ -658,14 +632,12 @@ class TestSourceCodePatterns: f"Found 'git pull' in llama.cpp build section at line {i+1}" ) - def test_setup_ps1_latest_resolution_queries_unsloth_first(self): - """PS1 should query Unsloth repo before ggml-org.""" + def test_setup_ps1_latest_resolution_uses_helper_only(self): + """PS1 fallback should rely on helper output, not raw GitHub API tag_name.""" content = SETUP_PS1.read_text() - idx_unsloth = content.find("$HelperReleaseRepo/releases/latest") - idx_ggml = content.find("ggml-org/llama.cpp/releases/latest") - assert idx_unsloth != -1, "Unsloth API query not found in PS1" - assert idx_ggml != -1, "ggml-org API query not found in PS1" - assert idx_unsloth < idx_ggml, "Unsloth should be queried before ggml-org" + assert "--resolve-llama-tag" in content + assert "$HelperReleaseRepo/releases/latest" not in content + assert "ggml-org/llama.cpp/releases/latest" not in content def test_binary_env_linux_has_binary_parent(self): """The Linux branch of binary_env should include binary_path.parent.""" diff --git a/tests/studio/install/test_selection_logic.py b/tests/studio/install/test_selection_logic.py index 906c978b0d..d7bea4bb5c 100644 --- a/tests/studio/install/test_selection_logic.py +++ b/tests/studio/install/test_selection_logic.py @@ -54,6 +54,12 @@ apply_approved_hashes = INSTALL_LLAMA_PREBUILT.apply_approved_hashes linux_cuda_choice_from_release = INSTALL_LLAMA_PREBUILT.linux_cuda_choice_from_release windows_cuda_attempts = INSTALL_LLAMA_PREBUILT.windows_cuda_attempts resolve_upstream_asset_choice = INSTALL_LLAMA_PREBUILT.resolve_upstream_asset_choice +resolve_requested_install_tag = INSTALL_LLAMA_PREBUILT.resolve_requested_install_tag +resolve_install_attempts = INSTALL_LLAMA_PREBUILT.resolve_install_attempts +resolve_install_release_plans = INSTALL_LLAMA_PREBUILT.resolve_install_release_plans +resolve_published_release = INSTALL_LLAMA_PREBUILT.resolve_published_release +source_archive_logical_name = INSTALL_LLAMA_PREBUILT.source_archive_logical_name +env_int = INSTALL_LLAMA_PREBUILT.env_int # --------------------------------------------------------------------------- @@ -131,6 +137,37 @@ def make_checksums(asset_names): ) +def make_checksums_with_source( + asset_names, + *, + release_tag = "v1.0", + upstream_tag = "b8508", +): + return ApprovedReleaseChecksums( + repo = "unslothai/llama.cpp", + release_tag = release_tag, + upstream_tag = upstream_tag, + source_commit = None, + artifacts = { + **{ + name: ApprovedArtifactHash( + asset_name = name, + sha256 = "a" * 64, + repo = "unslothai/llama.cpp", + kind = "prebuilt", + ) + for name in asset_names + }, + source_archive_logical_name(upstream_tag): ApprovedArtifactHash( + asset_name = source_archive_logical_name(upstream_tag), + sha256 = "b" * 64, + repo = "ggml-org/llama.cpp", + kind = "upstream-source", + ), + }, + ) + + def mock_linux_runtime(monkeypatch, lines): dirs = {line: ["/usr/lib/stub"] for line in lines} monkeypatch.setattr( @@ -408,7 +445,100 @@ class TestApplyApprovedHashes: # =========================================================================== -# J. linux_cuda_choice_from_release -- core selection +# J. published release resolution +# =========================================================================== + + +class TestPublishedReleaseResolution: + def test_latest_skips_invalid_release_and_uses_next_valid(self, monkeypatch): + invalid = make_release([], release_tag = "v2.0", upstream_tag = "b9000") + valid = make_release([], release_tag = "v1.0", upstream_tag = "b8999") + + monkeypatch.setattr( + INSTALL_LLAMA_PREBUILT, + "iter_published_release_bundles", + lambda repo, published_release_tag = "": iter([invalid, valid]), + ) + + def fake_load(repo, release_tag): + if release_tag == "v2.0": + raise PrebuiltFallback("checksum asset missing") + return make_checksums_with_source( + [], release_tag = "v1.0", upstream_tag = "b8999" + ) + + monkeypatch.setattr( + INSTALL_LLAMA_PREBUILT, + "load_approved_release_checksums", + fake_load, + ) + + resolved = resolve_published_release("latest", "unslothai/llama.cpp") + assert resolved.bundle.release_tag == "v1.0" + assert resolved.bundle.upstream_tag == "b8999" + assert resolved.checksums.release_tag == "v1.0" + + def test_concrete_tag_matches_manifest_upstream_tag(self, monkeypatch): + release = make_release([], release_tag = "release-b8508", upstream_tag = "b8508") + monkeypatch.setattr( + INSTALL_LLAMA_PREBUILT, + "iter_published_release_bundles", + lambda repo, published_release_tag = "": iter([release]), + ) + monkeypatch.setattr( + INSTALL_LLAMA_PREBUILT, + "load_approved_release_checksums", + lambda repo, release_tag: make_checksums_with_source( + [], + release_tag = release_tag, + upstream_tag = "b8508", + ), + ) + + assert ( + resolve_requested_install_tag("b8508", "", "unslothai/llama.cpp") == "b8508" + ) + + def test_concrete_tag_without_matching_release_raises(self, monkeypatch): + release = make_release([], release_tag = "release-b9000", upstream_tag = "b9000") + monkeypatch.setattr( + INSTALL_LLAMA_PREBUILT, + "iter_published_release_bundles", + lambda repo, published_release_tag = "": iter([release]), + ) + + with pytest.raises(PrebuiltFallback, match = "matched upstream tag b8508"): + resolve_requested_install_tag("b8508", "", "unslothai/llama.cpp") + + def test_pinned_release_must_match_requested_upstream_tag(self, monkeypatch): + bundle = make_release( + [], release_tag = "llama-prebuilt-latest", upstream_tag = "b9000" + ) + monkeypatch.setattr( + INSTALL_LLAMA_PREBUILT, + "pinned_published_release_bundle", + lambda repo, release_tag: bundle, + ) + monkeypatch.setattr( + INSTALL_LLAMA_PREBUILT, + "load_approved_release_checksums", + lambda repo, release_tag: make_checksums_with_source( + [], + release_tag = release_tag, + upstream_tag = "b9000", + ), + ) + + with pytest.raises(PrebuiltFallback, match = "but requested b8508"): + resolve_requested_install_tag( + "b8508", + "llama-prebuilt-latest", + "unslothai/llama.cpp", + ) + + +# =========================================================================== +# K. linux_cuda_choice_from_release -- core selection # =========================================================================== @@ -676,7 +806,554 @@ class TestLinuxCudaChoiceFromRelease: # =========================================================================== -# K. windows_cuda_attempts +# L. resolve_install_attempts +# =========================================================================== + + +class TestResolveInstallAttempts: + def test_windows_cuda_prefers_published_asset_from_selected_release( + self, monkeypatch + ): + host = make_host(system = "Windows", machine = "AMD64") + host.driver_cuda_version = (12, 4) + mock_windows_runtime(monkeypatch, ["cuda12"]) + asset_name = "llama-b9000-bin-win-cuda-12.4-x64.zip" + release = make_release( + [ + make_artifact( + asset_name, + install_kind = "windows-cuda", + runtime_line = "cuda12", + coverage_class = None, + supported_sms = [], + min_sm = None, + max_sm = None, + bundle_profile = None, + ) + ], + release_tag = "llama-prebuilt-latest", + upstream_tag = "b9000", + assets = {asset_name: f"https://published.example/{asset_name}"}, + ) + checksums = make_checksums_with_source( + [asset_name], + release_tag = release.release_tag, + upstream_tag = "b9000", + ) + + monkeypatch.setattr( + INSTALL_LLAMA_PREBUILT, + "iter_resolved_published_releases", + lambda requested_tag, published_repo, published_release_tag = "": iter( + [ + INSTALL_LLAMA_PREBUILT.ResolvedPublishedRelease( + bundle = release, + checksums = checksums, + ) + ] + ), + ) + monkeypatch.setattr( + INSTALL_LLAMA_PREBUILT, + "github_release_assets", + lambda repo, tag: (_ for _ in ()).throw( + AssertionError( + "published Windows CUDA choice should not query upstream" + ) + ), + ) + + requested_tag, resolved_tag, attempts, approved = resolve_install_attempts( + "latest", + host, + "unslothai/llama.cpp", + "", + ) + + assert requested_tag == "latest" + assert resolved_tag == "b9000" + assert attempts[0].name == asset_name + assert attempts[0].source_label == "published" + assert attempts[0].expected_sha256 == "a" * 64 + assert approved.release_tag == "llama-prebuilt-latest" + + def test_windows_cuda_uses_selected_release_upstream_tag(self, monkeypatch): + host = make_host(system = "Windows", machine = "AMD64") + host.driver_cuda_version = (12, 4) + mock_windows_runtime(monkeypatch, ["cuda12"]) + release = make_release( + [], release_tag = "llama-prebuilt-latest", upstream_tag = "b9000" + ) + checksums = make_checksums_with_source( + ["llama-b9000-bin-win-cuda-12.4-x64.zip"], + release_tag = release.release_tag, + upstream_tag = "b9000", + ) + + monkeypatch.setattr( + INSTALL_LLAMA_PREBUILT, + "iter_resolved_published_releases", + lambda requested_tag, published_repo, published_release_tag = "": iter( + [ + INSTALL_LLAMA_PREBUILT.ResolvedPublishedRelease( + bundle = release, + checksums = checksums, + ) + ] + ), + ) + monkeypatch.setattr( + INSTALL_LLAMA_PREBUILT, + "github_release_assets", + lambda repo, tag: { + f"llama-{tag}-bin-win-cuda-12.4-x64.zip": f"https://example.com/llama-{tag}-bin-win-cuda-12.4-x64.zip" + }, + ) + monkeypatch.setattr( + INSTALL_LLAMA_PREBUILT, + "resolve_windows_cuda_choices", + lambda host, tag, assets: [ + AssetChoice( + repo = UPSTREAM_REPO, + tag = tag, + name = f"llama-{tag}-bin-win-cuda-12.4-x64.zip", + url = assets[f"llama-{tag}-bin-win-cuda-12.4-x64.zip"], + source_label = "upstream", + install_kind = "windows-cuda", + runtime_line = "cuda12", + ) + ], + ) + + requested_tag, resolved_tag, attempts, approved = resolve_install_attempts( + "latest", + host, + "unslothai/llama.cpp", + "", + ) + + assert requested_tag == "latest" + assert resolved_tag == "b9000" + assert attempts[0].name == "llama-b9000-bin-win-cuda-12.4-x64.zip" + assert attempts[0].expected_sha256 == "a" * 64 + assert approved.release_tag == "llama-prebuilt-latest" + + def test_linux_cpu_uses_same_tag_upstream_asset(self, monkeypatch): + host = make_host( + has_usable_nvidia = False, + has_physical_nvidia = False, + nvidia_smi = None, + ) + release = make_release( + [], release_tag = "llama-prebuilt-latest", upstream_tag = "b9000" + ) + checksums = make_checksums_with_source( + ["llama-b9000-bin-ubuntu-x64.tar.gz"], + release_tag = release.release_tag, + upstream_tag = "b9000", + ) + + monkeypatch.setattr( + INSTALL_LLAMA_PREBUILT, + "iter_resolved_published_releases", + lambda requested_tag, published_repo, published_release_tag = "": iter( + [ + INSTALL_LLAMA_PREBUILT.ResolvedPublishedRelease( + bundle = release, + checksums = checksums, + ) + ] + ), + ) + monkeypatch.setattr( + INSTALL_LLAMA_PREBUILT, + "github_release_assets", + lambda repo, tag: { + f"llama-{tag}-bin-ubuntu-x64.tar.gz": f"https://example.com/llama-{tag}-bin-ubuntu-x64.tar.gz" + }, + ) + + _requested_tag, resolved_tag, attempts, _approved = resolve_install_attempts( + "latest", + host, + "unslothai/llama.cpp", + "", + ) + + assert resolved_tag == "b9000" + assert attempts[0].name == "llama-b9000-bin-ubuntu-x64.tar.gz" + assert attempts[0].source_label == "upstream" + assert attempts[0].expected_sha256 == "a" * 64 + + def test_linux_cuda_does_not_fall_back_to_upstream_cpu(self, monkeypatch): + host = make_host(system = "Linux", machine = "x86_64", compute_caps = ["86"]) + release = make_release( + [], release_tag = "llama-prebuilt-latest", upstream_tag = "b9000" + ) + checksums = make_checksums_with_source( + [], + release_tag = release.release_tag, + upstream_tag = "b9000", + ) + + monkeypatch.setattr( + INSTALL_LLAMA_PREBUILT, + "iter_resolved_published_releases", + lambda requested_tag, published_repo, published_release_tag = "": iter( + [ + INSTALL_LLAMA_PREBUILT.ResolvedPublishedRelease( + bundle = release, + checksums = checksums, + ) + ] + ), + ) + mock_linux_runtime(monkeypatch, ["cuda12"]) + + with pytest.raises( + PrebuiltFallback, match = "no compatible published Linux CUDA bundle" + ): + resolve_install_attempts("latest", host, "unslothai/llama.cpp", "") + + def test_windows_cpu_prefers_published_asset(self, monkeypatch): + host = make_host( + system = "Windows", + machine = "AMD64", + has_usable_nvidia = False, + has_physical_nvidia = False, + nvidia_smi = None, + ) + asset_name = "llama-b9000-bin-win-cpu-x64.zip" + release = make_release( + [ + make_artifact( + asset_name, + install_kind = "windows-cpu", + runtime_line = None, + coverage_class = None, + supported_sms = [], + min_sm = None, + max_sm = None, + bundle_profile = None, + ) + ], + release_tag = "llama-prebuilt-latest", + upstream_tag = "b9000", + assets = {asset_name: f"https://published.example/{asset_name}"}, + ) + checksums = make_checksums_with_source( + [asset_name], + release_tag = release.release_tag, + upstream_tag = "b9000", + ) + + monkeypatch.setattr( + INSTALL_LLAMA_PREBUILT, + "iter_resolved_published_releases", + lambda requested_tag, published_repo, published_release_tag = "": iter( + [ + INSTALL_LLAMA_PREBUILT.ResolvedPublishedRelease( + bundle = release, + checksums = checksums, + ) + ] + ), + ) + monkeypatch.setattr( + INSTALL_LLAMA_PREBUILT, + "github_release_assets", + lambda repo, tag: (_ for _ in ()).throw( + AssertionError("published Windows CPU choice should not query upstream") + ), + ) + + _requested_tag, resolved_tag, attempts, _approved = resolve_install_attempts( + "latest", + host, + "unslothai/llama.cpp", + "", + ) + + assert resolved_tag == "b9000" + assert attempts[0].name == asset_name + assert attempts[0].source_label == "published" + + def test_macos_prefers_published_asset(self, monkeypatch): + host = make_host( + system = "Darwin", + machine = "arm64", + nvidia_smi = None, + driver_cuda_version = None, + compute_caps = [], + has_physical_nvidia = False, + has_usable_nvidia = False, + ) + asset_name = "llama-b9000-bin-macos-arm64.tar.gz" + release = make_release( + [ + make_artifact( + asset_name, + install_kind = "macos-arm64", + runtime_line = None, + coverage_class = None, + supported_sms = [], + min_sm = None, + max_sm = None, + bundle_profile = None, + ) + ], + release_tag = "llama-prebuilt-latest", + upstream_tag = "b9000", + assets = {asset_name: f"https://published.example/{asset_name}"}, + ) + checksums = make_checksums_with_source( + [asset_name], + release_tag = release.release_tag, + upstream_tag = "b9000", + ) + + monkeypatch.setattr( + INSTALL_LLAMA_PREBUILT, + "iter_resolved_published_releases", + lambda requested_tag, published_repo, published_release_tag = "": iter( + [ + INSTALL_LLAMA_PREBUILT.ResolvedPublishedRelease( + bundle = release, + checksums = checksums, + ) + ] + ), + ) + monkeypatch.setattr( + INSTALL_LLAMA_PREBUILT, + "github_release_assets", + lambda repo, tag: (_ for _ in ()).throw( + AssertionError("published macOS choice should not query upstream") + ), + ) + + _requested_tag, resolved_tag, attempts, _approved = resolve_install_attempts( + "latest", + host, + "unslothai/llama.cpp", + "", + ) + + assert resolved_tag == "b9000" + assert attempts[0].name == asset_name + assert attempts[0].source_label == "published" + + def test_windows_cpu_missing_checksum_rejects_install(self, monkeypatch): + host = make_host( + system = "Windows", + machine = "AMD64", + has_usable_nvidia = False, + has_physical_nvidia = False, + nvidia_smi = None, + ) + published_name = "llama-b9000-bin-win-cpu-x64.zip" + release = make_release( + [ + make_artifact( + published_name, + install_kind = "windows-cpu", + runtime_line = None, + coverage_class = None, + supported_sms = [], + min_sm = None, + max_sm = None, + bundle_profile = None, + ) + ], + release_tag = "llama-prebuilt-latest", + upstream_tag = "b9000", + assets = {published_name: f"https://published.example/{published_name}"}, + ) + checksums = make_checksums_with_source( + [], + release_tag = release.release_tag, + upstream_tag = "b9000", + ) + + monkeypatch.setattr( + INSTALL_LLAMA_PREBUILT, + "iter_resolved_published_releases", + lambda requested_tag, published_repo, published_release_tag = "": iter( + [ + INSTALL_LLAMA_PREBUILT.ResolvedPublishedRelease( + bundle = release, + checksums = checksums, + ) + ] + ), + ) + monkeypatch.setattr( + INSTALL_LLAMA_PREBUILT, + "github_release_assets", + lambda repo, tag: { + f"llama-{tag}-bin-win-cpu-x64.zip": f"https://upstream.example/llama-{tag}-bin-win-cpu-x64.zip" + }, + ) + + with pytest.raises( + PrebuiltFallback, + match = "approved checksum asset did not contain the selected prebuilt archive", + ): + resolve_install_attempts( + "latest", + host, + "unslothai/llama.cpp", + "", + ) + + +class TestResolveInstallReleasePlans: + def test_latest_collects_multiple_older_release_plans_up_to_limit( + self, monkeypatch + ): + host = make_host( + has_usable_nvidia = False, + has_physical_nvidia = False, + nvidia_smi = None, + ) + releases = [ + INSTALL_LLAMA_PREBUILT.ResolvedPublishedRelease( + bundle = make_release([], release_tag = "r3", upstream_tag = "b9003"), + checksums = make_checksums_with_source( + ["llama-b9003-bin-ubuntu-x64.tar.gz"], + release_tag = "r3", + upstream_tag = "b9003", + ), + ), + INSTALL_LLAMA_PREBUILT.ResolvedPublishedRelease( + bundle = make_release([], release_tag = "r2", upstream_tag = "b9002"), + checksums = make_checksums_with_source( + ["llama-b9002-bin-ubuntu-x64.tar.gz"], + release_tag = "r2", + upstream_tag = "b9002", + ), + ), + INSTALL_LLAMA_PREBUILT.ResolvedPublishedRelease( + bundle = make_release([], release_tag = "r1", upstream_tag = "b9001"), + checksums = make_checksums_with_source( + ["llama-b9001-bin-ubuntu-x64.tar.gz"], + release_tag = "r1", + upstream_tag = "b9001", + ), + ), + ] + + monkeypatch.setattr( + INSTALL_LLAMA_PREBUILT, + "iter_resolved_published_releases", + lambda requested_tag, published_repo, published_release_tag = "": iter( + releases + ), + ) + monkeypatch.setattr( + INSTALL_LLAMA_PREBUILT, + "github_release_assets", + lambda repo, tag: { + f"llama-{tag}-bin-ubuntu-x64.tar.gz": f"https://example.com/llama-{tag}-bin-ubuntu-x64.tar.gz" + }, + ) + + requested_tag, plans = resolve_install_release_plans( + "latest", + host, + "unslothai/llama.cpp", + "", + max_release_fallbacks = 2, + ) + + assert requested_tag == "latest" + assert [plan.release_tag for plan in plans] == ["r3", "r2"] + assert [plan.llama_tag for plan in plans] == ["b9003", "b9002"] + + def test_latest_skips_non_installable_release_and_keeps_searching( + self, monkeypatch + ): + host = make_host( + has_usable_nvidia = False, + has_physical_nvidia = False, + nvidia_smi = None, + ) + releases = [ + INSTALL_LLAMA_PREBUILT.ResolvedPublishedRelease( + bundle = make_release([], release_tag = "r2", upstream_tag = "b9002"), + checksums = make_checksums_with_source( + [], + release_tag = "r2", + upstream_tag = "b9002", + ), + ), + INSTALL_LLAMA_PREBUILT.ResolvedPublishedRelease( + bundle = make_release([], release_tag = "r1", upstream_tag = "b9001"), + checksums = make_checksums_with_source( + ["llama-b9001-bin-ubuntu-x64.tar.gz"], + release_tag = "r1", + upstream_tag = "b9001", + ), + ), + ] + + monkeypatch.setattr( + INSTALL_LLAMA_PREBUILT, + "iter_resolved_published_releases", + lambda requested_tag, published_repo, published_release_tag = "": iter( + releases + ), + ) + monkeypatch.setattr( + INSTALL_LLAMA_PREBUILT, + "github_release_assets", + lambda repo, tag: ( + {} + if tag == "b9002" + else { + f"llama-{tag}-bin-ubuntu-x64.tar.gz": f"https://example.com/llama-{tag}-bin-ubuntu-x64.tar.gz" + } + ), + ) + + _requested_tag, plans = resolve_install_release_plans( + "latest", + host, + "unslothai/llama.cpp", + "", + max_release_fallbacks = 2, + ) + + assert len(plans) == 1 + assert plans[0].release_tag == "r1" + assert plans[0].llama_tag == "b9001" + + def test_malformed_release_fallback_env_uses_default(self, monkeypatch): + monkeypatch.setenv("UNSLOTH_LLAMA_MAX_PREBUILT_RELEASE_FALLBACKS", "not-an-int") + assert ( + env_int("UNSLOTH_LLAMA_MAX_PREBUILT_RELEASE_FALLBACKS", 3, minimum = 1) == 3 + ) + + def test_import_with_malformed_release_fallback_env_does_not_crash( + self, monkeypatch + ): + monkeypatch.setenv("UNSLOTH_LLAMA_MAX_PREBUILT_RELEASE_FALLBACKS", "bad-value") + spec = importlib.util.spec_from_file_location( + "studio_install_llama_prebuilt_env_reload", + MODULE_PATH, + ) + assert spec is not None and spec.loader is not None + module = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = module + try: + spec.loader.exec_module(module) + assert module.DEFAULT_MAX_PREBUILT_RELEASE_FALLBACKS == 2 + finally: + sys.modules.pop(spec.name, None) + + +# =========================================================================== +# N. windows_cuda_attempts # =========================================================================== @@ -753,7 +1430,7 @@ class TestWindowsCudaAttempts: # =========================================================================== -# L. resolve_upstream_asset_choice -- platform routing +# O. resolve_upstream_asset_choice -- platform routing # =========================================================================== diff --git a/tests/studio/install/test_validate_llama_prebuilt.py b/tests/studio/install/test_validate_llama_prebuilt.py new file mode 100644 index 0000000000..0f384b715e --- /dev/null +++ b/tests/studio/install/test_validate_llama_prebuilt.py @@ -0,0 +1,175 @@ +import importlib.util +import sys +from pathlib import Path + + +import pytest + + +REPO_ROOT = Path(__file__).resolve().parents[3] +MODULE_PATH = REPO_ROOT / "validate-llama-prebuilt.py" + +if not MODULE_PATH.is_file(): + pytest.skip( + f"validate-llama-prebuilt.py not present at {MODULE_PATH}", + allow_module_level = True, + ) + +SPEC = importlib.util.spec_from_file_location("validate_llama_prebuilt", MODULE_PATH) +assert SPEC is not None and SPEC.loader is not None +VALIDATE = importlib.util.module_from_spec(SPEC) +sys.modules[SPEC.name] = VALIDATE +SPEC.loader.exec_module(VALIDATE) + + +def test_build_local_approved_checksums_uses_staged_upstream_tag( + tmp_path: Path, monkeypatch +): + stage_dir = tmp_path / "release-1" + stage_dir.mkdir() + asset_path = stage_dir / "app-test-linux-x64-cuda12-newer.tar.gz" + asset_path.write_bytes(b"bundle") + sibling_checksums = stage_dir / VALIDATE.installer.DEFAULT_PUBLISHED_SHA256_ASSET + sibling_checksums.write_text( + """ +{ + "schema_version": 1, + "component": "llama.cpp", + "release_tag": "release-1", + "upstream_tag": "b9001", + "source_commit": "deadbeef", + "artifacts": { + "llama.cpp-source-b9001.tar.gz": { + "sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "repo": "ggml-org/llama.cpp", + "kind": "upstream-source" + } + } +} + """.strip() + + "\n", + encoding = "utf-8", + ) + asset = VALIDATE.LocalAsset( + path = asset_path, + tag = "test", + name = asset_path.name, + install_kind = "linux-cuda", + source_kind = "app-bundle", + native_runnable = True, + bundle_profile = "cuda12-newer", + runtime_line = "cuda12", + ) + + checksums = VALIDATE.build_local_approved_checksums( + asset, + allow_network_source_hash = False, + ) + + assert checksums.release_tag == "release-1" + assert checksums.upstream_tag == "b9001" + assert "llama.cpp-source-b9001.tar.gz" in checksums.artifacts + assert "llama.cpp-source-test.tar.gz" not in checksums.artifacts + + +def test_validate_native_asset_passes_release_tag_and_upstream_tag( + tmp_path: Path, monkeypatch +): + stage_dir = tmp_path / "release-7" + stage_dir.mkdir() + asset_path = stage_dir / "app-test-linux-x64-cuda12-newer.tar.gz" + asset_path.write_bytes(b"bundle") + sibling_checksums = stage_dir / VALIDATE.installer.DEFAULT_PUBLISHED_SHA256_ASSET + sibling_checksums.write_text( + """ +{ + "schema_version": 1, + "component": "llama.cpp", + "release_tag": "release-7", + "upstream_tag": "b9007", + "source_commit": "deadbeef", + "artifacts": { + "llama.cpp-source-b9007.tar.gz": { + "sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "repo": "ggml-org/llama.cpp", + "kind": "upstream-source" + } + } +} + """.strip() + + "\n", + encoding = "utf-8", + ) + asset = VALIDATE.LocalAsset( + path = asset_path, + tag = "test", + name = asset_path.name, + install_kind = "linux-cuda", + source_kind = "app-bundle", + native_runnable = True, + bundle_profile = "cuda12-newer", + runtime_line = "cuda12", + ) + + host = VALIDATE.installer.HostInfo( + system = "Linux", + machine = "x86_64", + is_windows = False, + is_linux = True, + is_macos = False, + is_x86_64 = True, + is_arm64 = False, + nvidia_smi = None, + driver_cuda_version = None, + compute_caps = [], + visible_cuda_devices = None, + has_physical_nvidia = False, + has_usable_nvidia = False, + ) + monkeypatch.setattr(VALIDATE.installer, "detect_host", lambda: host) + monkeypatch.setattr( + VALIDATE.installer, + "download_validation_model", + lambda probe_path, cache_path: probe_path.write_bytes(b"probe"), + ) + + captured = {} + + def fake_validate_prebuilt_attempts( + attempts, + host, + install_dir, + work_dir, + probe_path, + *, + requested_tag, + llama_tag, + release_tag, + approved_checksums, + initial_fallback_used = False, + existing_install_dir = None, + ): + captured["requested_tag"] = requested_tag + captured["llama_tag"] = llama_tag + captured["release_tag"] = release_tag + staging_dir = VALIDATE.installer.create_install_staging_dir(install_dir) + return attempts[0], staging_dir, False + + monkeypatch.setattr( + VALIDATE.installer, + "validate_prebuilt_attempts", + fake_validate_prebuilt_attempts, + ) + + record = VALIDATE.validate_native_asset( + asset, + keep_temp = False, + allow_network_source_hash = False, + ) + + assert record.status == "PASS" + assert captured == { + "requested_tag": "test", + "llama_tag": "b9007", + "release_tag": "release-7", + }