Ctrl K
ComunidadGuías…

docker/run.sh

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/usr/bin/env bash
# Convenience wrapper for `docker run unsloth/unsloth`. Sets the easily-forgotten
# flags behind the most confusing failures:
#   --gpus all           attach a GPU (entrypoint refuses to start without one)
#   --ipc=host           ample /dev/shm; the default 64MB crashes DataLoader workers
#   --ulimit memlock=-1  unlimited pinned memory (else multi-GPU training stalls)
#   --ulimit stack=64MB  larger libtorch thread stack (some kernels OOM the 8MB default)
# Plus mounts the host HF + Triton caches so downloads and kernels persist, and the
# LM Studio, Ollama and Hermes model folders it finds, read-only, so Studio lists them.
#
# With no command the image's own CMD runs, which on unsloth/unsloth:latest is the
# Studio (8000) + JupyterLab (8888) launcher, not a REPL. $PWD is at /workspace/host.
#   bash docker/run.sh                                  # start Studio + JupyterLab
#   bash docker/run.sh bash                             # shell in the container
#   bash docker/run.sh python /workspace/host/train.py  # run your training script
#   bash docker/run.sh unsloth-run <notebook URL>       # notebook and saves stay in $PWD
#   UNSLOTH_PORTS="-p 8000:8000 -p 8888:8888" bash docker/run.sh   # publish the ports
#
# JupyterLab on the lean core image (unsloth/unsloth:core):
#   UNSLOTH_PORTS="-p 8888:8888" UNSLOTH_IMAGE=unsloth/unsloth:core \
#       bash docker/run.sh jupyter lab --ip 0.0.0.0 --port 8888 --allow-root
# CPU-only hosts: no --gpus and UNSLOTH_ALLOW_CPU=1. No training, but Studio chat,
# Jupyter and GGUF tooling work:
#   UNSLOTH_GPUS=none UNSLOTH_ALLOW_CPU=1 \
#       UNSLOTH_PORTS="-p 8000:8000 -p 8888:8888" bash docker/run.sh
#
# AMD hosts: a leading --rocm (or UNSLOTH_ROCM=1) runs the ROCm image and passes
# the AMD device nodes instead of --gpus, which is NVIDIA-only.
#   UNSLOTH_WORKSPACE_VOLUME= bash docker/run.sh --rocm python /workspace/smoke_test_rocm.py
# (no /workspace volume: it would keep the first image's copy of the smoke test)
#
# Overridable env:
#   UNSLOTH_IMAGE=unsloth/unsloth:latest    image and tag to pull/run
#   UNSLOTH_GPUS=all                        "all" | "0" | "0,1" | "none"
#   UNSLOTH_ALLOW_CPU=                      set to 1 to allow GPU-less runs
#   UNSLOTH_PORTS=                          extra -p publish flags
#   HF_HOME=$HOME/.cache/huggingface        host HF cache dir to mount
#   TRITON_CACHE_DIR=...unsloth-triton      host Triton cache dir to mount
#   UNSLOTH_WORKDIR=$PWD                    host dir mounted at /workspace/host
#   UNSLOTH_LMSTUDIO_DIR=<detected>         host LM Studio models dir, "none" to skip
#   UNSLOTH_OLLAMA_DIR=<detected>           host Ollama models dir, "none" to skip
#   UNSLOTH_HERMES_DIR=<detected>           host Hermes models dir, "none" to skip
#   UNSLOTH_MODELS_DIR=                     host dir of GGUFs/model folders for Studio
#   UNSLOTH_STUDIO_VOLUME=unsloth-studio    named volume for Studio's data (accounts,
#                                           chats, outputs) at /opt/unsloth-studio;
#                                           set it empty to run without one
#   UNSLOTH_WORKSPACE_VOLUME=unsloth-workspace  named volume for /workspace (unsloth-workspace-rocm
#                                           under --rocm); empty to skip it
#   UNSLOTH_STUDIO_SHUTDOWN_STOP_TIMEOUT_S=120  how long a training run gets to save a
#                                           checkpoint on docker stop; --stop-timeout is
#                                           set 30s above it
#   UNSLOTH_STUDIO_TRAINING_STOP_TIMEOUT_S=600  when the training stop watchdog gives up
#                                           on a saving worker; raise it alongside the
#                                           budget above, which it caps
# --rocm only:
#   UNSLOTH_ROCM=1                          same as a leading --rocm
#   HSA_OVERRIDE_GFX_VERSION                force a gfx target (e.g. 10.3.0)
#   UNSLOTH_ROCM_GFX_ARCH                   gfx arch override (e.g. gfx1151)
set -euo pipefail

