Skip to content

Commit 09b6bf6

Browse files
fix(studio): opt-in source-build GPU smoke validation (#7322)
* fix(studio): opt-in source-build GPU smoke validation (#5854) Gap 1 (empty CUDA arch -> CPU) already landed in #6481. Wire gap 2: after a GPU source build, optionally run the same staged llama-server smoke test as the prebuilt path, then CPU-fallback on failure. Gated by UNSLOTH_LLAMA_STAGED_VALIDATION (default off) to avoid Blackwell JIT stalls. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix(install): normalize staged validation env in setup.sh (#7322) Strip and lowercase UNSLOTH_LLAMA_STAGED_VALIDATION before the shell gate so values like True and surrounding whitespace match the Python staged_validation_enabled() helper. * Rebuild visual server after staged-validation CPU fallback (#5854) Mirror the primary source-build path by best-effort building llama-diffusion-gemma-visual-server after smoke-failure CPU fallback. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 8c975fc commit 09b6bf6

6 files changed

Lines changed: 334 additions & 4 deletions

File tree

.github/workflows/studio-backend-ci.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,7 @@ jobs:
228228
tests/sh/test_system_node_readonly.sh \
229229
tests/sh/test_nvcc_meets_llama_minimum.sh \
230230
tests/sh/test_resolve_cuda_archs.sh \
231+
tests/sh/test_staged_validation_enabled.sh \
231232
tests/sh/test_tauri_install_exit_order.sh \
232233
tests/sh/test_torch_constraint.sh \
233234
tests/sh/test_torch_flavor.sh \

studio/install_llama_prebuilt.py

Lines changed: 93 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -186,8 +186,24 @@ def env_int(
186186
# in validate_prebuilt_choice. Disabled for now: the llama-server GPU forward pass
187187
# JIT-compiles CUDA kernels on first load and stalls every install and update by
188188
# minutes on Blackwell (sm_100). The check and the source-build fallback it triggers
189-
# are kept intact -- set this to True to re-enable them.
189+
# are kept intact -- set this to True, or set UNSLOTH_LLAMA_STAGED_VALIDATION=1, to
190+
# re-enable them (#5854 gap 2).
190191
_RUN_STAGED_PREBUILT_VALIDATION = False
192+
193+
194+
def staged_validation_enabled() -> bool:
195+
"""True when the expensive llama-server GPU smoke test should run.
196+
197+
Default off (Blackwell CUDA JIT stalls installs). Opt in via the module
198+
constant or ``UNSLOTH_LLAMA_STAGED_VALIDATION`` (1/true/yes/on). Used by both
199+
the prebuilt path and setup.sh's source-build post-check (#5854).
200+
"""
201+
if _RUN_STAGED_PREBUILT_VALIDATION:
202+
return True
203+
raw = os.environ.get("UNSLOTH_LLAMA_STAGED_VALIDATION", "").strip().lower()
204+
return raw in ("1", "true", "yes", "on")
205+
206+
191207
INSTALL_LOCK_TIMEOUT_SECONDS = 300
192208
INSTALL_STAGING_ROOT_NAME = ".staging"
193209
GITHUB_AUTH_HOSTS = {"api.github.com", "github.com"}
@@ -5868,9 +5884,10 @@ def validate_prebuilt_choice(
58685884
# so they are always validated. For an approved bundle the sha256 manifest
58695885
# already proves integrity, so its runtime smoke test -- a cold CUDA-JIT pass
58705886
# costing minutes on Blackwell sm_100 -- is gated behind
5871-
# _RUN_STAGED_PREBUILT_VALIDATION, disabled for now. The check and the
5872-
# source-build fallback it triggers are kept intact; flip the flag to restore it.
5873-
if choice.expected_sha256 is None or _RUN_STAGED_PREBUILT_VALIDATION:
5887+
# staged_validation_enabled() (constant or UNSLOTH_LLAMA_STAGED_VALIDATION),
5888+
# disabled for now. The check and the source-build fallback it triggers are
5889+
# kept intact; flip the flag / env to restore it (#5854).
5890+
if choice.expected_sha256 is None or staged_validation_enabled():
58745891
validate_quantize(
58755892
quantize_path,
58765893
probe_path,
@@ -5891,6 +5908,49 @@ def validate_prebuilt_choice(
58915908
return server_path, quantize_path
58925909

58935910

5911+
def validate_existing_install(
5912+
install_dir: Path,
5913+
*,
5914+
install_kind: str | None = None,
5915+
host: HostInfo | None = None,
5916+
) -> None:
5917+
"""Run the staged smoke test against an already-built llama.cpp tree (#5854).
5918+
5919+
Used by setup.sh after a GPU source build when ``UNSLOTH_LLAMA_STAGED_VALIDATION``
5920+
is set. Raises ``PrebuiltFallback`` on failure so the caller can retry CPU.
5921+
"""
5922+
host = host or detect_host()
5923+
bin_dir = install_dir / "build" / "bin"
5924+
server_name = "llama-server.exe" if host.is_windows else "llama-server"
5925+
quantize_name = "llama-quantize.exe" if host.is_windows else "llama-quantize"
5926+
server_path = bin_dir / server_name
5927+
quantize_path = bin_dir / quantize_name
5928+
if not server_path.is_file():
5929+
raise PrebuiltFallback(f"llama-server not found at {server_path}")
5930+
5931+
with tempfile.TemporaryDirectory(prefix = "unsloth-llama-source-validate-") as tmp:
5932+
work_dir = Path(tmp)
5933+
probe_path = work_dir / "stories260K.gguf"
5934+
quantized_path = work_dir / "stories260K-q4.gguf"
5935+
download_validation_model(probe_path, validation_model_cache_path(install_dir))
5936+
if quantize_path.is_file():
5937+
validate_quantize(
5938+
quantize_path,
5939+
probe_path,
5940+
quantized_path,
5941+
install_dir,
5942+
host,
5943+
)
5944+
validate_server(
5945+
server_path,
5946+
probe_path,
5947+
host,
5948+
install_dir,
5949+
install_kind = install_kind,
5950+
)
5951+
log(f"staged source-build validation succeeded for {install_dir}")
5952+
5953+
58945954
def validate_prebuilt_attempts(
58955955
attempts: Iterable[AssetChoice],
58965956
host: HostInfo,
@@ -6345,6 +6405,24 @@ def parse_args() -> argparse.Namespace:
63456405
"fork). Use --output-format json."
63466406
),
63476407
)
6408+
resolve_group.add_argument(
6409+
"--validate-install",
6410+
metavar = "DIR",
6411+
help = (
6412+
"Run the staged llama-server smoke test against an existing build "
6413+
"tree (setup.sh source-build post-check, #5854). Exit 2 on failure. "
6414+
"Normally gated by UNSLOTH_LLAMA_STAGED_VALIDATION; this flag always "
6415+
"runs the check."
6416+
),
6417+
)
6418+
parser.add_argument(
6419+
"--install-kind",
6420+
default = None,
6421+
help = (
6422+
"Install kind for --validate-install GPU offload (e.g. linux-cuda, "
6423+
"linux-rocm, macos-arm64). When omitted, host detection decides."
6424+
),
6425+
)
63486426
parser.add_argument(
63496427
"--output-format",
63506428
choices = ("plain", "json"),
@@ -6381,6 +6459,17 @@ def emit_resolver_output(payload: dict[str, Any], *, output_format: str) -> None
63816459

63826460
def main() -> int:
63836461
args = parse_args()
6462+
if args.validate_install is not None:
6463+
try:
6464+
validate_existing_install(
6465+
Path(args.validate_install),
6466+
install_kind = args.install_kind,
6467+
)
6468+
except PrebuiltFallback as exc:
6469+
print(str(exc), file = sys.stderr)
6470+
raise SystemExit(EXIT_FALLBACK) from exc
6471+
return EXIT_SUCCESS
6472+
63846473
if args.resolve_llama_tag is not None:
63856474
resolved = resolve_requested_llama_tag(
63866475
args.resolve_llama_tag,

studio/setup.sh

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,38 @@ _resolve_cuda_archs() {
251251
printf '%s' "$_archs"
252252
}
253253

254+
# Opt-in staged GPU smoke test after a source build (#5854 gap 2). Default off:
255+
# llama-server's first GPU forward pass JIT-compiles CUDA kernels and stalls
256+
# installs for minutes on Blackwell. Same env as install_llama_prebuilt.py.
257+
_staged_validation_enabled() {
258+
local _raw="${UNSLOTH_LLAMA_STAGED_VALIDATION:-}"
259+
# Match install_llama_prebuilt.py staged_validation_enabled(): strip + lowercase.
260+
_raw="$(printf '%s' "$_raw" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' | tr '[:upper:]' '[:lower:]')"
261+
case "$_raw" in
262+
1|true|yes|on) return 0 ;;
263+
*) return 1 ;;
264+
esac
265+
}
266+
267+
# Map the source-build GPU backend to install_llama_prebuilt --install-kind so
268+
# validate_server enables --n-gpu-layers for the right backends.
269+
_source_smoke_install_kind() {
270+
if [ "${_TRY_METAL_CPU_FALLBACK:-false}" = true ]; then
271+
printf '%s' "macos-arm64"
272+
return 0
273+
fi
274+
case "${GPU_BACKEND:-}" in
275+
cuda)
276+
case "$(uname -m 2>/dev/null || true)" in
277+
aarch64|arm64) printf '%s' "linux-arm64-cuda" ;;
278+
*) printf '%s' "linux-cuda" ;;
279+
esac
280+
;;
281+
rocm) printf '%s' "linux-rocm" ;;
282+
*) printf '%s' "" ;;
283+
esac
284+
}
285+
254286
# Run a GPU probe under a 10s timeout when `timeout` is available so a wedged
255287
# NVIDIA driver cannot hang setup; fall back to a bare call where it is not.
256288
_setup_run_smi() {
@@ -1900,6 +1932,37 @@ else
19001932
run_quiet_no_exit "build diffusion visual server" cmake --build "$_BUILD_TMP/build" --config Release --target llama-diffusion-gemma-visual-server -j"$NCPU" || true
19011933
fi
19021934

1935+
# Opt-in post-build GPU smoke test (#5854 gap 2). Default off (Blackwell
1936+
# CUDA JIT stalls). On failure, reuse the CPU fallback path so the user
1937+
# still gets a working llama-server. Runs before the install swap.
1938+
if [ "$BUILD_OK" = true ] && _staged_validation_enabled; then
1939+
_FB_LABEL="$(_gpu_fallback_label)"
1940+
_SMOKE_KIND="$(_source_smoke_install_kind)"
1941+
if [ -n "$_FB_LABEL" ]; then
1942+
_SMOKE_CMD=(
1943+
python "$SCRIPT_DIR/install_llama_prebuilt.py"
1944+
--validate-install "$_BUILD_TMP"
1945+
)
1946+
[ -n "$_SMOKE_KIND" ] && _SMOKE_CMD+=(--install-kind "$_SMOKE_KIND")
1947+
if ! run_quiet_no_exit "validate source llama.cpp" "${_SMOKE_CMD[@]}"; then
1948+
substep "$_FB_LABEL source build failed smoke test; retrying CPU build..." "$C_WARN"
1949+
_TRY_METAL_CPU_FALLBACK=false
1950+
rm -rf "$_BUILD_TMP/build"
1951+
if run_quiet_no_exit "cmake llama.cpp (cpu fallback)" cmake $CMAKE_GENERATOR_ARGS -S "$_BUILD_TMP" -B "$_BUILD_TMP/build" $CPU_FALLBACK_CMAKE_ARGS; then
1952+
_BUILD_DESC="building (CPU fallback after $_FB_LABEL smoke failed)"
1953+
GPU_BACKEND=""
1954+
run_quiet_no_exit "build llama-server (cpu fallback)" cmake --build "$_BUILD_TMP/build" --config Release --target llama-server -j"$NCPU" || BUILD_OK=false
1955+
if [ "$BUILD_OK" = true ]; then
1956+
run_quiet_no_exit "build llama-quantize (cpu fallback)" cmake --build "$_BUILD_TMP/build" --config Release --target llama-quantize -j"$NCPU" || true
1957+
run_quiet_no_exit "build diffusion visual server (cpu fallback)" cmake --build "$_BUILD_TMP/build" --config Release --target llama-diffusion-gemma-visual-server -j"$NCPU" || true
1958+
fi
1959+
else
1960+
BUILD_OK=false
1961+
fi
1962+
fi
1963+
fi
1964+
fi
1965+
19031966
# Swap only after build succeeds -- preserves existing install on failure
19041967
if [ "$BUILD_OK" = true ]; then
19051968
_assert_studio_owned_or_absent "$LLAMA_CPP_DIR" "llama.cpp install"

tests/run_all.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ sh "$TESTS_DIR/sh/test_mac_intel_compat.sh"
1212
sh "$TESTS_DIR/sh/test_torch_constraint.sh"
1313
sh "$TESTS_DIR/sh/test_nvcc_meets_llama_minimum.sh"
1414
sh "$TESTS_DIR/sh/test_resolve_cuda_archs.sh"
15+
sh "$TESTS_DIR/sh/test_staged_validation_enabled.sh"
1516
sh "$TESTS_DIR/sh/test_strixhalo_wsl_reroute.sh"
1617
sh "$TESTS_DIR/sh/test_uninstall_shared_icon.sh"
1718
sh "$TESTS_DIR/sh/test_torch_flavor.sh"
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
#!/bin/bash
2+
# SPDX-License-Identifier: AGPL-3.0-only
3+
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
4+
# Unit tests for setup.sh staged-validation helpers (#5854 gap 2).
5+
# Opt-in GPU smoke after a source build; default off (Blackwell JIT stall).
6+
set -e
7+
8+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
9+
SETUP_SH="$SCRIPT_DIR/../../studio/setup.sh"
10+
PASS=0
11+
FAIL=0
12+
13+
_FUNC_FILE=$(mktemp)
14+
{
15+
sed -n '/^_staged_validation_enabled()/,/^}/p' "$SETUP_SH"
16+
sed -n '/^_source_smoke_install_kind()/,/^}/p' "$SETUP_SH"
17+
} > "$_FUNC_FILE"
18+
# shellcheck disable=SC1090
19+
. "$_FUNC_FILE"
20+
rm -f "$_FUNC_FILE"
21+
22+
assert_eq() {
23+
_label="$1"; _expected="$2"; _actual="$3"
24+
if [ "$_actual" = "$_expected" ]; then
25+
echo " PASS: $_label"; PASS=$((PASS + 1))
26+
else
27+
echo " FAIL: $_label (expected '$_expected', got '$_actual')"; FAIL=$((FAIL + 1))
28+
fi
29+
}
30+
31+
assert_rc() {
32+
_label="$1"; _expected="$2"
33+
shift 2
34+
set +e
35+
"$@" >/dev/null 2>&1
36+
_rc=$?
37+
set -e
38+
if [ "$_rc" -eq "$_expected" ]; then
39+
echo " PASS: $_label"; PASS=$((PASS + 1))
40+
else
41+
echo " FAIL: $_label (expected rc $_expected, got $_rc)"; FAIL=$((FAIL + 1))
42+
fi
43+
}
44+
45+
echo "=== _staged_validation_enabled ==="
46+
unset UNSLOTH_LLAMA_STAGED_VALIDATION
47+
assert_rc "default off" 1 _staged_validation_enabled
48+
49+
UNSLOTH_LLAMA_STAGED_VALIDATION=0
50+
assert_rc "0 is off" 1 _staged_validation_enabled
51+
52+
UNSLOTH_LLAMA_STAGED_VALIDATION=1
53+
assert_rc "1 is on" 0 _staged_validation_enabled
54+
55+
UNSLOTH_LLAMA_STAGED_VALIDATION=true
56+
assert_rc "true is on" 0 _staged_validation_enabled
57+
58+
UNSLOTH_LLAMA_STAGED_VALIDATION=yes
59+
assert_rc "yes is on" 0 _staged_validation_enabled
60+
61+
UNSLOTH_LLAMA_STAGED_VALIDATION=on
62+
assert_rc "on is on" 0 _staged_validation_enabled
63+
64+
UNSLOTH_LLAMA_STAGED_VALIDATION=True
65+
assert_rc "True is on" 0 _staged_validation_enabled
66+
67+
UNSLOTH_LLAMA_STAGED_VALIDATION=' yes '
68+
assert_rc "whitespace yes is on" 0 _staged_validation_enabled
69+
70+
UNSLOTH_LLAMA_STAGED_VALIDATION=maybe
71+
assert_rc "maybe is off" 1 _staged_validation_enabled
72+
unset UNSLOTH_LLAMA_STAGED_VALIDATION
73+
74+
echo "=== _source_smoke_install_kind ==="
75+
_TRY_METAL_CPU_FALLBACK=true
76+
GPU_BACKEND=""
77+
assert_eq "metal" "macos-arm64" "$(_source_smoke_install_kind)"
78+
79+
_TRY_METAL_CPU_FALLBACK=false
80+
GPU_BACKEND=cuda
81+
_kind="$(_source_smoke_install_kind)"
82+
case "$(uname -m)" in
83+
aarch64|arm64) assert_eq "cuda arm" "linux-arm64-cuda" "$_kind" ;;
84+
*) assert_eq "cuda x86" "linux-cuda" "$_kind" ;;
85+
esac
86+
87+
GPU_BACKEND=rocm
88+
assert_eq "rocm" "linux-rocm" "$(_source_smoke_install_kind)"
89+
90+
GPU_BACKEND=""
91+
assert_eq "cpu empty" "" "$(_source_smoke_install_kind)"
92+
93+
echo "=== setup.sh source smoke contract ==="
94+
assert_contains() {
95+
_label="$1"; _hay="$2"; _needle="$3"
96+
case "$_hay" in
97+
*"$_needle"*) echo " PASS: $_label"; PASS=$((PASS + 1)) ;;
98+
*) echo " FAIL: $_label (missing '$_needle')"; FAIL=$((FAIL + 1)) ;;
99+
esac
100+
}
101+
_src=$(cat "$SETUP_SH")
102+
assert_contains "env gate present" "$_src" "UNSLOTH_LLAMA_STAGED_VALIDATION"
103+
assert_contains "calls validate-install" "$_src" "--validate-install"
104+
assert_contains "smoke fail retries CPU" "$_src" "source build failed smoke test; retrying CPU build"
105+
# Smoke must run before the install swap.
106+
_smoke_pos=$(printf '%s' "$_src" | awk '/validate source llama.cpp/{print NR; exit}')
107+
_swap_pos=$(printf '%s' "$_src" | awk '/mv "\$_BUILD_TMP" "\$LLAMA_CPP_DIR"/{print NR; exit}')
108+
if [ -n "$_smoke_pos" ] && [ -n "$_swap_pos" ] && [ "$_smoke_pos" -lt "$_swap_pos" ]; then
109+
echo " PASS: smoke before install swap"; PASS=$((PASS + 1))
110+
else
111+
echo " FAIL: smoke before install swap (smoke=$_smoke_pos swap=$_swap_pos)"; FAIL=$((FAIL + 1))
112+
fi
113+
114+
echo ""
115+
echo "Results: $PASS passed, $FAIL failed"
116+
[ "$FAIL" -eq 0 ]

0 commit comments

Comments
 (0)