MiniMax H3 on RunPod: install/download scripts, ComfyUI wiring, benchmark harness, research docs

- scripts/pod: weight downloads (workspace + tmpfs overflow for volume quota),
  extra_model_paths.yaml, headless ComfyUI launcher, test image generator,
  one-shot pod installer
- scripts/local: auto-reconnecting port forward for the ComfyUI panel
- bench: API-based harness for t2v/i2v/ref2v across durations, resolutions,
  and weight families; CSV/JSONL results
- docs: WEIGHTS.md (what's downloaded where), RESEARCH.md (ComfyUI guides,
  no-Comfy options via SGLang/vLLM/diffusers, concurrency model, serverless)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Hanashi
2026-08-06 00:28:16 -04:00
parent 50417b5530
commit 225aa97620
13 changed files with 907 additions and 1 deletions
+15
View File
@@ -0,0 +1,15 @@
#!/bin/bash
# Persistent port forward: localhost:8188 -> pod ComfyUI.
# Auto-reconnects if the tunnel drops. Ctrl-C to stop.
# Requires the echelon key loaded in your ssh agent.
POD_HOST=216.243.220.136
POD_PORT=14278
while true; do
ssh -N -L 8188:127.0.0.1:8188 \
-o ServerAliveInterval=30 -o ServerAliveCountMax=3 \
-o ExitOnForwardFailure=yes \
-o StrictHostKeyChecking=accept-new \
-p "$POD_PORT" root@"$POD_HOST"
echo "tunnel dropped, reconnecting in 3s..."
sleep 3
done
+41
View File
@@ -0,0 +1,41 @@
#!/bin/bash
# Overflow download for the weight files that don't fit under the /workspace
# network-volume quota (~100GB on the current pod). Puts them on tmpfs
# (/dev/shm, 176GB on the B200 pod) — EPHEMERAL: lost on pod restart, so this
# is for benchmarking sessions. Re-run after any pod restart.
#
# tmux new -s downloads-shm -d 'bash /workspace/minimax-h3/download-weights-tmpfs.sh'
#
# ComfyUI sees this path via the minimax_shm section of extra_model_paths.yaml.
set -uo pipefail
REPO="Comfy-Org/MiniMax-H3"
DEST="/dev/shm/minimax-h3-models"
LOG="/workspace/minimax-h3/download.log"
mkdir -p "$DEST/diffusion_models"
export HF_HUB_ENABLE_HF_TRANSFER=1
log() { echo "[$(date -u +%FT%TZ)] $*" | tee -a "$LOG"; }
dl() {
local file="$1"
if [ -f "$DEST/$file" ]; then
log "SKIP $file (already present in shm)"
return 0
fi
log "START $file -> tmpfs"
if hf download "$REPO" "$file" --local-dir "$DEST" >>"$LOG" 2>&1; then
log "DONE $file ($(du -h "$DEST/$file" | cut -f1))"
else
log "FAILED $file"
return 1
fi
}
log "=== tmpfs overflow download starting ==="
dl diffusion_models/minimax_h3_fl2va_int8_convrot.safetensors
dl diffusion_models/minimax_h3_ref2va_bf16.safetensors
dl diffusion_models/minimax_h3_fl2va_bf16.safetensors
rm -rf "$DEST/.cache"
log "=== tmpfs overflow downloads complete ==="
+58
View File
@@ -0,0 +1,58 @@
#!/bin/bash
# Download all MiniMax H3 weight families from Comfy-Org/MiniMax-H3 to the
# RunPod network volume. Run on the pod, ideally inside tmux:
# tmux new -s downloads -d 'bash /workspace/minimax-h3/download-weights.sh'
#
# Ordered so ComfyUI becomes usable as early as possible:
# phase 1: VAEs + nvfp4 text encoder + pruned-int8 diffusion models (smallest family)
# phase 2: int8_convrot family
# phase 3: bf16 family (largest)
#
# Everything lands under /workspace/minimax-h3/models/{diffusion_models,text_encoders,vae}
# which ComfyUI picks up via extra_model_paths.yaml.
set -uo pipefail
REPO="Comfy-Org/MiniMax-H3"
DEST="/workspace/minimax-h3/models"
LOG="/workspace/minimax-h3/download.log"
mkdir -p "$DEST"
export HF_HUB_ENABLE_HF_TRANSFER=1
log() { echo "[$(date -u +%FT%TZ)] $*" | tee -a "$LOG"; }
dl() {
local file="$1"
# hf download preserves the repo subpath (diffusion_models/..., vae/...) under --local-dir
if [ -f "$DEST/$file" ]; then
log "SKIP $file (already present)"
return 0
fi
log "START $file"
if hf download "$REPO" "$file" --local-dir "$DEST" >>"$LOG" 2>&1; then
log "DONE $file ($(du -h "$DEST/$file" | cut -f1))"
else
log "FAILED $file"
return 1
fi
}
log "=== MiniMax H3 weight download run starting ==="
# --- Phase 1: minimum viable set (VAEs, text encoder, pruned int8 models) ---
dl vae/minimax_h3_video_vae_fp16.safetensors
dl vae/minimax_h3_audio_vae_fp32.safetensors
dl text_encoders/qwen3vl_32b_minimax_h3_nvfp4_awq.safetensors
dl diffusion_models/minimax_h3_ref2va_pruned_int8_convrot.safetensors
dl diffusion_models/minimax_h3_fl2va_pruned_int8_convrot.safetensors
# --- Phase 2: int8_convrot (mid-size family) ---
dl diffusion_models/minimax_h3_ref2va_int8_convrot.safetensors
dl diffusion_models/minimax_h3_fl2va_int8_convrot.safetensors
# --- Phase 3: bf16 (full-precision family) ---
dl diffusion_models/minimax_h3_ref2va_bf16.safetensors
dl diffusion_models/minimax_h3_fl2va_bf16.safetensors
log "=== All downloads complete ==="
+13
View File
@@ -0,0 +1,13 @@
# ComfyUI extra model paths — points at the RunPod network volume where all
# MiniMax H3 weights live. Install to /ComfyUI/extra_model_paths.yaml
minimax_workspace:
base_path: /workspace/minimax-h3/models
diffusion_models: diffusion_models
text_encoders: text_encoders
vae: vae
# tmpfs overflow for files that exceed the network-volume quota (~100GB).
# EPHEMERAL — refill with download-weights-tmpfs.sh after a pod restart.
minimax_shm:
base_path: /dev/shm/minimax-h3-models
diffusion_models: diffusion_models
+34
View File
@@ -0,0 +1,34 @@
#!/bin/bash
# One-shot MiniMax H3 setup for a fresh RunPod pod that already has ComfyUI at
# /ComfyUI (see install-comfy.sh). Idempotent.
#
# Usage (from your machine):
# scp -O -P <port> scripts/pod/{install-minimax-h3.sh,download-weights.sh,download-weights-tmpfs.sh,extra_model_paths.yaml,make-test-images.py} root@<pod>:/root/
# ssh -p <port> root@<pod> 'bash /root/install-minimax-h3.sh'
set -euo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)"
# deps
pip install -q --break-system-packages hf_transfer 2>/dev/null || pip install -q hf_transfer
# layout + scripts on the persistent volume
mkdir -p /workspace/minimax-h3/models
cp "$HERE"/download-weights.sh "$HERE"/download-weights-tmpfs.sh \
"$HERE"/make-test-images.py /workspace/minimax-h3/ 2>/dev/null || true
cp "$HERE"/start-comfyui.sh /workspace/minimax-h3/ 2>/dev/null || true
# wire model paths into ComfyUI
cp "$HERE"/extra_model_paths.yaml /ComfyUI/extra_model_paths.yaml
# test/reference images
python3 /workspace/minimax-h3/make-test-images.py
# weights (phase 1 makes ComfyUI usable; later phases are the bigger families)
tmux new -s downloads -d 'bash /workspace/minimax-h3/download-weights.sh' 2>/dev/null \
|| echo "downloads session already exists"
# ComfyUI headless
bash /workspace/minimax-h3/start-comfyui.sh
echo "Done. Watch downloads: tmux attach -t downloads ; tail -f /workspace/minimax-h3/download.log"
+121
View File
@@ -0,0 +1,121 @@
#!/usr/bin/env python3
"""Generate synthetic test/reference images into /ComfyUI/input.
Creates:
- red_superboy_on_city_roof.png / mecha_dragon_lightning.png: stylized
placeholders matching the refs in the installed r2v workflow so it runs
out of the box.
- bench_ref_1344x768.png: generic scene used by the benchmark harness for
i2v (first frame) and ref2v runs.
"""
import math
import random
from PIL import Image, ImageDraw, ImageFilter
OUT = "/ComfyUI/input"
def city_backdrop(w, h, sky_top, sky_bottom, seed=7):
rng = random.Random(seed)
img = Image.new("RGB", (w, h))
d = ImageDraw.Draw(img)
for y in range(h):
t = y / h
d.line(
[(0, y), (w, y)],
fill=tuple(int(a + (b - a) * t) for a, b in zip(sky_top, sky_bottom)),
)
# skyline
x = 0
while x < w:
bw = rng.randint(w // 20, w // 8)
bh = rng.randint(h // 4, int(h * 0.55))
d.rectangle([x, h - bh, x + bw, h], fill=(18, 16, 28))
for wy in range(h - bh + 10, h - 10, 24):
for wx in range(x + 8, x + bw - 8, 20):
if rng.random() < 0.35:
d.rectangle([wx, wy, wx + 8, wy + 12], fill=(255, 220, 120))
x += bw + rng.randint(4, 24)
return img
def superboy():
w, h = 1344, 768
img = city_backdrop(w, h, (30, 20, 60), (120, 40, 80), seed=11)
d = ImageDraw.Draw(img)
# rooftop
d.rectangle([0, int(h * 0.78), w, h], fill=(28, 26, 36))
cx, cy = w // 2, int(h * 0.55)
# cape
d.polygon(
[(cx - 28, cy - 40), (cx + 28, cy - 40), (cx + 95, cy + 130), (cx - 60, cy + 120)],
fill=(190, 20, 30),
)
# body
d.rectangle([cx - 26, cy - 35, cx + 26, cy + 70], fill=(40, 60, 160))
# legs
d.rectangle([cx - 24, cy + 70, cx - 6, cy + 170], fill=(190, 20, 30))
d.rectangle([cx + 6, cy + 70, cx + 24, cy + 170], fill=(190, 20, 30))
# arms on hips
d.polygon([(cx - 26, cy - 25), (cx - 62, cy + 25), (cx - 26, cy + 32)], fill=(40, 60, 160))
d.polygon([(cx + 26, cy - 25), (cx + 62, cy + 25), (cx + 26, cy + 32)], fill=(40, 60, 160))
# head
d.ellipse([cx - 24, cy - 88, cx + 24, cy - 40], fill=(250, 210, 170))
d.arc([cx - 16, cy - 70, cx + 16, cy - 46], 20, 160, fill=(120, 60, 30), width=3)
# freckles + grin
for fx, fy in [(-10, -62), (10, -62), (-14, -58), (14, -58)]:
d.point((cx + fx, cy + fy), fill=(180, 120, 80))
# chest emblem
d.polygon([(cx, cy - 20), (cx - 14, cy + 5), (cx + 14, cy + 5)], fill=(250, 210, 60))
return img
def mecha_dragon():
w, h = 1344, 768
img = city_backdrop(w, h, (8, 8, 20), (40, 20, 50), seed=23)
d = ImageDraw.Draw(img)
cx, cy = int(w * 0.6), int(h * 0.45)
# body silhouette
d.polygon(
[(cx - 220, h), (cx - 120, cy + 60), (cx - 40, cy - 60), (cx + 60, cy - 140),
(cx + 180, cy - 100), (cx + 240, cy + 40), (cx + 320, h)],
fill=(10, 10, 14),
)
# head / jaws
d.polygon(
[(cx + 60, cy - 140), (cx + 10, cy - 220), (cx + 120, cy - 190), (cx + 180, cy - 100)],
fill=(14, 14, 18),
)
d.polygon([(cx + 20, cy - 210), (cx - 40, cy - 250), (cx + 40, cy - 225)], fill=(16, 16, 20))
# glowing eyes and chest core
d.ellipse([cx + 60, cy - 195, cx + 80, cy - 180], fill=(255, 40, 40))
d.ellipse([cx + 95, cy - 190, cx + 112, cy - 176], fill=(255, 40, 40))
core = Image.new("RGB", (w, h))
cd = ImageDraw.Draw(core)
cd.ellipse([cx - 20, cy - 40, cx + 60, cy + 40], fill=(255, 60, 60))
core = core.filter(ImageFilter.GaussianBlur(18))
img.paste(Image.blend(img.crop((0, 0, w, h)), core, 0.5), (0, 0), core.convert("L"))
# lightning bolts
rng = random.Random(5)
for _ in range(4):
x0 = rng.randint(cx - 100, cx + 200)
y0 = 0
pts = [(x0, y0)]
while pts[-1][1] < cy - 160:
px, py = pts[-1]
pts.append((px + rng.randint(-40, 40), py + rng.randint(30, 70)))
d.line(pts, fill=(120, 180, 255), width=4)
return img
def bench_ref():
img = city_backdrop(1344, 768, (200, 120, 60), (240, 200, 140), seed=42)
return img
if __name__ == "__main__":
superboy().save(f"{OUT}/red_superboy_on_city_roof.png")
mecha_dragon().save(f"{OUT}/mecha_dragon_lightning.png")
bench_ref().save(f"{OUT}/bench_ref_1344x768.png")
print("wrote 3 images to", OUT)
+16
View File
@@ -0,0 +1,16 @@
#!/bin/bash
# Start ComfyUI headless on the pod inside tmux, listening on 127.0.0.1:8188.
# Access from your laptop via scripts/local/port-forward.sh
set -euo pipefail
SESSION=comfyui
LOG=/workspace/minimax-h3/comfyui.log
if tmux has-session -t "$SESSION" 2>/dev/null; then
echo "ComfyUI tmux session already running"
exit 0
fi
tmux new -s "$SESSION" -d \
"cd /ComfyUI && python main.py --listen 127.0.0.1 --port 8188 2>&1 | tee -a $LOG"
echo "started; tail $LOG"