hunyuan3d_mlx operator (local MLX image→3D) + cluster docs
- server/operators/hunyuan3d_mlx: native-MLX Hunyuan3D 2.1, both stages, PBR. Verified end-to-end on M3 Ultra: 260s total, 40k-face GLB w/ 2048² PBR. Weights public (no HF login). Runs on M1 (MLX-native). - scripts/install_hunyuan3d_mlx.sh: uv py3.11 venv + MLX-path deps. - CLUSTER.md: local-first policy + M3/M1/M4 fleet roles + central-queue howto. - BENCHMARKS/README/AGENTS: hunyuan3d_mlx rows + trellis head-to-head.
This commit is contained in:
parent
19cf9ed21e
commit
573afa7b7a
@ -6,6 +6,8 @@ M3 Ultra Mac Studio, 256GB unified memory — runs MODELBEAST, a job server that
|
|||||||
wraps local Apple-Silicon ML tools and cloud APIs behind one HTTP API. You send
|
wraps local Apple-Silicon ML tools and cloud APIs behind one HTTP API. You send
|
||||||
files and job requests; it returns finished assets.
|
files and job requests; it returns finished assets.
|
||||||
|
|
||||||
|
**Fleet-aware?** This M3 is the primary of a 3-Mac Tailscale pool (M3 256GB + M1 128GB + M4 24GB). Read [CLUSTER.md](CLUSTER.md) for the local-first policy, node roles, and how the central queue distributes gpu jobs across machines.
|
||||||
|
|
||||||
## 1. Reaching it
|
## 1. Reaching it
|
||||||
|
|
||||||
- **Base URL (tailnet):** `http://100.89.131.57:8777` (M3 Ultra, primary) — from the machine itself, `http://localhost:8777`. Public (once the VPS is up): `https://modelbeast.digalot.fyi`.
|
- **Base URL (tailnet):** `http://100.89.131.57:8777` (M3 Ultra, primary) — from the machine itself, `http://localhost:8777`. Public (once the VPS is up): `https://modelbeast.digalot.fyi`.
|
||||||
@ -52,7 +54,7 @@ Poll `GET /api/jobs/{id}` every ~2s. When `done`, your outputs are the assets wh
|
|||||||
|
|
||||||
| Goal | Operator(s) | Notes |
|
| Goal | Operator(s) | Notes |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| **image → 3D mesh (GLB)** | `fal_trellis2` (SOTA open, PBR, $0.05/unit — verified from real billing 2026-07) · `fal_hunyuan3d_v21` (~90s, PBR) · `fal_rodin` (hero-grade, $0.40+) · `fal_trellis` ($0.02 draft) · local: `trellis_mac`, `sf3d` | **Best practice: run `fal_bg_remove` first** and feed the cutout — biggest quality lever. Local ops need the owner's HF token; fal ops need FAL_KEY (check `mb ops` for `[needs ...]` flags) |
|
| **image → 3D mesh (GLB)** | `fal_trellis2` (SOTA open, PBR, $0.05/unit — verified from real billing 2026-07) · `fal_hunyuan3d_v21` (~90s, PBR) · `fal_rodin` (hero-grade, $0.40+) · `fal_trellis` ($0.02 draft) · local: `trellis_mac`, `hunyuan3d_mlx` (native MLX, ~4.3 min on M3 Ultra, PBR, **no HF login** — weights public), `sf3d` | **Best practice: run `fal_bg_remove` first** and feed the cutout — biggest quality lever. `trellis_mac`/`sf3d` need the owner's HF token; `hunyuan3d_mlx` does not; fal ops need FAL_KEY (check `mb ops` for `[needs ...]` flags) |
|
||||||
| **video → gaussian splat** | `ffmpeg_frames` → `colmap_poses` → `brush_train` | 100% local/free. Orbit video, 2–3 fps sampling, 150–300 frames. 30k steps = quality, 1500 = fast preview |
|
| **video → gaussian splat** | `ffmpeg_frames` → `colmap_poses` → `brush_train` | 100% local/free. Orbit video, 2–3 fps sampling, 150–300 frames. 30k steps = quality, 1500 = fast preview |
|
||||||
| **video → camera poses** | `ffmpeg_frames` → `colmap_poses` | Outputs a COLMAP dataset folder (cameras/images/points3D) |
|
| **video → camera poses** | `ffmpeg_frames` → `colmap_poses` | Outputs a COLMAP dataset folder (cameras/images/points3D) |
|
||||||
| **prompt → image** | `flux_local` (on-device, free, seed-reproducible; `flux2-klein-4b` works with zero keys) · `openrouter_image` (nano-banana family, ~$0.04–0.24/img) · `fal_text_image` (Ideogram, readable text) | A/B: same prompt through several, then judge |
|
| **prompt → image** | `flux_local` (on-device, free, seed-reproducible; `flux2-klein-4b` works with zero keys) · `openrouter_image` (nano-banana family, ~$0.04–0.24/img) · `fal_text_image` (Ideogram, readable text) | A/B: same prompt through several, then judge |
|
||||||
|
|||||||
@ -36,9 +36,16 @@ First-run downloads (one-time): schnell/dev ~31GB & ~16–19 min each, Klein 9B
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `sf3d` | image → GLB, MPS, tex 1024 | **~5s**, ~9GB peak, 1.5MB GLB. Fast draft tier — good on solid objects, struggles on thin/open geometry. Needs `OMP_NUM_THREADS=1`+`KMP_DUPLICATE_LIB_OK` (segfaults otherwise). |
|
| `sf3d` | image → GLB, MPS, tex 1024 | **~5s**, ~9GB peak, 1.5MB GLB. Fast draft tier — good on solid objects, struggles on thin/open geometry. Needs `OMP_NUM_THREADS=1`+`KMP_DUPLICATE_LIB_OK` (segfaults otherwise). |
|
||||||
| `trellis_mac` | TRELLIS.2-4B, pipeline 1024, tex 2048, MPS | **289s (~4.8 min) generation** + 16s bake, 18.4MB GLB with PBR. SOTA-tier local quality — clean coherent geometry even on a thin-ringed astrolabe (dramatically better than SF3D). First run adds a one-time ~15GB download (~30 min); cached after. Needs `HF_HUB_DISABLE_XET=1` + the OMP guards. |
|
| `trellis_mac` | TRELLIS.2-4B, pipeline 1024, tex 2048, MPS | **289s (~4.8 min) generation** + 16s bake, 18.4MB GLB with PBR. SOTA-tier local quality — clean coherent geometry even on a thin-ringed astrolabe (dramatically better than SF3D). First run adds a one-time ~15GB download (~30 min); cached after. Needs `HF_HUB_DISABLE_XET=1` + the OMP guards. |
|
||||||
|
| `hunyuan3d_mlx` | Hunyuan3D 2.1, native MLX (fp16), shape+PBR, tex 2048, remesh 40k | **260s (~4.3 min) total** (shape 149s + texture 112s), peak 20.2GB, 7.6MB GLB (40k faces, 2048² baseColor+MR PBR). Weights **public — no HF login**. Needs `diffusers`+`fast_simplification` in the venv. **MLX-native → the one local 3D op that runs on M1 Ultra** (trellis_mac's torch-MPS bf16 is unverified there). |
|
||||||
| `bg_remove_local` | RMBG-2.0, MPS, 1024 | seconds; clean transparent cutout. Run before SF3D for a big geometry improvement. Note: keeps original RGB under alpha (upscale before cutout). |
|
| `bg_remove_local` | RMBG-2.0, MPS, 1024 | seconds; clean transparent cutout. Run before SF3D for a big geometry improvement. Note: keeps original RGB under alpha (upscale before cutout). |
|
||||||
|
|
||||||
**Local image→3D verdict:** `trellis_mac` for hero assets (~5 min, SOTA), `sf3d` for fast drafts (~5s). Both free/offline; fal cloud (`fal_trellis2` etc.) for on-demand SOTA without the local wait.
|
### Head-to-head, same mermaid cutout (M3 Ultra, 2026-07-16)
|
||||||
|
| | speed | GLB | faces | face/detail quality |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `trellis_mac` | 318s | 23MB | 175,842 | **sharper** — defined eyes/nose/mouth, individually raised tail scales, vivid colors |
|
||||||
|
| `hunyuan3d_mlx` | 260s | 7.6MB | 40,000 | softer — melted face, smoothed scales, muted texture |
|
||||||
|
|
||||||
|
**Verdict:** at defaults `trellis_mac` wins on quality (crisper face + geometry, richer color); `hunyuan3d_mlx` is faster, ~3× lighter, and the only local 3D op that runs on M1. Hunyuan's softness is largely its **laptop-tuned 40k-face decimation** — on a Studio, raise `HY3D_REMESH_FACES` (100–200k) and try `texture_size 4096` / `octree_resolution 384` (Phase D) before judging it beaten. `sf3d` stays the ~5s draft tier. All free/offline; fal cloud for on-demand SOTA without the local wait.
|
||||||
|
|
||||||
## Mesh-gen — earlier install notes (superseded by the table above)
|
## Mesh-gen — earlier install notes (superseded by the table above)
|
||||||
| Operator | Install | Runtime status |
|
| Operator | Install | Runtime status |
|
||||||
|
|||||||
40
CLUSTER.md
Normal file
40
CLUSTER.md
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
# MODELBEAST cluster — read this first
|
||||||
|
|
||||||
|
**Policy: LOCAL FIRST.** This is a multi-Mac render farm on Tailscale. Every generation should run on our own silicon ($0, private, unlimited) unless a cloud model is genuinely better for the job. Reach for `fal_*` cloud operators only when no local operator covers the need (e.g. Rodin hero-grade meshes, the multi-view volume bargain) — never as the default. Cost of a local gen: nothing. Cost of a fal gen: real money (see [BENCHMARKS.md](BENCHMARKS.md) for per-call prices).
|
||||||
|
|
||||||
|
The order of preference for any task:
|
||||||
|
1. **Local operator** on whichever node is free (the queue picks it — see below).
|
||||||
|
2. **fal cloud** only for capabilities local can't match yet.
|
||||||
|
|
||||||
|
## The fleet
|
||||||
|
|
||||||
|
| Node | Host / Tailscale IP | RAM | Role |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **M3 Ultra** (primary) | `m3ultra.local` · 100.89.131.57 | 256 GB | Runs the server + the central queue on :8777. Handles everything, especially the heavy 3D models (`hunyuan3d_mlx`, `trellis_mac`) and FLUX. |
|
||||||
|
| **M1 Ultra** (worker) | `ultra.local` · 100.91.239.7 | 128 GB | GPU worker in the pool **and** its own standalone instance. MLX-native — the right node for `hunyuan3d_mlx` (trellis's torch-MPS is unverified here). Also FLUX, `sf3d`, `bg_remove_local`. |
|
||||||
|
| **M4 Pro** (helper) | `m4pro.local` · 100.69.21.128 | 24 GB | Lightweight helper. CPU work (`ffmpeg_frames`, `ffprobe`, `colmap_poses`), light image ops (`bg_remove_local`), and small LLMs via Ollama. **Not** the big 3D/diffusion models — too little RAM. |
|
||||||
|
|
||||||
|
Git origin (shared by all nodes): `ssh://git@100.71.119.27:222/monster/modelbeast.git`
|
||||||
|
|
||||||
|
## The central queue (how tasks route to the right machine)
|
||||||
|
|
||||||
|
The M3 primary runs one server with per-lane concurrency:
|
||||||
|
- **`gpu` lane = a pool of nodes.** Each Mac has one Metal GPU, so each node runs **one** gpu job at a time. When gpu jobs queue up, the primary hands each to the first *free* node that supports that operator — rsyncing inputs over SSH, running the operator's `run.py` on that node, and rsyncing outputs back. This is the "unified queue": submit to M3, it load-balances across M3 + M1 (+ M4 for light ops).
|
||||||
|
- **`cpu` / `net` lanes** run on the primary (M3) with simple concurrency limits (`cpu` 3, `net` 6).
|
||||||
|
|
||||||
|
Which node may run which operator is set in `nodes.json` (repo root, gitignored, primary-only) via each node's `operators` allowlist. Omit the allowlist to let a node run every gpu op. **`nodes.json` is read at server startup — restart the server (`scripts/serve.sh`) after editing it.**
|
||||||
|
|
||||||
|
## Adding a machine to the fleet
|
||||||
|
|
||||||
|
1. `git clone ssh://git@100.71.119.27:222/monster/modelbeast.git ~/MODELBEAST`
|
||||||
|
2. Install `uv` (`curl -LsSf https://astral.sh/uv/install.sh | sh`) — the venvs use it.
|
||||||
|
3. Run the per-operator install scripts you want that node to serve (`scripts/install_*.sh`). Each builds its own `vendor/*/.venv`.
|
||||||
|
4. Model weights: **copy from a node that already has them** (`rsync -a ~/.cache/huggingface/hub/models--… newnode:~/.cache/huggingface/hub/`) rather than re-downloading — faster and dodges HF rate limits. We have an HF token in Settings if a fresh download is unavoidable.
|
||||||
|
5. On the **primary**, add the node to `nodes.json` with an `operators` allowlist matched to its RAM, then restart the server.
|
||||||
|
|
||||||
|
## Operator cheat-sheet (local first!)
|
||||||
|
|
||||||
|
- **image → 3D mesh:** local `hunyuan3d_mlx` (MLX, M1-capable, no HF login) or `trellis_mac` (sharper at defaults) → `sf3d` for fast drafts. fal only for Rodin hero-grade / multi-view. Always `bg_remove_local` (or `fal_bg_remove`) the input first.
|
||||||
|
- **prompt/image → image:** local `flux_local` (FLUX.2 Klein) → `mflux_image_edit`. fal/OpenRouter for nano-banana scene-storytelling.
|
||||||
|
- **video → frames / poses:** `ffmpeg_frames`, `colmap_poses` (CPU — great M4 work).
|
||||||
|
- Full matrix + prices: [AGENTS.md](AGENTS.md) · [BENCHMARKS.md](BENCHMARKS.md).
|
||||||
90
HANDOFF_HY3D_MLX.md
Normal file
90
HANDOFF_HY3D_MLX.md
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
# HANDOFF: `hunyuan3d_mlx` operator — fully-local Hunyuan3D 2.1 via MLX
|
||||||
|
|
||||||
|
**For:** Opus 4.8 executing autonomously in this repo.
|
||||||
|
**Goal:** Add a second fully-local image→3D generator (Hunyuan3D 2.1, native MLX) alongside `trellis_mac`, benchmark it on both nodes, and (stretch) fork-improve it to exploit Studio-class GPUs (M3 Ultra 256GB / 819GB/s, M1 Ultra 128GB). Written 2026-07-16 after code-level assessment of the upstream repo.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Context & assessment (read first, don't re-derive)
|
||||||
|
|
||||||
|
**Upstream:** https://github.com/dgrauet/Hunyuan3D-2.1-mlx — fork of Tencent's Hunyuan3D-2.1 adding native MLX inference for BOTH stages:
|
||||||
|
- **Stage 1** (image → mesh): `hy3dshape/hy3dshape/pipeline_mlx.py` → `ShapePipeline.from_pretrained("dgrauet/hunyuan3d-2.1-mlx")`. Validated numerically vs PyTorch within 1e-5.
|
||||||
|
- **Stage 2** (mesh + reference image → PBR-textured GLB): `hy3dpaint/textureGenPipeline_mlx.py` → `Hunyuan3DPaintPipelineMLX`. UNet match 1.17e-5 vs PyTorch. Includes MLX RealESRGAN x4 (512²→2048² per view), cosine-weighted bake blend, EDT UV-gutter fill, proper glTF PBR export (`baseColorTexture` + `metallicRoughnessTexture`, doubleSided).
|
||||||
|
|
||||||
|
**Weights:** HF `dgrauet/hunyuan3d-2.1-mlx`, ~15.1GB total, auto-downloaded on first use. Override dir via env `HUNYUAN3D_MLX_WEIGHTS_DIR`. FP16 DiT is 5.68GB (`dit.safetensors`); paint stack adds `paint_unet` 3.66GB + `paint_dino` 2.12GB + `paint_clip` 1.18GB + VAEs + `realesrgan_x4plus_mlx`. Peak memory ~10GB stage 1 fp16 + ~6GB stage 2 → trivial on both our nodes. **Use FP16, not INT8** — both Ultras have memory to burn and the owner wants best quality; INT8/INT4 exist only for 16GB Macs.
|
||||||
|
|
||||||
|
**Port quality verdict: high for a 4-star repo.** Disciplined single-author work: `docs/forward_pass.md` documents porting principles, `docs/adr/` has architecture decisions, `tests/` includes an end-to-end stage1→stage2 chain and unit tests. Deviations from the PyTorch reference are documented, not silent. Rough edges to expect: single author, ~188 weight downloads, no community battle-testing — treat every first run as a probe.
|
||||||
|
|
||||||
|
**Reference timings (M2 Pro):** ~9 min for 6-view texture synthesis at 512px. Our Ultras should beat this substantially; M1 Ultra runs MLX natively (unlike TRELLIS.2's torch-MPS bf16 question flagged in AGENTS.md).
|
||||||
|
|
||||||
|
### The 4096-texture situation (this is the fork opportunity)
|
||||||
|
|
||||||
|
The README claims `texture_size=2048` vs PyTorch's 4096 because "the MLX Metal rasterizer has no tiling." **The code is ahead of the README:**
|
||||||
|
|
||||||
|
- `hy3dpaint/textureGenPipeline_mlx.py:82` — config default is ALREADY `texture_size = 1024 * 4` (4096).
|
||||||
|
- `hy3dpaint/DifferentiableRenderer/mesh_render_mlx.py:267` — `_rasterize_tiled(pos_clip, resolution, tiles=4)` exists, verified against single-dispatch at 2048².
|
||||||
|
- `mesh_render_mlx.py:647-658` — `extract_textiles()` (the main UV-space raster feeding `back_project(method="back_sample")`) already auto-tiles: 2 tiles at ≥3072, 4 tiles at ≥4096.
|
||||||
|
- **Remaining untiled gaps:** `uv_feature_map()` at `mesh_render_mlx.py:728` calls plain `_rasterize(vtx_uv_clip, self.texture_size)` — this will blow the Metal command-buffer budget at 4096 if it's on the hot path. (`mesh_render_mlx.py:815` is camera-view raster at `render_size`=2048, fine as-is.) The e2e test (`hy3dpaint/tests/test_e2e_mesh.py:49-51`) pins 2048 "because we don't tile the bake."
|
||||||
|
- The command-buffer error (`kIOGPUCommandBufferCallbackErrorImpactingInteractivity`) is a watchdog concern documented **on laptop GPUs**. An M3 Ultra Studio may simply not hit it untiled — test before patching.
|
||||||
|
|
||||||
|
Other Studio-class headroom knobs (all currently tuned for laptops):
|
||||||
|
- **Remesh budget:** `tests/test_stage1_to_stage2.py:82-91` decimates Stage 1's ~500k-face mesh to ~40-60k before texturing (rasterizer budget again). On Ultra, try 100-200k for bake fidelity.
|
||||||
|
- **Stage 1 `octree_resolution`:** default 256; try 384 for finer geometry (memory is not our constraint).
|
||||||
|
- **`max_num_view`:** default 6; the candidate camera list supports many more views.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Phase A — Fork + vendor + install (M3 first)
|
||||||
|
|
||||||
|
1. **Fork** `dgrauet/Hunyuan3D-2.1-mlx` to the owner's GitHub account with `gh repo fork dgrauet/Hunyuan3D-2.1-mlx --clone=false` (confirm the account `gh auth status` shows; do NOT create the fork under an org without asking). All our patches go to the fork's `modelbeast` branch; keep `main` tracking upstream for rebases.
|
||||||
|
2. **Vendor:** clone the fork to `vendor/hunyuan3d-mlx` (repo convention: `vendor/trellis-mac`, `vendor/stable-fast-3d`).
|
||||||
|
3. **Install script** `scripts/install_hunyuan3d_mlx.sh`, modeled on `scripts/install_trellis_mac.sh`:
|
||||||
|
- Create `vendor/hunyuan3d-mlx/.venv` (python3.11, same as trellis-mac).
|
||||||
|
- `pip install mlx mlx-arsenal safetensors Pillow trimesh scikit-image PyMCubes scipy huggingface_hub xatlas opencv-python` (README's list) **plus** `pymeshlab` (needed for `use_remesh=True` — see `textureGenPipeline_mlx.py:39-41`, it soft-fails to None without it).
|
||||||
|
- NOTE: Stage 2's `Hunyuan3DPaintConfigMLX` references torch-era paths (`device="cuda"`, `multiview_pretrained_path`) but with `use_mlx_diffusion=True` (the default) the PyTorch multiview model is skipped — verify at runtime whether `torch` is still imported anywhere on the MLX path before adding it to the venv. Prefer NOT installing torch if the MLX path doesn't need it.
|
||||||
|
- Verify: `.venv/bin/python -c "import mlx.core as mx; print(mx.default_device())"`.
|
||||||
|
4. **Env guards:** reuse the `HF_HUB_DISABLE_XET=1` guard from `trellis_mac/run.py` (xet downloader breaks on some repos); the OMP guards likely unneeded without torch but harmless.
|
||||||
|
|
||||||
|
## 3. Phase B — CLI wrapper + operator
|
||||||
|
|
||||||
|
The upstream repo has no single-command CLI for the full chain — `tests/test_stage1_to_stage2.py` is the reference implementation of image→textured-GLB. Build:
|
||||||
|
|
||||||
|
1. **In the fork** (so it's upstreamable): `generate_e2e.py` at repo root —
|
||||||
|
`python generate_e2e.py input.png --output out_stem [--steps 50] [--guidance 7.5] [--octree-resolution 256] [--max-num-view 6] [--view-resolution 512] [--texture-size 2048|4096] [--seed 42] [--no-texture] [--remesh-faces 40000]`
|
||||||
|
- Stage 1: `ShapePipeline.from_pretrained(...)`, export `<stem>_shape.glb`.
|
||||||
|
- If `--no-texture`, stop there. Else Stage 2 per the test file: remesh → `Hunyuan3DPaintPipelineMLX` → `<stem>.glb`.
|
||||||
|
- Print stage timings and peak memory (`mx.get_peak_memory()`) — we want these in job logs.
|
||||||
|
2. **Operator** `server/operators/hunyuan3d_mlx/` mirroring `trellis_mac` exactly:
|
||||||
|
- `manifest.json`: id `hunyuan3d_mlx`, name "Hunyuan3D 2.1 (local MLX)", category `mesh-gen`, accepts `["image"]`, produces `["model"]`, resources `"gpu"`, python `vendor/hunyuan3d-mlx/.venv/bin/python`. Params: `steps` (default 50), `guidance_scale` (7.5), `octree_resolution` (enum 256/384, default 256), `texture_size` (enum 1024/2048/4096, default 2048 until Phase D proves 4096), `max_num_view` (default 6), `seed`, `no_texture`.
|
||||||
|
- `run.py`: copy `trellis_mac/run.py` shape — subprocess `generate_e2e.py`, cwd vendor dir, collect `*.glb` (prefer the textured one over `*_shape.glb`), write `result.json` with `{"outputs": [...], "meta": {"tool": "hunyuan3d_mlx"}}`.
|
||||||
|
3. **Smoke test on scratch DB** (per repo rule: never test against the running gens DB): `./mb run hunyuan3d_mlx --file <test image> --wait --download out/`. First run downloads ~15GB — job log shows progress, don't kill it (AGENTS.md warns 10-30+ min).
|
||||||
|
4. Best-practice chain: `fal_bg_remove` (or local `bg_remove_local`) → cutout → `hunyuan3d_mlx`, same as the fal chain in AGENTS.md.
|
||||||
|
|
||||||
|
## 4. Phase C — M1 Ultra node + benchmarks
|
||||||
|
|
||||||
|
1. Install on the M1 node (`http://100.91.239.7:8777`, see AGENTS.md) — same scripts; MLX is architecture-native on M1, no bf16 concern. Mind the unified worker pool (commit `b8c333a`): check how the gpu lane discovers operators per node in `server/runner.py` / `server/remote.py` before assuming both nodes can serve the op.
|
||||||
|
2. **Benchmark protocol** — same cutout image through all three, one at a time (gpu lane serializes anyway):
|
||||||
|
- `trellis_mac` (M3, defaults: 1024 pipeline, 2048 texture)
|
||||||
|
- `hunyuan3d_mlx` on M3 AND on M1 (fp16, defaults)
|
||||||
|
- `fal_hunyuan3d_v21` (single-image mode — multi-view is broken on fal, verified 2026-07)
|
||||||
|
Record per run: wall time per stage, peak memory, GLB size, vert/face count, and a viewer screenshot. Subjective quality notes: geometry coherence, texture seams, PBR plausibility.
|
||||||
|
3. **Record in BENCHMARKS.md** following the existing table format, and update the local-verdict line if Hunyuan changes the recommendation (e.g. "trellis_mac for geometry, hunyuan3d_mlx for textures"). Add the operator row to README.md's table and AGENTS.md's mesh-gen row.
|
||||||
|
|
||||||
|
## 5. Phase D (stretch) — Studio-class fork improvements
|
||||||
|
|
||||||
|
Work on the fork's `modelbeast` branch; each item is independently shippable and upstreamable as a PR to dgrauet:
|
||||||
|
|
||||||
|
1. **4096² textures.** First just run `--texture-size 4096` on the M3 Ultra untiled — the command-buffer watchdog may not fire on Studio GPUs. If it fires: patch `uv_feature_map()` (`mesh_render_mlx.py:728`) to route through the existing `_rasterize_tiled()` using the same ≥3072/≥4096 tile thresholds as `extract_textiles()` (line 647). Grep for any other `_rasterize(..., self.texture_size)` call. Validate: tiled output must equal untiled at 2048 (the codebase already did this for `extract_textiles`; reuse that method). Compare 2048 vs 4096 bakes on the same mesh — if 4096 wins visibly, flip the operator default on the M3.
|
||||||
|
2. **Bigger bake mesh.** Raise the remesh decimation target from ~40k to 100-200k faces (flag already exposed via `--remesh-faces`); check bake time scaling and whether the rasterizer holds.
|
||||||
|
3. **Stage 1 detail:** `octree_resolution=384` A/B vs 256 on a detail-heavy cutout.
|
||||||
|
4. **More views:** `max_num_view=8-12` A/B — the candidate camera ring (30° azimuth steps at ±20° elevation, `textureGenPipeline_mlx.py:89-100`) already supports it. Watch for texture-blend regressions, not just wall time.
|
||||||
|
5. Log every result in BENCHMARKS.md; if any patch is a clear win, open the upstream PR (small, focused, with the validation evidence — the author clearly values PT-parity proofs).
|
||||||
|
|
||||||
|
## 6. Guardrails
|
||||||
|
|
||||||
|
- Never test against the production DB with running generations — scratch DB only (repo rule).
|
||||||
|
- gpu lane = ONE job at a time; don't parallelize benchmarks on a node.
|
||||||
|
- First-run weight downloads are long; don't cancel "stuck" jobs that are downloading.
|
||||||
|
- The reference image must match the mesh content (upstream caveat) — for e2e runs this is automatic (same image drives both stages).
|
||||||
|
- If Stage 2 quality disappoints out of the box, benchmark Stage 1 + `--no-texture` anyway: Hunyuan shape + TRELLIS texture routing is a valid future pipeline.
|
||||||
|
- Ask the owner before: forking under an org account, pushing PRs upstream, or changing existing operator defaults.
|
||||||
@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
Local-first web app that turns videos, images, and 3D files into meshes, splats, mocap, and rigged characters on the M3 Ultra. See [PLAN.md](PLAN.md) for the full verified tool matrix and roadmap, and [HANDOFF.md](HANDOFF.md) for the agent build brief (phases 1–4 instructions).
|
Local-first web app that turns videos, images, and 3D files into meshes, splats, mocap, and rigged characters on the M3 Ultra. See [PLAN.md](PLAN.md) for the full verified tool matrix and roadmap, and [HANDOFF.md](HANDOFF.md) for the agent build brief (phases 1–4 instructions).
|
||||||
|
|
||||||
|
> 🖥️ **Multi-Mac fleet? Read [CLUSTER.md](CLUSTER.md) first** — the local-first policy, the M3/M1/M4 node roles, and how the central queue routes jobs across machines.
|
||||||
|
|
||||||
## Run
|
## Run
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@ -55,6 +57,7 @@ Current operators:
|
|||||||
| `brush_train` | gpu | colmap dataset → 3D gaussian splat (Brush, native Metal) |
|
| `brush_train` | gpu | colmap dataset → 3D gaussian splat (Brush, native Metal) |
|
||||||
| `sf3d` | gpu | image → GLB locally (Stable-Fast-3D, MPS) *[HF-gated]* |
|
| `sf3d` | gpu | image → GLB locally (Stable-Fast-3D, MPS) *[HF-gated]* |
|
||||||
| `trellis_mac` | gpu | image → GLB+PBR locally (TRELLIS.2 MPS port) *[HF-gated]* |
|
| `trellis_mac` | gpu | image → GLB+PBR locally (TRELLIS.2 MPS port) *[HF-gated]* |
|
||||||
|
| `hunyuan3d_mlx` | gpu | image → GLB+PBR locally (Hunyuan3D 2.1, native MLX — runs on M1 too; weights public, no HF login) |
|
||||||
| `fal_trellis` / `fal_trellis2` / `fal_hunyuan3d` / `fal_hunyuan3d_v21` / `fal_rodin` | net | image → mesh via fal.ai API *[needs FAL_KEY]* |
|
| `fal_trellis` / `fal_trellis2` / `fal_hunyuan3d` / `fal_hunyuan3d_v21` / `fal_rodin` | net | image → mesh via fal.ai API *[needs FAL_KEY]* |
|
||||||
| `fal_bg_remove` | net | image → subject cutout (BiRefNet v2) — run before any image→3D for a big quality jump |
|
| `fal_bg_remove` | net | image → subject cutout (BiRefNet v2) — run before any image→3D for a big quality jump |
|
||||||
| `fal_upscale` | net | image → faithful upscale (SeedVR) |
|
| `fal_upscale` | net | image → faithful upscale (SeedVR) |
|
||||||
|
|||||||
56
scripts/install_hunyuan3d_mlx.sh
Executable file
56
scripts/install_hunyuan3d_mlx.sh
Executable file
@ -0,0 +1,56 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# hunyuan3d-mlx — Hunyuan3D 2.1 native MLX port (local image→3D, both stages).
|
||||||
|
# Unlike trellis-mac there is no upstream setup.sh, so we build the venv here.
|
||||||
|
# The MLX path is HYBRID: MLX for the diffusion UNet/VAE/rasterizer/super-res,
|
||||||
|
# but PyTorch(+transformers) for DINOv2-giant image encoding — so torch is
|
||||||
|
# required, but NOT the CUDA-era requirements.txt (no basicsr/realesrgan/
|
||||||
|
# lightning/diffusers). bpy is optional (trimesh GLB fallback exists).
|
||||||
|
# MLX weights (dgrauet/hunyuan3d-2.1-mlx, ~15GB) + dinov2-giant (~4.5GB) are
|
||||||
|
# public and download at runtime on first generate.
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
VENDOR="vendor/hunyuan3d-mlx"
|
||||||
|
|
||||||
|
if [ ! -d "$VENDOR" ]; then
|
||||||
|
echo "[hy3d] ERROR: $VENDOR missing. Vendor the repo there first."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
cd "$VENDOR"
|
||||||
|
|
||||||
|
echo "[hy3d] creating uv venv (python 3.11, matches trellis-mac) ..."
|
||||||
|
[ -x .venv/bin/python ] || uv venv --python 3.11 .venv
|
||||||
|
# shellcheck disable=SC1091
|
||||||
|
source .venv/bin/activate
|
||||||
|
|
||||||
|
echo "[hy3d] installing MLX-path dependencies (torch is MPS/CPU, no CUDA) ..."
|
||||||
|
uv pip install \
|
||||||
|
mlx mlx-arsenal \
|
||||||
|
torch torchvision \
|
||||||
|
transformers timm \
|
||||||
|
huggingface_hub safetensors \
|
||||||
|
numpy scipy scikit-image PyMCubes \
|
||||||
|
trimesh pygltflib xatlas \
|
||||||
|
opencv-python pillow \
|
||||||
|
pymeshlab \
|
||||||
|
einops omegaconf pyyaml tqdm torchdiffeq \
|
||||||
|
diffusers accelerate \
|
||||||
|
fast_simplification # trimesh 4.12 delegates simplify_quadric_decimation to this (Stage 2 remesh)
|
||||||
|
|
||||||
|
echo "[hy3d] import probe — the authoritative check of the real MLX entry points ..."
|
||||||
|
export HF_HUB_DISABLE_XET=1
|
||||||
|
python - <<'PY' || { echo "[hy3d] PROBE FAILED — see ModuleNotFoundError above; add the package to this script and re-run."; exit 2; }
|
||||||
|
import sys, os
|
||||||
|
sys.path.insert(0, "hy3dshape")
|
||||||
|
sys.path.insert(0, "hy3dpaint")
|
||||||
|
import mlx.core as mx
|
||||||
|
print("[probe] mlx device:", mx.default_device())
|
||||||
|
import torch
|
||||||
|
print("[probe] torch:", torch.__version__, "mps:", torch.backends.mps.is_available())
|
||||||
|
from hy3dshape.pipeline_mlx import ShapePipeline
|
||||||
|
print("[probe] Stage 1 ShapePipeline import OK")
|
||||||
|
from textureGenPipeline_mlx import Hunyuan3DPaintConfigMLX, Hunyuan3DPaintPipelineMLX
|
||||||
|
print("[probe] Stage 2 Hunyuan3DPaintPipelineMLX import OK")
|
||||||
|
print("[probe] ALL IMPORTS OK")
|
||||||
|
PY
|
||||||
|
|
||||||
|
echo "[hy3d] done — venv ready. First generate downloads ~15GB MLX weights + dinov2-giant."
|
||||||
23
server/operators/hunyuan3d_mlx/manifest.json
Normal file
23
server/operators/hunyuan3d_mlx/manifest.json
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"id": "hunyuan3d_mlx",
|
||||||
|
"name": "Hunyuan3D 2.1 (local MLX)",
|
||||||
|
"category": "mesh-gen",
|
||||||
|
"description": "Image → GLB + baked PBR locally via the native-MLX Hunyuan3D 2.1 port (both shape + PBR-texture stages on Apple Silicon; runs on M1 too, MLX-native). Weights (~15GB, dgrauet/hunyuan3d-2.1-mlx) auto-download on first run. Best paired with a bg-removed cutout. texture_size 4096 is experimental (verify on the M3 before defaulting).",
|
||||||
|
"accepts": ["image"],
|
||||||
|
"produces": ["model"],
|
||||||
|
"resources": "gpu",
|
||||||
|
"entry": "run.py",
|
||||||
|
"python": "vendor/hunyuan3d-mlx/.venv/bin/python",
|
||||||
|
"params_schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"steps": {"type": "integer", "default": 50, "description": "Stage 1 shape denoising steps"},
|
||||||
|
"guidance_scale": {"type": "number", "default": 7.5, "description": "Stage 1 CFG scale"},
|
||||||
|
"octree_resolution": {"type": "integer", "enum": [256, 384], "default": 256, "description": "Stage 1 marching-cubes grid (384 = finer geometry, slower)"},
|
||||||
|
"texture_size": {"type": "integer", "enum": [1024, 2048, 4096], "default": 2048, "description": "Stage 2 baked UV atlas resolution"},
|
||||||
|
"max_num_view": {"type": "integer", "default": 6, "description": "Stage 2 number of baked views"},
|
||||||
|
"seed": {"type": "integer", "default": 42, "description": "Random seed"},
|
||||||
|
"no_texture": {"type": "boolean", "default": false, "description": "Geometry only (skip Stage 2, much faster)"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
71
server/operators/hunyuan3d_mlx/run.py
Normal file
71
server/operators/hunyuan3d_mlx/run.py
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[3]
|
||||||
|
VENDOR = ROOT / "vendor" / "hunyuan3d-mlx"
|
||||||
|
|
||||||
|
ap = argparse.ArgumentParser()
|
||||||
|
ap.add_argument("--input", action="append", default=[])
|
||||||
|
ap.add_argument("--outdir", required=True)
|
||||||
|
ap.add_argument("--params", default="{}")
|
||||||
|
a = ap.parse_args()
|
||||||
|
p = json.loads(a.params)
|
||||||
|
|
||||||
|
if not a.input:
|
||||||
|
print("ERROR: no input image")
|
||||||
|
sys.exit(1)
|
||||||
|
if not (VENDOR / "generate_e2e.py").exists():
|
||||||
|
print(f"ERROR: hunyuan3d-mlx not installed at {VENDOR}. Run scripts/install_hunyuan3d_mlx.sh")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
outdir = Path(a.outdir)
|
||||||
|
out_stem = outdir / f"hunyuan3d_{Path(a.input[0]).stem}"
|
||||||
|
|
||||||
|
cmd = [
|
||||||
|
sys.executable, "generate_e2e.py", str(Path(a.input[0]).resolve()),
|
||||||
|
"--output", str(out_stem.resolve()),
|
||||||
|
"--steps", str(p.get("steps", 50)),
|
||||||
|
"--guidance", str(p.get("guidance_scale", 7.5)),
|
||||||
|
"--octree-resolution", str(p.get("octree_resolution", 256)),
|
||||||
|
"--texture-size", str(p.get("texture_size", 2048)),
|
||||||
|
"--max-num-view", str(p.get("max_num_view", 6)),
|
||||||
|
"--seed", str(p.get("seed", 42)),
|
||||||
|
]
|
||||||
|
if p.get("no_texture"):
|
||||||
|
cmd.append("--no-texture")
|
||||||
|
|
||||||
|
env = os.environ.copy()
|
||||||
|
# xet chunked downloader fails on some HF repos — force the reliable HTTP path
|
||||||
|
# (same fix as trellis_mac / flux_local).
|
||||||
|
env["HF_HUB_DISABLE_XET"] = "1"
|
||||||
|
# guard the torch + compiled-kernel OpenMP collision like sf3d / trellis_mac.
|
||||||
|
env["KMP_DUPLICATE_LIB_OK"] = "TRUE"
|
||||||
|
env.setdefault("OMP_NUM_THREADS", "1")
|
||||||
|
env.setdefault("MKL_NUM_THREADS", "1")
|
||||||
|
|
||||||
|
print("+", " ".join(cmd), flush=True)
|
||||||
|
print("(first run downloads ~15GB MLX weights: dgrauet/hunyuan3d-2.1-mlx)", flush=True)
|
||||||
|
res = subprocess.run(cmd, cwd=str(VENDOR), stdout=sys.stdout, stderr=subprocess.STDOUT, env=env)
|
||||||
|
if res.returncode != 0:
|
||||||
|
sys.exit(res.returncode)
|
||||||
|
|
||||||
|
# generate_e2e.py writes <stem>.glb (textured, or geometry if --no-texture) and
|
||||||
|
# optionally <stem>_shape.glb. Prefer the primary <stem>.glb.
|
||||||
|
final = out_stem.with_suffix(".glb")
|
||||||
|
if final.exists():
|
||||||
|
chosen = final
|
||||||
|
else:
|
||||||
|
glbs = sorted(outdir.rglob("*.glb"))
|
||||||
|
if not glbs:
|
||||||
|
print("ERROR: hunyuan3d_mlx produced no .glb")
|
||||||
|
sys.exit(1)
|
||||||
|
chosen = glbs[0]
|
||||||
|
|
||||||
|
outputs = [{"path": str(chosen), "name": f"hunyuan3d_{Path(a.input[0]).stem}.glb",
|
||||||
|
"meta": {"tool": "hunyuan3d_mlx"}}]
|
||||||
|
(outdir / "result.json").write_text(json.dumps({"outputs": outputs}))
|
||||||
|
print("done:", chosen, flush=True)
|
||||||
Loading…
Reference in New Issue
Block a user