From 2d29f10ca8618e3f5a7ca601072ec054787f77f5 Mon Sep 17 00:00:00 2001 From: BP <11394934+benjipeng@users.noreply.github.com> Date: Fri, 14 Aug 2026 16:49:18 -0400 Subject: [PATCH] Make golden fixtures fast and self-contained --- docs/development.md | 42 +++- .../pixal3d/write_derived_golden_fixture.py | 223 ++++++++++++++++++ tests/data/pixal3d_derived_golden/golden.json | 131 ++++++++++ .../shape_decoder_fields.npz | Bin 0 -> 34107 bytes .../texture_decoder_pbr.npz | Bin 0 -> 28004 bytes tests/data/trellis2_miniature_golden.json | 88 +++++-- tests/golden_assertions.py | 143 +++++++++++ tests/test_golden_assertions.py | 38 +++ tests/test_pixal3d_derived_golden.py | 96 ++++++++ tests/test_trellis2_golden_fixture.py | 27 ++- tests/trellis2_golden_fixture.py | 109 ++++----- 11 files changed, 807 insertions(+), 90 deletions(-) create mode 100644 scripts/pixal3d/write_derived_golden_fixture.py create mode 100644 tests/data/pixal3d_derived_golden/golden.json create mode 100644 tests/data/pixal3d_derived_golden/shape_decoder_fields.npz create mode 100644 tests/data/pixal3d_derived_golden/texture_decoder_pbr.npz create mode 100644 tests/golden_assertions.py create mode 100644 tests/test_golden_assertions.py create mode 100644 tests/test_pixal3d_derived_golden.py diff --git a/docs/development.md b/docs/development.md index d24420c..5d11664 100644 --- a/docs/development.md +++ b/docs/development.md @@ -77,13 +77,20 @@ user-requested inference results, not test or audit scratch data. Preserve the temporary root only when its artifacts are needed for diagnosis; otherwise remove it after recording the relevant result. -## TRELLIS.2 Miniature Golden Fixture +## Model-Independent Golden Fixtures + +Golden coverage uses two complementary fixture types. Synthetic miniature +checkpoints exercise complete inference orchestration. Real-weight-derived +decoder patches preserve a reviewed real-model boundary without committing or +loading the source checkpoints. + +### TRELLIS.2 Synthetic Miniature The TRELLIS.2 golden test does not read `weights/` or download model assets. It generates a miniature source checkpoint, applies the production selective INT8 quantizer, and runs image conditioning, sparse sampling, shape and texture SLat -sampling, both decoders, artifact serialization, and GLB export without stage -mocking. +sampling, both decoders, artifact serialization, and the real SpatialKit export +path with miniature export settings. ```bash uv run pytest -m heavy tests/test_trellis2_golden_fixture.py -q @@ -94,6 +101,35 @@ Reviewed tensor and GLB expectations live in automatically during tests. Rebaseline it only after reviewing an intentional inference-contract change. +### Pixal3D Real-Weight-Derived Decoder Patch + +`tests/data/pixal3d_derived_golden/` contains a 16-cubed spatial patch captured +from an official-sample run of `TencentARC/Pixal3D` revision +`0b31f9160aa400719af409098bff7936a932f726`. The source used unquantized BF16 +flow and FP16 decoder checkpoints. The committed patch is under 64 KiB and +replays decoded O-Voxel validation, mesh extraction, remeshing, MLX QEM, UV, +texture baking, and GLB writing without the 22 GB source bundle. + +```bash +uv run pytest tests/test_pixal3d_derived_golden.py -q +uv run pytest -m heavy tests/test_pixal3d_derived_golden.py -q +``` + +The default test verifies provenance, checksums, and decoded contracts. The +Metal-backed replay is marked `heavy` and normally completes in under one +second. It does not replace the synthetic full-pipeline Pixal3D tests: the +derived fixture begins at the decoder-output boundary. + +Rebaseline only from a reviewed real inference result: + +```bash +uv run python scripts/pixal3d/write_derived_golden_fixture.py \ + outputs/pixal3d/real-smoke-moge-balanced-decoders1100k \ + outputs/pixal3d/real-smoke-moge-balanced-decoders1100k/trace.json \ + tests/data/pixal3d_derived_golden \ + --source-revision 0b31f9160aa400719af409098bff7936a932f726 +``` + ## Editing Constraints - Prefer existing module boundaries over new abstractions. diff --git a/scripts/pixal3d/write_derived_golden_fixture.py b/scripts/pixal3d/write_derived_golden_fixture.py new file mode 100644 index 0000000..0575933 --- /dev/null +++ b/scripts/pixal3d/write_derived_golden_fixture.py @@ -0,0 +1,223 @@ +#!/usr/bin/env python3 +"""Write a compact real-weight-derived Pixal3D decoder/export fixture.""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import tempfile +from pathlib import Path +from typing import Any + +import numpy as np + + +SOURCE_REPOSITORY = "TencentARC/Pixal3D" +SHAPE_FILENAME = "shape_decoder_fields.npz" +TEXTURE_FILENAME = "texture_decoder_pbr.npz" +MANIFEST_FILENAME = "golden.json" + + +def main() -> int: + args = _parser().parse_args() + # Keep --help and argument validation usable without a Metal device. + from mlx_spatial.spatialkit import export_decoded_ovoxel_glb, inspect_glb + + source_dir = args.source_decoded_dir + output_dir = args.output_dir + trace = json.loads(args.source_trace.read_text(encoding="utf-8")) + + with np.load(source_dir / SHAPE_FILENAME, allow_pickle=False) as payload: + source_coordinates = np.asarray(payload["coordinates"]) + source_fields = np.asarray(payload["fields"]) + with np.load(source_dir / TEXTURE_FILENAME, allow_pickle=False) as payload: + texture_coordinates = np.asarray(payload["coordinates"]) + source_attributes = np.asarray(payload["attributes"]) + source_grid_size = int(payload["decode_resolution"].item()) + + if not np.array_equal(source_coordinates, texture_coordinates): + raise ValueError("source shape and texture coordinates differ") + + origin = np.asarray(args.origin, dtype=np.int32) + local_grid_size = int(args.local_grid_size) + spatial = source_coordinates[:, 1:] + mask = np.all((spatial >= origin) & (spatial < origin + local_grid_size), axis=1) + if not np.any(mask): + raise ValueError("selected local patch is empty") + + coordinates = np.ascontiguousarray(source_coordinates[mask]) + coordinates[:, 1:] -= origin + fields = np.ascontiguousarray(source_fields[mask]) + attributes = np.ascontiguousarray(source_attributes[mask]) + metadata = { + "fixture_kind": "real-weight-derived-decoder-patch", + "local_grid_size": local_grid_size, + "local_origin": origin.tolist(), + "model_family": "pixal3d", + "source_coordinate_system": "gltf-y-up", + "source_grid_size": source_grid_size, + "source_repository": SOURCE_REPOSITORY, + "source_revision": args.source_revision, + } + + output_dir.mkdir(parents=True, exist_ok=True) + shape_path = output_dir / SHAPE_FILENAME + texture_path = output_dir / TEXTURE_FILENAME + metadata_json = np.asarray(json.dumps(metadata, sort_keys=True)) + np.savez_compressed( + shape_path, + coordinates=coordinates, + fields=fields, + metadata_json=metadata_json, + ) + np.savez_compressed( + texture_path, + coordinates=coordinates, + attributes=attributes, + spatial_shape=np.asarray([local_grid_size] * 3, dtype=np.int32), + batch_size=np.asarray(1, dtype=np.int32), + decode_resolution=np.asarray(local_grid_size, dtype=np.int32), + voxel_size=np.asarray(1.0 / local_grid_size, dtype=np.float32), + metadata_json=metadata_json, + ) + + export_settings = { + "grid_size": local_grid_size, + "quality_preset": "reference-target", + "remesh": True, + "remesh_resolution": local_grid_size, + "simplify_backend": "mlx-qem", + "target_faces": int(args.target_faces), + "texture_postprocess": "telea", + "texture_size": int(args.texture_size), + "uv_backend": "xatlas-equivalent-native", + } + with tempfile.TemporaryDirectory(prefix="pixal3d-derived-golden.") as directory: + result = export_decoded_ovoxel_glb( + output_dir, + Path(directory) / "model.glb", + **export_settings, + ) + glb = inspect_glb(result.glb.path) + + source_input = Path(str(trace.get("image_path", ""))) + manifest = { + "schema_version": 1, + "fixture_kind": "real-weight-derived-decoder-patch", + "scope": { + "covered": "decoded O-Voxel contract through native textured GLB export", + "not_covered": ( + "Pixal3D checkpoint loading, conditioning, flow sampling, " + "and decoder execution" + ), + }, + "source": { + "repository": SOURCE_REPOSITORY, + "revision": args.source_revision, + "precision": "unquantized mixed BF16 flow and FP16 decoder checkpoints", + "quantization": "none", + "pipeline_type": trace.get("pipeline_type"), + "seed": trace.get("seed"), + "grid_size": source_grid_size, + "input_sha256": _sha256(source_input) if source_input.is_file() else None, + "trace_sha256": _sha256(args.source_trace), + "completed_stages": trace.get("completed_stages", []), + }, + "selection": { + "origin": origin.tolist(), + "grid_size": local_grid_size, + "source_token_count": int(source_coordinates.shape[0]), + "selected_token_count": int(coordinates.shape[0]), + }, + "files": { + SHAPE_FILENAME: { + "bytes": shape_path.stat().st_size, + "sha256": _sha256(shape_path), + }, + TEXTURE_FILENAME: { + "bytes": texture_path.stat().st_size, + "sha256": _sha256(texture_path), + }, + }, + "arrays": { + "coordinates": _array_summary(coordinates), + "fields": _array_summary(fields), + "attributes": _array_summary(attributes), + }, + "expected_export": { + "settings": export_settings, + "source_vertices": int( + result.diagnostics["stages"]["extract_mesh"]["source_vertices"] + ), + "source_faces": int( + result.diagnostics["stages"]["extract_mesh"]["source_faces"] + ), + "final_faces": int( + result.diagnostics["stages"]["simplify_mesh"]["stats"]["final_faces"] + ), + "glb": { + "meshes": int(glb["mesh_count"]), + "primitives": int(glb["primitive_count"]), + "materials": int(glb["material_count"]), + "textures": int(glb["texture_count"]), + "images": int(glb["image_count"]), + "vertices": int(glb["total_vertices"]), + "faces": int(glb["total_faces"]), + }, + }, + } + (output_dir / MANIFEST_FILENAME).write_text( + json.dumps(manifest, indent=2, sort_keys=True) + "\n", + encoding="utf-8", + ) + return 0 + + +def _array_summary(array: np.ndarray) -> dict[str, Any]: + summary: dict[str, Any] = {"shape": list(array.shape), "dtype": str(array.dtype)} + if np.issubdtype(array.dtype, np.integer): + summary["sha256"] = hashlib.sha256( + np.ascontiguousarray(array).tobytes() + ).hexdigest() + else: + values = np.asarray(array, dtype=np.float64) + summary["statistics"] = { + "min": float(values.min()), + "max": float(values.max()), + "mean": float(values.mean()), + "std": float(values.std()), + "l2": float(np.linalg.norm(values)), + } + return summary + + +def _sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as handle: + for chunk in iter(lambda: handle.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def _parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("source_decoded_dir", type=Path) + parser.add_argument("source_trace", type=Path) + parser.add_argument("output_dir", type=Path) + parser.add_argument("--source-revision", required=True) + parser.add_argument( + "--origin", + type=int, + nargs=3, + default=(224, 496, 688), + metavar=("Z", "Y", "X"), + ) + parser.add_argument("--local-grid-size", type=int, default=16) + parser.add_argument("--target-faces", type=int, default=512) + parser.add_argument("--texture-size", type=int, default=16) + return parser + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/data/pixal3d_derived_golden/golden.json b/tests/data/pixal3d_derived_golden/golden.json new file mode 100644 index 0000000..9a33eb9 --- /dev/null +++ b/tests/data/pixal3d_derived_golden/golden.json @@ -0,0 +1,131 @@ +{ + "arrays": { + "attributes": { + "dtype": "float32", + "shape": [ + 1166, + 6 + ], + "statistics": { + "l2": 54.5424308341348, + "max": 1.0007288455963135, + "mean": 0.5237411886598465, + "min": 0.012211322784423828, + "std": 0.3884849388377122 + } + }, + "coordinates": { + "dtype": "int32", + "sha256": "1e352c7b7638293edf65e86c4afec0dac2078a0137d5249bdbf8accd37e4750e", + "shape": [ + 1166, + 4 + ] + }, + "fields": { + "dtype": "float32", + "shape": [ + 1166, + 7 + ], + "statistics": { + "l2": 882.6670673262066, + "max": 19.59222412109375, + "mean": -3.7514042070301383, + "min": -61.986366271972656, + "std": 9.021178029521382 + } + } + }, + "expected_export": { + "final_faces": 512, + "glb": { + "faces": 512, + "images": 2, + "materials": 1, + "meshes": 1, + "primitives": 1, + "textures": 2, + "vertices": 442 + }, + "settings": { + "grid_size": 16, + "quality_preset": "reference-target", + "remesh": true, + "remesh_resolution": 16, + "simplify_backend": "mlx-qem", + "target_faces": 512, + "texture_postprocess": "telea", + "texture_size": 16, + "uv_backend": "xatlas-equivalent-native" + }, + "source_faces": 2130, + "source_vertices": 1166 + }, + "files": { + "shape_decoder_fields.npz": { + "bytes": 34107, + "sha256": "d9e2f2aa86cbee5efe844d3bf303c6bb2424c06be3b929f8bc66664e9e2c8e99" + }, + "texture_decoder_pbr.npz": { + "bytes": 28004, + "sha256": "81fd1ce2b17d63c79fc318b098fdf44698cbee20bc1d9e63daeb9a3c935e3ee2" + } + }, + "fixture_kind": "real-weight-derived-decoder-patch", + "schema_version": 1, + "scope": { + "covered": "decoded O-Voxel contract through native textured GLB export", + "not_covered": "Pixal3D checkpoint loading, conditioning, flow sampling, and decoder execution" + }, + "selection": { + "grid_size": 16, + "origin": [ + 224, + 496, + 688 + ], + "selected_token_count": 1166, + "source_token_count": 4150336 + }, + "source": { + "completed_stages": [ + "input-image", + "asset-validation", + "pipeline-config", + "camera-setup", + "image-conditioning", + "projection-conditioning:ss", + "artifact:sparse_projection", + "sparse-structure-flow", + "sparse-structure-decoding", + "artifact:sparse_structure", + "projection-conditioning:shape_512", + "shape-slat-sampling:512", + "artifact:shape_slat_lr", + "shape-slat-cascade:upsample", + "artifact:shape_slat_hr_coordinates", + "projection-conditioning:shape_1024", + "shape-slat-sampling:1024", + "artifact:shape_slat_hr", + "projection-conditioning:tex_1024", + "texture-slat-sampling:1024", + "artifact:texture_slat", + "shape-decoder", + "artifact:shape_decoder_fields", + "texture-decoder", + "artifact:texture_decoder_pbr", + "mesh-export", + "artifact:textured_glb" + ], + "grid_size": 1024, + "input_sha256": "6959e517ee4bc6852791f69bd6ece696a435abcda8321727c07db8daf7f457cf", + "pipeline_type": "1024_cascade", + "precision": "unquantized mixed BF16 flow and FP16 decoder checkpoints", + "quantization": "none", + "repository": "TencentARC/Pixal3D", + "revision": "0b31f9160aa400719af409098bff7936a932f726", + "seed": 42, + "trace_sha256": "b37e1dd38acbdf7190010e4d0d1147ac5c2191c8df6233e7f3132c699282a533" + } +} diff --git a/tests/data/pixal3d_derived_golden/shape_decoder_fields.npz b/tests/data/pixal3d_derived_golden/shape_decoder_fields.npz new file mode 100644 index 0000000000000000000000000000000000000000..e89855e0a9b59ef780fe3dab20dfcbc0b8845262 GIT binary patch literal 34107 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

