fix(install,studio): address review round 5 (3 real of 10; 5 PS-5.1 claims disproven on hardware)

Real fixes:

- uninstall.ps1: scope the WSL process kill to argv referencing
  /root/.unsloth/ (the fallback's install dir, which its Studio server,
  llama-server, and build runner all reference) instead of the bare
  '[l]lama-server' / '[u]nsloth_studio' name patterns -- uninstalling the
  Windows shim must not kill a user's own unrelated llama.cpp server or a
  /home Studio in a probed distro. Proven live: the path pattern matched
  exactly the three fallback processes while a planted /tmp/llama-server
  decoy matched the old pattern and not the new one. The backslash in
  '/root/\.unslot[h]/' keeps the pattern from matching the cleanup
  command's own argv.

- install.ps1: bridge UNSLOTH_LLAMA_TAG / UNSLOTH_LLAMA_PR into the
  background CUDA-build runner -- the provisioner honors both pins, but
  Windows env vars don't cross into WSL on their own, so a user's pin was
  silently ignored by the deferred build. (Deliberately NOT forwarded into
  the inner install.sh env: setup.sh skips its deferral when a PR pin is
  visible there, which would CPU-build the pin in the foreground.)

- kernels/flex_attention.py: make _flex_is_dgx_spark() CUDA-free
  (nvidia-smi device names, mirroring _is_dgx_spark_no_cuda_init) -- it
  runs at module import and called torch.cuda.get_device_name(), which
  initializes the CUDA allocator before patch_dgx_spark_memory_config()
  can set PYTORCH_CUDA_ALLOC_CONF on exactly the Spark hosts it targets
  (reachable via vision.py importing ..kernels before ._utils). Verified
  on the N1X: detects the machine with torch.cuda.is_initialized() still
  False.

- _utils.py: the TrainingArguments __post_init__ wrapper now forwards
  *args/**kwargs (robustness against future InitVar signatures).

Disproven on hardware (no change): the five "high" PS-5.1 claims --
String.TrimEnd('\', '/') with multiple char args binds fine to
params char[] (verified on PS 5.1.28000.1737, and the uninstaller's PATH
cleanup using exactly that code ran successfully this same day), and
[Text.Encoding] resolves via the System namespace prefix (the background
build dispatch using it has run in every install this week). The worker
"_sp possibly undefined" claim is false: `import subprocess as _sp` is at
worker.py line 25.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Daniel Han 2026-06-09 23:56:31 -07:00
commit 5c2aefc6d1
4 changed files with 32 additions and 14 deletions

View file

@ -1808,7 +1808,13 @@ shell.Run cmd, 0, False
# pass only space-free args, avoiding Start-Process mis-splitting `bash -lc <str>`.
$_pathLine = 'export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/lib/wsl/lib:$PATH"' + "`n"
$_jobsLine = if ($env:UNSLOTH_LLAMA_BUILD_JOBS) { "export UNSLOTH_LLAMA_BUILD_JOBS=$($env:UNSLOTH_LLAMA_BUILD_JOBS)`n" } else { "" }
$_runner = "#!/usr/bin/env bash`n" + $_pathLine + $_jobsLine + "exec bash /root/.unsloth/provision_llama_cuda.sh > /root/.unsloth/llama_cuda_build.log 2>&1`n"
# Bridge llama.cpp pins into WSL: the provisioner honors UNSLOTH_LLAMA_TAG /
# UNSLOTH_LLAMA_PR, but Windows env vars don't cross into WSL on their own --
# without these exports a user's pin would be silently ignored by the
# deferred background build. sh-single-quoted (tags/PRs are simple tokens).
$_tagLine = if ($env:UNSLOTH_LLAMA_TAG) { "export UNSLOTH_LLAMA_TAG='$($env:UNSLOTH_LLAMA_TAG)'`n" } else { "" }
$_prLine = if ($env:UNSLOTH_LLAMA_PR) { "export UNSLOTH_LLAMA_PR='$($env:UNSLOTH_LLAMA_PR)'`n" } else { "" }
$_runner = "#!/usr/bin/env bash`n" + $_pathLine + $_jobsLine + $_tagLine + $_prLine + "exec bash /root/.unsloth/provision_llama_cuda.sh > /root/.unsloth/llama_cuda_build.log 2>&1`n"
$_runnerB64 = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($_runner))
$_fetchCmd = 'mkdir -p /root/.unsloth; if curl -fsSL "' + $_llamaUrl + '" -o /root/.unsloth/provision_llama_cuda.sh && [ -s /root/.unsloth/provision_llama_cuda.sh ]; then chmod +x /root/.unsloth/provision_llama_cuda.sh; echo ' + $_runnerB64 + ' | base64 -d > /root/.unsloth/run_llama_build.sh; chmod +x /root/.unsloth/run_llama_build.sh; echo PROV_FETCHED; else echo PROV_NOSCRIPT; fi'
$_fetchOut = & wsl.exe -d $distro --cd /root -u root -- bash -lc $_fetchCmd 2>$null

View file

@ -390,13 +390,17 @@ function Uninstall-UnslothStudio {
# /root/.local/bin/unsloth symlink (its target under /root/.unsloth is gone but the link still
# resolves on PATH). Scope STRICTLY to /root: the WoA fallback installs there (wsl -u root), so
# touching /home/*/.unsloth would erase an unrelated WSL user's own Unsloth/cache that this
# installer never created. pkill patterns use the [x]-regex self-exclusion trick: '[u]nsloth_studio'
# keeps the shell's own argv from matching while real processes still match. Same for '[l]lama-server'.
# installer never created.
# The port-8888 kill is gated on an Unsloth install actually existing in the
# distro (checked BEFORE the rm deletes the marker): a probed distro with an
# unrelated listener on 8888 (Jupyter etc.) must not lose it. The pkills are
# already Unsloth-specific, so they stay unconditional.
$_clean = '_had=0; if [ -d /root/.unsloth ] || [ -L /root/.local/bin/unsloth ]; then _had=1; fi; rm -rf /root/.unsloth /root/llama-cuda /root/provision_llama_cuda.sh /root/llama_cuda_build.log 2>/dev/null; rm -f /root/.local/bin/unsloth 2>/dev/null; if [ $_had -eq 1 ]; then fuser -k 8888/tcp 2>/dev/null; fi; pkill -9 -f ''[u]nsloth_studio'' 2>/dev/null; pkill -9 -f ''[l]lama-server'' 2>/dev/null; true'
# unrelated listener on 8888 (Jupyter etc.) must not lose it. The process kill is
# scoped to argv referencing /root/.unsloth/ -- the fallback's install dir, which
# its Studio server, llama-server, and build runner all reference -- instead of
# bare name patterns that would also kill a user's own unrelated llama-server or
# a /home Studio in that distro. The backslash in '/root/\.unslot[h]/' keeps the
# pattern from matching this command's own argv (whose literal text contains the
# escaped form, not the resolved path) -- same idea as the [x]-bracket trick.
$_clean = '_had=0; if [ -d /root/.unsloth ] || [ -L /root/.local/bin/unsloth ]; then _had=1; fi; rm -rf /root/.unsloth /root/llama-cuda /root/provision_llama_cuda.sh /root/llama_cuda_build.log 2>/dev/null; rm -f /root/.local/bin/unsloth 2>/dev/null; if [ $_had -eq 1 ]; then fuser -k 8888/tcp 2>/dev/null; fi; pkill -9 -f ''/root/\.unslot[h]/'' 2>/dev/null; true'
$_cands = @('', 'Ubuntu', 'Ubuntu-24.04', 'Ubuntu-22.04', 'Debian')
if ($_recordedDistro) { $_cands = @($_recordedDistro) + $_cands }
if ($env:UNSLOTH_WSL_DISTRO) { $_cands = @($env:UNSLOTH_WSL_DISTRO) + $_cands }

