Skip to content

CI: build the prebuilts from this repository, not from a fetched tarball - #8

Merged
danielhanchen merged 4 commits into
masterfrom
ci-build-from-this-repo
Aug 9, 2026
Merged

CI: build the prebuilts from this repository, not from a fetched tarball#8
danielhanchen merged 4 commits into
masterfrom
ci-build-from-this-repo

Conversation

@danielhanchen

@danielhanchen danielhanchen commented Aug 8, 2026

Copy link
Copy Markdown
Member

The pipeline resolved an upstream release tag, fetched that tree, applied the patch set in patches/, and built the result. That made every published binary depend on a foreign repository at build time, and it left three fixes living as diffs that had to be kept applying to a tree we do not control.

All three are already commits here, merged as #2, #3 and #4. So build this checkout instead. patches/ is deleted; there is nothing left to re-apply, and the "every patch must be open upstream, delete on merge" bookkeeping goes with it.

Tag naming

The tag still names the upstream release the tree descends from, read from our own history rather than from an API, with the head sha as the -u suffix:

master-813-bfbef5b-u22e2879

Two details worth calling out.

Highest reachable release, not nearest. git describe answers "nearest", and on a merge-shaped history that is the wrong answer. This tree reaches master-813 through a merge 73 commits back, and master-811 on its own line 13 commits back, so describe names the build after 811 and understates what it actually contains:

$ git describe --tags --abbrev=0 --match 'master-[0-9]*-*' --exclude '*-u*' HEAD
master-811-b4e67d1
$ git tag -l --merged HEAD 'master-*' | grep -E '^master-[0-9]+-[0-9a-f]+$' | sort -t- -k2,2n | tail -1
master-813-bfbef5b

The -u suffix is now always present, which is what it should have been all along. Studio's installer treats a -u tag as mirror-only and goes straight to this repository's releases, instead of first trying an upstream download that is guaranteed to 404. A build of ours is never a stock upstream build, so it should never wear a bare upstream tag.

What is unchanged

Supply-chain aging is kept, re-pointed: it now guards the age of the upstream release the tree descends from, rather than the moment a release appeared. Our own commits on top are reviewed here, so they are not what the delay is for.

assemble_metadata.py keeps its --patches flag, always empty, so existing manifest readers do not have to change. sd-prebuilt-manifest.json records the commit, which is now the precise answer to "what is in this binary".

Asset names, the coverage gate, the fingerprint gate and the atomic draft-then-publish are all untouched.

Follow-up

The next scheduled run publishes a new tag, so studio/install_sd_cpp_prebuilt.py's DEFAULT_TAG in unslothai/unsloth needs bumping to it once that release exists. The current pin resolves fine until then.


Added: a Linux CUDA prebuilt

This pipeline was written on the premise that a GPU host runs diffusers/torch, so the native engine only needed CPU and Apple builds. MiniMax-H3 falsifies that premise. Its diffusers path wants roughly 68.5 GB of VRAM, so every consumer card falls back to the GGUF engine, and on Linux that engine had no accelerated build to fall back to.

Measured on one box, same tag, same model:

build resolution per step one default render
published Linux CPU asset, 96 threads 320x192 65 s ~4.5 h
local -DSD_CUDA=ON 960x544 21.5 s ~11 min

Higher resolution and eleven minutes instead of four and a half hours.

The new build-linux-cuda job is continue-on-error and is deliberately not in the coverage gate, which still lists exactly the same five CPU/Apple assets. assemble collects bundles with the sd-*-bin-* pattern, so the CUDA asset is published when it built and simply absent when it did not. A broken CUDA toolchain cannot hold back the assets Studio falls back to.

Details worth review:

  • sm_75;80;86;89;90;100;120. 12.8 is the first toolkit that can emit sm_100 (B200) and sm_120 (RTX 50), and nothing older than Turing is a realistic host for a 20 GB video denoiser. The existing Windows CUDA leg in build.yml uses the same list plus 61 and 70.
  • The CUDA runtime ships in the bundle. A host with an NVIDIA driver does not necessarily have a CUDA runtime installed, and we must not lean on the copies torch keeps. libcudart.so.12, libcublas.so.12 and libcublasLt.so.12 are copied beside the binaries with an $ORIGIN rpath.
  • package_bundle.py had to change to allow that. It matched runtime libraries on Path.suffix, which reads .12 for libcudart.so.12, so the copied libraries would have been silently dropped from the zip and the binaries would have shipped with unsatisfiable DT_NEEDED entries. It now also matches the .so. infix, so a versioned soname ships under the exact name the linker asks for. Verified locally against a fixture directory.
  • The runner image ships around 25 GB free, which does not fit the toolkit beside a seven-architecture ggml-cuda build, so the job clears the preinstalled Android and .NET trees first.

No installer change is needed on the Studio side: resolve_release_asset already maps a Linux cuda accelerator onto a cuda asset token, and _LINUX_ACCEL_MARKERS already contains cuda.