P(b@f(+=yno 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 MAlw3^3tK=u0G@k@6aWAK literal 0 HcmV?d00001 diff --git a/tests/data/pixal3d_derived_golden/texture_decoder_pbr.npz b/tests/data/pixal3d_derived_golden/texture_decoder_pbr.npz new file mode 100644 index 0000000000000000000000000000000000000000..e5327a1327e97555a50a9eb3352e2b8c33cfa3f1 GIT binary patch literal 28004 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 literal 0 HcmV?d00001 diff --git a/tests/data/trellis2_miniature_golden.json b/tests/data/trellis2_miniature_golden.json index 4df5a8b..efaf299 100644 --- a/tests/data/trellis2_miniature_golden.json +++ b/tests/data/trellis2_miniature_golden.json @@ -1,4 +1,23 @@ { + "export": { + "effective": { + "grid_size": 32, + "target_faces": 64, + "texture_size": 16 + }, + "requested": { + "diagnostics_path": null, + "grid_size": 512, + "quality_preset": "reference-target", + "remesh": true, + "remesh_resolution": 512, + "simplify_backend": "mlx-qem", + "target_faces": 256, + "texture_postprocess": "telea", + "texture_size": 32, + "uv_backend": "xatlas-equivalent-native" + } + }, "fixture": { "checkpoint_source": "generated synthetic tensors", "glb_target_faces": 256, @@ -18,14 +37,13 @@ "has_normal": true, "has_texcoord_0": true, "material": 0, - "positions": 4565, - "triangles": 3912 + "positions": 12, + "triangles": 4 } ], - "sha256": "44b55e7184c56449d87a9bed214a7ab24460f9ad3bd159e92867cec67efa9470", "textures": 2 }, - "schema_version": 1, + "schema_version": 2, "trace": { "completed_stages": [ "asset-config-validation", @@ -44,59 +62,95 @@ "tensor_outputs": { "cond_512": { "dtype": "float32", - "sha256": "aad5ce8209c55c84c2e64b92b8162e7e49ba89af0f19270a3762df796b90a5e5", "shape": [ 1, 65, 64 - ] + ], + "statistics": { + "l2": 46.05630751333882, + "max": 1.3148114681243896, + "mean": -1.0446456144563853e-09, + "min": -1.7143969535827637, + "std": 0.7140727348219936 + } }, "shape_flexidualgrid_fields": { "dtype": "float32", - "sha256": "d13460feb6ebeeb09b7fb153ee3c5c607bf1ec2205471396b5a8c5908e5f1f62", "shape": [ 512, 7 - ] + ], + "statistics": { + "l2": 156.75664142258063, + "max": 4.1127119064331055, + "mean": 1.7140144131821475, + "min": -0.14383302628993988, + "std": 1.9794850947718232 + } }, "shape_slat": { "dtype": "float32", - "sha256": "69670f899f1c5f681fb9b3fd71a229e3f4e22271ed6ee470f36af14f32cc31bd", "shape": [ 64, 32 - ] + ], + "statistics": { + "l2": 45.74745586307211, + "max": 3.7916488647460938, + "mean": -0.01963400755175826, + "min": -3.6229214668273926, + "std": 1.010694818658021 + } }, "sparse_latent": { "dtype": "float32", - "sha256": "2fa4d566772d77461fd6630c042e69b0002adf31ee4b724b6557c8a45666ef51", "shape": [ 1, 2, 2, 2, 2 - ] + ], + "statistics": { + "l2": 4.890631746635004, + "max": 2.205594778060913, + "mean": -0.06916660442948341, + "min": -2.760120153427124, + "std": 1.2206999676031494 + } }, "texture_slat": { "dtype": "float32", - "sha256": "99d23e5097506efb79c83865dc63c505ad4fad0db6c8d468d22845baa66b2141", "shape": [ 64, 32 - ] + ], + "statistics": { + "l2": 47.3129009807639, + "max": 3.7781307697296143, + "mean": 0.016508268732650322, + "min": -3.8875527381896973, + "std": 1.0453469426877748 + } }, "texture_voxel_attrs": { "dtype": "float32", - "sha256": "49b45f028a546f707bcdf37677c63eb279187de4561bbe06f1a91720320396b4", "shape": [ 512, 6 - ] + ], + "statistics": { + "l2": 27.73014181281817, + "max": 0.5602271556854248, + "mean": 0.4999949636403471, + "min": 0.43711984157562256, + "std": 0.01782653483942547 + } }, "texture_voxel_coordinates": { "dtype": "int32", - "sha256": "d572dc92213b089492d529519ba3356793c57f19955851405b63c16bc944b436", + "sha256": "893113016750894983cc6e51682d666b9697e3c1834c864977738ff8369a1586", "shape": [ 512, 4 diff --git a/tests/golden_assertions.py b/tests/golden_assertions.py new file mode 100644 index 0000000..a1a7253 --- /dev/null +++ b/tests/golden_assertions.py @@ -0,0 +1,143 @@ +"""Compact numerical summaries and tolerant golden comparisons for MLX fixtures.""" + +from __future__ import annotations + +import hashlib +import json +import struct +from collections.abc import Mapping, Sequence +from pathlib import Path +from typing import Any + +import mlx.core as mx +import numpy as np + + +def summarize_array(value: Any) -> dict[str, Any]: + """Summarize an array using stable structure and distribution statistics.""" + + if isinstance(value, mx.array): + mx.eval(value) + dtype = str(value.dtype).removeprefix("mlx.core.") + array = np.asarray(value.astype(mx.float32) if dtype == "bfloat16" else value) + else: + array = np.asarray(value) + dtype = str(array.dtype) + + summary: dict[str, Any] = {"shape": list(array.shape), "dtype": dtype} + flat = array.reshape(-1) + if np.issubdtype(array.dtype, np.floating): + finite = np.asarray(flat, dtype=np.float64) + if not np.all(np.isfinite(finite)): + raise ValueError("golden fixture arrays must contain only finite values") + summary["statistics"] = { + "min": float(np.min(finite)), + "max": float(np.max(finite)), + "mean": float(np.mean(finite)), + "std": float(np.std(finite)), + "l2": float(np.linalg.norm(finite)), + } + return summary + + canonical = np.ascontiguousarray(array.astype(array.dtype.newbyteorder("<"), copy=False)) + summary["sha256"] = hashlib.sha256(canonical.tobytes(order="C")).hexdigest() + return summary + + +def assert_golden_close( + actual: Any, + expected: Any, + *, + path: str = "golden", + rtol: float = 5e-4, + atol: float = 1e-4, +) -> None: + """Compare nested golden data exactly except for floating-point leaves.""" + + if isinstance(expected, Mapping): + if not isinstance(actual, Mapping): + raise AssertionError(f"{path}: expected a mapping, got {type(actual).__name__}") + if set(actual) != set(expected): + raise AssertionError( + f"{path}: keys differ; actual={sorted(actual)} expected={sorted(expected)}" + ) + for key in expected: + assert_golden_close( + actual[key], expected[key], path=f"{path}.{key}", rtol=rtol, atol=atol + ) + return + + if isinstance(expected, Sequence) and not isinstance( + expected, (str, bytes, bytearray) + ): + if not isinstance(actual, Sequence) or isinstance(actual, (str, bytes, bytearray)): + raise AssertionError( + f"{path}: expected a sequence, got {type(actual).__name__}" + ) + if len(actual) != len(expected): + raise AssertionError( + f"{path}: length differs; actual={len(actual)} expected={len(expected)}" + ) + for index, expected_value in enumerate(expected): + assert_golden_close( + actual[index], expected_value, path=f"{path}[{index}]", rtol=rtol, atol=atol + ) + return + + if isinstance(expected, float): + if not np.isclose(float(actual), expected, rtol=rtol, atol=atol): + raise AssertionError(f"{path}: actual={actual!r} expected={expected!r}") + return + + if actual != expected: + raise AssertionError(f"{path}: actual={actual!r} expected={expected!r}") + + +def summarize_glb(path: Path) -> dict[str, Any]: + """Read a GLB and summarize stable structural fields without third-party parsers.""" + + payload = path.read_bytes() + if len(payload) < 20: + raise ValueError("GLB payload is too short") + magic, version, declared_length = struct.unpack_from("<4sII", payload, 0) + if magic != b"glTF" or version != 2 or declared_length != len(payload): + raise ValueError("invalid GLB header") + json_length, json_type = struct.unpack_from(" dict: + return json.loads(MANIFEST_PATH.read_text(encoding="utf-8")) + + +def _sha256(path: Path) -> str: + return hashlib.sha256(path.read_bytes()).hexdigest() diff --git a/tests/test_trellis2_golden_fixture.py b/tests/test_trellis2_golden_fixture.py index bc2b8a7..4a08255 100644 --- a/tests/test_trellis2_golden_fixture.py +++ b/tests/test_trellis2_golden_fixture.py @@ -7,11 +7,16 @@ import pytest +import mlx_spatial.trellis2_inference as trellis2_inference from mlx_spatial.trellis2_inference import Trellis2InferencePipeline from mlx_spatial.trellis2_quantization import read_trellis2_quantization_spec +from tests.golden_assertions import assert_golden_close, summarize_glb from tests.trellis2_golden_fixture import ( + TRELLIS2_MINIATURE_EXPORT_GRID_SIZE, + TRELLIS2_MINIATURE_EXPORT_TARGET_FACES, + TRELLIS2_MINIATURE_EXPORT_TEXTURE_SIZE, + Trellis2MiniatureSpatialKitExporter, build_trellis2_miniature_golden_fixture, - summarize_glb, summarize_trellis2_golden_trace, ) @@ -19,8 +24,14 @@ @pytest.mark.heavy -def test_miniature_int8_pipeline_emits_golden_trace_and_glb(tmp_path): +def test_miniature_int8_pipeline_emits_golden_trace_and_glb(tmp_path, monkeypatch): fixture = build_trellis2_miniature_golden_fixture(tmp_path) + exporter = Trellis2MiniatureSpatialKitExporter() + monkeypatch.setattr( + trellis2_inference, + "load_spatialkit_exporter", + lambda: (exporter, None), + ) quantization = read_trellis2_quantization_spec( fixture.quantized_root / "ckpts/ss_flow_img_dit_1_3B_64_bf16.safetensors" ) @@ -52,7 +63,7 @@ def test_miniature_int8_pipeline_emits_golden_trace_and_glb(tmp_path): assert result.ready, result.trace.blocker summary = { - "schema_version": 1, + "schema_version": 2, "fixture": { "checkpoint_source": "generated synthetic tensors", "pipeline_type": "512", @@ -65,6 +76,14 @@ def test_miniature_int8_pipeline_emits_golden_trace_and_glb(tmp_path): }, "trace": summarize_trellis2_golden_trace(result.trace), "glb": summarize_glb(fixture.output_path), + "export": { + "requested": exporter.requested_options, + "effective": { + "grid_size": TRELLIS2_MINIATURE_EXPORT_GRID_SIZE, + "target_faces": TRELLIS2_MINIATURE_EXPORT_TARGET_FACES, + "texture_size": TRELLIS2_MINIATURE_EXPORT_TEXTURE_SIZE, + }, + }, } expected = json.loads(GOLDEN_MANIFEST.read_text(encoding="utf-8")) - assert summary == expected + assert_golden_close(summary, expected) diff --git a/tests/trellis2_golden_fixture.py b/tests/trellis2_golden_fixture.py index 201d332..79c6712 100644 --- a/tests/trellis2_golden_fixture.py +++ b/tests/trellis2_golden_fixture.py @@ -2,10 +2,8 @@ from __future__ import annotations -import hashlib import json import math -import struct from dataclasses import dataclass from pathlib import Path from typing import Any, Mapping @@ -15,12 +13,19 @@ from PIL import Image from mlx_spatial.safetensors_io import save_safetensors +from mlx_spatial.spatialkit import export_decoded_ovoxel_glb from mlx_spatial.trellis2_decode import StructuredLatentDecoderConfig from mlx_spatial.trellis2_dinov3 import DinoV3ModelConfig from mlx_spatial.trellis2_forward import Trellis2ForwardTraceResult from mlx_spatial.trellis2_quantization import quantize_trellis2_weights from mlx_spatial.trellis2_slat import SLatFlowConfig from mlx_spatial.trellis2_sparse_structure import SparseStructureDecoderConfig, SparseStructureFlowConfig +from tests.golden_assertions import summarize_array + + +TRELLIS2_MINIATURE_EXPORT_GRID_SIZE = 32 +TRELLIS2_MINIATURE_EXPORT_TARGET_FACES = 64 +TRELLIS2_MINIATURE_EXPORT_TEXTURE_SIZE = 16 @dataclass(frozen=True) @@ -34,6 +39,30 @@ class Trellis2MiniatureGoldenFixture: output_path: Path +@dataclass +class Trellis2MiniatureSpatialKitExporter: + """Run the real SpatialKit exporter with a miniature remesh policy.""" + + requested_options: dict[str, Any] | None = None + + def __call__(self, decoded_dir: str | Path, output_path: str | Path, **options: Any): + self.requested_options = dict(options) + return export_decoded_ovoxel_glb( + decoded_dir, + output_path, + texture_size=TRELLIS2_MINIATURE_EXPORT_TEXTURE_SIZE, + target_faces=TRELLIS2_MINIATURE_EXPORT_TARGET_FACES, + quality_preset="reference-target", + grid_size=TRELLIS2_MINIATURE_EXPORT_GRID_SIZE, + uv_backend="xatlas-equivalent-native", + remesh=True, + remesh_resolution=TRELLIS2_MINIATURE_EXPORT_GRID_SIZE, + simplify_backend="mlx-qem", + texture_postprocess="telea", + diagnostics_path=options.get("diagnostics_path"), + ) + + def build_trellis2_miniature_golden_fixture(root: Path) -> Trellis2MiniatureGoldenFixture: """Create tiny source checkpoints, quantize them, and return runnable fixture paths.""" @@ -173,55 +202,20 @@ def summarize_trellis2_golden_trace(trace: Trellis2ForwardTraceResult) -> dict[s for output in trace.outputs: if output.payload is None: continue - tensor_outputs[output.name] = { - "shape": list(output.shape), - "dtype": output.dtype, - "sha256": _tensor_digest(output.payload), - } + summary = summarize_array(output.payload) + if summary["shape"] != list(output.shape) or summary["dtype"] != output.dtype: + raise AssertionError( + f"trace metadata disagrees with payload for {output.name}: " + f"declared shape={output.shape} dtype={output.dtype}; " + f"actual shape={summary['shape']} dtype={summary['dtype']}" + ) + tensor_outputs[output.name] = summary return { "completed_stages": list(trace.completed_stages), "tensor_outputs": tensor_outputs, } -def summarize_glb(path: Path) -> dict[str, Any]: - """Read a GLB without third-party parsers and summarize stable structural fields.""" - - payload = path.read_bytes() - if len(payload) < 20: - raise ValueError("GLB payload is too short") - magic, version, declared_length = struct.unpack_from("<4sII", payload, 0) - if magic != b"glTF" or version != 2 or declared_length != len(payload): - raise ValueError("invalid GLB header") - json_length, json_type = struct.unpack_from(" SLatFlowConfig: return SLatFlowConfig( name="SLatFlowModel", @@ -539,29 +533,12 @@ def _center_identity_conv(channels: int) -> mx.array: return mx.array(values) -def _tensor_digest(value: mx.array) -> str: - mx.eval(value) - dtype = str(value.dtype).removeprefix("mlx.core.") - if dtype.startswith("float") or dtype == "bfloat16": - array = np.asarray(value.astype(mx.float32), dtype="