[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, attn_metadata,
target_dtype, target_dtype,
current_guidance_scale, current_guidance_scale,
image_kwargs: dict[str, Any], cfg_policy,
pos_cond_kwargs: dict[str, Any], cfg_gate_state,
neg_cond_kwargs: dict[str, Any],
server_args, server_args,
guidance, guidance,
latents, latents,
): ):
"""Hunyuan3D-specific CFG: concat latents, single forward, then split.""" """Hunyuan3D-specific CFG: concat latents, single forward, then split.
cond = pos_cond_kwargs.get("encoder_hidden_states")
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 do_cfg = batch.do_classifier_free_guidance
if do_cfg: if do_cfg:
@@ -2115,10 +2115,7 @@
"Hunyuan3DShapeBeforeDenoisingStage": 544.59, "Hunyuan3DShapeBeforeDenoisingStage": 544.59,
"Hunyuan3DShapeDenoisingStage": 3306.16, "Hunyuan3DShapeDenoisingStage": 3306.16,
"Hunyuan3DShapeExportStage": 8488.42, "Hunyuan3DShapeExportStage": 8488.42,
"Hunyuan3DShapeSaveStage": 859.23, "Hunyuan3DShapeSaveStage": 859.23
"Hunyuan3DPaintPreprocessStage": 256020.36,
"Hunyuan3DPaintTexGenStage": 23764.05,
"Hunyuan3DPaintPostprocessStage": 7095.01
}, },
"denoise_step_ms": { "denoise_step_ms": {
"0": 137.54, "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). # picking another one (which causes the test client to connect to the wrong server).
extra_args += " --strict-ports" 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: for arg in server_args.extras:
extra_args += f" {arg}" extra_args += f" {arg}"
@@ -732,24 +732,60 @@ class MeshValidator(PerformanceValidator):
pass 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 = ( HUNYUAN3D_REFERENCE_URL = (
"https://raw.githubusercontent.com/sgl-project/sgl-test-files/" "https://raw.githubusercontent.com/sgl-project/ci-data/"
"main/diffusion-ci/consistency_gt/1-gpu/hunyuan3d_2_0/hunyuan3d.glb" "395f6e49c37d22a57d79fbcd3653d43984099ae2"
"/diffusion-ci/consistency_gt/1-gpu/hunyuan3d_2_0/hunyuan3d.glb"
) )
def _download_reference_mesh(url: str) -> Path: 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 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 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}") logger.info(f"Using cached reference mesh: {cache_path}")
return cache_path return cache_path
logger.info(f"Downloading reference mesh from: {url}") logger.info(f"Downloading reference mesh from: {url}")
cache_path.write_bytes(_urlopen_with_retry(url, timeout=60)) 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}") logger.info(f"Reference mesh cached at: {cache_path}")
return cache_path return cache_path
@@ -1446,8 +1482,15 @@ def get_generate_fn(
if content_resp.status_code != 200: if content_resp.status_code != 200:
pytest.fail(f"{case_id}: mesh download failed: {content_resp.text}") pytest.fail(f"{case_id}: mesh download failed: {content_resp.text}")
temp_path = Path(tempfile.gettempdir()) / f"mesh_test_{mesh_id}.glb" content = content_resp.content
temp_path.write_bytes(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) MESH_OUTPUT_PATHS[case_id] = str(temp_path)
logger.info(f"[Mesh Gen] Mesh downloaded to {temp_path}") logger.info(f"[Mesh Gen] Mesh downloaded to {temp_path}")
@@ -102,10 +102,19 @@ class DiffusionTestCaseVisitor(ast.NodeVisitor):
if case_id: if case_id:
self.factory_case_ids[stmt.name] = case_id self.factory_case_ids[stmt.name] = case_id
for stmt in node.body: self.generic_visit(node)
if isinstance(stmt, ast.Expr):
self._process_expr(stmt.value)
def visit_Expr(self, node: ast.Expr):
"""Handle ``LIST.append(...)`` mutations at any nesting level.
Previously only module-top-level ``ast.Expr`` statements were scanned for
``.append()`` calls, so cases registered under a platform guard such as
``if not current_platform.is_hip(): ONE_GPU_CASES.append(...)`` (used by
``hunyuan3d_shape_gen`` and ``turbo_wan2_1_t2v_1.3b``) were invisible to
the partition planner and therefore never scheduled in CI. Visiting every
``Expr`` lets ``generic_visit`` reach appends inside ``if``/``else`` blocks.
"""
self._process_expr(node.value)
self.generic_visit(node) self.generic_visit(node)
def visit_Assign(self, node: ast.Assign): def visit_Assign(self, node: ast.Assign):