# --rocm is ours only as the FIRST argument.
ROCM=0
[[ "${UNSLOTH_ROCM:-}" == "1" ]] && ROCM=1
if [[ $# -gt 0 && "$1" == "--rocm" ]]; then
    ROCM=1
    shift
fi

# UNSLOTH_DEV_ROOT prefixes /dev probes for the regression tests; unset in normal use.
DEV_ROOT="${UNSLOTH_DEV_ROOT:-}"

# WSL2 has no /dev/kfd: the card is reached over /dev/dxg and librocdxg, with no groups.
wsl_dxg_host() {
    [[ ! -e "$DEV_ROOT/dev/kfd" && -e "$DEV_ROOT/dev/dxg" ]]
}
amd_dxg_flags() {
    printf '%s\n' --device /dev/dxg
    # librocdxg cannot be built into the image (needs Windows SDK headers); mount the host's.
    local _so=""
    for _c in "$DEV_ROOT"/opt/rocm/lib/librocdxg.so.1* "$DEV_ROOT"/opt/rocm-*/lib/librocdxg.so.1* \
              "$DEV_ROOT"/opt/rocm/lib64/librocdxg.so.1* ; do
        [[ -e "$_c" ]] && { _so="$_c"; break; }
    done
    if [[ -z "$_so" ]]; then
        printf "\033[1;33mWARN:\033[0m /dev/dxg is present but no librocdxg was found under /opt/rocm.\n" >&2
        printf "      Install ROCm for WSL first:  bash scripts/install_rocm_wsl_strixhalo.sh\n" >&2
        return 0
    fi
    printf '%s\n' -v "${_so}:/usr/lib/x86_64-linux-gnu/librocdxg.so:ro"
    # librocdxg dlopens libdxcore from WSL's lib dir.
    if [[ -d "$DEV_ROOT/usr/lib/wsl/lib" ]]; then
        printf '%s\n' -v /usr/lib/wsl/lib:/usr/lib/wsl/lib:ro -e LD_LIBRARY_PATH=/usr/lib/wsl/lib
    else
        printf "\033[1;33mWARN:\033[0m /usr/lib/wsl/lib is missing, so librocdxg cannot load libdxcore.\n" >&2
    fi
    # The standard HSA runtime only looks for the bridge when this is set.
    printf '%s\n' -e HSA_ENABLE_DXG_DETECTION=1
    return 0
}
TOOLKIT_URL="${UNSLOTH_TOOLKIT_URL:-https://raw.githubusercontent.com/unslothai/unsloth/main/docker/install_nvidia_toolkit.sh}"

# --group-add needs NUMERIC gids: names resolve inside the container.
amd_device_flags() {
    printf '%s\n' --device /dev/kfd
    # docker refuses to start over a missing host device.
    if [[ -e "$DEV_ROOT/dev/dri" ]]; then
        printf '%s\n' --device /dev/dri
    else
        printf "\033[1;33mWARN:\033[0m /dev/kfd is present but /dev/dri is not; passing the compute node alone.\n" >&2
    fi
    command -v getent >/dev/null 2>&1 || return 0
    local _grp _gid
    for _grp in video render; do
        # getent fails for an unknown group, which would kill the assignment under set -e.
        _gid="$(getent group "$_grp" | cut -d: -f3)" || _gid=""
        [[ -n "$_gid" ]] && printf '%s\n' --group-add "$_gid"
    done
    return 0
}
# Read loop, not mapfile: macOS ships bash 3.2.
collect_amd_device_flags() {
    local _flag
    GPU_FLAG=()
    while IFS= read -r _flag; do
        GPU_FLAG+=("$_flag")
    done < <(amd_device_flags)
}
collect_amd_dxg_flags() {
    local _flag
    GPU_FLAG=()
    while IFS= read -r _flag; do
        GPU_FLAG+=("$_flag")
    done < <(amd_dxg_flags)
}

if [[ $ROCM -eq 1 ]]; then
    IMAGE="${UNSLOTH_IMAGE:-unsloth/unsloth-rocm:latest}"
    GPUS=none
    if [[ -e "$DEV_ROOT/dev/kfd" ]]; then
        collect_amd_device_flags
    elif wsl_dxg_host; then
        collect_amd_dxg_flags
        printf "\033[1;33mNOTE:\033[0m no /dev/kfd; passing /dev/dxg, the WSL2 bridge to the card.\n" >&2
    else
        GPU_FLAG=()
        printf "\033[1;33mWARN:\033[0m /dev/kfd is not present, so no AMD GPU can be passed through.\n" >&2
        printf "      On Linux install the amdgpu driver and add yourself to the video/render\n" >&2
        printf "      groups. On Windows the card is reached over WSL2's /dev/dxg, which only a\n" >&2
        printf "      docker engine running INSIDE your WSL distribution can pass through.\n\n" >&2
    fi
else
IMAGE="${UNSLOTH_IMAGE:-unsloth/unsloth:latest}"
GPUS="${UNSLOTH_GPUS:-all}"
# A bare integer is a COUNT to --gpus, so translate indexes to `device=`.
GPU_FLAG=(--gpus "$GPUS")
case "$GPUS" in
    none)       GPU_FLAG=()              ;;
    all|"")                              ;;
    \"device=*)                          ;;
    device=*,*) GPU_FLAG=(--gpus "\"${GPUS}\"") ;;  # docker needs the quotes
    device=*)                            ;;  # single device, fine unquoted
    *[!0-9]*) GPU_FLAG=(--gpus "\"device=${GPUS}\"") ;;  # comma list / UUID
    *)        GPU_FLAG=(--gpus "\"device=${GPUS}\"") ;;  # bare integer index