View file

@ -27,8 +27,12 @@ torch_compile_options = {
def _flex_is_dgx_spark():
# Inlined copy of _utils.is_dgx_spark() to avoid a circular import.
# Spark = aarch64 + NVIDIA CUDA + a Spark device-name token.
# Inlined CUDA-free copy of _utils._is_dgx_spark_no_cuda_init() (kept local to
# avoid a circular import). Spark = aarch64 + a Spark device name via nvidia-smi.
# Must NOT touch torch.cuda: this runs at module import, and vision.py imports
# ..kernels before ._utils -- a device-name query here would initialize the CUDA
# allocator before patch_dgx_spark_memory_config() can set PYTORCH_CUDA_ALLOC_CONF
# on the very Spark hosts this check targets.
_force = os.environ.get("UNSLOTH_FORCE_DGX_SPARK")
if _force == "1":
return True
@ -39,11 +43,13 @@ def _flex_is_dgx_spark():
if platform.machine().lower() not in ("aarch64", "arm64"):
return False
if not (hasattr(torch, "cuda") and torch.cuda.is_available()):
return False
names = " ".join(
str(torch.cuda.get_device_name(i)).upper() for i in range(torch.cuda.device_count())
import subprocess
out = subprocess.run(
["nvidia-smi", "--query-gpu=name", "--format=csv,noheader"],
capture_output = True, text = True, timeout = 5,
)
names = (out.stdout or "").upper()
return any(t in names for t in ("GB10", "JMJWOA", "N1X", "DGX SPARK", "GB110"))
except Exception:
return False

View file

@ -1177,8 +1177,10 @@ def patch_dgx_spark_dataloader_defaults():
return
_orig_post_init = Base.__post_init__
def __post_init__(self):
_orig_post_init(self)
# Forward *args/**kwargs so a future TrainingArguments (or a subclass) that
# adds InitVar parameters to __post_init__ keeps working through the wrapper.
def __post_init__(self, *args, **kwargs):
_orig_post_init(self, *args, **kwargs)
if getattr(self, "dataloader_pin_memory", None) is True:
self.dataloader_pin_memory = False