playbooks: pull-hf-model — generic HF→ana-ml2 cache puller

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=<user>/<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)
This commit is contained in:
vh
2026-05-13 16:00:45 -07:00
parent 9523343d3a
commit f9b933808c
+105
View File
@@ -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 <user>/<repo>`
# - vLLM stacks reference via `--model <user>/<repo>` with HF_HOME mounted
#
# This playbook does NOT edit any consumer config. Run it for the download,
# then hand-edit the relevant `stacks/<consumer>/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=<user>/<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"