From f9b933808cb361724cec1ebcc366ad61f6ec5cd0 Mon Sep 17 00:00:00 2001 From: Vuong Hoang Date: Wed, 13 May 2026 16:00:45 -0700 Subject: [PATCH] =?UTF-8?q?playbooks:=20pull-hf-model=20=E2=80=94=20generi?= =?UTF-8?q?c=20HF=E2=86=92ana-ml2=20cache=20puller?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codifies the previously-manual workflow described in stacks/llama-swap/README.md: install hf CLI via pipx (one-time), inject hf_transfer for fast multi-connection downloads, `hf download` into the shared HF cache at /tank/aimodels/huggingface with optional --include filter. Model-format-agnostic by design — same playbook handles GGUFs for llama-swap and safetensors for vLLM (both stacks read the same cache dir via HF_HOME=/hfcache). Does NOT edit any consumer's config.yaml; per-model run params (ctx-size, sampler defaults, quant choice, chat template, etc.) stay human-curated. Usage: scripts/elway ana-ml2 --playbook playbooks/pull-hf-model.yaml \ --var hf_repo=/ \ [--var allow_patterns='*Q6_K*'] Idempotent: hf CLI skips already-cached blobs; re-runs are sub-second when the snapshot is already complete. Smoke-tested 2026-05-13 against: - mradermacher/Selene-1-Mini-Llama-3.1-8B-GGUF (Q6_K, ~6.5 GB) - Skywork/Skywork-Reward-V2-Llama-3.1-8B (full safetensors, ~16 GB) --- playbooks/pull-hf-model.yaml | 105 +++++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 playbooks/pull-hf-model.yaml diff --git a/playbooks/pull-hf-model.yaml b/playbooks/pull-hf-model.yaml new file mode 100644 index 0000000..bb80ada --- /dev/null +++ b/playbooks/pull-hf-model.yaml @@ -0,0 +1,105 @@ +# Pull a model from HuggingFace into ana-ml2's shared HF cache +# (`/tank/aimodels/huggingface/`). Model-format-agnostic — handles GGUFs, +# safetensors, or anything else HF hosts. +# +# Consumers (llama-swap, vLLM, others) read from the same cache: +# - llama-swap's config.yaml references entries via `-hf /` +# - vLLM stacks reference via `--model /` with HF_HOME mounted +# +# This playbook does NOT edit any consumer config. Run it for the download, +# then hand-edit the relevant `stacks//conf/*.yaml` and deploy +# that consumer separately. Keeps the download mechanical and the +# per-model params (ctx-size, quant choice, sampler defaults, etc.) where +# they belong — in human-curated config. +# +# Usage: +# +# # Full repo (e.g. safetensors for later AWQ quant + vLLM serving): +# scripts/elway ana-ml2 --playbook playbooks/pull-hf-model.yaml \ +# --var hf_repo=Skywork/Skywork-Reward-V2-Llama-3.1-8B +# +# # Single quant pattern (e.g. one GGUF for llama-swap): +# scripts/elway ana-ml2 --playbook playbooks/pull-hf-model.yaml \ +# --var hf_repo=mradermacher/Selene-1-Mini-Llama-3.1-8B-GGUF \ +# --var allow_patterns='*Q6_K*' +# +# allow_patterns is forwarded to `hf download --include` — set it to filter +# down to specific files (e.g. `*UD-Q6_K_XL*` or `*Q6_K*.gguf`). Omit to +# pull every file in the repo. + +vars: + hf_repo: "" # REQUIRED, e.g. unsloth/Qwen3.6-35B-A3B-GGUF + allow_patterns: "" # OPTIONAL, e.g. "*Q6_K*" + hf_cache_dir: /tank/aimodels/huggingface + +steps: + # ── Sanity: refuse to run with an empty hf_repo. ────────────────────── + - name: Check hf_repo is set + shell: | + [ -n "{{ hf_repo }}" ] || { + echo "elway: --var hf_repo=/ is required" >&2 + exit 2 + } + changed_when: "false" + + # ── Tooling: install `hf` CLI via pipx once, inject hf_transfer for + # the fast multi-connection download path. Both no-ops if + # already installed. ────────────────────────────────────── + - name: Install huggingface_hub via pipx (one-time) + shell: pipx install --quiet huggingface_hub + creates: ~/.local/bin/hf + + - name: Inject hf_transfer for faster downloads + # pipx inject is idempotent — if hf_transfer is already in the + # huggingface_hub venv, this is a fast no-op. Cheaper to run + # unconditionally than to glob-check the venv path (elway's + # creates: doesn't expand shell globs). + shell: pipx inject --quiet huggingface_hub hf_transfer + changed_when: "false" + + # ── Pull. hf CLI is itself idempotent — re-runs only fetch missing + # blobs, so safe to invoke unconditionally. ───────────────── + - name: Pull {{ hf_repo }} into {{ hf_cache_dir }} + shell: | + export PATH="$HOME/.local/bin:$PATH" + pat="{{ allow_patterns }}" + args="" + if [ -n "$pat" ]; then + args="--include $pat" + fi + HF_HOME={{ hf_cache_dir }} \ + HF_HUB_ENABLE_HF_TRANSFER=1 \ + hf download {{ hf_repo }} $args + # changed_when:false — hf CLI doesn't expose pulled-vs-cached cheaply, + # and a `du` diff would itself take seconds on a multi-GB repo. Accept + # always-reporting-ok here; the verify step below confirms presence. + changed_when: "false" + + - name: Verify cache hit for {{ hf_repo }} + shell: | + # hf normalizes user/repo to models--user--repo in the hub cache. + slug=$(printf '%s' "{{ hf_repo }}" | tr '/' '~') + slug=${slug//\~/--} + d="{{ hf_cache_dir }}/hub/models--${slug}" + [ -d "$d" ] || { echo "elway: cache dir not found for {{ hf_repo }} at $d" >&2; exit 1; } + n=$(find "$d/snapshots/" -mindepth 2 -maxdepth 2 \( -type f -o -type l \) 2>/dev/null | wc -l) + [ "$n" -gt 0 ] || { echo "elway: snapshot dir exists but is empty under $d" >&2; exit 1; } + echo "elway: $n files present under $d/snapshots/" + changed_when: "false" + +verify: + - name: hf CLI installed + shell: test -x ~/.local/bin/hf + changed_when: "false" + + - name: HF cache dir writable by SSH user + shell: test -w {{ hf_cache_dir }} + changed_when: "false" + + - name: Model present in cache (post-pull) + shell: | + slug=$(printf '%s' "{{ hf_repo }}" | tr '/' '~') + slug=${slug//\~/--} + d="{{ hf_cache_dir }}/hub/models--${slug}" + test -d "$d" && [ -n "$(find $d/snapshots/ -mindepth 2 -maxdepth 2 \( -type f -o -type l \) 2>/dev/null | head -1)" ] + changed_when: "false"