The open questions are all things only a real run can answer: whether the toolkit plus the build fits in the freed disk, how long seven architectures take against the job timeout, and whether the bundled runtime actually resolves on a clean host. A dispatch run is in flight.

The pipeline resolved an upstream release tag, fetched that tree, applied
the patch set in patches/, and built the result. That made every
published binary depend on a foreign repository at build time and left
three fixes living as diffs that had to be kept applying to a tree we do
not control.

All three are already commits here, merged as PRs 2, 3 and 4. So build
this checkout. patches/ is deleted; there is nothing left to re-apply.

The tag still names the upstream release the tree descends from, read
from our own history rather than from an API, with the head sha as the
-u suffix:

    master-813-bfbef5b-u22e2879

Two details worth keeping.

HIGHEST reachable release, not nearest. git describe answers "nearest",
and on a merge-shaped history that is wrong: this tree reaches
master-813 through a merge 73 commits back and master-811 on its own
line 13 commits back, so describe names the build after 811 and
understates what it contains.

The -u suffix is now always present, which is what it should have been.
Studio's installer treats a -u tag as mirror-only and goes straight to
this repository's releases instead of trying an upstream download that
is guaranteed to 404. A build of ours is never a stock upstream build,
so it should never carry a bare upstream tag.

Supply-chain aging is kept and re-pointed: it now guards the age of the
upstream release the tree descends from rather than the moment a
release appeared. Our own commits on top are reviewed here, so they are
not what the delay is for.

assemble_metadata keeps its --patches flag, always empty, so existing
manifest readers do not have to change.
The prebuilts now build this tree, but that alone only ships what is
already merged. The llama.cpp pipeline exists to ship a reviewed mix:
an aged upstream base plus a set of pull requests pinned to exact
commits, merged at build time. This adds the same mechanism here.

scripts/unsloth/pr-set.json lists PRs to merge, each pinned to a
40-hex commit copied from the PR's commits tab. Only that commit is
built, so an author pushing more commits cannot change what the
nightly ships. Non-open required pins fail the build rather than
silently publishing without them, because dropping a pin changes the
tag and would ship a quietly different binary under a new name.
additive_merge.py, vendored from unslothai/llama.cpp, resolves the one
conflict shape that is mechanical (both sides only added, at a place
the merge base had nothing) and refuses to guess at anything else.

One deliberate difference from llama.cpp. There the base is a pristine
upstream release, so every Unsloth change has to stay pinned and open,
and merging one into fork master drops it from the nightly. Here the
base is our own tree, so a merged fix is simply in it and its pin is
deleted. That is why the three MiniMax-H3 fixes need no pins.

Only PRs in this repository may be pinned. To carry a fix that exists
as an upstream pull request, vendor it here as a PR first and pin
that. The build fetches from nowhere else.

The tag suffix absorbs the set: with no pins it is the head sha, with
pins it hashes the pinned number:sha pairs together with the head sha,
so a repin or a reorder yields a new tag and a rebuild while an
unchanged set still matches an existing release and skips. The
existing -u shape is kept rather than llama.cpp's -mix-, because
Studio's installer keys mirror-only resolution on it.

Verified locally against this tree: the schema gate passes, an empty
set yields master-813-bfbef5b-u692a7c8, a one-pin set yields a
different suffix, and both match the installer's mirror-only pattern.
@danielhanchen

Copy link
Copy Markdown
Member Author

Second commit on this branch answers the question the first one only half answered.

The goal for these prebuilts is the one unslothai/llama.cpp already implements: take upstream, merge a reviewed set of pull requests, build that. The first commit did the "build our own tree" half. This adds the mix half.

scripts/unsloth/pr-set.json lists PRs to merge, each pinned to an exact 40-hex commit copied from the PR's commits tab:

{
  "prs": [
    "https://github.com/unslothai/stable-diffusion.cpp/pull/8/commits/<40-hex-sha>"
  ]
}

Only that commit is built, so an author pushing more commits cannot change what the nightly ships. The same guards as llama.cpp: a schema gate, a check that the pinned sha really is a commit of that PR (a wrong paste would build arbitrary code while the manifest blames a PR number), and a rule that a non-open required pin fails the build rather than silently publishing without it, since dropping a pin changes the tag and would ship a quietly different binary under a new name. The gate lives in resolve rather than only in a lint, because a red lint does not stop the schedule.

additive_merge.py is vendored from unslothai/llama.cpp unchanged. It resolves the one conflict shape that is mechanical, where both sides only added text at a spot the merge base had nothing, and refuses to guess at anything else.

One deliberate difference from llama.cpp

There the base is a pristine upstream release, so every Unsloth change has to stay pinned and open, and merging one into fork master silently drops it from the nightly. Here the base is our own tree, so a merged fix is simply in it and its pin is deleted. That is why the three MiniMax-H3 fixes need no pins at all: they are commits, not diffs to re-apply.

The other difference is cosmetic. llama.cpp tags a mix <base>-mix-<hash>; this keeps the existing -u<id> shape, because Studio's installer keys mirror-only resolution on -u. The id is the head sha with no pins, and a hash of the pinned number:sha pairs plus the head sha with pins, so a repin or a reorder yields a new tag and a rebuild while an unchanged set still matches an existing release and skips.

