| #!/usr/bin/env bash |
| set -euo pipefail |
|
|
| image="${PAITON_QWEN3_CODER_IMAGE:-ghcr.io/eliovp/paiton-vllm-plugin:qwen3-coder-30b-awq-rdna4-v1.0.0}" |
| container="${PAITON_CONTAINER_NAME:-paiton-qwen3-coder}" |
| port="${PAITON_HOST_PORT:-8010}" |
| cache_dir="${PAITON_CACHE_DIR:-${HOME}/.cache/paiton}" |
| chat=0 |
| stock=0 |
| download=0 |
| server_args=() |
| for option in "$@"; do |
| case "$option" in |
| --chat) chat=1 ;; |
| --stock) stock=1; server_args+=(--stock) ;; |
| --offline) server_args+=(--offline) ;; |
| --download-only) download=1; server_args+=(--download-only) ;; |
| -h|--help) |
| cat <<'HELP' |
| Run Qwen3-Coder 30B on one Radeon AI PRO R9700. |
| Usage: ./serve-docker.sh [--chat] [--stock] [--offline] [--download-only] |
|
|
| Default: start the API in the background; download the pinned model if needed. |
| --chat Wait for readiness and open terminal chat; server stays running. |
| --stock Run stock vLLM for the matched comparison. |
| --offline Require the pinned model in the persistent cache. |
| --download-only Populate the cache without loading the GPU model. |
|
|
| Environment: PAITON_CACHE_DIR (default ~/.cache/paiton), PAITON_HOST_PORT (8010), |
| PAITON_CONTAINER_NAME (paiton-qwen3-coder), PAITON_QWEN3_CODER_IMAGE. |
| HELP |
| exit 0 ;; |
| *) printf 'Unknown option: %s. Use --help.\n' "$option" >&2; exit 2 ;; |
| esac |
| done |
| if [[ ! "$port" =~ ^[0-9]{1,5}$ ]] || ((10#$port < 1 || 10#$port > 65535)); then |
| printf 'PAITON_HOST_PORT must be an integer from 1 to 65535.\n' >&2 |
| exit 2 |
| fi |
| port="$((10#$port))" |
| if ((chat && download)); then |
| printf -- '--chat and --download-only cannot be combined.\n' >&2 |
| exit 2 |
| fi |
| if ! docker info >/dev/null 2>&1; then |
| printf 'Docker is unavailable to this user.\n' >&2 |
| exit 1 |
| fi |
| mkdir -p "$cache_dir" |
| cache_dir="$(cd "$cache_dir" && pwd -P)" |
| if ((download)); then |
| exec docker run --rm --no-healthcheck -v "$cache_dir:/models/cache" "$image" "${server_args[@]}" |
| fi |
| if [[ ! -e /dev/kfd || ! -d /dev/dri ]]; then |
| printf 'ROCm devices /dev/kfd and /dev/dri are required.\n' >&2 |
| exit 1 |
| fi |
| mode=paiton |
| if ((stock)); then mode=stock; fi |
| if docker container inspect "$container" >/dev/null 2>&1; then |
| existing_image="$(docker inspect -f '{{.Config.Image}}' "$container")" |
| existing_mode="$(docker inspect -f '{{index .Config.Labels "dev.paiton.qwen3-coder.mode"}}' "$container")" |
| if [[ "$existing_image" != "$image" || "$existing_mode" != "$mode" ]]; then |
| printf 'Container %s uses a different image or mode. Choose PAITON_CONTAINER_NAME or remove it first.\n' "$container" >&2 |
| exit 1 |
| fi |
| existing_port="$(docker inspect -f '{{(index (index .HostConfig.PortBindings "8010/tcp") 0).HostPort}}' "$container")" |
| existing_cache="$(docker inspect -f '{{range .Mounts}}{{if eq .Destination "/models/cache"}}{{.Source}}{{end}}{{end}}' "$container")" |
| if [[ "$existing_port" != "$port" || "$existing_cache" != "$cache_dir" ]]; then |
| printf 'Container %s uses a different port or cache. Reuse those settings or choose another container name.\n' "$container" >&2 |
| exit 1 |
| fi |
| if [[ "$(docker inspect -f '{{.State.Running}}' "$container")" != true ]]; then |
| docker start "$container" >/dev/null |
| fi |
| else |
| docker run -d --name "$container" --device /dev/kfd --device /dev/dri \ |
| --group-add video --ipc=host --label "dev.paiton.qwen3-coder.mode=$mode" \ |
| -p "127.0.0.1:$port:8010" -v "$cache_dir:/models/cache" "$image" "${server_args[@]}" >/dev/null |
| fi |
| if ((!chat)); then |
| printf 'Qwen3-Coder is starting. API: http://127.0.0.1:%s/v1\n' "$port" |
| printf 'Model: qwen3-coder. Logs: docker logs -f %s\n' "$container" |
| printf 'Run this command again with --chat to open terminal chat.\n' |
| exit 0 |
| fi |
| printf 'Waiting for Qwen3-Coder; the first launch downloads 18.1 GB of weights.\n' |
| for ((attempt=0; attempt<3600; attempt++)); do |
| if docker exec "$container" python3 -c \ |
| "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8010/health', timeout=2)" >/dev/null 2>&1; then |
| chat_flags=(-i) |
| if [[ -t 0 && -t 1 ]]; then chat_flags=(-it); fi |
| exec docker exec "${chat_flags[@]}" "$container" paiton-chat \ |
| --url http://127.0.0.1:8010/v1/chat/completions --model qwen3-coder \ |
| --temperature 0 --max-tokens 1024 |
| fi |
| if [[ "$(docker inspect -f '{{.State.Running}}' "$container")" != true ]]; then |
| docker logs --tail 60 "$container" >&2 |
| printf 'Qwen3-Coder stopped before it became ready.\n' >&2 |
| exit 1 |
| fi |
| sleep 1 |
| done |
| printf 'Timed out waiting for readiness. Inspect: docker logs %s\n' "$container" >&2 |
| exit 1 |
|
|