esac
fi
HF_CACHE="${HF_HOME:-$HOME/.cache/huggingface}"
TRITON_CACHE="${TRITON_CACHE_DIR:-$HOME/.cache/unsloth-triton}"
WORK_DIR="${UNSLOTH_WORKDIR:-$PWD}"
# The image relinks its code into the volume at every start. `-`, not `:-`: an explicitly
# empty value disables the mount.
STUDIO_VOLUME="${UNSLOTH_STUDIO_VOLUME-unsloth-studio}"
STUDIO_MOUNT=()
if [ -n "$STUDIO_VOLUME" ]; then
    STUDIO_MOUNT=(-v "$STUDIO_VOLUME":/opt/unsloth-studio)
fi
# per image family: a new volume keeps the files of the first image that mounts it
WORKSPACE_DEFAULT=unsloth-workspace
[[ $ROCM -eq 1 ]] && WORKSPACE_DEFAULT=unsloth-workspace-rocm
WORKSPACE_VOLUME="${UNSLOTH_WORKSPACE_VOLUME-$WORKSPACE_DEFAULT}"
WORKSPACE_MOUNT=()
if [ -n "$WORKSPACE_VOLUME" ]; then
    WORKSPACE_MOUNT=(-v "$WORKSPACE_VOLUME":/workspace)
fi

mkdir -p "$HF_CACHE" "$TRITON_CACHE"

first_dir() {
    local dir
    for dir in "$@"; do
        if [[ -n "$dir" && -d "$dir" ]]; then
            printf '%s' "$dir"
            return 0
        fi
    done
}

