Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 21 additions & 17 deletions .github/workflows/unsloth-pr-set-lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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/<n>/commits/<40-hex-sha>)"
echo "::error file=$FILE::malformed entry '$url' (expected https://github.com/{ggml-org,unslothai}/llama.cpp/pull/<n>/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"
108 changes: 58 additions & 50 deletions .github/workflows/unsloth-prebuilt.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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-<hash>. 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-<hash>. Empty list = vanilla
# upstream build.

on:
schedule:
Expand Down Expand Up @@ -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/<n>/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/<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 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 #<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.
# 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: 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
Expand All @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -592,17 +599,18 @@ jobs:
# Link the base to the upstream release tag (always resolves). Each PR
# line is "<title> (#<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

Expand Down
4 changes: 2 additions & 2 deletions scripts/unsloth/assemble_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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,
Expand Down
8 changes: 5 additions & 3 deletions scripts/unsloth/pr-set.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
{
"_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. Each entry",
Comment thread
oobabooga marked this conversation as resolved.
"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.",
"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",
Expand Down
Loading