Only this repository's PRs may be pinned

To carry a fix that exists as an upstream pull request, vendor it here as a PR first and pin that. The build fetches from nowhere else.

Verified locally against this tree

schema gate                     pass
empty set    -> master-813-bfbef5b-u692a7c8
one-pin set  -> master-813-bfbef5b-ufcb0b5c
both match the installer's mirror-only pattern
resolve step parses under bash -n; additive_merge.py compiles

The CPU/Apple matrix rests on the assumption that a GPU host runs the
diffusers path instead. MiniMax-H3 breaks that assumption: its diffusers
path wants ~68.5 GB of VRAM, so every consumer card falls back to the
GGUF engine, and on Linux that engine had no accelerated build to fall
back to. Measured on one box, 65 s/step at 320x192 across 96 CPU
threads, against 21.5 s/step at 960x544 from a local CUDA build of the
same tag: four hours per clip versus eleven minutes.

The new leg is continue-on-error and is not in the coverage gate, which
still lists exactly the five CPU/Apple assets. assemble collects bundles
by the sd-*-bin-* pattern, so the CUDA asset is published when it built
and simply absent when it did not; a broken CUDA toolchain can never
hold back the assets Studio falls back to.

sm_75 through sm_120, which is the first toolkit able to emit sm_100 and
sm_120 and covers everything from Turing up. The CUDA runtime libraries
are copied in beside the binaries with an $ORIGIN rpath, because a host
with an NVIDIA driver does not necessarily have a CUDA runtime installed
and we must not lean on the copies torch keeps.

package_bundle matched runtime libraries on Path.suffix, which reads
".12" for libcudart.so.12 and dropped it. It now matches the ".so."
infix as well, so a versioned soname ships under the exact name
DT_NEEDED spells.
@danielhanchen
danielhanchen force-pushed the ci-build-from-this-repo branch from e0741d5 to 994faa9 Compare August 8, 2026 17:56
cuda-toolkit installs sub-packages as cuda-<name>-12-8, and cuBLAS does not use
that prefix, so apt could not find cuda-cublas-12-8 or cuda-cublas_dev-12-8 and
the leg died before it compiled anything. They belong in non-cuda-sub-packages
as libcublas and libcublas-dev. cudart-dev joins the list too, since cudart on
its own is the runtime and carries no headers to compile against.

The libcublas debs land in the system multiarch directory rather than under the
toolkit root, so the bundling step now searches both.
@danielhanchen

Copy link
Copy Markdown
Member Author

The CUDA leg is green and I verified the asset on a real GPU box rather than trusting the build.

Run: 31271264076 at master-813-bfbef5b-udec5bf4. All seven jobs succeeded, including assemble, so the CPU and Apple assets published alongside it exactly as before.

Downloaded sd-...-bin-Linux-Ubuntu-22.04-x86_64-cuda12.zip and unpacked it on a machine with an NVIDIA driver and no CUDA toolkit on the path:

LICENSE  UNSLOTH_BUILD.txt
libcublas.so.12    116388640
libcublasLt.so.12  751775824
libcudart.so.12       728800
sd-cli             361385720
sd-server          361905760

$ readelf -d sd-cli | grep -E 'RUNPATH|NEEDED'
  (NEEDED)   libcudart.so.12
  (NEEDED)   libcublas.so.12
  (NEEDED)   libcuda.so.1
  (RUNPATH)  $ORIGIN

libcuda.so.1 is the driver and is deliberately not bundled. Everything else resolves out of the bundle's own directory.

Running it:

ggml_cuda_init: found 1 CUDA devices (Total VRAM: 182633 MiB):
  Device 0: NVIDIA B200, compute capability 10.0, VMM: yes

Compute capability 10.0 is sm_100, so the architecture list reaches Blackwell in practice and not just in the cmake flags. That was the open question I could not answer without a run.

The three worries in the PR body are settled:

  • The toolkit plus a seven-architecture build fits after clearing the preinstalled Android and .NET trees.
  • Build time was acceptable; the leg finished alongside the others rather than becoming the long pole.
  • The bundled runtime resolves against a driver-only host.

Two follow-ups, neither blocking

Size. The bundle is 1.15 GiB compressed, 1.55 GiB unpacked. libcublasLt.so.12 is 752 MB of that and is not really avoidable if we ship the runtime. But sd-cli and sd-server are 361 MB each and are carrying near-identical copies of the same fat CUDA kernels. Building ggml as a shared library would let them share one copy and should take roughly 350 MB off. Worth doing before this becomes the asset most users download.

Version stamping. sd-cli --help prints version unknown, commit unknown. The bundle's UNSLOTH_BUILD.txt records the tag and commit correctly, so provenance is not lost, but anything that wants to ask the binary directly how old it is cannot. That matters for the staleness check on the consumer side, which currently has to infer it.

@danielhanchen
danielhanchen merged commit 13b9d92 into master Aug 9, 2026
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant