diff --git a/pmoves/creator/installers/MINIMAX-H3-AUTO_INSTALL-RUNPOD.sh b/pmoves/creator/installers/MINIMAX-H3-AUTO_INSTALL-RUNPOD.sh new file mode 100644 index 0000000000..c1fbaf5769 --- /dev/null +++ b/pmoves/creator/installers/MINIMAX-H3-AUTO_INSTALL-RUNPOD.sh @@ -0,0 +1,115 @@ +#!/usr/bin/env bash +# MiniMax-H3 (NVFP4) — models + aux nodes installer (RunPod / Linux, cu128). +# Companion to MINIMAX-H3-MODELS-NODES_INSTALL.bat (Windows/5090). +# +# H3 nodes are NATIVE in the PMOVES-Creator fork (comfy_extras/nodes_minimax_h3.py) +# and load in stock ComfyUI — nothing to clone for H3 itself. Only 3 workflow- +# support node packs are cloned. Loaders are dropdowns → files keep real repo +# names (no renaming). NVFP4 UNETs halve the diffusion-model size on Blackwell +# (10.86 vs 20.94 GiB per UNET); peak VRAM is SEQUENTIAL residency, fits 32 GB. +# Total ≈ 55.5 GB (63 GB with the optional tail). +# +# !! ENCODER IS AN ABLITERATED / UNCENSORED FINE-TUNE — no stock alternative +# !! exists. For UNFCU / client-facing work this is an operator decision (README). +# +# RUNTIME-AGNOSTIC: set COMFY_ROOT to target a specific ComfyUI install, else run +# from inside the ComfyUI root (needs models/ and custom_nodes/). +# Options: H3_PROFILE=NVFP4|NVFP4-HQ INSTALL_TAIL=0|1 +set -euo pipefail + +# ───────────────────── Config (override via env) ───────────────────── +COMFY_ROOT="${COMFY_ROOT:-$PWD}" +PYTHON_BIN="${PYTHON_BIN:-python3}" +VENV_DIR="${VENV_DIR:-venv}" +WANT_TORCH_STACK="${WANT_TORCH_STACK:-auto}" # auto | cu128 | keep +CUDA_TAG="${CUDA_TAG:-cu128}" +TORCH_VERSION="${TORCH_VERSION:-2.8.0}" +TORCHVISION_VERSION="${TORCHVISION_VERSION:-0.23.0}" +TORCHAUDIO_VERSION="${TORCHAUDIO_VERSION:-2.8.0}" +TORCH_INDEX="https://download.pytorch.org/whl/${CUDA_TAG}" +H3_PROFILE="${H3_PROFILE:-NVFP4}" # NVFP4 (8-12GB) | NVFP4-HQ (16-24GB) +INSTALL_TAIL="${INSTALL_TAIL:-0}" + +export PIP_DISABLE_PIP_VERSION_CHECK=1 PIP_ROOT_USER_ACTION=ignore +export PYTHONUNBUFFERED=1 HF_HUB_ENABLE_HF_TRANSFER=1 + +# ── VERIFIED SOURCES (hf_fs byte-exact, 2026-08-09 recon) ── +REPO_QUANTS="DmitryDB/MiniMax-H3-ComfyUI-Quants" +REPO_ENCODER="OTMFLY/Qwen3-VL-32B-Ultra-Heretic-MiniMax-H3-ComfyUI-INT8-ConvRot" +ENC_MAIN="qwen3vl_32b_minimax_h3_ultra_uncensored_heretic_int8_convrot.safetensors" +ENC_TAIL="qwen3vl_32b_minimax_h3_generation_tail_50_63_int8_convrot.safetensors" + +# ───────────────────── Helpers ───────────────────── +die() { echo "[ERROR] $*"; exit 1; } +need_pkg() { command -v "$1" &>/dev/null || { apt-get update -y && apt-get install -y "$@"; }; } +get_node() { # get_node + local dir=$1 url=$2 + if [[ -d "custom_nodes/$dir" ]]; then echo " [SKIP] $dir"; else + echo " • cloning $dir"; git clone "$url" "custom_nodes/$dir"; fi + [[ -f "custom_nodes/$dir/requirements.txt" ]] && \ + "$PIP" install --no-input --prefer-binary -r "custom_nodes/$dir/requirements.txt" || true +} +hf_get() { # hf_get + local repo=$1 file=$2 sub=$3 dir="$COMFY_ROOT/$sub" base + base="$(basename "$file")" + [[ -f "$dir/$base" ]] && { echo " • $base present — skip"; return 0; } + mkdir -p "$dir" + echo " • downloading $file" + hf download "$repo" "$file" --local-dir "$dir" + # hf preserves the repo path — flatten the leaf up, drop the empty subdir + if [[ "$file" != "$base" && -f "$dir/$file" ]]; then + mv -f "$dir/$file" "$dir/$base" + rmdir "$dir/$(dirname "$file")" 2>/dev/null || true + fi + return 0 +} + +# ───────────────────── Verify root + base tools ───────────────────── +cd "$COMFY_ROOT" +[[ -d "models" && -d "custom_nodes" ]] || die "COMFY_ROOT=$COMFY_ROOT is not a ComfyUI root." +echo "[INFO] ComfyUI root: $COMFY_ROOT | profile=$H3_PROFILE tail=$INSTALL_TAIL" +need_pkg curl git git-lfs +git lfs install || true + +# ───────────────────── Venv + torch ───────────────────── +[[ -d "$VENV_DIR" ]] || { echo "Creating venv → $VENV_DIR"; $PYTHON_BIN -m venv "$VENV_DIR"; } +# shellcheck disable=SC1091 +source "$VENV_DIR/bin/activate" +PIP="$(command -v pip)" +"$PIP" install --no-input -U pip setuptools wheel "huggingface_hub[cli]" hf_transfer + +GPU="$(nvidia-smi --query-gpu=name --format=csv,noheader 2>/dev/null | head -n1 || true)" +[[ "$WANT_TORCH_STACK" == "auto" ]] && { [[ -n "$GPU" ]] && WANT_TORCH_STACK="cu128" || WANT_TORCH_STACK="keep"; } +echo "[INFO] GPU=${GPU:-none} WANT_TORCH_STACK=$WANT_TORCH_STACK" +if [[ "$WANT_TORCH_STACK" != "keep" ]]; then + "$PIP" install --no-input --upgrade-strategy only-if-needed \ + --index-url "$TORCH_INDEX" --extra-index-url https://pypi.org/simple \ + "torch==${TORCH_VERSION}+${CUDA_TAG}" \ + "torchvision==${TORCHVISION_VERSION}+${CUDA_TAG}" \ + "torchaudio==${TORCHAUDIO_VERSION}+${CUDA_TAG}" +fi + +# ───────────────────── Nodes (3 support packs; H3 is native) ───────────────────── +echo "Cloning workflow-support custom nodes…" +get_node "rgthree-comfy" "https://github.com/rgthree/rgthree-comfy" +get_node "ComfyUI-KJNodes" "https://github.com/kijai/ComfyUI-KJNodes" +get_node "ComfyUI-VideoHelperSuite" "https://github.com/Kosinkadink/ComfyUI-VideoHelperSuite" + +# ───────────────────── Models ───────────────────── +echo "Downloading MiniMax-H3 models (profile=$H3_PROFILE)…" +hf_get "$REPO_QUANTS" "FL2VA/MiniMax-H3_FL2VA-${H3_PROFILE}.safetensors" "models/diffusion_models" +hf_get "$REPO_QUANTS" "Ref2VA/MiniMax-H3_Ref2VA-${H3_PROFILE}.safetensors" "models/diffusion_models" +hf_get "$REPO_QUANTS" "vae/MiniMax-H3_VideoVAE-FP16.safetensors" "models/vae" +hf_get "$REPO_QUANTS" "vae/MiniMax-H3_AudioVAE-FP32.safetensors" "models/vae" +echo " [REQUIRED] conditioning encoder (layers 0-49, ~24.6 GiB)" +hf_get "$REPO_ENCODER" "$ENC_MAIN" "models/text_encoders/MiniMax-H3" +if [[ "$INSTALL_TAIL" == "1" ]]; then + echo " [OPTIONAL] prompt-enhancement tail (layers 50-63, ~7.1 GiB)" + hf_get "$REPO_ENCODER" "$ENC_TAIL" "models/text_encoders/MiniMax-H3" +else + echo " [SKIP] optional tail (set INSTALL_TAIL=1 to fetch)" +fi + +echo +echo "✅ MiniMax-H3 install complete." +echo " Encoder dir: models/text_encoders/MiniMax-H3 (CLIPLoader type: minimax)" diff --git a/pmoves/creator/installers/MINIMAX-H3-MODELS-NODES_INSTALL.bat b/pmoves/creator/installers/MINIMAX-H3-MODELS-NODES_INSTALL.bat new file mode 100644 index 0000000000..6d5aab9456 --- /dev/null +++ b/pmoves/creator/installers/MINIMAX-H3-MODELS-NODES_INSTALL.bat @@ -0,0 +1,189 @@ +@echo off +setlocal EnableExtensions EnableDelayedExpansion +chcp 65001 >nul + +rem ============================================================================ +rem MiniMax-H3 (NVFP4) ▸ download models + clone aux nodes [5090 / Blackwell] +rem +rem H3 = audio+video generation (t2va / fl2va / ref2va). On Blackwell (RTX 5090) +rem the NVFP4 (FP4) UNET builds HALVE the diffusion-model size vs INT8-ConvRot +rem (10.86 GiB vs 20.94 GiB per UNET — a verified 48% cut). Peak VRAM is +rem SEQUENTIAL residency (encoder ~26 GiB is unloaded before the UNET samples), +rem not a sum — it fits 32 GB. Total download ≈ 55.5 GB (63 GB with the +rem optional prompt-enhancement tail). +rem +rem The H3 nodes are NATIVE in the PMOVES-Creator fork +rem (comfy_extras/nodes_minimax_h3.py) and load in stock ComfyUI — there is NO +rem H3 custom node to clone. Loaders are dropdowns, so filenames are free (the +rem files keep their real repo names; no renaming). Only the 3 workflow-support +rem node packs below are cloned. +rem +rem !! ENCODER IS AN ABLITERATED / UNCENSORED FINE-TUNE (Qwen3-VL-32B ultra- +rem !! uncensored-heretic). No stock-Qwen3-VL H3 encoder exists. For UNFCU / +rem !! client-facing work this is an EXPLICIT OPERATOR DECISION — see README. +rem +rem RUNTIME-AGNOSTIC: set COMFY_ROOT to target a specific ComfyUI, else run from +rem the ComfyUI root (needs models\ and custom_nodes\). +rem Options (env): +rem COMFY_ROOT ComfyUI root (default: current dir) +rem H3_PROFILE NVFP4 (default, 8-12GB class) | NVFP4-HQ (16-24GB class) +rem INSTALL_TAIL 1 to also fetch the optional 7.6 GB prompt-enhancement tail +rem ============================================================================ + +:: ── RESOLVE COMFY ROOT (parametric) ───────────────────────────── +if defined COMFY_ROOT (set "COMFY=%COMFY_ROOT%") else (set "COMFY=%CD%") +if not exist "%COMFY%\models" ( + echo [ERROR] "%COMFY%" is not a ComfyUI root ^(no models\ dir^). Set COMFY_ROOT or cd there. + pause & exit /b 1 +) +if not exist "%COMFY%\custom_nodes" (echo [ERROR] "%COMFY%" has no custom_nodes\ dir. & pause & exit /b 1) +echo [INFO] ComfyUI root: %COMFY% + +if not defined H3_PROFILE set "H3_PROFILE=NVFP4" +if not defined INSTALL_TAIL set "INSTALL_TAIL=0" +echo [INFO] UNET profile: %H3_PROFILE% optional tail: %INSTALL_TAIL% + +:: ── PYTHON (ComfyUI portable) — used to bootstrap the hf CLI ───── +set "PY=%COMFY%\..\python_embeded\python.exe" +set "PY_SCRIPTS=" +if exist "%PY%" ( + rem ~f / ~dp both resolve the ..\ segment, so Scripts\ below is absolute. + for %%P in ("%PY%") do (set "PY=%%~fP" & set "PY_SCRIPTS=%%~dpPScripts") +) else ( + set "PY=python" +) +echo [INFO] Python: !PY! + +git --version >nul 2>&1 || (echo [ERROR] Git not in PATH – install Git for Windows. & pause & exit /b 1) + +:: ── hf CLI — resolve an ABSOLUTE path, never a bare `hf` ───────── +:: pip drops console scripts into the TARGET interpreter's Scripts\ dir, and a +:: stock ComfyUI portable never puts python_embeded\Scripts on PATH. So a bare +:: `hf download` fails with "not recognized" in this installer's PRIMARY scenario +:: — even though the pip install immediately below has just succeeded. +call :resolve_hf +if not defined HF ( + echo [INFO] Installing huggingface_hub CLI + hf_transfer into "!PY!"... + "!PY!" -m pip install -U "huggingface_hub[cli]" hf_transfer + if errorlevel 1 (echo [ERROR] pip install of huggingface_hub failed. & pause & exit /b 1) + call :resolve_hf +) +if not defined HF ( + echo [ERROR] hf CLI not found after install. + echo Looked for "!PY_SCRIPTS!\hf.exe" and for hf on PATH. + pause & exit /b 1 +) +echo [INFO] hf CLI: !HF! +set "HF_HUB_ENABLE_HF_TRANSFER=1" +rem For gated/rate-limited pulls authenticate once first: "%HF%" auth login + +:: ── VERIFIED SOURCES (hf_fs byte-exact, 2026-08-09 recon) ─────── +set "REPO_QUANTS=DmitryDB/MiniMax-H3-ComfyUI-Quants" +set "REPO_ENCODER=OTMFLY/Qwen3-VL-32B-Ultra-Heretic-MiniMax-H3-ComfyUI-INT8-ConvRot" +set "ENC_MAIN=qwen3vl_32b_minimax_h3_ultra_uncensored_heretic_int8_convrot.safetensors" +set "ENC_TAIL=qwen3vl_32b_minimax_h3_generation_tail_50_63_int8_convrot.safetensors" + +:: ── CLONE ONLY THE 3 WORKFLOW-SUPPORT NODE PACKS ──────────────── +echo( +echo -------- Custom nodes ^(H3 nodes are native — not cloned^) -------- +pushd "%COMFY%\custom_nodes" +call :get_node "rgthree-comfy" "https://github.com/rgthree/rgthree-comfy" +call :get_node "ComfyUI-KJNodes" "https://github.com/kijai/ComfyUI-KJNodes" +call :get_node "ComfyUI-VideoHelperSuite" "https://github.com/Kosinkadink/ComfyUI-VideoHelperSuite" +popd + +:: ── DOWNLOAD MODELS via hf (real repo names; flattened into subdir) ─ +echo( +echo -------- Downloading MiniMax-H3 models -------- +:: Every download is checked. A failure here — auth, disk exhaustion, a bad +:: profile name, a dropped connection mid-pull — must NOT fall through to the +:: "models ready" banner below: a false success on a 55-63 GB install is +:: expensive to discover later, usually at generation time. +call :hf "%REPO_QUANTS%" "FL2VA/MiniMax-H3_FL2VA-%H3_PROFILE%.safetensors" "models\diffusion_models" +if errorlevel 1 goto :download_failed +call :hf "%REPO_QUANTS%" "Ref2VA/MiniMax-H3_Ref2VA-%H3_PROFILE%.safetensors" "models\diffusion_models" +if errorlevel 1 goto :download_failed +call :hf "%REPO_QUANTS%" "vae/MiniMax-H3_VideoVAE-FP16.safetensors" "models\vae" +if errorlevel 1 goto :download_failed +call :hf "%REPO_QUANTS%" "vae/MiniMax-H3_AudioVAE-FP32.safetensors" "models\vae" +if errorlevel 1 goto :download_failed +echo [REQUIRED] conditioning encoder (layers 0-49, ~24.6 GiB) +call :hf "%REPO_ENCODER%" "%ENC_MAIN%" "models\text_encoders\MiniMax-H3" +if errorlevel 1 goto :download_failed +if "%INSTALL_TAIL%"=="1" ( + rem Parens MUST be escaped inside a block: an unescaped ^) closed this if-body + rem early, so the 7.1 GiB tail downloaded unconditionally and "[SKIP] optional + rem tail" printed right after it. Verified before/after by running the script. + echo [OPTIONAL] prompt-enhancement tail ^(layers 50-63, ~7.1 GiB^) + call :hf "%REPO_ENCODER%" "%ENC_TAIL%" "models\text_encoders\MiniMax-H3" + if errorlevel 1 goto :download_failed +) else ( + echo [SKIP] optional tail ^(set INSTALL_TAIL=1 to fetch^) +) + +echo( +echo ------------------------------------------------------------- +echo MiniMax-H3 models + support nodes ready. +echo Encoder dir: models\text_encoders\MiniMax-H3 (CLIPLoader type: minimax) +echo ------------------------------------------------------------- +pause +exit /b 0 + + +:download_failed +echo( +echo ------------------------------------------------------------- +echo [ERROR] A model download FAILED — the install is INCOMPLETE. +echo Do NOT treat the models as ready. Fix the cause and re-run: +echo auth : "!HF!" auth login ^(gated or rate-limited repos^) +echo disk : needs ~55.5 GB free ^(63 GB with INSTALL_TAIL=1^) +echo profile : H3_PROFILE must be NVFP4 or NVFP4-HQ +echo network : transient drops are common on multi-GiB pulls +echo Files already fetched are kept, so a re-run resumes. +echo ------------------------------------------------------------- +pause +exit /b 1 + + +:: ==================== SUBROUTINES ============================ + +:resolve_hf +rem Prefer the CLI that ships next to the interpreter we install INTO — a stock +rem ComfyUI portable does not put that Scripts\ dir on PATH. Fall back to PATH +rem for venv / system installs where a bare `hf` does resolve. +set "HF=" +if defined PY_SCRIPTS if exist "%PY_SCRIPTS%\hf.exe" set "HF=%PY_SCRIPTS%\hf.exe" +if not defined HF (where hf >nul 2>&1 && set "HF=hf") +goto :eof + +:get_node +set "DIR=%~1" +set "URL=%~2" +if not exist "%DIR%" (echo • cloning %DIR% & git clone "%URL%" "%DIR%") else ( + echo • updating %DIR% + if exist "%DIR%\.git" (pushd "%DIR%" & git pull --ff-only & popd) else (echo [WARN] %DIR% not a git repo – skip) +) +if exist "%DIR%\requirements.txt" ("%PY%" -m pip install --upgrade -r "%DIR%\requirements.txt") +goto :eof + +:hf +rem %1 = repo id %2 = repo-relative file (fwd slashes) %3 = dest subdir (rel to COMFY) +set "H_REPO=%~1" +set "H_FILE=%~2" +set "H_DIR=%COMFY%\%~3" +set "H_WIN=%H_FILE:/=\%" +for %%A in ("%H_WIN%") do set "H_BASE=%%~nxA" +if exist "%H_DIR%\%H_BASE%" (echo • %H_BASE% already present – skip & exit /b 0) +if not exist "%H_DIR%" mkdir "%H_DIR%" +echo • downloading %H_FILE% +"%HF%" download "%H_REPO%" "%H_FILE%" --local-dir "%H_DIR%" +rem Propagate, do not warn-and-continue: the caller aborts the whole install. +if errorlevel 1 (echo [ERROR] Download failed: %H_FILE% & exit /b 1) +rem hf preserves the repo path — flatten the leaf up into H_DIR, drop empty subdir +if not "%H_WIN%"=="%H_BASE%" ( + if exist "%H_DIR%\%H_WIN%" ( + move /Y "%H_DIR%\%H_WIN%" "%H_DIR%\%H_BASE%" >nul + for %%D in ("%H_WIN%") do rd "%H_DIR%\%%~pD" 2>nul + ) +) +goto :eof diff --git a/pmoves/creator/installers/MINIMAX-H3-README.md b/pmoves/creator/installers/MINIMAX-H3-README.md new file mode 100644 index 0000000000..97efb0a73d --- /dev/null +++ b/pmoves/creator/installers/MINIMAX-H3-README.md @@ -0,0 +1,181 @@ +# MiniMax-H3 (NVFP4) ComfyUI Installer — 5090 / Blackwell + +Installs the **MiniMax-H3** audio+video generation models (NVFP4 quant) plus the +3 workflow-support node packs, against **either** runtime: + +- the in-tree **PMOVES-Creator** fork (native H3 nodes), or +- the operator's **Pinokio** ComfyUI fork. + +Both scripts are **parametric on `COMFY_ROOT`** (set it, or run from the ComfyUI root): + +| Script | Host | Options (env) | +|--------|------|---------------| +| `MINIMAX-H3-MODELS-NODES_INSTALL.bat` | Windows (5090) | `H3_PROFILE`, `INSTALL_TAIL`, `COMFY_ROOT` | +| `MINIMAX-H3-AUTO_INSTALL-RUNPOD.sh` | Linux / RunPod | same + torch-stack pins | + +> Sources below were **byte-exact verified** against the HF file index on +> 2026-08-09 (`hf_fs` listings + each repo's `SHA256SUMS`). Where a claim could +> not be confirmed it is marked UNVERIFIED — nothing is guessed. + +## Why NVFP4 (Blackwell) + +The RTX 5090 (Blackwell, sm_120) has native FP4. The **NVFP4** UNET builds are +**10.86 GiB** each vs **20.94 GiB** for INT8-ConvRot — a verified **48% cut**, +consistent with the halving claim (block-scaled NVFP4, not AWQ; covers all 208 +main + token-refiner matrices). Peak VRAM is **sequential residency** — the +~24.6 GiB encoder is unloaded before the UNET samples, so it is **not** a sum and +fits 32 GB. + +## H3 nodes are NATIVE — only 3 packs are cloned + +The H3 nodes ship in the PMOVES-Creator fork at `comfy_extras/nodes_minimax_h3.py` +(`EmptyMiniMaxH3LatentAV`, `MiniMaxH3ImageToVideo` = t2va/fl2va, +`MiniMaxH3ReferenceToVideo` = ref2va, `MiniMaxH3SigmaShift`); INT8/ConvRot layouts +live in `comfy/quant_ops.py`. They load in **stock ComfyUI** (commit `14b05228`) +with no core patch. **There is no H3 custom node to install.** The installer clones +only the workflow-support packs: + +- `rgthree-comfy` · `ComfyUI-KJNodes` · `ComfyUI-VideoHelperSuite` + +## Filenames are FREE — no renaming + +ComfyUI's H3 loaders select from **dropdowns**, so the on-disk filename does not +matter. The installer keeps each file's **real repo name** and only flattens the +`hf download` repo-subpath into the target `models/` subdir. + +> **Do not** chase the `minimax_h3_fl2va_pruned_int8_convrot.safetensors` name from +> the original brief — that string is **UNVERIFIED / likely nonexistent**. The +> `*_pruned_*` naming family traces to a *different, structurally different* repo +> (`Winnougan/MiniMax-H3-INT4_Convrot_ComfyUI`, pruned **W4A8**). Do not mix +> families. DmitryDB's NVFP4 files retain all 50 transformer blocks (not pruned). + +## Verified model sources + +### UNETs + VAEs — `DmitryDB/MiniMax-H3-ComfyUI-Quants` + +| File (repo path) | Size | → dir | +|---|---:|---| +| `FL2VA/MiniMax-H3_FL2VA-NVFP4.safetensors` | 10.862 GiB | `models/diffusion_models` | +| `Ref2VA/MiniMax-H3_Ref2VA-NVFP4.safetensors` | 10.862 GiB | `models/diffusion_models` | +| `vae/MiniMax-H3_VideoVAE-FP16.safetensors` | 4.850 GiB | `models/vae` | +| `vae/MiniMax-H3_AudioVAE-FP32.safetensors` | 0.564 GiB | `models/vae` | + +Profiles present: `NVFP4` (10.86 GiB, 8–12 GB class), `NVFP4-HQ` (13.60 GiB, 16–24 GB +class), and three `INT8-ConvRot` profiles (20–22 GiB, RTX 30/40). Set `H3_PROFILE` +to switch (default `NVFP4`). License on these files is the **MiniMax-H3 community +license** (not Apache). + +### Encoder — `OTMFLY/Qwen3-VL-32B-Ultra-Heretic-MiniMax-H3-ComfyUI-INT8-ConvRot` + +| File | Size | → dir | Status | +|---|---:|---|---| +| `qwen3vl_32b_minimax_h3_ultra_uncensored_heretic_int8_convrot.safetensors` | 24.553 GiB | `models/text_encoders/MiniMax-H3` | **REQUIRED** | +| `qwen3vl_32b_minimax_h3_generation_tail_50_63_int8_convrot.safetensors` | 7.086 GiB | `models/text_encoders/MiniMax-H3` | OPTIONAL (`INSTALL_TAIL=1`) | + +Selected in **CLIPLoader** with type `minimax`. No NVFP4 build of the encoder +exists — it is INT8 at 24.55 GiB regardless. + +> ### CORRECTION — the layers-50–63 "tail" is NOT the conditioning encoder +> The brief assumed the ~7.6 GB tail "may be all H3 needs" (saving ~19 GB). **That +> is wrong.** The repo README states H3 "consumes the unnormalized hidden state +> after language layer 49" — so the **layers 0–49 file (24.55 GiB) is REQUIRED**. +> The tail (layers 50–63 + final norm + LM head) is used **only** for optional +> prompt enhancement (loaded temporarily by the `ComfyUI-MiniMax-H3-Guide` node, +> then unloaded). Shipping only the tail produces a workflow that cannot encode a +> prompt. **Budget the 24.55 GiB file.** + +## ⚠️ Operator decision — abliterated / uncensored encoder + +Both candidate encoder repos derive from `llmfan46/Qwen3-VL-32B-Instruct-ultra- +uncensored-heretic` (an **abliterated / uncensored** fine-tune). **No stock +Qwen3-VL H3 encoder exists.** For PMOVES / UNFCU / client-facing work this is an +**explicit operator decision** — confirm it is acceptable before deploying. + +**Alternative source (recommended upstream):** `ethanfel/Qwen3-VL-32B-Ultra-Heretic- +H3-ComfyUI-INT8-ConvRot` — same content, authored by the `ComfyUI-MiniMax-H3-Guide` +node author (416 likes vs OTMFLY's 4; OTMFLY is a 2-file mirror). Byte sizes and the +pinned upstream revision match, but SHA-256 was **not** cross-compared between the +two (UNVERIFIED equivalence). The installer defaults to OTMFLY because its exact +filenames are byte-confirmed; switch `REPO_ENCODER` to ethanfel if you prefer +upstream (confirm ethanfel's exact filename first). + +## Exact `hf download` lines + +```bash +export HF_HUB_ENABLE_HF_TRANSFER=1 + +# --- diffusion_models (NVFP4, Blackwell) --- +hf download DmitryDB/MiniMax-H3-ComfyUI-Quants FL2VA/MiniMax-H3_FL2VA-NVFP4.safetensors --local-dir models/diffusion_models +hf download DmitryDB/MiniMax-H3-ComfyUI-Quants Ref2VA/MiniMax-H3_Ref2VA-NVFP4.safetensors --local-dir models/diffusion_models + +# --- vae --- +hf download DmitryDB/MiniMax-H3-ComfyUI-Quants vae/MiniMax-H3_VideoVAE-FP16.safetensors --local-dir models/vae +hf download DmitryDB/MiniMax-H3-ComfyUI-Quants vae/MiniMax-H3_AudioVAE-FP32.safetensors --local-dir models/vae + +# --- text_encoders (REQUIRED: layers 0-49 conditioning encoder, 24.55 GiB) --- +hf download OTMFLY/Qwen3-VL-32B-Ultra-Heretic-MiniMax-H3-ComfyUI-INT8-ConvRot \ + qwen3vl_32b_minimax_h3_ultra_uncensored_heretic_int8_convrot.safetensors \ + --local-dir models/text_encoders/MiniMax-H3 + +# --- text_encoders (OPTIONAL: prompt-enhancement tail, layers 50-63, 7.09 GiB) --- +hf download OTMFLY/Qwen3-VL-32B-Ultra-Heretic-MiniMax-H3-ComfyUI-INT8-ConvRot \ + qwen3vl_32b_minimax_h3_generation_tail_50_63_int8_convrot.safetensors \ + --local-dir models/text_encoders/MiniMax-H3 +``` + +> **hf-download path note:** `hf download` preserves repo-relative paths, so the +> first four land under `FL2VA/`, `Ref2VA/`, `vae/` subfolders of `--local-dir` +> (ComfyUI scans recursively, so they still resolve). The `.bat`/`.sh` installers +> **flatten** the leaf up into the target dir and drop the empty subfolder; the raw +> lines above do not. `NVFP4-HQ` swap: substitute `-NVFP4-HQ` in the two UNET names. + +## Download totals + +| Set | GB | +|---|---:| +| 2× NVFP4 UNET | 23.33 | +| 2× VAE | 5.81 | +| Encoder (0–49, required) | 26.36 | +| Tail (50–63, optional) | 7.61 | +| **Working set (no tail)** | **55.50** | +| Full set (with tail) | 63.11 | + +## Run + +```bat +:: Windows (5090) +set "COMFY_ROOT=D:\path\to\ComfyUI" +MINIMAX-H3-MODELS-NODES_INSTALL.bat +``` + +```bash +# Linux / RunPod +COMFY_ROOT=/workspace/ComfyUI bash MINIMAX-H3-AUTO_INSTALL-RUNPOD.sh +``` + +## Verify (T4 pass criteria) + +- The 3 node packs are present under `custom_nodes/`. +- The NVFP4 UNETs + VAEs + the 24.55 GiB encoder are downloaded to the dirs above. +- Both runtimes load the H3 nodes (native — appear in node search). +- The workflow's loaders resolve (no red "missing file" nodes). +- A test generation runs within 32 GB VRAM (watch `nvidia-smi`; sequential-residency + peak ≈ 26.4 GiB — encoder and UNET are **not** co-resident). + +## UNVERIFIED / caveats (from recon) + +1. **NVFP4 never run end-to-end on a 5090** — DmitryDB's RTX-50 ratings are + architecture-based; all PASS results in their table are INT8 on a 4090. NVFP4 + passed numerical/structural validation only. Treat first-run success as unproven. +2. **Uncensored encoder** — operator decision (above); no stock alternative exists. +3. **OTMFLY vs ethanfel** equivalence is size-based only (SHA not cross-checked). +4. Optional-tail path needs `github.com/ethanfel/ComfyUI-MiniMax-H3-Guide` + (verified stack: comfy-kitchen 0.2.26, comfy-aimdo 0.4.11, PyTorch 2.8.0+cu128; + comfy-kitchen recommends CUDA 13.0+ — 12.8 completes with unoptimized kernels). + +## Operator inputs still required to reconcile + +1. **SEAP `.bat` contents + folder listing** (operator-local on the 5090). +2. **The H3 workflow JSON** — to confirm which loaders/encoder path it wires. +3. **Aitrepreneur's model-mirror link** — cross-check against the verified sources + above (which stand on their own; the mirror is a confirmation, not a dependency). diff --git a/pmoves/docs/operations/B850_BRINGBACK_RUNBOOK.md b/pmoves/docs/operations/B850_BRINGBACK_RUNBOOK.md new file mode 100644 index 0000000000..3ef1342bb1 --- /dev/null +++ b/pmoves/docs/operations/B850_BRINGBACK_RUNBOOK.md @@ -0,0 +1,215 @@ +# B850 "Knuckles" Bring-Back Runbook — Tailscale/SSH Re-Enroll + Security Follow-ups + +> **Status:** operator-run. Authored by z890-claude 2026-08-09 to close the "not +> documented" gap — no prior checklist listed the exact pending enroll commands. +> **Node:** B850 "Knuckles" — primary dev host + heavyweight ROCm inference, and +> the **single data-tier home** (Postgres / NATS / the JuiceFS metadata store). +> Do **not** split-brain the data tier onto another node. + +B850 is back online after being offline. This runbook re-enrolls it into the +Tailscale mesh, verifies fleet visibility, and closes two **HIGH-severity** +security items on its live `juicefs-mount`. + +--- + +## 0. Resolve the hostname FIRST (read-only) + +B850 appears under three different names across the docs — they are **not** +interchangeable for enrollment: + +| Name | Source | Authority | +|------|--------|-----------| +| `pmoves-b850-ai-top` | `.claude/context/runner-topology.md` | **authoritative** | +| `pmoves-9850x3d-r9700` | some runbooks | drift | +| `pmoves-rdna4` | AI-Top service script | drift | + +The enroll `DEVICE=` and the `tailscale up --hostname` **must match the name the +node is actually registered under**. Confirm the live name before doing anything: + +```bash +# On any node already on the tailnet (read-only — changes nothing). +# fleet-status, not raw `tailscale status`: it prints the hostname column and +# redacts IPs, which is the standing convention (BOOTSTRAP.md § Fleet view — +# "never raw tailscale status for public IPs"). Hostnames are all this step needs. +make -C pmoves fleet-status | grep -iE 'b850|knuckles|9850|rdna4' +``` + +Set `CONFIRMED` to whatever that prints (expected: `pmoves-b850-ai-top`) and use +it verbatim everywhere below. If the node does not appear at all, it has no live +registration yet — proceed to step 1 (enroll) which creates it. + +> Hostname canonicalization across the three docs is flagged as a **separate +> docs-fix** (deferred this pass) — do not "fix" it by renaming the live node. + +--- + +## 1. Generate the enrollment token (on z890 or any owner node) + +```bash +# Sources CHIT-managed fleet secrets into the tier env files (never edit env.shared). +make -C pmoves secrets-funnel + +# Needs CHIT_PASSPHRASE in the environment (voice-activated / CHIT vault — do NOT +# paste it on a shared CLI). Target: pmoves/mk/infra.mk : fleet-enroll. +export CHIT_PASSPHRASE=... # from the CHIT vault +make -C pmoves fleet-enroll ROLE=owner DEVICE="$CONFIRMED" +``` + +`ROLE=owner` because B850 is your own management/inference workstation, not a +third party. (Enrollment tokens for `partner`/`guest` are for others — see the +`enrollment-is-for-others` note.) + +--- + +## 2. Join the tailnet FROM B850 with the correct tags + +Run **on B850**. The tags come from `pmoves/configs/tailscale-acl-policy.json` +(the `tag:lab` fleet-management block, lines ~120–154) — **not** the z890 +launcher script's tag set: + +```bash +sudo tailscale up \ + --hostname "$CONFIRMED" \ + --accept-routes \ + --accept-dns \ + --advertise-tags=tag:pmoves,tag:gpu,tag:lab +``` + +Why these three tags: +- `tag:pmoves` — full-mesh membership (port-22 reachability, exit-node egress). +- `tag:gpu` — B850 serves Ollama/llama-server to the fleet (a destination in the + `:11434` / gpu-orchestrator rules). +- `tag:lab` — fleet-management identity; grants root-SSH to the KVM concentrators + via the scoped `tag:lab → tag:vps,tag:exit` rule. + +> **Do NOT untag B850 to "fix" reachability.** Measured 2026-08-04, untagging a +> tagged compute node silently drops it out of `tag:gpu` (breaking fleet Ollama +> at `:11434`) and out of the exit-node egress src list. Untagging pmoves-4090 +> took it offline from the mesh. The ACL, not a tag removal, is the reachability +> mechanism here. + +--- + +## 3. Approve the tags (admin console, if prompted) + +If Tailscale prompts for tag approval (device-auth or tag-owner approval), approve +`tag:pmoves`, `tag:gpu`, `tag:lab` for the node in the admin console. + +--- + +## 4. Verify mesh + fleet visibility + +```bash +# From any node on the tailnet. fleet-status is the Known Road: it prints +# hostname / OS / connection and redacts IPs (see BOOTSTRAP.md § Fleet view). +make -C pmoves fleet-status | grep -i "$CONFIRMED" # expect: present, online + +make -C pmoves fleet-status # expect: relay health green +``` + +**Tags are not in either of those.** `tailscale status` does not print per-node +tags at all — its third column shows the *owner*, which for a tagged node reads +`tagged-devices`, and `fleet-status` does not surface that column either. Tags +live only in the JSON, so read them there and print nothing but hostname + tags +(no addresses, so the no-IPs convention still holds): + +```bash +tailscale status --json \ + | jq -r --arg h "$CONFIRMED" '.Peer[] | select(.HostName==$h) | "\(.HostName): \(.Tags // [""] | join(", "))"' +# expect: pmoves-b850-ai-top: tag:gpu, tag:lab, tag:pmoves +``` + +**T2 pass criteria:** `make -C pmoves fleet-status` shows `$CONFIRMED` online and +relay health green; the JSON query above lists `tag:pmoves`, `tag:gpu`, `tag:lab`. + +--- + +## 5. SSH — nothing to install on B850 + +The fleet does **not** install SSH keys on B850. Access is **Tailscale SSH**, +authorized by tag in the ACL (`tag:lab` sources reach `tag:vps`/`tag:exit` as +root; owner identities reach nodes per the owner rule). There is no per-node key +step here. + +> **Tracked gap:** the per-node SSH-key story is still incomplete fleet-wide — see +> `pmoves/docs/handoffs/doc-coordination-juicefs-ingest-2026-08-06.md` (§ around +> lines 28–31: `claude-pmoves` SSH is authorized only on 4090 + jetsons, not the +> 5090). This does not block B850 (owner-role, Tailscale-SSH by tag), but keep it +> in mind when driving *other* nodes from z890. + +--- + +## 6. Security follow-ups — HIGH severity (operator) + +B850's currently-running `juicefs-mount` was started with the **Supabase admin +password inline in the container command line**, so it is visible in `ps` and +`docker inspect` to any local user — and has been for **days**. Ref: +`pmoves/docs/handoffs/juicefs-cross-node-storage-blocker-2026-08-04.md` § +"Security item" (lines ~79–95). + +### 6a. Re-create the mount with `META_PASSWORD` (no inline secret) + +The password must be passed via the `META_PASSWORD` env var so the DSN in the +command line carries no credential. The canonical targets already do this: + +```bash +# Local-DB mount on the data-tier host (B850), password via env, cache bounded: +export SUPABASE_DB_PASSWORD=... # from the CHIT secrets pipeline +make -C pmoves juicefs-mount-local +``` + +`juicefs-mount-local` (see `pmoves/mk/egress.mk`) now also injects **per-host +bounded cache flags** (`scripts/juicefs-cache-bounds.sh`) so B850 does not inherit +the JuiceFS 100 GiB default nor self-disable caching on a full disk. Confirm the +new container has **no password** in its command line: + +```bash +docker inspect juicefs-mount --format '{{json .Args}}' # must NOT contain the password +docker inspect juicefs-mount --format '{{json .Config.Env}}' | grep -c META_PASSWORD # expect 1 +``` + +> **This narrows the exposure — it does not close it. Read before signing off.** +> +> `docker run -e META_PASSWORD` (value-less, inherited from the shell) makes Docker +> persist the **expanded** value into the container's `.Config.Env`. So this step +> moves the credential out of `.Args` — where it was visible in **both** `ps` and +> `docker inspect` — into `.Config.Env`, where it is still visible to +> `docker inspect`. The second verification command above is itself the proof: it +> greps `.Config.Env` and expects a hit. +> +> **Who can still read it:** any local user in the `docker` group (and thus root), +> via `docker inspect juicefs-mount`. That is a smaller set than "anyone who can run +> `ps`", which is the point of doing this — but it is not "no cleartext in inspect". +> +> **Real remediation:** file-mounted Docker secrets with a `*_FILE` indirection read +> inside the entrypoint, which keeps the value out of `.Args` *and* `.Config.Env`. +> The fleet already specifies this — see `#2492 § 8` (materialized per node as a +> file-mounted secret, never committed, never in `ps`), following the `#1901` +> precedent. Until that lands here, treat § 6a as *reduction*, not closure, and +> rotate on the assumption the value is readable by the docker group. + +### 6b. Rotate the exposed Supabase admin password + +Because the old password sat in process listings and container metadata for at +least three days, **rotate it** after the mount is re-created without it. Rotate +in Supabase, then flow the new value through the CHIT secrets pipeline +(`make -C pmoves secrets-funnel`) — do not hand-edit `env.shared`. Any other +consumer of `SUPABASE_DB_PASSWORD` / the JuiceFS meta DSN (the gateway, cross-node +mounts) picks up the new value on next `up`. + +**T2 security pass criteria:** `juicefs-mount` re-created with `META_PASSWORD` +(no cleartext in `ps`, and none in `.Args` — but **still readable in +`.Config.Env`** via `docker inspect`, see the note in § 6a); Supabase admin +password rotated and funneled. The file-mounted-secret work in `#2492 § 8` is +what closes `inspect`; this pass does not. + +--- + +## Related + +- Cross-node mounts on 4090/5090 depend on B850 being up + reachable → + `JUICEFS_CROSS_NODE_MOUNT_RUNBOOK.md`. +- Cache-bounds helper (wired into all mount call sites) → + `pmoves/scripts/juicefs-cache-bounds.sh`. +- Storage-backend blocker + security note → + `pmoves/docs/handoffs/juicefs-cross-node-storage-blocker-2026-08-04.md`. diff --git a/pmoves/docs/operations/JUICEFS_CROSS_NODE_MOUNT_RUNBOOK.md b/pmoves/docs/operations/JUICEFS_CROSS_NODE_MOUNT_RUNBOOK.md new file mode 100644 index 0000000000..280d9dc121 --- /dev/null +++ b/pmoves/docs/operations/JUICEFS_CROSS_NODE_MOUNT_RUNBOOK.md @@ -0,0 +1,120 @@ +# JuiceFS Cross-Node Mount Runbook — 4090 / 5090 + +> **Status:** operator-run. Authored by z890-claude 2026-08-09. +> **Goal:** mount the shared JuiceFS filesystem on the 4090 and 5090 so both can +> read/write fleet content, with a **per-host bounded cache** so neither inherits +> the JuiceFS 100 GiB default (or streams every read from tailnet MinIO). + +The JuiceFS metadata + storage home is **B850** (the data-tier host). Bring it +back first (`B850_BRINGBACK_RUNBOOK.md`) — the mounts below fail if B850 is not +up and reachable over the tailnet. + +--- + +## 0. Confirm the canonical volume BEFORE mounting + +There are two JuiceFS volumes and **only one is cross-node-capable**: + +| Volume | Storage backend | Meta | Cross-node? | +|--------|-----------------|------|-------------| +| **`pmoves`** | **minio** (tailnet MinIO) | Postgres (`juicefs_meta`) | ✅ **yes — mount this** | +| `pmoves-media` | `file` (host-local disk blocks) | — | ❌ no — lists filenames, I/O-errors every read | + +`pmoves` is minio-backed, Postgres-meta, writable (mirror-confirmed on SPARK). +`pmoves-media` is formatted with `Storage:"file"`, so its data blocks live on the +formatting host's local disk and are unreachable from any other node — the +cross-node setup script **refuses** it unless you set `ALLOW_FILE_STORAGE=1` +(don't, for the shared mount). Verify before mounting: + +```bash +make -C pmoves juicefs-storage-check +# Expect Storage: minio (or a MinIO bucket). If it reports Storage:"file", STOP — +# you are pointed at the wrong volume. See the storage blocker handoff. +``` + +--- + +## 1. Prerequisites (per node) + +- Node is on the Tailscale mesh (`make -C pmoves fleet-status` shows it online — + the Known Road; it redacts IPs, unlike raw `tailscale status`). +- Docker is installed and running. +- The Supabase DB (JuiceFS meta) on B850 is reachable **by MagicDNS hostname**, + never a literal Tailscale/LAN IP (committed docs carry no literal IPs; the + DARKXSIDE egress floor fails closed on literal IPs). + +--- + +## 2. Mount sequence (run on each node — 4090, then 5090) + +```bash +# On the target node (4090 / 5090). JUICEFS_HOST is the MagicDNS hostname of the +# JuiceFS meta host (B850) — use the name confirmed in B850_BRINGBACK_RUNBOOK.md. +# DB_PASS is the Supabase DB password, sourced from the CHIT secrets pipeline +# (exported as an env var so it reaches JuiceFS via META_PASSWORD and never +# appears in `ps` / `docker inspect`). +export DB_PASS=... # from the CHIT secrets pipeline — do not paste on a shared CLI + +make -C pmoves juicefs-cross-node-setup \ + JUICEFS_HOST=pmoves-b850-ai-top \ + DB_PASS="$DB_PASS" +``` + +This target (`pmoves/mk/egress.mk` → `pmoves/scripts/juicefs-cross-node-setup.sh`): +1. Pulls `juicedata/mount:ce-v1.3.0`. +2. Runs a **storage preflight** — refuses to proceed on a `file`-backed volume. +3. **Computes per-host bounded cache flags** via `scripts/juicefs-cache-bounds.sh` + (measures the `/data` volume's host backing dir) so the 4090/5090 do **not** + inherit the 100 GiB default and caching does not self-disable on a full disk. +4. Mounts at `$HOME/pmoves-fs` (override with `MOUNT_POINT=`). + +The setup echoes the chosen cache bounds, e.g.: + +``` +Cache bounds: --cache-dir /data --cache-size 102400 --free-space-ratio 0.100 +``` + +On a near-full node the helper drops `--free-space-ratio` below 0.1 (keeping +caching enabled) and warns that the mount is too small to hold a large working +set — heed that warning before relying on the node for heavy reads. + +--- + +## 3. Verify (per node) + +```bash +make -C pmoves juicefs-mount-status # container up + content dirs visible +ls "$HOME/pmoves-fs" # lists shared content + +# Read-path proof (the file-backed volume fails HERE with an I/O error): +cat "$HOME/pmoves-fs"/**/ >/dev/null && echo "READ OK" + +# Cache is actually being used (not self-disabled): +docker exec juicefs-mount sh -c 'du -sh /data/jfsCache 2>/dev/null || du -sh /var/jfsCache' +# Expect a NON-trivial size after a few reads (bounded, not zero-because-disabled). +``` + +**T3 pass criteria:** 4090 and 5090 both mount the **minio-backed `pmoves`** +volume; a read of a real file succeeds (no I/O error); `jfsCache` grows to a +non-trivial size (cache bounded and active, not self-disabled). + +--- + +## Windows note (5090) + +The 5090 is a Windows host. The Docker-based `juicefs-cross-node-setup` path +assumes a Linux Docker host. For a native Windows mount, the alternative is +**WinFsp + `juicefs.exe`** pointed at the same Postgres meta DSN (MagicDNS host, +not an IP) with the same `--cache-dir/--cache-size/--free-space-ratio` bounds +from `scripts/juicefs-cache-bounds.sh` (run it under Git Bash / WSL to compute the +numbers, then pass them to `juicefs.exe mount`). Pick whichever runtime matches +how the 5090 runs ComfyUI (WSL2 vs native). + +--- + +## Related + +- Data-tier host bring-up → `B850_BRINGBACK_RUNBOOK.md`. +- Storage-backend gotcha (`file` vs `minio`) → + `pmoves/docs/handoffs/juicefs-cross-node-storage-blocker-2026-08-04.md`. +- Cache-bounds helper → `pmoves/scripts/juicefs-cache-bounds.sh`.