[diffusion] CI: restore Hunyuan3D-2 image-to-3D (#28781)
This commit is contained in:
+12
-5
@@ -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}")
|
||||
|
||||
Reference in New Issue
Block a user