diff --git a/studio/install_llama_prebuilt.py b/studio/install_llama_prebuilt.py index 0f96cf34eb..76d22984d7 100755 --- a/studio/install_llama_prebuilt.py +++ b/studio/install_llama_prebuilt.py @@ -28,7 +28,7 @@ import urllib.parse import urllib.request import zipfile from contextlib import contextmanager -from dataclasses import dataclass +from dataclasses import dataclass, field try: from filelock import FileLock, Timeout as FileLockTimeout @@ -59,7 +59,6 @@ def env_int(name: str, default: int, *, minimum: int | None = None) -> int: return value -APPROVED_PREBUILT_LLAMA_TAG = "b8508" DEFAULT_LLAMA_TAG = os.environ.get("UNSLOTH_LLAMA_TAG", "latest") DEFAULT_PUBLISHED_REPO = os.environ.get( "UNSLOTH_LLAMA_RELEASE_REPO", "unslothai/llama.cpp" @@ -151,10 +150,18 @@ class PublishedReleaseBundle: repo: str release_tag: str upstream_tag: str - assets: dict[str, str] - manifest_asset_name: str - artifacts: list[PublishedLlamaArtifact] - selection_log: list[str] + manifest_sha256: str | None = None + source_repo: str | None = None + source_repo_url: str | None = None + source_ref_kind: str | None = None + requested_source_ref: str | None = None + resolved_source_ref: str | None = None + source_commit: str | None = None + source_commit_short: str | None = None + assets: dict[str, str] = field(default_factory = dict) + manifest_asset_name: str = DEFAULT_PUBLISHED_MANIFEST_ASSET + artifacts: list[PublishedLlamaArtifact] = field(default_factory = list) + selection_log: list[str] = field(default_factory = list) @dataclass @@ -188,8 +195,14 @@ class ApprovedReleaseChecksums: repo: str release_tag: str upstream_tag: str - source_commit: str | None - artifacts: dict[str, ApprovedArtifactHash] + source_repo: str | None = None + source_repo_url: str | None = None + source_ref_kind: str | None = None + requested_source_ref: str | None = None + resolved_source_ref: str | None = None + source_commit: str | None = None + source_commit_short: str | None = None + artifacts: dict[str, ApprovedArtifactHash] = field(default_factory = dict) @dataclass(frozen = True) @@ -198,6 +211,19 @@ class ResolvedPublishedRelease: checksums: ApprovedReleaseChecksums +@dataclass(frozen = True) +class SourceBuildPlan: + source_url: str + source_ref: str + source_ref_kind: str + compatibility_upstream_tag: str + source_repo: str | None = None + source_repo_url: str | None = None + requested_source_ref: str | None = None + resolved_source_ref: str | None = None + source_commit: str | None = None + + @dataclass(frozen = True) class InstallReleasePlan: requested_tag: str @@ -270,7 +296,7 @@ def is_busy_lock_error(exc: BaseException) -> bool: def log(message: str) -> None: - print(f"[llama-prebuilt] {message}") + print(f"[llama-prebuilt] {message}", file = sys.stderr) def log_lines(lines: Iterable[str]) -> None: @@ -358,6 +384,10 @@ def source_archive_logical_name(upstream_tag: str) -> str: return f"llama.cpp-source-{upstream_tag}.tar.gz" +def exact_source_archive_logical_name(source_commit: str) -> str: + return f"llama.cpp-source-commit-{source_commit}.tar.gz" + + def sha256_file(path: Path) -> str: digest = hashlib.sha256() with path.open("rb") as handle: @@ -366,6 +396,10 @@ def sha256_file(path: Path) -> str: return digest.hexdigest() +def sha256_bytes(data: bytes) -> str: + return hashlib.sha256(data).hexdigest() + + def normalize_sha256_digest(value: str | None) -> str | None: if not isinstance(value, str) or not value: return None @@ -377,6 +411,156 @@ def normalize_sha256_digest(value: str | None) -> str | None: return lowered +def normalize_source_ref_kind(value: str | None) -> str | None: + if not isinstance(value, str): + return None + normalized = value.strip().lower() + if normalized in {"tag", "branch", "pull", "commit", "custom"}: + return normalized + return None + + +def normalize_source_commit(value: str | None) -> str | None: + if not isinstance(value, str): + return None + normalized = value.strip().lower() + if len(normalized) < 7 or len(normalized) > 40: + return None + if any(ch not in "0123456789abcdef" for ch in normalized): + return None + return normalized + + +def validate_schema_version(payload: dict[str, Any], *, label: str) -> None: + schema_version = payload.get("schema_version") + if schema_version is None: + return + try: + normalized = int(schema_version) + except (TypeError, ValueError) as exc: + raise RuntimeError(f"{label} schema_version was not an integer") from exc + if normalized != 1: + raise RuntimeError(f"{label} schema_version={normalized} is unsupported") + + +def repo_slug_from_source(value: str | None) -> str | None: + if not isinstance(value, str): + return None + normalized = value.strip() + if not normalized: + return None + normalized = normalized.removesuffix(".git") + if normalized.startswith("https://github.com/"): + slug = normalized[len("https://github.com/") :] + elif normalized.startswith("http://github.com/"): + slug = normalized[len("http://github.com/") :] + elif normalized.startswith("git@github.com:"): + slug = normalized[len("git@github.com:") :] + else: + slug = normalized + slug = slug.strip("/") + parts = slug.split("/") + if len(parts) != 2 or not all(parts): + return None + return f"{parts[0]}/{parts[1]}" + + +def source_url_from_repo_slug(repo_slug: str | None) -> str | None: + if not isinstance(repo_slug, str) or not repo_slug: + return None + return f"https://github.com/{repo_slug}" + + +def source_repo_clone_url(repo: str | None, repo_url: str | None) -> str | None: + if isinstance(repo_url, str) and repo_url.strip(): + return repo_url.strip().removesuffix(".git") + return source_url_from_repo_slug(repo_slug_from_source(repo)) + + +def infer_source_ref_kind(ref: str | None) -> str: + if not isinstance(ref, str): + return "tag" + normalized = ref.strip() + lowered = normalized.lower() + if not normalized: + return "tag" + if lowered.startswith("refs/pull/") or lowered.startswith("pull/"): + return "pull" + if ( + lowered.startswith("refs/heads/") + or lowered in {"main", "master", "head"} + or lowered.startswith("origin/") + ): + return "branch" + normalized_commit = normalize_source_commit(normalized) + if normalized_commit is not None: + return "commit" + return "tag" + + +def normalized_ref_aliases(ref: str | None) -> set[str]: + if not isinstance(ref, str): + return set() + normalized = ref.strip() + if not normalized: + return set() + aliases = {normalized} + lowered = normalized.lower() + commit = normalize_source_commit(normalized) + if commit is not None: + aliases.add(commit) + if lowered.startswith("refs/heads/"): + aliases.add(normalized.split("/", 2)[2]) + elif "/" not in normalized and infer_source_ref_kind(normalized) == "branch": + aliases.add(f"refs/heads/{normalized}") + if lowered.startswith("refs/pull/"): + aliases.add(normalized.removeprefix("refs/")) + elif lowered.startswith("pull/"): + aliases.add(f"refs/{normalized}") + return aliases + + +def refs_match(candidate_ref: str | None, requested_ref: str | None) -> bool: + candidate_aliases = normalized_ref_aliases(candidate_ref) + requested_aliases = normalized_ref_aliases(requested_ref) + if not candidate_aliases or not requested_aliases: + return False + if candidate_aliases & requested_aliases: + return True + candidate_commit = normalize_source_commit(candidate_ref) + requested_commit = normalize_source_commit(requested_ref) + if candidate_commit and requested_commit: + return candidate_commit.startswith( + requested_commit + ) or requested_commit.startswith(candidate_commit) + return False + + +def checkout_friendly_ref(ref_kind: str | None, ref: str | None) -> str | None: + """Normalize a source ref to a form that ``git clone --branch`` accepts. + + Fully qualified branch refs like ``refs/heads/main`` are stripped to + ``main``; tag refs like ``refs/tags/b8508`` are stripped to ``b8508``. + Pull refs like ``refs/pull/123/head`` are left as-is since they are + always fetched explicitly rather than cloned with ``--branch``. + """ + if not isinstance(ref, str) or not ref: + return ref + lowered = ref.lower() + if ref_kind == "branch" and lowered.startswith("refs/heads/"): + return ref.split("/", 2)[2] + if ref_kind == "tag" and lowered.startswith("refs/tags/"): + return ref.split("/", 2)[2] + return ref + + +def windows_cuda_upstream_asset_names(llama_tag: str, runtime: str) -> list[str]: + return [ + f"llama-{llama_tag}-bin-win-cuda-{runtime}-x64.zip", + f"cudart-llama-bin-win-cuda-{runtime}-x64.zip", + ] + + def format_byte_count(num_bytes: float) -> str: units = ["B", "KiB", "MiB", "GiB", "TiB"] value = float(num_bytes) @@ -537,13 +721,21 @@ def download_bytes( def fetch_json(url: str) -> Any: - data = download_bytes( - url, - timeout = 30, - headers = github_api_headers(url) - if is_github_api_url(url) - else auth_headers(url), - ) + try: + data = download_bytes( + url, + timeout = 30, + headers = github_api_headers(url) + if is_github_api_url(url) + else auth_headers(url), + ) + except urllib.error.HTTPError as exc: + if exc.code == 403 and is_github_api_url(url): + hint = "" + if not (os.environ.get("GH_TOKEN") or os.environ.get("GITHUB_TOKEN")): + hint = "; set GH_TOKEN or GITHUB_TOKEN to avoid GitHub API rate limits" + raise RuntimeError(f"GitHub API returned 403 for {url}{hint}") from exc + raise if not data: raise RuntimeError(f"downloaded empty JSON payload from {url}") try: @@ -648,6 +840,14 @@ def upstream_source_archive_urls(tag: str) -> list[str]: ] +def commit_source_archive_urls(repo: str, source_commit: str) -> list[str]: + encoded_commit = urllib.parse.quote(source_commit, safe = "") + return [ + f"https://codeload.github.com/{repo}/tar.gz/{encoded_commit}", + f"https://github.com/{repo}/archive/{encoded_commit}.tar.gz", + ] + + def github_release_assets(repo: str, tag: str) -> dict[str, str]: payload = fetch_json( f"https://api.github.com/repos/{repo}/releases/tags/{urllib.parse.quote(tag, safe = '')}" @@ -984,13 +1184,35 @@ def parse_published_release_bundle( # Mixed repos are filtered by an explicit release-side manifest rather than # by release tag or asset filename conventions. - manifest_payload = fetch_json(manifest_url) + manifest_bytes = download_bytes( + manifest_url, + timeout = 30, + headers = auth_headers(manifest_url), + ) + manifest_sha256 = sha256_bytes(manifest_bytes) + try: + manifest_payload = json.loads(manifest_bytes.decode("utf-8")) + except (UnicodeDecodeError, json.JSONDecodeError) as exc: + raise RuntimeError( + f"published manifest {DEFAULT_PUBLISHED_MANIFEST_ASSET} was not valid JSON" + ) from exc if not isinstance(manifest_payload, dict): raise RuntimeError( f"published manifest {DEFAULT_PUBLISHED_MANIFEST_ASSET} was not a JSON object" ) + validate_schema_version( + manifest_payload, + label = f"published manifest {DEFAULT_PUBLISHED_MANIFEST_ASSET} in {repo}@{release_tag}", + ) component = manifest_payload.get("component") upstream_tag = manifest_payload.get("upstream_tag") + source_repo = manifest_payload.get("source_repo") + source_repo_url = manifest_payload.get("source_repo_url") + source_ref_kind = normalize_source_ref_kind(manifest_payload.get("source_ref_kind")) + requested_source_ref = manifest_payload.get("requested_source_ref") + resolved_source_ref = manifest_payload.get("resolved_source_ref") + source_commit = normalize_source_commit(manifest_payload.get("source_commit")) + source_commit_short = manifest_payload.get("source_commit_short") if component != "llama.cpp": return None if not isinstance(upstream_tag, str) or not upstream_tag: @@ -1021,10 +1243,32 @@ def parse_published_release_bundle( f"published_release: manifest={DEFAULT_PUBLISHED_MANIFEST_ASSET}", f"published_release: upstream_tag={upstream_tag}", ] + if isinstance(source_repo, str) and source_repo: + selection_log.append(f"published_release: source_repo={source_repo}") + if source_commit: + selection_log.append(f"published_release: source_commit={source_commit}") return PublishedReleaseBundle( repo = repo, release_tag = release_tag, upstream_tag = upstream_tag, + manifest_sha256 = manifest_sha256, + source_repo = source_repo + if isinstance(source_repo, str) and source_repo + else None, + source_repo_url = source_repo_url + if isinstance(source_repo_url, str) and source_repo_url + else None, + source_ref_kind = source_ref_kind, + requested_source_ref = requested_source_ref + if isinstance(requested_source_ref, str) and requested_source_ref + else None, + resolved_source_ref = resolved_source_ref + if isinstance(resolved_source_ref, str) and resolved_source_ref + else None, + source_commit = source_commit, + source_commit_short = source_commit_short + if isinstance(source_commit_short, str) and source_commit_short + else None, assets = assets, manifest_asset_name = DEFAULT_PUBLISHED_MANIFEST_ASSET, artifacts = artifacts, @@ -1041,6 +1285,10 @@ def parse_approved_release_checksums( raise RuntimeError( f"published checksum asset {DEFAULT_PUBLISHED_SHA256_ASSET} was not a JSON object" ) + validate_schema_version( + payload, + label = f"published checksum asset {DEFAULT_PUBLISHED_SHA256_ASSET}", + ) if payload.get("component") != "llama.cpp": raise RuntimeError( f"published checksum asset {DEFAULT_PUBLISHED_SHA256_ASSET} did not describe llama.cpp" @@ -1090,13 +1338,33 @@ def parse_approved_release_checksums( kind = kind_value if isinstance(kind_value, str) and kind_value else None, ) - source_commit = payload.get("source_commit") + source_commit = normalize_source_commit(payload.get("source_commit")) + source_commit_short = payload.get("source_commit_short") + source_repo = payload.get("source_repo") + source_repo_url = payload.get("source_repo_url") + source_ref_kind = normalize_source_ref_kind(payload.get("source_ref_kind")) + requested_source_ref = payload.get("requested_source_ref") + resolved_source_ref = payload.get("resolved_source_ref") return ApprovedReleaseChecksums( repo = repo, release_tag = release_tag, upstream_tag = upstream_tag, - source_commit = source_commit - if isinstance(source_commit, str) and source_commit + source_repo = source_repo + if isinstance(source_repo, str) and source_repo + else None, + source_repo_url = source_repo_url + if isinstance(source_repo_url, str) and source_repo_url + else None, + source_ref_kind = source_ref_kind, + requested_source_ref = requested_source_ref + if isinstance(requested_source_ref, str) and requested_source_ref + else None, + resolved_source_ref = resolved_source_ref + if isinstance(resolved_source_ref, str) and resolved_source_ref + else None, + source_commit = source_commit, + source_commit_short = source_commit_short + if isinstance(source_commit_short, str) and source_commit_short else None, artifacts = artifacts, ) @@ -1390,10 +1658,36 @@ 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) + manifest_hash = checksums.artifacts.get(bundle.manifest_asset_name) + if manifest_hash is not None and bundle.manifest_sha256 is not None: + if manifest_hash.sha256 != bundle.manifest_sha256: + raise PrebuiltFallback( + "published manifest checksum did not match the approved checksum asset" + ) + # Accept bundles that carry only an exact-commit source archive + # (e.g. llama.cpp-source-commit-.tar.gz) without requiring the + # legacy llama.cpp-source-.tar.gz entry. + if exact_source_archive_hash(checksums) is None: + require_approved_source_hash(checksums, bundle.upstream_tag) return checksums +def published_release_matches_request( + bundle: PublishedReleaseBundle, requested_ref: str +) -> bool: + if requested_ref == "latest": + return True + for candidate in ( + bundle.upstream_tag, + bundle.requested_source_ref, + bundle.resolved_source_ref, + bundle.source_commit, + ): + if refs_match(candidate, requested_ref): + return True + return False + + def resolve_published_release( requested_tag: str | None, published_repo: str, @@ -1404,10 +1698,7 @@ def resolve_published_release( if published_release_tag: bundle = pinned_published_release_bundle(repo, published_release_tag) - if ( - normalized_requested != "latest" - and bundle.upstream_tag != normalized_requested - ): + if not published_release_matches_request(bundle, normalized_requested): raise PrebuiltFallback( "published release " f"{repo}@{published_release_tag} targeted upstream tag {bundle.upstream_tag}, " @@ -1420,10 +1711,7 @@ def resolve_published_release( skipped_invalid = 0 for bundle in iter_published_release_bundles(repo): - if ( - normalized_requested != "latest" - and bundle.upstream_tag != normalized_requested - ): + if not published_release_matches_request(bundle, normalized_requested): continue try: checksums = validated_checksums_for_bundle(repo, bundle) @@ -1460,10 +1748,7 @@ def iter_resolved_published_releases( if published_release_tag: bundle = pinned_published_release_bundle(repo, published_release_tag) - if ( - normalized_requested != "latest" - and bundle.upstream_tag != normalized_requested - ): + if not published_release_matches_request(bundle, normalized_requested): raise PrebuiltFallback( "published release " f"{repo}@{published_release_tag} targeted upstream tag {bundle.upstream_tag}, " @@ -1479,10 +1764,7 @@ def iter_resolved_published_releases( skipped_invalid = 0 yielded_valid = False for bundle in iter_published_release_bundles(repo): - if ( - normalized_requested != "latest" - and bundle.upstream_tag != normalized_requested - ): + if not published_release_matches_request(bundle, normalized_requested): continue matched_any = True try: @@ -1520,6 +1802,7 @@ def iter_resolved_published_releases( def resolve_requested_llama_tag( requested_tag: str | None, published_repo: str = "", + published_release_tag: str = "", ) -> str: """Resolve a llama.cpp tag for source-build fallback. @@ -1547,6 +1830,7 @@ def resolve_requested_llama_tag( return resolve_published_release( "latest", published_repo, + published_release_tag, ).bundle.upstream_tag except Exception: pass @@ -1566,6 +1850,125 @@ def resolve_requested_install_tag( ).bundle.upstream_tag +def exact_source_archive_hash( + checksums: ApprovedReleaseChecksums, +) -> ApprovedArtifactHash | None: + if not checksums.source_commit: + return None + return checksums.artifacts.get( + exact_source_archive_logical_name(checksums.source_commit) + ) + + +def source_clone_url_from_checksums(checksums: ApprovedReleaseChecksums) -> str | None: + return source_repo_clone_url(checksums.source_repo, checksums.source_repo_url) + + +def source_build_plan_for_release( + release: ResolvedPublishedRelease, +) -> SourceBuildPlan: + checksums = release.checksums + exact_source = exact_source_archive_hash(checksums) + source_repo = checksums.source_repo or release.bundle.source_repo + source_repo_url = checksums.source_repo_url or release.bundle.source_repo_url + requested_source_ref = ( + checksums.requested_source_ref or release.bundle.requested_source_ref + ) + resolved_source_ref = ( + checksums.resolved_source_ref or release.bundle.resolved_source_ref + ) + source_commit = checksums.source_commit or release.bundle.source_commit + source_ref_kind = checksums.source_ref_kind or release.bundle.source_ref_kind + source_url = source_repo_clone_url(source_repo, source_repo_url) + if exact_source is not None and source_url and source_commit: + return SourceBuildPlan( + source_url = source_url, + source_ref = source_commit, + source_ref_kind = "commit", + compatibility_upstream_tag = release.bundle.upstream_tag, + source_repo = source_repo, + source_repo_url = source_repo_url, + requested_source_ref = requested_source_ref, + resolved_source_ref = resolved_source_ref, + source_commit = source_commit, + ) + source_ref = checkout_friendly_ref( + source_ref_kind, resolved_source_ref or requested_source_ref + ) + if ( + source_url + and source_ref + and source_ref_kind in {"tag", "branch", "pull", "commit"} + ): + return SourceBuildPlan( + source_url = source_url, + source_ref = source_ref, + source_ref_kind = source_ref_kind, + compatibility_upstream_tag = release.bundle.upstream_tag, + source_repo = source_repo, + source_repo_url = source_repo_url, + requested_source_ref = requested_source_ref, + resolved_source_ref = resolved_source_ref, + source_commit = source_commit, + ) + return SourceBuildPlan( + source_url = source_url_from_repo_slug(UPSTREAM_REPO) + or "https://github.com/ggml-org/llama.cpp", + source_ref = release.bundle.upstream_tag, + source_ref_kind = "tag", + compatibility_upstream_tag = release.bundle.upstream_tag, + source_repo = source_repo, + source_repo_url = source_repo_url, + requested_source_ref = requested_source_ref, + resolved_source_ref = resolved_source_ref, + source_commit = source_commit, + ) + + +def resolve_source_build_plan( + requested_tag: str | None, + published_repo: str, + published_release_tag: str = "", +) -> SourceBuildPlan: + normalized_requested = normalized_requested_llama_tag(requested_tag) + if normalized_requested != "latest": + try: + release = resolve_published_release( + normalized_requested, + published_repo, + published_release_tag, + ) + return source_build_plan_for_release(release) + except Exception: + pass + inferred_kind = infer_source_ref_kind(normalized_requested) + return SourceBuildPlan( + source_url = "https://github.com/ggml-org/llama.cpp", + source_ref = checkout_friendly_ref(inferred_kind, normalized_requested) + or normalized_requested, + source_ref_kind = inferred_kind, + compatibility_upstream_tag = normalized_requested, + ) + + if published_repo: + try: + release = resolve_published_release( + "latest", + published_repo, + published_release_tag, + ) + return source_build_plan_for_release(release) + except Exception: + pass + latest_tag = latest_upstream_release_tag() + return SourceBuildPlan( + source_url = "https://github.com/ggml-org/llama.cpp", + source_ref = latest_tag, + source_ref_kind = "tag", + compatibility_upstream_tag = latest_tag, + ) + + def run_capture( command: list[str], *, @@ -1883,25 +2286,31 @@ def windows_cuda_attempts( attempts: list[AssetChoice] = [] for runtime_line in runtime_order: runtime = runtime_by_line[runtime_line] - upstream_name = f"llama-{llama_tag}-bin-win-cuda-{runtime}-x64.zip" - asset_url = upstream_assets.get(upstream_name) - if not asset_url: + selected_name = None + asset_url = None + for candidate_name in windows_cuda_upstream_asset_names(llama_tag, runtime): + asset_url = upstream_assets.get(candidate_name) + if asset_url: + selected_name = candidate_name + break + if not asset_url or not selected_name: selection_log.append( - f"windows_cuda_selection: skip missing asset {upstream_name}" + "windows_cuda_selection: skip missing assets " + + ",".join(windows_cuda_upstream_asset_names(llama_tag, runtime)) ) continue attempts.append( AssetChoice( repo = UPSTREAM_REPO, tag = llama_tag, - name = upstream_name, + name = selected_name, url = asset_url, source_label = "upstream", install_kind = "windows-cuda", runtime_line = runtime_line, selection_log = list(selection_log) + [ - f"windows_cuda_selection: selected {upstream_name} runtime={runtime}" + f"windows_cuda_selection: selected {selected_name} runtime={runtime}" ], ) ) @@ -2353,18 +2762,26 @@ def copy_directory_contents(source_dir: Path, destination: Path) -> None: def hydrate_source_tree( - upstream_tag: str, + source_ref: str, install_dir: Path, work_dir: Path, *, + source_repo: str = UPSTREAM_REPO, expected_sha256: str, + source_label: str | None = None, + exact_source: bool = False, ) -> None: - archive_path = work_dir / f"llama.cpp-source-{upstream_tag}.tar.gz" - source_urls = upstream_source_archive_urls(upstream_tag) + archive_path = work_dir / f"llama.cpp-source-{source_ref}.tar.gz" + source_urls = ( + commit_source_archive_urls(source_repo, source_ref) + if exact_source + else upstream_source_archive_urls(source_ref) + ) + label = source_label or f"llama.cpp source tree for {source_ref}" extract_dir = Path(tempfile.mkdtemp(prefix = "source-extract-", dir = work_dir)) try: - log(f"downloading llama.cpp source tree for upstream tag {upstream_tag}") + log(f"downloading {label}") last_exc: Exception | None = None downloaded = False for index, source_url in enumerate(source_urls): @@ -2377,7 +2794,7 @@ def hydrate_source_tree( source_url, archive_path, expected_sha256 = expected_sha256, - label = f"llama.cpp source tree for {upstream_tag}", + label = label, ) downloaded = True break @@ -2410,9 +2827,7 @@ def hydrate_source_tree( except PrebuiltFallback: raise except Exception as exc: - raise PrebuiltFallback( - f"failed to hydrate upstream llama.cpp source tree for {upstream_tag}: {exc}" - ) from exc + raise PrebuiltFallback(f"failed to hydrate {label}: {exc}") from exc finally: remove_tree(extract_dir) @@ -3451,10 +3866,32 @@ def apply_approved_hashes( attempts: Iterable[AssetChoice], checksums: ApprovedReleaseChecksums, ) -> list[AssetChoice]: + def approved_hash_for_attempt(attempt: AssetChoice) -> ApprovedArtifactHash | None: + approved = checksums.artifacts.get(attempt.name) + if approved is not None: + return approved + if ( + isinstance(attempt.tag, str) + and attempt.tag + and attempt.tag != checksums.upstream_tag + and attempt.name.startswith("llama-") + ): + legacy_prefix = f"llama-{attempt.tag}-" + compatibility_prefix = f"llama-{checksums.upstream_tag}-" + compatibility_name = ( + attempt.name.replace(legacy_prefix, compatibility_prefix, 1) + if attempt.name.startswith(legacy_prefix) + else attempt.name + ) + approved = checksums.artifacts.get(compatibility_name) + if approved is not None: + return approved + return None + approved_attempts: list[AssetChoice] = [] missing_assets: list[str] = [] for attempt in attempts: - approved = checksums.artifacts.get(attempt.name) + approved = approved_hash_for_attempt(attempt) if approved is None: missing_assets.append(attempt.name) continue @@ -3481,6 +3918,39 @@ def require_approved_source_hash( return approved_source +def preferred_source_archive( + checksums: ApprovedReleaseChecksums, llama_tag: str +) -> tuple[str, str, ApprovedArtifactHash, bool]: + exact_source = exact_source_archive_hash(checksums) + exact_repo = repo_slug_from_source(checksums.source_repo) or repo_slug_from_source( + checksums.source_repo_url + ) + if exact_source is not None and exact_repo and checksums.source_commit: + return ( + exact_repo, + checksums.source_commit, + exact_source, + True, + ) + legacy = require_approved_source_hash(checksums, llama_tag) + return ( + UPSTREAM_REPO, + llama_tag, + legacy, + False, + ) + + +def selected_source_archive_metadata( + checksums: ApprovedReleaseChecksums, + llama_tag: str, +) -> tuple[str, str | None]: + _source_repo, _source_ref, source_archive, _exact_source = preferred_source_archive( + checksums, llama_tag + ) + return source_archive.asset_name, source_archive.sha256 + + def resolve_install_attempts( llama_tag: str, host: HostInfo, @@ -3583,10 +4053,10 @@ def write_prebuilt_metadata( approved_checksums: ApprovedReleaseChecksums, prebuilt_fallback_used: bool, ) -> None: - source_archive = approved_checksums.artifacts.get( - source_archive_logical_name(llama_tag) + source_asset_name, source_sha256 = selected_source_archive_metadata( + approved_checksums, + 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, @@ -3594,6 +4064,7 @@ def write_prebuilt_metadata( "asset": choice.name, "asset_sha256": choice.expected_sha256, "source": choice.source_label, + "source_asset": source_asset_name, "source_sha256": source_sha256, "runtime_line": choice.runtime_line, "bundle_profile": choice.bundle_profile, @@ -3612,8 +4083,15 @@ def write_prebuilt_metadata( "asset": choice.name, "asset_sha256": choice.expected_sha256, "source": choice.source_label, + "source_asset": source_asset_name, "source_sha256": source_sha256, "source_commit": approved_checksums.source_commit, + "source_commit_short": approved_checksums.source_commit_short, + "source_repo": approved_checksums.source_repo, + "source_repo_url": approved_checksums.source_repo_url, + "source_ref_kind": approved_checksums.source_ref_kind, + "requested_source_ref": approved_checksums.requested_source_ref, + "resolved_source_ref": approved_checksums.resolved_source_ref, "bundle_profile": choice.bundle_profile, "runtime_line": choice.runtime_line, "coverage_class": choice.coverage_class, @@ -3635,10 +4113,10 @@ def expected_install_fingerprint( ) -> str | None: if not choice.expected_sha256: return None - source_archive = approved_checksums.artifacts.get( - source_archive_logical_name(llama_tag) + source_asset_name, source_sha256 = selected_source_archive_metadata( + approved_checksums, + llama_tag, ) - source_sha256 = source_archive.sha256 if source_archive is not None else None payload = { "published_repo": approved_checksums.repo, "release_tag": release_tag, @@ -3646,6 +4124,7 @@ def expected_install_fingerprint( "asset": choice.name, "asset_sha256": choice.expected_sha256, "source": choice.source_label, + "source_asset": source_asset_name, "source_sha256": source_sha256, "runtime_line": choice.runtime_line, "bundle_profile": choice.bundle_profile, @@ -3817,19 +4296,27 @@ def validate_prebuilt_choice( prebuilt_fallback_used: bool, quantized_path: Path, ) -> tuple[Path, Path]: - source_archive = approved_checksums.artifacts.get( - source_archive_logical_name(llama_tag) + source_repo, source_ref, source_archive, exact_source = preferred_source_archive( + approved_checksums, llama_tag ) - if source_archive is None: - raise PrebuiltFallback( - f"approved checksum asset did not contain source archive {source_archive_logical_name(llama_tag)}" + if exact_source: + log( + f"hydrating exact llama.cpp source for {source_repo}@{source_ref} into {install_dir}" ) - log(f"hydrating upstream llama.cpp source for {llama_tag} into {install_dir}") + else: + log(f"hydrating upstream llama.cpp source for {llama_tag} into {install_dir}") hydrate_source_tree( - llama_tag, + source_ref, install_dir, work_dir, + source_repo = source_repo, expected_sha256 = source_archive.sha256, + source_label = ( + f"llama.cpp source tree for {source_repo}@{source_ref}" + if exact_source + else f"llama.cpp source tree for {llama_tag}" + ), + exact_source = exact_source, ) log(f"overlaying prebuilt bundle {choice.name} into {install_dir}") server_path, quantize_path = install_from_archives( @@ -4087,30 +4574,103 @@ def parse_args() -> argparse.Namespace: "selected by the current published-release policy." ), ) + resolve_group.add_argument( + "--resolve-source-build", + nargs = "?", + const = "latest", + help = ("Resolve the source-build fallback plan."), + ) + parser.add_argument( + "--output-format", + choices = ("plain", "json"), + default = "plain", + help = "Resolver output format. Defaults to plain.", + ) return parser.parse_args() +def emit_resolver_output(payload: dict[str, Any], *, output_format: str) -> None: + if output_format == "json": + print(json.dumps(payload, sort_keys = True)) + return + if "llama_tag" in payload: + print(payload["llama_tag"]) + return + if { + "source_url", + "source_ref_kind", + "source_ref", + }.issubset(payload): + print( + "\t".join( + ( + str(payload["source_url"]), + str(payload["source_ref_kind"]), + str(payload["source_ref"]), + ) + ) + ) + return + print(json.dumps(payload, sort_keys = True)) + + def main() -> int: args = parse_args() if args.resolve_llama_tag is not None: - # Pass published_repo so the resolver prefers the Unsloth release tag - # (tested/approved) over the upstream ggml-org bleeding-edge tag. - print(resolve_requested_llama_tag(args.resolve_llama_tag, args.published_repo)) + resolved = resolve_requested_llama_tag( + args.resolve_llama_tag, + args.published_repo, + args.published_release_tag or "", + ) + emit_resolver_output( + { + "requested_tag": normalized_requested_llama_tag(args.resolve_llama_tag), + "llama_tag": resolved, + }, + output_format = args.output_format, + ) return EXIT_SUCCESS if args.resolve_install_tag is not None: - print( - resolve_requested_install_tag( - args.resolve_install_tag, - args.published_release_tag or "", - args.published_repo, - ) + resolved = resolve_requested_install_tag( + args.resolve_install_tag, + args.published_release_tag or "", + args.published_repo, + ) + emit_resolver_output( + { + "requested_tag": normalized_requested_llama_tag( + args.resolve_install_tag + ), + "llama_tag": resolved, + }, + output_format = args.output_format, + ) + return EXIT_SUCCESS + + if args.resolve_source_build is not None: + plan = resolve_source_build_plan( + args.resolve_source_build, + args.published_repo, + args.published_release_tag or "", + ) + emit_resolver_output( + { + "requested_tag": normalized_requested_llama_tag( + args.resolve_source_build + ), + "source_url": plan.source_url, + "source_ref_kind": plan.source_ref_kind, + "source_ref": plan.source_ref, + "compatibility_upstream_tag": plan.compatibility_upstream_tag, + }, + output_format = args.output_format, ) return EXIT_SUCCESS if not args.install_dir: raise SystemExit( - "install_llama_prebuilt.py: --install-dir is required unless --resolve-llama-tag or --resolve-install-tag is used" + "install_llama_prebuilt.py: --install-dir is required unless --resolve-llama-tag, --resolve-install-tag, or --resolve-source-build is used" ) install_prebuilt( install_dir = Path(args.install_dir).expanduser().resolve(), diff --git a/studio/setup.ps1 b/studio/setup.ps1 index f4ad42d615..1784b66ee9 100644 --- a/studio/setup.ps1 +++ b/studio/setup.ps1 @@ -1604,6 +1604,9 @@ $LlamaPr = if ($env:UNSLOTH_LLAMA_PR) { $env:UNSLOTH_LLAMA_PR.Trim() } else { "" $LlamaPrForce = if ($env:UNSLOTH_LLAMA_PR_FORCE) { $env:UNSLOTH_LLAMA_PR_FORCE.Trim() } else { $DefaultLlamaPrForce } $LlamaSource = if ($env:UNSLOTH_LLAMA_SOURCE) { $env:UNSLOTH_LLAMA_SOURCE.Trim() } else { $DefaultLlamaSource } if ($LlamaSource.EndsWith('.git')) { $LlamaSource = $LlamaSource.Substring(0, $LlamaSource.Length - 4) } +$ResolvedSourceUrl = $LlamaSource +$ResolvedSourceRef = $RequestedLlamaTag +$ResolvedSourceRefKind = "tag" if ($LlamaSource -ne "https://github.com/ggml-org/llama.cpp") { step "llama.cpp" "custom source: $LlamaSource -- forcing source build" "Yellow" @@ -1623,6 +1626,9 @@ if ($LlamaPr) { } step "llama.cpp" "UNSLOTH_LLAMA_PR=$LlamaPr -- will build from PR head" "Yellow" $ResolvedLlamaTag = "pr-$LlamaPr" + $ResolvedSourceUrl = $LlamaSource + $ResolvedSourceRef = "pr-$LlamaPr" + $ResolvedSourceRefKind = "pull" $NeedLlamaSourceBuild = $true $SkipPrebuiltInstall = $true } elseif ($SkipPrebuiltInstall) { @@ -1630,12 +1636,16 @@ if ($LlamaPr) { # prebuilt release resolution. When building from a custom fork, the fork # may not carry upstream bNNNN tags. if ($LlamaSource -eq "https://github.com/ggml-org/llama.cpp") { - $resolveTagArgs = @("--resolve-llama-tag", $RequestedLlamaTag, "--published-repo", $HelperReleaseRepo) + $resolveTagArgs = @("--resolve-llama-tag", $RequestedLlamaTag, "--published-repo", $HelperReleaseRepo, "--output-format", "json") if ($env:UNSLOTH_LLAMA_RELEASE_TAG) { $resolveTagArgs += @("--published-release-tag", $env:UNSLOTH_LLAMA_RELEASE_TAG) } $fallbackOutput = & python "$PSScriptRoot\install_llama_prebuilt.py" @resolveTagArgs 2>$null $fallbackExit = $LASTEXITCODE $ResolvedLlamaTag = if ($fallbackExit -eq 0 -and $fallbackOutput) { - ($fallbackOutput | Select-Object -Last 1).ToString().Trim() + try { + (($fallbackOutput | Out-String) | ConvertFrom-Json).llama_tag + } catch { + $RequestedLlamaTag + } } else { $RequestedLlamaTag } @@ -1643,30 +1653,42 @@ if ($LlamaPr) { $ResolvedLlamaTag = $RequestedLlamaTag } } else { - $resolveInstallArgs = @("--resolve-install-tag", $RequestedLlamaTag, "--published-repo", $HelperReleaseRepo) + $resolveInstallArgs = @("--resolve-install-tag", $RequestedLlamaTag, "--published-repo", $HelperReleaseRepo, "--output-format", "json") if ($env:UNSLOTH_LLAMA_RELEASE_TAG) { $resolveInstallArgs += @("--published-release-tag", $env:UNSLOTH_LLAMA_RELEASE_TAG) } - $resolveOutput = & python "$PSScriptRoot\install_llama_prebuilt.py" @resolveInstallArgs 2>&1 + $resolveErrorLog = New-TemporaryFile + $resolveOutput = & python "$PSScriptRoot\install_llama_prebuilt.py" @resolveInstallArgs 2>$resolveErrorLog $resolveExit = $LASTEXITCODE - $ResolvedLlamaTag = if ($resolveOutput) { ($resolveOutput | Select-Object -Last 1).ToString().Trim() } else { "" } + $ResolvedLlamaTag = if ($resolveOutput) { + try { + (($resolveOutput | Out-String) | ConvertFrom-Json).llama_tag + } catch { + "" + } + } else { "" } if ($resolveExit -ne 0 -or [string]::IsNullOrWhiteSpace($ResolvedLlamaTag)) { Write-Host "" substep "Failed to resolve a published llama.cpp release via $HelperReleaseRepo" "Yellow" - Write-LlamaFailureLog -Output ($resolveOutput | Out-String) + Write-LlamaFailureLog -Output (Get-Content -Raw $resolveErrorLog) # Resolve the llama.cpp tag for source-build fallback. Pass --published-repo # so the resolver prefers the latest usable Unsloth-published upstream tag # before falling back to the bleeding-edge ggml-org/llama.cpp tag. - $resolveFallbackArgs = @("--resolve-llama-tag", $RequestedLlamaTag, "--published-repo", $HelperReleaseRepo) + $resolveFallbackArgs = @("--resolve-llama-tag", $RequestedLlamaTag, "--published-repo", $HelperReleaseRepo, "--output-format", "json") if ($env:UNSLOTH_LLAMA_RELEASE_TAG) { $resolveFallbackArgs += @("--published-release-tag", $env:UNSLOTH_LLAMA_RELEASE_TAG) } $fallbackOutput = & python "$PSScriptRoot\install_llama_prebuilt.py" @resolveFallbackArgs 2>$null $fallbackExit = $LASTEXITCODE $ResolvedLlamaTag = if ($fallbackExit -eq 0 -and $fallbackOutput) { - ($fallbackOutput | Select-Object -Last 1).ToString().Trim() + try { + (($fallbackOutput | Out-String) | ConvertFrom-Json).llama_tag + } catch { + $RequestedLlamaTag + } } else { $RequestedLlamaTag } $NeedLlamaSourceBuild = $true $SkipPrebuiltInstall = $true } + Remove-Item $resolveErrorLog -Force -ErrorAction SilentlyContinue } Write-Host "" @@ -1875,14 +1897,34 @@ if (-not $NeedLlamaSourceBuild) { [Environment]::SetEnvironmentVariable('CudaToolkitDir', "$CudaToolkitRoot\", 'Process') } + if (-not $LlamaPr) { + if ($LlamaSource -eq "https://github.com/ggml-org/llama.cpp") { + $resolveSourceArgs = @("--resolve-source-build", $RequestedLlamaTag, "--published-repo", $HelperReleaseRepo, "--output-format", "json") + if ($env:UNSLOTH_LLAMA_RELEASE_TAG) { $resolveSourceArgs += @("--published-release-tag", $env:UNSLOTH_LLAMA_RELEASE_TAG) } + $sourcePlanOutput = & python "$PSScriptRoot\install_llama_prebuilt.py" @resolveSourceArgs 2>$null + $sourcePlanExit = $LASTEXITCODE + if ($sourcePlanExit -eq 0 -and $sourcePlanOutput) { + try { + $sourcePlan = ($sourcePlanOutput | Out-String) | ConvertFrom-Json + $ResolvedSourceUrl = $sourcePlan.source_url + $ResolvedSourceRefKind = $sourcePlan.source_ref_kind + $ResolvedSourceRef = $sourcePlan.source_ref + } catch { + } + } + } + if ([string]::IsNullOrWhiteSpace($ResolvedSourceUrl)) { $ResolvedSourceUrl = $LlamaSource } + if ([string]::IsNullOrWhiteSpace($ResolvedSourceRef)) { $ResolvedSourceRef = $ResolvedLlamaTag } + } + # -- Step A: Clone or pull llama.cpp -- - $UseConcreteRef = ($ResolvedLlamaTag -ne "latest" -and -not [string]::IsNullOrWhiteSpace($ResolvedLlamaTag)) + $UseConcreteRef = ($ResolvedSourceRef -ne "latest" -and -not [string]::IsNullOrWhiteSpace($ResolvedSourceRef)) if (Test-Path (Join-Path $LlamaCppDir ".git")) { - Write-Host " Syncing llama.cpp to $ResolvedLlamaTag..." -ForegroundColor Gray + Write-Host " Syncing llama.cpp to $ResolvedSourceRef..." -ForegroundColor Gray # Always sync the remote URL so switching between default/fork sources works - Invoke-SetupCommand -AlwaysQuiet { git -C $LlamaCppDir remote set-url origin "$LlamaSource.git" } | Out-Null + Invoke-SetupCommand -AlwaysQuiet { git -C $LlamaCppDir remote set-url origin "$ResolvedSourceUrl.git" } | Out-Null if ($LlamaPr) { $gitFetchExit = Invoke-SetupCommand -AlwaysQuiet { git -C $LlamaCppDir fetch --depth 1 origin "pull/$LlamaPr/head" } if ($gitFetchExit -ne 0) { @@ -1897,8 +1939,34 @@ if (-not $NeedLlamaSourceBuild) { Invoke-SetupCommand -AlwaysQuiet { git -C $LlamaCppDir clean -fdx } | Out-Null } } + } elseif ($ResolvedSourceRefKind -eq "pull") { + $gitFetchExit = Invoke-SetupCommand -AlwaysQuiet { git -C $LlamaCppDir fetch --depth 1 origin $ResolvedSourceRef } + if ($gitFetchExit -ne 0) { + substep "git fetch failed -- using existing source" "Yellow" + } else { + $gitCheckoutExit = Invoke-SetupCommand -AlwaysQuiet { git -C $LlamaCppDir checkout -B unsloth-llama-build FETCH_HEAD } + if ($gitCheckoutExit -ne 0) { + $BuildOk = $false + $FailedStep = "git checkout" + } else { + Invoke-SetupCommand -AlwaysQuiet { git -C $LlamaCppDir clean -fdx } | Out-Null + } + } + } elseif ($ResolvedSourceRefKind -eq "commit") { + $gitFetchExit = Invoke-SetupCommand -AlwaysQuiet { git -C $LlamaCppDir fetch --depth 1 origin $ResolvedSourceRef } + if ($gitFetchExit -ne 0) { + substep "git fetch failed -- using existing source" "Yellow" + } else { + $gitCheckoutExit = Invoke-SetupCommand -AlwaysQuiet { git -C $LlamaCppDir checkout -B unsloth-llama-build FETCH_HEAD } + if ($gitCheckoutExit -ne 0) { + $BuildOk = $false + $FailedStep = "git checkout" + } else { + Invoke-SetupCommand -AlwaysQuiet { git -C $LlamaCppDir clean -fdx } | Out-Null + } + } } elseif ($UseConcreteRef) { - $gitFetchExit = Invoke-SetupCommand -AlwaysQuiet { git -C $LlamaCppDir fetch --depth 1 origin $ResolvedLlamaTag } + $gitFetchExit = Invoke-SetupCommand -AlwaysQuiet { git -C $LlamaCppDir fetch --depth 1 origin $ResolvedSourceRef } if ($gitFetchExit -ne 0) { substep "git fetch failed -- using existing source" "Yellow" } else { @@ -1925,8 +1993,9 @@ if (-not $NeedLlamaSourceBuild) { } } } else { - Write-Host " Cloning llama.cpp @ $ResolvedLlamaTag..." -ForegroundColor Gray + Write-Host " Cloning llama.cpp @ $ResolvedSourceRef..." -ForegroundColor Gray $buildTmp = "$LlamaCppDir.build.$PID" + $null = New-Item -ItemType Directory -Force -Path (Split-Path $LlamaCppDir -Parent) if (Test-Path $buildTmp) { Remove-Item -Recurse -Force $buildTmp } if ($LlamaPr) { $cloneExit = Invoke-SetupCommand -AlwaysQuiet { git clone --depth 1 "$LlamaSource.git" $buildTmp } @@ -1951,12 +2020,58 @@ if (-not $NeedLlamaSourceBuild) { if (Test-Path $buildTmp) { Remove-Item -Recurse -Force $buildTmp } } } + } elseif ($ResolvedSourceRefKind -eq "pull") { + $cloneExit = Invoke-SetupCommand -AlwaysQuiet { git clone --depth 1 "$ResolvedSourceUrl.git" $buildTmp } + if ($cloneExit -ne 0) { + $BuildOk = $false + $FailedStep = "git clone" + if (Test-Path $buildTmp) { Remove-Item -Recurse -Force $buildTmp } + } + if ($BuildOk) { + $fetchExit = Invoke-SetupCommand -AlwaysQuiet { git -C $buildTmp fetch --depth 1 origin $ResolvedSourceRef } + if ($fetchExit -ne 0) { + $BuildOk = $false + $FailedStep = "git fetch source PR ref" + if (Test-Path $buildTmp) { Remove-Item -Recurse -Force $buildTmp } + } + } + if ($BuildOk) { + $checkoutExit = Invoke-SetupCommand -AlwaysQuiet { git -C $buildTmp checkout -B unsloth-llama-build FETCH_HEAD } + if ($checkoutExit -ne 0) { + $BuildOk = $false + $FailedStep = "git checkout source PR ref" + if (Test-Path $buildTmp) { Remove-Item -Recurse -Force $buildTmp } + } + } + } elseif ($ResolvedSourceRefKind -eq "commit") { + $cloneExit = Invoke-SetupCommand -AlwaysQuiet { git clone --depth 1 "$ResolvedSourceUrl.git" $buildTmp } + if ($cloneExit -ne 0) { + $BuildOk = $false + $FailedStep = "git clone" + if (Test-Path $buildTmp) { Remove-Item -Recurse -Force $buildTmp } + } + if ($BuildOk) { + $fetchExit = Invoke-SetupCommand -AlwaysQuiet { git -C $buildTmp fetch --depth 1 origin $ResolvedSourceRef } + if ($fetchExit -ne 0) { + $BuildOk = $false + $FailedStep = "git fetch source commit" + if (Test-Path $buildTmp) { Remove-Item -Recurse -Force $buildTmp } + } + } + if ($BuildOk) { + $checkoutExit = Invoke-SetupCommand -AlwaysQuiet { git -C $buildTmp checkout -B unsloth-llama-build FETCH_HEAD } + if ($checkoutExit -ne 0) { + $BuildOk = $false + $FailedStep = "git checkout source commit" + if (Test-Path $buildTmp) { Remove-Item -Recurse -Force $buildTmp } + } + } } else { $cloneArgs = @("clone", "--depth", "1") if ($UseConcreteRef) { - $cloneArgs += @("--branch", $ResolvedLlamaTag) + $cloneArgs += @("--branch", $ResolvedSourceRef) } - $cloneArgs += @("$LlamaSource.git", $buildTmp) + $cloneArgs += @("$ResolvedSourceUrl.git", $buildTmp) $cloneExit = Invoke-SetupCommand -AlwaysQuiet { git @cloneArgs } if ($cloneExit -ne 0) { $BuildOk = $false diff --git a/studio/setup.sh b/studio/setup.sh index 36926733e1..26903b120a 100755 --- a/studio/setup.sh +++ b/studio/setup.sh @@ -485,6 +485,9 @@ _LLAMA_PR="${UNSLOTH_LLAMA_PR:-}" _LLAMA_PR_FORCE="${UNSLOTH_LLAMA_PR_FORCE:-${_DEFAULT_LLAMA_PR_FORCE}}" _LLAMA_SOURCE="${UNSLOTH_LLAMA_SOURCE:-${_DEFAULT_LLAMA_SOURCE}}" _LLAMA_SOURCE="${_LLAMA_SOURCE%.git}" # normalize: strip trailing .git +_RESOLVED_SOURCE_URL="$_LLAMA_SOURCE" +_RESOLVED_SOURCE_REF="$_REQUESTED_LLAMA_TAG" +_RESOLVED_SOURCE_REF_KIND="tag" # Non-default source URL forces source build (fork has different code than prebuilt). if [ "$_LLAMA_SOURCE" != "https://github.com/ggml-org/llama.cpp" ]; then @@ -507,6 +510,9 @@ if [ -n "$_LLAMA_PR" ]; then fi step "llama.cpp" "UNSLOTH_LLAMA_PR=$_LLAMA_PR -- will build from PR head" "$C_WARN" _RESOLVED_LLAMA_TAG="pr-$_LLAMA_PR" + _RESOLVED_SOURCE_URL="$_LLAMA_SOURCE" + _RESOLVED_SOURCE_REF="pr-$_LLAMA_PR" + _RESOLVED_SOURCE_REF_KIND="pull" _NEED_LLAMA_SOURCE_BUILD=true _SKIP_PREBUILT_INSTALL=true elif [ "${_SKIP_PREBUILT_INSTALL:-false}" = true ]; then @@ -516,14 +522,22 @@ elif [ "${_SKIP_PREBUILT_INSTALL:-false}" = true ]; then # only when the source is the default ggml-org repo. if [ "$_LLAMA_SOURCE" = "https://github.com/ggml-org/llama.cpp" ]; then _RESOLVE_TAG_ARGS=(--resolve-llama-tag "$_REQUESTED_LLAMA_TAG" --published-repo "$_HELPER_RELEASE_REPO") + _RESOLVE_TAG_ARGS+=(--output-format json) if [ -n "${UNSLOTH_LLAMA_RELEASE_TAG:-}" ]; then _RESOLVE_TAG_ARGS+=(--published-release-tag "$UNSLOTH_LLAMA_RELEASE_TAG") fi set +e - _RESOLVED_LLAMA_TAG="$(python "$SCRIPT_DIR/install_llama_prebuilt.py" "${_RESOLVE_TAG_ARGS[@]}" 2>/dev/null)" + _RESOLVE_TAG_JSON="$(python "$SCRIPT_DIR/install_llama_prebuilt.py" "${_RESOLVE_TAG_ARGS[@]}" 2>/dev/null)" _RESOLVE_UPSTREAM_STATUS=$? set -e - if [ "$_RESOLVE_UPSTREAM_STATUS" -ne 0 ] || [ -z "$_RESOLVED_LLAMA_TAG" ]; then + if [ "$_RESOLVE_UPSTREAM_STATUS" -eq 0 ] && [ -n "${_RESOLVE_TAG_JSON:-}" ]; then + _RESOLVED_LLAMA_TAG="$( + printf '%s' "$_RESOLVE_TAG_JSON" | python -c 'import json,sys; print(json.load(sys.stdin).get("llama_tag",""))' 2>/dev/null || true + )" + else + _RESOLVED_LLAMA_TAG="" + fi + if [ -z "$_RESOLVED_LLAMA_TAG" ]; then _RESOLVED_LLAMA_TAG="$_REQUESTED_LLAMA_TAG" fi else @@ -531,17 +545,22 @@ elif [ "${_SKIP_PREBUILT_INSTALL:-false}" = true ]; then fi else _RESOLVE_INSTALL_ARGS=(--resolve-install-tag "$_REQUESTED_LLAMA_TAG" --published-repo "$_HELPER_RELEASE_REPO") + _RESOLVE_INSTALL_ARGS+=(--output-format json) if [ -n "${UNSLOTH_LLAMA_RELEASE_TAG:-}" ]; then _RESOLVE_INSTALL_ARGS+=(--published-release-tag "$UNSLOTH_LLAMA_RELEASE_TAG") fi _RESOLVE_LLAMA_LOG="$(mktemp)" set +e - python "$SCRIPT_DIR/install_llama_prebuilt.py" \ - "${_RESOLVE_INSTALL_ARGS[@]}" >"$_RESOLVE_LLAMA_LOG" 2>&1 + _RESOLVE_INSTALL_JSON="$( + python "$SCRIPT_DIR/install_llama_prebuilt.py" \ + "${_RESOLVE_INSTALL_ARGS[@]}" 2>"$_RESOLVE_LLAMA_LOG" + )" _RESOLVE_LLAMA_STATUS=$? set -e if [ "$_RESOLVE_LLAMA_STATUS" -eq 0 ]; then - _RESOLVED_LLAMA_TAG="$(tail -n 1 "$_RESOLVE_LLAMA_LOG" | tr -d '\r')" + _RESOLVED_LLAMA_TAG="$( + printf '%s' "${_RESOLVE_INSTALL_JSON:-}" | python -c 'import json,sys; print(json.load(sys.stdin).get("llama_tag",""))' 2>/dev/null || true + )" else _RESOLVED_LLAMA_TAG="" fi @@ -553,13 +572,21 @@ else # so the resolver prefers the latest usable Unsloth-published upstream tag # before falling back to the bleeding-edge ggml-org/llama.cpp tag. _RESOLVE_FALLBACK_ARGS=(--resolve-llama-tag "$_REQUESTED_LLAMA_TAG" --published-repo "$_HELPER_RELEASE_REPO") + _RESOLVE_FALLBACK_ARGS+=(--output-format json) if [ -n "${UNSLOTH_LLAMA_RELEASE_TAG:-}" ]; then _RESOLVE_FALLBACK_ARGS+=(--published-release-tag "$UNSLOTH_LLAMA_RELEASE_TAG") fi - _RESOLVED_LLAMA_TAG="$(python "$SCRIPT_DIR/install_llama_prebuilt.py" "${_RESOLVE_FALLBACK_ARGS[@]}" 2>/dev/null)" + _RESOLVE_FALLBACK_JSON="$(python "$SCRIPT_DIR/install_llama_prebuilt.py" "${_RESOLVE_FALLBACK_ARGS[@]}" 2>/dev/null)" _RESOLVE_UPSTREAM_STATUS=$? set -e - if [ "$_RESOLVE_UPSTREAM_STATUS" -ne 0 ] || [ -z "$_RESOLVED_LLAMA_TAG" ]; then + if [ "$_RESOLVE_UPSTREAM_STATUS" -eq 0 ] && [ -n "${_RESOLVE_FALLBACK_JSON:-}" ]; then + _RESOLVED_LLAMA_TAG="$( + printf '%s' "$_RESOLVE_FALLBACK_JSON" | python -c 'import json,sys; print(json.load(sys.stdin).get("llama_tag",""))' 2>/dev/null || true + )" + else + _RESOLVED_LLAMA_TAG="" + fi + if [ -z "$_RESOLVED_LLAMA_TAG" ]; then _RESOLVED_LLAMA_TAG="$_REQUESTED_LLAMA_TAG" fi _NEED_LLAMA_SOURCE_BUILD=true @@ -701,7 +728,40 @@ else step "llama.cpp" "skipped (git not found)" "$C_WARN" [ -f "$LLAMA_SERVER_BIN" ] || _LLAMA_CPP_DEGRADED=true else + if [ -z "$_LLAMA_PR" ]; then + if [ "$_LLAMA_SOURCE" = "https://github.com/ggml-org/llama.cpp" ]; then + _RESOLVE_SOURCE_ARGS=(--resolve-source-build "$_REQUESTED_LLAMA_TAG" --published-repo "$_HELPER_RELEASE_REPO") + _RESOLVE_SOURCE_ARGS+=(--output-format json) + if [ -n "${UNSLOTH_LLAMA_RELEASE_TAG:-}" ]; then + _RESOLVE_SOURCE_ARGS+=(--published-release-tag "$UNSLOTH_LLAMA_RELEASE_TAG") + fi + set +e + _SOURCE_BUILD_PLAN="$(python "$SCRIPT_DIR/install_llama_prebuilt.py" "${_RESOLVE_SOURCE_ARGS[@]}" 2>/dev/null)" + _RESOLVE_SOURCE_STATUS=$? + set -e + if [ "$_RESOLVE_SOURCE_STATUS" -eq 0 ] && [ -n "$_SOURCE_BUILD_PLAN" ]; then + _RESOLVED_SOURCE_URL="$( + printf '%s' "$_SOURCE_BUILD_PLAN" | python -c 'import json,sys; print(json.load(sys.stdin).get("source_url",""))' 2>/dev/null || true + )" + _RESOLVED_SOURCE_REF_KIND="$( + printf '%s' "$_SOURCE_BUILD_PLAN" | python -c 'import json,sys; print(json.load(sys.stdin).get("source_ref_kind",""))' 2>/dev/null || true + )" + _RESOLVED_SOURCE_REF="$( + printf '%s' "$_SOURCE_BUILD_PLAN" | python -c 'import json,sys; print(json.load(sys.stdin).get("source_ref",""))' 2>/dev/null || true + )" + fi + fi + if [ -z "$_RESOLVED_SOURCE_URL" ]; then + _RESOLVED_SOURCE_URL="$_LLAMA_SOURCE" + fi + if [ -z "$_RESOLVED_SOURCE_REF" ]; then + _RESOLVED_SOURCE_REF="$_RESOLVED_LLAMA_TAG" + fi + fi + verbose_substep "source build repo: $_RESOLVED_SOURCE_URL" + verbose_substep "source build ref: ${_RESOLVED_SOURCE_REF:-latest} (${_RESOLVED_SOURCE_REF_KIND})" BUILD_OK=true + mkdir -p "$(dirname "$LLAMA_CPP_DIR")" _BUILD_TMP="${LLAMA_CPP_DIR}.build.$$" rm -rf "$_BUILD_TMP" if [ -n "$_LLAMA_PR" ]; then @@ -715,12 +775,34 @@ else run_quiet_no_exit "checkout PR #$_LLAMA_PR" \ git -C "$_BUILD_TMP" checkout "pr-$_LLAMA_PR" || BUILD_OK=false fi + elif [ "$_RESOLVED_SOURCE_REF_KIND" = "pull" ] && [ -n "$_RESOLVED_SOURCE_REF" ]; then + run_quiet_no_exit "clone llama.cpp" \ + git clone --depth 1 "${_RESOLVED_SOURCE_URL}.git" "$_BUILD_TMP" || BUILD_OK=false + if [ "$BUILD_OK" = true ]; then + run_quiet_no_exit "fetch source PR ref" \ + git -C "$_BUILD_TMP" fetch --depth 1 origin "$_RESOLVED_SOURCE_REF" || BUILD_OK=false + fi + if [ "$BUILD_OK" = true ]; then + run_quiet_no_exit "checkout source PR ref" \ + git -C "$_BUILD_TMP" checkout -B unsloth-llama-build FETCH_HEAD || BUILD_OK=false + fi + elif [ "$_RESOLVED_SOURCE_REF_KIND" = "commit" ] && [ -n "$_RESOLVED_SOURCE_REF" ]; then + run_quiet_no_exit "clone llama.cpp" \ + git clone --depth 1 "${_RESOLVED_SOURCE_URL}.git" "$_BUILD_TMP" || BUILD_OK=false + if [ "$BUILD_OK" = true ]; then + run_quiet_no_exit "fetch source commit" \ + git -C "$_BUILD_TMP" fetch --depth 1 origin "$_RESOLVED_SOURCE_REF" || BUILD_OK=false + fi + if [ "$BUILD_OK" = true ]; then + run_quiet_no_exit "checkout source commit" \ + git -C "$_BUILD_TMP" checkout -B unsloth-llama-build FETCH_HEAD || BUILD_OK=false + fi else _CLONE_ARGS=(git clone --depth 1) - if [ "$_RESOLVED_LLAMA_TAG" != "latest" ] && [ -n "$_RESOLVED_LLAMA_TAG" ]; then - _CLONE_ARGS+=(--branch "$_RESOLVED_LLAMA_TAG") + if [ "$_RESOLVED_SOURCE_REF" != "latest" ] && [ -n "$_RESOLVED_SOURCE_REF" ]; then + _CLONE_ARGS+=(--branch "$_RESOLVED_SOURCE_REF") fi - _CLONE_ARGS+=("${_LLAMA_SOURCE}.git" "$_BUILD_TMP") + _CLONE_ARGS+=("${_RESOLVED_SOURCE_URL}.git" "$_BUILD_TMP") run_quiet_no_exit "clone llama.cpp" \ "${_CLONE_ARGS[@]}" || BUILD_OK=false fi