# Like Studio's _host_path: expand ~, and map a Windows path to its WSL mount.
host_path() {
    local path="$1"
    case "$path" in
        # Escaped ~ matches the literal character (a quoted one trips SC2088).
        \~ | \~/*) path="$HOME${path:1}" ;;
        [A-Za-z]:\\* | [A-Za-z]:/*) path="$(wslpath -u "$path" 2>/dev/null)" || path="" ;;
    esac
    printf '%s' "$path"
}

lmstudio_dir() {
    local settings="$HOME/.lmstudio/settings.json" custom=""
    local re='"downloadsFolder"[[:space:]]*:[[:space:]]*"([^"]*)"'
    if [[ -f "$settings" && "$(<"$settings")" =~ $re ]]; then
        # JSON doubles any backslash in the path
        custom="$(printf '%s' "${BASH_REMATCH[1]}" | sed 's/\\\\/\\/g')"
        custom="$(host_path "$custom")"
    fi
    first_dir "$custom" "$HOME/.lmstudio/models" "$HOME/.cache/lm-studio/models"
}

# Mirrors Studio's _hermes_root: a HERMES_HOME outside ~/.hermes is the root, or
# <root>/profiles/<name> for a profile; downloads land in <root>/models.
hermes_dir() {
    local native="$HOME/.hermes" root
    root="$(host_path "${HERMES_HOME:-}")"
    root="${root%/}"
    if [[ -z "$root" || "$root" == "$native" || "$root" == "$native"/* ]]; then
        root="$native"
    elif [[ "${root%/*}" == */profiles ]]; then
        root="${root%/profiles/*}"
    fi
    first_dir "$root/models" "$native/models"
}

ollama_dir() {
    first_dir "$(host_path "${OLLAMA_MODELS:-}")" "$HOME/.ollama/models" \
        /usr/share/ollama/.ollama/models /var/lib/ollama/.ollama/models
}

