[diffusion] CI: restore Hunyuan3D-2 image-to-3D (#28781)

This commit is contained in:
Mick
2026-06-22 23:36:18 +08:00
committed by GitHub
parent b28e990161
commit bbe8b7dd8a
5 changed files with 91 additions and 19 deletions
@@ -381,15 +381,22 @@ class Hunyuan3DShapeDenoisingStage(DenoisingStage):
attn_metadata,
target_dtype,
current_guidance_scale,
image_kwargs: dict[str, Any],
pos_cond_kwargs: dict[str, Any],
neg_cond_kwargs: dict[str, Any],
cfg_policy,
cfg_gate_state,
server_args,
guidance,
latents,
):
"""Hunyuan3D-specific CFG: concat latents, single forward, then split."""
cond = pos_cond_kwargs.get("encoder_hidden_states")
"""Hunyuan3D-specific CFG: concat latents, single forward, then split.
Hunyuan3D pre-stacks ``[uncond, cond]`` in ``prompt_embeds`` and runs a
single batched forward, combining manually. It therefore does not use the
shared multi-branch ``cfg_policy`` machinery; ``cfg_policy`` and
``cfg_gate_state`` are accepted only to match the base
:meth:`DenoisingStage._predict_noise_with_cfg` signature (the base loop
always passes them) and are intentionally unused here.
"""
cond = batch.prompt_embeds[0] if batch.prompt_embeds else None
do_cfg = batch.do_classifier_free_guidance
if do_cfg:
@@ -2115,10 +2115,7 @@
"Hunyuan3DShapeBeforeDenoisingStage": 544.59,
"Hunyuan3DShapeDenoisingStage": 3306.16,
"Hunyuan3DShapeExportStage": 8488.42,
"Hunyuan3DShapeSaveStage": 859.23,
"Hunyuan3DPaintPreprocessStage": 256020.36,
"Hunyuan3DPaintTexGenStage": 23764.05,
"Hunyuan3DPaintPostprocessStage": 7095.01
"Hunyuan3DShapeSaveStage": 859.23
},
"denoise_step_ms": {
"0": 137.54,
@@ -138,6 +138,22 @@ def diffusion_server(case: DiffusionTestCase) -> ServerContext:
# picking another one (which causes the test client to connect to the wrong server).
extra_args += " --strict-ports"
# Shape-only mesh cases (e.g. hunyuan3d_shape_gen) validate geometry via
# mesh-correctness and must NOT run the paint/texture stages, whose
# verification checks texture artifacts (paint_mesh/normal_maps/renderer)
# that the shape-only path never produces. Inject a pipeline-config override
# disabling paint for these cases.
if server_args.custom_validator == "mesh":
import json as _json
import tempfile as _tempfile
_paint_off_cfg = os.path.join(
_tempfile.gettempdir(), f"{case.id}_paint_off.json"
)
with open(_paint_off_cfg, "w") as _f:
_json.dump({"paint_enable": False}, _f)
extra_args += f" --config {_paint_off_cfg}"
for arg in server_args.extras:
extra_args += f" {arg}"
@@ -732,24 +732,60 @@ class MeshValidator(PerformanceValidator):
pass
# Pinned to a ci-data commit (not main): invalidates the per-URL download cache
# whenever the reference is regenerated, and keeps the mesh GT reproducible.
# Bump this SHA when pushing a new hunyuan3d.glb to ci-data.
HUNYUAN3D_REFERENCE_URL = (
"https://raw.githubusercontent.com/sgl-project/sgl-test-files/"
"main/diffusion-ci/consistency_gt/1-gpu/hunyuan3d_2_0/hunyuan3d.glb"
"https://raw.githubusercontent.com/sgl-project/ci-data/"
"395f6e49c37d22a57d79fbcd3653d43984099ae2"
"/diffusion-ci/consistency_gt/1-gpu/hunyuan3d_2_0/hunyuan3d.glb"
)
def _download_reference_mesh(url: str) -> Path:
"""Download a reference mesh from URL, caching in temp dir."""
"""Download a reference mesh from URL, caching in temp dir.
Validates that the cached/downloaded file actually *loads* as a non-empty
mesh — not just that a magic/length header looks right. raw.githubusercontent
can briefly serve a truncated or corrupt response for a just-pushed large
file, and a prior run may have cached those bytes on a persistent runner; a
size/magic check can't catch a blob whose byte count matches the declared
length but whose body is corrupt (exactly what poisoned this CI cache and
surfaced as a cryptic trimesh "incorrect header on GLB file" deep inside
validation). Loading via trimesh rejects any such cache (forcing a
re-download) and turns a bad fresh download into a clear error. The ``v2``
cache prefix also invalidates blobs written by the earlier, weaker checks.
"""
import hashlib
cache_name = f"ref_mesh_{hashlib.md5(url.encode()).hexdigest()}.glb"
cache_name = f"ref_mesh_v2_{hashlib.md5(url.encode()).hexdigest()}.glb"
cache_path = Path(tempfile.gettempdir()) / cache_name
if cache_path.exists():
def _loads_as_mesh(path: Path) -> bool:
try:
import trimesh
mesh = trimesh.load(str(path), force="mesh")
return (
getattr(mesh, "vertices", None) is not None and len(mesh.vertices) > 0
)
except Exception:
return False
if cache_path.exists() and _loads_as_mesh(cache_path):
logger.info(f"Using cached reference mesh: {cache_path}")
return cache_path
logger.info(f"Downloading reference mesh from: {url}")
cache_path.write_bytes(_urlopen_with_retry(url, timeout=60))
if not _loads_as_mesh(cache_path):
size = cache_path.stat().st_size if cache_path.exists() else 0
cache_path.unlink(missing_ok=True)
raise RuntimeError(
f"Reference mesh from {url} did not load as a valid mesh "
f"({size} bytes). The CDN may not have propagated a recently-pushed "
f"file yet; retry shortly."
)
logger.info(f"Reference mesh cached at: {cache_path}")
return cache_path
@@ -1446,8 +1482,15 @@ def get_generate_fn(
if content_resp.status_code != 200:
pytest.fail(f"{case_id}: mesh download failed: {content_resp.text}")
temp_path = Path(tempfile.gettempdir()) / f"mesh_test_{mesh_id}.glb"
temp_path.write_bytes(content_resp.content)
content = content_resp.content
# Shape-only Hunyuan3D meshes are returned as OBJ, painted meshes
# as GLB. Pick the extension from the content magic so trimesh.load
# (which dispatches on the file extension) parses it correctly,
# instead of raising "incorrect header on GLB file" when an OBJ
# body is saved under a .glb name.
ext = ".glb" if content[:4] == b"glTF" else ".obj"
temp_path = Path(tempfile.gettempdir()) / f"mesh_test_{mesh_id}{ext}"
temp_path.write_bytes(content)
MESH_OUTPUT_PATHS[case_id] = str(temp_path)
logger.info(f"[Mesh Gen] Mesh downloaded to {temp_path}")