mirror of
https://github.com/storytold/cloud-worker.git
synced 2026-10-09 00:09:43 +00:00
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:
Executable
+15
@@ -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
|
||||
@@ -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 ==="
|
||||
Executable
+58
@@ -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 ==="
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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)
|
||||
Executable
+16
@@ -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"
|
||||
Reference in New Issue
Block a user