declare -a MODEL_MOUNTS=()
mount_models() {
    local name="$1" dir="$2" target="$3"
    [[ -z "$dir" || "$dir" == none ]] && return 0
    if [[ ! -d "$dir" ]]; then
        printf "\033[1;33mWARN:\033[0m %s models folder %s does not exist; not mounting it.\n" "$name" "$dir" >&2
        return 0
    fi
    [[ "$dir" == /* ]] || dir="$PWD/$dir"
    MODEL_MOUNTS+=(-v "$dir:$target:ro")
    printf "Mounting %s models from %s (read-only)\n" "$name" "$dir" >&2
}

mount_models "LM Studio" "${UNSLOTH_LMSTUDIO_DIR:-$(lmstudio_dir)}" /root/.lmstudio/models
mount_models Ollama "${UNSLOTH_OLLAMA_DIR:-$(ollama_dir)}" /root/.ollama/models
mount_models Hermes "${UNSLOTH_HERMES_DIR:-$(hermes_dir)}" /root/.hermes/models
mount_models local "${UNSLOTH_MODELS_DIR:-}" /workspace/models

# --gpus fails in the daemon (exit 125) on a host without NVIDIA, before the entrypoint can
# explain; drop the flag instead.
host_has_nvidia() {
    local listing
    [[ -e "$DEV_ROOT/dev/nvidiactl" ]] && return 0
    command -v nvidia-smi >/dev/null 2>&1 || return 1
    # buffer before grep -q: under pipefail the producer's SIGPIPE can become the pipeline status, which reads as "no GPU" and silently drops --gpus
    listing="$(nvidia-smi -L 2>/dev/null || true)"
    grep -q '^GPU' <<< "${listing}"
}

if [[ $ROCM -eq 0 && ${#GPU_FLAG[@]} -gt 0 ]] && ! host_has_nvidia; then
    printf "\033[1;33mWARN:\033[0m no NVIDIA GPU on this host; dropping --gpus %s.\n" "$GPUS" >&2
    printf "      'docker run --gpus' would fail at the daemon (exit 125) before the\n" >&2
    printf "      container starts. Set UNSLOTH_GPUS=none to silence this.\n" >&2
    GPU_FLAG=()
    # Render nodes help llama.cpp only; torch here is cu128. Use --rocm for torch on AMD.
    if [[ -e "$DEV_ROOT/dev/kfd" && -d "$DEV_ROOT/dev/dri" ]]; then
        collect_amd_device_flags
        printf "      AMD devices found: passing /dev/kfd and /dev/dri through.\n" >&2
        # published images only (untagged is :latest); a custom image may carry a HIP or Vulkan build
        if [[ "$IMAGE" == unsloth/unsloth || "$IMAGE" == unsloth/unsloth:* ]]; then
            printf "      Nothing in the image uses them yet: torch is cu128 and the bundled\n" >&2
            printf "      llama.cpp has no HIP or Vulkan backend, so this container runs on the CPU.\n" >&2
            if [[ "$IMAGE" == unsloth/unsloth:core* ]]; then
                printf "      :core refuses a CPU-only start unless UNSLOTH_ALLOW_CPU=1 is set (:latest allows it).\n" >&2
            fi
        else
            printf "      The published unsloth/unsloth images cannot use them (cu128 torch, no HIP\n" >&2
            printf "      or Vulkan llama.cpp); whether %s does is up to that image.\n" "$IMAGE" >&2
        fi
    fi
    printf "\n" >&2
fi

# A GPU host with no nvidia runtime would fail --gpus at the daemon with exit 125, so offer the installer.
# UNSLOTH_INSTALL_TOOLKIT=1 says yes without a prompt, =0 never asks; with neither and no terminal, print the one-liner and continue.
DOCKER_INFO=""
DOCKER_ERR=""
# Under --rocm the NVIDIA toolkit is irrelevant.
if [[ $ROCM -eq 0 && ${#GPU_FLAG[@]} -gt 0 ]] && host_has_nvidia; then
    # stderr folded in: on failure the captured text IS the daemon's error
    DOCKER_INFO="$(docker info 2>&1)" || { DOCKER_ERR="$DOCKER_INFO"; DOCKER_INFO=""; }
fi
if [[ -n "$DOCKER_ERR" ]]; then
    printf "\033[1;33mWARN:\033[0m 'docker info' failed, so the GPU runtime could not be checked:\n      %s\n" "${DOCKER_ERR##*$'\n'}" >&2
    printf "      Start the Docker daemon, or add yourself to the docker group (newgrp docker).\n\n" >&2
elif [[ $ROCM -eq 0 && ${#GPU_FLAG[@]} -gt 0 ]] && host_has_nvidia \
        && ! grep -qi 'Runtimes:.*nvidia' <<<"$DOCKER_INFO"; then
    # run.sh is published standalone, so fetch the installer when it is not beside it.
    INSTALLER="$(dirname "${BASH_SOURCE[0]}")/install_nvidia_toolkit.sh"
    if [[ ! -f "$INSTALLER" ]]; then
        curl -fsSL "$TOOLKIT_URL" -o "$INSTALLER" 2>/dev/null || { rm -f "$INSTALLER"; INSTALLER=""; }
    fi
    printf "\033[1;33mWARN:\033[0m 'docker info' does not list 'nvidia' as a runtime: the NVIDIA\n" >&2
    printf "      Container Toolkit is not set up, so --gpus %s would fail at the daemon.\n" "$GPUS" >&2
    answer="${UNSLOTH_INSTALL_TOOLKIT:-}"
    if [[ -n "$INSTALLER" && -z "$answer" && -t 0 && -t 1 ]]; then
        read -r -p "      Install it now with sudo (bash $INSTALLER)? [Y/n] " answer </dev/tty || answer=n
        answer="${answer:-y}"
    fi
    # Without an installer on disk, never take the install branch.
    [[ -n "$INSTALLER" ]] || answer=n
    case "$answer" in
        1|[Yy]*)
            # -E keeps UNSLOTH_TOOLKIT_VERIFY and the proxy settings through env_reset; a failed, cancelled or driver-too-old install (exit 3) must not stop the docker run below.
            if [[ "$(id -u)" = 0 ]]; then bash "$INSTALLER" || true; else sudo -E bash "$INSTALLER" || true; fi
            ;;
        *)
            printf "      Install it with one command (Linux, needs root):\n" >&2
            printf "      curl -fsSL %s -o install_nvidia_toolkit.sh && sudo -E bash install_nvidia_toolkit.sh\n\n" "$TOOLKIT_URL" >&2
            ;;
    esac
fi

# Only if set, else an empty string shadows the image's. The dash-only `-e VAR` form
# makes Docker read the value from the parent shell, so it never lands in argv.
declare -a ENV_FORWARD=(-e HF_HUB_ENABLE_HF_TRANSFER=1)
[[ -n "${HF_TOKEN:-}"          ]] && ENV_FORWARD+=(-e HF_TOKEN)
[[ -n "${WANDB_API_KEY:-}"     ]] && ENV_FORWARD+=(-e WANDB_API_KEY)
[[ -n "${UNSLOTH_LICENSE:-}"   ]] && ENV_FORWARD+=(-e UNSLOTH_LICENSE)
[[ -n "${UNSLOTH_ALLOW_CPU:-}" ]] && ENV_FORWARD+=(-e UNSLOTH_ALLOW_CPU)
[[ -n "${UNSLOTH_SKIP_GPU_CHECK:-}" ]] && ENV_FORWARD+=(-e UNSLOTH_SKIP_GPU_CHECK)
# gfx overrides for cards the installed ROCm build has no kernels for
[[ -n "${HSA_OVERRIDE_GFX_VERSION:-}" ]] && ENV_FORWARD+=(-e HSA_OVERRIDE_GFX_VERSION)
[[ -n "${UNSLOTH_ROCM_GFX_ARCH:-}"    ]] && ENV_FORWARD+=(-e UNSLOTH_ROCM_GFX_ARCH)
# studio_launch.sh generates passwords when unset and enables sshd only when a key is forwarded
[[ -n "${JUPYTER_PASSWORD:-}"           ]] && ENV_FORWARD+=(-e JUPYTER_PASSWORD)
[[ -n "${JUPYTER_PORT:-}"               ]] && ENV_FORWARD+=(-e JUPYTER_PORT)
[[ -n "${UNSLOTH_STUDIO_PASSWORD:-}"    ]] && ENV_FORWARD+=(-e UNSLOTH_STUDIO_PASSWORD)
[[ -n "${UNSLOTH_STUDIO_PORT:-}"        ]] && ENV_FORWARD+=(-e UNSLOTH_STUDIO_PORT)
[[ -n "${UNSLOTH_STUDIO_BOOTSTRAP_TIMEOUT:-}" ]] && ENV_FORWARD+=(-e UNSLOTH_STUDIO_BOOTSTRAP_TIMEOUT)
[[ -n "${UNSLOTH_STUDIO_KEEP_LEGACY:-}" ]] && ENV_FORWARD+=(-e UNSLOTH_STUDIO_KEEP_LEGACY)
# raise both timeouts; otherwise the watchdog can kill a save before the shutdown budget expires
[[ -n "${UNSLOTH_STUDIO_SHUTDOWN_STOP_TIMEOUT_S:-}" ]] && ENV_FORWARD+=(-e UNSLOTH_STUDIO_SHUTDOWN_STOP_TIMEOUT_S)
[[ -n "${UNSLOTH_STUDIO_TRAINING_STOP_TIMEOUT_S:-}" ]] && ENV_FORWARD+=(-e UNSLOTH_STUDIO_TRAINING_STOP_TIMEOUT_S)
[[ -n "${PUBLIC_KEY:-}"                 ]] && ENV_FORWARD+=(-e PUBLIC_KEY)
[[ -n "${SSH_KEY:-}"                    ]] && ENV_FORWARD+=(-e SSH_KEY)
[[ -n "${UNSLOTH_JUPYTER_CLOUDFLARE:-}" ]] && ENV_FORWARD+=(-e UNSLOTH_JUPYTER_CLOUDFLARE)
[[ -n "${UNSLOTH_SKIP_NOTEBOOK_SYNC:-}"    ]] && ENV_FORWARD+=(-e UNSLOTH_SKIP_NOTEBOOK_SYNC)
[[ -n "${UNSLOTH_SKIP_NOTEBOOK_REFRESH:-}" ]] && ENV_FORWARD+=(-e UNSLOTH_SKIP_NOTEBOOK_REFRESH)
# Studio exposure modes need explicit allowlisting or they remain inert in the documented helper
[[ -n "${UNSLOTH_STUDIO_SECURE:-}" ]]     && ENV_FORWARD+=(-e UNSLOTH_STUDIO_SECURE)
[[ -n "${UNSLOTH_STUDIO_CLOUDFLARE:-}" ]] && ENV_FORWARD+=(-e UNSLOTH_STUDIO_CLOUDFLARE)

STOP_BUDGET="${UNSLOTH_STUDIO_SHUTDOWN_STOP_TIMEOUT_S:-120}"
if ! [[ "$STOP_BUDGET" =~ ^[0-9]+$ ]]; then
    printf "\033[1;31mERROR:\033[0m UNSLOTH_STUDIO_SHUTDOWN_STOP_TIMEOUT_S=%s is not a number of seconds.\n" "$STOP_BUDGET" >&2
    exit 1
fi
# matching studio_launch.sh, 10# prevents leading zeros from selecting octal
STOP_TIMEOUT=$(( 10#$STOP_BUDGET + 30 ))

declare -a PORT_FLAGS=()
if [[ -n "${UNSLOTH_PORTS:-}" ]]; then
    # shellcheck disable=SC2206  # intentional word splitting of "-p X -p Y"
    PORT_FLAGS=(${UNSLOTH_PORTS})
fi

# CI and piped invocations otherwise hit "the input device is not a TTY"
TTY_FLAG=()
if [ -t 0 ] && [ -t 1 ]; then
    TTY_FLAG=(-it)
fi

# a mounted script starts in its directory so relative saves survive --rm
WORKDIR_FLAG=()
RUN_USER_ENV=()
if [[ $# -gt 0 ]]; then
    case "$1" in
        /workspace/host | /workspace/host/*)
            WORKDIR_FLAG=(-w /workspace/host)
            ;;
        *)
            _args=("$@")
            _runner="${1##*/}"
            _scan_from=1
            case "$_runner" in
                accelerate)
                    _runner=""
                    if [[ "${_args[1]:-}" == launch ]]; then
                        _runner=launcher
                        _scan_from=2
                    fi
                    ;;
                accelerate-launch | torchrun | deepspeed) _runner=launcher ;;
                python | python[0-9]* | pypy | pypy[0-9]*)
                    _runner=""
                    _script=""
                    for (( _i=1; _i < ${#_args[@]}; _i++ )); do
                        _arg="${_args[$_i]}"
                        if [[ "$_arg" =~ ^-([bBdEhiIOPqRsSuvVx]*)([cmWX])(.*)$ ]]; then
                            _kind="${BASH_REMATCH[2]}"
                            _value="${BASH_REMATCH[3]}"
                            case "$_kind" in
                                c) break ;;
                                m)
                                    if [[ -z "$_value" ]]; then
                                        _value="${_args[$((_i + 1))]:-}"
                                        _scan_from=$((_i + 2))
                                    else
                                        _scan_from=$((_i + 1))
                                    fi
                                    case "$_value" in
                                        accelerate.commands.launch | deepspeed.launcher.runner | torch.distributed.launch | torch.distributed.run)
                                            _runner=launcher
                                            ;;
                                    esac
                                    break
                                    ;;
                                W | X)
                                    [[ -z "$_value" ]] && _i=$((_i + 1))
                                    continue
                                    ;;
                            esac
                        fi
                        case "$_arg" in
                            -) break ;;
                            --check-hash-based-pycs) _i=$((_i + 1)) ;;
                            --)
                                _script="${_args[$((_i + 1))]:-}"
                                break
                                ;;
                            -*) ;;
                            *) _script="${_args[$_i]}"; break ;;
                        esac
                    done
                    case "$_script" in
                        /workspace/host | /workspace/host/*) WORKDIR_FLAG=(-w /workspace/host) ;;
                    esac
                    ;;
                bash | sh | zsh)
                    _runner=""
                    _script=""
                    for (( _i=1; _i < ${#_args[@]}; _i++ )); do
                        case "${_args[$_i]}" in
                            -c | -s) break ;;
                            -O | +O | -o | +o | --init-file | --rcfile) _i=$((_i + 1)) ;;
                            --)
                                _script="${_args[$((_i + 1))]:-}"
                                break
                                ;;
                            -* | +*) ;;
                            *) _script="${_args[$_i]}"; break ;;
                        esac
                    done
                    case "$_script" in
                        /workspace/host | /workspace/host/*) WORKDIR_FLAG=(-w /workspace/host) ;;
                    esac
                    ;;
                *) _runner="" ;;
            esac
            if [[ "$_runner" == launcher ]]; then
                _prev=""
                for (( _i=_scan_from; _i < ${#_args[@]}; _i++ )); do
                    _arg="${_args[$_i]}"
                    _arg_case="$_arg"
                    if [[ "$_arg_case" == --* ]]; then
                        _arg_case="${_arg_case#--}"
                        _arg_case="--${_arg_case//-/_}"
                    fi
                    if [[ "$_prev" == -* ]]; then
                        _prev=""
                        continue
                    fi
                    case "$_arg_case" in
                        -m | -q | --bind_cores_to_rank | --cpu | --debug | --downcast_bf16 | --dynamo_use_dynamic | --dynamo_use_fullgraph | --dynamo_use_regional_compilation | --enable_cpu_affinity | --elastic_training | --force_multi | --fp8_use_autocast_during_eval | --module | --multi_gpu | --no_local_rank | --no_python | --no_ssh | --no_ssh_check | --no_tpu_cluster | --quiet | --run_path | --same_network | --save_pid | --standalone | --tpu | --tpu_cluster | --tpu_use_sudo | --use_cpu | --use_deepspeed | --use_env | --use_fsdp | --use_megatron_lm | --use_mps_device | --use_parallelism_config | --use_tp | --use_xpu | --virtual_local_rank)
                            _prev=""
                            continue
                            ;;
                        -- | -H?* | -e?* | -i?* | -r?* | -t?* | -*=*)
                            _prev=""
                            continue
                            ;;
                        -*)
                            _prev="$_arg"
                            continue
                            ;;
                        /workspace/host | /workspace/host/*)
                            WORKDIR_FLAG=(-w /workspace/host)
                            break
                            ;;
                        *) break ;;
                    esac
                done
            fi
            ;;
    esac
fi
if [[ $# -gt 0 && "$1" == "unsloth-run" ]]; then
    for _arg in "${@:2}"; do
        case "$_arg" in
            http://* | https://*)
                WORKDIR_FLAG=(-w /workspace/host)
                RUN_USER_ENV=(-e "UNSLOTH_RUN_UID=$(id -u)" -e "UNSLOTH_RUN_GID=$(id -g)")
                break
                ;;
        esac
    done
fi

# set -x leaks HF_TOKEN/WANDB_API_KEY; this array form is nounset-safe on macOS Bash 3.2
exec docker run --rm ${TTY_FLAG[@]+"${TTY_FLAG[@]}"} \
    ${GPU_FLAG[@]+"${GPU_FLAG[@]}"} \
    --ipc=host \
    --stop-timeout "$STOP_TIMEOUT" \
    --ulimit memlock=-1 \
    --ulimit stack=67108864 \
    ${WORKSPACE_MOUNT[@]+"${WORKSPACE_MOUNT[@]}"} \
    -v "$HF_CACHE":/workspace/.cache/huggingface \
    -v "$TRITON_CACHE":/workspace/.cache/triton \
    -v "$WORK_DIR":/workspace/host \
    ${WORKDIR_FLAG[@]+"${WORKDIR_FLAG[@]}"} \
    ${RUN_USER_ENV[@]+"${RUN_USER_ENV[@]}"} \
    ${STUDIO_MOUNT[@]+"${STUDIO_MOUNT[@]}"} \
    ${MODEL_MOUNTS[@]+"${MODEL_MOUNTS[@]}"} \
    "${ENV_FORWARD[@]}" \
    ${PORT_FLAGS[@]+"${PORT_FLAGS[@]}"} \
    "$IMAGE" "$@"