From ef29dd8bad59680cd4b115c343f73534978f6659 Mon Sep 17 00:00:00 2001 From: oobabooga <112222186+oobabooga@users.noreply.github.com> Date: Tue, 7 Jul 2026 14:43:26 -0300 Subject: [PATCH 1/4] Prebuilds: accept unslothai/llama.cpp PRs in pr-set.json --- .github/workflows/unsloth-pr-set-lint.yml | 38 ++++---- .github/workflows/unsloth-prebuilt.yml | 108 ++++++++++++---------- scripts/unsloth/assemble_metadata.py | 4 +- scripts/unsloth/pr-set.json | 12 ++- 4 files changed, 90 insertions(+), 72 deletions(-) diff --git a/.github/workflows/unsloth-pr-set-lint.yml b/.github/workflows/unsloth-pr-set-lint.yml index 37a4a68d7d3e..36d2ccb2cd8e 100644 --- a/.github/workflows/unsloth-pr-set-lint.yml +++ b/.github/workflows/unsloth-pr-set-lint.yml @@ -4,13 +4,12 @@ name: "Unsloth: lint pr-set.json" # Tripwire for edits to the mix-build PR set: validates the file the moment a -# commit touching it is pushed (the team edits it by pushing to master, so -# there is no PR gate to hook). A bad entry can never build -- the nightly's -# resolve job re-runs the same structural checks -- but this surfaces the -# mistake on the commit within seconds instead of failing the 3 AM build, and -# adds the one check resolve skips: that each pinned commit actually belongs -# to the PR it is listed under (a wrong paste would otherwise merge unreviewed -# code while the manifest blames the named PR). +# push or PR touches it (the team usually edits it by pushing straight to +# master, so the push trigger is the main hook). A bad entry can never build -- +# the nightly's resolve job re-runs the same checks, including that each +# pinned commit actually belongs to the PR it is listed under -- but a red +# lint does not stop the schedule; this just surfaces the mistake on the +# commit within seconds instead of failing the 3 AM build. on: push: @@ -36,31 +35,36 @@ jobs: FILE=scripts/unsloth/pr-set.json jq -e '.prs | type == "array" and all(.[]; type == "string")' "$FILE" >/dev/null \ || { echo "::error file=$FILE::.prs must be an array of PR url strings" >&2; exit 1; } - URL_RE='^https://github\.com/ggml-org/llama\.cpp/pull/([0-9]+)/commits/([0-9a-f]{40})/?$' + URL_RE='^https://github\.com/(ggml-org|unslothai)/llama\.cpp/pull/([0-9]+)/commits/([0-9a-f]{40})/?$' fail=0 for url in $(jq -r '.prs[]' "$FILE"); do if ! [[ "$url" =~ $URL_RE ]]; then - echo "::error file=$FILE::malformed entry '$url' (expected https://github.com/ggml-org/llama.cpp/pull//commits/<40-hex-sha>)" + echo "::error file=$FILE::malformed entry '$url' (expected https://github.com/{ggml-org,unslothai}/llama.cpp/pull//commits/<40-hex-sha>)" + fail=1 + continue + fi + SRC="${BASH_REMATCH[1]}/llama.cpp"; NUM="${BASH_REMATCH[2]}"; PIN="${BASH_REMATCH[3]}" + # Don't let a 404 under set -e kill the collect-all-errors loop. + if ! PR_JSON="$(gh api "repos/${SRC}/pulls/${NUM}" --jq '{state: .state, commits: .commits, head: .head.sha}')"; then + echo "::error file=$FILE::could not fetch ${SRC}#${NUM} (nonexistent PR number in '$url', or a transient API failure)" fail=1 continue fi - NUM="${BASH_REMATCH[1]}"; PIN="${BASH_REMATCH[2]}" - PR_JSON="$(gh api "repos/ggml-org/llama.cpp/pulls/${NUM}" --jq '{state: .state, commits: .commits, head: .head.sha}')" STATE="$(jq -r .state <<<"$PR_JSON")" COMMITS="$(jq -r .commits <<<"$PR_JSON")" HEAD="$(jq -r .head <<<"$PR_JSON")" - [ "$STATE" = "open" ] || echo "::warning file=$FILE::PR #${NUM} is ${STATE}; the nightly will skip this entry" + [ "$STATE" = "open" ] || echo "::warning file=$FILE::${SRC}#${NUM} is ${STATE}; the nightly will skip this entry" # The commits listing is capped at 250 by the API; past that the # membership check cannot be trusted, so skip it rather than # false-fail a legitimate giant PR. if [ "$COMMITS" -gt 250 ]; then - echo "::notice::PR #${NUM} has ${COMMITS} commits (over the API listing cap); skipping pin membership check" - elif ! gh api "repos/ggml-org/llama.cpp/pulls/${NUM}/commits" --paginate --jq '.[].sha' | grep -qx "$PIN"; then - echo "::error file=$FILE::pinned commit ${PIN} is not a commit of PR #${NUM}" + echo "::notice::${SRC}#${NUM} has ${COMMITS} commits (over the API listing cap); skipping pin membership check" + elif ! gh api "repos/${SRC}/pulls/${NUM}/commits" --paginate --jq '.[].sha' | grep -qx "$PIN"; then + echo "::error file=$FILE::pinned commit ${PIN} is not a commit of ${SRC}#${NUM}" fail=1 continue fi - [ "$PIN" = "$HEAD" ] || echo "::notice::PR #${NUM} pin ${PIN} is behind its head ${HEAD}" - echo "OK: PR #${NUM} @ ${PIN} (${STATE})" + [ "$PIN" = "$HEAD" ] || echo "::notice::${SRC}#${NUM} pin ${PIN} is behind its head ${HEAD}" + echo "OK: ${SRC}#${NUM} @ ${PIN} (${STATE})" done exit "$fail" diff --git a/.github/workflows/unsloth-prebuilt.yml b/.github/workflows/unsloth-prebuilt.yml index 13ff8d0626e2..714dc6f29d5a 100644 --- a/.github/workflows/unsloth-prebuilt.yml +++ b/.github/workflows/unsloth-prebuilt.yml @@ -19,11 +19,12 @@ name: Unsloth prebuilt (full release) # installer needs the full bundle set at the same tag or it'll dispatch to # something that isn't there. # -# Mix builds: scripts/unsloth/pr-set.json can list upstream PRs (each pinned -# to an exact commit) to merge into the build. `resolve` merges the open ones -# onto the base tag and uploads the merged tree as a workflow artifact that -# the children extract instead of cloning; the release is tagged -# b####-mix-. Empty list = vanilla upstream build. +# Mix builds: scripts/unsloth/pr-set.json can list ggml-org/llama.cpp or +# unslothai/llama.cpp PRs (each pinned to an exact commit) to merge into the +# build. `resolve` merges the open ones onto the base tag and uploads the +# merged tree as a workflow artifact that the children extract instead of +# cloning; the release is tagged b####-mix-. Empty list = vanilla +# upstream build. on: schedule: @@ -150,47 +151,49 @@ jobs: fi printf '%s' "$BASE" | grep -qE '^b[0-9]+$' || { echo "refusing non-release tag '$BASE'" >&2; exit 1; } - # Resolve the PR mix set (scripts/unsloth/pr-set.json): upstream PR - # commit urls. Pins are mandatory -- only an exact, reviewed commit + # Resolve the PR mix set (scripts/unsloth/pr-set.json): PR commit + # urls from ggml-org/llama.cpp or unslothai/llama.cpp (no other + # repos). Pins are mandatory -- only an exact, reviewed commit # is ever built, so a PR author pushing more commits cannot change # what the nightly ships. Non-open PRs are skipped, so merged or - # abandoned entries rot away harmlessly instead of breaking the - # nightly. unsloth-pr-set-lint.yml runs the same checks on every + # abandoned entries rot away instead of breaking the nightly (but a + # merged unslothai pin drops out of the build entirely; see the + # pr-set.json _doc). unsloth-pr-set-lint.yml runs the same checks on every # push that edits the file, but that is only a tripwire -- a red # lint does not stop the schedule, so the gate must live here. jq -e '.prs | type == "array" and all(.[]; type == "string")' scripts/unsloth/pr-set.json >/dev/null \ || { echo "scripts/unsloth/pr-set.json: .prs must be an array of PR url strings" >&2; exit 1; } PRS='[]' - URL_RE='^https://github\.com/ggml-org/llama\.cpp/pull/([0-9]+)/commits/([0-9a-f]{40})/?$' + URL_RE='^https://github\.com/(ggml-org|unslothai)/llama\.cpp/pull/([0-9]+)/commits/([0-9a-f]{40})/?$' for url in $(jq -r '.prs[]' scripts/unsloth/pr-set.json); do - [[ "$url" =~ $URL_RE ]] || { echo "refusing malformed PR url '$url' (expected https://github.com/ggml-org/llama.cpp/pull//commits/<40-hex-sha>)" >&2; exit 1; } - NUM="${BASH_REMATCH[1]}"; SHA="${BASH_REMATCH[2]}" - # plain assignment first so a gh failure aborts the run (set -e) - # instead of being swallowed by a later read's exit status. Title can - # contain spaces, so it can't ride a space-delimited read -- pull each - # field out on its own. - PR_JSON="$(gh api "repos/ggml-org/llama.cpp/pulls/${NUM}")" + [[ "$url" =~ $URL_RE ]] || { echo "refusing malformed PR url '$url' (expected https://github.com/{ggml-org,unslothai}/llama.cpp/pull//commits/<40-hex-sha>)" >&2; exit 1; } + SRC="${BASH_REMATCH[1]}/llama.cpp"; NUM="${BASH_REMATCH[2]}"; SHA="${BASH_REMATCH[3]}" + # Abort naming the entry on a gh failure (typo'd numbers 404). + # Title can contain spaces, so it can't ride a space-delimited + # read -- pull each field out on its own. + PR_JSON="$(gh api "repos/${SRC}/pulls/${NUM}")" \ + || { echo "refusing ${SRC}#${NUM}: could not fetch PR metadata (nonexistent PR number in '$url', or a transient API failure); fix the pin in scripts/unsloth/pr-set.json or retry" >&2; exit 1; } STATE="$(jq -r '.state' <<<"$PR_JSON")" HEAD="$(jq -r '.head.sha' <<<"$PR_JSON")" N_COMMITS="$(jq -r '.commits' <<<"$PR_JSON")" TITLE="$(jq -r '.title' <<<"$PR_JSON")" if [ "$STATE" != "open" ]; then - echo "skipping PR #${NUM} (${STATE}): $url" + echo "skipping ${SRC}#${NUM} (${STATE}): $url" continue fi - # A pin pasted from the wrong PR would build arbitrary upstream - # code while the manifest blames PR #; require the pinned - # commit to be a commit of that PR. The commits listing is capped - # at 250 by the API; past that, skip rather than false-fail. + # A pin pasted from the wrong PR would build arbitrary code while + # the manifest blames PR #; require the pinned commit to be + # a commit of that PR. The commits listing is capped at 250 by + # the API; past that, skip rather than false-fail. if [ "$N_COMMITS" -gt 250 ]; then - echo "note: PR #${NUM} has ${N_COMMITS} commits (over the API listing cap); skipping pin membership check" - elif ! gh api "repos/ggml-org/llama.cpp/pulls/${NUM}/commits" --paginate --jq '.[].sha' | grep -qx "$SHA"; then - echo "refusing PR #${NUM}: pinned commit ${SHA} is not a commit of that PR (wrong paste, or force-pushed away); fix the pin in scripts/unsloth/pr-set.json" >&2 + echo "note: ${SRC}#${NUM} has ${N_COMMITS} commits (over the API listing cap); skipping pin membership check" + elif ! gh api "repos/${SRC}/pulls/${NUM}/commits" --paginate --jq '.[].sha' | grep -qx "$SHA"; then + echo "refusing ${SRC}#${NUM}: pinned commit ${SHA} is not a commit of that PR (wrong paste, or force-pushed away); fix the pin in scripts/unsloth/pr-set.json" >&2 exit 1 fi - [ "$SHA" = "$HEAD" ] || echo "note: PR #${NUM} is pinned to ${SHA} but its head has moved to ${HEAD}" - echo "including PR #${NUM} @ ${SHA}" - PRS="$(jq -c --arg n "$NUM" --arg s "$SHA" --arg u "$url" --arg t "$TITLE" '. + [{number: ($n|tonumber), sha: $s, url: $u, title: $t}]' <<<"$PRS")" + [ "$SHA" = "$HEAD" ] || echo "note: ${SRC}#${NUM} is pinned to ${SHA} but its head has moved to ${HEAD}" + echo "including ${SRC}#${NUM} @ ${SHA}" + PRS="$(jq -c --arg r "$SRC" --arg n "$NUM" --arg s "$SHA" --arg u "$url" --arg t "$TITLE" '. + [{repo: $r, number: ($n|tonumber), sha: $s, url: $u, title: $t}]' <<<"$PRS")" done # Decide the tag and source repo first; the source tree is built @@ -201,12 +204,14 @@ jobs: TAG="$BASE" REPO='ggml-org/llama.cpp' else - # The synthetic tag embeds a hash of the pinned SHAs in listed order - # (merge order can matter for conflicts), so a pin update or a reorder - # yields a new tag and build, while the "exists" check below still - # skips rebuilding an already-published set. The merged tree exists - # only as this repo's release assets, never upstream. - SETHASH="$(jq -r 'map("\(.number):\(.sha)") | join("\n")' <<<"$PRS" | sha256sum | cut -c1-7)" + # The synthetic tag embeds a hash of the pinned repo#number:sha + # triples in listed order (merge order can matter for conflicts), so + # a pin update or a reorder yields a new tag and build, while the + # "exists" check below still skips rebuilding an already-published + # set. The repo is part of the key so PR #n in the two repos can't + # hash to the same set. The merged tree exists only as this repo's + # release assets, never upstream. + SETHASH="$(jq -r 'map("\(.repo)#\(.number):\(.sha)") | join("\n")' <<<"$PRS" | sha256sum | cut -c1-7)" TAG="${BASE}-mix-${SETHASH}" REPO="$GITHUB_REPOSITORY" fi @@ -239,14 +244,16 @@ jobs: fi git fetch -q --no-tags upstream "refs/tags/${BASE}:refs/tags/${BASE}" git checkout -q --detach "refs/tags/${BASE}" - for pair in $(jq -r '.[] | "\(.number):\(.sha)"' <<<"$PRS"); do - NUM="${pair%%:*}"; SHA="${pair##*:}" - git fetch -q --no-tags upstream "$SHA" \ - || { echo "could not fetch commit ${SHA} for PR #${NUM}; it may have been pruned after a force-push -- update or remove the pin in scripts/unsloth/pr-set.json" >&2; exit 1; } + # Each pin is fetched from the repo its PR lives in (repo/number/ + # sha are machine fields, so the space-delimited read is safe; + # titles stay out of this loop). + while read -r SRC NUM SHA; do + git fetch -q --no-tags "https://github.com/${SRC}.git" "$SHA" \ + || { echo "could not fetch commit ${SHA} for ${SRC}#${NUM}; it may have been pruned after a force-push -- update or remove the pin in scripts/unsloth/pr-set.json" >&2; exit 1; } git -c user.name='github-actions[bot]' -c user.email='41898282+github-actions[bot]@users.noreply.github.com' \ - merge --no-ff --no-edit -m "Merge ggml-org/llama.cpp#${NUM} @ ${SHA}" "$SHA" \ - || { echo "PR #${NUM} (${SHA}) does not merge cleanly onto ${BASE} + the PRs listed before it; reorder or drop it in scripts/unsloth/pr-set.json" >&2; exit 1; } - done + merge --no-ff --no-edit -m "Merge ${SRC}#${NUM} @ ${SHA}" "$SHA" \ + || { echo "${SRC}#${NUM} (${SHA}) does not merge cleanly onto ${BASE} + the PRs listed before it; reorder or drop it in scripts/unsloth/pr-set.json" >&2; exit 1; } + done < <(jq -r '.[] | "\(.repo) \(.number) \(.sha)"' <<<"$PRS") else # Plain build: only the base tree is needed. Shallow is fine -- the # build number is baked below, so no git history is needed at build time. @@ -508,7 +515,7 @@ jobs: cp "dist/llama.cpp-source-${TAG}.tar.gz" "dist/llama.cpp-source-commit-${SHA}.tar.gz" - name: Generate manifest + sha256 index - # prs carries upstream PR titles (arbitrary text), so pass it through env + # prs carries PR titles (arbitrary text), so pass it through env # rather than inlining it: a title with a quote would otherwise break out # of the shell command. Same handling in the publish step below. env: @@ -578,7 +585,7 @@ jobs: - name: Publish GitHub release if: ${{ (github.event_name == 'schedule' || inputs.publish) && needs.resolve.outputs.exists != 'true' }} - # prs carries upstream PR titles (arbitrary text), so pass it through env + # prs carries PR titles (arbitrary text), so pass it through env # rather than inlining it: a title with a quote would otherwise break out # of the shell command. env: @@ -592,17 +599,18 @@ jobs: # Link the base to the upstream release tag (always resolves). Each PR # line is " (#<n>, commit <sha>)": GitHub does not expand a bare # reference into the PR title inline (only on hover), so we bake the - # title in ourselves. #<n> links to the upstream PR (full URL, so it - # resolves to upstream no matter which repo hosts the release, and - # GitHub still attaches its hovercard), and <sha> links to that pin's - # commit-in-PR URL. We deliberately do not link the merged commit: in - # mix mode it is a throwaway merge made on the runner and never pushed - # anywhere, so any repo@sha URL for it 404s. + # title in ourselves. #<n> links to the PR in its home repo (full URL, + # so it resolves no matter which repo hosts the release, and GitHub + # still attaches its hovercard); non-upstream PRs spell the repo in + # the link text so an unslothai pin can't read as an upstream one. + # <sha> links to that pin's commit-in-PR URL. We deliberately do not + # link the merged commit: in mix mode it is a throwaway merge made on + # the runner and never pushed anywhere, so any repo@sha URL for it 404s. NOTES="Automated Unsloth llama.cpp CUDA + ROCm + Vulkan + macOS + CPU prebuild for upstream [${BASE}](https://github.com/ggml-org/llama.cpp/releases/tag/${BASE})" if [ "$(jq length <<<"$PRS")" = 0 ]; then NOTES="${NOTES}." else - PR_LIST="$(jq -r 'map("- \(.title) ([#\(.number)](https://github.com/ggml-org/llama.cpp/pull/\(.number)), commit [\(.sha[0:7])](\(.url)))") | join("\n")' <<<"$PRS")" + PR_LIST="$(jq -r 'map("- \(.title) ([\(if .repo == "ggml-org/llama.cpp" then "" else .repo end)#\(.number)](https://github.com/\(.repo)/pull/\(.number)), commit [\(.sha[0:7])](\(.url)))") | join("\n")' <<<"$PRS")" NOTES="$(printf '%s, merged with:\n\n%s' "$NOTES" "$PR_LIST")" fi diff --git a/scripts/unsloth/assemble_metadata.py b/scripts/unsloth/assemble_metadata.py index 2e92b2dac371..836a8dbbc202 100644 --- a/scripts/unsloth/assemble_metadata.py +++ b/scripts/unsloth/assemble_metadata.py @@ -293,7 +293,7 @@ def main() -> int: ap.add_argument("--base-tag", default=None, help="upstream release tag the build is based on; defaults to --tag (differs for mix builds)") ap.add_argument("--pr-set", default="[]", - help='JSON array of merged upstream PRs: [{"number":..,"sha":..,"url":..},..]') + help='JSON array of merged PRs: [{"repo":..,"number":..,"sha":..,"url":..,"title":..},..]') ap.add_argument("--commit", required=True) ap.add_argument("--dist", required=True, type=Path, help="dir holding the built app-*.tar.gz bundles") ap.add_argument("--out", required=True, type=Path, help="dir to write the two JSON sidecars into") @@ -476,7 +476,7 @@ def source_digest(name: str, url: str) -> str: sha_artifacts[name] = base_entry(kind, source_repo, digest) # 4) manifest, then hash it into the index. Both sidecars share the same - # source-description header; merged_prs records the exact PR head SHAs + # source-description header; merged_prs records the exact pinned PR SHAs # a mix build compiled (empty for vanilla builds). common = { "schema_version": 1, diff --git a/scripts/unsloth/pr-set.json b/scripts/unsloth/pr-set.json index 50ed1344ea1c..4e2042757520 100644 --- a/scripts/unsloth/pr-set.json +++ b/scripts/unsloth/pr-set.json @@ -1,10 +1,16 @@ { "_doc": [ - "ggml-org/llama.cpp PRs to merge into the nightly prebuilds. Each entry pins an exact", - "commit -- copy the url of the commit you reviewed from the PR's commits tab:", + "ggml-org/llama.cpp or unslothai/llama.cpp PRs to merge into the nightly prebuilds (no", + "other repos). Each entry pins an exact commit -- copy the url of the commit you", + "reviewed from the PR's commits tab:", " https://github.com/ggml-org/llama.cpp/pull/15926/commits/59a3d0cb8f611aa3110ecea3d0afd16b1b18ee06", "Only that commit is built, even if the author keeps pushing; update the pin to take newer code.", - "Closed or merged PRs are skipped automatically, and an empty list is a plain upstream build." + "Closed or merged PRs are skipped automatically, and an empty list is a plain upstream build.", + "An unslothai PR branched off this fork's master merges that ancestry in too (workflow/", + "script files, upstream commits synced past the base tag), not just its own diff.", + "Careful: the build tree is the upstream tag + pins, never fork master, so merging an", + "unslothai PR into master DROPS it from the nightly; keep the pin open until the change", + "lands upstream or is no longer needed." ], "prs": [ "https://github.com/ggml-org/llama.cpp/pull/24423/commits/1d2faac2d8248c3d5df999712c42fe0fc72e7f07", From 5eed57e071a0a759f1cb9f47ded66f4765d3b58c Mon Sep 17 00:00:00 2001 From: oobabooga <112222186+oobabooga@users.noreply.github.com> Date: Tue, 7 Jul 2026 15:38:50 -0300 Subject: [PATCH 2/4] Trim the pr-set.json doc --- scripts/unsloth/pr-set.json | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/scripts/unsloth/pr-set.json b/scripts/unsloth/pr-set.json index 4e2042757520..7fcda1f8587b 100644 --- a/scripts/unsloth/pr-set.json +++ b/scripts/unsloth/pr-set.json @@ -1,16 +1,12 @@ { "_doc": [ - "ggml-org/llama.cpp or unslothai/llama.cpp PRs to merge into the nightly prebuilds (no", - "other repos). Each entry pins an exact commit -- copy the url of the commit you", - "reviewed from the PR's commits tab:", + "ggml-org/llama.cpp or unslothai/llama.cpp PRs to merge into the nightly prebuilds. Each entry", + "pins an exact commit -- copy the url of the commit you reviewed from the PR's commits tab:", " https://github.com/ggml-org/llama.cpp/pull/15926/commits/59a3d0cb8f611aa3110ecea3d0afd16b1b18ee06", "Only that commit is built, even if the author keeps pushing; update the pin to take newer code.", "Closed or merged PRs are skipped automatically, and an empty list is a plain upstream build.", - "An unslothai PR branched off this fork's master merges that ancestry in too (workflow/", - "script files, upstream commits synced past the base tag), not just its own diff.", - "Careful: the build tree is the upstream tag + pins, never fork master, so merging an", - "unslothai PR into master DROPS it from the nightly; keep the pin open until the change", - "lands upstream or is no longer needed." + "Merging an unslothai PR into fork master drops it from the nightly (the tree is the upstream", + "tag + pins), so keep its pin open until the change lands upstream." ], "prs": [ "https://github.com/ggml-org/llama.cpp/pull/24423/commits/1d2faac2d8248c3d5df999712c42fe0fc72e7f07", From 97f2c2a2f03635111b035770f9e2e57a299399e1 Mon Sep 17 00:00:00 2001 From: oobabooga <112222186+oobabooga@users.noreply.github.com> Date: Tue, 7 Jul 2026 15:47:50 -0300 Subject: [PATCH 3/4] Pin unslothai#16 to exercise the fork-pin path (throwaway, CI only) --- scripts/unsloth/pr-set.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/unsloth/pr-set.json b/scripts/unsloth/pr-set.json index 7fcda1f8587b..19498e3cffa2 100644 --- a/scripts/unsloth/pr-set.json +++ b/scripts/unsloth/pr-set.json @@ -11,6 +11,7 @@ "prs": [ "https://github.com/ggml-org/llama.cpp/pull/24423/commits/1d2faac2d8248c3d5df999712c42fe0fc72e7f07", "https://github.com/ggml-org/llama.cpp/pull/24523/commits/00f95bd2d59ed2d6408fa3777b43ff5bd5fdc79d", - "https://github.com/ggml-org/llama.cpp/pull/25402/commits/15f732183bc1029df4b41ae2118364c3ccb6c162" + "https://github.com/ggml-org/llama.cpp/pull/25402/commits/15f732183bc1029df4b41ae2118364c3ccb6c162", + "https://github.com/unslothai/llama.cpp/pull/16/commits/e617ca3a4ddcbef48ef0d5cad8e5c87e39a97ebe" ] } From dd613160715cceba64ba4256b3ff021e29070f0a Mon Sep 17 00:00:00 2001 From: oobabooga <112222186+oobabooga@users.noreply.github.com> Date: Tue, 7 Jul 2026 15:47:50 -0300 Subject: [PATCH 4/4] Throwaway: run the resolve mix logic on CI (do not merge) --- .../workflows/throwaway-resolve-mix-test.yml | 297 ++++++++++++++++++ 1 file changed, 297 insertions(+) create mode 100644 .github/workflows/throwaway-resolve-mix-test.yml diff --git a/.github/workflows/throwaway-resolve-mix-test.yml b/.github/workflows/throwaway-resolve-mix-test.yml new file mode 100644 index 000000000000..cd7380270bf6 --- /dev/null +++ b/.github/workflows/throwaway-resolve-mix-test.yml @@ -0,0 +1,297 @@ +# Throwaway CI-only workflow: runs the resolve job's script from this branch's +# unsloth-prebuilt.yml byte-for-byte (single deviation: base tag pinned to b9902) +# to exercise the mixed ggml-org + unslothai pin set on a runner. Do not merge. +name: 'Throwaway: resolve mix test (do not merge)' +'on': + push: + branches: + - pr-set-unslothai-ci +permissions: + contents: read +jobs: + resolve-test: + runs-on: ubuntu-22.04 + env: + GH_TOKEN: ${{ github.token }} + steps: + - uses: actions/checkout@v6 + - id: r + name: Resolve (verbatim from unsloth-prebuilt.yml, base pinned to b9902) + run: | + set -euo pipefail + REQ='b9902' # throwaway: pinned base (orig: github.event.inputs.tag || latest) + ONLY='${{ github.event.inputs.only_profile || 'all' }}' + GFX_DEFAULT='gfx1151,gfx1150,gfx120X,gfx110X,gfx103X,gfx90a,gfx908' + GFX='${{ github.event.inputs.gfx_target }}'; GFX="${GFX:-$GFX_DEFAULT}" + OS_LIST='${{ github.event.inputs.operating_systems || 'windows,ubuntu' }}' + AGE_H='${{ github.event.inputs.min_age_hours }}' + + # publish does delete-then-create on the whole release, and the installer + # treats the published manifest as the authoritative bundle set: a partial + # build drops bundles and silently downgrades uncovered hosts to slow + # source builds. Refuse to publish unless the full default set is built; + # publish=false test runs may use subsets. + if [ "${{ github.event_name }}" = "schedule" ] || [ "${{ inputs.publish }}" = "true" ]; then + [ "$ONLY" = "all" ] || { echo "refusing to publish only_profile=$ONLY: a partial CUDA set clobbers manifest coverage. Use only_profile=all to publish, or publish=false for an artifact-only test." >&2; exit 1; } + [ "$GFX" = "$GFX_DEFAULT" ] || { echo "refusing to publish gfx_target='$GFX': publish requires the full default set ($GFX_DEFAULT); use publish=false for a subset test." >&2; exit 1; } + case "$OS_LIST" in + *windows*ubuntu*|*ubuntu*windows*) : ;; + *) echo "refusing to publish operating_systems='$OS_LIST': both windows and ubuntu ROCm bundles are required. Use publish=false for a subset test." >&2; exit 1 ;; + esac + fi + [ -n "$AGE_H" ] || AGE_H="${UNSLOTH_LLAMA_MIN_RELEASE_AGE_HOURS:-6}" + if [ "$REQ" = "latest" ]; then + # Newest published (non-draft, non-prerelease) release that has been + # public for >= AGE_H hours -- the supply-chain aging window. GitHub + # does not guarantee the list order, so pick the max by published_at + # explicitly rather than trusting `first`. + CUTOFF="$(date -u -d "-${AGE_H} hours" +%s)" + BASE="$(gh api 'repos/ggml-org/llama.cpp/releases?per_page=100' \ + --jq "[.[] | select(.draft==false and .prerelease==false) | select((.published_at|fromdateiso8601) <= ${CUTOFF})] | max_by(.published_at|fromdateiso8601) | .tag_name")" + if [ -z "$BASE" ] || [ "$BASE" = "null" ]; then + echo "refusing: no ggml-org release older than ${AGE_H}h in the last 100 releases" >&2 + exit 1 + fi + echo "selected $BASE (aged >= ${AGE_H}h)" + else + BASE="$REQ" # explicit b#### override skips the aging filter + fi + printf '%s' "$BASE" | grep -qE '^b[0-9]+$' || { echo "refusing non-release tag '$BASE'" >&2; exit 1; } + + # Resolve the PR mix set (scripts/unsloth/pr-set.json): PR commit + # urls from ggml-org/llama.cpp or unslothai/llama.cpp (no other + # repos). Pins are mandatory -- only an exact, reviewed commit + # is ever built, so a PR author pushing more commits cannot change + # what the nightly ships. Non-open PRs are skipped, so merged or + # abandoned entries rot away instead of breaking the nightly (but a + # merged unslothai pin drops out of the build entirely; see the + # pr-set.json _doc). unsloth-pr-set-lint.yml runs the same checks on every + # push that edits the file, but that is only a tripwire -- a red + # lint does not stop the schedule, so the gate must live here. + jq -e '.prs | type == "array" and all(.[]; type == "string")' scripts/unsloth/pr-set.json >/dev/null \ + || { echo "scripts/unsloth/pr-set.json: .prs must be an array of PR url strings" >&2; exit 1; } + PRS='[]' + URL_RE='^https://github\.com/(ggml-org|unslothai)/llama\.cpp/pull/([0-9]+)/commits/([0-9a-f]{40})/?$' + for url in $(jq -r '.prs[]' scripts/unsloth/pr-set.json); do + [[ "$url" =~ $URL_RE ]] || { echo "refusing malformed PR url '$url' (expected https://github.com/{ggml-org,unslothai}/llama.cpp/pull/<n>/commits/<40-hex-sha>)" >&2; exit 1; } + SRC="${BASH_REMATCH[1]}/llama.cpp"; NUM="${BASH_REMATCH[2]}"; SHA="${BASH_REMATCH[3]}" + # Abort naming the entry on a gh failure (typo'd numbers 404). + # Title can contain spaces, so it can't ride a space-delimited + # read -- pull each field out on its own. + PR_JSON="$(gh api "repos/${SRC}/pulls/${NUM}")" \ + || { echo "refusing ${SRC}#${NUM}: could not fetch PR metadata (nonexistent PR number in '$url', or a transient API failure); fix the pin in scripts/unsloth/pr-set.json or retry" >&2; exit 1; } + STATE="$(jq -r '.state' <<<"$PR_JSON")" + HEAD="$(jq -r '.head.sha' <<<"$PR_JSON")" + N_COMMITS="$(jq -r '.commits' <<<"$PR_JSON")" + TITLE="$(jq -r '.title' <<<"$PR_JSON")" + if [ "$STATE" != "open" ]; then + echo "skipping ${SRC}#${NUM} (${STATE}): $url" + continue + fi + # A pin pasted from the wrong PR would build arbitrary code while + # the manifest blames PR #<num>; require the pinned commit to be + # a commit of that PR. The commits listing is capped at 250 by + # the API; past that, skip rather than false-fail. + if [ "$N_COMMITS" -gt 250 ]; then + echo "note: ${SRC}#${NUM} has ${N_COMMITS} commits (over the API listing cap); skipping pin membership check" + elif ! gh api "repos/${SRC}/pulls/${NUM}/commits" --paginate --jq '.[].sha' | grep -qx "$SHA"; then + echo "refusing ${SRC}#${NUM}: pinned commit ${SHA} is not a commit of that PR (wrong paste, or force-pushed away); fix the pin in scripts/unsloth/pr-set.json" >&2 + exit 1 + fi + [ "$SHA" = "$HEAD" ] || echo "note: ${SRC}#${NUM} is pinned to ${SHA} but its head has moved to ${HEAD}" + echo "including ${SRC}#${NUM} @ ${SHA}" + PRS="$(jq -c --arg r "$SRC" --arg n "$NUM" --arg s "$SHA" --arg u "$url" --arg t "$TITLE" '. + [{repo: $r, number: ($n|tonumber), sha: $s, url: $u, title: $t}]' <<<"$PRS")" + done + + # Decide the tag and source repo first; the source tree is built + # further down, only if this release isn't already published. + if [ "$(jq length <<<"$PRS")" = 0 ]; then + # Plain build: pristine upstream base, no merge. REPO stays the real + # upstream repo even though we ship our own stamped tarball. + TAG="$BASE" + REPO='ggml-org/llama.cpp' + else + # The synthetic tag embeds a hash of the pinned repo#number:sha + # triples in listed order (merge order can matter for conflicts), so + # a pin update or a reorder yields a new tag and build, while the + # "exists" check below still skips rebuilding an already-published + # set. The repo is part of the key so PR #n in the two repos can't + # hash to the same set. The merged tree exists only as this repo's + # release assets, never upstream. + SETHASH="$(jq -r 'map("\(.repo)#\(.number):\(.sha)") | join("\n")' <<<"$PRS" | sha256sum | cut -c1-7)" + TAG="${BASE}-mix-${SETHASH}" + REPO="$GITHUB_REPOSITORY" + fi + SRC_ARTIFACT="app-source-${TAG}" + COMMIT="" + + # Only PUBLISHED releases count as "exists"; a leftover draft (from + # a failed prior publish) should not block today's rebuild. + EXISTS=false + if [ "$(gh release view "$TAG" --repo "$GITHUB_REPOSITORY" --json isDraft --jq .isDraft 2>/dev/null || true)" = "false" ]; then + EXISTS=true + fi + + # Build the source tree every child compiles, ONCE, here -- but only + # when something will actually be built. On a scheduled no-op (the aged + # "latest" was already published) skip the whole checkout/merge/tar + + # upload; a manual dispatch always rebuilds. Check out the upstream base, + # merge any pinned PRs (mix builds), then bake the build number/commit + # and the Unsloth fingerprint into cmake/build-info.cmake. The + # result is uploaded as the app-source-* artifact every child extracts -- + # one identical tree everywhere, no child clones, fingerprint patch in one + # place. Nothing is pushed (GITHUB_TOKEN may never push commits touching + # .github/workflows, which upstream history routinely does). + if [ "$EXISTS" != "true" ] || [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + git remote add upstream https://github.com/ggml-org/llama.cpp.git + if [ "$(jq length <<<"$PRS")" != 0 ]; then + # Merges need a merge-base, so unshallow first. + if [ "$(git rev-parse --is-shallow-repository)" = "true" ]; then + git fetch -q --unshallow --no-tags origin + fi + git fetch -q --no-tags upstream "refs/tags/${BASE}:refs/tags/${BASE}" + git checkout -q --detach "refs/tags/${BASE}" + # Each pin is fetched from the repo its PR lives in (repo/number/ + # sha are machine fields, so the space-delimited read is safe; + # titles stay out of this loop). + while read -r SRC NUM SHA; do + git fetch -q --no-tags "https://github.com/${SRC}.git" "$SHA" \ + || { echo "could not fetch commit ${SHA} for ${SRC}#${NUM}; it may have been pruned after a force-push -- update or remove the pin in scripts/unsloth/pr-set.json" >&2; exit 1; } + git -c user.name='github-actions[bot]' -c user.email='41898282+github-actions[bot]@users.noreply.github.com' \ + merge --no-ff --no-edit -m "Merge ${SRC}#${NUM} @ ${SHA}" "$SHA" \ + || { echo "${SRC}#${NUM} (${SHA}) does not merge cleanly onto ${BASE} + the PRs listed before it; reorder or drop it in scripts/unsloth/pr-set.json" >&2; exit 1; } + done < <(jq -r '.[] | "\(.repo) \(.number) \(.sha)"' <<<"$PRS") + else + # Plain build: only the base tree is needed. Shallow is fine -- the + # build number is baked below, so no git history is needed at build time. + git fetch -q --depth 1 --no-tags upstream "refs/tags/${BASE}:refs/tags/${BASE}" + git checkout -q --detach "refs/tags/${BASE}" + fi + COMMIT="$(git rev-parse HEAD)" + + # The tarball ships without .git, where cmake/build-info.cmake falls + # back to its defaults; bake real values into those defaults so + # llama-server --version doesn't report b0 (unknown). The build number + # stays the BASE's b#### number (the upstream tag's commit count), so + # anything comparing versions against upstream releases keeps working; + # BUILD_COMMIT identifies the (possibly merged) head. + COUNT="${BASE#b}" + SHORT="$(git rev-parse --short HEAD)" + sed -i "s/^set(BUILD_NUMBER 0)$/set(BUILD_NUMBER ${COUNT})/" cmake/build-info.cmake + sed -i "s/^set(BUILD_COMMIT \"unknown\")$/set(BUILD_COMMIT \"${SHORT}\")/" cmake/build-info.cmake + grep -q "set(BUILD_NUMBER ${COUNT})" cmake/build-info.cmake \ + && grep -q "set(BUILD_COMMIT \"${SHORT}\")" cmake/build-info.cmake \ + || { echo "cmake/build-info.cmake no longer has the expected fallback lines; cannot bake the build number into the source artifact" >&2; exit 1; } + + # Unsloth fingerprint: fold "Compiled by the Unsloth team" + # into BUILD_TARGET, which cmake bakes into LLAMA_BUILD_TARGET, so it + # prints on llama-server --version ("built with ... for ...") and shows + # up in `strings` on every binary that links common. Append so it wraps + # whatever BUILD_TARGET the build computes. Keep this string byte-identical + # to MARK in the assemble job's verify gate, which re-checks it landed. + grep -q 'set(BUILD_TARGET' cmake/build-info.cmake \ + || { echo "cmake/build-info.cmake has no BUILD_TARGET line to stamp" >&2; exit 1; } + printf '\n# Unsloth fingerprint: shows in --version and strings.\nset(BUILD_TARGET "${BUILD_TARGET} (Compiled by the Unsloth team)")\n' >> cmake/build-info.cmake + grep -q 'Compiled by the Unsloth team' cmake/build-info.cmake \ + || { echo "failed to stamp the Unsloth fingerprint into cmake/build-info.cmake" >&2; exit 1; } + + tar -czf "${RUNNER_TEMP}/llama.cpp-source-${TAG}.tar.gz" --exclude-vcs --transform "s,^\.,llama.cpp-${TAG}," . + echo "prepared ${TAG} (${COMMIT}, build ${COUNT})" + else + echo "release ${TAG} already published; skipping source prep (nothing to build)" + fi + + # ubuntu-22.04 is x64 (glibc 2.35). arm64 only has ubuntu-24.04-arm + # available on GitHub-hosted runners (glibc 2.39). arm64 is cuda13-only + # (no cuda12 SBSA) and ships the single "portable" coverage class. + # cuda12 installs via Jimver; cuda13 is pinned to 13.3 (matching + # upstream) which Jimver lacks, so those install via NVIDIA redist in + # the build children -- on the same runners, so the glibc floor holds. + ALL='[ + {"profile":"cuda12-older", "arch":"x64", "runner":"ubuntu-22.04", "line":"cuda12","klass":"older", "rank":10,"cuda":"12.8.0","toolkit_line":"12.8","archs":"70 75 80 86 89"}, + {"profile":"cuda12-newer", "arch":"x64", "runner":"ubuntu-22.04", "line":"cuda12","klass":"newer", "rank":20,"cuda":"12.8.0","toolkit_line":"12.8","archs":"86 89 90 100 120"}, + {"profile":"cuda12-portable","arch":"x64", "runner":"ubuntu-22.04", "line":"cuda12","klass":"portable","rank":30,"cuda":"12.8.0","toolkit_line":"12.8","archs":"70 75 80 86 89 90 100 120"}, + {"profile":"cuda13-older", "arch":"x64", "runner":"ubuntu-22.04", "line":"cuda13","klass":"older", "rank":40,"cuda":"13.3","toolkit_line":"13.3","archs":"75 80 86 89"}, + {"profile":"cuda13-newer", "arch":"x64", "runner":"ubuntu-22.04", "line":"cuda13","klass":"newer", "rank":50,"cuda":"13.3","toolkit_line":"13.3","archs":"86 89 90 100 120"}, + {"profile":"cuda13-portable","arch":"x64", "runner":"ubuntu-22.04", "line":"cuda13","klass":"portable","rank":60,"cuda":"13.3","toolkit_line":"13.3","archs":"75 80 86 89 90 100 120"}, + {"profile":"cuda13-portable","arch":"arm64","runner":"ubuntu-24.04-arm","line":"cuda13","klass":"portable","rank":60,"cuda":"13.3","toolkit_line":"13.3","archs":"90 100 120 121"} + ]' + if [ "$ONLY" = "all" ]; then + CUDA_INCLUDE="$(echo "$ALL" | jq -c .)" + else + CUDA_INCLUDE="$(echo "$ALL" | jq -c --arg p "$ONLY" '[.[] | select(.profile==$p)]')" + fi + # CUDA Windows reuses the x64 profiles (same arch lists / CUDA + # versions), just on a Windows runner. arm64 has no CUDA Windows target. + WIN_CUDA_INCLUDE="$(echo "$CUDA_INCLUDE" | jq -c '[.[] | select(.arch=="x64") | .runner="windows-2022"]')" + [ -n "$GFX" ] || { echo "refusing empty gfx_target (would publish a CUDA-only release labeled CUDA + ROCm)" >&2; exit 1; } + ROCM_MATRIX="$(jq -cn --arg g "$GFX" '{gfx_target: ($g | split(",") | map(gsub("^\\s+|\\s+$"; "")))}')" + + # macOS slices are static: two fixed runners with per-slice deployment + # targets. arm64 builds on macos-26 (newest Metal SDK; avoids the + # M5/A19 "error compiling source" the macos-14 SDK emits) yet pins + # minos 14.0 via deploy_target, so it still loads on macOS 14+. x64 + # pins 13.3 (matches upstream's Intel leg, covers 2017 Intel Macs + # capped at Ventura). + MACOS_INCLUDE='[ + {"build":"arm64","runner":"macos-26", "expect_arch":"arm64", "deploy_target":"14.0","defines":"-DGGML_METAL_USE_BF16=ON -DGGML_METAL_EMBED_LIBRARY=ON"}, + {"build":"x64", "runner":"macos-15-intel","expect_arch":"x86_64","deploy_target":"13.3","defines":"-DGGML_METAL=OFF"} + ]' + MACOS_INCLUDE="$(echo "$MACOS_INCLUDE" | jq -c .)" + + { + echo "tag=$TAG" + echo "repo=$REPO" + echo "base=$BASE" + echo "prs=$PRS" + echo "source_artifact=$SRC_ARTIFACT" + echo "commit=$COMMIT" + echo "exists=$EXISTS" + echo "cuda_matrix={\"include\":$CUDA_INCLUDE}" + echo "win_cuda_matrix={\"include\":$WIN_CUDA_INCLUDE}" + echo "rocm_matrix=$ROCM_MATRIX" + echo "macos_matrix={\"include\":$MACOS_INCLUDE}" + } >> "$GITHUB_OUTPUT" + echo "Resolved $REQ -> $TAG ($COMMIT); prs=$PRS; source_artifact=${SRC_ARTIFACT:-none}; release exists=$EXISTS; only=$ONLY; gfx=$GFX" + - name: Verify merged tree and outputs + env: + PRS_JSON: ${{ steps.r.outputs.prs }} + run: | + set -euo pipefail + TAG='${{ steps.r.outputs.tag }}' + echo "tag=$TAG" + grep -qE '^b9902-mix-[0-9a-f]{7}$' <<<"$TAG" + [ "$(jq length <<<"$PRS_JSON")" = 4 ] + [ "$(jq -r '.[3].repo' <<<"$PRS_JSON")" = "unslothai/llama.cpp" ] + git log --oneline --first-parent -5 + git log --format=%s --first-parent -4 > /tmp/subjects.txt + grep -qx 'Merge unslothai/llama.cpp#16 @ e617ca3a4ddcbef48ef0d5cad8e5c87e39a97ebe' /tmp/subjects.txt + grep -q 'Compiled by the Unsloth team' cmake/build-info.cmake + tar -xzOf "${RUNNER_TEMP}/llama.cpp-source-${TAG}.tar.gz" "llama.cpp-${TAG}/cmake/build-info.cmake" > /tmp/bi.cmake + grep -q 'Compiled by the Unsloth team' /tmp/bi.cmake + echo "all assertions passed" + - name: Render release notes (dry run) + env: + PRS_JSON: ${{ steps.r.outputs.prs }} + run: | + set -eu + PRS="$PRS_JSON" + BASE='${{ steps.r.outputs.base }}' + NOTES="Automated Unsloth llama.cpp CUDA + ROCm + Vulkan + macOS + CPU prebuild for upstream [${BASE}](https://github.com/ggml-org/llama.cpp/releases/tag/${BASE})" + if [ "$(jq length <<<"$PRS")" = 0 ]; then + NOTES="${NOTES}." + else + PR_LIST="$(jq -r 'map("- \(.title) ([\(if .repo == "ggml-org/llama.cpp" then "" else .repo end)#\(.number)](https://github.com/\(.repo)/pull/\(.number)), commit [\(.sha[0:7])](\(.url)))") | join("\n")' <<<"$PRS")" + NOTES="$(printf '%s, merged with:\n\n%s' "$NOTES" "$PR_LIST")" + fi + printf '%s\n' "$NOTES" > /tmp/notes.md + cat /tmp/notes.md + grep -qF '([unslothai/llama.cpp#16](https://github.com/unslothai/llama.cpp/pull/16)' /tmp/notes.md + grep -qF '([#24423](https://github.com/ggml-org/llama.cpp/pull/24423)' /tmp/notes.md + echo "notes render OK" + - uses: actions/upload-artifact@v6 + with: + name: ${{ steps.r.outputs.source_artifact }} + path: ${{ runner.temp }}/llama.cpp-source-${{ steps.r.outputs.tag }}.tar.gz + if-no-files-found: error + retention-days: 1