File size: 6,674 Bytes
31c7d49
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
#!/usr/bin/env python3
"""Export the official TripoSplat Gaussian feature decoder to fixed-shape ONNX.

Public graph contract (all tensors float32):

* ``points``: systematic-octree samples ``[1, 8192, 3]`` in ``[0, 1]``
* ``cond``: sampled TripoSplat latent ``[1, 8192, 16]``
* ``features``: raw Gaussian features ``[1, 8192, 480]``

The 480 channels are produced directly by the official
``ElasticGaussianFixedlenDecoder``.  This graph does not duplicate ``_get_offset``,
``_build_gaussians``, representation scaling/biases, or activation functions.
"""

from __future__ import annotations

import argparse
from pathlib import Path

from decoder_onnx_common import (
    COND_SHAPE,
    FEATURES_SHAPE,
    OFFICIAL_REPOSITORY_URL,
    POINTS_SHAPE,
    adapt_official_decoder_for_onnx,
    choose_torch_device,
    export_fixed_decoder_graph,
    load_official_decoder,
    make_dummy_inputs,
    make_gaussian_features_graph,
    sha256,
    source_commit,
)


COMPONENT = "gaussian_decoder"


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        description=__doc__,
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    parser.add_argument(
        "--triposplat-repo",
        type=Path,
        required=True,
        help=f"Local clone of {OFFICIAL_REPOSITORY_URL}.",
    )
    parser.add_argument(
        "--weights",
        type=Path,
        required=True,
        help="Official triposplat_vae_decoder_fp16.safetensors checkpoint.",
    )
    parser.add_argument(
        "--output",
        type=Path,
        default=Path("public/models/triposplat/gaussian_decoder.onnx"),
        help="Destination ONNX graph (default: %(default)s).",
    )
    parser.add_argument(
        "--precision",
        choices=("fp16", "fp32"),
        default="fp16",
        help=(
            "Internal parameter/compute precision. Public graph I/O stays float32 "
            "for both choices (default: %(default)s)."
        ),
    )
    parser.add_argument(
        "--device",
        choices=("cpu", "mps", "cuda", "auto"),
        default="cpu",
        help="PyTorch device used while tracing (default: %(default)s).",
    )
    parser.add_argument(
        "--opset",
        type=int,
        default=20,
        help="ONNX opset version (default: %(default)s).",
    )
    parser.add_argument(
        "--external-data-threshold",
        type=int,
        default=1024,
        metavar="BYTES",
        help=(
            "Initializers at least this large go into one .onnx.data sidecar "
            "(default: %(default)s)."
        ),
    )
    parser.add_argument(
        "--skip-check",
        action="store_true",
        help="Skip path-based onnx.checker validation after consolidation.",
    )
    parser.add_argument(
        "--verbose",
        action="store_true",
        help="Enable verbose torch.onnx tracing output.",
    )
    args = parser.parse_args()
    if args.opset < 18:
        parser.error("--opset must be at least 18 for scaled dot-product attention export")
    if args.external_data_threshold < 0:
        parser.error("--external-data-threshold must be non-negative")
    return args


def export_graph(args: argparse.Namespace) -> list[Path]:
    try:
        import onnx
        import torch
    except ImportError as exc:
        raise SystemExit(
            "Missing export dependency. Install a PyTorch-supported Python version "
            "and run `python -m pip install -r scripts/triposplat/requirements.txt`. "
            f"Original error: {exc}"
        ) from exc

    repo = args.triposplat_repo.expanduser().resolve()
    weights = args.weights.expanduser().resolve()
    output = args.output.expanduser().resolve()
    device = choose_torch_device(torch, args.device)

    print(f"Loading official TripoSplat decoder via load_decoder from {repo}")
    decoder = load_official_decoder(
        torch=torch,
        triposplat_repo=repo,
        weights=weights,
        device=device,
        internal_precision=args.precision,
    )
    adapter = adapt_official_decoder_for_onnx(torch, decoder.gs)
    graph = make_gaussian_features_graph(torch, decoder).to(device=device).eval()
    # The wrapper owns decoder.gs only; drop the unused octree sibling before tracing.
    del decoder
    dummy_inputs = make_dummy_inputs(torch, COMPONENT, device)

    metadata = {
        "triposplat.component": COMPONENT,
        "triposplat.source_repository": OFFICIAL_REPOSITORY_URL,
        "triposplat.source_commit": source_commit(repo),
        "triposplat.checkpoint_filename": weights.name,
        "triposplat.internal_precision": args.precision,
        "triposplat.public_io_precision": "float32",
        "triposplat.points_input": "points [1,8192,3] float32 normalized coordinates [0,1]",
        "triposplat.cond_input": "cond [1,8192,16] float32 sampled latent",
        "triposplat.output": "features [1,8192,480] float32 raw decoder features",
        "triposplat.feature_layout": (
            "official ElasticGaussianFixedlenDecoder layout; 32 Gaussians per point"
        ),
        "triposplat.excluded_host_logic": (
            "_get_offset, _build_gaussians, representation scales/biases/activations"
        ),
        "triposplat.attention_query_chunk": str(adapter["attention_query_chunk"]),
        "triposplat.attention_modules": str(adapter["attention_modules"]),
        "triposplat.qk_norm_padding_tokens": str(adapter["qk_norm_padding_tokens"]),
        "triposplat.qk_norm_modules": str(adapter["qk_norm_modules"]),
        "triposplat.attention_output_modules": str(adapter["attention_output_modules"]),
    }
    print(
        f"Tracing {args.precision}-internal graph on {device}: "
        f"points={POINTS_SHAPE}, cond={COND_SHAPE} -> features={FEATURES_SHAPE}"
    )
    artifacts = export_fixed_decoder_graph(
        torch=torch,
        onnx=onnx,
        graph=graph,
        dummy_inputs=dummy_inputs,
        input_names=("points", "cond"),
        output_name="features",
        output_path=output,
        component=COMPONENT,
        internal_precision=args.precision,
        opset=args.opset,
        external_data_threshold=args.external_data_threshold,
        metadata=metadata,
        verbose=args.verbose,
        run_checker=not args.skip_check,
    )

    print(f"Published fixed float32 I/O contract: features {FEATURES_SHAPE}")
    for artifact in artifacts:
        print(
            f"Wrote {artifact} ({artifact.stat().st_size:,} bytes, "
            f"sha256={sha256(artifact)})"
        )
    return artifacts


def main() -> None:
    export_graph(parse_args())


if __name__ == "__main__":
    main()