diff --git a/app.py b/app.py
index 525e26b2784f5022b87fbb9071cf02a3f3d312a6..79dca7a2aaef4a8d5ef3a24fd8605cda0f22b9f4 100644
--- a/app.py
+++ b/app.py
@@ -1,70 +1,70 @@
-"""Supra-Tex entrypoint with the supplied demo presentation layer."""
-import spaces
-import gradio as gr
-from hf_space import native_build
-from hf_space import backend
-from hf_space import result_cache
-from hf_space import generation_progress
-from hf_space import ovoxel_results
-import time
-
-result_cache.prepare_examples()
-native_build.start()
-# Preload once per server process, independently of visitor sessions.
-backend.start_preparation()
-
-
-def deployment_status():
- return {'native_build': native_build.status(), 'models': backend.model_status()}
-
-
-@spaces.GPU(duration=900, size='large')
-def generate_on_gpu(input_image, seed, progress, generation_token=None):
- mesh, _asset, report = backend.generate(input_image, int(seed), progress=progress,
- generation_token=generation_token)
- return mesh, report
-
-
-def process_image_generator(input_image, seed=42, generation_token=None, progress=gr.Progress()):
- if input_image is None:
- raise gr.Error('Please upload an image.')
- print('[generation] Explicit Generate request; running released model without cache reuse', flush=True)
- token = generation_token if generation_progress.valid(generation_token) else generation_progress.start()
- try:
- backend.ensure_prepared(token)
- generation_progress.update(token, status='queued', label='Waiting for GPU')
- mesh, report = generate_on_gpu(input_image, int(seed), progress, token)
- generation_progress.update(token, status='saving', label='Saving the 3D asset')
- try:
- identifier = ovoxel_results.persist(mesh, report)
- except Exception as error:
- print(f'[result-cache] Could not persist result: {type(error).__name__}: {error}', flush=True)
- raise
- # The pipeline already rendered the saved asset. Show those views now;
- # exporting a GLB belongs to the separate Extract GLB action.
- rendered = ovoxel_results.preview_html(identifier)
- generation_progress.update(token, status='complete', label='Complete', finished_unix=time.time())
- except Exception:
- generation_progress.update(token, status='failed', label='Generation failed', finished_unix=time.time())
- raise
- return rendered, identifier
-
-
-@spaces.GPU(duration=600, size='large')
-def extract_glb(identifier, extraction_token=None):
- return ovoxel_results.extract(identifier, extraction_token)
-
-
-@spaces.GPU(duration=600, size='large')
-def recover_preview(identifier):
- from hf_space.preview_recovery import recover
- return recover(identifier)
-
-
-from hf_space.ui import build_ui, CSS, THEME
-
-demo = build_ui(process_image_generator, deployment_status, cached_result=result_cache.lookup, extract_glb=extract_glb, recover_preview=recover_preview,
- default_image=None, default_model=None, estimated_seconds=390,
- example_names=result_cache.EXAMPLE_NAMES, examples_status=result_cache.examples_status)
-demo.queue().launch(allowed_paths=['/tmp/supratex-jobs', str(result_cache.ROOT/'assets/previews'), str(result_cache.CACHE),str(ovoxel_results.ROOT), str(result_cache.ROOT/'hf_space/gallery-viewer.bundle.js'), str(result_cache.ROOT/'hf_space/gallery-lights')],
- css=CSS, theme=THEME)
+"""Supra-Tex entrypoint with the supplied demo presentation layer."""
+import spaces
+import gradio as gr
+from hf_space import native_build
+from hf_space import release_backend as backend
+from hf_space import result_cache
+from hf_space import generation_progress
+from hf_space import ovoxel_results
+import time
+
+result_cache.prepare_examples()
+native_build.start()
+# Preload once per server process, independently of visitor sessions.
+backend.start_preparation()
+
+
+def deployment_status():
+ return {'native_build': native_build.status(), 'models': backend.model_status()}
+
+
+@spaces.GPU(duration=900, size='large')
+def generate_on_gpu(input_image, seed, progress, generation_token=None):
+ mesh, _asset, report = backend.generate(input_image, int(seed), progress=progress,
+ generation_token=generation_token)
+ return mesh, report
+
+
+def process_image_generator(input_image, seed=42, generation_token=None, progress=gr.Progress()):
+ if input_image is None:
+ raise gr.Error('Please upload an image.')
+ print('[generation] Explicit Generate request; running released model without cache reuse', flush=True)
+ token = generation_token if generation_progress.valid(generation_token) else generation_progress.start()
+ try:
+ backend.ensure_prepared(token)
+ generation_progress.update(token, status='queued', label='Waiting for GPU')
+ mesh, report = generate_on_gpu(input_image, int(seed), progress, token)
+ generation_progress.update(token, status='saving', label='Saving the 3D asset')
+ try:
+ identifier = ovoxel_results.persist(mesh, report)
+ except Exception as error:
+ print(f'[result-cache] Could not persist result: {type(error).__name__}: {error}', flush=True)
+ raise
+ # The pipeline already rendered the saved asset. Show those views now;
+ # exporting a GLB belongs to the separate Extract GLB action.
+ rendered = ovoxel_results.preview_html(identifier)
+ generation_progress.update(token, status='complete', label='Complete', finished_unix=time.time())
+ except Exception:
+ generation_progress.update(token, status='failed', label='Generation failed', finished_unix=time.time())
+ raise
+ return rendered, identifier
+
+
+@spaces.GPU(duration=600, size='large')
+def extract_glb(identifier, extraction_token=None):
+ return ovoxel_results.extract(identifier, extraction_token)
+
+
+@spaces.GPU(duration=600, size='large')
+def recover_preview(identifier):
+ from hf_space.preview_recovery import recover
+ return recover(identifier)
+
+
+from hf_space.ui import build_ui, CSS, THEME
+
+demo = build_ui(process_image_generator, deployment_status, cached_result=result_cache.lookup, extract_glb=extract_glb, recover_preview=recover_preview,
+ default_image=None, default_model=None, estimated_seconds=390,
+ example_names=result_cache.EXAMPLE_NAMES, examples_status=result_cache.examples_status)
+demo.queue().launch(allowed_paths=['/tmp/supratex-jobs', str(result_cache.ROOT/'assets/previews'), str(result_cache.CACHE),str(ovoxel_results.ROOT), str(result_cache.ROOT/'hf_space/gallery-viewer.bundle.js'), str(result_cache.ROOT/'hf_space/gallery-lights')],
+ css=CSS, theme=THEME)
diff --git a/configs/pipeline.json b/configs/pipeline.json
new file mode 100644
index 0000000000000000000000000000000000000000..1b9004548ece048f31bf5aaa9c69de04af643d09
--- /dev/null
+++ b/configs/pipeline.json
@@ -0,0 +1,98 @@
+{
+ "format": "supratex_pipeline",
+ "schema_version": 1,
+ "models": {
+ "completion": "completion",
+ "material": "material"
+ },
+ "external_models": {
+ "qwen-base": {
+ "repo_id": "Qwen/Qwen-Image-2.1",
+ "revision": "d26bb61231c349cf6b7896fa83353113880e1ba3",
+ "allow_patterns": [
+ "processor/*",
+ "vae/*",
+ "text_encoder/*",
+ "transformer/*",
+ "LICENSE*",
+ "README.md"
+ ]
+ },
+ "geometry": {
+ "repo_id": "TencentARC/Pixal3D",
+ "revision": "b0cb2e1b794cab9aa0ac38a95d794a4d9337437f",
+ "allow_patterns": [
+ "ckpts/ss_flow_img_dit_1_3B_64_bf16.json",
+ "ckpts/ss_flow_img_dit_1_3B_64_bf16.safetensors",
+ "ckpts/ss_dec_conv3d_16l8_fp16.json",
+ "ckpts/ss_dec_conv3d_16l8_fp16.safetensors",
+ "ckpts/slat_flow_img2shape_dit_1_3B_512_bf16.json",
+ "ckpts/slat_flow_img2shape_dit_1_3B_512_bf16.safetensors",
+ "ckpts/slat_flow_img2shape_dit_1_3B_1024_bf16.json",
+ "ckpts/slat_flow_img2shape_dit_1_3B_1024_bf16.safetensors",
+ "ckpts/shape_dec_next_dc_f16c32_fp16.json",
+ "ckpts/shape_dec_next_dc_f16c32_fp16.safetensors"
+ ]
+ },
+ "moge2": {
+ "repo_id": "Ruicheng/moge-2-vitl-normal",
+ "revision": "cb0e8bbd6b1e243589717c78e750b1ba4c093acf",
+ "allow_patterns": [
+ "model.pt",
+ "README.md",
+ "LICENSE*"
+ ]
+ },
+ "dino": {
+ "repo_id": "facebook/dinov3-vitl16-pretrain-lvd1689m",
+ "revision": "ea8dc2863c51be0a264bab82070e3e8836b02d51",
+ "gated": true,
+ "allow_patterns": [
+ "config.json",
+ "model.safetensors",
+ "preprocessor_config.json",
+ "LICENSE*",
+ "README.md"
+ ]
+ },
+ "rmbg2": {
+ "repo_id": "briaai/RMBG-2.0",
+ "revision": "5df4c9c76d8170882c34f6986e848ee07fd0ba43",
+ "gated": true,
+ "allow_patterns": [
+ "config.json",
+ "model.safetensors",
+ "BiRefNet_config.py",
+ "birefnet.py",
+ "preprocessor_config.json",
+ "LICENSE*",
+ "README.md"
+ ]
+ },
+ "vae": {
+ "repo_id": "microsoft/TRELLIS.2-4B",
+ "revision": "af44b45f2e35a493886929c6d786e563ec68364d",
+ "allow_patterns": [
+ "ckpts/shape_enc_next_dc_f16c32_fp16.json",
+ "ckpts/shape_enc_next_dc_f16c32_fp16.safetensors",
+ "ckpts/tex_enc_next_dc_f16c32_fp16.json",
+ "ckpts/tex_enc_next_dc_f16c32_fp16.safetensors",
+ "ckpts/shape_dec_next_dc_f16c32_fp16.json",
+ "ckpts/shape_dec_next_dc_f16c32_fp16.safetensors",
+ "ckpts/tex_dec_next_dc_f16c32_fp16.json",
+ "ckpts/tex_dec_next_dc_f16c32_fp16.safetensors"
+ ]
+ },
+ "naf": {
+ "url": "https://github.com/valeoai/NAF/releases/download/model/naf_release.pth",
+ "filename": "naf_release.pth"
+ }
+ },
+ "defaults": {
+ "seed": 42,
+ "texture_size": 4096,
+ "decimation_target": 500000,
+ "texture_format": "png",
+ "attention": "auto"
+ }
+}
diff --git a/deployment/runtime/SOURCE_PROVENANCE.json b/deployment/runtime/SOURCE_PROVENANCE.json
index ad7de677ff449e94e0a9ba72797aaf3d0f3dfad3..ab960ffdcdee487292ce9dfb5086095681fe2ead 100644
--- a/deployment/runtime/SOURCE_PROVENANCE.json
+++ b/deployment/runtime/SOURCE_PROVENANCE.json
@@ -1,29 +1,26 @@
-{
- "verification": "source file presence and sizes only; no hashes calculated",
- "geometry": {
- "source": "local Pixal3D/pixal3d source tree",
- "git_head": "0b9ff55cbcf5f87aaae9948742cf6388a1a3ecb1",
- "note": "Working tree snapshot; HEAD alone does not capture local edits"
- },
- "qwen": {
- "source": "ckpts/qwen-runtime",
- "snapshot_schema": "qwen_pbr_runtime_snapshot_v1",
- "snapshot_created_utc": "2026-10-03T23:58:03.610370+00:00",
- "note": "Exact staged inference source; upstream git identity is not recorded in staged manifest"
- },
- "moge": {
- "source": "installed moge 2.0.0 source",
- "upstream": "https://github.com/microsoft/MoGe.git",
- "recorded_commit": "07444410f1e33f402353b99d6ccd26bd31e469e8"
- },
- "utils3d": {
- "source": "installed utils3d 0.0.2 Python source",
- "upstream": "https://github.com/EasternJournalist/utils3d",
- "note": "Installed wheel provenance, no git commit recorded"
- },
- "naf": {
- "source": "local valeoai_NAF_main Torch Hub source cache",
- "upstream": "https://github.com/valeoai/NAF",
- "note": "No git metadata in staged torch hub cache; bundled source is the pin"
- }
-}
+{
+ "verification": "source file presence and sizes only; no hashes calculated",
+ "geometry": {
+ "source": "Bundled Pixal3D runtime based on TencentARC/Pixal3D and Microsoft TRELLIS.2",
+ "note": "Vendored files are the distribution reference; an exact upstream Git revision is unavailable."
+ },
+ "qwen": {
+ "source": "Bundled native Qwen-Image-2.1 material runtime",
+ "note": "Vendored files are the distribution reference; an exact upstream Git revision is unavailable."
+ },
+ "moge": {
+ "source": "installed moge 2.0.0 source",
+ "upstream": "https://github.com/microsoft/MoGe.git",
+ "recorded_commit": "07444410f1e33f402353b99d6ccd26bd31e469e8"
+ },
+ "utils3d": {
+ "source": "installed utils3d 0.0.2 Python source",
+ "upstream": "https://github.com/EasternJournalist/utils3d",
+ "note": "Installed wheel provenance, no git commit recorded"
+ },
+ "naf": {
+ "source": "Bundled NAF runtime",
+ "upstream": "https://github.com/valeoai/NAF",
+ "note": "Bundled source is the distribution reference; an exact upstream Git revision is unavailable."
+ }
+}
diff --git a/deployment/runtime/geometry/pixal3d/models/__init__.py b/deployment/runtime/geometry/pixal3d/models/__init__.py
index 15efa73b114edbc57ca52e993e2031a8f96da3b9..4e002253d8a7c5c2a8e73701f830bffb3dfaa02b 100644
--- a/deployment/runtime/geometry/pixal3d/models/__init__.py
+++ b/deployment/runtime/geometry/pixal3d/models/__init__.py
@@ -1,87 +1,78 @@
-import importlib
-
-__attributes = {
- # Sparse Structure
- 'SparseStructureEncoder': 'sparse_structure_vae',
- 'SparseStructureDecoder': 'sparse_structure_vae',
- 'SparseStructureFlowModel': 'sparse_structure_flow',
-
- # SLat Generation
- 'SLatFlowModel': 'structured_latent_flow',
- 'ElasticSLatFlowModel': 'structured_latent_flow',
- 'V13PSAFlowModel': 'v13psa',
- 'V13PSAILVFlowModel': 'v13psailv',
- 'V14FXFlowModel': 'v14fx',
-
- # SC-VAEs
- 'SparseUnetVaeEncoder': 'sc_vaes.sparse_unet_vae',
- 'SparseUnetVaeDecoder': 'sc_vaes.sparse_unet_vae',
- 'FlexiDualGridVaeEncoder': 'sc_vaes.fdg_vae',
- 'FlexiDualGridVaeDecoder': 'sc_vaes.fdg_vae'
-}
-
-__submodules = []
-
-__all__ = list(__attributes.keys()) + __submodules
-
-def __getattr__(name):
- if name not in globals():
- if name in __attributes:
- module_name = __attributes[name]
- module = importlib.import_module(f".{module_name}", __name__)
- globals()[name] = getattr(module, name)
- elif name in __submodules:
- module = importlib.import_module(f".{name}", __name__)
- globals()[name] = module
- else:
- raise AttributeError(f"module {__name__} has no attribute {name}")
- return globals()[name]
-
-
-def from_pretrained(path: str, **kwargs):
- """
- Load a model from a pretrained checkpoint.
-
- Args:
- path: The path to the checkpoint. Can be either local path or a Hugging Face model name.
- NOTE: config file and model file should take the name f'{path}.json' and f'{path}.safetensors' respectively.
- **kwargs: Additional arguments for the model constructor.
- """
- import os
- import json
- from safetensors.torch import load_file
- is_local = os.path.exists(f"{path}.json") and os.path.exists(f"{path}.safetensors")
-
- if is_local:
- config_file = f"{path}.json"
- model_file = f"{path}.safetensors"
- else:
- from huggingface_hub import hf_hub_download
- path_parts = path.split('/')
- repo_id = f'{path_parts[0]}/{path_parts[1]}'
- model_name = '/'.join(path_parts[2:])
- config_file = hf_hub_download(repo_id, f"{model_name}.json")
- model_file = hf_hub_download(repo_id, f"{model_name}.safetensors")
-
- with open(config_file, 'r') as f:
- config = json.load(f)
- if config['name'] == 'V14FXFlowModel':
- raise ValueError(
- 'V14FX requires strict native-checkpoint loading through '
- 'pixal3d.pipelines.build_v14fx_inference_model; '
- 'the generic non-strict pretrained loader is unsupported'
- )
- model = __getattr__(config['name'])(**config['args'], **kwargs)
- model.load_state_dict(load_file(model_file), strict=False)
-
- return model
-
-
-# For Pylance
-if __name__ == '__main__':
- from .sparse_structure_vae import SparseStructureEncoder, SparseStructureDecoder
- from .sparse_structure_flow import SparseStructureFlowModel
- from .structured_latent_flow import SLatFlowModel, ElasticSLatFlowModel
-
- from .sc_vaes.sparse_unet_vae import SparseUnetVaeEncoder, SparseUnetVaeDecoder
- from .sc_vaes.fdg_vae import FlexiDualGridVaeEncoder, FlexiDualGridVaeDecoder
+import importlib
+
+__attributes = {
+ # Sparse Structure
+ 'SparseStructureEncoder': 'sparse_structure_vae',
+ 'SparseStructureDecoder': 'sparse_structure_vae',
+ 'SparseStructureFlowModel': 'sparse_structure_flow',
+
+ # SLat Generation
+ 'SLatFlowModel': 'structured_latent_flow',
+ 'ElasticSLatFlowModel': 'structured_latent_flow',
+
+ # SC-VAEs
+ 'SparseUnetVaeEncoder': 'sc_vaes.sparse_unet_vae',
+ 'SparseUnetVaeDecoder': 'sc_vaes.sparse_unet_vae',
+ 'FlexiDualGridVaeEncoder': 'sc_vaes.fdg_vae',
+ 'FlexiDualGridVaeDecoder': 'sc_vaes.fdg_vae'
+}
+
+__submodules = []
+
+__all__ = list(__attributes.keys()) + __submodules
+
+def __getattr__(name):
+ if name not in globals():
+ if name in __attributes:
+ module_name = __attributes[name]
+ module = importlib.import_module(f".{module_name}", __name__)
+ globals()[name] = getattr(module, name)
+ elif name in __submodules:
+ module = importlib.import_module(f".{name}", __name__)
+ globals()[name] = module
+ else:
+ raise AttributeError(f"module {__name__} has no attribute {name}")
+ return globals()[name]
+
+
+def from_pretrained(path: str, **kwargs):
+ """
+ Load a model from a pretrained checkpoint.
+
+ Args:
+ path: The path to the checkpoint. Can be either local path or a Hugging Face model name.
+ NOTE: config file and model file should take the name f'{path}.json' and f'{path}.safetensors' respectively.
+ **kwargs: Additional arguments for the model constructor.
+ """
+ import os
+ import json
+ from safetensors.torch import load_file
+ is_local = os.path.exists(f"{path}.json") and os.path.exists(f"{path}.safetensors")
+
+ if is_local:
+ config_file = f"{path}.json"
+ model_file = f"{path}.safetensors"
+ else:
+ from huggingface_hub import hf_hub_download
+ path_parts = path.split('/')
+ repo_id = f'{path_parts[0]}/{path_parts[1]}'
+ model_name = '/'.join(path_parts[2:])
+ config_file = hf_hub_download(repo_id, f"{model_name}.json")
+ model_file = hf_hub_download(repo_id, f"{model_name}.safetensors")
+
+ with open(config_file, 'r') as f:
+ config = json.load(f)
+ model = __getattr__(config['name'])(**config['args'], **kwargs)
+ model.load_state_dict(load_file(model_file), strict=False)
+
+ return model
+
+
+# For Pylance
+if __name__ == '__main__':
+ from .sparse_structure_vae import SparseStructureEncoder, SparseStructureDecoder
+ from .sparse_structure_flow import SparseStructureFlowModel
+ from .structured_latent_flow import SLatFlowModel
+
+ from .sc_vaes.sparse_unet_vae import SparseUnetVaeEncoder, SparseUnetVaeDecoder
+ from .sc_vaes.fdg_vae import FlexiDualGridVaeEncoder, FlexiDualGridVaeDecoder
diff --git a/deployment/runtime/geometry/pixal3d/models/structured_latent_flow.py b/deployment/runtime/geometry/pixal3d/models/structured_latent_flow.py
index 50cb834086fc95d81d368fbc6a3fceaf122ebd68..3f2d09bf5009efe7808c0f974ee5a1bc082141d5 100644
--- a/deployment/runtime/geometry/pixal3d/models/structured_latent_flow.py
+++ b/deployment/runtime/geometry/pixal3d/models/structured_latent_flow.py
@@ -1,1391 +1,1387 @@
-from typing import *
-from functools import partial
-import torch
-import torch.nn as nn
-import torch.nn.functional as F
-import numpy as np
-from ..modules.utils import convert_module_to, manual_cast, str_to_dtype
-from ..modules.transformer import AbsolutePositionEmbedder
-from ..modules import sparse as sp
-from ..modules.sparse.transformer import ModulatedSparseTransformerCrossBlock
-from .sparse_structure_flow import TimestepEmbedder
-from .sparse_elastic_mixin import SparseTransformerElasticMixin
-
-
-class _PartialZObsInnovationGate(nn.Module):
- """Residual, per-observed-token correction for the partial z_obs carrier."""
-
- def __init__(self, model_channels: int, message_channels: int, dim: int, time_dim: int, num_blocks: int):
- super().__init__()
- self.alpha = nn.Parameter(torch.zeros(num_blocks))
- self.embed_y = nn.Linear(message_channels, dim, bias=True)
- self.time_proj = nn.Linear(model_channels, time_dim, bias=True)
- self.embed_x = nn.Linear(model_channels + time_dim, dim, bias=True)
- self.mlp = nn.Sequential(
- nn.Linear(dim + 2 + time_dim, dim, bias=True),
- nn.SiLU(),
- nn.Linear(dim, 1, bias=True),
- )
- self.proj = nn.Linear(dim, model_channels, bias=True)
-
-
-_LOCAL_ONE_HOP_OFFSETS = (
- (-1, 0, 0), (0, -1, 0), (0, 0, -1),
- (0, 0, 1), (0, 1, 0), (1, 0, 0),
-)
-_LOCAL_TWO_HOP_OFFSETS = tuple(sorted(
- (dx, dy, dz)
- for dx in range(-2, 3)
- for dy in range(-2, 3)
- for dz in range(-2, 3)
- if abs(dx) + abs(dy) + abs(dz) <= 2
-))
-assert len(_LOCAL_TWO_HOP_OFFSETS) == 25
-
-
-def _exact_coordinate_rows(
- full_coords: torch.Tensor,
- candidate_coords: torch.Tensor,
-) -> Tuple[torch.Tensor, torch.Tensor]:
- """Return exact full-support rows and a validity mask for candidate coords."""
- assert full_coords.ndim == candidate_coords.ndim == 2
- assert full_coords.shape[1] == candidate_coords.shape[1]
- assert full_coords.shape[0] > 0, 'local attention requires nonempty full sparse support'
- all_coords = torch.cat([full_coords, candidate_coords], dim=0)
- _, inverse = torch.unique(all_coords, dim=0, return_inverse=True)
- full_ids = inverse[:full_coords.shape[0]]
- candidate_ids = inverse[full_coords.shape[0]:]
- assert torch.unique(full_ids).numel() == full_ids.numel(), \
- 'x contains duplicate coordinates'
- ordered_ids, ordered_rows = torch.sort(full_ids)
- positions = torch.searchsorted(ordered_ids, candidate_ids)
- in_range = positions < ordered_ids.numel()
- safe_positions = positions.clamp(max=ordered_ids.numel() - 1)
- valid = in_range & (ordered_ids[safe_positions] == candidate_ids)
- rows = torch.where(valid, ordered_rows[safe_positions], -torch.ones_like(safe_positions))
- return rows, valid
-
-
-def _build_two_hop_observed_topology(
- full_coords: torch.Tensor,
- observed_rows: torch.Tensor,
-) -> Dict[str, torch.Tensor]:
- """Pack exact batch-local G(S) distance<=2 observed source/query pairs."""
- assert full_coords.ndim == 2 and full_coords.shape[1] == 4, \
- f'local attention requires [N,4] integer coords, got {tuple(full_coords.shape)}'
- assert not torch.is_floating_point(full_coords), 'local attention coordinates must be integer'
- assert observed_rows.ndim == 1 and observed_rows.dtype == torch.long
- # Validate full-coordinate uniqueness even for an all-absent batch.
- _exact_coordinate_rows(full_coords, full_coords.new_empty((0, full_coords.shape[1])))
- if observed_rows.numel() == 0:
- empty = torch.empty(0, dtype=torch.long, device=full_coords.device)
- return {
- 'query_rows': empty,
- 'source_indices': empty,
- 'offset_indices': empty,
- 'segment_indices': empty,
- 'neighbour_counts': empty,
- }
- assert observed_rows.min() >= 0 and observed_rows.max() < full_coords.shape[0], \
- 'observed row lies outside full sparse support'
- assert torch.unique(observed_rows).numel() == observed_rows.numel(), \
- 'observed rows must be unique'
-
- device = full_coords.device
- one_hop = torch.tensor(
- _LOCAL_ONE_HOP_OFFSETS, dtype=full_coords.dtype, device=device,
- )
- source_coords = full_coords[observed_rows]
- candidate_hop1 = source_coords[:, None, :].expand(-1, len(_LOCAL_ONE_HOP_OFFSETS), -1).clone()
- candidate_hop1[:, :, 1:] += one_hop[None]
- hop1_rows, hop1_valid = _exact_coordinate_rows(
- full_coords, candidate_hop1.reshape(-1, full_coords.shape[1]),
- )
- source_ids_hop1 = torch.arange(
- observed_rows.numel(), device=device, dtype=torch.long,
- )[:, None].expand(-1, len(_LOCAL_ONE_HOP_OFFSETS)).reshape(-1)
- hop1_rows = hop1_rows[hop1_valid]
- source_ids_hop1 = source_ids_hop1[hop1_valid]
-
- if hop1_rows.numel() != 0:
- candidate_hop2 = full_coords[hop1_rows][:, None, :].expand(
- -1, len(_LOCAL_ONE_HOP_OFFSETS), -1,
- ).clone()
- candidate_hop2[:, :, 1:] += one_hop[None]
- hop2_rows, hop2_valid = _exact_coordinate_rows(
- full_coords, candidate_hop2.reshape(-1, full_coords.shape[1]),
- )
- source_ids_hop2 = source_ids_hop1[:, None].expand(
- -1, len(_LOCAL_ONE_HOP_OFFSETS),
- ).reshape(-1)
- hop2_rows = hop2_rows[hop2_valid]
- source_ids_hop2 = source_ids_hop2[hop2_valid]
- else:
- hop2_rows = torch.empty(0, dtype=torch.long, device=device)
- source_ids_hop2 = torch.empty(0, dtype=torch.long, device=device)
-
- source_ids = torch.cat([
- torch.arange(observed_rows.numel(), device=device, dtype=torch.long),
- source_ids_hop1,
- source_ids_hop2,
- ])
- query_rows = torch.cat([observed_rows, hop1_rows, hop2_rows])
- # Multiple graph paths to the same query/source pair are one neighbourhood entry.
- unique_pairs = torch.unique(torch.stack([query_rows, source_ids], dim=1), dim=0)
- query_rows = unique_pairs[:, 0]
- source_ids = unique_pairs[:, 1]
- query_coords = full_coords[query_rows]
- selected_source_coords = source_coords[source_ids]
- assert torch.equal(query_coords[:, 0], selected_source_coords[:, 0]), \
- 'local attention constructed a cross-sample edge'
- relative = query_coords[:, 1:] - selected_source_coords[:, 1:]
- offset_table = torch.tensor(
- _LOCAL_TWO_HOP_OFFSETS, dtype=full_coords.dtype, device=device,
- )
- offset_matches = (relative[:, None, :] == offset_table[None, :, :]).all(dim=-1)
- assert torch.equal(offset_matches.sum(dim=1), torch.ones_like(offset_matches[:, 0], dtype=torch.long)), \
- 'local attention produced an invalid two-hop offset'
- offset_indices = offset_matches.to(dtype=torch.long).argmax(dim=1)
-
- # Canonical per-query real-KV order is lexicographic relative offset, independent
- # of either full-support or observed-message row ordering.
- order = torch.argsort(query_rows * len(_LOCAL_TWO_HOP_OFFSETS) + offset_indices)
- query_rows = query_rows[order]
- source_ids = source_ids[order]
- offset_indices = offset_indices[order]
- active_query_rows, segment_indices, counts = torch.unique_consecutive(
- query_rows, return_inverse=True, return_counts=True,
- )
- assert counts.max() <= len(_LOCAL_TWO_HOP_OFFSETS), \
- 'local observed neighbourhood exceeds frozen 25-source cap'
- return {
- 'query_rows': active_query_rows,
- 'source_indices': source_ids,
- 'offset_indices': offset_indices,
- 'segment_indices': segment_indices,
- 'neighbour_counts': counts,
- }
-
-
-class _PartialZObsLocalAttention(nn.Module):
- """Shared packed two-hop observed-innovation attention for V12-A."""
-
- def __init__(
- self,
- model_channels: int,
- message_channels: int,
- dim: int,
- time_dim: int,
- num_heads: int,
- num_active_blocks: int,
- ):
- super().__init__()
- assert dim == 128 and time_dim == 32 and num_heads == 8
- assert dim % num_heads == 0
- self.dim = dim
- self.num_heads = num_heads
- self.head_dim = dim // num_heads
- self.alpha = nn.Parameter(torch.zeros(num_active_blocks))
- self.time_proj = nn.Linear(model_channels, time_dim, bias=True)
- self.embed_x = nn.Linear(model_channels + time_dim, dim, bias=True)
- self.embed_y = nn.Linear(message_channels, dim, bias=True)
- self.key_proj = nn.Linear(dim, dim, bias=True)
- self.value_proj = nn.Linear(dim, dim, bias=True)
- self.relative_offset = nn.Parameter(torch.empty(len(_LOCAL_TWO_HOP_OFFSETS), dim))
- self.null_key = nn.Parameter(torch.empty(num_heads, self.head_dim))
- self.out_proj = nn.Linear(dim, model_channels, bias=False)
-
- @staticmethod
- def _rms_norm(value: torch.Tensor) -> torch.Tensor:
- return value * torch.rsqrt(value.square().mean(dim=-1, keepdim=True) + 1e-6)
-
- def forward(
- self,
- hidden: torch.Tensor,
- full_batch: torch.Tensor,
- t_base: torch.Tensor,
- message: torch.Tensor,
- observed_rows: torch.Tensor,
- topology: Dict[str, torch.Tensor],
- block_index: int,
- ) -> Tuple[torch.Tensor, Dict[str, torch.Tensor]]:
- assert 0 <= block_index < self.alpha.numel(), 'terminal block has no V12-A residual'
- query_rows = topology['query_rows']
- source_indices = topology['source_indices']
- offset_indices = topology['offset_indices']
- segment_indices = topology['segment_indices']
- counts = topology['neighbour_counts']
- assert full_batch.ndim == 1 and full_batch.shape[0] == hidden.shape[0]
- assert full_batch.numel() == 0 or (
- full_batch.min() >= 0 and full_batch.max() < t_base.shape[0]
- ), 'full sparse batch coordinate is outside timestep batch'
- assert message.shape == (observed_rows.numel(), 34)
-
- if query_rows.numel() == 0:
- live_zero = sum(parameter.sum() for parameter in self.parameters()) * 0
- empty_delta = hidden.new_empty((0, hidden.shape[1])) + live_zero
- zero = hidden.new_zeros(())
- return empty_delta, {
- 'null_attention_mean': zero,
- 'real_neighbour_entropy_mean': zero,
- 'effective_real_neighbour_count_mean': zero,
- 'active_queries': zero,
- 'singleton_queries': zero,
- 'packed_pairs': zero,
- }
-
- branch_dtype = self.embed_x.weight.dtype
- hidden_branch = hidden.to(dtype=branch_dtype)
- time = self.time_proj(t_base.to(dtype=branch_dtype))
- x_embed = self.embed_x(torch.cat([
- F.layer_norm(hidden_branch, hidden_branch.shape[-1:], eps=1e-6),
- time[full_batch],
- ], dim=-1))
- innovation = self.embed_y(message.to(dtype=branch_dtype)) - x_embed[observed_rows]
- query = self._rms_norm(
- x_embed[query_rows].reshape(-1, self.num_heads, self.head_dim)
- )
- real_key = self.key_proj(innovation)[source_indices]
- real_key = real_key + self.relative_offset[offset_indices]
- real_key = self._rms_norm(real_key.reshape(-1, self.num_heads, self.head_dim))
- real_value = self.value_proj(innovation)[source_indices].reshape(
- -1, self.num_heads, self.head_dim,
- )
- real_logits = (query[segment_indices] * real_key).sum(dim=-1) / np.sqrt(self.head_dim)
- null_key = self._rms_norm(self.null_key)
- null_logits = (query * null_key[None]).sum(dim=-1) / np.sqrt(self.head_dim)
-
- segment_heads = segment_indices[:, None].expand(-1, self.num_heads)
- maxima = null_logits.clone()
- maxima.scatter_reduce_(0, segment_heads, real_logits, reduce='amax', include_self=True)
- real_exp = torch.exp(real_logits - maxima[segment_indices])
- null_exp = torch.exp(null_logits - maxima)
- real_sum = torch.zeros_like(null_exp).scatter_add(0, segment_heads, real_exp)
- denominator = null_exp + real_sum
- real_weight = real_exp / denominator[segment_indices]
- null_weight = null_exp / denominator
- attended = hidden_branch.new_zeros(
- (query_rows.numel(), self.num_heads, self.head_dim),
- )
- attended.index_add_(
- 0, segment_indices, real_weight[..., None] * real_value,
- )
- delta = self.out_proj(attended.reshape(query_rows.numel(), self.dim))
- delta = delta * self.alpha[block_index].to(dtype=delta.dtype)
-
- # Entropy is conditional on selecting real evidence. Null-vs-real
- # acceptance is reported independently as null_attention_mean; mixing
- # that mass into this distribution would make singleton entropy nonzero.
- real_conditional_weight = real_exp / real_sum[segment_indices].clamp_min(
- torch.finfo(real_sum.dtype).tiny
- )
- entropy_terms = -real_conditional_weight * torch.log(
- real_conditional_weight.clamp_min(
- torch.finfo(real_conditional_weight.dtype).tiny
- )
- )
- real_entropy = torch.zeros_like(null_weight).scatter_add(
- 0, segment_heads, entropy_terms,
- )
- return delta, {
- 'null_attention_mean': null_weight.mean(),
- 'real_neighbour_entropy_mean': real_entropy.mean(),
- 'effective_real_neighbour_count_mean': real_entropy.exp().mean(),
- 'active_queries': delta.new_tensor(float(query_rows.numel())),
- 'singleton_queries': delta.new_tensor(float((counts == 1).sum())),
- 'packed_pairs': delta.new_tensor(float(source_indices.numel())),
- }
-
-
-class _V12LACrossRope(nn.Module):
- """Apply the same 3D rotary basis to independently packed Q and K rows."""
-
- def __init__(
- self,
- head_dim: int,
- rope_freq: Tuple[float, float],
- reserved_dims: int = 2,
- ):
- super().__init__()
- content_dim = head_dim - reserved_dims
- assert content_dim > 0 and content_dim % 6 == 0
- self.head_dim = head_dim
- self.content_dim = content_dim
- freq_dim = content_dim // 2 // 3
- frequencies = torch.arange(freq_dim, dtype=torch.float32) / freq_dim
- frequencies = rope_freq[0] / (rope_freq[1] ** frequencies)
- self.register_buffer('frequencies', frequencies, persistent=False)
-
- def forward(self, value: torch.Tensor, coords: torch.Tensor) -> torch.Tensor:
- assert value.ndim == 3 and value.shape[-1] == self.head_dim
- assert coords.shape == (value.shape[0], 3)
- if value.shape[0] == 0:
- return value
- phases = torch.outer(
- coords.reshape(-1).float(), self.frequencies,
- ).reshape(coords.shape[0], self.content_dim // 2)
- phases = torch.polar(torch.ones_like(phases), phases).unsqueeze(1)
- content = torch.view_as_complex(
- value[..., :self.content_dim].float().reshape(
- value.shape[0], value.shape[1], -1, 2,
- )
- )
- rotated = torch.view_as_real(content * phases).reshape(
- value.shape[0], value.shape[1], self.content_dim,
- ).to(dtype=value.dtype)
- return torch.cat([rotated, value[..., self.content_dim:]], dim=-1)
-
-
-class _PartialZObsLearnedAttention(nn.Module):
- """Five-site V12LA observation attention with an exact carrier anchor."""
-
- def __init__(
- self,
- model_channels: int,
- num_heads: int,
- sites: Sequence[int],
- rope_freq: Tuple[float, float],
- null_prior: float = 0.5,
- ):
- super().__init__()
- self.sites = tuple(int(site) for site in sites)
- assert self.sites == (5, 10, 15, 20, 25), self.sites
- assert num_heads == 12
- assert model_channels % num_heads == 0
- assert (model_channels // num_heads - 2) > 0
- assert (model_channels // num_heads - 2) % 6 == 0
- self.model_channels = model_channels
- self.num_heads = num_heads
- self.head_dim = model_channels // num_heads
- self.content_dim = self.head_dim - 2
- self.site_to_slot = {site: slot for slot, site in enumerate(self.sites)}
- self.null_prior = float(null_prior)
- assert 0.0 < self.null_prior < 1.0, \
- f'learned-attention null prior must lie in (0,1), got {self.null_prior}'
- self.null_logit_offset = float(
- np.log(self.null_prior) - np.log1p(-self.null_prior)
- )
-
- # K/V are source-only and shared across all five sites. Q and the
- # zero-start deviation/output matrix are site-specific.
- self.key_proj = nn.Linear(model_channels, model_channels, bias=False)
- self.value_proj = nn.Linear(model_channels, model_channels, bias=False)
- self.query_proj = nn.ModuleList([
- nn.Linear(model_channels, model_channels, bias=False)
- for _ in self.sites
- ])
- self.delta_proj = nn.ModuleList([
- nn.Linear(model_channels, model_channels, bias=False)
- for _ in self.sites
- ])
- self.null_key = nn.Parameter(torch.empty(num_heads, self.content_dim))
- self.rope = _V12LACrossRope(self.head_dim, rope_freq, reserved_dims=2)
- self.reset_parameters()
-
- def reset_parameters(self) -> None:
- nn.init.xavier_uniform_(self.key_proj.weight)
- # Attention mixes the already trained V11 carrier basis at warm start;
- # the value map may then adapt, but does not begin as a random rewrite.
- nn.init.eye_(self.value_proj.weight)
- nn.init.xavier_uniform_(self.null_key)
- for query_proj in self.query_proj:
- nn.init.xavier_uniform_(query_proj.weight)
- for delta_proj in self.delta_proj:
- nn.init.zeros_(delta_proj.weight)
-
- @staticmethod
- def _rms_norm(value: torch.Tensor) -> torch.Tensor:
- normalized = value.float() * torch.rsqrt(
- value.float().square().mean(dim=-1, keepdim=True) + 1e-6
- )
- return normalized.to(dtype=value.dtype)
-
- @staticmethod
- def _anchor(
- carrier: torch.Tensor,
- observed_rows: torch.Tensor,
- full_rows: int,
- ) -> torch.Tensor:
- """Evaluate fixed coordinate/null attention without an additive scatter."""
- assert carrier.ndim == 2
- assert observed_rows.ndim == 1 and observed_rows.dtype == torch.long
- assert carrier.shape[0] == observed_rows.numel()
- assert full_rows >= observed_rows.numel()
- if observed_rows.numel() != 0:
- assert observed_rows.min() >= 0 and observed_rows.max() < full_rows
- assert torch.unique(observed_rows).numel() == observed_rows.numel()
- # Index zero is the exact-zero null value. Every observed row is then
- # redirected to its own carrier row; unmatched rows keep selecting null.
- values = torch.cat([carrier.new_zeros((1, carrier.shape[1])), carrier], dim=0)
- anchor_index = torch.zeros(full_rows, dtype=torch.long, device=carrier.device)
- anchor_index[observed_rows] = torch.arange(
- 1, observed_rows.numel() + 1, dtype=torch.long, device=carrier.device,
- )
- return values[anchor_index]
-
- def build_cache(
- self,
- full_coords: torch.Tensor,
- full_layout: List[slice],
- carrier: torch.Tensor,
- observed_rows: torch.Tensor,
- ) -> Dict[str, Any]:
- """Build the exact anchor and the shared per-sample O+null K/V cache."""
- assert full_coords.ndim == 2 and full_coords.shape[1] == 4
- assert carrier.shape == (observed_rows.numel(), self.model_channels)
- assert len(full_layout) > 0
- anchor = self._anchor(carrier, observed_rows, full_coords.shape[0])
- observed_coords = full_coords[observed_rows]
- observed_batch = observed_coords[:, 0].long()
- assert observed_batch.numel() == 0 or (
- observed_batch.min() >= 0 and observed_batch.max() < len(full_layout)
- )
-
- branch_dtype = self.key_proj.weight.dtype
- carrier_branch = carrier.to(dtype=branch_dtype)
- carrier_norm = F.layer_norm(
- carrier_branch, (self.model_channels,), eps=1e-6,
- )
- real_key = self.key_proj(carrier_norm).reshape(
- -1, self.num_heads, self.head_dim,
- )
- real_key = self.rope(real_key, observed_coords[:, 1:])
- real_key = self._rms_norm(real_key[..., :self.content_dim])
- real_key = torch.cat([
- real_key,
- real_key.new_zeros((real_key.shape[0], self.num_heads, 2)),
- ], dim=-1)
- real_value = self.value_proj(carrier_branch).reshape(
- -1, self.num_heads, self.head_dim,
- )
- null_content = self._rms_norm(
- self.null_key.to(dtype=real_key.dtype)
- )
-
- packed_kv = []
- observed_counts = []
- for batch_index in range(len(full_layout)):
- source_indices = torch.nonzero(
- observed_batch == batch_index, as_tuple=False,
- ).flatten()
- observed_count = int(source_indices.numel())
- observed_counts.append(observed_count)
- # One null competes with O real keys. log(O) cancels its cardinality
- # disadvantage; the constant logit offset sets the null mass only in
- # the equal-content-logit reference case, not after learned Q/K drift.
- null_reserved = real_key.new_zeros((self.num_heads, 2))
- null_reserved[:, 0] = np.sqrt(self.head_dim) * (
- np.log(max(observed_count, 1)) + self.null_logit_offset
- )
- null_key = torch.cat([null_content, null_reserved], dim=-1).unsqueeze(0)
- null_value = real_value.new_zeros((1, self.num_heads, self.head_dim))
- keys = torch.cat([real_key[source_indices], null_key], dim=0)
- values = torch.cat([real_value[source_indices], null_value], dim=0)
- packed_kv.append(torch.stack([keys, values], dim=1))
-
- kv = sp.VarLenTensor.from_tensor_list(packed_kv)
- return {
- 'anchor': anchor,
- 'kv': kv,
- 'observed_rows': observed_rows,
- 'observed_counts': tuple(observed_counts),
- }
-
- def forward(
- self,
- hidden: torch.Tensor,
- full_coords: torch.Tensor,
- full_layout: List[slice],
- cache: Dict[str, Any],
- block_index: int,
- ) -> Tuple[torch.Tensor, Dict[str, torch.Tensor]]:
- assert block_index in self.site_to_slot, block_index
- assert hidden.shape == (full_coords.shape[0], self.model_channels)
- slot = self.site_to_slot[block_index]
- branch_dtype = self.query_proj[slot].weight.dtype
- hidden_branch = hidden.to(dtype=branch_dtype)
- query = self.query_proj[slot](F.layer_norm(
- hidden_branch, (self.model_channels,), eps=1e-6,
- )).reshape(-1, self.num_heads, self.head_dim)
- query = self.rope(query, full_coords[:, 1:])
- query_content = self._rms_norm(query[..., :self.content_dim])
- query_reserved = query.new_zeros((query.shape[0], self.num_heads, 2))
- query_reserved[..., 0] = 1
- query = torch.cat([query_content, query_reserved], dim=-1)
- query_varlen = sp.VarLenTensor(query, full_layout)
- attended = sp.sparse_scaled_dot_product_attention(
- query_varlen, cache['kv'],
- ).feats.reshape(-1, self.model_channels)
-
- anchor = cache['anchor'].to(dtype=attended.dtype)
- deviation_input = attended - anchor
- correction = self.delta_proj[slot](deviation_input)
- inject = anchor + correction
- eps = torch.finfo(inject.dtype).eps
- observed_rows = cache['observed_rows']
- observed_mask = torch.zeros(
- inject.shape[0], dtype=torch.bool, device=inject.device,
- )
- observed_mask[observed_rows] = True
- return inject, {
- 'correction_to_anchor_norm_ratio': correction.norm() / anchor.norm().clamp_min(eps),
- 'correction_to_hidden_norm_ratio': correction.norm() / hidden_branch.norm().clamp_min(eps),
- 'observed_correction_l2_norm': correction[observed_mask].float().norm(),
- 'unobserved_correction_l2_norm': correction[~observed_mask].float().norm(),
- 'attention_output_l2_norm': attended.float().norm(),
- 'anchor_l2_norm': anchor.float().norm(),
- 'delta_weight_l2_norm': self.delta_proj[slot].weight.float().norm(),
- }
-
-class SLatFlowModel(nn.Module):
- """
- Structured Latent Flow Model for 3D generation.
-
- Supports two conditioning modes:
- - "cross": Standard cross-attention with image features
- - "proj": View-aligned projection attention with camera-aware features
- """
- def __init__(
- self,
- resolution: int,
- in_channels: int,
- model_channels: int,
- cond_channels: int,
- out_channels: int,
- num_blocks: int,
- num_heads: Optional[int] = None,
- num_head_channels: Optional[int] = 64,
- mlp_ratio: float = 4,
- pe_mode: Literal["ape", "rope"] = "ape",
- rope_freq: Tuple[float, float] = (1.0, 10000.0),
- dtype: str = 'float32',
- use_checkpoint: bool = False,
- share_mod: bool = False,
- initialization: str = 'vanilla',
- qk_rms_norm: bool = False,
- qk_rms_norm_cross: bool = False,
- image_attn_mode: Literal["cross", "proj", "gated_proj"] = "cross",
- proj_in_channels: Optional[int] = None,
- vae_in_channels: Optional[int] = None,
- zobs_inject: bool = False,
- zobs_inject_in_channels: int = 33,
- zobs_inject_scale: float = 1.0,
- partial_zobs_inject: bool = False,
- partial_zobs_in_channels: int = 34,
- partial_zobs_inject_position: Literal['post_block', 'pre_mlp'] = 'post_block',
- legacy_zobs_zero_input: bool = False,
- partial_zobs_innovation_gate: bool = False,
- innovation_gate_dim: int = 128,
- innovation_gate_time_dim: int = 32,
- partial_zobs_innovation_gate_train_only: bool = False,
- partial_zobs_local_attention: bool = False,
- local_attention_dim: int = 128,
- local_attention_time_dim: int = 32,
- local_attention_heads: int = 8,
- partial_zobs_local_attention_train_only: bool = False,
- partial_zobs_learned_attention: bool = False,
- learned_attention_sites: Sequence[int] = (5, 10, 15, 20, 25),
- learned_attention_heads: int = 12,
- learned_attention_null_prior: float = 0.5,
- partial_zobs_learned_attention_train_only: bool = False,
- **kwargs
- ):
- super().__init__()
- self.resolution = resolution
- self.in_channels = in_channels
- self.model_channels = model_channels
- self.cond_channels = cond_channels
- self.out_channels = out_channels
- self.num_blocks = num_blocks
- self.num_heads = num_heads or model_channels // num_head_channels
- self.mlp_ratio = mlp_ratio
- self.pe_mode = pe_mode
- self.use_checkpoint = use_checkpoint
- self.share_mod = share_mod
- self.initialization = initialization
- self.qk_rms_norm = qk_rms_norm
- self.qk_rms_norm_cross = qk_rms_norm_cross
- self.image_attn_mode = image_attn_mode
- self.proj_in_channels = proj_in_channels
- self.vae_in_channels = vae_in_channels
- self.dtype = str_to_dtype(dtype)
-
- self.t_embedder = TimestepEmbedder(model_channels)
- if share_mod:
- self.adaLN_modulation = nn.Sequential(
- nn.SiLU(),
- nn.Linear(model_channels, 6 * model_channels, bias=True)
- )
-
- if pe_mode == "ape":
- self.pos_embedder = AbsolutePositionEmbedder(model_channels)
-
- self.input_layer = sp.SparseLinear(in_channels, model_channels)
- # Trained-bridge: single shared per-block injector of [z_obs (x) vis (32) | vis (1)] -> model_channels.
- self.zobs_inject = zobs_inject
- self.zobs_inject_scale = float(zobs_inject_scale)
- self.zobs_injector = sp.SparseLinear(zobs_inject_in_channels, model_channels) if zobs_inject else None
- # v8-partial: the observation lives only on its observed support. This adapter is
- # deliberately bias-free: absent rows never acquire a learned content/fill value.
- self.partial_zobs_inject = partial_zobs_inject
- self.partial_zobs_in_channels = partial_zobs_in_channels
- assert partial_zobs_inject_position in ('post_block', 'pre_mlp'), \
- f'partial_zobs_inject_position must be post_block or pre_mlp, got {partial_zobs_inject_position!r}'
- self.partial_zobs_inject_position = partial_zobs_inject_position
- if partial_zobs_inject_position == 'pre_mlp':
- assert not any((
- partial_zobs_innovation_gate,
- partial_zobs_local_attention,
- partial_zobs_learned_attention,
- )), 'partial_zobs_inject_position=pre_mlp is incompatible with post-block experimental branches'
- self.legacy_zobs_zero_input = legacy_zobs_zero_input
- self.partial_zobs_injector = (
- sp.SparseLinear(partial_zobs_in_channels, model_channels, bias=False)
- if partial_zobs_inject else None
- )
- self.partial_zobs_innovation_gate = partial_zobs_innovation_gate
- self.partial_zobs_innovation_gate_train_only = partial_zobs_innovation_gate_train_only
- if partial_zobs_innovation_gate_train_only:
- assert partial_zobs_innovation_gate, \
- 'partial_zobs_innovation_gate_train_only requires partial_zobs_innovation_gate=true'
- if partial_zobs_innovation_gate:
- assert partial_zobs_inject, 'partial_zobs_innovation_gate requires partial_zobs_inject=true'
- assert partial_zobs_in_channels == 34, \
- f'partial_zobs_innovation_gate requires 34 input channels, got {partial_zobs_in_channels}'
- assert innovation_gate_dim == 128, f'innovation_gate_dim is fixed at 128, got {innovation_gate_dim}'
- assert innovation_gate_time_dim == 32, \
- f'innovation_gate_time_dim is fixed at 32, got {innovation_gate_time_dim}'
- assert model_channels == 1536, f'partial_zobs_innovation_gate requires 1536 model channels, got {model_channels}'
- assert num_blocks == 30, f'partial_zobs_innovation_gate requires 30 blocks, got {num_blocks}'
- self.innovation_gate = _PartialZObsInnovationGate(
- model_channels, partial_zobs_in_channels, innovation_gate_dim,
- innovation_gate_time_dim, num_blocks,
- )
- else:
- self.innovation_gate = None
- self.partial_zobs_local_attention = partial_zobs_local_attention
- self.partial_zobs_local_attention_train_only = partial_zobs_local_attention_train_only
- if partial_zobs_local_attention_train_only:
- assert partial_zobs_local_attention, \
- 'partial_zobs_local_attention_train_only requires partial_zobs_local_attention=true'
- assert sum(bool(value) for value in (
- partial_zobs_innovation_gate,
- partial_zobs_local_attention,
- partial_zobs_learned_attention,
- )) <= 1, 'partial-zobs experimental attention/gate branches are mutually exclusive'
- if partial_zobs_local_attention:
- assert partial_zobs_inject, 'partial_zobs_local_attention requires partial_zobs_inject=true'
- assert partial_zobs_in_channels == 34, \
- f'partial_zobs_local_attention requires 34 input channels, got {partial_zobs_in_channels}'
- assert local_attention_dim == 128, \
- f'local_attention_dim is fixed at 128, got {local_attention_dim}'
- assert local_attention_time_dim == 32, \
- f'local_attention_time_dim is fixed at 32, got {local_attention_time_dim}'
- assert local_attention_heads == 8, \
- f'local_attention_heads is fixed at 8, got {local_attention_heads}'
- assert model_channels == 1536, \
- f'partial_zobs_local_attention requires 1536 model channels, got {model_channels}'
- assert num_blocks == 30, \
- f'partial_zobs_local_attention requires 30 blocks, got {num_blocks}'
- self.local_obs_attention = _PartialZObsLocalAttention(
- model_channels, partial_zobs_in_channels, local_attention_dim,
- local_attention_time_dim, local_attention_heads, num_blocks - 1,
- )
- else:
- self.local_obs_attention = None
-
- self.partial_zobs_learned_attention = partial_zobs_learned_attention
- self.partial_zobs_learned_attention_train_only = \
- partial_zobs_learned_attention_train_only
- if partial_zobs_learned_attention_train_only:
- assert partial_zobs_learned_attention, \
- 'partial_zobs_learned_attention_train_only requires partial_zobs_learned_attention=true'
- if partial_zobs_learned_attention:
- assert partial_zobs_inject, \
- 'partial_zobs_learned_attention requires partial_zobs_inject=true'
- assert partial_zobs_in_channels == 34, \
- f'partial_zobs_learned_attention requires 34 input channels, got {partial_zobs_in_channels}'
- assert model_channels == 1536, \
- f'partial_zobs_learned_attention requires 1536 model channels, got {model_channels}'
- assert num_blocks == 30, \
- f'partial_zobs_learned_attention requires 30 blocks, got {num_blocks}'
- assert learned_attention_heads == 12, \
- f'partial_zobs_learned_attention requires 12 heads, got {learned_attention_heads}'
- assert tuple(learned_attention_sites) == (5, 10, 15, 20, 25), \
- f'V12LA sites are fixed at [5,10,15,20,25], got {learned_attention_sites}'
- self.learned_attention = _PartialZObsLearnedAttention(
- model_channels,
- learned_attention_heads,
- learned_attention_sites,
- rope_freq,
- learned_attention_null_prior,
- )
- else:
- self.learned_attention = None
-
- self.blocks = nn.ModuleList([
- ModulatedSparseTransformerCrossBlock(
- model_channels,
- cond_channels,
- num_heads=self.num_heads,
- mlp_ratio=self.mlp_ratio,
- attn_mode='full',
- use_checkpoint=self.use_checkpoint,
- use_rope=(pe_mode == "rope"),
- rope_freq=rope_freq,
- share_mod=self.share_mod,
- qk_rms_norm=self.qk_rms_norm,
- qk_rms_norm_cross=self.qk_rms_norm_cross,
- image_attn_mode=image_attn_mode,
- proj_in_channels=proj_in_channels,
- vae_in_channels=vae_in_channels,
- )
- for _ in range(num_blocks)
- ])
-
- self.out_layer = sp.SparseLinear(model_channels, out_channels)
-
- self.initialize_weights()
- self.convert_to(self.dtype)
- if self.partial_zobs_innovation_gate_train_only:
- self._freeze_for_partial_zobs_innovation_gate()
- if self.partial_zobs_local_attention_train_only:
- self._freeze_for_partial_zobs_local_attention()
- if self.partial_zobs_learned_attention_train_only:
- self._freeze_for_partial_zobs_learned_attention()
-
- @property
- def device(self) -> torch.device:
- """
- Return the device of the model.
- """
- return next(self.parameters()).device
-
- def convert_to(self, dtype: torch.dtype) -> None:
- """
- Convert the torso of the model to the specified dtype.
- """
- self.dtype = dtype
- self.blocks.apply(partial(convert_module_to, dtype=dtype))
- if self.learned_attention is not None:
- self.learned_attention.apply(partial(convert_module_to, dtype=dtype))
-
- def initialize_weights(self) -> None:
- if self.initialization == 'vanilla':
- # Initialize transformer layers:
- def _basic_init(module):
- if isinstance(module, nn.Linear):
- torch.nn.init.xavier_uniform_(module.weight)
- if module.bias is not None:
- nn.init.constant_(module.bias, 0)
- self.apply(_basic_init)
-
- # Initialize timestep embedding MLP:
- nn.init.normal_(self.t_embedder.mlp[0].weight, std=0.02)
- nn.init.normal_(self.t_embedder.mlp[2].weight, std=0.02)
-
- # Zero-out adaLN modulation layers in DiT blocks:
- if self.share_mod:
- nn.init.constant_(self.adaLN_modulation[-1].weight, 0)
- nn.init.constant_(self.adaLN_modulation[-1].bias, 0)
- else:
- for block in self.blocks:
- nn.init.constant_(block.adaLN_modulation[-1].weight, 0)
- nn.init.constant_(block.adaLN_modulation[-1].bias, 0)
-
- # Zero-out output layers:
- nn.init.constant_(self.out_layer.weight, 0)
- nn.init.constant_(self.out_layer.bias, 0)
-
- elif self.initialization == 'scaled':
- # Initialize transformer layers:
- def _basic_init(module):
- if isinstance(module, nn.Linear):
- torch.nn.init.normal_(module.weight, std=np.sqrt(2.0 / (5.0 * self.model_channels)))
- if module.bias is not None:
- nn.init.constant_(module.bias, 0)
- self.apply(_basic_init)
-
- # Scaled init for to_out and ffn2
- def _scaled_init(module):
- if isinstance(module, nn.Linear):
- torch.nn.init.normal_(module.weight, std=1.0 / np.sqrt(5 * self.num_blocks * self.model_channels))
- if module.bias is not None:
- nn.init.constant_(module.bias, 0)
- for block in self.blocks:
- block.self_attn.to_out.apply(_scaled_init)
- # Handle cross, proj, and gated_proj modes
- if self.image_attn_mode in ("proj", "gated_proj"):
- block.cross_attn.cross_attn_block.to_out.apply(_scaled_init)
- else:
- block.cross_attn.to_out.apply(_scaled_init)
- block.mlp.mlp[2].apply(_scaled_init)
-
- # Initialize input layer to make the initial representation have variance 1
- nn.init.normal_(self.input_layer.weight, std=1.0 / np.sqrt(self.in_channels))
- nn.init.zeros_(self.input_layer.bias)
-
- # Initialize timestep embedding MLP:
- nn.init.normal_(self.t_embedder.mlp[0].weight, std=0.02)
- nn.init.normal_(self.t_embedder.mlp[2].weight, std=0.02)
-
- # Zero-out adaLN modulation layers in DiT blocks:
- if self.share_mod:
- nn.init.constant_(self.adaLN_modulation[-1].weight, 0)
- nn.init.constant_(self.adaLN_modulation[-1].bias, 0)
- else:
- for block in self.blocks:
- nn.init.constant_(block.adaLN_modulation[-1].weight, 0)
- nn.init.constant_(block.adaLN_modulation[-1].bias, 0)
-
- # Zero-out output layers:
- nn.init.constant_(self.out_layer.weight, 0)
- nn.init.constant_(self.out_layer.bias, 0)
-
- # Trained-bridge: zero-init the z_obs injector LAST (after _basic_init), so step-0 injection == 0.
- if getattr(self, 'zobs_injector', None) is not None:
- nn.init.zeros_(self.zobs_injector.weight)
- nn.init.zeros_(self.zobs_injector.bias)
- if getattr(self, 'partial_zobs_injector', None) is not None:
- nn.init.zeros_(self.partial_zobs_injector.weight)
- if getattr(self, 'innovation_gate', None) is not None:
- nn.init.zeros_(self.innovation_gate.alpha)
- if getattr(self, 'local_obs_attention', None) is not None:
- # V12-A freezes vanilla Xavier initialization independently of the
- # scaled V11 backbone initialization mode. Only alpha starts at zero.
- for module in self.local_obs_attention.modules():
- if isinstance(module, nn.Linear):
- nn.init.xavier_uniform_(module.weight)
- if module.bias is not None:
- nn.init.zeros_(module.bias)
- nn.init.xavier_uniform_(self.local_obs_attention.relative_offset)
- nn.init.xavier_uniform_(self.local_obs_attention.null_key)
- nn.init.zeros_(self.local_obs_attention.alpha)
- if getattr(self, 'learned_attention', None) is not None:
- self.learned_attention.reset_parameters()
-
- def _freeze_for_partial_zobs_innovation_gate(self) -> None:
- """Make the V12 gate the sole trainable branch before an optimizer is built."""
- assert self.innovation_gate is not None
- for parameter in self.parameters():
- parameter.requires_grad_(False)
- for parameter in self.innovation_gate.parameters():
- parameter.requires_grad_(True)
- trainable = [name for name, parameter in self.named_parameters() if parameter.requires_grad]
- expected = [f'innovation_gate.{name}' for name in self.innovation_gate.state_dict()]
- assert trainable == expected, (trainable, expected)
- assert sum(parameter.numel() for parameter in self.parameters() if parameter.requires_grad) == 473663
-
- def _freeze_for_partial_zobs_local_attention(self) -> None:
- """Make the V12-A local attention the sole trainable warm-start branch."""
- assert self.local_obs_attention is not None
- for parameter in self.parameters():
- parameter.requires_grad_(False)
- for parameter in self.local_obs_attention.parameters():
- parameter.requires_grad_(True)
- trainable = [name for name, parameter in self.named_parameters() if parameter.requires_grad]
- expected = [f'local_obs_attention.{name}' for name in self.local_obs_attention.state_dict()]
- assert trainable == expected, (trainable, expected)
- assert len(trainable) == 14
- assert sum(parameter.numel() for parameter in self.parameters() if parameter.requires_grad) == 487485
-
- def _freeze_for_partial_zobs_learned_attention(self) -> None:
- """Make the V12LA module the sole trainable warm-start branch."""
- assert self.learned_attention is not None
- for parameter in self.parameters():
- parameter.requires_grad_(False)
- for parameter in self.learned_attention.parameters():
- parameter.requires_grad_(True)
- trainable = [name for name, parameter in self.named_parameters() if parameter.requires_grad]
- expected = [
- f'learned_attention.{name}'
- for name, _ in self.learned_attention.named_parameters()
- ]
- assert trainable == expected, (trainable, expected)
- assert len(trainable) == 13, trainable
-
- def _partial_zobs_message(
- self,
- x: sp.SparseTensor,
- partial_z_obs: sp.SparseTensor,
- partial_z_obs_keep_mask: Optional[torch.Tensor],
- ) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
- """Project a variable-support observation and join its messages onto ``x`` rows.
-
- The return value stays variable-length: projected messages plus their exact full-row
- indices. Neither observation content nor a zero-filled model-channel tensor is
- materialized on full support. ``torch.unique(..., dim=0)`` makes the coordinate join
- exact and rejects duplicate or non-subset partial coordinates instead of assuming a
- row order.
- """
- adapter = self.partial_zobs_injector
- assert adapter is not None
- assert partial_z_obs.feats.ndim == 2 and \
- partial_z_obs.feats.shape[-1] == self.partial_zobs_in_channels, \
- f'partial_z_obs must be [N,{self.partial_zobs_in_channels}], got {tuple(partial_z_obs.feats.shape)}'
- assert partial_z_obs.coords.ndim == 2 and partial_z_obs.coords.shape[1] == x.coords.shape[1], \
- f'partial_z_obs coordinate rank mismatch: {tuple(partial_z_obs.coords.shape)} vs {tuple(x.coords.shape)}'
-
- active = torch.ones(partial_z_obs.feats.shape[0], dtype=torch.bool, device=partial_z_obs.feats.device)
- if partial_z_obs_keep_mask is not None:
- assert partial_z_obs_keep_mask.ndim == 1 and partial_z_obs_keep_mask.shape[0] == x.shape[0], \
- f'partial_z_obs_keep_mask must be [B={x.shape[0]}], got {tuple(partial_z_obs_keep_mask.shape)}'
- keep = partial_z_obs_keep_mask.to(device=partial_z_obs.feats.device, dtype=torch.bool)
- active = keep[partial_z_obs.coords[:, 0].long()]
-
- active_coords = partial_z_obs.coords[active]
- active_feats = partial_z_obs.feats[active]
- # F.linear on a zero-row tensor is intentional: it keeps the adapter parameters in the
- # graph on all-drop batches, so DDP sees a zero (rather than None) gradient.
- active_messages = F.linear(active_feats, adapter.weight, None)
-
- full_rows = torch.empty(0, dtype=torch.long, device=x.feats.device)
- if active_coords.shape[0] != 0:
- all_coords = torch.cat([x.coords, active_coords], dim=0)
- _, inverse = torch.unique(all_coords, dim=0, return_inverse=True)
- x_ids = inverse[:x.coords.shape[0]]
- partial_ids = inverse[x.coords.shape[0]:]
- assert torch.unique(x_ids).numel() == x_ids.numel(), 'x contains duplicate coordinates'
- assert torch.unique(partial_ids).numel() == partial_ids.numel(), 'partial_z_obs contains duplicate coordinates'
- ordered_x_ids, ordered_x_rows = torch.sort(x_ids)
- positions = torch.searchsorted(ordered_x_ids, partial_ids)
- assert (positions < ordered_x_ids.numel()).all() and \
- torch.equal(ordered_x_ids[positions], partial_ids), \
- 'partial_z_obs coordinates must be an exact subset of current x coordinates'
- full_rows = ordered_x_rows[positions]
-
- return active_messages, full_rows, active_feats, active_coords[:, 0].long()
-
- def forward(
- self,
- x: sp.SparseTensor,
- t: torch.Tensor,
- cond: Union[torch.Tensor, List[torch.Tensor], Dict[str, Union[torch.Tensor, sp.SparseTensor]], Tuple],
- concat_cond: Optional[sp.SparseTensor] = None,
- z_obs: Optional[sp.SparseTensor] = None,
- partial_z_obs: Optional[sp.SparseTensor] = None,
- partial_z_obs_keep_mask: Optional[torch.Tensor] = None,
- **kwargs
- ) -> sp.SparseTensor:
- """
- Forward pass.
-
- Args:
- x: SparseTensor input
- t: Timestep tensor [B]
- cond: Conditioning tensor. For "cross" mode: list of tensors or tensor.
- For "proj" mode: dict {'global': global_cond, 'proj': proj_cond}
- or tuple of (global_cond, proj_cond)
- concat_cond: Optional concatenation condition
- z_obs: Optional 33ch [z_obs(32)|vis(1)] sparse tensor, same coords as x. Feeds ONLY
- the zobs_injector (v6 patchcross path: concat_cond is the plain 32ch shape
- latent and z_obs never enters the input_layer). When None, the legacy 97ch
- concat slice below applies — shipped/v4 behavior is bit-identical.
- partial_z_obs: Optional variable-length v8 observation
- [normalized_z_obs(32)|coverage(1)|boundary_distance(1)].
- Its coordinates must be a subset of x; it is consumed only by the dedicated,
- bias-free partial adapter and is never converted to full-support content.
-
- Returns:
- SparseTensor output
- """
- if concat_cond is not None:
- x = sp.sparse_cat([x, concat_cond], dim=-1)
-
- h = self.input_layer(x)
- h = manual_cast(h, self.dtype)
-
- # Trained-bridge: compute the per-block injection vector ONCE (shared zero-init linear, added each block).
- # Sources, in priority order: separate z_obs kwarg (v6), else the trailing channels of a
- # 97-ch [x_t|shape|z_obs|vis] input (v4). z_obs*vis==0 at vis=0 in both.
- zobs_inject = None
- _injector = getattr(self, 'zobs_injector', None)
- if z_obs is not None:
- # Fail fast: without an injector the kwarg would be a silent no-op, and in the 64ch
- # patchcross architecture the injector is the ONLY z_obs consumption path.
- assert _injector is not None, \
- 'z_obs kwarg passed but the denoiser was built without zobs_inject=true'
- # Full coords equality is asserted at data-load; a row-count check catches gross mismatch.
- assert z_obs.feats.shape[-1] == 33 and z_obs.feats.shape[0] == x.feats.shape[0], \
- f'z_obs kwarg mismatch: {tuple(z_obs.feats.shape)} vs x rows {x.feats.shape[0]}'
- _zo = z_obs.feats[:, :32]
- _vis = z_obs.feats[:, 32:33]
- zobs_inject = manual_cast(self.zobs_injector(z_obs.replace(torch.cat([_zo * _vis, _vis], dim=-1))), self.dtype)
- if not getattr(self, '_zobs_kwarg_logged', False): # in-log proof hook (once per process)
- print('[zobs_injector] consuming separate z_obs kwarg (patchcross path)', flush=True)
- self._zobs_kwarg_logged = True
- elif _injector is not None and x.feats.shape[-1] >= 97:
- _zo = x.feats[:, 64:96]
- _vis = x.feats[:, 96:97]
- zobs_inject = manual_cast(self.zobs_injector(x.replace(torch.cat([_zo * _vis, _vis], dim=-1))), self.dtype)
- elif _injector is not None and self.legacy_zobs_zero_input:
- # 64ch v8 shell with a legacy injector: preserve the legacy all-zero z_obs mode,
- # including its learned per-block bias, without materialising observation content.
- # Evaluate one zero row and broadcast the result: this is exactly W*0+b, keeps a
- # zero (non-None) weight gradient under DDP, and avoids an N-row 33ch GEMM.
- zero_output = F.linear(
- x.feats.new_zeros((1, _injector.in_features)),
- _injector.weight,
- _injector.bias,
- )
- zobs_inject = manual_cast(
- x.replace(zero_output.expand(x.feats.shape[0], -1)), self.dtype
- )
- elif _injector is not None and not getattr(self, '_zobs_none_warned', False):
- # Injector present but NO z_obs source (64ch input, kwarg omitted): the trained
- # per-block bias is NOT applied — a third mode distinct from "z_obs dropped" (an
- # all-zero 33ch tensor still adds the bias each block). A nozobs arm on a patchcross
- # model must pass the zero tensor, never omit the kwarg.
- print('[zobs_injector] WARNING: injector present but no z_obs source (kwarg omitted, '
- 'input < 97ch) — per-block bias NOT applied; pass an all-zero 33ch z_obs for a '
- 'true nozobs arm', flush=True)
- self._zobs_none_warned = True
-
- partial_zobs_inject = None
- partial_zobs_rows = None
- partial_zobs_feats = None
- partial_zobs_batch = None
- if partial_z_obs is not None:
- assert self.partial_zobs_injector is not None, \
- 'partial_z_obs passed but denoiser was built without partial_zobs_inject=true'
- partial_zobs_inject, partial_zobs_rows, partial_zobs_feats, partial_zobs_batch = self._partial_zobs_message(
- x, partial_z_obs, partial_z_obs_keep_mask
- )
- partial_zobs_inject = manual_cast(partial_zobs_inject, self.dtype)
- elif self.partial_zobs_injector is not None:
- raise AssertionError('partial_zobs_inject=true requires a separate partial_z_obs tensor')
-
- if zobs_inject is not None and self.zobs_inject_scale != 1.0:
- zobs_inject = zobs_inject.replace(zobs_inject.feats * self.zobs_inject_scale)
- if not getattr(self, '_zobs_inject_scale_logged', False):
- print(f'[zobs_injector] output scale={self.zobs_inject_scale:g}', flush=True)
- self._zobs_inject_scale_logged = True
-
- # Keep the pre-AdaLN embedding for the innovation gate; blocks retain their existing
- # modulation input exactly.
- t_base = self.t_embedder(t)
- t_emb = t_base
- if self.share_mod:
- t_emb = self.adaLN_modulation(t_emb)
- t_emb = manual_cast(t_emb, self.dtype)
-
- innovation_time = None
- innovation_y = None
- innovation_gate_y = None
- if self.innovation_gate is not None and partial_zobs_feats is not None:
- # Observation-only terms are shared by all blocks. Empty rows deliberately pass
- # through these linears too, so an all-drop batch keeps the full gate graph live.
- gate_dtype = self.innovation_gate.embed_y.weight.dtype
- innovation_gate_y = partial_zobs_feats.to(dtype=gate_dtype)
- innovation_y = self.innovation_gate.embed_y(innovation_gate_y)
- innovation_time = self.innovation_gate.time_proj(t_base)
-
- if self.pe_mode == "ape":
- pe = self.pos_embedder(h.coords[:, 1:])
- h = h + manual_cast(pe, self.dtype)
-
- # Handle different conditioning modes
- if self.image_attn_mode == 'proj':
- if isinstance(cond, dict):
- global_cond = cond['global']
- proj_cond = cond['proj']
- else:
- global_cond, proj_cond = cond
- if isinstance(global_cond, list):
- global_cond = sp.VarLenTensor.from_tensor_list(global_cond)
- global_cond = manual_cast(global_cond, self.dtype)
- proj_cond = manual_cast(proj_cond, self.dtype)
- cond = (global_cond, proj_cond)
- elif self.image_attn_mode == 'gated_proj':
- global_cond = cond['global']
- if isinstance(global_cond, list):
- global_cond = sp.VarLenTensor.from_tensor_list(global_cond)
- global_cond = manual_cast(global_cond, self.dtype)
- proj_semantic = manual_cast(cond['proj_semantic'], self.dtype)
- proj_color = manual_cast(cond['proj_color'], self.dtype)
- cond = {'global': global_cond, 'proj_semantic': proj_semantic, 'proj_color': proj_color}
- else:
- if isinstance(cond, list):
- cond = sp.VarLenTensor.from_tensor_list(cond)
- cond = manual_cast(cond, self.dtype)
-
- # Inference-only probe hook. When the attribute is absent (the default, and the
- # only state any training path produces) this is None and the loop below is
- # byte-identical to the unhooked path: no constructor arg, no config key, and no
- # entry in the inference ABI contract. A probe sets it to a length-num_blocks
- # sequence to rescale the shared partial message per block.
- _partial_scales = getattr(self, 'partial_zobs_layer_scales', None)
- # Optional evaluator-only callable: ``fn(gate_value, active_batch, block_index)``.
- # It is an instance attribute rather than a constructor/config argument, so no model
- # state or training ABI changes; callers can implement neutral, constant, or shuffled
- # gate-value probes deterministically.
- _gate_value_transform = getattr(self, 'partial_zobs_innovation_gate_value_transform', None)
- _innovation_ratio_stats = [] if self.innovation_gate is not None else None
- _innovation_gate_mean_stats = [] if self.innovation_gate is not None else None
- _innovation_observed_rows = 0 if partial_zobs_rows is None else int(partial_zobs_rows.numel())
- _local_attention = getattr(self, 'local_obs_attention', None)
- _local_topology = None
- _local_full_batch = None
- _local_ratio_base_stats = [] if _local_attention is not None else None
- _local_ratio_hidden_stats = [] if _local_attention is not None else None
- _local_null_stats = [] if _local_attention is not None else None
- _local_entropy_stats = [] if _local_attention is not None else None
- _local_effective_neighbour_stats = [] if _local_attention is not None else None
- _local_observed_residual_stats = [] if _local_attention is not None else None
- _local_unobserved_residual_stats = [] if _local_attention is not None else None
- _local_query_is_observed = None
- _local_neighbourhood_histogram = None
- if _local_attention is not None:
- assert partial_zobs_rows is not None and partial_zobs_feats is not None
- _local_topology = _build_two_hop_observed_topology(x.coords, partial_zobs_rows)
- _local_full_batch = x.coords[:, 0].long()
- _active_observed_mask = torch.zeros(
- x.coords.shape[0], dtype=torch.bool, device=x.coords.device,
- )
- _active_observed_mask[partial_zobs_rows] = True
- _local_query_is_observed = _active_observed_mask[
- _local_topology['query_rows']
- ]
- _local_neighbourhood_histogram = torch.bincount(
- _local_topology['neighbour_counts'], minlength=26,
- )[:26]
- _local_neighbourhood_histogram[0] = (
- x.coords.shape[0] - _local_topology['query_rows'].numel()
- )
- assert int(_local_neighbourhood_histogram.sum()) == x.coords.shape[0]
- assert int(_local_neighbourhood_histogram[1:].sum()) == \
- _local_topology['query_rows'].numel()
- _learned_attention = getattr(self, 'learned_attention', None)
- _learned_cache = None
- _learned_stats = [] if _learned_attention is not None else None
- if _learned_attention is not None:
- assert partial_zobs_inject is not None and partial_zobs_rows is not None
- assert _partial_scales is None, \
- 'partial_zobs_layer_scales is not defined for the five-site V12LA operator'
- _learned_cache = _learned_attention.build_cache(
- x.coords,
- x.layout,
- partial_zobs_inject,
- partial_zobs_rows,
- )
- partial_zobs_inject_position = getattr(
- self, 'partial_zobs_inject_position', 'post_block',
- )
- for _block_index, block in enumerate(self.blocks):
- if partial_zobs_inject is not None and partial_zobs_inject_position == 'pre_mlp':
- _pre_mlp_scale = 1.0 if _partial_scales is None else float(
- _partial_scales[_block_index]
- )
- h = block(
- h,
- t_emb,
- cond,
- partial_zobs_rows=partial_zobs_rows,
- partial_zobs_inject=partial_zobs_inject,
- partial_zobs_inject_scale=_pre_mlp_scale,
- )
- else:
- h = block(h, t_emb, cond)
- if zobs_inject is not None:
- h = h + zobs_inject
- _innovation_ratio = None
- _innovation_gate_mean = None
- _local_delta = None
- _local_stats = None
- if _local_attention is not None and _block_index < _local_attention.alpha.numel():
- assert _local_topology is not None and _local_full_batch is not None
- _local_delta, _local_stats = _local_attention(
- h.feats,
- _local_full_batch,
- t_base,
- partial_zobs_feats,
- partial_zobs_rows,
- _local_topology,
- _block_index,
- )
- _local_delta = manual_cast(_local_delta, self.dtype)
- _local_ratio_base = _local_delta.norm() / partial_zobs_inject.norm().clamp_min(
- torch.finfo(partial_zobs_inject.dtype).eps
- )
- _local_hidden = h.feats[_local_topology['query_rows']]
- _local_ratio_hidden = _local_delta.norm() / _local_hidden.norm().clamp_min(
- torch.finfo(_local_hidden.dtype).eps
- )
- assert _local_query_is_observed is not None
- _local_observed_residual = _local_delta[
- _local_query_is_observed
- ].float().norm()
- _local_unobserved_residual = _local_delta[
- ~_local_query_is_observed
- ].float().norm()
- if _learned_attention is not None and _block_index in _learned_attention.site_to_slot:
- assert _learned_cache is not None
- _learned_inject, _site_stats = _learned_attention(
- h.feats,
- h.coords,
- h.layout,
- _learned_cache,
- _block_index,
- )
- h = h.replace(h.feats + manual_cast(_learned_inject, self.dtype))
- _learned_stats.append({
- name: value.detach() for name, value in _site_stats.items()
- })
- if partial_zobs_inject is not None and _learned_attention is None and \
- partial_zobs_inject_position == 'post_block':
- layer_inject = partial_zobs_inject
- if self.innovation_gate is not None:
- gate = self.innovation_gate
- # Every operation intentionally accepts an empty leading dimension. This
- # keeps all innovation parameters in the all-drop DDP graph.
- assert innovation_gate_y is not None and innovation_y is not None and innovation_time is not None
- t_obs = innovation_time[partial_zobs_batch]
- h_obs = h.feats[partial_zobs_rows].to(dtype=gate.embed_y.weight.dtype)
- x_embed = gate.embed_x(torch.cat([h_obs, t_obs], dim=-1))
- innovation = innovation_y - x_embed
- gate_input = torch.cat([
- innovation,
- innovation_gate_y[:, 32:34],
- t_obs,
- ], dim=-1)
- gate_value = torch.sigmoid(gate.mlp(gate_input))
- if _gate_value_transform is not None:
- gate_value = _gate_value_transform(
- gate_value, partial_zobs_batch, _block_index
- )
- assert gate_value.shape == (innovation.shape[0], 1), \
- f'gate value transform must return [Nobs,1], got {tuple(gate_value.shape)}'
- delta = gate.proj(innovation) * gate_value
- delta = delta * gate.alpha[_block_index].to(dtype=delta.dtype)
- if _block_index == len(self.blocks) - 1:
- # Keep the frozen V11 base carrier at the terminal site,
- # but never add a newly learned observed-only shortcut:
- # no attention block remains to transport/smooth it into U.
- delta = delta * 0
- # Detached telemetry for the training log. This is before the legacy
- # layer-scale hook and therefore measures the learned branch itself.
- _innovation_ratio = delta.norm() / partial_zobs_inject.norm().clamp_min(
- torch.finfo(partial_zobs_inject.dtype).eps
- )
- _innovation_gate_mean = (
- gate_value.mean() if gate_value.numel() != 0 else delta.new_zeros(())
- )
- layer_inject = partial_zobs_inject + manual_cast(delta, self.dtype)
- if partial_zobs_rows.numel() != 0:
- if _partial_scales is None:
- h = h.replace(h.feats.index_add(
- 0, partial_zobs_rows, layer_inject
- ))
- else:
- h = h.replace(h.feats.index_add(
- 0, partial_zobs_rows, layer_inject,
- alpha=float(_partial_scales[_block_index]),
- ))
- else:
- # Preserve every empty carrier/gate GEMM in the graph on all-drop batches.
- h = h.replace(h.feats + layer_inject.sum() * 0)
- if _local_delta is not None:
- _local_scale = 1.0 if _partial_scales is None else float(
- _partial_scales[_block_index]
- )
- if _local_topology['query_rows'].numel() != 0:
- h = h.replace(h.feats.index_add(
- 0, _local_topology['query_rows'], _local_delta,
- alpha=_local_scale,
- ))
- else:
- # The module returns an exact-zero parameter edge for all 14
- # trainable tensors, so DDP observes finite numerical-zero grads.
- h = h.replace(h.feats + _local_delta.sum() * _local_scale)
- if self.innovation_gate is not None:
- if _innovation_ratio is None:
- _innovation_ratio = h.feats.new_zeros(())
- _innovation_gate_mean = h.feats.new_zeros(())
- _innovation_ratio_stats.append(_innovation_ratio.detach())
- _innovation_gate_mean_stats.append(_innovation_gate_mean.detach())
- if _local_attention is not None and _block_index < _local_attention.alpha.numel():
- assert _local_stats is not None
- _local_ratio_base_stats.append(_local_ratio_base.detach())
- _local_ratio_hidden_stats.append(_local_ratio_hidden.detach())
- _local_null_stats.append(_local_stats['null_attention_mean'].detach())
- _local_entropy_stats.append(_local_stats['real_neighbour_entropy_mean'].detach())
- _local_effective_neighbour_stats.append(
- _local_stats['effective_real_neighbour_count_mean'].detach()
- )
- _local_observed_residual_stats.append(
- _local_observed_residual.detach()
- )
- _local_unobserved_residual_stats.append(
- _local_unobserved_residual.detach()
- )
-
- if self.innovation_gate is not None:
- # Instance-only telemetry: deliberately neither buffer nor state-dict entry.
- self.partial_zobs_innovation_gate_last_stats = {
- 'delta_to_base_norm_ratio': torch.stack(_innovation_ratio_stats),
- 'gate_mean': torch.stack(_innovation_gate_mean_stats),
- 'observed_rows': _innovation_observed_rows,
- }
- if _local_attention is not None:
- # Block 29 is structurally absent: every telemetry vector has length 29.
- assert _local_query_is_observed is not None
- assert _local_neighbourhood_histogram is not None
- _full_support_rows = int(x.coords.shape[0])
- _active_queries = int(_local_topology['query_rows'].numel())
- _empty_queries = _full_support_rows - _active_queries
- _singleton_queries = int(
- (_local_topology['neighbour_counts'] == 1).sum()
- )
- self.partial_zobs_local_attention_last_stats = {
- 'delta_to_base_norm_ratio': torch.stack(_local_ratio_base_stats),
- 'delta_to_hidden_norm_ratio': torch.stack(_local_ratio_hidden_stats),
- 'null_attention_mean': torch.stack(_local_null_stats),
- 'real_neighbour_entropy_mean': torch.stack(_local_entropy_stats),
- 'effective_real_neighbour_count_mean': torch.stack(
- _local_effective_neighbour_stats
- ),
- 'observed_residual_l2_norm': torch.stack(
- _local_observed_residual_stats
- ),
- 'unobserved_residual_l2_norm': torch.stack(
- _local_unobserved_residual_stats
- ),
- 'observed_rows': _innovation_observed_rows,
- 'full_support_rows': _full_support_rows,
- 'active_queries': _active_queries,
- 'empty_queries': _empty_queries,
- 'singleton_queries': _singleton_queries,
- 'observed_active_queries': int(_local_query_is_observed.sum()),
- 'unobserved_active_queries': int((~_local_query_is_observed).sum()),
- 'active_query_fraction': _active_queries / max(_full_support_rows, 1),
- 'empty_query_fraction': _empty_queries / max(_full_support_rows, 1),
- 'singleton_query_fraction': _singleton_queries / max(_active_queries, 1),
- 'neighbourhood_size_histogram': _local_neighbourhood_histogram.detach(),
- 'packed_pairs': int(_local_topology['source_indices'].numel()),
- }
- if _learned_attention is not None:
- assert _learned_cache is not None
- assert len(_learned_stats) == len(_learned_attention.sites)
- self.partial_zobs_learned_attention_last_stats = {
- name: torch.stack([site[name] for site in _learned_stats])
- for name in _learned_stats[0]
- }
- self.partial_zobs_learned_attention_last_stats.update({
- 'sites': _learned_attention.sites,
- 'observed_rows': int(partial_zobs_rows.numel()),
- 'full_support_rows': int(x.coords.shape[0]),
- 'observed_counts': _learned_cache['observed_counts'],
- })
-
- h = manual_cast(h, x.dtype)
- h = h.replace(F.layer_norm(h.feats, h.feats.shape[-1:]))
- h = self.out_layer(h)
- return h
-
-
-class ElasticSLatFlowModel(SparseTransformerElasticMixin, SLatFlowModel):
- """
- SLat Flow Model with elastic memory management.
- Used for training with low VRAM.
- """
- pass
+from typing import *
+from functools import partial
+import torch
+import torch.nn as nn
+import torch.nn.functional as F
+import numpy as np
+from ..modules.utils import convert_module_to, manual_cast, str_to_dtype
+from ..modules.transformer import AbsolutePositionEmbedder
+from ..modules import sparse as sp
+from ..modules.sparse.transformer import ModulatedSparseTransformerCrossBlock
+from .sparse_structure_flow import TimestepEmbedder
+
+
+class _PartialZObsInnovationGate(nn.Module):
+ """Residual, per-observed-token correction for the partial z_obs carrier."""
+
+ def __init__(self, model_channels: int, message_channels: int, dim: int, time_dim: int, num_blocks: int):
+ super().__init__()
+ self.alpha = nn.Parameter(torch.zeros(num_blocks))
+ self.embed_y = nn.Linear(message_channels, dim, bias=True)
+ self.time_proj = nn.Linear(model_channels, time_dim, bias=True)
+ self.embed_x = nn.Linear(model_channels + time_dim, dim, bias=True)
+ self.mlp = nn.Sequential(
+ nn.Linear(dim + 2 + time_dim, dim, bias=True),
+ nn.SiLU(),
+ nn.Linear(dim, 1, bias=True),
+ )
+ self.proj = nn.Linear(dim, model_channels, bias=True)
+
+
+_LOCAL_ONE_HOP_OFFSETS = (
+ (-1, 0, 0), (0, -1, 0), (0, 0, -1),
+ (0, 0, 1), (0, 1, 0), (1, 0, 0),
+)
+_LOCAL_TWO_HOP_OFFSETS = tuple(sorted(
+ (dx, dy, dz)
+ for dx in range(-2, 3)
+ for dy in range(-2, 3)
+ for dz in range(-2, 3)
+ if abs(dx) + abs(dy) + abs(dz) <= 2
+))
+assert len(_LOCAL_TWO_HOP_OFFSETS) == 25
+
+
+def _exact_coordinate_rows(
+ full_coords: torch.Tensor,
+ candidate_coords: torch.Tensor,
+) -> Tuple[torch.Tensor, torch.Tensor]:
+ """Return exact full-support rows and a validity mask for candidate coords."""
+ assert full_coords.ndim == candidate_coords.ndim == 2
+ assert full_coords.shape[1] == candidate_coords.shape[1]
+ assert full_coords.shape[0] > 0, 'local attention requires nonempty full sparse support'
+ all_coords = torch.cat([full_coords, candidate_coords], dim=0)
+ _, inverse = torch.unique(all_coords, dim=0, return_inverse=True)
+ full_ids = inverse[:full_coords.shape[0]]
+ candidate_ids = inverse[full_coords.shape[0]:]
+ assert torch.unique(full_ids).numel() == full_ids.numel(), \
+ 'x contains duplicate coordinates'
+ ordered_ids, ordered_rows = torch.sort(full_ids)
+ positions = torch.searchsorted(ordered_ids, candidate_ids)
+ in_range = positions < ordered_ids.numel()
+ safe_positions = positions.clamp(max=ordered_ids.numel() - 1)
+ valid = in_range & (ordered_ids[safe_positions] == candidate_ids)
+ rows = torch.where(valid, ordered_rows[safe_positions], -torch.ones_like(safe_positions))
+ return rows, valid
+
+
+def _build_two_hop_observed_topology(
+ full_coords: torch.Tensor,
+ observed_rows: torch.Tensor,
+) -> Dict[str, torch.Tensor]:
+ """Pack exact batch-local G(S) distance<=2 observed source/query pairs."""
+ assert full_coords.ndim == 2 and full_coords.shape[1] == 4, \
+ f'local attention requires [N,4] integer coords, got {tuple(full_coords.shape)}'
+ assert not torch.is_floating_point(full_coords), 'local attention coordinates must be integer'
+ assert observed_rows.ndim == 1 and observed_rows.dtype == torch.long
+ # Validate full-coordinate uniqueness even for an all-absent batch.
+ _exact_coordinate_rows(full_coords, full_coords.new_empty((0, full_coords.shape[1])))
+ if observed_rows.numel() == 0:
+ empty = torch.empty(0, dtype=torch.long, device=full_coords.device)
+ return {
+ 'query_rows': empty,
+ 'source_indices': empty,
+ 'offset_indices': empty,
+ 'segment_indices': empty,
+ 'neighbour_counts': empty,
+ }
+ assert observed_rows.min() >= 0 and observed_rows.max() < full_coords.shape[0], \
+ 'observed row lies outside full sparse support'
+ assert torch.unique(observed_rows).numel() == observed_rows.numel(), \
+ 'observed rows must be unique'
+
+ device = full_coords.device
+ one_hop = torch.tensor(
+ _LOCAL_ONE_HOP_OFFSETS, dtype=full_coords.dtype, device=device,
+ )
+ source_coords = full_coords[observed_rows]
+ candidate_hop1 = source_coords[:, None, :].expand(-1, len(_LOCAL_ONE_HOP_OFFSETS), -1).clone()
+ candidate_hop1[:, :, 1:] += one_hop[None]
+ hop1_rows, hop1_valid = _exact_coordinate_rows(
+ full_coords, candidate_hop1.reshape(-1, full_coords.shape[1]),
+ )
+ source_ids_hop1 = torch.arange(
+ observed_rows.numel(), device=device, dtype=torch.long,
+ )[:, None].expand(-1, len(_LOCAL_ONE_HOP_OFFSETS)).reshape(-1)
+ hop1_rows = hop1_rows[hop1_valid]
+ source_ids_hop1 = source_ids_hop1[hop1_valid]
+
+ if hop1_rows.numel() != 0:
+ candidate_hop2 = full_coords[hop1_rows][:, None, :].expand(
+ -1, len(_LOCAL_ONE_HOP_OFFSETS), -1,
+ ).clone()
+ candidate_hop2[:, :, 1:] += one_hop[None]
+ hop2_rows, hop2_valid = _exact_coordinate_rows(
+ full_coords, candidate_hop2.reshape(-1, full_coords.shape[1]),
+ )
+ source_ids_hop2 = source_ids_hop1[:, None].expand(
+ -1, len(_LOCAL_ONE_HOP_OFFSETS),
+ ).reshape(-1)
+ hop2_rows = hop2_rows[hop2_valid]
+ source_ids_hop2 = source_ids_hop2[hop2_valid]
+ else:
+ hop2_rows = torch.empty(0, dtype=torch.long, device=device)
+ source_ids_hop2 = torch.empty(0, dtype=torch.long, device=device)
+
+ source_ids = torch.cat([
+ torch.arange(observed_rows.numel(), device=device, dtype=torch.long),
+ source_ids_hop1,
+ source_ids_hop2,
+ ])
+ query_rows = torch.cat([observed_rows, hop1_rows, hop2_rows])
+ # Multiple graph paths to the same query/source pair are one neighbourhood entry.
+ unique_pairs = torch.unique(torch.stack([query_rows, source_ids], dim=1), dim=0)
+ query_rows = unique_pairs[:, 0]
+ source_ids = unique_pairs[:, 1]
+ query_coords = full_coords[query_rows]
+ selected_source_coords = source_coords[source_ids]
+ assert torch.equal(query_coords[:, 0], selected_source_coords[:, 0]), \
+ 'local attention constructed a cross-sample edge'
+ relative = query_coords[:, 1:] - selected_source_coords[:, 1:]
+ offset_table = torch.tensor(
+ _LOCAL_TWO_HOP_OFFSETS, dtype=full_coords.dtype, device=device,
+ )
+ offset_matches = (relative[:, None, :] == offset_table[None, :, :]).all(dim=-1)
+ assert torch.equal(offset_matches.sum(dim=1), torch.ones_like(offset_matches[:, 0], dtype=torch.long)), \
+ 'local attention produced an invalid two-hop offset'
+ offset_indices = offset_matches.to(dtype=torch.long).argmax(dim=1)
+
+ # Canonical per-query real-KV order is lexicographic relative offset, independent
+ # of either full-support or observed-message row ordering.
+ order = torch.argsort(query_rows * len(_LOCAL_TWO_HOP_OFFSETS) + offset_indices)
+ query_rows = query_rows[order]
+ source_ids = source_ids[order]
+ offset_indices = offset_indices[order]
+ active_query_rows, segment_indices, counts = torch.unique_consecutive(
+ query_rows, return_inverse=True, return_counts=True,
+ )
+ assert counts.max() <= len(_LOCAL_TWO_HOP_OFFSETS), \
+ 'local observed neighbourhood exceeds frozen 25-source cap'
+ return {
+ 'query_rows': active_query_rows,
+ 'source_indices': source_ids,
+ 'offset_indices': offset_indices,
+ 'segment_indices': segment_indices,
+ 'neighbour_counts': counts,
+ }
+
+
+class _PartialZObsLocalAttention(nn.Module):
+ """Shared packed two-hop observed-innovation attention for V12-A."""
+
+ def __init__(
+ self,
+ model_channels: int,
+ message_channels: int,
+ dim: int,
+ time_dim: int,
+ num_heads: int,
+ num_active_blocks: int,
+ ):
+ super().__init__()
+ assert dim == 128 and time_dim == 32 and num_heads == 8
+ assert dim % num_heads == 0
+ self.dim = dim
+ self.num_heads = num_heads
+ self.head_dim = dim // num_heads
+ self.alpha = nn.Parameter(torch.zeros(num_active_blocks))
+ self.time_proj = nn.Linear(model_channels, time_dim, bias=True)
+ self.embed_x = nn.Linear(model_channels + time_dim, dim, bias=True)
+ self.embed_y = nn.Linear(message_channels, dim, bias=True)
+ self.key_proj = nn.Linear(dim, dim, bias=True)
+ self.value_proj = nn.Linear(dim, dim, bias=True)
+ self.relative_offset = nn.Parameter(torch.empty(len(_LOCAL_TWO_HOP_OFFSETS), dim))
+ self.null_key = nn.Parameter(torch.empty(num_heads, self.head_dim))
+ self.out_proj = nn.Linear(dim, model_channels, bias=False)
+
+ @staticmethod
+ def _rms_norm(value: torch.Tensor) -> torch.Tensor:
+ return value * torch.rsqrt(value.square().mean(dim=-1, keepdim=True) + 1e-6)
+
+ def forward(
+ self,
+ hidden: torch.Tensor,
+ full_batch: torch.Tensor,
+ t_base: torch.Tensor,
+ message: torch.Tensor,
+ observed_rows: torch.Tensor,
+ topology: Dict[str, torch.Tensor],
+ block_index: int,
+ ) -> Tuple[torch.Tensor, Dict[str, torch.Tensor]]:
+ assert 0 <= block_index < self.alpha.numel(), 'terminal block has no V12-A residual'
+ query_rows = topology['query_rows']
+ source_indices = topology['source_indices']
+ offset_indices = topology['offset_indices']
+ segment_indices = topology['segment_indices']
+ counts = topology['neighbour_counts']
+ assert full_batch.ndim == 1 and full_batch.shape[0] == hidden.shape[0]
+ assert full_batch.numel() == 0 or (
+ full_batch.min() >= 0 and full_batch.max() < t_base.shape[0]
+ ), 'full sparse batch coordinate is outside timestep batch'
+ assert message.shape == (observed_rows.numel(), 34)
+
+ if query_rows.numel() == 0:
+ live_zero = sum(parameter.sum() for parameter in self.parameters()) * 0
+ empty_delta = hidden.new_empty((0, hidden.shape[1])) + live_zero
+ zero = hidden.new_zeros(())
+ return empty_delta, {
+ 'null_attention_mean': zero,
+ 'real_neighbour_entropy_mean': zero,
+ 'effective_real_neighbour_count_mean': zero,
+ 'active_queries': zero,
+ 'singleton_queries': zero,
+ 'packed_pairs': zero,
+ }
+
+ branch_dtype = self.embed_x.weight.dtype
+ hidden_branch = hidden.to(dtype=branch_dtype)
+ time = self.time_proj(t_base.to(dtype=branch_dtype))
+ x_embed = self.embed_x(torch.cat([
+ F.layer_norm(hidden_branch, hidden_branch.shape[-1:], eps=1e-6),
+ time[full_batch],
+ ], dim=-1))
+ innovation = self.embed_y(message.to(dtype=branch_dtype)) - x_embed[observed_rows]
+ query = self._rms_norm(
+ x_embed[query_rows].reshape(-1, self.num_heads, self.head_dim)
+ )
+ real_key = self.key_proj(innovation)[source_indices]
+ real_key = real_key + self.relative_offset[offset_indices]
+ real_key = self._rms_norm(real_key.reshape(-1, self.num_heads, self.head_dim))
+ real_value = self.value_proj(innovation)[source_indices].reshape(
+ -1, self.num_heads, self.head_dim,
+ )
+ real_logits = (query[segment_indices] * real_key).sum(dim=-1) / np.sqrt(self.head_dim)
+ null_key = self._rms_norm(self.null_key)
+ null_logits = (query * null_key[None]).sum(dim=-1) / np.sqrt(self.head_dim)
+
+ segment_heads = segment_indices[:, None].expand(-1, self.num_heads)
+ maxima = null_logits.clone()
+ maxima.scatter_reduce_(0, segment_heads, real_logits, reduce='amax', include_self=True)
+ real_exp = torch.exp(real_logits - maxima[segment_indices])
+ null_exp = torch.exp(null_logits - maxima)
+ real_sum = torch.zeros_like(null_exp).scatter_add(0, segment_heads, real_exp)
+ denominator = null_exp + real_sum
+ real_weight = real_exp / denominator[segment_indices]
+ null_weight = null_exp / denominator
+ attended = hidden_branch.new_zeros(
+ (query_rows.numel(), self.num_heads, self.head_dim),
+ )
+ attended.index_add_(
+ 0, segment_indices, real_weight[..., None] * real_value,
+ )
+ delta = self.out_proj(attended.reshape(query_rows.numel(), self.dim))
+ delta = delta * self.alpha[block_index].to(dtype=delta.dtype)
+
+ # Entropy is conditional on selecting real evidence. Null-vs-real
+ # acceptance is reported independently as null_attention_mean; mixing
+ # that mass into this distribution would make singleton entropy nonzero.
+ real_conditional_weight = real_exp / real_sum[segment_indices].clamp_min(
+ torch.finfo(real_sum.dtype).tiny
+ )
+ entropy_terms = -real_conditional_weight * torch.log(
+ real_conditional_weight.clamp_min(
+ torch.finfo(real_conditional_weight.dtype).tiny
+ )
+ )
+ real_entropy = torch.zeros_like(null_weight).scatter_add(
+ 0, segment_heads, entropy_terms,
+ )
+ return delta, {
+ 'null_attention_mean': null_weight.mean(),
+ 'real_neighbour_entropy_mean': real_entropy.mean(),
+ 'effective_real_neighbour_count_mean': real_entropy.exp().mean(),
+ 'active_queries': delta.new_tensor(float(query_rows.numel())),
+ 'singleton_queries': delta.new_tensor(float((counts == 1).sum())),
+ 'packed_pairs': delta.new_tensor(float(source_indices.numel())),
+ }
+
+
+class _V12LACrossRope(nn.Module):
+ """Apply the same 3D rotary basis to independently packed Q and K rows."""
+
+ def __init__(
+ self,
+ head_dim: int,
+ rope_freq: Tuple[float, float],
+ reserved_dims: int = 2,
+ ):
+ super().__init__()
+ content_dim = head_dim - reserved_dims
+ assert content_dim > 0 and content_dim % 6 == 0
+ self.head_dim = head_dim
+ self.content_dim = content_dim
+ freq_dim = content_dim // 2 // 3
+ frequencies = torch.arange(freq_dim, dtype=torch.float32) / freq_dim
+ frequencies = rope_freq[0] / (rope_freq[1] ** frequencies)
+ self.register_buffer('frequencies', frequencies, persistent=False)
+
+ def forward(self, value: torch.Tensor, coords: torch.Tensor) -> torch.Tensor:
+ assert value.ndim == 3 and value.shape[-1] == self.head_dim
+ assert coords.shape == (value.shape[0], 3)
+ if value.shape[0] == 0:
+ return value
+ phases = torch.outer(
+ coords.reshape(-1).float(), self.frequencies,
+ ).reshape(coords.shape[0], self.content_dim // 2)
+ phases = torch.polar(torch.ones_like(phases), phases).unsqueeze(1)
+ content = torch.view_as_complex(
+ value[..., :self.content_dim].float().reshape(
+ value.shape[0], value.shape[1], -1, 2,
+ )
+ )
+ rotated = torch.view_as_real(content * phases).reshape(
+ value.shape[0], value.shape[1], self.content_dim,
+ ).to(dtype=value.dtype)
+ return torch.cat([rotated, value[..., self.content_dim:]], dim=-1)
+
+
+class _PartialZObsLearnedAttention(nn.Module):
+ """Five-site V12LA observation attention with an exact carrier anchor."""
+
+ def __init__(
+ self,
+ model_channels: int,
+ num_heads: int,
+ sites: Sequence[int],
+ rope_freq: Tuple[float, float],
+ null_prior: float = 0.5,
+ ):
+ super().__init__()
+ self.sites = tuple(int(site) for site in sites)
+ assert self.sites == (5, 10, 15, 20, 25), self.sites
+ assert num_heads == 12
+ assert model_channels % num_heads == 0
+ assert (model_channels // num_heads - 2) > 0
+ assert (model_channels // num_heads - 2) % 6 == 0
+ self.model_channels = model_channels
+ self.num_heads = num_heads
+ self.head_dim = model_channels // num_heads
+ self.content_dim = self.head_dim - 2
+ self.site_to_slot = {site: slot for slot, site in enumerate(self.sites)}
+ self.null_prior = float(null_prior)
+ assert 0.0 < self.null_prior < 1.0, \
+ f'learned-attention null prior must lie in (0,1), got {self.null_prior}'
+ self.null_logit_offset = float(
+ np.log(self.null_prior) - np.log1p(-self.null_prior)
+ )
+
+ # K/V are source-only and shared across all five sites. Q and the
+ # zero-start deviation/output matrix are site-specific.
+ self.key_proj = nn.Linear(model_channels, model_channels, bias=False)
+ self.value_proj = nn.Linear(model_channels, model_channels, bias=False)
+ self.query_proj = nn.ModuleList([
+ nn.Linear(model_channels, model_channels, bias=False)
+ for _ in self.sites
+ ])
+ self.delta_proj = nn.ModuleList([
+ nn.Linear(model_channels, model_channels, bias=False)
+ for _ in self.sites
+ ])
+ self.null_key = nn.Parameter(torch.empty(num_heads, self.content_dim))
+ self.rope = _V12LACrossRope(self.head_dim, rope_freq, reserved_dims=2)
+ self.reset_parameters()
+
+ def reset_parameters(self) -> None:
+ nn.init.xavier_uniform_(self.key_proj.weight)
+ # Attention mixes the already trained V11 carrier basis at warm start;
+ # the value map may then adapt, but does not begin as a random rewrite.
+ nn.init.eye_(self.value_proj.weight)
+ nn.init.xavier_uniform_(self.null_key)
+ for query_proj in self.query_proj:
+ nn.init.xavier_uniform_(query_proj.weight)
+ for delta_proj in self.delta_proj:
+ nn.init.zeros_(delta_proj.weight)
+
+ @staticmethod
+ def _rms_norm(value: torch.Tensor) -> torch.Tensor:
+ normalized = value.float() * torch.rsqrt(
+ value.float().square().mean(dim=-1, keepdim=True) + 1e-6
+ )
+ return normalized.to(dtype=value.dtype)
+
+ @staticmethod
+ def _anchor(
+ carrier: torch.Tensor,
+ observed_rows: torch.Tensor,
+ full_rows: int,
+ ) -> torch.Tensor:
+ """Evaluate fixed coordinate/null attention without an additive scatter."""
+ assert carrier.ndim == 2
+ assert observed_rows.ndim == 1 and observed_rows.dtype == torch.long
+ assert carrier.shape[0] == observed_rows.numel()
+ assert full_rows >= observed_rows.numel()
+ if observed_rows.numel() != 0:
+ assert observed_rows.min() >= 0 and observed_rows.max() < full_rows
+ assert torch.unique(observed_rows).numel() == observed_rows.numel()
+ # Index zero is the exact-zero null value. Every observed row is then
+ # redirected to its own carrier row; unmatched rows keep selecting null.
+ values = torch.cat([carrier.new_zeros((1, carrier.shape[1])), carrier], dim=0)
+ anchor_index = torch.zeros(full_rows, dtype=torch.long, device=carrier.device)
+ anchor_index[observed_rows] = torch.arange(
+ 1, observed_rows.numel() + 1, dtype=torch.long, device=carrier.device,
+ )
+ return values[anchor_index]
+
+ def build_cache(
+ self,
+ full_coords: torch.Tensor,
+ full_layout: List[slice],
+ carrier: torch.Tensor,
+ observed_rows: torch.Tensor,
+ ) -> Dict[str, Any]:
+ """Build the exact anchor and the shared per-sample O+null K/V cache."""
+ assert full_coords.ndim == 2 and full_coords.shape[1] == 4
+ assert carrier.shape == (observed_rows.numel(), self.model_channels)
+ assert len(full_layout) > 0
+ anchor = self._anchor(carrier, observed_rows, full_coords.shape[0])
+ observed_coords = full_coords[observed_rows]
+ observed_batch = observed_coords[:, 0].long()
+ assert observed_batch.numel() == 0 or (
+ observed_batch.min() >= 0 and observed_batch.max() < len(full_layout)
+ )
+
+ branch_dtype = self.key_proj.weight.dtype
+ carrier_branch = carrier.to(dtype=branch_dtype)
+ carrier_norm = F.layer_norm(
+ carrier_branch, (self.model_channels,), eps=1e-6,
+ )
+ real_key = self.key_proj(carrier_norm).reshape(
+ -1, self.num_heads, self.head_dim,
+ )
+ real_key = self.rope(real_key, observed_coords[:, 1:])
+ real_key = self._rms_norm(real_key[..., :self.content_dim])
+ real_key = torch.cat([
+ real_key,
+ real_key.new_zeros((real_key.shape[0], self.num_heads, 2)),
+ ], dim=-1)
+ real_value = self.value_proj(carrier_branch).reshape(
+ -1, self.num_heads, self.head_dim,
+ )
+ null_content = self._rms_norm(
+ self.null_key.to(dtype=real_key.dtype)
+ )
+
+ packed_kv = []
+ observed_counts = []
+ for batch_index in range(len(full_layout)):
+ source_indices = torch.nonzero(
+ observed_batch == batch_index, as_tuple=False,
+ ).flatten()
+ observed_count = int(source_indices.numel())
+ observed_counts.append(observed_count)
+ # One null competes with O real keys. log(O) cancels its cardinality
+ # disadvantage; the constant logit offset sets the null mass only in
+ # the equal-content-logit reference case, not after learned Q/K drift.
+ null_reserved = real_key.new_zeros((self.num_heads, 2))
+ null_reserved[:, 0] = np.sqrt(self.head_dim) * (
+ np.log(max(observed_count, 1)) + self.null_logit_offset
+ )
+ null_key = torch.cat([null_content, null_reserved], dim=-1).unsqueeze(0)
+ null_value = real_value.new_zeros((1, self.num_heads, self.head_dim))
+ keys = torch.cat([real_key[source_indices], null_key], dim=0)
+ values = torch.cat([real_value[source_indices], null_value], dim=0)
+ packed_kv.append(torch.stack([keys, values], dim=1))
+
+ kv = sp.VarLenTensor.from_tensor_list(packed_kv)
+ return {
+ 'anchor': anchor,
+ 'kv': kv,
+ 'observed_rows': observed_rows,
+ 'observed_counts': tuple(observed_counts),
+ }
+
+ def forward(
+ self,
+ hidden: torch.Tensor,
+ full_coords: torch.Tensor,
+ full_layout: List[slice],
+ cache: Dict[str, Any],
+ block_index: int,
+ ) -> Tuple[torch.Tensor, Dict[str, torch.Tensor]]:
+ assert block_index in self.site_to_slot, block_index
+ assert hidden.shape == (full_coords.shape[0], self.model_channels)
+ slot = self.site_to_slot[block_index]
+ branch_dtype = self.query_proj[slot].weight.dtype
+ hidden_branch = hidden.to(dtype=branch_dtype)
+ query = self.query_proj[slot](F.layer_norm(
+ hidden_branch, (self.model_channels,), eps=1e-6,
+ )).reshape(-1, self.num_heads, self.head_dim)
+ query = self.rope(query, full_coords[:, 1:])
+ query_content = self._rms_norm(query[..., :self.content_dim])
+ query_reserved = query.new_zeros((query.shape[0], self.num_heads, 2))
+ query_reserved[..., 0] = 1
+ query = torch.cat([query_content, query_reserved], dim=-1)
+ query_varlen = sp.VarLenTensor(query, full_layout)
+ attended = sp.sparse_scaled_dot_product_attention(
+ query_varlen, cache['kv'],
+ ).feats.reshape(-1, self.model_channels)
+
+ anchor = cache['anchor'].to(dtype=attended.dtype)
+ deviation_input = attended - anchor
+ correction = self.delta_proj[slot](deviation_input)
+ inject = anchor + correction
+ eps = torch.finfo(inject.dtype).eps
+ observed_rows = cache['observed_rows']
+ observed_mask = torch.zeros(
+ inject.shape[0], dtype=torch.bool, device=inject.device,
+ )
+ observed_mask[observed_rows] = True
+ return inject, {
+ 'correction_to_anchor_norm_ratio': correction.norm() / anchor.norm().clamp_min(eps),
+ 'correction_to_hidden_norm_ratio': correction.norm() / hidden_branch.norm().clamp_min(eps),
+ 'observed_correction_l2_norm': correction[observed_mask].float().norm(),
+ 'unobserved_correction_l2_norm': correction[~observed_mask].float().norm(),
+ 'attention_output_l2_norm': attended.float().norm(),
+ 'anchor_l2_norm': anchor.float().norm(),
+ 'delta_weight_l2_norm': self.delta_proj[slot].weight.float().norm(),
+ }
+
+class SLatFlowModel(nn.Module):
+ """
+ Structured Latent Flow Model for 3D generation.
+
+ Supports two conditioning modes:
+ - "cross": Standard cross-attention with image features
+ - "proj": View-aligned projection attention with camera-aware features
+ """
+ def __init__(
+ self,
+ resolution: int,
+ in_channels: int,
+ model_channels: int,
+ cond_channels: int,
+ out_channels: int,
+ num_blocks: int,
+ num_heads: Optional[int] = None,
+ num_head_channels: Optional[int] = 64,
+ mlp_ratio: float = 4,
+ pe_mode: Literal["ape", "rope"] = "ape",
+ rope_freq: Tuple[float, float] = (1.0, 10000.0),
+ dtype: str = 'float32',
+ use_checkpoint: bool = False,
+ share_mod: bool = False,
+ initialization: str = 'vanilla',
+ qk_rms_norm: bool = False,
+ qk_rms_norm_cross: bool = False,
+ image_attn_mode: Literal["cross", "proj", "gated_proj"] = "cross",
+ proj_in_channels: Optional[int] = None,
+ vae_in_channels: Optional[int] = None,
+ zobs_inject: bool = False,
+ zobs_inject_in_channels: int = 33,
+ zobs_inject_scale: float = 1.0,
+ partial_zobs_inject: bool = False,
+ partial_zobs_in_channels: int = 34,
+ partial_zobs_inject_position: Literal['post_block', 'pre_mlp'] = 'post_block',
+ legacy_zobs_zero_input: bool = False,
+ partial_zobs_innovation_gate: bool = False,
+ innovation_gate_dim: int = 128,
+ innovation_gate_time_dim: int = 32,
+ partial_zobs_innovation_gate_train_only: bool = False,
+ partial_zobs_local_attention: bool = False,
+ local_attention_dim: int = 128,
+ local_attention_time_dim: int = 32,
+ local_attention_heads: int = 8,
+ partial_zobs_local_attention_train_only: bool = False,
+ partial_zobs_learned_attention: bool = False,
+ learned_attention_sites: Sequence[int] = (5, 10, 15, 20, 25),
+ learned_attention_heads: int = 12,
+ learned_attention_null_prior: float = 0.5,
+ partial_zobs_learned_attention_train_only: bool = False,
+ **kwargs
+ ):
+ super().__init__()
+ self.resolution = resolution
+ self.in_channels = in_channels
+ self.model_channels = model_channels
+ self.cond_channels = cond_channels
+ self.out_channels = out_channels
+ self.num_blocks = num_blocks
+ self.num_heads = num_heads or model_channels // num_head_channels
+ self.mlp_ratio = mlp_ratio
+ self.pe_mode = pe_mode
+ self.use_checkpoint = use_checkpoint
+ self.share_mod = share_mod
+ self.initialization = initialization
+ self.qk_rms_norm = qk_rms_norm
+ self.qk_rms_norm_cross = qk_rms_norm_cross
+ self.image_attn_mode = image_attn_mode
+ self.proj_in_channels = proj_in_channels
+ self.vae_in_channels = vae_in_channels
+ self.dtype = str_to_dtype(dtype)
+
+ self.t_embedder = TimestepEmbedder(model_channels)
+ if share_mod:
+ self.adaLN_modulation = nn.Sequential(
+ nn.SiLU(),
+ nn.Linear(model_channels, 6 * model_channels, bias=True)
+ )
+
+ if pe_mode == "ape":
+ self.pos_embedder = AbsolutePositionEmbedder(model_channels)
+
+ self.input_layer = sp.SparseLinear(in_channels, model_channels)
+ # Trained-bridge: single shared per-block injector of [z_obs (x) vis (32) | vis (1)] -> model_channels.
+ self.zobs_inject = zobs_inject
+ self.zobs_inject_scale = float(zobs_inject_scale)
+ self.zobs_injector = sp.SparseLinear(zobs_inject_in_channels, model_channels) if zobs_inject else None
+ # v8-partial: the observation lives only on its observed support. This adapter is
+ # deliberately bias-free: absent rows never acquire a learned content/fill value.
+ self.partial_zobs_inject = partial_zobs_inject
+ self.partial_zobs_in_channels = partial_zobs_in_channels
+ assert partial_zobs_inject_position in ('post_block', 'pre_mlp'), \
+ f'partial_zobs_inject_position must be post_block or pre_mlp, got {partial_zobs_inject_position!r}'
+ self.partial_zobs_inject_position = partial_zobs_inject_position
+ if partial_zobs_inject_position == 'pre_mlp':
+ assert not any((
+ partial_zobs_innovation_gate,
+ partial_zobs_local_attention,
+ partial_zobs_learned_attention,
+ )), 'partial_zobs_inject_position=pre_mlp is incompatible with post-block experimental branches'
+ self.legacy_zobs_zero_input = legacy_zobs_zero_input
+ self.partial_zobs_injector = (
+ sp.SparseLinear(partial_zobs_in_channels, model_channels, bias=False)
+ if partial_zobs_inject else None
+ )
+ self.partial_zobs_innovation_gate = partial_zobs_innovation_gate
+ self.partial_zobs_innovation_gate_train_only = partial_zobs_innovation_gate_train_only
+ if partial_zobs_innovation_gate_train_only:
+ assert partial_zobs_innovation_gate, \
+ 'partial_zobs_innovation_gate_train_only requires partial_zobs_innovation_gate=true'
+ if partial_zobs_innovation_gate:
+ assert partial_zobs_inject, 'partial_zobs_innovation_gate requires partial_zobs_inject=true'
+ assert partial_zobs_in_channels == 34, \
+ f'partial_zobs_innovation_gate requires 34 input channels, got {partial_zobs_in_channels}'
+ assert innovation_gate_dim == 128, f'innovation_gate_dim is fixed at 128, got {innovation_gate_dim}'
+ assert innovation_gate_time_dim == 32, \
+ f'innovation_gate_time_dim is fixed at 32, got {innovation_gate_time_dim}'
+ assert model_channels == 1536, f'partial_zobs_innovation_gate requires 1536 model channels, got {model_channels}'
+ assert num_blocks == 30, f'partial_zobs_innovation_gate requires 30 blocks, got {num_blocks}'
+ self.innovation_gate = _PartialZObsInnovationGate(
+ model_channels, partial_zobs_in_channels, innovation_gate_dim,
+ innovation_gate_time_dim, num_blocks,
+ )
+ else:
+ self.innovation_gate = None
+ self.partial_zobs_local_attention = partial_zobs_local_attention
+ self.partial_zobs_local_attention_train_only = partial_zobs_local_attention_train_only
+ if partial_zobs_local_attention_train_only:
+ assert partial_zobs_local_attention, \
+ 'partial_zobs_local_attention_train_only requires partial_zobs_local_attention=true'
+ assert sum(bool(value) for value in (
+ partial_zobs_innovation_gate,
+ partial_zobs_local_attention,
+ partial_zobs_learned_attention,
+ )) <= 1, 'partial-zobs experimental attention/gate branches are mutually exclusive'
+ if partial_zobs_local_attention:
+ assert partial_zobs_inject, 'partial_zobs_local_attention requires partial_zobs_inject=true'
+ assert partial_zobs_in_channels == 34, \
+ f'partial_zobs_local_attention requires 34 input channels, got {partial_zobs_in_channels}'
+ assert local_attention_dim == 128, \
+ f'local_attention_dim is fixed at 128, got {local_attention_dim}'
+ assert local_attention_time_dim == 32, \
+ f'local_attention_time_dim is fixed at 32, got {local_attention_time_dim}'
+ assert local_attention_heads == 8, \
+ f'local_attention_heads is fixed at 8, got {local_attention_heads}'
+ assert model_channels == 1536, \
+ f'partial_zobs_local_attention requires 1536 model channels, got {model_channels}'
+ assert num_blocks == 30, \
+ f'partial_zobs_local_attention requires 30 blocks, got {num_blocks}'
+ self.local_obs_attention = _PartialZObsLocalAttention(
+ model_channels, partial_zobs_in_channels, local_attention_dim,
+ local_attention_time_dim, local_attention_heads, num_blocks - 1,
+ )
+ else:
+ self.local_obs_attention = None
+
+ self.partial_zobs_learned_attention = partial_zobs_learned_attention
+ self.partial_zobs_learned_attention_train_only = \
+ partial_zobs_learned_attention_train_only
+ if partial_zobs_learned_attention_train_only:
+ assert partial_zobs_learned_attention, \
+ 'partial_zobs_learned_attention_train_only requires partial_zobs_learned_attention=true'
+ if partial_zobs_learned_attention:
+ assert partial_zobs_inject, \
+ 'partial_zobs_learned_attention requires partial_zobs_inject=true'
+ assert partial_zobs_in_channels == 34, \
+ f'partial_zobs_learned_attention requires 34 input channels, got {partial_zobs_in_channels}'
+ assert model_channels == 1536, \
+ f'partial_zobs_learned_attention requires 1536 model channels, got {model_channels}'
+ assert num_blocks == 30, \
+ f'partial_zobs_learned_attention requires 30 blocks, got {num_blocks}'
+ assert learned_attention_heads == 12, \
+ f'partial_zobs_learned_attention requires 12 heads, got {learned_attention_heads}'
+ assert tuple(learned_attention_sites) == (5, 10, 15, 20, 25), \
+ f'V12LA sites are fixed at [5,10,15,20,25], got {learned_attention_sites}'
+ self.learned_attention = _PartialZObsLearnedAttention(
+ model_channels,
+ learned_attention_heads,
+ learned_attention_sites,
+ rope_freq,
+ learned_attention_null_prior,
+ )
+ else:
+ self.learned_attention = None
+
+ self.blocks = nn.ModuleList([
+ ModulatedSparseTransformerCrossBlock(
+ model_channels,
+ cond_channels,
+ num_heads=self.num_heads,
+ mlp_ratio=self.mlp_ratio,
+ attn_mode='full',
+ use_checkpoint=self.use_checkpoint,
+ use_rope=(pe_mode == "rope"),
+ rope_freq=rope_freq,
+ share_mod=self.share_mod,
+ qk_rms_norm=self.qk_rms_norm,
+ qk_rms_norm_cross=self.qk_rms_norm_cross,
+ image_attn_mode=image_attn_mode,
+ proj_in_channels=proj_in_channels,
+ vae_in_channels=vae_in_channels,
+ )
+ for _ in range(num_blocks)
+ ])
+
+ self.out_layer = sp.SparseLinear(model_channels, out_channels)
+
+ self.initialize_weights()
+ self.convert_to(self.dtype)
+ if self.partial_zobs_innovation_gate_train_only:
+ self._freeze_for_partial_zobs_innovation_gate()
+ if self.partial_zobs_local_attention_train_only:
+ self._freeze_for_partial_zobs_local_attention()
+ if self.partial_zobs_learned_attention_train_only:
+ self._freeze_for_partial_zobs_learned_attention()
+
+ @property
+ def device(self) -> torch.device:
+ """
+ Return the device of the model.
+ """
+ return next(self.parameters()).device
+
+ def convert_to(self, dtype: torch.dtype) -> None:
+ """
+ Convert the torso of the model to the specified dtype.
+ """
+ self.dtype = dtype
+ self.blocks.apply(partial(convert_module_to, dtype=dtype))
+ if self.learned_attention is not None:
+ self.learned_attention.apply(partial(convert_module_to, dtype=dtype))
+
+ def initialize_weights(self) -> None:
+ if self.initialization == 'vanilla':
+ # Initialize transformer layers:
+ def _basic_init(module):
+ if isinstance(module, nn.Linear):
+ torch.nn.init.xavier_uniform_(module.weight)
+ if module.bias is not None:
+ nn.init.constant_(module.bias, 0)
+ self.apply(_basic_init)
+
+ # Initialize timestep embedding MLP:
+ nn.init.normal_(self.t_embedder.mlp[0].weight, std=0.02)
+ nn.init.normal_(self.t_embedder.mlp[2].weight, std=0.02)
+
+ # Zero-out adaLN modulation layers in DiT blocks:
+ if self.share_mod:
+ nn.init.constant_(self.adaLN_modulation[-1].weight, 0)
+ nn.init.constant_(self.adaLN_modulation[-1].bias, 0)
+ else:
+ for block in self.blocks:
+ nn.init.constant_(block.adaLN_modulation[-1].weight, 0)
+ nn.init.constant_(block.adaLN_modulation[-1].bias, 0)
+
+ # Zero-out output layers:
+ nn.init.constant_(self.out_layer.weight, 0)
+ nn.init.constant_(self.out_layer.bias, 0)
+
+ elif self.initialization == 'scaled':
+ # Initialize transformer layers:
+ def _basic_init(module):
+ if isinstance(module, nn.Linear):
+ torch.nn.init.normal_(module.weight, std=np.sqrt(2.0 / (5.0 * self.model_channels)))
+ if module.bias is not None:
+ nn.init.constant_(module.bias, 0)
+ self.apply(_basic_init)
+
+ # Scaled init for to_out and ffn2
+ def _scaled_init(module):
+ if isinstance(module, nn.Linear):
+ torch.nn.init.normal_(module.weight, std=1.0 / np.sqrt(5 * self.num_blocks * self.model_channels))
+ if module.bias is not None:
+ nn.init.constant_(module.bias, 0)
+ for block in self.blocks:
+ block.self_attn.to_out.apply(_scaled_init)
+ # Handle cross, proj, and gated_proj modes
+ if self.image_attn_mode in ("proj", "gated_proj"):
+ block.cross_attn.cross_attn_block.to_out.apply(_scaled_init)
+ else:
+ block.cross_attn.to_out.apply(_scaled_init)
+ block.mlp.mlp[2].apply(_scaled_init)
+
+ # Initialize input layer to make the initial representation have variance 1
+ nn.init.normal_(self.input_layer.weight, std=1.0 / np.sqrt(self.in_channels))
+ nn.init.zeros_(self.input_layer.bias)
+
+ # Initialize timestep embedding MLP:
+ nn.init.normal_(self.t_embedder.mlp[0].weight, std=0.02)
+ nn.init.normal_(self.t_embedder.mlp[2].weight, std=0.02)
+
+ # Zero-out adaLN modulation layers in DiT blocks:
+ if self.share_mod:
+ nn.init.constant_(self.adaLN_modulation[-1].weight, 0)
+ nn.init.constant_(self.adaLN_modulation[-1].bias, 0)
+ else:
+ for block in self.blocks:
+ nn.init.constant_(block.adaLN_modulation[-1].weight, 0)
+ nn.init.constant_(block.adaLN_modulation[-1].bias, 0)
+
+ # Zero-out output layers:
+ nn.init.constant_(self.out_layer.weight, 0)
+ nn.init.constant_(self.out_layer.bias, 0)
+
+ # Trained-bridge: zero-init the z_obs injector LAST (after _basic_init), so step-0 injection == 0.
+ if getattr(self, 'zobs_injector', None) is not None:
+ nn.init.zeros_(self.zobs_injector.weight)
+ nn.init.zeros_(self.zobs_injector.bias)
+ if getattr(self, 'partial_zobs_injector', None) is not None:
+ nn.init.zeros_(self.partial_zobs_injector.weight)
+ if getattr(self, 'innovation_gate', None) is not None:
+ nn.init.zeros_(self.innovation_gate.alpha)
+ if getattr(self, 'local_obs_attention', None) is not None:
+ # V12-A freezes vanilla Xavier initialization independently of the
+ # scaled V11 backbone initialization mode. Only alpha starts at zero.
+ for module in self.local_obs_attention.modules():
+ if isinstance(module, nn.Linear):
+ nn.init.xavier_uniform_(module.weight)
+ if module.bias is not None:
+ nn.init.zeros_(module.bias)
+ nn.init.xavier_uniform_(self.local_obs_attention.relative_offset)
+ nn.init.xavier_uniform_(self.local_obs_attention.null_key)
+ nn.init.zeros_(self.local_obs_attention.alpha)
+ if getattr(self, 'learned_attention', None) is not None:
+ self.learned_attention.reset_parameters()
+
+ def _freeze_for_partial_zobs_innovation_gate(self) -> None:
+ """Make the V12 gate the sole trainable branch before an optimizer is built."""
+ assert self.innovation_gate is not None
+ for parameter in self.parameters():
+ parameter.requires_grad_(False)
+ for parameter in self.innovation_gate.parameters():
+ parameter.requires_grad_(True)
+ trainable = [name for name, parameter in self.named_parameters() if parameter.requires_grad]
+ expected = [f'innovation_gate.{name}' for name in self.innovation_gate.state_dict()]
+ assert trainable == expected, (trainable, expected)
+ assert sum(parameter.numel() for parameter in self.parameters() if parameter.requires_grad) == 473663
+
+ def _freeze_for_partial_zobs_local_attention(self) -> None:
+ """Make the V12-A local attention the sole trainable warm-start branch."""
+ assert self.local_obs_attention is not None
+ for parameter in self.parameters():
+ parameter.requires_grad_(False)
+ for parameter in self.local_obs_attention.parameters():
+ parameter.requires_grad_(True)
+ trainable = [name for name, parameter in self.named_parameters() if parameter.requires_grad]
+ expected = [f'local_obs_attention.{name}' for name in self.local_obs_attention.state_dict()]
+ assert trainable == expected, (trainable, expected)
+ assert len(trainable) == 14
+ assert sum(parameter.numel() for parameter in self.parameters() if parameter.requires_grad) == 487485
+
+ def _freeze_for_partial_zobs_learned_attention(self) -> None:
+ """Make the V12LA module the sole trainable warm-start branch."""
+ assert self.learned_attention is not None
+ for parameter in self.parameters():
+ parameter.requires_grad_(False)
+ for parameter in self.learned_attention.parameters():
+ parameter.requires_grad_(True)
+ trainable = [name for name, parameter in self.named_parameters() if parameter.requires_grad]
+ expected = [
+ f'learned_attention.{name}'
+ for name, _ in self.learned_attention.named_parameters()
+ ]
+ assert trainable == expected, (trainable, expected)
+ assert len(trainable) == 13, trainable
+
+ def _partial_zobs_message(
+ self,
+ x: sp.SparseTensor,
+ partial_z_obs: sp.SparseTensor,
+ partial_z_obs_keep_mask: Optional[torch.Tensor],
+ ) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
+ """Project a variable-support observation and join its messages onto ``x`` rows.
+
+ The return value stays variable-length: projected messages plus their exact full-row
+ indices. Neither observation content nor a zero-filled model-channel tensor is
+ materialized on full support. ``torch.unique(..., dim=0)`` makes the coordinate join
+ exact and rejects duplicate or non-subset partial coordinates instead of assuming a
+ row order.
+ """
+ adapter = self.partial_zobs_injector
+ assert adapter is not None
+ assert partial_z_obs.feats.ndim == 2 and \
+ partial_z_obs.feats.shape[-1] == self.partial_zobs_in_channels, \
+ f'partial_z_obs must be [N,{self.partial_zobs_in_channels}], got {tuple(partial_z_obs.feats.shape)}'
+ assert partial_z_obs.coords.ndim == 2 and partial_z_obs.coords.shape[1] == x.coords.shape[1], \
+ f'partial_z_obs coordinate rank mismatch: {tuple(partial_z_obs.coords.shape)} vs {tuple(x.coords.shape)}'
+
+ active = torch.ones(partial_z_obs.feats.shape[0], dtype=torch.bool, device=partial_z_obs.feats.device)
+ if partial_z_obs_keep_mask is not None:
+ assert partial_z_obs_keep_mask.ndim == 1 and partial_z_obs_keep_mask.shape[0] == x.shape[0], \
+ f'partial_z_obs_keep_mask must be [B={x.shape[0]}], got {tuple(partial_z_obs_keep_mask.shape)}'
+ keep = partial_z_obs_keep_mask.to(device=partial_z_obs.feats.device, dtype=torch.bool)
+ active = keep[partial_z_obs.coords[:, 0].long()]
+
+ active_coords = partial_z_obs.coords[active]
+ active_feats = partial_z_obs.feats[active]
+ # F.linear on a zero-row tensor is intentional: it keeps the adapter parameters in the
+ # graph on all-drop batches, so DDP sees a zero (rather than None) gradient.
+ active_messages = F.linear(active_feats, adapter.weight, None)
+
+ full_rows = torch.empty(0, dtype=torch.long, device=x.feats.device)
+ if active_coords.shape[0] != 0:
+ all_coords = torch.cat([x.coords, active_coords], dim=0)
+ _, inverse = torch.unique(all_coords, dim=0, return_inverse=True)
+ x_ids = inverse[:x.coords.shape[0]]
+ partial_ids = inverse[x.coords.shape[0]:]
+ assert torch.unique(x_ids).numel() == x_ids.numel(), 'x contains duplicate coordinates'
+ assert torch.unique(partial_ids).numel() == partial_ids.numel(), 'partial_z_obs contains duplicate coordinates'
+ ordered_x_ids, ordered_x_rows = torch.sort(x_ids)
+ positions = torch.searchsorted(ordered_x_ids, partial_ids)
+ assert (positions < ordered_x_ids.numel()).all() and \
+ torch.equal(ordered_x_ids[positions], partial_ids), \
+ 'partial_z_obs coordinates must be an exact subset of current x coordinates'
+ full_rows = ordered_x_rows[positions]
+
+ return active_messages, full_rows, active_feats, active_coords[:, 0].long()
+
+ def forward(
+ self,
+ x: sp.SparseTensor,
+ t: torch.Tensor,
+ cond: Union[torch.Tensor, List[torch.Tensor], Dict[str, Union[torch.Tensor, sp.SparseTensor]], Tuple],
+ concat_cond: Optional[sp.SparseTensor] = None,
+ z_obs: Optional[sp.SparseTensor] = None,
+ partial_z_obs: Optional[sp.SparseTensor] = None,
+ partial_z_obs_keep_mask: Optional[torch.Tensor] = None,
+ **kwargs
+ ) -> sp.SparseTensor:
+ """
+ Forward pass.
+
+ Args:
+ x: SparseTensor input
+ t: Timestep tensor [B]
+ cond: Conditioning tensor. For "cross" mode: list of tensors or tensor.
+ For "proj" mode: dict {'global': global_cond, 'proj': proj_cond}
+ or tuple of (global_cond, proj_cond)
+ concat_cond: Optional concatenation condition
+ z_obs: Optional 33ch [z_obs(32)|vis(1)] sparse tensor, same coords as x. Feeds ONLY
+ the zobs_injector (v6 patchcross path: concat_cond is the plain 32ch shape
+ latent and z_obs never enters the input_layer). When None, the legacy 97ch
+ concat slice below applies — shipped/v4 behavior is bit-identical.
+ partial_z_obs: Optional variable-length v8 observation
+ [normalized_z_obs(32)|coverage(1)|boundary_distance(1)].
+ Its coordinates must be a subset of x; it is consumed only by the dedicated,
+ bias-free partial adapter and is never converted to full-support content.
+
+ Returns:
+ SparseTensor output
+ """
+ if concat_cond is not None:
+ x = sp.sparse_cat([x, concat_cond], dim=-1)
+
+ h = self.input_layer(x)
+ h = manual_cast(h, self.dtype)
+
+ # Trained-bridge: compute the per-block injection vector ONCE (shared zero-init linear, added each block).
+ # Sources, in priority order: separate z_obs kwarg (v6), else the trailing channels of a
+ # 97-ch [x_t|shape|z_obs|vis] input (v4). z_obs*vis==0 at vis=0 in both.
+ zobs_inject = None
+ _injector = getattr(self, 'zobs_injector', None)
+ if z_obs is not None:
+ # Fail fast: without an injector the kwarg would be a silent no-op, and in the 64ch
+ # patchcross architecture the injector is the ONLY z_obs consumption path.
+ assert _injector is not None, \
+ 'z_obs kwarg passed but the denoiser was built without zobs_inject=true'
+ # Full coords equality is asserted at data-load; a row-count check catches gross mismatch.
+ assert z_obs.feats.shape[-1] == 33 and z_obs.feats.shape[0] == x.feats.shape[0], \
+ f'z_obs kwarg mismatch: {tuple(z_obs.feats.shape)} vs x rows {x.feats.shape[0]}'
+ _zo = z_obs.feats[:, :32]
+ _vis = z_obs.feats[:, 32:33]
+ zobs_inject = manual_cast(self.zobs_injector(z_obs.replace(torch.cat([_zo * _vis, _vis], dim=-1))), self.dtype)
+ if not getattr(self, '_zobs_kwarg_logged', False): # in-log proof hook (once per process)
+ print('[zobs_injector] consuming separate z_obs kwarg (patchcross path)', flush=True)
+ self._zobs_kwarg_logged = True
+ elif _injector is not None and x.feats.shape[-1] >= 97:
+ _zo = x.feats[:, 64:96]
+ _vis = x.feats[:, 96:97]
+ zobs_inject = manual_cast(self.zobs_injector(x.replace(torch.cat([_zo * _vis, _vis], dim=-1))), self.dtype)
+ elif _injector is not None and self.legacy_zobs_zero_input:
+ # 64ch v8 shell with a legacy injector: preserve the legacy all-zero z_obs mode,
+ # including its learned per-block bias, without materialising observation content.
+ # Evaluate one zero row and broadcast the result: this is exactly W*0+b, keeps a
+ # zero (non-None) weight gradient under DDP, and avoids an N-row 33ch GEMM.
+ zero_output = F.linear(
+ x.feats.new_zeros((1, _injector.in_features)),
+ _injector.weight,
+ _injector.bias,
+ )
+ zobs_inject = manual_cast(
+ x.replace(zero_output.expand(x.feats.shape[0], -1)), self.dtype
+ )
+ elif _injector is not None and not getattr(self, '_zobs_none_warned', False):
+ # Injector present but NO z_obs source (64ch input, kwarg omitted): the trained
+ # per-block bias is NOT applied — a third mode distinct from "z_obs dropped" (an
+ # all-zero 33ch tensor still adds the bias each block). A nozobs arm on a patchcross
+ # model must pass the zero tensor, never omit the kwarg.
+ print('[zobs_injector] WARNING: injector present but no z_obs source (kwarg omitted, '
+ 'input < 97ch) — per-block bias NOT applied; pass an all-zero 33ch z_obs for a '
+ 'true nozobs arm', flush=True)
+ self._zobs_none_warned = True
+
+ partial_zobs_inject = None
+ partial_zobs_rows = None
+ partial_zobs_feats = None
+ partial_zobs_batch = None
+ if partial_z_obs is not None:
+ assert self.partial_zobs_injector is not None, \
+ 'partial_z_obs passed but denoiser was built without partial_zobs_inject=true'
+ partial_zobs_inject, partial_zobs_rows, partial_zobs_feats, partial_zobs_batch = self._partial_zobs_message(
+ x, partial_z_obs, partial_z_obs_keep_mask
+ )
+ partial_zobs_inject = manual_cast(partial_zobs_inject, self.dtype)
+ elif self.partial_zobs_injector is not None:
+ raise AssertionError('partial_zobs_inject=true requires a separate partial_z_obs tensor')
+
+ if zobs_inject is not None and self.zobs_inject_scale != 1.0:
+ zobs_inject = zobs_inject.replace(zobs_inject.feats * self.zobs_inject_scale)
+ if not getattr(self, '_zobs_inject_scale_logged', False):
+ print(f'[zobs_injector] output scale={self.zobs_inject_scale:g}', flush=True)
+ self._zobs_inject_scale_logged = True
+
+ # Keep the pre-AdaLN embedding for the innovation gate; blocks retain their existing
+ # modulation input exactly.
+ t_base = self.t_embedder(t)
+ t_emb = t_base
+ if self.share_mod:
+ t_emb = self.adaLN_modulation(t_emb)
+ t_emb = manual_cast(t_emb, self.dtype)
+
+ innovation_time = None
+ innovation_y = None
+ innovation_gate_y = None
+ if self.innovation_gate is not None and partial_zobs_feats is not None:
+ # Observation-only terms are shared by all blocks. Empty rows deliberately pass
+ # through these linears too, so an all-drop batch keeps the full gate graph live.
+ gate_dtype = self.innovation_gate.embed_y.weight.dtype
+ innovation_gate_y = partial_zobs_feats.to(dtype=gate_dtype)
+ innovation_y = self.innovation_gate.embed_y(innovation_gate_y)
+ innovation_time = self.innovation_gate.time_proj(t_base)
+
+ if self.pe_mode == "ape":
+ pe = self.pos_embedder(h.coords[:, 1:])
+ h = h + manual_cast(pe, self.dtype)
+
+ # Handle different conditioning modes
+ if self.image_attn_mode == 'proj':
+ if isinstance(cond, dict):
+ global_cond = cond['global']
+ proj_cond = cond['proj']
+ else:
+ global_cond, proj_cond = cond
+ if isinstance(global_cond, list):
+ global_cond = sp.VarLenTensor.from_tensor_list(global_cond)
+ global_cond = manual_cast(global_cond, self.dtype)
+ proj_cond = manual_cast(proj_cond, self.dtype)
+ cond = (global_cond, proj_cond)
+ elif self.image_attn_mode == 'gated_proj':
+ global_cond = cond['global']
+ if isinstance(global_cond, list):
+ global_cond = sp.VarLenTensor.from_tensor_list(global_cond)
+ global_cond = manual_cast(global_cond, self.dtype)
+ proj_semantic = manual_cast(cond['proj_semantic'], self.dtype)
+ proj_color = manual_cast(cond['proj_color'], self.dtype)
+ cond = {'global': global_cond, 'proj_semantic': proj_semantic, 'proj_color': proj_color}
+ else:
+ if isinstance(cond, list):
+ cond = sp.VarLenTensor.from_tensor_list(cond)
+ cond = manual_cast(cond, self.dtype)
+
+ # Inference-only probe hook. When the attribute is absent (the default, and the
+ # only state any training path produces) this is None and the loop below is
+ # byte-identical to the unhooked path: no constructor arg, no config key, and no
+ # entry in the inference ABI contract. A probe sets it to a length-num_blocks
+ # sequence to rescale the shared partial message per block.
+ _partial_scales = getattr(self, 'partial_zobs_layer_scales', None)
+ # Optional evaluator-only callable: ``fn(gate_value, active_batch, block_index)``.
+ # It is an instance attribute rather than a constructor/config argument, so no model
+ # state or training ABI changes; callers can implement neutral, constant, or shuffled
+ # gate-value probes deterministically.
+ _gate_value_transform = getattr(self, 'partial_zobs_innovation_gate_value_transform', None)
+ _innovation_ratio_stats = [] if self.innovation_gate is not None else None
+ _innovation_gate_mean_stats = [] if self.innovation_gate is not None else None
+ _innovation_observed_rows = 0 if partial_zobs_rows is None else int(partial_zobs_rows.numel())
+ _local_attention = getattr(self, 'local_obs_attention', None)
+ _local_topology = None
+ _local_full_batch = None
+ _local_ratio_base_stats = [] if _local_attention is not None else None
+ _local_ratio_hidden_stats = [] if _local_attention is not None else None
+ _local_null_stats = [] if _local_attention is not None else None
+ _local_entropy_stats = [] if _local_attention is not None else None
+ _local_effective_neighbour_stats = [] if _local_attention is not None else None
+ _local_observed_residual_stats = [] if _local_attention is not None else None
+ _local_unobserved_residual_stats = [] if _local_attention is not None else None
+ _local_query_is_observed = None
+ _local_neighbourhood_histogram = None
+ if _local_attention is not None:
+ assert partial_zobs_rows is not None and partial_zobs_feats is not None
+ _local_topology = _build_two_hop_observed_topology(x.coords, partial_zobs_rows)
+ _local_full_batch = x.coords[:, 0].long()
+ _active_observed_mask = torch.zeros(
+ x.coords.shape[0], dtype=torch.bool, device=x.coords.device,
+ )
+ _active_observed_mask[partial_zobs_rows] = True
+ _local_query_is_observed = _active_observed_mask[
+ _local_topology['query_rows']
+ ]
+ _local_neighbourhood_histogram = torch.bincount(
+ _local_topology['neighbour_counts'], minlength=26,
+ )[:26]
+ _local_neighbourhood_histogram[0] = (
+ x.coords.shape[0] - _local_topology['query_rows'].numel()
+ )
+ assert int(_local_neighbourhood_histogram.sum()) == x.coords.shape[0]
+ assert int(_local_neighbourhood_histogram[1:].sum()) == \
+ _local_topology['query_rows'].numel()
+ _learned_attention = getattr(self, 'learned_attention', None)
+ _learned_cache = None
+ _learned_stats = [] if _learned_attention is not None else None
+ if _learned_attention is not None:
+ assert partial_zobs_inject is not None and partial_zobs_rows is not None
+ assert _partial_scales is None, \
+ 'partial_zobs_layer_scales is not defined for the five-site V12LA operator'
+ _learned_cache = _learned_attention.build_cache(
+ x.coords,
+ x.layout,
+ partial_zobs_inject,
+ partial_zobs_rows,
+ )
+ partial_zobs_inject_position = getattr(
+ self, 'partial_zobs_inject_position', 'post_block',
+ )
+ for _block_index, block in enumerate(self.blocks):
+ if partial_zobs_inject is not None and partial_zobs_inject_position == 'pre_mlp':
+ _pre_mlp_scale = 1.0 if _partial_scales is None else float(
+ _partial_scales[_block_index]
+ )
+ h = block(
+ h,
+ t_emb,
+ cond,
+ partial_zobs_rows=partial_zobs_rows,
+ partial_zobs_inject=partial_zobs_inject,
+ partial_zobs_inject_scale=_pre_mlp_scale,
+ )
+ else:
+ h = block(h, t_emb, cond)
+ if zobs_inject is not None:
+ h = h + zobs_inject
+ _innovation_ratio = None
+ _innovation_gate_mean = None
+ _local_delta = None
+ _local_stats = None
+ if _local_attention is not None and _block_index < _local_attention.alpha.numel():
+ assert _local_topology is not None and _local_full_batch is not None
+ _local_delta, _local_stats = _local_attention(
+ h.feats,
+ _local_full_batch,
+ t_base,
+ partial_zobs_feats,
+ partial_zobs_rows,
+ _local_topology,
+ _block_index,
+ )
+ _local_delta = manual_cast(_local_delta, self.dtype)
+ _local_ratio_base = _local_delta.norm() / partial_zobs_inject.norm().clamp_min(
+ torch.finfo(partial_zobs_inject.dtype).eps
+ )
+ _local_hidden = h.feats[_local_topology['query_rows']]
+ _local_ratio_hidden = _local_delta.norm() / _local_hidden.norm().clamp_min(
+ torch.finfo(_local_hidden.dtype).eps
+ )
+ assert _local_query_is_observed is not None
+ _local_observed_residual = _local_delta[
+ _local_query_is_observed
+ ].float().norm()
+ _local_unobserved_residual = _local_delta[
+ ~_local_query_is_observed
+ ].float().norm()
+ if _learned_attention is not None and _block_index in _learned_attention.site_to_slot:
+ assert _learned_cache is not None
+ _learned_inject, _site_stats = _learned_attention(
+ h.feats,
+ h.coords,
+ h.layout,
+ _learned_cache,
+ _block_index,
+ )
+ h = h.replace(h.feats + manual_cast(_learned_inject, self.dtype))
+ _learned_stats.append({
+ name: value.detach() for name, value in _site_stats.items()
+ })
+ if partial_zobs_inject is not None and _learned_attention is None and \
+ partial_zobs_inject_position == 'post_block':
+ layer_inject = partial_zobs_inject
+ if self.innovation_gate is not None:
+ gate = self.innovation_gate
+ # Every operation intentionally accepts an empty leading dimension. This
+ # keeps all innovation parameters in the all-drop DDP graph.
+ assert innovation_gate_y is not None and innovation_y is not None and innovation_time is not None
+ t_obs = innovation_time[partial_zobs_batch]
+ h_obs = h.feats[partial_zobs_rows].to(dtype=gate.embed_y.weight.dtype)
+ x_embed = gate.embed_x(torch.cat([h_obs, t_obs], dim=-1))
+ innovation = innovation_y - x_embed
+ gate_input = torch.cat([
+ innovation,
+ innovation_gate_y[:, 32:34],
+ t_obs,
+ ], dim=-1)
+ gate_value = torch.sigmoid(gate.mlp(gate_input))
+ if _gate_value_transform is not None:
+ gate_value = _gate_value_transform(
+ gate_value, partial_zobs_batch, _block_index
+ )
+ assert gate_value.shape == (innovation.shape[0], 1), \
+ f'gate value transform must return [Nobs,1], got {tuple(gate_value.shape)}'
+ delta = gate.proj(innovation) * gate_value
+ delta = delta * gate.alpha[_block_index].to(dtype=delta.dtype)
+ if _block_index == len(self.blocks) - 1:
+ # Keep the frozen V11 base carrier at the terminal site,
+ # but never add a newly learned observed-only shortcut:
+ # no attention block remains to transport/smooth it into U.
+ delta = delta * 0
+ # Detached telemetry for the training log. This is before the legacy
+ # layer-scale hook and therefore measures the learned branch itself.
+ _innovation_ratio = delta.norm() / partial_zobs_inject.norm().clamp_min(
+ torch.finfo(partial_zobs_inject.dtype).eps
+ )
+ _innovation_gate_mean = (
+ gate_value.mean() if gate_value.numel() != 0 else delta.new_zeros(())
+ )
+ layer_inject = partial_zobs_inject + manual_cast(delta, self.dtype)
+ if partial_zobs_rows.numel() != 0:
+ if _partial_scales is None:
+ h = h.replace(h.feats.index_add(
+ 0, partial_zobs_rows, layer_inject
+ ))
+ else:
+ h = h.replace(h.feats.index_add(
+ 0, partial_zobs_rows, layer_inject,
+ alpha=float(_partial_scales[_block_index]),
+ ))
+ else:
+ # Preserve every empty carrier/gate GEMM in the graph on all-drop batches.
+ h = h.replace(h.feats + layer_inject.sum() * 0)
+ if _local_delta is not None:
+ _local_scale = 1.0 if _partial_scales is None else float(
+ _partial_scales[_block_index]
+ )
+ if _local_topology['query_rows'].numel() != 0:
+ h = h.replace(h.feats.index_add(
+ 0, _local_topology['query_rows'], _local_delta,
+ alpha=_local_scale,
+ ))
+ else:
+ # The module returns an exact-zero parameter edge for all 14
+ # trainable tensors, so DDP observes finite numerical-zero grads.
+ h = h.replace(h.feats + _local_delta.sum() * _local_scale)
+ if self.innovation_gate is not None:
+ if _innovation_ratio is None:
+ _innovation_ratio = h.feats.new_zeros(())
+ _innovation_gate_mean = h.feats.new_zeros(())
+ _innovation_ratio_stats.append(_innovation_ratio.detach())
+ _innovation_gate_mean_stats.append(_innovation_gate_mean.detach())
+ if _local_attention is not None and _block_index < _local_attention.alpha.numel():
+ assert _local_stats is not None
+ _local_ratio_base_stats.append(_local_ratio_base.detach())
+ _local_ratio_hidden_stats.append(_local_ratio_hidden.detach())
+ _local_null_stats.append(_local_stats['null_attention_mean'].detach())
+ _local_entropy_stats.append(_local_stats['real_neighbour_entropy_mean'].detach())
+ _local_effective_neighbour_stats.append(
+ _local_stats['effective_real_neighbour_count_mean'].detach()
+ )
+ _local_observed_residual_stats.append(
+ _local_observed_residual.detach()
+ )
+ _local_unobserved_residual_stats.append(
+ _local_unobserved_residual.detach()
+ )
+
+ if self.innovation_gate is not None:
+ # Instance-only telemetry: deliberately neither buffer nor state-dict entry.
+ self.partial_zobs_innovation_gate_last_stats = {
+ 'delta_to_base_norm_ratio': torch.stack(_innovation_ratio_stats),
+ 'gate_mean': torch.stack(_innovation_gate_mean_stats),
+ 'observed_rows': _innovation_observed_rows,
+ }
+ if _local_attention is not None:
+ # Block 29 is structurally absent: every telemetry vector has length 29.
+ assert _local_query_is_observed is not None
+ assert _local_neighbourhood_histogram is not None
+ _full_support_rows = int(x.coords.shape[0])
+ _active_queries = int(_local_topology['query_rows'].numel())
+ _empty_queries = _full_support_rows - _active_queries
+ _singleton_queries = int(
+ (_local_topology['neighbour_counts'] == 1).sum()
+ )
+ self.partial_zobs_local_attention_last_stats = {
+ 'delta_to_base_norm_ratio': torch.stack(_local_ratio_base_stats),
+ 'delta_to_hidden_norm_ratio': torch.stack(_local_ratio_hidden_stats),
+ 'null_attention_mean': torch.stack(_local_null_stats),
+ 'real_neighbour_entropy_mean': torch.stack(_local_entropy_stats),
+ 'effective_real_neighbour_count_mean': torch.stack(
+ _local_effective_neighbour_stats
+ ),
+ 'observed_residual_l2_norm': torch.stack(
+ _local_observed_residual_stats
+ ),
+ 'unobserved_residual_l2_norm': torch.stack(
+ _local_unobserved_residual_stats
+ ),
+ 'observed_rows': _innovation_observed_rows,
+ 'full_support_rows': _full_support_rows,
+ 'active_queries': _active_queries,
+ 'empty_queries': _empty_queries,
+ 'singleton_queries': _singleton_queries,
+ 'observed_active_queries': int(_local_query_is_observed.sum()),
+ 'unobserved_active_queries': int((~_local_query_is_observed).sum()),
+ 'active_query_fraction': _active_queries / max(_full_support_rows, 1),
+ 'empty_query_fraction': _empty_queries / max(_full_support_rows, 1),
+ 'singleton_query_fraction': _singleton_queries / max(_active_queries, 1),
+ 'neighbourhood_size_histogram': _local_neighbourhood_histogram.detach(),
+ 'packed_pairs': int(_local_topology['source_indices'].numel()),
+ }
+ if _learned_attention is not None:
+ assert _learned_cache is not None
+ assert len(_learned_stats) == len(_learned_attention.sites)
+ self.partial_zobs_learned_attention_last_stats = {
+ name: torch.stack([site[name] for site in _learned_stats])
+ for name in _learned_stats[0]
+ }
+ self.partial_zobs_learned_attention_last_stats.update({
+ 'sites': _learned_attention.sites,
+ 'observed_rows': int(partial_zobs_rows.numel()),
+ 'full_support_rows': int(x.coords.shape[0]),
+ 'observed_counts': _learned_cache['observed_counts'],
+ })
+
+ h = manual_cast(h, x.dtype)
+ h = h.replace(F.layer_norm(h.feats, h.feats.shape[-1:]))
+ h = self.out_layer(h)
+ return h
+
+
+# Published Pixal3D geometry configs use this name. The former elastic mixin
+# only controlled training memory; inference uses the same SLatFlowModel.
+ElasticSLatFlowModel = SLatFlowModel
diff --git a/deployment/runtime/geometry/pixal3d/modules/projected_image_feature_extractor.py b/deployment/runtime/geometry/pixal3d/modules/projected_image_feature_extractor.py
new file mode 100644
index 0000000000000000000000000000000000000000..d532b1e781b02d44b9bbce0eb8e9fd092469e5de
--- /dev/null
+++ b/deployment/runtime/geometry/pixal3d/modules/projected_image_feature_extractor.py
@@ -0,0 +1,458 @@
+"""Frozen DINOv3 features with camera-aligned projection for Pixal3D inference."""
+from typing import List, Optional, Tuple, Union
+
+import numpy as np
+from PIL import Image
+import torch
+from torch import nn
+import torch.nn.functional as F
+from torchvision import transforms
+
+
+def project_points_to_image_batch(
+ points_3d: torch.Tensor,
+ transform_matrix: torch.Tensor,
+ camera_angle_x: torch.Tensor,
+ resolution: int = 518
+) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
+ """
+ Project 3D points to 2D image coordinates (batch processing).
+
+ Args:
+ points_3d: torch.Tensor, shape [N, 3] or [B, N, 3], 3D point coordinates (in [-1, 1] range)
+ transform_matrix: torch.Tensor, shape [B, 4, 4], camera transformation matrix
+ camera_angle_x: torch.Tensor, shape [B], horizontal field of view angle (radians)
+ resolution: int, image resolution, default 518
+
+ Returns:
+ points_2d: torch.Tensor, shape [B, N, 2], image coordinates [x, y]
+ depth: torch.Tensor, shape [B, N], depth values
+ valid_mask: torch.Tensor, shape [B, N], mask for points within view
+ """
+ device = points_3d.device
+ B = transform_matrix.shape[0]
+
+ # Ensure inputs are torch.Tensor on correct device
+ if not isinstance(transform_matrix, torch.Tensor):
+ transform_matrix = torch.tensor(transform_matrix, dtype=torch.float32, device=device)
+ if not isinstance(points_3d, torch.Tensor):
+ points_3d = torch.tensor(points_3d, dtype=torch.float32, device=device)
+ if not isinstance(camera_angle_x, torch.Tensor):
+ camera_angle_x = torch.tensor(camera_angle_x, dtype=torch.float32, device=device)
+
+ # Expand points_3d to batch dimension: [N, 3] -> [B, N, 3]
+ if points_3d.dim() == 2:
+ points_3d_batch = points_3d.unsqueeze(0).expand(B, -1, -1)
+ else:
+ points_3d_batch = points_3d
+ N = points_3d_batch.shape[1]
+
+ # Add homogeneous coordinates: [B, N, 3] -> [B, N, 4]
+ ones = torch.ones(B, N, 1, device=device, dtype=points_3d_batch.dtype)
+ points_homogeneous = torch.cat([points_3d_batch, ones], dim=-1) # [B, N, 4]
+
+ # Compute world to camera transformation matrix
+ world_to_camera = torch.linalg.inv(transform_matrix.float()).to(transform_matrix.dtype) # linalg.inv requires fp32+
+
+ # Batch transform to camera coordinate system: [B, N, 4] @ [B, 4, 4]^T -> [B, N, 3]
+ points_camera = torch.bmm(points_homogeneous, world_to_camera.transpose(-2, -1))[..., :3] # [B, N, 3]
+
+ # Extract camera coordinates
+ x_cam = points_camera[..., 0] # [B, N]
+ y_cam = points_camera[..., 1] # [B, N]
+ z_cam = points_camera[..., 2] # [B, N]
+
+ # Depth value (Z value in camera coordinate system, note Blender camera faces -Z direction)
+ depth = -z_cam # [B, N]
+
+ # Compute camera intrinsics (batch processing)
+ sensor_width = 32.0 # mm
+ focal_length = 16.0 / torch.tan(camera_angle_x / 2.0) # [B]
+ focal_length_pixels = focal_length * resolution / sensor_width # [B]
+
+ # Expand focal_length_pixels dimension for broadcasting: [B] -> [B, 1]
+ focal_length_pixels = focal_length_pixels.unsqueeze(1) # [B, 1]
+
+ # Perspective projection to NDC coordinates
+ x_ndc = focal_length_pixels * x_cam / (-z_cam + 1e-8) # [B, N]
+ y_ndc = focal_length_pixels * y_cam / (-z_cam + 1e-8) # [B, N]
+
+ # Convert to image coordinates (pixel coordinates)
+ x_pixel = x_ndc + resolution / 2.0 # [B, N]
+ y_pixel = -y_ndc + resolution / 2.0 # [B, N], flip Y axis
+
+ # Create validity mask (points within image range and in front of camera)
+ valid_mask = (
+ (x_pixel >= 0) & (x_pixel < resolution) &
+ (y_pixel >= 0) & (y_pixel < resolution) &
+ (depth > 0) # In front of camera
+ ) # [B, N]
+
+ points_2d = torch.stack([x_pixel, y_pixel], dim=-1) # [B, N, 2]
+
+ return points_2d, depth, valid_mask
+
+def sample_features(fmap: torch.Tensor, queries_ndc: torch.Tensor) -> torch.Tensor:
+ """
+ Sample features from feature map at specified NDC coordinates.
+
+ Args:
+ fmap: torch.Tensor, shape [B, C, H, W], feature map
+ queries_ndc: torch.Tensor, shape [B, K, 2], normalized device coordinates
+
+ Returns:
+ torch.Tensor, shape [B, C, K], sampled features
+ """
+ B, C, H, W = fmap.shape
+ Bq, K, _ = queries_ndc.shape
+ assert Bq == B, "Batch size mismatch"
+
+ # grid_sample requires (B, out_h, out_w, 2), here we want K points -> out_h=K, out_w=1
+ grid = queries_ndc.view(B, K, 1, 2) # (B, K, 1, 2)
+
+ # Bilinear interpolation, align_corners=False (consistent with [-1,1] pixel center convention)
+ feat = F.grid_sample(
+ fmap, grid, mode='bilinear',
+ align_corners=False, padding_mode='border' # border avoids out-of-bound becoming 0
+ ) # (B, C, K, 1)
+
+ return feat.squeeze(-1) # (B, C, K)
+
+class ProjGrid(nn.Module):
+ """
+ 3D Grid Projection Module.
+
+ Projects a 3D grid of points to 2D image coordinates and samples features
+ from the image feature map at those locations.
+
+ This is the core module for view-aligned feature extraction.
+ """
+ def __init__(self, grid_resolution: int = 16, image_resolution: int = 518):
+ super().__init__()
+ self.grid_resolution = grid_resolution
+ self.image_resolution = image_resolution
+
+ # Create 3D grid points
+ one_dim = torch.linspace(-1, 1, grid_resolution)
+ x, y, z = torch.meshgrid(one_dim, one_dim, one_dim, indexing='ij')
+ grid_points = torch.stack((x, y, z), dim=-1)
+
+ # Rotation matrix to align with Blender coordinate system
+ rotation_matrix = torch.tensor([
+ [1.0, 0.0, 0.0],
+ [0.0, 0.0, -1.0],
+ [0.0, 1.0, 0.0]
+ ])
+ grid_points = torch.matmul(grid_points, rotation_matrix.T)
+ grid_points = grid_points.reshape(-1, 3)
+ self.register_buffer('grid_points', grid_points) # [R³, 3]
+
+ # Default front view transformation matrix
+ front_view_transform_matrix = torch.tensor([
+ [1.0, 0.0, 0.0, 0.0],
+ [0.0, 0.0, -1.0, -2.0],
+ [0.0, 1.0, 0.0, 0.0],
+ [0.0, 0.0, 0.0, 1.0]
+ ])
+ self.register_buffer("front_view_transform_matrix", front_view_transform_matrix)
+
+ def forward(
+ self,
+ features_map: torch.Tensor,
+ camera_angle_x: torch.Tensor,
+ distance: torch.Tensor,
+ mesh_scale: torch.Tensor,
+ transform_matrix: Optional[torch.Tensor] = None,
+ BHWC: bool = True
+ ) -> torch.Tensor:
+ """
+ Project 3D grid points to image and sample features.
+
+ Args:
+ features_map: Feature map, shape [B, H, W, C] if BHWC else [B, C, H, W]
+ camera_angle_x: Camera FOV angle, shape [B]
+ distance: Camera distance, shape [B]
+ mesh_scale: Mesh scale factor, shape [B]
+ transform_matrix: Optional camera transform matrix, shape [B, 4, 4]
+ BHWC: Whether features_map is in BHWC format
+
+ Returns:
+ Projected features, shape [B, grid_resolution³, C]
+ """
+ if BHWC:
+ B, H, W, C = features_map.shape
+ else:
+ B, C, H, W = features_map.shape
+
+ grid_points = self.grid_points
+ grid_points = grid_points.expand(B, -1, -1)
+ grid_points = grid_points / mesh_scale.unsqueeze(-1).unsqueeze(-1) / 2 # Scale alignment
+ assert transform_matrix is None, "transform_matrix is not None"
+ if transform_matrix is None:
+ transform_matrix = self.front_view_transform_matrix
+ transform_matrix = transform_matrix.expand(B, -1, -1).clone()
+ transform_matrix[:, 1, 3] = -distance # Set camera distance
+
+ # Project to image coordinates (simulate Blender projection)
+ image_points, depth, valid_mask = project_points_to_image_batch(
+ grid_points, transform_matrix, camera_angle_x, self.image_resolution
+ )
+
+ # Normalize to [-1, 1] for grid_sample
+ image_points_norm = (image_points + 0.5) / self.image_resolution * 2 - 1
+
+ if BHWC:
+ features_map = features_map.permute(0, 3, 1, 2) # [B, C, H, W]
+
+ # Sample features from DINOv3 patch feature map
+ x = sample_features(features_map, image_points_norm) # [B, C, K]
+ x = x.permute(0, 2, 1) # [B, K, C]
+
+ return x
+
+
+class DinoV3ProjFeatureExtractor(nn.Module):
+ """
+ DINOv3 Feature Extractor with View-Aligned Projection.
+
+ This extractor produces both:
+ 1. Global features (CLS token + register tokens) in embed_dim
+ 2. View-aligned projected features (3D grid projected to 2D and sampled)
+ - Without NAF: [B, R³, embed_dim]
+ - With NAF: [B, R³, embed_dim * 2] (concat of lr and hr features)
+
+ NOTE: proj_linear has been moved to per-block ProjectAttention / SparseProjectAttention.
+ This module now outputs raw DINOv3 features for proj (optionally concatenated with NAF-upsampled features).
+
+ Args:
+ model_name: Name of the pretrained DINOv3 model
+ image_size: Input image size (default: 512)
+ grid_resolution: Resolution of the 3D projection grid (default: 16)
+ use_naf_upsample: Whether to use NAF to upsample features (default: False)
+ naf_target_size: Target spatial size for NAF upsampling (default: [128, 128])
+ """
+ def __init__(
+ self,
+ model_name: str,
+ image_size: int = 512,
+ grid_resolution: int = 16,
+ use_naf_upsample: bool = False,
+ naf_target_size: Optional[List[int]] = None,
+ cls_only: bool = False,
+ cls_n_tokens: int = 1,
+ full_tokens: bool = False,
+ ):
+ super().__init__()
+ self.model_name = model_name
+ self.image_size = image_size
+ self.grid_resolution = grid_resolution
+ self.use_naf_upsample = use_naf_upsample
+ self.cls_only = cls_only
+ self.cls_n_tokens = cls_n_tokens
+ self.full_tokens = full_tokens
+ if naf_target_size is None:
+ self.naf_target_size = (128, 128)
+ elif isinstance(naf_target_size, int):
+ self.naf_target_size = (naf_target_size, naf_target_size)
+ else:
+ self.naf_target_size = tuple(naf_target_size)
+
+ # Load DINOv3 model (frozen, no trainable params in this module)
+ from transformers import DINOv3ViTModel
+
+ self.model = DINOv3ViTModel.from_pretrained(model_name)
+ self.model.eval()
+ self.model.requires_grad_(False)
+
+ # Image transform (only normalize, no resize - assume already resized)
+ self.transform = transforms.Compose([
+ transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
+ ])
+
+ # Get patch info
+ self.patch_size = self.model.config.patch_size
+ self.patch_number = image_size // self.patch_size
+ self.embed_dim = self.model.config.hidden_size
+
+ # Projection grid for view-aligned features
+ self.proj_grid = ProjGrid(
+ grid_resolution=grid_resolution,
+ image_resolution=image_size
+ )
+
+ # NAF upsampler (frozen, no trainable params)
+ self.naf_model = None # Lazy-loaded on first use to avoid import if not needed
+
+ # proj_channels: the output dimension of proj features
+ # Without NAF: embed_dim (e.g. 1024)
+ # With NAF: embed_dim * 2 (e.g. 2048, concat of lr and hr)
+ self.proj_channels = self.embed_dim * 2 if use_naf_upsample else self.embed_dim
+
+ # NOTE: proj_linear removed — now lives in each denoiser block's ProjectAttention
+
+ def _load_naf(self):
+ """Lazy-load pretrained NAF model."""
+ if self.naf_model is None:
+ import torch.hub
+ device = next(self.model.parameters()).device
+ self.naf_model = torch.hub.load(
+ "valeoai/NAF", "naf", pretrained=True, device=device, trust_repo=True
+ )
+ self.naf_model.eval()
+ self.naf_model.requires_grad_(False)
+
+ def to(self, device):
+ super().to(device)
+ self.model.to(device)
+ self.proj_grid.to(device)
+ if self.naf_model is not None:
+ self.naf_model.to(device)
+ return self
+
+ def cuda(self):
+ super().cuda()
+ self.model.cuda()
+ self.proj_grid.cuda()
+ if self.naf_model is not None:
+ self.naf_model.cuda()
+ return self
+
+ def cpu(self):
+ super().cpu()
+ self.model.cpu()
+ self.proj_grid.cpu()
+ if self.naf_model is not None:
+ self.naf_model.cpu()
+ return self
+
+ def extract_features(self, image: torch.Tensor) -> torch.Tensor:
+ """Extract features using DINOv3."""
+ image = image.to(self.model.embeddings.patch_embeddings.weight.dtype)
+ hidden_states = self.model.embeddings(image, bool_masked_pos=None)
+ position_embeddings = self.model.rope_embeddings(image)
+
+ for layer_module in self.model.layer:
+ hidden_states = layer_module(
+ hidden_states,
+ position_embeddings=position_embeddings,
+ )
+
+ return F.layer_norm(hidden_states, hidden_states.shape[-1:])
+
+ def forward(
+ self,
+ image: Union[torch.Tensor, List[Image.Image]],
+ camera_angle_x: Optional[torch.Tensor] = None,
+ distance: Optional[torch.Tensor] = None,
+ mesh_scale: Optional[torch.Tensor] = None,
+ transform_matrix: Optional[torch.Tensor] = None,
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
+ """
+ Extract view-aligned features from the image.
+
+ Args:
+ image: Input image tensor [B, C, H, W] or list of PIL images
+ camera_angle_x: Camera FOV angle in radians [B]
+ distance: Camera distance [B]
+ mesh_scale: Mesh scale factor [B]
+ transform_matrix: Optional camera transform matrix [B, 4, 4]
+
+ Returns:
+ Tuple of (global_features, proj_features):
+ - global_features: [B, num_global_tokens, embed_dim]
+ - proj_features: [B, grid_resolution³, proj_channels]
+ where proj_channels = embed_dim (no NAF) or embed_dim*2 (with NAF)
+ """
+ # Handle input types
+ if isinstance(image, torch.Tensor):
+ assert image.ndim == 4, "Image tensor should be batched (B, C, H, W)"
+ elif isinstance(image, list):
+ assert all(isinstance(i, Image.Image) for i in image), "Image list should be list of PIL images"
+ image = [i.resize((self.image_size, self.image_size), Image.LANCZOS) for i in image]
+ image = [np.array(i.convert('RGB')).astype(np.float32) / 255 for i in image]
+ image = [torch.from_numpy(i).permute(2, 0, 1).float() for i in image]
+ image = torch.stack(image).cuda()
+ else:
+ raise ValueError(f"Unsupported type of image: {type(image)}")
+
+ B = image.shape[0]
+
+ # Trained-bridge CLS cross-attn mode: skip ProjGrid + NAF (no camera); return global token(s).
+ # cls_n_tokens=1 -> CLS only (v4 default); =5 (1 CLS + 4 register) -> faithful warm start of the
+ # shipped cross-attn (trained on [CLS + register] global tokens; registers are global, NOT patch).
+ # full_tokens=True (v6 patchcross) -> the UNSLICED [CLS|4 reg|patch...] sequence
+ # (1029 tokens @ image 512), i.e. the TRELLIS.2 cross-attn context. Lives in this branch
+ # because the non-cls path below requires camera params for ProjGrid.
+ if getattr(self, 'cls_only', False):
+ img = self.transform(image)
+ with torch.no_grad():
+ z = self.extract_features(img)
+ if getattr(self, 'full_tokens', False):
+ return z # [B, 1+4+patch_number**2, D=1024]
+ return z[:, 0:self.cls_n_tokens] # [B, cls_n_tokens, D=1024]
+
+ # Keep a copy of the unnormalized image for NAF guide
+ if self.use_naf_upsample:
+ image_for_naf = image.clone() # [B, 3, H, W], in [0, 1] range
+
+ # Apply transform (ImageNet normalization)
+ image = self.transform(image)
+
+ # Extract DINOv3 features (frozen, no gradients)
+ with torch.no_grad():
+ z = self.extract_features(image)
+
+ # Split into CLS token, register tokens, and patch tokens
+ z_clstoken = z[:, 0:1] # [B, 1, D]
+ num_reg = getattr(self.model.config, 'num_register_tokens', 4)
+ z_regtokens = z[:, 1:1+num_reg] # [B, num_reg, D]
+ z_patchtokens = z[:, 1+num_reg:] # [B, num_patches, D]
+
+ # Reshape patch tokens to spatial grid: [B, h, w, D]
+ z_patchtokens_spatial = z_patchtokens.reshape(
+ B, self.patch_number, self.patch_number, -1
+ ) # [B, h, w, D]
+
+ if camera_angle_x is None or distance is None or mesh_scale is None:
+ raise ValueError("camera_angle_x, distance, and mesh_scale must be provided")
+
+ # --- Low-resolution branch: sample from DINOv3 patch feature map ---
+ z_proj_lr = self.proj_grid(
+ z_patchtokens_spatial,
+ camera_angle_x,
+ distance,
+ mesh_scale,
+ transform_matrix
+ ) # [B, grid_res³, D]
+
+ # --- High-resolution branch (NAF): upsample then sample ---
+ if self.use_naf_upsample:
+ self._load_naf()
+ # NAF expects: guide [B, 3, H, W], lr_features [B, C, h, w], target_size (H', W')
+ lr_features_bchw = z_patchtokens_spatial.permute(0, 3, 1, 2) # [B, D, h, w]
+ hr_features = self.naf_model(
+ image_for_naf, lr_features_bchw, self.naf_target_size
+ ) # [B, D, H', W']
+
+ # Sample from high-res feature map using same projection coordinates
+ z_proj_hr = self.proj_grid(
+ hr_features,
+ camera_angle_x,
+ distance,
+ mesh_scale,
+ transform_matrix,
+ BHWC=False # hr_features is [B, C, H', W']
+ ) # [B, grid_res³, D]
+
+ # Concatenate lr and hr: [B, grid_res³, D*2]
+ z_proj = torch.cat([z_proj_lr, z_proj_hr], dim=-1)
+ else:
+ z_proj = z_proj_lr # [B, grid_res³, D]
+
+ # Combine global tokens
+ z_global = torch.cat([z_clstoken, z_regtokens], dim=1) # [B, 1+num_reg, D]
+
+ # proj_linear has been moved to per-block ProjectAttention
+ # z_proj stays in proj_channels, each block will project independently
+
+ return z_global, z_proj
diff --git a/deployment/runtime/geometry/pixal3d/pipelines/__init__.py b/deployment/runtime/geometry/pixal3d/pipelines/__init__.py
index de01583213347ec0172b5d2ab7c21cd03474136e..b882ce968b6cbc6eee9093e173a14d3a1e26c6ce 100644
--- a/deployment/runtime/geometry/pixal3d/pipelines/__init__.py
+++ b/deployment/runtime/geometry/pixal3d/pipelines/__init__.py
@@ -1,57 +1,55 @@
-import importlib
-
-__attributes = {
- "Trellis2ImageTo3DPipeline": "trellis2_image_to_3d",
- "Trellis2TexturingPipeline": "trellis2_texturing",
- "Pixal3DImageTo3DPipeline": "pixal3d_image_to_3d",
- "build_v14fx_inference_model": "v14fx",
- "sample_v14fx_texture": "v14fx",
-}
-
-
-__submodules = ['samplers', 'rembg']
-
-__all__ = list(__attributes.keys()) + __submodules
-
-def __getattr__(name):
- if name not in globals():
- if name in __attributes:
- module_name = __attributes[name]
- module = importlib.import_module(f".{module_name}", __name__)
- globals()[name] = getattr(module, name)
- elif name in __submodules:
- module = importlib.import_module(f".{name}", __name__)
- globals()[name] = module
- else:
- raise AttributeError(f"module {__name__} has no attribute {name}")
- return globals()[name]
-
-
-def from_pretrained(path: str):
- """
- Load a pipeline from a model folder or a Hugging Face model hub.
-
- Args:
- path: The path to the model. Can be either local path or a Hugging Face model name.
- """
- import os
- import json
- is_local = os.path.exists(f"{path}/pipeline.json")
-
- if is_local:
- config_file = f"{path}/pipeline.json"
- else:
- from huggingface_hub import hf_hub_download
- config_file = hf_hub_download(path, "pipeline.json")
-
- with open(config_file, 'r') as f:
- config = json.load(f)
- return globals()[config['name']].from_pretrained(path)
-
-
-# For PyLance
-if __name__ == '__main__':
- from . import samplers, rembg
- from .trellis2_image_to_3d import Trellis2ImageTo3DPipeline
- from .trellis2_texturing import Trellis2TexturingPipeline
- from .pixal3d_image_to_3d import Pixal3DImageTo3DPipeline
+import importlib
+
+__attributes = {
+ "Trellis2ImageTo3DPipeline": "trellis2_image_to_3d",
+ "Trellis2TexturingPipeline": "trellis2_texturing",
+ "Pixal3DImageTo3DPipeline": "pixal3d_image_to_3d",
+}
+
+
+__submodules = ['samplers', 'rembg']
+
+__all__ = list(__attributes.keys()) + __submodules
+
+def __getattr__(name):
+ if name not in globals():
+ if name in __attributes:
+ module_name = __attributes[name]
+ module = importlib.import_module(f".{module_name}", __name__)
+ globals()[name] = getattr(module, name)
+ elif name in __submodules:
+ module = importlib.import_module(f".{name}", __name__)
+ globals()[name] = module
+ else:
+ raise AttributeError(f"module {__name__} has no attribute {name}")
+ return globals()[name]
+
+
+def from_pretrained(path: str):
+ """
+ Load a pipeline from a model folder or a Hugging Face model hub.
+
+ Args:
+ path: The path to the model. Can be either local path or a Hugging Face model name.
+ """
+ import os
+ import json
+ is_local = os.path.exists(f"{path}/pipeline.json")
+
+ if is_local:
+ config_file = f"{path}/pipeline.json"
+ else:
+ from huggingface_hub import hf_hub_download
+ config_file = hf_hub_download(path, "pipeline.json")
+
+ with open(config_file, 'r') as f:
+ config = json.load(f)
+ return globals()[config['name']].from_pretrained(path)
+
+
+# For PyLance
+if __name__ == '__main__':
+ from . import samplers, rembg
+ from .trellis2_image_to_3d import Trellis2ImageTo3DPipeline
+ from .trellis2_texturing import Trellis2TexturingPipeline
+ from .pixal3d_image_to_3d import Pixal3DImageTo3DPipeline
diff --git a/deployment/runtime/geometry/pixal3d/pipelines/rembg/RMBG2.py b/deployment/runtime/geometry/pixal3d/pipelines/rembg/RMBG2.py
index 1a809226fc3da4607f956f34052d751bb2cf205d..13cfc519256518e04c403d1bb85757e7e92e1218 100644
--- a/deployment/runtime/geometry/pixal3d/pipelines/rembg/RMBG2.py
+++ b/deployment/runtime/geometry/pixal3d/pipelines/rembg/RMBG2.py
@@ -1,64 +1,59 @@
-from typing import Optional
-import os
-from pathlib import Path
-
-from transformers import AutoModelForImageSegmentation
-import torch
-from torchvision import transforms
-from PIL import Image
-
-
-_CY_ROOT = os.environ.get("CY_ROOT", str(Path(__file__).parents[5]))
-RMBG2_MODEL_PATH = os.environ.get(
- "PIXAL3D_RMBG_MODEL_PATH",
- os.path.join(_CY_ROOT, "model_ckpts", "RMBG-2.0"),
-)
-_REMOTE_MODEL_NAMES = {"ZhengPeng7/BiRefNet", "briaai/RMBG-2.0"}
-
-
-class RMBG2:
- def __init__(
- self,
- model_name: str = RMBG2_MODEL_PATH,
- alpha_threshold: Optional[float] = None,
- ):
- if model_name in _REMOTE_MODEL_NAMES:
- model_name = RMBG2_MODEL_PATH
- if alpha_threshold is not None and not 0.0 <= alpha_threshold <= 1.0:
- raise ValueError(f"alpha_threshold must be in [0, 1], got {alpha_threshold}")
- self.model_name = model_name
- self.alpha_threshold = alpha_threshold
- self.model = AutoModelForImageSegmentation.from_pretrained(
- model_name, trust_remote_code=True
- )
- self.model.eval()
- self.transform_image = transforms.Compose(
- [
- transforms.Resize((1024, 1024)),
- transforms.ToTensor(),
- transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]),
- ]
- )
-
- def to(self, device: str):
- self.model.to(device)
-
- def cuda(self):
- self.model.cuda()
-
- def cpu(self):
- self.model.cpu()
-
- def __call__(self, image: Image.Image) -> Image.Image:
- image_size = image.size
- input_images = self.transform_image(image).unsqueeze(0).to("cuda")
- with torch.no_grad():
- preds = self.model(input_images)[-1].sigmoid().cpu()
- pred = preds[0].squeeze()
- pred_pil = transforms.ToPILImage()(pred)
- mask = pred_pil.resize(image_size)
- if self.alpha_threshold is not None:
- cutoff = int(round(self.alpha_threshold * 255))
- mask = mask.point([0 if value < cutoff else 255 for value in range(256)])
- image.putalpha(mask)
- return image
+from typing import Optional
+import os
+
+from transformers import AutoModelForImageSegmentation
+import torch
+from torchvision import transforms
+from PIL import Image
+
+
+# A local RMBG-2.0 or BiRefNet directory can replace the Hub model.
+RMBG2_MODEL_PATH = os.environ.get("PIXAL3D_RMBG_MODEL_PATH", "briaai/RMBG-2.0")
+
+
+class RMBG2:
+ def __init__(
+ self,
+ model_name: str = RMBG2_MODEL_PATH,
+ alpha_threshold: Optional[float] = None,
+ ):
+ if model_name == "briaai/RMBG-2.0":
+ model_name = RMBG2_MODEL_PATH
+ if alpha_threshold is not None and not 0.0 <= alpha_threshold <= 1.0:
+ raise ValueError(f"alpha_threshold must be in [0, 1], got {alpha_threshold}")
+ self.model_name = model_name
+ self.alpha_threshold = alpha_threshold
+ self.model = AutoModelForImageSegmentation.from_pretrained(
+ model_name, trust_remote_code=True
+ )
+ self.model.eval()
+ self.transform_image = transforms.Compose(
+ [
+ transforms.Resize((1024, 1024)),
+ transforms.ToTensor(),
+ transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]),
+ ]
+ )
+
+ def to(self, device: str):
+ self.model.to(device)
+
+ def cuda(self):
+ self.model.cuda()
+
+ def cpu(self):
+ self.model.cpu()
+
+ def __call__(self, image: Image.Image) -> Image.Image:
+ image_size = image.size
+ input_images = self.transform_image(image).unsqueeze(0).to("cuda")
+ with torch.no_grad():
+ preds = self.model(input_images)[-1].sigmoid().cpu()
+ pred = preds[0].squeeze()
+ pred_pil = transforms.ToPILImage()(pred)
+ mask = pred_pil.resize(image_size)
+ if self.alpha_threshold is not None:
+ cutoff = int(round(self.alpha_threshold * 255))
+ mask = mask.point([0 if value < cutoff else 255 for value in range(256)])
+ image.putalpha(mask)
+ return image
diff --git a/deployment/runtime/geometry/pixal3d/representations/mesh/base.py b/deployment/runtime/geometry/pixal3d/representations/mesh/base.py
index 23bcf6d14ebac5b08893579fb7dd834a11c0d9a1..bff3d8eaae4bf679b8ba099808a83761e2bf2dbb 100644
--- a/deployment/runtime/geometry/pixal3d/representations/mesh/base.py
+++ b/deployment/runtime/geometry/pixal3d/representations/mesh/base.py
@@ -1,239 +1,239 @@
-from typing import *
-import torch
-from ..voxel import Voxel
-import cumesh
-# NOTE: flex_gemm is imported lazily in query_attrs (matching the renderers' pattern), NOT here.
-# This module sits in the DataLoader workers' import closure (dataset -> MeshWithVoxel), and
-# flex_gemm's triton @autotune decorators query the GPU driver AT IMPORT — in a spawned CPU-only
-# worker that intermittently dies with "RuntimeError: 0 active drivers", killing the rank and
-# wedging all other ranks in NCCL collectives (observed 2026-07-02 on the 64-rank 512-all run).
-
-
-class Mesh:
- def __init__(self,
- vertices,
- faces,
- vertex_attrs=None
- ):
- self.vertices = vertices.float()
- self.faces = faces.int()
- self.vertex_attrs = vertex_attrs
-
- @property
- def device(self):
- return self.vertices.device
-
- def to(self, device, non_blocking=False):
- return Mesh(
- self.vertices.to(device, non_blocking=non_blocking),
- self.faces.to(device, non_blocking=non_blocking),
- self.vertex_attrs.to(device, non_blocking=non_blocking) if self.vertex_attrs is not None else None,
- )
-
- def cuda(self, non_blocking=False):
- return self.to('cuda', non_blocking=non_blocking)
-
- def cpu(self):
- return self.to('cpu')
-
- def fill_holes(self, max_hole_perimeter=3e-2):
- vertices = self.vertices.clone().cuda().contiguous()
- faces = self.faces.clone().cuda().contiguous()
-
- mesh = cumesh.CuMesh()
- mesh.init(vertices, faces)
- mesh.get_edges()
- mesh.get_boundary_info()
- if mesh.num_boundaries == 0:
- return
- mesh.get_vertex_edge_adjacency()
- mesh.get_vertex_boundary_adjacency()
- mesh.get_manifold_boundary_adjacency()
- mesh.read_manifold_boundary_adjacency()
- mesh.get_boundary_connected_components()
- mesh.get_boundary_loops()
- if mesh.num_boundary_loops == 0:
- return
- mesh.fill_holes(max_hole_perimeter=max_hole_perimeter)
- new_vertices, new_faces = mesh.read()
-
- self.vertices = new_vertices.to(self.device)
- self.faces = new_faces.to(self.device)
-
- def remove_faces(self, face_mask: torch.Tensor):
- vertices = self.vertices.clone().cuda().contiguous()
- faces = self.faces.clone().cuda().contiguous()
-
- mesh = cumesh.CuMesh()
- mesh.init(vertices, faces)
- mesh.remove_faces(face_mask)
- new_vertices, new_faces = mesh.read()
-
- self.vertices = new_vertices.to(self.device)
- self.faces = new_faces.to(self.device)
-
- def simplify(self, target=1000000, verbose: bool=False, options: dict={}):
- vertices = self.vertices.clone().cuda().contiguous()
- faces = self.faces.clone().cuda().contiguous()
-
- mesh = cumesh.CuMesh()
- mesh.init(vertices, faces)
- mesh.simplify(target, verbose=verbose, options=options)
- new_vertices, new_faces = mesh.read()
-
- self.vertices = new_vertices.to(self.device)
- self.faces = new_faces.to(self.device)
-
-
-class TextureFilterMode:
- CLOSEST = 0
- LINEAR = 1
-
-
-class TextureWrapMode:
- CLAMP_TO_EDGE = 0
- REPEAT = 1
- MIRRORED_REPEAT = 2
-
-
-class AlphaMode:
- OPAQUE = 0
- MASK = 1
- BLEND = 2
-
-
-class Texture:
- def __init__(
- self,
- image: torch.Tensor,
- filter_mode: TextureFilterMode = TextureFilterMode.LINEAR,
- wrap_mode: TextureWrapMode = TextureWrapMode.REPEAT
- ):
- self.image = image
- self.filter_mode = filter_mode
- self.wrap_mode = wrap_mode
-
- def to(self, device, non_blocking=False):
- return Texture(
- self.image.to(device, non_blocking=non_blocking),
- self.filter_mode,
- self.wrap_mode,
- )
-
-
-class PbrMaterial:
- def __init__(
- self,
- base_color_texture: Optional[Texture] = None,
- base_color_factor: Union[torch.Tensor, List[float]] = [1.0, 1.0, 1.0],
- metallic_texture: Optional[Texture] = None,
- metallic_factor: float = 1.0,
- roughness_texture: Optional[Texture] = None,
- roughness_factor: float = 1.0,
- alpha_texture: Optional[Texture] = None,
- alpha_factor: float = 1.0,
- alpha_mode: AlphaMode = AlphaMode.OPAQUE,
- alpha_cutoff: float = 0.5,
- ):
- self.base_color_texture = base_color_texture
- self.base_color_factor = torch.tensor(base_color_factor, dtype=torch.float32)[:3]
- self.metallic_texture = metallic_texture
- self.metallic_factor = metallic_factor
- self.roughness_texture = roughness_texture
- self.roughness_factor = roughness_factor
- self.alpha_texture = alpha_texture
- self.alpha_factor = alpha_factor
- self.alpha_mode = alpha_mode
- self.alpha_cutoff = alpha_cutoff
-
- def to(self, device, non_blocking=False):
- return PbrMaterial(
- base_color_texture=self.base_color_texture.to(device, non_blocking=non_blocking) if self.base_color_texture is not None else None,
- base_color_factor=self.base_color_factor.to(device, non_blocking=non_blocking),
- metallic_texture=self.metallic_texture.to(device, non_blocking=non_blocking) if self.metallic_texture is not None else None,
- metallic_factor=self.metallic_factor,
- roughness_texture=self.roughness_texture.to(device, non_blocking=non_blocking) if self.roughness_texture is not None else None,
- roughness_factor=self.roughness_factor,
- alpha_texture=self.alpha_texture.to(device, non_blocking=non_blocking) if self.alpha_texture is not None else None,
- alpha_factor=self.alpha_factor,
- alpha_mode=self.alpha_mode,
- alpha_cutoff=self.alpha_cutoff,
- )
-
-
-class MeshWithPbrMaterial(Mesh):
- def __init__(self,
- vertices,
- faces,
- material_ids,
- uv_coords,
- materials: List[PbrMaterial],
- ):
- self.vertices = vertices.float()
- self.faces = faces.int()
- self.material_ids = material_ids # [M]
- self.uv_coords = uv_coords # [M, 3, 2]
- self.materials = materials
- self.layout = {
- 'base_color': slice(0, 3),
- 'metallic': slice(3, 4),
- 'roughness': slice(4, 5),
- 'alpha': slice(5, 6),
- }
-
- def to(self, device, non_blocking=False):
- return MeshWithPbrMaterial(
- self.vertices.to(device, non_blocking=non_blocking),
- self.faces.to(device, non_blocking=non_blocking),
- self.material_ids.to(device, non_blocking=non_blocking),
- self.uv_coords.to(device, non_blocking=non_blocking),
- [material.to(device, non_blocking=non_blocking) for material in self.materials],
- )
-
-
-class MeshWithVoxel(Mesh, Voxel):
- def __init__(self,
- vertices: torch.Tensor,
- faces: torch.Tensor,
- origin: list,
- voxel_size: float,
- coords: torch.Tensor,
- attrs: torch.Tensor,
- voxel_shape: torch.Size,
- layout: Dict = {},
- ):
- self.vertices = vertices.float()
- self.faces = faces.int()
- self.origin = torch.tensor(origin, dtype=torch.float32, device=self.device)
- self.voxel_size = voxel_size
- self.coords = coords
- self.attrs = attrs
- self.voxel_shape = voxel_shape
- self.layout = layout
-
- def to(self, device, non_blocking=False):
- return MeshWithVoxel(
- self.vertices.to(device, non_blocking=non_blocking),
- self.faces.to(device, non_blocking=non_blocking),
- self.origin.tolist(),
- self.voxel_size,
- self.coords.to(device, non_blocking=non_blocking),
- self.attrs.to(device, non_blocking=non_blocking),
- self.voxel_shape,
- self.layout,
- )
-
- def query_attrs(self, xyz):
- from flex_gemm.ops.grid_sample import grid_sample_3d
- grid = ((xyz - self.origin) / self.voxel_size).reshape(1, -1, 3)
- vertex_attrs = grid_sample_3d(
- self.attrs,
- torch.cat([torch.zeros_like(self.coords[..., :1]), self.coords], dim=-1),
- self.voxel_shape,
- grid,
- mode='trilinear'
- )[0]
- return vertex_attrs
-
- def query_vertex_attrs(self):
- return self.query_attrs(self.vertices)
+from typing import *
+import torch
+from ..voxel import Voxel
+import cumesh
+# NOTE: flex_gemm is imported lazily in query_attrs (matching the renderers' pattern), NOT here.
+# This module sits in the DataLoader workers' import closure (dataset -> MeshWithVoxel), and
+# flex_gemm's triton @autotune decorators query the GPU driver AT IMPORT — in a spawned CPU-only
+# worker this can raise "RuntimeError: 0 active drivers", killing the rank and
+# leaving other ranks blocked in NCCL collectives.
+
+
+class Mesh:
+ def __init__(self,
+ vertices,
+ faces,
+ vertex_attrs=None
+ ):
+ self.vertices = vertices.float()
+ self.faces = faces.int()
+ self.vertex_attrs = vertex_attrs
+
+ @property
+ def device(self):
+ return self.vertices.device
+
+ def to(self, device, non_blocking=False):
+ return Mesh(
+ self.vertices.to(device, non_blocking=non_blocking),
+ self.faces.to(device, non_blocking=non_blocking),
+ self.vertex_attrs.to(device, non_blocking=non_blocking) if self.vertex_attrs is not None else None,
+ )
+
+ def cuda(self, non_blocking=False):
+ return self.to('cuda', non_blocking=non_blocking)
+
+ def cpu(self):
+ return self.to('cpu')
+
+ def fill_holes(self, max_hole_perimeter=3e-2):
+ vertices = self.vertices.clone().cuda().contiguous()
+ faces = self.faces.clone().cuda().contiguous()
+
+ mesh = cumesh.CuMesh()
+ mesh.init(vertices, faces)
+ mesh.get_edges()
+ mesh.get_boundary_info()
+ if mesh.num_boundaries == 0:
+ return
+ mesh.get_vertex_edge_adjacency()
+ mesh.get_vertex_boundary_adjacency()
+ mesh.get_manifold_boundary_adjacency()
+ mesh.read_manifold_boundary_adjacency()
+ mesh.get_boundary_connected_components()
+ mesh.get_boundary_loops()
+ if mesh.num_boundary_loops == 0:
+ return
+ mesh.fill_holes(max_hole_perimeter=max_hole_perimeter)
+ new_vertices, new_faces = mesh.read()
+
+ self.vertices = new_vertices.to(self.device)
+ self.faces = new_faces.to(self.device)
+
+ def remove_faces(self, face_mask: torch.Tensor):
+ vertices = self.vertices.clone().cuda().contiguous()
+ faces = self.faces.clone().cuda().contiguous()
+
+ mesh = cumesh.CuMesh()
+ mesh.init(vertices, faces)
+ mesh.remove_faces(face_mask)
+ new_vertices, new_faces = mesh.read()
+
+ self.vertices = new_vertices.to(self.device)
+ self.faces = new_faces.to(self.device)
+
+ def simplify(self, target=1000000, verbose: bool=False, options: dict={}):
+ vertices = self.vertices.clone().cuda().contiguous()
+ faces = self.faces.clone().cuda().contiguous()
+
+ mesh = cumesh.CuMesh()
+ mesh.init(vertices, faces)
+ mesh.simplify(target, verbose=verbose, options=options)
+ new_vertices, new_faces = mesh.read()
+
+ self.vertices = new_vertices.to(self.device)
+ self.faces = new_faces.to(self.device)
+
+
+class TextureFilterMode:
+ CLOSEST = 0
+ LINEAR = 1
+
+
+class TextureWrapMode:
+ CLAMP_TO_EDGE = 0
+ REPEAT = 1
+ MIRRORED_REPEAT = 2
+
+
+class AlphaMode:
+ OPAQUE = 0
+ MASK = 1
+ BLEND = 2
+
+
+class Texture:
+ def __init__(
+ self,
+ image: torch.Tensor,
+ filter_mode: TextureFilterMode = TextureFilterMode.LINEAR,
+ wrap_mode: TextureWrapMode = TextureWrapMode.REPEAT
+ ):
+ self.image = image
+ self.filter_mode = filter_mode
+ self.wrap_mode = wrap_mode
+
+ def to(self, device, non_blocking=False):
+ return Texture(
+ self.image.to(device, non_blocking=non_blocking),
+ self.filter_mode,
+ self.wrap_mode,
+ )
+
+
+class PbrMaterial:
+ def __init__(
+ self,
+ base_color_texture: Optional[Texture] = None,
+ base_color_factor: Union[torch.Tensor, List[float]] = [1.0, 1.0, 1.0],
+ metallic_texture: Optional[Texture] = None,
+ metallic_factor: float = 1.0,
+ roughness_texture: Optional[Texture] = None,
+ roughness_factor: float = 1.0,
+ alpha_texture: Optional[Texture] = None,
+ alpha_factor: float = 1.0,
+ alpha_mode: AlphaMode = AlphaMode.OPAQUE,
+ alpha_cutoff: float = 0.5,
+ ):
+ self.base_color_texture = base_color_texture
+ self.base_color_factor = torch.tensor(base_color_factor, dtype=torch.float32)[:3]
+ self.metallic_texture = metallic_texture
+ self.metallic_factor = metallic_factor
+ self.roughness_texture = roughness_texture
+ self.roughness_factor = roughness_factor
+ self.alpha_texture = alpha_texture
+ self.alpha_factor = alpha_factor
+ self.alpha_mode = alpha_mode
+ self.alpha_cutoff = alpha_cutoff
+
+ def to(self, device, non_blocking=False):
+ return PbrMaterial(
+ base_color_texture=self.base_color_texture.to(device, non_blocking=non_blocking) if self.base_color_texture is not None else None,
+ base_color_factor=self.base_color_factor.to(device, non_blocking=non_blocking),
+ metallic_texture=self.metallic_texture.to(device, non_blocking=non_blocking) if self.metallic_texture is not None else None,
+ metallic_factor=self.metallic_factor,
+ roughness_texture=self.roughness_texture.to(device, non_blocking=non_blocking) if self.roughness_texture is not None else None,
+ roughness_factor=self.roughness_factor,
+ alpha_texture=self.alpha_texture.to(device, non_blocking=non_blocking) if self.alpha_texture is not None else None,
+ alpha_factor=self.alpha_factor,
+ alpha_mode=self.alpha_mode,
+ alpha_cutoff=self.alpha_cutoff,
+ )
+
+
+class MeshWithPbrMaterial(Mesh):
+ def __init__(self,
+ vertices,
+ faces,
+ material_ids,
+ uv_coords,
+ materials: List[PbrMaterial],
+ ):
+ self.vertices = vertices.float()
+ self.faces = faces.int()
+ self.material_ids = material_ids # [M]
+ self.uv_coords = uv_coords # [M, 3, 2]
+ self.materials = materials
+ self.layout = {
+ 'base_color': slice(0, 3),
+ 'metallic': slice(3, 4),
+ 'roughness': slice(4, 5),
+ 'alpha': slice(5, 6),
+ }
+
+ def to(self, device, non_blocking=False):
+ return MeshWithPbrMaterial(
+ self.vertices.to(device, non_blocking=non_blocking),
+ self.faces.to(device, non_blocking=non_blocking),
+ self.material_ids.to(device, non_blocking=non_blocking),
+ self.uv_coords.to(device, non_blocking=non_blocking),
+ [material.to(device, non_blocking=non_blocking) for material in self.materials],
+ )
+
+
+class MeshWithVoxel(Mesh, Voxel):
+ def __init__(self,
+ vertices: torch.Tensor,
+ faces: torch.Tensor,
+ origin: list,
+ voxel_size: float,
+ coords: torch.Tensor,
+ attrs: torch.Tensor,
+ voxel_shape: torch.Size,
+ layout: Dict = {},
+ ):
+ self.vertices = vertices.float()
+ self.faces = faces.int()
+ self.origin = torch.tensor(origin, dtype=torch.float32, device=self.device)
+ self.voxel_size = voxel_size
+ self.coords = coords
+ self.attrs = attrs
+ self.voxel_shape = voxel_shape
+ self.layout = layout
+
+ def to(self, device, non_blocking=False):
+ return MeshWithVoxel(
+ self.vertices.to(device, non_blocking=non_blocking),
+ self.faces.to(device, non_blocking=non_blocking),
+ self.origin.tolist(),
+ self.voxel_size,
+ self.coords.to(device, non_blocking=non_blocking),
+ self.attrs.to(device, non_blocking=non_blocking),
+ self.voxel_shape,
+ self.layout,
+ )
+
+ def query_attrs(self, xyz):
+ from flex_gemm.ops.grid_sample import grid_sample_3d
+ grid = ((xyz - self.origin) / self.voxel_size).reshape(1, -1, 3)
+ vertex_attrs = grid_sample_3d(
+ self.attrs,
+ torch.cat([torch.zeros_like(self.coords[..., :1]), self.coords], dim=-1),
+ self.voxel_shape,
+ grid,
+ mode='trilinear'
+ )[0]
+ return vertex_attrs
+
+ def query_vertex_attrs(self):
+ return self.query_attrs(self.vertices)
diff --git a/deployment/runtime/qwen/diffsynth/core/__init__.py b/deployment/runtime/qwen/diffsynth/core/__init__.py
index 8b1747471525a8c0d6c17131afd05778faa9bc2a..9b7b3b62be0972179f27c41dd9adebb30d4543e6 100644
--- a/deployment/runtime/qwen/diffsynth/core/__init__.py
+++ b/deployment/runtime/qwen/diffsynth/core/__init__.py
@@ -1,8 +1,6 @@
-from .attention import *
-from .data import *
-from .gradient import *
-from .quant import *
-from .loader import *
-from .vram import *
-from .device import *
-from .offload_training import *
+from .attention import *
+from .gradient import *
+from .quant import *
+from .loader import *
+from .vram import *
+from .device import *
diff --git a/deployment/runtime/qwen/diffsynth/core/data/__init__.py b/deployment/runtime/qwen/diffsynth/core/data/__init__.py
index d494a277d81eeb2a9575155eb983d8bc3879590a..099e29fd09df53079662eefc4f8e89ae013d0109 100644
--- a/deployment/runtime/qwen/diffsynth/core/data/__init__.py
+++ b/deployment/runtime/qwen/diffsynth/core/data/__init__.py
@@ -1 +1 @@
-from .unified_dataset import UnifiedDataset
+"""Image and media preprocessing operators used by inference pipelines."""
diff --git a/deployment/runtime/qwen/diffsynth/diffusion/__init__.py b/deployment/runtime/qwen/diffsynth/diffusion/__init__.py
index f5d9ea1946dcee7a040ef1a21eaa8d1d4f160871..ea8c7d010c8a9c9b0c9198797f2669030693a00e 100644
--- a/deployment/runtime/qwen/diffsynth/diffusion/__init__.py
+++ b/deployment/runtime/qwen/diffsynth/diffusion/__init__.py
@@ -1,7 +1 @@
-from .flow_match import FlowMatchScheduler, HiDreamO1FlashScheduler, AncestralFlowMatchScheduler
-from .training_module import DiffusionTrainingModule
-from .logger import ModelLogger
-from .runner import launch_training_task, launch_data_process_task
-from .parsers import *
-from .loss import *
-from .dmd2 import *
+from .flow_match import FlowMatchScheduler, HiDreamO1FlashScheduler, AncestralFlowMatchScheduler
diff --git a/deployment/runtime/qwen/diffsynth/diffusion/base_pipeline.py b/deployment/runtime/qwen/diffsynth/diffusion/base_pipeline.py
index 5af48a11085e90ab14254f43a094a5c52012361f..e23b696c0782e7a0a4189ed221708d33e5429ff1 100644
--- a/deployment/runtime/qwen/diffsynth/diffusion/base_pipeline.py
+++ b/deployment/runtime/qwen/diffsynth/diffusion/base_pipeline.py
@@ -1,542 +1,526 @@
-from PIL import Image
-import torch
-import numpy as np
-from einops import repeat, reduce
-from typing import Union
-from ..core import AutoTorchModule, AutoWrappedLinear, LoRAHotLoadMixin, load_state_dict, ModelConfig, parse_device_type, enable_vram_management
-from ..core.device.npu_compatible_device import get_device_type
-from ..utils.lora import GeneralLoRALoader
-from ..models.model_loader import ModelPool
-from ..utils.controlnet import ControlNetInput
-from ..core.device import get_device_name, IS_NPU_AVAILABLE
-from .template import load_template_model, load_template_data_processor
-
-
-class PipelineUnit:
- def __init__(
- self,
- seperate_cfg: bool = False,
- take_over: bool = False,
- input_params: tuple[str] = None,
- output_params: tuple[str] = None,
- input_params_posi: dict[str, str] = None,
- input_params_nega: dict[str, str] = None,
- onload_model_names: tuple[str] = None
- ):
- self.seperate_cfg = seperate_cfg
- self.take_over = take_over
- self.input_params = input_params
- self.output_params = output_params
- self.input_params_posi = input_params_posi
- self.input_params_nega = input_params_nega
- self.onload_model_names = onload_model_names
-
- def fetch_input_params(self):
- params = []
- if self.input_params is not None:
- for param in self.input_params:
- params.append(param)
- if self.input_params_posi is not None:
- for _, param in self.input_params_posi.items():
- params.append(param)
- if self.input_params_nega is not None:
- for _, param in self.input_params_nega.items():
- params.append(param)
- params = sorted(list(set(params)))
- return params
-
- def fetch_output_params(self):
- params = []
- if self.output_params is not None:
- for param in self.output_params:
- params.append(param)
- return params
-
- def process(self, pipe, **kwargs) -> dict:
- return {}
-
- def post_process(self, pipe, **kwargs) -> dict:
- return {}
-
-
-class BasePipeline(torch.nn.Module):
-
- def __init__(
- self,
- device=get_device_type(), torch_dtype=torch.float16,
- height_division_factor=64, width_division_factor=64,
- time_division_factor=None, time_division_remainder=None,
- ):
- super().__init__()
- # The device and torch_dtype is used for the storage of intermediate variables, not models.
- self.device = device
- self.torch_dtype = torch_dtype
- self.device_type = parse_device_type(device)
- # The following parameters are used for shape check.
- self.height_division_factor = height_division_factor
- self.width_division_factor = width_division_factor
- self.time_division_factor = time_division_factor
- self.time_division_remainder = time_division_remainder
- # VRAM management
- self.vram_management_enabled = False
- # Pipeline Unit Runner
- self.unit_runner = PipelineUnitRunner()
- # LoRA Loader
- self.lora_loader = GeneralLoRALoader
-
-
- def to(self, *args, **kwargs):
- device, dtype, non_blocking, convert_to_format = torch._C._nn._parse_to(*args, **kwargs)
- if device is not None:
- self.device = device
- if dtype is not None:
- self.torch_dtype = dtype
- super().to(*args, **kwargs)
- return self
-
-
- def check_resize_height_width(self, height, width, num_frames=None, verbose=1):
- # Shape check
- if height % self.height_division_factor != 0:
- height = (height + self.height_division_factor - 1) // self.height_division_factor * self.height_division_factor
- if verbose > 0:
- print(f"height % {self.height_division_factor} != 0. We round it up to {height}.")
- if width % self.width_division_factor != 0:
- width = (width + self.width_division_factor - 1) // self.width_division_factor * self.width_division_factor
- if verbose > 0:
- print(f"width % {self.width_division_factor} != 0. We round it up to {width}.")
- if num_frames is None:
- return height, width
- else:
- if num_frames % self.time_division_factor != self.time_division_remainder:
- num_frames = (num_frames - self.time_division_remainder + self.time_division_factor - 1) // self.time_division_factor * self.time_division_factor + self.time_division_remainder
- if verbose > 0:
- print(f"num_frames % {self.time_division_factor} != {self.time_division_remainder}. We round it up to {num_frames}.")
- return height, width, num_frames
-
-
- def preprocess_image(self, image, torch_dtype=None, device=None, pattern="B C H W", min_value=-1, max_value=1):
- # Transform a PIL.Image to torch.Tensor
- image = torch.Tensor(np.array(image, dtype=np.float32))
- image = image.to(dtype=torch_dtype or self.torch_dtype, device=device or self.device)
- image = image * ((max_value - min_value) / 255) + min_value
- image = repeat(image, f"H W C -> {pattern}", **({"B": 1} if "B" in pattern else {}))
- return image
-
-
- def preprocess_video(self, video, torch_dtype=None, device=None, pattern="B C T H W", min_value=-1, max_value=1):
- # Transform a list of PIL.Image to torch.Tensor
- video = [self.preprocess_image(image, torch_dtype=torch_dtype, device=device, min_value=min_value, max_value=max_value) for image in video]
- video = torch.stack(video, dim=pattern.index("T") // 2)
- return video
-
-
- def vae_output_to_image(self, vae_output, pattern="B C H W", min_value=-1, max_value=1):
- # Transform a torch.Tensor to PIL.Image
- if pattern != "H W C":
- vae_output = reduce(vae_output, f"{pattern} -> H W C", reduction="mean")
- image = ((vae_output - min_value) * (255 / (max_value - min_value))).clip(0, 255)
- image = image.to(device="cpu", dtype=torch.uint8)
- image = Image.fromarray(image.numpy())
- return image
-
-
- def vae_output_to_video(self, vae_output, pattern="B C T H W", min_value=-1, max_value=1):
- # Transform a torch.Tensor to list of PIL.Image
- if pattern != "T H W C":
- vae_output = reduce(vae_output, f"{pattern} -> T H W C", reduction="mean")
- video = [self.vae_output_to_image(image, pattern="H W C", min_value=min_value, max_value=max_value) for image in vae_output]
- return video
-
- def output_audio_format_check(self, audio_output):
- # output standard foramt: [C, T], output dtype: float()
- # remove batch dim
- if audio_output.ndim == 3:
- audio_output = audio_output.squeeze(0)
- return audio_output.float().cpu()
-
- def load_models_to_device(self, model_names):
- if self.vram_management_enabled:
- # offload models
- for name, model in self.named_children():
- if name not in model_names:
- if hasattr(model, "vram_management_enabled") and model.vram_management_enabled:
- if hasattr(model, "offload"):
- model.offload()
- else:
- for module in model.modules():
- if hasattr(module, "offload"):
- module.offload()
- getattr(torch, self.device_type).empty_cache()
- # onload models
- for name, model in self.named_children():
- if name in model_names:
- if hasattr(model, "vram_management_enabled") and model.vram_management_enabled:
- if hasattr(model, "onload"):
- model.onload()
- else:
- for module in model.modules():
- if hasattr(module, "onload"):
- module.onload()
-
-
- def generate_noise(self, shape, seed=None, rand_device="cpu", rand_torch_dtype=torch.float32, device=None, torch_dtype=None):
- # Initialize Gaussian noise
- generator = None if seed is None else torch.Generator(rand_device).manual_seed(seed)
- noise = torch.randn(shape, generator=generator, device=rand_device, dtype=rand_torch_dtype)
- noise = noise.to(dtype=torch_dtype or self.torch_dtype, device=device or self.device)
- return noise
-
-
- def get_vram(self):
- device = self.device if not IS_NPU_AVAILABLE else get_device_name()
- return getattr(torch, self.device_type).mem_get_info(device)[1] / (1024 ** 3)
-
- def get_module(self, model, name):
- if "." in name:
- name, suffix = name[:name.index(".")], name[name.index(".") + 1:]
- if name.isdigit():
- return self.get_module(model[int(name)], suffix)
- else:
- return self.get_module(getattr(model, name), suffix)
- else:
- return getattr(model, name)
-
- def freeze_except(self, model_names):
- self.eval()
- self.requires_grad_(False)
- for name in model_names:
- module = self.get_module(self, name)
- if module is None:
- print(f"No {name} models in the pipeline. We cannot enable training on the model. If this occurs during the data processing stage, it is normal.")
- continue
- module.train()
- module.requires_grad_(True)
-
-
- def blend_with_mask(self, base, addition, mask):
- return base * (1 - mask) + addition * mask
-
-
- def step(self, scheduler, latents, progress_id, noise_pred, input_latents=None, inpaint_mask=None, **kwargs):
- timestep = scheduler.timesteps[progress_id]
- if inpaint_mask is not None:
- noise_pred_expected = scheduler.return_to_timestep(scheduler.timesteps[progress_id], latents, input_latents)
- noise_pred = self.blend_with_mask(noise_pred_expected, noise_pred, inpaint_mask)
- latents_next = scheduler.step(noise_pred, timestep, latents)
- return latents_next
-
-
- def split_pipeline_units(self, model_names: list[str]):
- return PipelineUnitGraph().split_pipeline_units(self.units, model_names)
-
-
- def flush_vram_management_device(self, device):
- for module in self.modules():
- if isinstance(module, AutoTorchModule):
- module.offload_device = device
- module.onload_device = device
- module.preparing_device = device
- module.computation_device = device
-
-
- def check_quant_hot_load(self, module: torch.nn.Module):
- # A quantized weight cannot absorb a fused LoRA, so hot-loading is the only option for it.
- if getattr(module, "quantize_config", None) is None:
- return module
- return self.enable_lora_hot_loading(module)
-
-
- def load_lora(
- self,
- module: torch.nn.Module,
- lora_config: Union[ModelConfig, str] = None,
- alpha=1,
- hotload=None,
- state_dict=None,
- verbose=1,
- ):
- module = self.check_quant_hot_load(module)
- if state_dict is None:
- if isinstance(lora_config, str):
- lora = load_state_dict(lora_config, torch_dtype=self.torch_dtype, device=self.device)
- else:
- lora_config.download_if_necessary()
- lora = load_state_dict(lora_config.path, torch_dtype=self.torch_dtype, device=self.device)
- else:
- lora = state_dict
- lora_loader = self.lora_loader(torch_dtype=self.torch_dtype, device=self.device)
- lora = lora_loader.convert_state_dict(lora)
- if hotload is None:
- hotload = hasattr(module, "vram_management_enabled") and getattr(module, "vram_management_enabled")
- if hotload:
- if not (hasattr(module, "vram_management_enabled") and getattr(module, "vram_management_enabled")):
- raise ValueError("VRAM Management is not enabled. LoRA hotloading is not supported.")
- updated_num = 0
- for _, module in module.named_modules():
- if isinstance(module, LoRAHotLoadMixin):
- name = module.name
- lora_a_name = f'{name}.lora_A.weight'
- lora_b_name = f'{name}.lora_B.weight'
- if lora_a_name in lora and lora_b_name in lora:
- updated_num += 1
- module.lora_A_weights.append(lora[lora_a_name] * alpha)
- module.lora_B_weights.append(lora[lora_b_name])
- if verbose >= 1:
- print(f"{updated_num} tensors are patched by LoRA. You can use `pipe.clear_lora()` to clear all LoRA layers.")
- else:
- lora_loader.fuse_lora_to_base_model(module, lora, alpha=alpha)
-
-
- def clear_lora(self, verbose=1):
- cleared_num = 0
- for name, module in self.named_modules():
- if isinstance(module, LoRAHotLoadMixin):
- if hasattr(module, "lora_A_weights"):
- if len(module.lora_A_weights) > 0:
- cleared_num += 1
- module.lora_A_weights.clear()
- if hasattr(module, "lora_B_weights"):
- module.lora_B_weights.clear()
- if verbose >= 1:
- print(f"{cleared_num} LoRA layers are cleared.")
-
-
- def download_and_load_models(self, model_configs: list[ModelConfig] = [], vram_limit: float = None):
- model_pool = ModelPool()
- for model_config in model_configs:
- model_config.download_if_necessary()
- vram_config = model_config.vram_config()
- vram_config["computation_dtype"] = vram_config["computation_dtype"] or self.torch_dtype
- vram_config["computation_device"] = vram_config["computation_device"] or self.device
- model_pool.auto_load_model(
- model_config.path,
- vram_config=vram_config,
- vram_limit=vram_limit,
- clear_parameters=model_config.clear_parameters,
- state_dict=model_config.state_dict,
- quantize=model_config.quantize,
- )
- return model_pool
-
-
- def check_vram_management_state(self):
- vram_management_enabled = False
- for module in self.children():
- if hasattr(module, "vram_management_enabled") and getattr(module, "vram_management_enabled"):
- vram_management_enabled = True
- return vram_management_enabled
-
-
- def cfg_guided_model_fn(self, model_fn, cfg_scale, inputs_shared, inputs_posi, inputs_nega, **inputs_others):
- # Positive side forward
- if inputs_shared.get("positive_only_lora", None) is not None:
- self.load_lora(self.dit, state_dict=inputs_shared["positive_only_lora"], verbose=0)
- noise_pred_posi = model_fn(**inputs_posi, **inputs_shared, **inputs_others)
- if inputs_shared.get("positive_only_lora", None) is not None:
- self.clear_lora(verbose=0)
-
- if cfg_scale != 1.0:
- # Negative side forward
- if inputs_shared.get("negative_only_lora", None) is not None:
- self.load_lora(self.dit, state_dict=inputs_shared["negative_only_lora"], verbose=0)
- noise_pred_nega = model_fn(**inputs_nega, **inputs_shared, **inputs_others)
- if inputs_shared.get("negative_only_lora", None) is not None:
- self.clear_lora(verbose=0)
-
- if isinstance(noise_pred_posi, tuple):
- # Separately handling different output types of latents, eg. video and audio latents.
- noise_pred = tuple(
- n_nega + cfg_scale * (n_posi - n_nega)
- for n_posi, n_nega in zip(noise_pred_posi, noise_pred_nega)
- )
- else:
- noise_pred = noise_pred_nega + cfg_scale * (noise_pred_posi - noise_pred_nega)
- else:
- noise_pred = noise_pred_posi
- return noise_pred
-
-
- def load_training_template_model(self, model_config: ModelConfig = None):
- if model_config is not None:
- model_config.download_if_necessary()
- self.template_model = load_template_model(model_config.path, torch_dtype=self.torch_dtype, device=self.device)
- self.template_data_processor = load_template_data_processor(model_config.path)()
-
-
- def enable_lora_hot_loading(self, model: torch.nn.Module):
- if hasattr(model, "vram_management_enabled") and getattr(model, "vram_management_enabled"):
- return model
- module_map = {torch.nn.Linear: AutoWrappedLinear}
- vram_config = {
- "offload_dtype": self.torch_dtype,
- "offload_device": self.device,
- "onload_dtype": self.torch_dtype,
- "onload_device": self.device,
- "preparing_dtype": self.torch_dtype,
- "preparing_device": self.device,
- "computation_dtype": self.torch_dtype,
- "computation_device": self.device,
- }
- model = enable_vram_management(model, module_map, vram_config=vram_config, quantize=getattr(model, "quantize_config", None))
- return model
-
- def compile_pipeline(self, mode: str = "default", dynamic: bool = True, fullgraph: bool = False, compile_models: list = None, **kwargs):
- """
- compile the pipeline with torch.compile. The models that will be compiled are determined by the `compilable_models` attribute of the pipeline.
- If a model has `_repeated_blocks` attribute, we will compile these blocks with regional compilation. Otherwise, we will compile the whole model.
- See https://docs.pytorch.org/docs/stable/generated/torch.compile.html#torch.compile for details about compilation arguments.
- Args:
- mode: The compilation mode, which will be passed to `torch.compile`, options are "default", "reduce-overhead", "max-autotune" and "max-autotune-no-cudagraphs. Default to "default".
- dynamic: Whether to enable dynamic graph compilation to support dynamic input shapes, which will be passed to `torch.compile`. Default to True (recommended).
- fullgraph: Whether to use full graph compilation, which will be passed to `torch.compile`. Default to False (recommended).
- compile_models: The list of model names to be compiled. If None, we will compile the models in `pipeline.compilable_models`. Default to None.
- **kwargs: Other arguments for `torch.compile`.
- """
- compile_models = compile_models or getattr(self, "compilable_models", [])
- if len(compile_models) == 0:
- print("No compilable models in the pipeline. Skip compilation.")
- return
- for name in compile_models:
- model = getattr(self, name, None)
- if model is None:
- print(f"Model '{name}' not found in the pipeline.")
- continue
- repeated_blocks = getattr(model, "_repeated_blocks", None)
- # regional compilation for repeated blocks.
- if repeated_blocks is not None:
- for submod in model.modules():
- if submod.__class__.__name__ in repeated_blocks:
- submod.compile(mode=mode, dynamic=dynamic, fullgraph=fullgraph, **kwargs)
- # compile the whole model.
- else:
- model.compile(mode=mode, dynamic=dynamic, fullgraph=fullgraph, **kwargs)
- print(f"{name} is compiled with mode={mode}, dynamic={dynamic}, fullgraph={fullgraph}.")
-
-
-class PipelineUnitGraph:
- def __init__(self):
- pass
-
- def build_edges(self, units: list[PipelineUnit]):
- # Establish dependencies between units
- # to search for subsequent related computation units.
- last_compute_unit_id = {}
- edges = []
- for unit_id, unit in enumerate(units):
- for input_param in unit.fetch_input_params():
- if input_param in last_compute_unit_id:
- edges.append((last_compute_unit_id[input_param], unit_id))
- for output_param in unit.fetch_output_params():
- last_compute_unit_id[output_param] = unit_id
- return edges
-
- def build_chains(self, units: list[PipelineUnit]):
- # Establish updating chains for each variable
- # to track their computation process.
- params = sum([unit.fetch_input_params() + unit.fetch_output_params() for unit in units], [])
- params = sorted(list(set(params)))
- chains = {param: [] for param in params}
- for unit_id, unit in enumerate(units):
- for param in unit.fetch_output_params():
- chains[param].append(unit_id)
- return chains
-
- def search_direct_unit_ids(self, units: list[PipelineUnit], model_names: list[str]):
- # Search for units that directly participate in the model's computation.
- related_unit_ids = []
- for unit_id, unit in enumerate(units):
- for model_name in model_names:
- if unit.onload_model_names is not None and model_name in unit.onload_model_names:
- related_unit_ids.append(unit_id)
- break
- return related_unit_ids
-
- def search_related_unit_ids(self, edges, start_unit_ids, direction="target"):
- # Search for subsequent related computation units.
- related_unit_ids = [unit_id for unit_id in start_unit_ids]
- while True:
- neighbors = []
- for source, target in edges:
- if direction == "target" and source in related_unit_ids and target not in related_unit_ids:
- neighbors.append(target)
- elif direction == "source" and source not in related_unit_ids and target in related_unit_ids:
- neighbors.append(source)
- neighbors = sorted(list(set(neighbors)))
- if len(neighbors) == 0:
- break
- else:
- related_unit_ids.extend(neighbors)
- related_unit_ids = sorted(list(set(related_unit_ids)))
- return related_unit_ids
-
- def search_updating_unit_ids(self, units: list[PipelineUnit], chains, related_unit_ids):
- # If the input parameters of this subgraph are updated outside the subgraph,
- # search for the units where these updates occur.
- first_compute_unit_id = {}
- for unit_id in related_unit_ids:
- for param in units[unit_id].fetch_input_params():
- if param not in first_compute_unit_id:
- first_compute_unit_id[param] = unit_id
- updating_unit_ids = []
- for param in first_compute_unit_id:
- unit_id = first_compute_unit_id[param]
- chain = chains[param]
- if unit_id in chain and chain.index(unit_id) != len(chain) - 1:
- for unit_id_ in chain[chain.index(unit_id) + 1:]:
- if unit_id_ not in related_unit_ids:
- updating_unit_ids.append(unit_id_)
- related_unit_ids.extend(updating_unit_ids)
- related_unit_ids = sorted(list(set(related_unit_ids)))
- return related_unit_ids
-
- def split_pipeline_units(self, units: list[PipelineUnit], model_names: list[str]):
- # Split the computation graph,
- # separating all model-related computations.
- related_unit_ids = self.search_direct_unit_ids(units, model_names)
- edges = self.build_edges(units)
- chains = self.build_chains(units)
- while True:
- num_related_unit_ids = len(related_unit_ids)
- related_unit_ids = self.search_related_unit_ids(edges, related_unit_ids, "target")
- related_unit_ids = self.search_updating_unit_ids(units, chains, related_unit_ids)
- if len(related_unit_ids) == num_related_unit_ids:
- break
- else:
- num_related_unit_ids = len(related_unit_ids)
- related_units = [units[i] for i in related_unit_ids]
- unrelated_units = [units[i] for i in range(len(units)) if i not in related_unit_ids]
- return related_units, unrelated_units
-
-
-class PipelineUnitRunner:
- def __init__(self):
- pass
-
- def __call__(self, unit: PipelineUnit, pipe: BasePipeline, inputs_shared: dict, inputs_posi: dict, inputs_nega: dict) -> tuple[dict, dict]:
- if unit.take_over:
- # Let the pipeline unit take over this function.
- inputs_shared, inputs_posi, inputs_nega = unit.process(pipe, inputs_shared=inputs_shared, inputs_posi=inputs_posi, inputs_nega=inputs_nega)
- elif unit.seperate_cfg:
- # Positive side
- processor_inputs = {name: inputs_posi.get(name_) for name, name_ in unit.input_params_posi.items()}
- if unit.input_params is not None:
- for name in unit.input_params:
- processor_inputs[name] = inputs_shared.get(name)
- processor_outputs = unit.process(pipe, **processor_inputs)
- inputs_posi.update(processor_outputs)
- # Negative side
- if inputs_shared["cfg_scale"] != 1:
- processor_inputs = {name: inputs_nega.get(name_) for name, name_ in unit.input_params_nega.items()}
- if unit.input_params is not None:
- for name in unit.input_params:
- processor_inputs[name] = inputs_shared.get(name)
- processor_outputs = unit.process(pipe, **processor_inputs)
- inputs_nega.update(processor_outputs)
- else:
- inputs_nega.update(processor_outputs)
- else:
- processor_inputs = {name: inputs_shared.get(name) for name in unit.input_params}
- processor_outputs = unit.process(pipe, **processor_inputs)
- inputs_shared.update(processor_outputs)
- return inputs_shared, inputs_posi, inputs_nega
+from PIL import Image
+import torch
+import numpy as np
+from einops import repeat, reduce
+from typing import Union
+from ..core import AutoTorchModule, AutoWrappedLinear, LoRAHotLoadMixin, load_state_dict, ModelConfig, parse_device_type, enable_vram_management
+from ..core.device.npu_compatible_device import get_device_type
+from ..utils.lora import GeneralLoRALoader
+from ..models.model_loader import ModelPool
+from ..utils.controlnet import ControlNetInput
+from ..core.device import get_device_name, IS_NPU_AVAILABLE
+
+
+class PipelineUnit:
+ def __init__(
+ self,
+ seperate_cfg: bool = False,
+ take_over: bool = False,
+ input_params: tuple[str] = None,
+ output_params: tuple[str] = None,
+ input_params_posi: dict[str, str] = None,
+ input_params_nega: dict[str, str] = None,
+ onload_model_names: tuple[str] = None
+ ):
+ self.seperate_cfg = seperate_cfg
+ self.take_over = take_over
+ self.input_params = input_params
+ self.output_params = output_params
+ self.input_params_posi = input_params_posi
+ self.input_params_nega = input_params_nega
+ self.onload_model_names = onload_model_names
+
+ def fetch_input_params(self):
+ params = []
+ if self.input_params is not None:
+ for param in self.input_params:
+ params.append(param)
+ if self.input_params_posi is not None:
+ for _, param in self.input_params_posi.items():
+ params.append(param)
+ if self.input_params_nega is not None:
+ for _, param in self.input_params_nega.items():
+ params.append(param)
+ params = sorted(list(set(params)))
+ return params
+
+ def fetch_output_params(self):
+ params = []
+ if self.output_params is not None:
+ for param in self.output_params:
+ params.append(param)
+ return params
+
+ def process(self, pipe, **kwargs) -> dict:
+ return {}
+
+ def post_process(self, pipe, **kwargs) -> dict:
+ return {}
+
+
+class BasePipeline(torch.nn.Module):
+
+ def __init__(
+ self,
+ device=get_device_type(), torch_dtype=torch.float16,
+ height_division_factor=64, width_division_factor=64,
+ time_division_factor=None, time_division_remainder=None,
+ ):
+ super().__init__()
+ # The device and torch_dtype is used for the storage of intermediate variables, not models.
+ self.device = device
+ self.torch_dtype = torch_dtype
+ self.device_type = parse_device_type(device)
+ # The following parameters are used for shape check.
+ self.height_division_factor = height_division_factor
+ self.width_division_factor = width_division_factor
+ self.time_division_factor = time_division_factor
+ self.time_division_remainder = time_division_remainder
+ # VRAM management
+ self.vram_management_enabled = False
+ # Pipeline Unit Runner
+ self.unit_runner = PipelineUnitRunner()
+ # LoRA Loader
+ self.lora_loader = GeneralLoRALoader
+
+
+ def to(self, *args, **kwargs):
+ device, dtype, non_blocking, convert_to_format = torch._C._nn._parse_to(*args, **kwargs)
+ if device is not None:
+ self.device = device
+ if dtype is not None:
+ self.torch_dtype = dtype
+ super().to(*args, **kwargs)
+ return self
+
+
+ def check_resize_height_width(self, height, width, num_frames=None, verbose=1):
+ # Shape check
+ if height % self.height_division_factor != 0:
+ height = (height + self.height_division_factor - 1) // self.height_division_factor * self.height_division_factor
+ if verbose > 0:
+ print(f"height % {self.height_division_factor} != 0. We round it up to {height}.")
+ if width % self.width_division_factor != 0:
+ width = (width + self.width_division_factor - 1) // self.width_division_factor * self.width_division_factor
+ if verbose > 0:
+ print(f"width % {self.width_division_factor} != 0. We round it up to {width}.")
+ if num_frames is None:
+ return height, width
+ else:
+ if num_frames % self.time_division_factor != self.time_division_remainder:
+ num_frames = (num_frames - self.time_division_remainder + self.time_division_factor - 1) // self.time_division_factor * self.time_division_factor + self.time_division_remainder
+ if verbose > 0:
+ print(f"num_frames % {self.time_division_factor} != {self.time_division_remainder}. We round it up to {num_frames}.")
+ return height, width, num_frames
+
+
+ def preprocess_image(self, image, torch_dtype=None, device=None, pattern="B C H W", min_value=-1, max_value=1):
+ # Transform a PIL.Image to torch.Tensor
+ image = torch.Tensor(np.array(image, dtype=np.float32))
+ image = image.to(dtype=torch_dtype or self.torch_dtype, device=device or self.device)
+ image = image * ((max_value - min_value) / 255) + min_value
+ image = repeat(image, f"H W C -> {pattern}", **({"B": 1} if "B" in pattern else {}))
+ return image
+
+
+ def preprocess_video(self, video, torch_dtype=None, device=None, pattern="B C T H W", min_value=-1, max_value=1):
+ # Transform a list of PIL.Image to torch.Tensor
+ video = [self.preprocess_image(image, torch_dtype=torch_dtype, device=device, min_value=min_value, max_value=max_value) for image in video]
+ video = torch.stack(video, dim=pattern.index("T") // 2)
+ return video
+
+
+ def vae_output_to_image(self, vae_output, pattern="B C H W", min_value=-1, max_value=1):
+ # Transform a torch.Tensor to PIL.Image
+ if pattern != "H W C":
+ vae_output = reduce(vae_output, f"{pattern} -> H W C", reduction="mean")
+ image = ((vae_output - min_value) * (255 / (max_value - min_value))).clip(0, 255)
+ image = image.to(device="cpu", dtype=torch.uint8)
+ image = Image.fromarray(image.numpy())
+ return image
+
+
+ def vae_output_to_video(self, vae_output, pattern="B C T H W", min_value=-1, max_value=1):
+ # Transform a torch.Tensor to list of PIL.Image
+ if pattern != "T H W C":
+ vae_output = reduce(vae_output, f"{pattern} -> T H W C", reduction="mean")
+ video = [self.vae_output_to_image(image, pattern="H W C", min_value=min_value, max_value=max_value) for image in vae_output]
+ return video
+
+ def output_audio_format_check(self, audio_output):
+ # output standard foramt: [C, T], output dtype: float()
+ # remove batch dim
+ if audio_output.ndim == 3:
+ audio_output = audio_output.squeeze(0)
+ return audio_output.float().cpu()
+
+ def load_models_to_device(self, model_names):
+ if self.vram_management_enabled:
+ # offload models
+ for name, model in self.named_children():
+ if name not in model_names:
+ if hasattr(model, "vram_management_enabled") and model.vram_management_enabled:
+ if hasattr(model, "offload"):
+ model.offload()
+ else:
+ for module in model.modules():
+ if hasattr(module, "offload"):
+ module.offload()
+ getattr(torch, self.device_type).empty_cache()
+ # onload models
+ for name, model in self.named_children():
+ if name in model_names:
+ if hasattr(model, "vram_management_enabled") and model.vram_management_enabled:
+ if hasattr(model, "onload"):
+ model.onload()
+ else:
+ for module in model.modules():
+ if hasattr(module, "onload"):
+ module.onload()
+
+
+ def generate_noise(self, shape, seed=None, rand_device="cpu", rand_torch_dtype=torch.float32, device=None, torch_dtype=None):
+ # Initialize Gaussian noise
+ generator = None if seed is None else torch.Generator(rand_device).manual_seed(seed)
+ noise = torch.randn(shape, generator=generator, device=rand_device, dtype=rand_torch_dtype)
+ noise = noise.to(dtype=torch_dtype or self.torch_dtype, device=device or self.device)
+ return noise
+
+
+ def get_vram(self):
+ device = self.device if not IS_NPU_AVAILABLE else get_device_name()
+ return getattr(torch, self.device_type).mem_get_info(device)[1] / (1024 ** 3)
+
+ def get_module(self, model, name):
+ if "." in name:
+ name, suffix = name[:name.index(".")], name[name.index(".") + 1:]
+ if name.isdigit():
+ return self.get_module(model[int(name)], suffix)
+ else:
+ return self.get_module(getattr(model, name), suffix)
+ else:
+ return getattr(model, name)
+
+
+
+ def blend_with_mask(self, base, addition, mask):
+ return base * (1 - mask) + addition * mask
+
+
+ def step(self, scheduler, latents, progress_id, noise_pred, input_latents=None, inpaint_mask=None, **kwargs):
+ timestep = scheduler.timesteps[progress_id]
+ if inpaint_mask is not None:
+ noise_pred_expected = scheduler.return_to_timestep(scheduler.timesteps[progress_id], latents, input_latents)
+ noise_pred = self.blend_with_mask(noise_pred_expected, noise_pred, inpaint_mask)
+ latents_next = scheduler.step(noise_pred, timestep, latents)
+ return latents_next
+
+
+ def split_pipeline_units(self, model_names: list[str]):
+ return PipelineUnitGraph().split_pipeline_units(self.units, model_names)
+
+
+ def flush_vram_management_device(self, device):
+ for module in self.modules():
+ if isinstance(module, AutoTorchModule):
+ module.offload_device = device
+ module.onload_device = device
+ module.preparing_device = device
+ module.computation_device = device
+
+
+ def check_quant_hot_load(self, module: torch.nn.Module):
+ # A quantized weight cannot absorb a fused LoRA, so hot-loading is the only option for it.
+ if getattr(module, "quantize_config", None) is None:
+ return module
+ return self.enable_lora_hot_loading(module)
+
+
+ def load_lora(
+ self,
+ module: torch.nn.Module,
+ lora_config: Union[ModelConfig, str] = None,
+ alpha=1,
+ hotload=None,
+ state_dict=None,
+ verbose=1,
+ ):
+ module = self.check_quant_hot_load(module)
+ if state_dict is None:
+ if isinstance(lora_config, str):
+ lora = load_state_dict(lora_config, torch_dtype=self.torch_dtype, device=self.device)
+ else:
+ lora_config.download_if_necessary()
+ lora = load_state_dict(lora_config.path, torch_dtype=self.torch_dtype, device=self.device)
+ else:
+ lora = state_dict
+ lora_loader = self.lora_loader(torch_dtype=self.torch_dtype, device=self.device)
+ lora = lora_loader.convert_state_dict(lora)
+ if hotload is None:
+ hotload = hasattr(module, "vram_management_enabled") and getattr(module, "vram_management_enabled")
+ if hotload:
+ if not (hasattr(module, "vram_management_enabled") and getattr(module, "vram_management_enabled")):
+ raise ValueError("VRAM Management is not enabled. LoRA hotloading is not supported.")
+ updated_num = 0
+ for _, module in module.named_modules():
+ if isinstance(module, LoRAHotLoadMixin):
+ name = module.name
+ lora_a_name = f'{name}.lora_A.weight'
+ lora_b_name = f'{name}.lora_B.weight'
+ if lora_a_name in lora and lora_b_name in lora:
+ updated_num += 1
+ module.lora_A_weights.append(lora[lora_a_name] * alpha)
+ module.lora_B_weights.append(lora[lora_b_name])
+ if verbose >= 1:
+ print(f"{updated_num} tensors are patched by LoRA. You can use `pipe.clear_lora()` to clear all LoRA layers.")
+ else:
+ lora_loader.fuse_lora_to_base_model(module, lora, alpha=alpha)
+
+
+ def clear_lora(self, verbose=1):
+ cleared_num = 0
+ for name, module in self.named_modules():
+ if isinstance(module, LoRAHotLoadMixin):
+ if hasattr(module, "lora_A_weights"):
+ if len(module.lora_A_weights) > 0:
+ cleared_num += 1
+ module.lora_A_weights.clear()
+ if hasattr(module, "lora_B_weights"):
+ module.lora_B_weights.clear()
+ if verbose >= 1:
+ print(f"{cleared_num} LoRA layers are cleared.")
+
+
+ def download_and_load_models(self, model_configs: list[ModelConfig] = [], vram_limit: float = None):
+ model_pool = ModelPool()
+ for model_config in model_configs:
+ model_config.download_if_necessary()
+ vram_config = model_config.vram_config()
+ vram_config["computation_dtype"] = vram_config["computation_dtype"] or self.torch_dtype
+ vram_config["computation_device"] = vram_config["computation_device"] or self.device
+ model_pool.auto_load_model(
+ model_config.path,
+ vram_config=vram_config,
+ vram_limit=vram_limit,
+ clear_parameters=model_config.clear_parameters,
+ state_dict=model_config.state_dict,
+ quantize=model_config.quantize,
+ )
+ return model_pool
+
+
+ def check_vram_management_state(self):
+ vram_management_enabled = False
+ for module in self.children():
+ if hasattr(module, "vram_management_enabled") and getattr(module, "vram_management_enabled"):
+ vram_management_enabled = True
+ return vram_management_enabled
+
+
+ def cfg_guided_model_fn(self, model_fn, cfg_scale, inputs_shared, inputs_posi, inputs_nega, **inputs_others):
+ # Positive side forward
+ if inputs_shared.get("positive_only_lora", None) is not None:
+ self.load_lora(self.dit, state_dict=inputs_shared["positive_only_lora"], verbose=0)
+ noise_pred_posi = model_fn(**inputs_posi, **inputs_shared, **inputs_others)
+ if inputs_shared.get("positive_only_lora", None) is not None:
+ self.clear_lora(verbose=0)
+
+ if cfg_scale != 1.0:
+ # Negative side forward
+ if inputs_shared.get("negative_only_lora", None) is not None:
+ self.load_lora(self.dit, state_dict=inputs_shared["negative_only_lora"], verbose=0)
+ noise_pred_nega = model_fn(**inputs_nega, **inputs_shared, **inputs_others)
+ if inputs_shared.get("negative_only_lora", None) is not None:
+ self.clear_lora(verbose=0)
+
+ if isinstance(noise_pred_posi, tuple):
+ # Separately handling different output types of latents, eg. video and audio latents.
+ noise_pred = tuple(
+ n_nega + cfg_scale * (n_posi - n_nega)
+ for n_posi, n_nega in zip(noise_pred_posi, noise_pred_nega)
+ )
+ else:
+ noise_pred = noise_pred_nega + cfg_scale * (noise_pred_posi - noise_pred_nega)
+ else:
+ noise_pred = noise_pred_posi
+ return noise_pred
+
+
+
+
+ def enable_lora_hot_loading(self, model: torch.nn.Module):
+ if hasattr(model, "vram_management_enabled") and getattr(model, "vram_management_enabled"):
+ return model
+ module_map = {torch.nn.Linear: AutoWrappedLinear}
+ vram_config = {
+ "offload_dtype": self.torch_dtype,
+ "offload_device": self.device,
+ "onload_dtype": self.torch_dtype,
+ "onload_device": self.device,
+ "preparing_dtype": self.torch_dtype,
+ "preparing_device": self.device,
+ "computation_dtype": self.torch_dtype,
+ "computation_device": self.device,
+ }
+ model = enable_vram_management(model, module_map, vram_config=vram_config, quantize=getattr(model, "quantize_config", None))
+ return model
+
+ def compile_pipeline(self, mode: str = "default", dynamic: bool = True, fullgraph: bool = False, compile_models: list = None, **kwargs):
+ """
+ compile the pipeline with torch.compile. The models that will be compiled are determined by the `compilable_models` attribute of the pipeline.
+ If a model has `_repeated_blocks` attribute, we will compile these blocks with regional compilation. Otherwise, we will compile the whole model.
+ See https://docs.pytorch.org/docs/stable/generated/torch.compile.html#torch.compile for details about compilation arguments.
+ Args:
+ mode: The compilation mode, which will be passed to `torch.compile`, options are "default", "reduce-overhead", "max-autotune" and "max-autotune-no-cudagraphs. Default to "default".
+ dynamic: Whether to enable dynamic graph compilation to support dynamic input shapes, which will be passed to `torch.compile`. Default to True (recommended).
+ fullgraph: Whether to use full graph compilation, which will be passed to `torch.compile`. Default to False (recommended).
+ compile_models: The list of model names to be compiled. If None, we will compile the models in `pipeline.compilable_models`. Default to None.
+ **kwargs: Other arguments for `torch.compile`.
+ """
+ compile_models = compile_models or getattr(self, "compilable_models", [])
+ if len(compile_models) == 0:
+ print("No compilable models in the pipeline. Skip compilation.")
+ return
+ for name in compile_models:
+ model = getattr(self, name, None)
+ if model is None:
+ print(f"Model '{name}' not found in the pipeline.")
+ continue
+ repeated_blocks = getattr(model, "_repeated_blocks", None)
+ # regional compilation for repeated blocks.
+ if repeated_blocks is not None:
+ for submod in model.modules():
+ if submod.__class__.__name__ in repeated_blocks:
+ submod.compile(mode=mode, dynamic=dynamic, fullgraph=fullgraph, **kwargs)
+ # compile the whole model.
+ else:
+ model.compile(mode=mode, dynamic=dynamic, fullgraph=fullgraph, **kwargs)
+ print(f"{name} is compiled with mode={mode}, dynamic={dynamic}, fullgraph={fullgraph}.")
+
+
+class PipelineUnitGraph:
+ def __init__(self):
+ pass
+
+ def build_edges(self, units: list[PipelineUnit]):
+ # Establish dependencies between units
+ # to search for subsequent related computation units.
+ last_compute_unit_id = {}
+ edges = []
+ for unit_id, unit in enumerate(units):
+ for input_param in unit.fetch_input_params():
+ if input_param in last_compute_unit_id:
+ edges.append((last_compute_unit_id[input_param], unit_id))
+ for output_param in unit.fetch_output_params():
+ last_compute_unit_id[output_param] = unit_id
+ return edges
+
+ def build_chains(self, units: list[PipelineUnit]):
+ # Establish updating chains for each variable
+ # to track their computation process.
+ params = sum([unit.fetch_input_params() + unit.fetch_output_params() for unit in units], [])
+ params = sorted(list(set(params)))
+ chains = {param: [] for param in params}
+ for unit_id, unit in enumerate(units):
+ for param in unit.fetch_output_params():
+ chains[param].append(unit_id)
+ return chains
+
+ def search_direct_unit_ids(self, units: list[PipelineUnit], model_names: list[str]):
+ # Search for units that directly participate in the model's computation.
+ related_unit_ids = []
+ for unit_id, unit in enumerate(units):
+ for model_name in model_names:
+ if unit.onload_model_names is not None and model_name in unit.onload_model_names:
+ related_unit_ids.append(unit_id)
+ break
+ return related_unit_ids
+
+ def search_related_unit_ids(self, edges, start_unit_ids, direction="target"):
+ # Search for subsequent related computation units.
+ related_unit_ids = [unit_id for unit_id in start_unit_ids]
+ while True:
+ neighbors = []
+ for source, target in edges:
+ if direction == "target" and source in related_unit_ids and target not in related_unit_ids:
+ neighbors.append(target)
+ elif direction == "source" and source not in related_unit_ids and target in related_unit_ids:
+ neighbors.append(source)
+ neighbors = sorted(list(set(neighbors)))
+ if len(neighbors) == 0:
+ break
+ else:
+ related_unit_ids.extend(neighbors)
+ related_unit_ids = sorted(list(set(related_unit_ids)))
+ return related_unit_ids
+
+ def search_updating_unit_ids(self, units: list[PipelineUnit], chains, related_unit_ids):
+ # If the input parameters of this subgraph are updated outside the subgraph,
+ # search for the units where these updates occur.
+ first_compute_unit_id = {}
+ for unit_id in related_unit_ids:
+ for param in units[unit_id].fetch_input_params():
+ if param not in first_compute_unit_id:
+ first_compute_unit_id[param] = unit_id
+ updating_unit_ids = []
+ for param in first_compute_unit_id:
+ unit_id = first_compute_unit_id[param]
+ chain = chains[param]
+ if unit_id in chain and chain.index(unit_id) != len(chain) - 1:
+ for unit_id_ in chain[chain.index(unit_id) + 1:]:
+ if unit_id_ not in related_unit_ids:
+ updating_unit_ids.append(unit_id_)
+ related_unit_ids.extend(updating_unit_ids)
+ related_unit_ids = sorted(list(set(related_unit_ids)))
+ return related_unit_ids
+
+ def split_pipeline_units(self, units: list[PipelineUnit], model_names: list[str]):
+ # Split the computation graph,
+ # separating all model-related computations.
+ related_unit_ids = self.search_direct_unit_ids(units, model_names)
+ edges = self.build_edges(units)
+ chains = self.build_chains(units)
+ while True:
+ num_related_unit_ids = len(related_unit_ids)
+ related_unit_ids = self.search_related_unit_ids(edges, related_unit_ids, "target")
+ related_unit_ids = self.search_updating_unit_ids(units, chains, related_unit_ids)
+ if len(related_unit_ids) == num_related_unit_ids:
+ break
+ else:
+ num_related_unit_ids = len(related_unit_ids)
+ related_units = [units[i] for i in related_unit_ids]
+ unrelated_units = [units[i] for i in range(len(units)) if i not in related_unit_ids]
+ return related_units, unrelated_units
+
+
+class PipelineUnitRunner:
+ def __init__(self):
+ pass
+
+ def __call__(self, unit: PipelineUnit, pipe: BasePipeline, inputs_shared: dict, inputs_posi: dict, inputs_nega: dict) -> tuple[dict, dict]:
+ if unit.take_over:
+ # Let the pipeline unit take over this function.
+ inputs_shared, inputs_posi, inputs_nega = unit.process(pipe, inputs_shared=inputs_shared, inputs_posi=inputs_posi, inputs_nega=inputs_nega)
+ elif unit.seperate_cfg:
+ # Positive side
+ processor_inputs = {name: inputs_posi.get(name_) for name, name_ in unit.input_params_posi.items()}
+ if unit.input_params is not None:
+ for name in unit.input_params:
+ processor_inputs[name] = inputs_shared.get(name)
+ processor_outputs = unit.process(pipe, **processor_inputs)
+ inputs_posi.update(processor_outputs)
+ # Negative side
+ if inputs_shared["cfg_scale"] != 1:
+ processor_inputs = {name: inputs_nega.get(name_) for name, name_ in unit.input_params_nega.items()}
+ if unit.input_params is not None:
+ for name in unit.input_params:
+ processor_inputs[name] = inputs_shared.get(name)
+ processor_outputs = unit.process(pipe, **processor_inputs)
+ inputs_nega.update(processor_outputs)
+ else:
+ inputs_nega.update(processor_outputs)
+ else:
+ processor_inputs = {name: inputs_shared.get(name) for name in unit.input_params}
+ processor_outputs = unit.process(pipe, **processor_inputs)
+ inputs_shared.update(processor_outputs)
+ return inputs_shared, inputs_posi, inputs_nega
diff --git a/deployment/runtime/qwen/diffsynth/diffusion/template.py b/deployment/runtime/qwen/diffsynth/diffusion/template.py
index 8e1cdb18cdaa265f11c1e6ee287c325855185393..fdbd84e8d87d58a125a4025f9da729c510eafe39 100644
--- a/deployment/runtime/qwen/diffsynth/diffusion/template.py
+++ b/deployment/runtime/qwen/diffsynth/diffusion/template.py
@@ -1,209 +1,200 @@
-import torch, os, importlib, warnings, json, inspect
-from typing import Dict, List, Tuple, Union
-from ..core import ModelConfig, load_model
-from ..core.device.npu_compatible_device import get_device_type
-from ..utils.lora.merge import merge_lora
-
-
-KVCache = Dict[str, Tuple[torch.Tensor, torch.Tensor]]
-
-
-class TemplateModel(torch.nn.Module):
- def __init__(self):
- super().__init__()
-
- @torch.no_grad()
- def process_inputs(self, **kwargs):
- return {}
-
- def forward(self, **kwargs):
- raise NotImplementedError()
-
-
-def check_template_model_format(model):
- if not hasattr(model, "process_inputs"):
- raise NotImplementedError("`process_inputs` is not implemented in the Template model.")
- if "kwargs" not in inspect.signature(model.process_inputs).parameters:
- raise NotImplementedError("`**kwargs` is not included in `process_inputs`.")
- if not hasattr(model, "forward"):
- raise NotImplementedError("`forward` is not implemented in the Template model.")
- if "kwargs" not in inspect.signature(model.forward).parameters:
- raise NotImplementedError("`**kwargs` is not included in `forward`.")
-
-
-def load_template_model(path, torch_dtype=torch.bfloat16, device="cuda", verbose=1, state_dict=None):
- spec = importlib.util.spec_from_file_location("template_model", os.path.join(path, "model.py"))
- module = importlib.util.module_from_spec(spec)
- spec.loader.exec_module(module)
- template_model_path = getattr(module, 'TEMPLATE_MODEL_PATH') if hasattr(module, 'TEMPLATE_MODEL_PATH') else None
- if template_model_path is not None:
- # With `TEMPLATE_MODEL_PATH`, a pretrained model will be loaded.
- model = load_model(
- model_class=getattr(module, 'TEMPLATE_MODEL'),
- config=getattr(module, 'TEMPLATE_MODEL_CONFIG') if hasattr(module, 'TEMPLATE_MODEL_CONFIG') else None,
- path=os.path.join(path, getattr(module, 'TEMPLATE_MODEL_PATH')),
- torch_dtype=torch_dtype,
- device=device,
- state_dict=state_dict,
- )
- else:
- # Without `TEMPLATE_MODEL_PATH`, a randomly initialized model or a non-model module will be loaded.
- model = module.TEMPLATE_MODEL()
- if hasattr(model, "to"):
- model = model.to(dtype=torch_dtype, device=device)
- if hasattr(model, "eval"):
- model = model.eval()
- check_template_model_format(model)
- if verbose > 0:
- metadata = {
- "model_architecture": getattr(module, 'TEMPLATE_MODEL').__name__,
- "code_path": os.path.join(path, "model.py"),
- "weight_path": template_model_path,
- }
- print(f"Template model loaded: {json.dumps(metadata, indent=4)}")
- return model
-
-
-def load_template_data_processor(path):
- spec = importlib.util.spec_from_file_location("template_model", os.path.join(path, "model.py"))
- module = importlib.util.module_from_spec(spec)
- spec.loader.exec_module(module)
- if hasattr(module, 'TEMPLATE_DATA_PROCESSOR'):
- processor = getattr(module, 'TEMPLATE_DATA_PROCESSOR')
- return processor
- else:
- return None
-
-
-class TemplatePipeline(torch.nn.Module):
- def __init__(
- self,
- torch_dtype: torch.dtype = torch.bfloat16,
- device: Union[str, torch.device] = get_device_type(),
- model_configs: list[ModelConfig] = [],
- lazy_loading: bool = False,
- ):
- super().__init__()
- self.torch_dtype = torch_dtype
- self.device = device
- self.model_configs = model_configs
- self.lazy_loading = lazy_loading
- if lazy_loading:
- for model_config in model_configs:
- TemplatePipeline.check_vram_config(model_config)
- model_config.download_if_necessary()
- self.models = None
- else:
- models = []
- for model_config in model_configs:
- TemplatePipeline.check_vram_config(model_config)
- model_config.download_if_necessary()
- model = load_template_model(model_config.path, torch_dtype=torch_dtype, device=device, state_dict=model_config.state_dict)
- models.append(model)
- self.models = torch.nn.ModuleList(models)
-
- def merge_kv_cache(self, kv_cache_list: List[KVCache]) -> KVCache:
- names = {}
- for kv_cache in kv_cache_list:
- for name in kv_cache:
- names[name] = None
- kv_cache_merged = {}
- for name in names:
- kv_list = [kv_cache.get(name) for kv_cache in kv_cache_list]
- kv_list = [kv for kv in kv_list if kv is not None]
- if len(kv_list) > 0:
- k = torch.concat([kv[0] for kv in kv_list], dim=1)
- v = torch.concat([kv[1] for kv in kv_list], dim=1)
- kv_cache_merged[name] = (k, v)
- return kv_cache_merged
-
- def merge_sequential_embeddings(self, embeddings: List[torch.Tensor]):
- return torch.concat(embeddings, dim=-2)
-
- def merge_template_cache(self, template_cache_list):
- params = sorted(list(set(sum([list(template_cache.keys()) for template_cache in template_cache_list], []))))
- template_cache_merged = {}
- for param in params:
- data = [template_cache[param] for template_cache in template_cache_list if param in template_cache]
- if param == "kv_cache":
- data = self.merge_kv_cache(data)
- elif param == "lora":
- data = merge_lora(data)
- elif param == "text_embedding":
- data = self.merge_sequential_embeddings(data)
- elif len(data) == 1:
- data = data[0]
- else:
- print(f"Conflict detected: `{param}` appears in the outputs of multiple Template models. Only the first one will be retained.")
- data = data[0]
- template_cache_merged[param] = data
- return template_cache_merged
-
- @staticmethod
- def check_vram_config(model_config: ModelConfig):
- params = [
- model_config.offload_device, model_config.offload_dtype,
- model_config.onload_device, model_config.onload_dtype,
- model_config.preparing_device, model_config.preparing_dtype,
- model_config.computation_device, model_config.computation_dtype,
- ]
- for param in params:
- if param is not None:
- warnings.warn("TemplatePipeline doesn't support VRAM management. VRAM config will be ignored.")
-
- @staticmethod
- def from_pretrained(
- torch_dtype: torch.dtype = torch.bfloat16,
- device: Union[str, torch.device] = get_device_type(),
- model_configs: list[ModelConfig] = [],
- lazy_loading: bool = False,
- ):
- pipe = TemplatePipeline(torch_dtype, device, model_configs, lazy_loading)
- return pipe
-
- def fetch_model(self, model_id):
- if self.lazy_loading:
- model_config = self.model_configs[model_id]
- model_config.download_if_necessary()
- model = load_template_model(model_config.path, torch_dtype=self.torch_dtype, device=self.device, state_dict=model_config.state_dict)
- else:
- model = self.models[model_id]
- return model
-
- def call_single_side(self, pipe=None, inputs: List[Dict] = None):
- model = None
- onload_model_id = -1
- template_cache = []
- for i in inputs:
- model_id = i.get("model_id", 0)
- if model_id != onload_model_id:
- model = self.fetch_model(model_id)
- onload_model_id = model_id
- cache = model.process_inputs(pipe=pipe, **i)
- cache = model.forward(pipe=pipe, **cache)
- template_cache.append(cache)
- template_cache = self.merge_template_cache(template_cache)
- return template_cache
-
- @torch.no_grad()
- def __call__(
- self,
- pipe=None,
- template_inputs: List[Dict] = None,
- negative_template_inputs: List[Dict] = None,
- **kwargs,
- ):
- template_cache = self.call_single_side(pipe=pipe, inputs=template_inputs or [])
- negative_template_cache = self.call_single_side(pipe=pipe, inputs=negative_template_inputs or [])
- required_params = list(inspect.signature(pipe.__call__).parameters.keys())
- for param in template_cache:
- if param in required_params:
- kwargs[param] = template_cache[param]
- else:
- print(f"`{param}` is not included in the inputs of `{pipe.__class__.__name__}`. This parameter will be ignored.")
- for param in negative_template_cache:
- if "negative_" + param in required_params:
- kwargs["negative_" + param] = negative_template_cache[param]
- else:
- print(f"`{'negative_' + param}` is not included in the inputs of `{pipe.__class__.__name__}`. This parameter will be ignored.")
- return pipe(**kwargs)
+import torch, os, importlib, warnings, json, inspect
+from typing import Dict, List, Tuple, Union
+from ..core import ModelConfig, load_model
+from ..core.device.npu_compatible_device import get_device_type
+from ..utils.lora.merge import merge_lora
+
+
+KVCache = Dict[str, Tuple[torch.Tensor, torch.Tensor]]
+
+
+class TemplateModel(torch.nn.Module):
+ def __init__(self):
+ super().__init__()
+
+ @torch.no_grad()
+ def process_inputs(self, **kwargs):
+ return {}
+
+ def forward(self, **kwargs):
+ raise NotImplementedError()
+
+
+def check_template_model_format(model):
+ if not hasattr(model, "process_inputs"):
+ raise NotImplementedError("`process_inputs` is not implemented in the Template model.")
+ if "kwargs" not in inspect.signature(model.process_inputs).parameters:
+ raise NotImplementedError("`**kwargs` is not included in `process_inputs`.")
+ if not hasattr(model, "forward"):
+ raise NotImplementedError("`forward` is not implemented in the Template model.")
+ if "kwargs" not in inspect.signature(model.forward).parameters:
+ raise NotImplementedError("`**kwargs` is not included in `forward`.")
+
+
+def load_template_model(path, torch_dtype=torch.bfloat16, device="cuda", verbose=1, state_dict=None):
+ spec = importlib.util.spec_from_file_location("template_model", os.path.join(path, "model.py"))
+ module = importlib.util.module_from_spec(spec)
+ spec.loader.exec_module(module)
+ template_model_path = getattr(module, 'TEMPLATE_MODEL_PATH') if hasattr(module, 'TEMPLATE_MODEL_PATH') else None
+ if template_model_path is not None:
+ # With `TEMPLATE_MODEL_PATH`, a pretrained model will be loaded.
+ model = load_model(
+ model_class=getattr(module, 'TEMPLATE_MODEL'),
+ config=getattr(module, 'TEMPLATE_MODEL_CONFIG') if hasattr(module, 'TEMPLATE_MODEL_CONFIG') else None,
+ path=os.path.join(path, getattr(module, 'TEMPLATE_MODEL_PATH')),
+ torch_dtype=torch_dtype,
+ device=device,
+ state_dict=state_dict,
+ )
+ else:
+ # Without `TEMPLATE_MODEL_PATH`, a randomly initialized model or a non-model module will be loaded.
+ model = module.TEMPLATE_MODEL()
+ if hasattr(model, "to"):
+ model = model.to(dtype=torch_dtype, device=device)
+ if hasattr(model, "eval"):
+ model = model.eval()
+ check_template_model_format(model)
+ if verbose > 0:
+ metadata = {
+ "model_architecture": getattr(module, 'TEMPLATE_MODEL').__name__,
+ "code_path": os.path.join(path, "model.py"),
+ "weight_path": template_model_path,
+ }
+ print(f"Template model loaded: {json.dumps(metadata, indent=4)}")
+ return model
+
+
+
+
+class TemplatePipeline(torch.nn.Module):
+ def __init__(
+ self,
+ torch_dtype: torch.dtype = torch.bfloat16,
+ device: Union[str, torch.device] = get_device_type(),
+ model_configs: list[ModelConfig] = [],
+ lazy_loading: bool = False,
+ ):
+ super().__init__()
+ self.torch_dtype = torch_dtype
+ self.device = device
+ self.model_configs = model_configs
+ self.lazy_loading = lazy_loading
+ if lazy_loading:
+ for model_config in model_configs:
+ TemplatePipeline.check_vram_config(model_config)
+ model_config.download_if_necessary()
+ self.models = None
+ else:
+ models = []
+ for model_config in model_configs:
+ TemplatePipeline.check_vram_config(model_config)
+ model_config.download_if_necessary()
+ model = load_template_model(model_config.path, torch_dtype=torch_dtype, device=device, state_dict=model_config.state_dict)
+ models.append(model)
+ self.models = torch.nn.ModuleList(models)
+
+ def merge_kv_cache(self, kv_cache_list: List[KVCache]) -> KVCache:
+ names = {}
+ for kv_cache in kv_cache_list:
+ for name in kv_cache:
+ names[name] = None
+ kv_cache_merged = {}
+ for name in names:
+ kv_list = [kv_cache.get(name) for kv_cache in kv_cache_list]
+ kv_list = [kv for kv in kv_list if kv is not None]
+ if len(kv_list) > 0:
+ k = torch.concat([kv[0] for kv in kv_list], dim=1)
+ v = torch.concat([kv[1] for kv in kv_list], dim=1)
+ kv_cache_merged[name] = (k, v)
+ return kv_cache_merged
+
+ def merge_sequential_embeddings(self, embeddings: List[torch.Tensor]):
+ return torch.concat(embeddings, dim=-2)
+
+ def merge_template_cache(self, template_cache_list):
+ params = sorted(list(set(sum([list(template_cache.keys()) for template_cache in template_cache_list], []))))
+ template_cache_merged = {}
+ for param in params:
+ data = [template_cache[param] for template_cache in template_cache_list if param in template_cache]
+ if param == "kv_cache":
+ data = self.merge_kv_cache(data)
+ elif param == "lora":
+ data = merge_lora(data)
+ elif param == "text_embedding":
+ data = self.merge_sequential_embeddings(data)
+ elif len(data) == 1:
+ data = data[0]
+ else:
+ print(f"Conflict detected: `{param}` appears in the outputs of multiple Template models. Only the first one will be retained.")
+ data = data[0]
+ template_cache_merged[param] = data
+ return template_cache_merged
+
+ @staticmethod
+ def check_vram_config(model_config: ModelConfig):
+ params = [
+ model_config.offload_device, model_config.offload_dtype,
+ model_config.onload_device, model_config.onload_dtype,
+ model_config.preparing_device, model_config.preparing_dtype,
+ model_config.computation_device, model_config.computation_dtype,
+ ]
+ for param in params:
+ if param is not None:
+ warnings.warn("TemplatePipeline doesn't support VRAM management. VRAM config will be ignored.")
+
+ @staticmethod
+ def from_pretrained(
+ torch_dtype: torch.dtype = torch.bfloat16,
+ device: Union[str, torch.device] = get_device_type(),
+ model_configs: list[ModelConfig] = [],
+ lazy_loading: bool = False,
+ ):
+ pipe = TemplatePipeline(torch_dtype, device, model_configs, lazy_loading)
+ return pipe
+
+ def fetch_model(self, model_id):
+ if self.lazy_loading:
+ model_config = self.model_configs[model_id]
+ model_config.download_if_necessary()
+ model = load_template_model(model_config.path, torch_dtype=self.torch_dtype, device=self.device, state_dict=model_config.state_dict)
+ else:
+ model = self.models[model_id]
+ return model
+
+ def call_single_side(self, pipe=None, inputs: List[Dict] = None):
+ model = None
+ onload_model_id = -1
+ template_cache = []
+ for i in inputs:
+ model_id = i.get("model_id", 0)
+ if model_id != onload_model_id:
+ model = self.fetch_model(model_id)
+ onload_model_id = model_id
+ cache = model.process_inputs(pipe=pipe, **i)
+ cache = model.forward(pipe=pipe, **cache)
+ template_cache.append(cache)
+ template_cache = self.merge_template_cache(template_cache)
+ return template_cache
+
+ @torch.no_grad()
+ def __call__(
+ self,
+ pipe=None,
+ template_inputs: List[Dict] = None,
+ negative_template_inputs: List[Dict] = None,
+ **kwargs,
+ ):
+ template_cache = self.call_single_side(pipe=pipe, inputs=template_inputs or [])
+ negative_template_cache = self.call_single_side(pipe=pipe, inputs=negative_template_inputs or [])
+ required_params = list(inspect.signature(pipe.__call__).parameters.keys())
+ for param in template_cache:
+ if param in required_params:
+ kwargs[param] = template_cache[param]
+ else:
+ print(f"`{param}` is not included in the inputs of `{pipe.__class__.__name__}`. This parameter will be ignored.")
+ for param in negative_template_cache:
+ if "negative_" + param in required_params:
+ kwargs["negative_" + param] = negative_template_cache[param]
+ else:
+ print(f"`{'negative_' + param}` is not included in the inputs of `{pipe.__class__.__name__}`. This parameter will be ignored.")
+ return pipe(**kwargs)
diff --git a/deployment/runtime/qwen/diffsynth/pipelines/qwen_image_21_pbr.py b/deployment/runtime/qwen/diffsynth/pipelines/qwen_image_21_pbr.py
index 7d2db08bdec04da95d6182dba5ac30e20bf7e1a7..0005448a806f6b7bcee63afc7cef5878dfbe351a 100644
--- a/deployment/runtime/qwen/diffsynth/pipelines/qwen_image_21_pbr.py
+++ b/deployment/runtime/qwen/diffsynth/pipelines/qwen_image_21_pbr.py
@@ -1,98 +1,90 @@
-"""Joint Albedo/MRA latent prediction using the Qwen-Image 2.1 backbone."""
-
-import torch
-from torch import nn
-
-from .qwen_image_21 import patchify, unpatchify
-
-
-class QwenImage21PBRModel(nn.Module):
- """Predict two separate material maps with a shared noisy target group.
-
- The modality axis is ordered ``(albedo, mra)``. MRA pixel semantics belong
- to the dataset contract; this module does not infer or repack AO/alpha.
- Conditions remain clean, and each map retains its own spatial RoPE block.
- """
-
- modalities = ("albedo", "mra")
-
- def __init__(self, dit):
- super().__init__()
- self.dit = dit
- channels = dit.img_in.in_features
- if channels != dit.out_channels:
- raise ValueError("PBR requires equal input and output latent channels")
- self.modality_embeddings = nn.Parameter(torch.zeros(2, channels))
-
- def forward(
- self,
- latents,
- timestep,
- prompt_embeds,
- prompt_embeds_mask,
- edit_image_pad_mask,
- edit_latents=None,
- use_gradient_checkpointing=False,
- use_gradient_checkpointing_offload=False,
- use_flex_attention=False,
- kv_cache=None,
- gradient_checkpointing_interval=1,
- ):
- if latents.ndim != 5 or latents.shape[1] != len(self.modalities):
- raise ValueError("Expected latents shaped [batch, 2, channels, height, width]")
- batch_size, _, channels, height, width = latents.shape
- if channels != self.modality_embeddings.shape[1]:
- raise ValueError("Target latent channels do not match the DiT")
- if height % 2 or width % 2:
- raise ValueError("Target latent dimensions must be even (pixel dimensions divisible by 32)")
- if prompt_embeds.shape[0] != batch_size or edit_image_pad_mask.shape[0] != batch_size:
- raise ValueError("Condition and target batch sizes must match")
- if not torch.equal(edit_image_pad_mask, edit_image_pad_mask[:1].expand_as(edit_image_pad_mask)):
- raise ValueError("Batch elements must share the same image-token layout")
- edit_latents = [] if edit_latents is None else edit_latents
- for condition in edit_latents:
- if condition.ndim != 4 or condition.shape[:2] != (batch_size, channels):
- raise ValueError("Each condition must be shaped [batch, channels, height, width]")
-
- target_tokens = []
- for index in range(len(self.modalities)):
- tokens = patchify(latents[:, index])
- embedding = self.modality_embeddings[index].to(device=tokens.device, dtype=tokens.dtype)
- target_tokens.append(tokens + embedding)
- hidden_states = torch.cat([*[patchify(image) for image in edit_latents], *target_tokens], dim=1)
- target_seq_len = len(self.modalities) * height * width
- target_image_pad_mask = torch.ones(
- (batch_size, target_seq_len // 4), dtype=torch.bool, device=latents.device
- )
- image_pad_mask = torch.cat([edit_image_pad_mask.bool(), target_image_pad_mask], dim=1)
- img_shapes = [[
- *[(1, image.shape[2], image.shape[3]) for image in edit_latents],
- *[(1, height, width) for _ in self.modalities],
- ] for _ in range(batch_size)]
- model_output = self.dit(
- hidden_states=hidden_states,
- timestep=timestep / 1000,
- encoder_hidden_states=prompt_embeds,
- encoder_hidden_states_mask=prompt_embeds_mask,
- img_shapes=img_shapes,
- img_mask=image_pad_mask,
- kv_cache=kv_cache,
- use_gradient_checkpointing=use_gradient_checkpointing,
- use_gradient_checkpointing_offload=use_gradient_checkpointing_offload,
- use_flex_attention=use_flex_attention,
- num_target_images=len(self.modalities),
- gradient_checkpointing_interval=gradient_checkpointing_interval,
- )
- target_output = model_output[:, -target_seq_len:]
- return torch.stack([
- unpatchify(tokens, height, width)
- for tokens in target_output.split(height * width, dim=1)
- ], dim=1)
-
-
-def pbr_flow_matching_losses(prediction, target):
- """Equal-weight latent flow losses, with separate material diagnostics."""
- if prediction.shape != target.shape or prediction.ndim != 5 or prediction.shape[1] != 2:
- raise ValueError("Prediction and target must have matching [batch, 2, channels, height, width] shapes")
- losses = (prediction.float() - target.float()).square().mean(dim=(0, 2, 3, 4))
- return {"albedo": losses[0], "mra": losses[1], "loss": losses.mean()}
+"""Joint Albedo/MRA latent prediction using the Qwen-Image 2.1 backbone."""
+
+import torch
+from torch import nn
+
+from .qwen_image_21 import patchify, unpatchify
+
+
+class QwenImage21PBRModel(nn.Module):
+ """Predict two separate material maps with a shared noisy target group.
+
+ The modality axis is ordered ``(albedo, mra)``. MRA pixel semantics belong
+ to the dataset contract; this module does not infer or repack AO/alpha.
+ Conditions remain clean, and each map retains its own spatial RoPE block.
+ """
+
+ modalities = ("albedo", "mra")
+
+ def __init__(self, dit):
+ super().__init__()
+ self.dit = dit
+ channels = dit.img_in.in_features
+ if channels != dit.out_channels:
+ raise ValueError("PBR requires equal input and output latent channels")
+ self.modality_embeddings = nn.Parameter(torch.zeros(2, channels))
+
+ def forward(
+ self,
+ latents,
+ timestep,
+ prompt_embeds,
+ prompt_embeds_mask,
+ edit_image_pad_mask,
+ edit_latents=None,
+ use_gradient_checkpointing=False,
+ use_gradient_checkpointing_offload=False,
+ use_flex_attention=False,
+ kv_cache=None,
+ gradient_checkpointing_interval=1,
+ ):
+ if latents.ndim != 5 or latents.shape[1] != len(self.modalities):
+ raise ValueError("Expected latents shaped [batch, 2, channels, height, width]")
+ batch_size, _, channels, height, width = latents.shape
+ if channels != self.modality_embeddings.shape[1]:
+ raise ValueError("Target latent channels do not match the DiT")
+ if height % 2 or width % 2:
+ raise ValueError("Target latent dimensions must be even (pixel dimensions divisible by 32)")
+ if prompt_embeds.shape[0] != batch_size or edit_image_pad_mask.shape[0] != batch_size:
+ raise ValueError("Condition and target batch sizes must match")
+ if not torch.equal(edit_image_pad_mask, edit_image_pad_mask[:1].expand_as(edit_image_pad_mask)):
+ raise ValueError("Batch elements must share the same image-token layout")
+ edit_latents = [] if edit_latents is None else edit_latents
+ for condition in edit_latents:
+ if condition.ndim != 4 or condition.shape[:2] != (batch_size, channels):
+ raise ValueError("Each condition must be shaped [batch, channels, height, width]")
+
+ target_tokens = []
+ for index in range(len(self.modalities)):
+ tokens = patchify(latents[:, index])
+ embedding = self.modality_embeddings[index].to(device=tokens.device, dtype=tokens.dtype)
+ target_tokens.append(tokens + embedding)
+ hidden_states = torch.cat([*[patchify(image) for image in edit_latents], *target_tokens], dim=1)
+ target_seq_len = len(self.modalities) * height * width
+ target_image_pad_mask = torch.ones(
+ (batch_size, target_seq_len // 4), dtype=torch.bool, device=latents.device
+ )
+ image_pad_mask = torch.cat([edit_image_pad_mask.bool(), target_image_pad_mask], dim=1)
+ img_shapes = [[
+ *[(1, image.shape[2], image.shape[3]) for image in edit_latents],
+ *[(1, height, width) for _ in self.modalities],
+ ] for _ in range(batch_size)]
+ model_output = self.dit(
+ hidden_states=hidden_states,
+ timestep=timestep / 1000,
+ encoder_hidden_states=prompt_embeds,
+ encoder_hidden_states_mask=prompt_embeds_mask,
+ img_shapes=img_shapes,
+ img_mask=image_pad_mask,
+ kv_cache=kv_cache,
+ use_gradient_checkpointing=use_gradient_checkpointing,
+ use_gradient_checkpointing_offload=use_gradient_checkpointing_offload,
+ use_flex_attention=use_flex_attention,
+ num_target_images=len(self.modalities),
+ gradient_checkpointing_interval=gradient_checkpointing_interval,
+ )
+ target_output = model_output[:, -target_seq_len:]
+ return torch.stack([
+ unpatchify(tokens, height, width)
+ for tokens in target_output.split(height * width, dim=1)
+ ], dim=1)
diff --git a/deployment/runtime/qwen/examples/qwen_image_21/pbr/infer.py b/deployment/runtime/qwen/examples/qwen_image_21/pbr/infer.py
index 7e971b0ffb43420ca54c9e1d57acde05f83e041a..753ecd9f2f4de857e10d8f5b57a253fddafb681a 100644
--- a/deployment/runtime/qwen/examples/qwen_image_21/pbr/infer.py
+++ b/deployment/runtime/qwen/examples/qwen_image_21/pbr/infer.py
@@ -1,390 +1,371 @@
-#!/usr/bin/env python3
-"""RGB/RGBA-only endpoint inference. No GT, data manifests, downloads or hashes."""
-from __future__ import annotations
-import argparse
-from contextlib import nullcontext
-import gc
-import importlib.util
-import json
-import math
-import os
-from pathlib import Path
-import time
-
-import numpy as np
-from PIL import Image
-import torch
-from safetensors import safe_open
-from safetensors.torch import save_file
-
-from train import DEFAULTS, build_model, decode_pair, endpoint_noise
-from validate_local import checkpoint_component, explicit_load
-from encode_cache import PROMPT
-from inference_offload import (
- release_device_cache, sequential_cpu_offload, stage_on_device,
- text_encoder_offload_blocks,
-)
-
-# The isolated Qwen environment does not include the application package path.
-_timing_helper_path = os.environ.get('SUPRATEX_TIMING_HELPER')
-if _timing_helper_path:
- _timing_spec = importlib.util.spec_from_file_location('_supratex_timing', _timing_helper_path)
- if _timing_spec is None or _timing_spec.loader is None:
- raise RuntimeError('could not load the configured Supra-Tex timing helper')
- _timing_helper = importlib.util.module_from_spec(_timing_spec)
- _timing_spec.loader.exec_module(_timing_helper)
- span = _timing_helper.span
-else:
- span = nullcontext
-
-
-def prepare_image(path, resolution):
- if resolution < 32 or resolution % 32:
- raise ValueError('resolution must be positive and divisible by 32')
- with Image.open(path) as source:
- had_alpha = 'A' in source.getbands() or 'transparency' in source.info
- original = source.convert('RGBA')
- width, height = original.size
- scale = min(1., resolution / max(width, height))
- content = (max(1, round(width * scale)), max(1, round(height * scale)))
- aligned = tuple(math.ceil(value / 32) * 32 for value in content)
- offset = tuple((aligned[i] - content[i]) // 2 + (resolution - aligned[i]) // 2 for i in range(2))
- resized = original.resize(content, Image.Resampling.LANCZOS) if original.size != content else original.copy()
- square = Image.new('RGBA', (resolution, resolution), (0, 0, 0, 0))
- square.paste(resized, offset)
- transform = dict(source_size=[width, height], content_size=list(content), padded_size=[resolution, resolution],
- offset=list(offset), scale=scale, input_had_alpha=had_alpha,
- resampling='LANCZOS, same two-stage centered padding as training, no crop or alpha compositing',
- coverage='input alpha retained' if had_alpha else 'all-one source coverage; no segmentation inferred')
- return original, square, transform
-
-
-def load_checkpoint(path):
- path = Path(path)
- if path.suffix == '.safetensors':
- from safetensors import safe_open
- import math
-
- adapter = json.loads(path.with_name('adapter_config.json').read_text())
- if (not isinstance(adapter, dict) or adapter.get('format') != 'supratex_qwen_material_adapter'
- or type(adapter.get('schema_version')) is not int or adapter['schema_version'] != 1
- or adapter.get('adapter_type') != 'qwen_image_21_pbr_endpoint'
- or adapter.get('base_model_name_or_path') != 'Qwen/Qwen-Image-2.1'
- or adapter.get('weight_file') != path.name
- or type(adapter.get('source_step')) is not int or adapter['source_step'] != 100000):
- raise ValueError('unsupported Qwen material adapter config')
- inference = adapter.get('inference_config')
- booleans = {'lora_output_projection', 'omit_all_valid_mask', 'bf16_autocast', 'deterministic_algorithms'}
- required = booleans | {'resolution', 'noise_seed', 'lora_rank', 'lora_alpha', 'attention_backend'}
- if not isinstance(inference, dict) or set(inference) != required:
- raise ValueError('Qwen adapter requires complete inference config')
- if (type(inference['resolution']) is not int or inference['resolution'] != 1024
- or type(inference['noise_seed']) is not int or inference['noise_seed'] != 42
- or inference['attention_backend'] != 'torch'
- or type(inference['lora_rank']) is not int or inference['lora_rank'] <= 0
- or type(inference['lora_alpha']) not in {int, float}
- or not math.isfinite(inference['lora_alpha']) or inference['lora_alpha'] <= 0
- or any(type(inference[key]) is not bool for key in booleans)):
- raise ValueError('invalid Qwen adapter inference config')
- tensors = adapter.get('tensors')
- if not isinstance(tensors, dict) or not tensors:
- raise ValueError('Qwen adapter requires a nonempty tensor manifest')
- with safe_open(str(path), framework='pt', device='cpu') as reader:
- metadata = reader.metadata() or {}
- if (metadata.get('format') != 'pt' or metadata.get('adapter_format') != adapter['format']
- or metadata.get('schema_version') != str(adapter['schema_version'])
- or set(reader.keys()) != set(tensors)):
- raise ValueError('Qwen adapter tensor keys or metadata mismatch')
- for name, specification in tensors.items():
- header = reader.get_slice(name)
- if (not isinstance(specification, dict)
- or not isinstance(specification.get('shape'), list)
- or any(type(size) is not int or size < 0 for size in specification['shape'])
- or specification['shape'] != header.get_shape()
- or specification.get('dtype') not in {'F64', 'F32', 'F16', 'BF16'}
- or specification['dtype'] != header.get_dtype()):
- raise ValueError(f'Qwen adapter tensor shape or dtype mismatch: {name}')
- weights = {name: reader.get_tensor(name) for name in reader.keys()}
- config = DEFAULTS | inference | {'model_root': adapter['base_model_name_or_path']}
- return config, weights, adapter['source_step']
- marker = json.loads(path.with_suffix('.complete.json').read_text())
- if marker.get('status') != 'complete' or marker.get('bytes') != path.stat().st_size:
- raise ValueError('checkpoint completion record is absent or mismatched')
- # SupraTex adaptation: public inference weights use restricted loading.
- # Legacy trainer checkpoints also contain NumPy uint32 RNG arrays; these
- # exact reconstruction types retain compatibility without arbitrary pickle.
- from numpy.core.multiarray import _reconstruct
- with torch.serialization.safe_globals([_reconstruct, (_reconstruct, 'numpy.core.multiarray._reconstruct'),
- (_reconstruct, 'numpy._core.multiarray._reconstruct'),
- np.ndarray, np.dtype, type(np.dtype('uint32'))]):
- state = torch.load(path, map_location='cpu', weights_only=True)
- if state.get('format') != 'qwen_pbr_endpoint_v1' or marker.get('step') != state.get('step'):
- raise ValueError('unsupported checkpoint or inconsistent step')
- if not isinstance(state.get('config'), dict) or not isinstance(state.get('model'), dict):
- raise ValueError('checkpoint needs complete config and trainable state')
- config = DEFAULTS | state['config']
- for key in ('model_root', 'resolution', 'lora_rank', 'lora_alpha', 'noise_seed'):
- if key not in state['config']:
- raise ValueError(f'checkpoint config lacks {key}')
- return config, state['model'], state['step']
-
-
-def strict_load_trainable(model, weights):
- expected = {name: parameter for name, parameter in model.named_parameters() if parameter.requires_grad}
- if expected.keys() != weights.keys():
- raise ValueError('checkpoint trainable key set differs from constructed model')
- with torch.no_grad():
- for name, parameter in expected.items():
- tensor = weights[name]
- if tensor.shape != parameter.shape or tensor.dtype != parameter.dtype or not torch.isfinite(tensor).all():
- raise ValueError(f'checkpoint tensor mismatch/nonfinite: {name}')
- parameter.copy_(tensor)
- model.requires_grad_(False).eval()
-
-
-def canonical_conditions(values, resolution):
- """Only retain consumed conditioning; material targets are never loaded."""
- values = dict(values)
- layout = values['edit_image_pad_mask'].bool()
- prompt = values.get('prompt_embeds')
- if prompt is None:
- text = values['prompt_text_embeds']
- if layout.ndim != 2 or layout.shape[0] != 1 or text.shape[1] != int((~layout[0]).sum()):
- raise ValueError('compact conditioning layout mismatch')
- prompt = text.new_zeros((1, layout.shape[1], text.shape[-1]))
- prompt[:, ~layout[0]] = text
- if prompt.ndim != 3 or prompt.shape[0] != 1 or prompt.shape[2] != 4096 or layout.shape != prompt.shape[:2]:
- raise ValueError('invalid prompt/layout shape')
- # Model replaces image-placeholder rows from RGB VAE. Canonical zeros allow
- # exact native-v2 comparison without changing consumed values.
- prompt = prompt.clone()
- prompt[:, layout[0]] = 0
- mask = values.get('prompt_embeds_mask')
- mask = torch.ones_like(layout) if mask is None else mask.bool()
- rgb = values['rgb']
- if rgb.shape != (1, 64, resolution // 16, resolution // 16) or mask.shape != layout.shape:
- raise ValueError('invalid RGB latent/mask shape')
- result = dict(rgb=rgb, prompt_embeds=prompt, prompt_embeds_mask=mask, edit_image_pad_mask=layout)
- if any(value.is_floating_point() and not torch.isfinite(value).all() for value in result.values()):
- raise ValueError('nonfinite conditioning')
- return result
-
-
-def load_conditions(path, resolution, device):
- values = {}
- keys = {'rgb', 'prompt_embeds', 'prompt_text_embeds', 'prompt_embeds_mask', 'edit_image_pad_mask'}
- with safe_open(str(path), framework='pt', device=str(device)) as reader:
- for key in keys & set(reader.keys()):
- values[key] = reader.get_tensor(key)
- return canonical_conditions(values, resolution)
-
-
-def input_binding(path):
- path = Path(os.path.abspath(path)); stat = path.stat()
- return dict(path=str(path), size=stat.st_size, mtime_ns=stat.st_mtime_ns)
-
-
-def verify_cache_input(cache, image, resolution, model_references):
- metadata = json.loads(Path(cache).with_suffix('.json').read_text())
- binding = metadata.get('input', metadata.get('sources', {}).get('rgb'))
- if binding != input_binding(image) or metadata.get('resolution') != resolution:
- raise ValueError('cache conditioning is not bound to this exact input/stat/resolution')
- if metadata.get('model_root') not in model_references or metadata.get('prompt') != PROMPT:
- raise ValueError('cached model reference or prompt differs from checkpoint inference contract')
- return metadata['model_root']
-
-
-def native_conditions(square, model_root, resolution, device, low_vram=False):
- from transformers import AutoProcessor
- from diffsynth.models.qwen_image_21_vae import QwenImage21VAE
- from diffsynth.models.qwen_image_21_text_encoder import QwenImage21TextEncoder
- from diffsynth.pipelines.qwen_image_21 import QwenImage21Pipeline, QwenImage21Unit_EditImageEmbedder, QwenImage21Unit_PromptEmbedder
- from diffsynth.utils.state_dict_converters.qwen_image_21_text_encoder import QwenImage21TextEncoderStateDictConverter
- processor = AutoProcessor.from_pretrained(str(model_root / 'processor'), local_files_only=True)
- processor.image_processor.size = {'shortest_edge': resolution ** 2, 'longest_edge': resolution ** 2}
- pipe = QwenImage21Pipeline(device=device, torch_dtype=torch.bfloat16)
- pipe.processor = processor
- loading_device = 'cpu' if low_vram else device
- vae = explicit_load(QwenImage21VAE, checkpoint_component(model_root, 'vae'), loading_device, torch.bfloat16)
- pipe.vae = vae
- with stage_on_device(vae, device, enabled=low_vram), torch.inference_mode():
- edited = QwenImage21Unit_EditImageEmbedder().process(pipe, edit_image=[square], height=resolution, width=resolution,
- tiled=False, tile_size=256, tile_stride=192)
- if edited['edit_image'][0].size != square.size:
- raise ValueError('native edit path changed dimensions')
- rgb = edited['edit_latents'][0]
- encoder = explicit_load(QwenImage21TextEncoder, checkpoint_component(model_root, 'text_encoder'), loading_device,
- torch.bfloat16, QwenImage21TextEncoderStateDictConverter)
- pipe.text_encoder = encoder
- blocks = text_encoder_offload_blocks(encoder) if low_vram else ()
- with sequential_cpu_offload(encoder, blocks, device, enabled=low_vram), torch.inference_mode():
- values = QwenImage21Unit_PromptEmbedder().process(pipe, prompt=PROMPT, edit_image=[square])
- conditions = canonical_conditions(dict(values, rgb=rgb), resolution)
- pipe.text_encoder = None
- del encoder, pipe, values, edited, blocks
- gc.collect(); torch.cuda.empty_cache()
- return conditions, vae
-
-
-def predict(model, conditions, cfg):
- # Shape comes from observed RGB, never a GT or fabricated target tensor.
- shape = (conditions['rgb'].shape[0], 2, *conditions['rgb'].shape[1:])
- shape_carrier = conditions['rgb'].new_empty(shape)
- noise = endpoint_noise(shape_carrier, cfg['noise_seed'])
- mask = conditions['prompt_embeds_mask']
- if cfg['omit_all_valid_mask'] and bool(mask.all()):
- mask = None
- with torch.inference_mode(), torch.autocast('cuda', dtype=torch.bfloat16, enabled=cfg['bf16_autocast']):
- velocity = model(latents=noise, timestep=torch.full((noise.shape[0],), 1000., device=noise.device),
- prompt_embeds=conditions['prompt_embeds'], prompt_embeds_mask=mask,
- edit_image_pad_mask=conditions['edit_image_pad_mask'], edit_latents=[conditions['rgb']],
- use_gradient_checkpointing=False, use_flex_attention=False, kv_cache=None)
- prediction = noise.float() - velocity.float()
- if not torch.isfinite(prediction).all():
- raise FloatingPointError('nonfinite material latent prediction')
- return prediction
-
-
-def predict_and_decode(model, vae, conditions, cfg, low_vram=False):
- device = conditions['rgb'].device
- try:
- with sequential_cpu_offload(model, model.dit.transformer_blocks, device, enabled=low_vram):
- latent = predict(model, conditions, cfg)
- finally:
- if low_vram:
- # Qwen stores RoPE tables as an ordinary list, outside Module buffers.
- model.dit.pos_embed.freqs = [value.cpu() for value in model.dit.pos_embed.freqs]
- release_device_cache(device)
- with stage_on_device(vae, device, enabled=low_vram), torch.inference_mode():
- decoded = decode_pair(vae, latent)
- return latent, decoded
-
-
-def output_maps(decoded, square, original, transform, restore_size):
- if decoded.shape[0:3] != (1, 2, 4) or not torch.isfinite(decoded).all():
- raise ValueError('invalid/nonfinite decoded material pair')
- arrays = np.clip(np.rint(decoded[0].detach().float().cpu().permute(0, 2, 3, 1).numpy() * 255), 0, 255).astype(np.uint8)
- diagnostics = [Image.fromarray(array[:, :, 3], 'L') for array in arrays]
- maps = []
- for modality, array in enumerate(arrays):
- color = Image.fromarray(array[:, :, :3], 'RGB')
- coverage = square.getchannel('A')
- if restore_size:
- x, y = transform['offset']; w, h = transform['content_size']
- color = color.crop((x, y, x + w, y + h))
- if color.size != original.size:
- color = color.resize(original.size, Image.Resampling.LANCZOS)
- coverage = original.getchannel('A') # exact original coverage, no second interpolation
- out = np.array(color.convert('RGBA'))
- out[:, :, 3] = np.array(coverage)
- if modality == 1:
- out[:, :, 2] = 255 # placeholder, not material opacity or AO prediction
- maps.append(Image.fromarray(out, 'RGBA'))
- return maps, diagnostics
-
-
-def main():
- parser = argparse.ArgumentParser(description=__doc__)
- parser.add_argument('--checkpoint', required=True, type=Path,
- help='Native adapter_model.safetensors with sibling adapter_config.json, or legacy PT with completion marker')
- parser.add_argument('--input', required=True, type=Path)
- parser.add_argument('--output-dir', required=True, type=Path)
- parser.add_argument('--model-root', type=Path, help='Explicit loading location only; checkpoint model reference retained')
- parser.add_argument('--conditions-cache', type=Path, help='Read RGB conditioning only; sibling JSON must bind input/model/resolution')
- parser.add_argument('--compare-cache', type=Path, help='Native path plus exact conditioning/latent/decoded comparison against cache')
- parser.add_argument('--comparison-atol', type=float, default=0.)
- parser.add_argument('--save-conditions', action='store_true')
- parser.add_argument('--restore-input-size', action='store_true')
- parser.add_argument('--save-predicted-coverage', action='store_true')
- parser.add_argument('--device', default='cuda:0')
- parser.add_argument('--low-vram', action='store_true', default=os.environ.get('SUPRATEX_LOW_VRAM') == '1',
- help='CPU stage/block offload; preserves weights and sampling, uses more CPU RAM and transfers (also SUPRATEX_LOW_VRAM=1)')
- args = parser.parse_args()
- if args.conditions_cache and args.compare_cache:
- parser.error('--conditions-cache and --compare-cache are mutually exclusive')
- if args.comparison_atol < 0:
- parser.error('--comparison-atol must be nonnegative')
- with span('qwen.checkpoint_load'):
- cfg, weights, step = load_checkpoint(args.checkpoint)
- os.environ['DIFFSYNTH_ATTENTION_IMPLEMENTATION'] = cfg['attention_backend']
- os.environ['HF_HUB_OFFLINE'] = '1'; os.environ['TRANSFORMERS_OFFLINE'] = '1'
- if cfg['deterministic_algorithms']:
- os.environ.setdefault('CUBLAS_WORKSPACE_CONFIG', ':4096:8')
- torch.use_deterministic_algorithms(cfg['deterministic_algorithms'])
- device = torch.device(args.device)
- torch.cuda.set_device(device)
- reference_root = cfg['model_root']
- model_root = Path(os.path.abspath(args.model_root or reference_root))
- # An explicit load-location override is an operator-supplied alias, never an
- # implicit identity rewrite or a digest-based equivalence claim.
- model_references = {reference_root, str(model_root)}
- original, square, transform = prepare_image(args.input, cfg['resolution'])
- if args.output_dir.exists():
- raise FileExistsError('choose a fresh inference output directory')
- args.output_dir.mkdir(parents=True)
- started = time.perf_counter()
- torch.cuda.reset_peak_memory_stats(device)
- with span('qwen.conditions'):
- if args.conditions_cache:
- verify_cache_input(args.conditions_cache, args.input, cfg['resolution'], model_references)
- conditions = load_conditions(args.conditions_cache, cfg['resolution'], device)
- from diffsynth.models.qwen_image_21_vae import QwenImage21VAE
- vae = explicit_load(QwenImage21VAE, checkpoint_component(model_root, 'vae'),
- 'cpu' if args.low_vram else device, torch.bfloat16)
- else:
- conditions, vae = native_conditions(square, model_root, cfg['resolution'], device, low_vram=args.low_vram)
- with span('qwen.transformer_load'):
- load_cfg = cfg | {'model_root': str(model_root)}
- model = build_model(load_cfg, 'cpu' if args.low_vram else device)
- with span('qwen.adapter_load'):
- strict_load_trainable(model, weights)
- del weights
- with span('qwen.predict_decode'):
- latent, decoded = predict_and_decode(model, vae, conditions, cfg, low_vram=args.low_vram)
- comparison = None
- if args.compare_cache:
- verify_cache_input(args.compare_cache, args.input, cfg['resolution'], model_references)
- cached = load_conditions(args.compare_cache, cfg['resolution'], device)
- differences = {key: float((conditions[key].float() - cached[key].float()).abs().max()) for key in conditions}
- other_latent, other_decoded = predict_and_decode(model, vae, cached, cfg, low_vram=args.low_vram)
- differences.update(latent=float((latent - other_latent).abs().max()), decoded=float((decoded - other_decoded).abs().max()))
- comparison = dict(max_abs=differences, atol=args.comparison_atol, passed=all(v <= args.comparison_atol for v in differences.values()))
- if not comparison['passed']:
- # Discriminate native/cache disagreement from repeated-kernel noise
- # while holding conditions, checkpoint and process constant.
- repeat_latent, repeat_decoded = predict_and_decode(model, vae, cached, cfg, low_vram=args.low_vram)
- comparison['cached_repeat_control_max_abs'] = dict(
- latent=float((other_latent-repeat_latent).abs().max()),
- decoded=float((other_decoded-repeat_decoded).abs().max()))
- (args.output_dir / 'comparison.json').write_text(json.dumps(comparison, indent=2) + '\n')
- if not comparison['passed']:
- raise ValueError('native/cache comparison exceeded requested tolerance')
- with span('qwen.output_write'):
- maps, diagnostics = output_maps(decoded, square, original, transform, args.restore_input_size)
- for label, image, diag in zip(('albedo', 'mra'), maps, diagnostics):
- image.save(args.output_dir / f'{label}.png')
- if args.save_predicted_coverage:
- diag.save(args.output_dir / f'{label}_predicted_coverage.png')
- if args.save_conditions:
- save_file({k: v.detach().cpu().contiguous() for k, v in conditions.items()}, str(args.output_dir / 'conditions.safetensors'))
- (args.output_dir / 'conditions.json').write_text(json.dumps(dict(input=input_binding(args.input), resolution=cfg['resolution'],
- model_root=reference_root, prompt=PROMPT, transform=transform), indent=2) + '\n')
- report = dict(status='complete', checkpoint=os.path.abspath(args.checkpoint), checkpoint_step=step,
- input=input_binding(args.input), loading_model_root=str(model_root), checkpoint_model_reference=reference_root,
- model_override_is_operator_reference_not_hash_verified=bool(args.model_root), config=cfg, transform=transform,
- accepted_model_reference_aliases=sorted(model_references),
- low_vram=args.low_vram,
- memory_policy='cpu_stages_and_sequential_transformer_blocks' if args.low_vram else 'resident_models',
- restore_input_size=args.restore_input_size, restoration='crop padding; LANCZOS color interpolation if size changed; exact original coverage' if args.restore_input_size else 'training square canvas',
- output_semantics='Albedo sRGB; MRA R=linear Metallic G=linear Roughness B=1 placeholder; A=input coverage',
- boundary='Opaque material estimator. Input alpha retained as coverage; no segmentation, material transparency or hidden-layer recovery inferred.',
- path='cached_conditioning_only' if args.conditions_cache else 'native_rgb_vae_and_per_image_vl', comparison=comparison,
- peak_gpu_allocated_bytes=torch.cuda.max_memory_allocated(device),
- peak_gpu_reserved_bytes=torch.cuda.max_memory_reserved(device),
- elapsed_seconds=time.perf_counter()-started)
- (args.output_dir / 'REPORT.json').write_text(json.dumps(report, indent=2) + '\n')
- print(json.dumps(dict(status='complete', output=str(args.output_dir), checkpoint_step=step)))
-
-
-if __name__ == '__main__':
- main()
+#!/usr/bin/env python3
+"""RGB/RGBA-only endpoint inference. No GT, data manifests, downloads or hashes."""
+from __future__ import annotations
+import argparse
+import gc
+import json
+import math
+import os
+from pathlib import Path
+import time
+
+import numpy as np
+from PIL import Image
+import torch
+from safetensors import safe_open
+from safetensors.torch import save_file
+
+from inference_utils import (
+ DEFAULTS, PROMPT, build_model, checkpoint_component, decode_pair,
+ endpoint_noise, explicit_load,
+)
+from inference_offload import (
+ release_device_cache, sequential_cpu_offload, stage_on_device,
+ text_encoder_offload_blocks,
+)
+
+
+def prepare_image(path, resolution):
+ if resolution < 32 or resolution % 32:
+ raise ValueError('resolution must be positive and divisible by 32')
+ with Image.open(path) as source:
+ had_alpha = 'A' in source.getbands() or 'transparency' in source.info
+ original = source.convert('RGBA')
+ width, height = original.size
+ scale = min(1., resolution / max(width, height))
+ content = (max(1, round(width * scale)), max(1, round(height * scale)))
+ aligned = tuple(math.ceil(value / 32) * 32 for value in content)
+ offset = tuple((aligned[i] - content[i]) // 2 + (resolution - aligned[i]) // 2 for i in range(2))
+ resized = original.resize(content, Image.Resampling.LANCZOS) if original.size != content else original.copy()
+ square = Image.new('RGBA', (resolution, resolution), (0, 0, 0, 0))
+ square.paste(resized, offset)
+ transform = dict(source_size=[width, height], content_size=list(content), padded_size=[resolution, resolution],
+ offset=list(offset), scale=scale, input_had_alpha=had_alpha,
+ resampling='LANCZOS, two-stage centered padding, no crop or alpha compositing',
+ coverage='input alpha retained' if had_alpha else 'all-one source coverage; no segmentation inferred')
+ return original, square, transform
+
+
+def load_checkpoint(path):
+ path = Path(path)
+ if path.suffix == '.safetensors':
+ from safetensors import safe_open
+ import math
+
+ adapter = json.loads(path.with_name('adapter_config.json').read_text())
+ if (not isinstance(adapter, dict) or adapter.get('format') != 'supratex_qwen_material_adapter'
+ or type(adapter.get('schema_version')) is not int or adapter['schema_version'] != 1
+ or adapter.get('adapter_type') != 'qwen_image_21_pbr_endpoint'
+ or adapter.get('base_model_name_or_path') != 'Qwen/Qwen-Image-2.1'
+ or adapter.get('weight_file') != path.name
+ or type(adapter.get('source_step')) is not int or adapter['source_step'] != 100000):
+ raise ValueError('unsupported Qwen material adapter config')
+ inference = adapter.get('inference_config')
+ booleans = {'lora_output_projection', 'omit_all_valid_mask', 'bf16_autocast', 'deterministic_algorithms'}
+ required = booleans | {'resolution', 'noise_seed', 'lora_rank', 'lora_alpha', 'attention_backend'}
+ if not isinstance(inference, dict) or set(inference) != required:
+ raise ValueError('Qwen adapter requires complete inference config')
+ if (type(inference['resolution']) is not int or inference['resolution'] != 1024
+ or type(inference['noise_seed']) is not int or inference['noise_seed'] != 42
+ or inference['attention_backend'] != 'torch'
+ or type(inference['lora_rank']) is not int or inference['lora_rank'] <= 0
+ or type(inference['lora_alpha']) not in {int, float}
+ or not math.isfinite(inference['lora_alpha']) or inference['lora_alpha'] <= 0
+ or any(type(inference[key]) is not bool for key in booleans)):
+ raise ValueError('invalid Qwen adapter inference config')
+ tensors = adapter.get('tensors')
+ if not isinstance(tensors, dict) or not tensors:
+ raise ValueError('Qwen adapter requires a nonempty tensor manifest')
+ with safe_open(str(path), framework='pt', device='cpu') as reader:
+ metadata = reader.metadata() or {}
+ if (metadata.get('format') != 'pt' or metadata.get('adapter_format') != adapter['format']
+ or metadata.get('schema_version') != str(adapter['schema_version'])
+ or set(reader.keys()) != set(tensors)):
+ raise ValueError('Qwen adapter tensor keys or metadata mismatch')
+ for name, specification in tensors.items():
+ header = reader.get_slice(name)
+ if (not isinstance(specification, dict)
+ or not isinstance(specification.get('shape'), list)
+ or any(type(size) is not int or size < 0 for size in specification['shape'])
+ or specification['shape'] != header.get_shape()
+ or specification.get('dtype') not in {'F64', 'F32', 'F16', 'BF16'}
+ or specification['dtype'] != header.get_dtype()):
+ raise ValueError(f'Qwen adapter tensor shape or dtype mismatch: {name}')
+ weights = {name: reader.get_tensor(name) for name in reader.keys()}
+ config = DEFAULTS | inference | {'model_root': adapter['base_model_name_or_path']}
+ return config, weights, adapter['source_step']
+ marker = json.loads(path.with_suffix('.complete.json').read_text())
+ if marker.get('status') != 'complete' or marker.get('bytes') != path.stat().st_size:
+ raise ValueError('checkpoint completion record is absent or mismatched')
+ # SupraTex adaptation: public inference weights use restricted loading.
+ # Legacy trainer checkpoints also contain NumPy uint32 RNG arrays; these
+ # exact reconstruction types retain compatibility without arbitrary pickle.
+ from numpy.core.multiarray import _reconstruct
+ with torch.serialization.safe_globals([_reconstruct, (_reconstruct, 'numpy.core.multiarray._reconstruct'),
+ (_reconstruct, 'numpy._core.multiarray._reconstruct'),
+ np.ndarray, np.dtype, type(np.dtype('uint32'))]):
+ state = torch.load(path, map_location='cpu', weights_only=True)
+ if state.get('format') != 'qwen_pbr_endpoint_v1' or marker.get('step') != state.get('step'):
+ raise ValueError('unsupported checkpoint or inconsistent step')
+ if not isinstance(state.get('config'), dict) or not isinstance(state.get('model'), dict):
+ raise ValueError('checkpoint needs complete config and trainable state')
+ config = DEFAULTS | state['config']
+ for key in ('model_root', 'resolution', 'lora_rank', 'lora_alpha', 'noise_seed'):
+ if key not in state['config']:
+ raise ValueError(f'checkpoint config lacks {key}')
+ return config, state['model'], state['step']
+
+
+def strict_load_adapter(model, weights):
+ expected = {name: parameter for name, parameter in model.named_parameters() if parameter.requires_grad}
+ if expected.keys() != weights.keys():
+ raise ValueError('checkpoint adapter key set differs from constructed model')
+ with torch.no_grad():
+ for name, parameter in expected.items():
+ tensor = weights[name]
+ if tensor.shape != parameter.shape or tensor.dtype != parameter.dtype or not torch.isfinite(tensor).all():
+ raise ValueError(f'checkpoint tensor mismatch/nonfinite: {name}')
+ parameter.copy_(tensor)
+ model.requires_grad_(False).eval()
+
+
+def canonical_conditions(values, resolution):
+ """Only retain consumed conditioning; material targets are never loaded."""
+ values = dict(values)
+ layout = values['edit_image_pad_mask'].bool()
+ prompt = values.get('prompt_embeds')
+ if prompt is None:
+ text = values['prompt_text_embeds']
+ if layout.ndim != 2 or layout.shape[0] != 1 or text.shape[1] != int((~layout[0]).sum()):
+ raise ValueError('compact conditioning layout mismatch')
+ prompt = text.new_zeros((1, layout.shape[1], text.shape[-1]))
+ prompt[:, ~layout[0]] = text
+ if prompt.ndim != 3 or prompt.shape[0] != 1 or prompt.shape[2] != 4096 or layout.shape != prompt.shape[:2]:
+ raise ValueError('invalid prompt/layout shape')
+ # Model replaces image-placeholder rows from RGB VAE. Canonical zeros allow
+ # exact native-v2 comparison without changing consumed values.
+ prompt = prompt.clone()
+ prompt[:, layout[0]] = 0
+ mask = values.get('prompt_embeds_mask')
+ mask = torch.ones_like(layout) if mask is None else mask.bool()
+ rgb = values['rgb']
+ if rgb.shape != (1, 64, resolution // 16, resolution // 16) or mask.shape != layout.shape:
+ raise ValueError('invalid RGB latent/mask shape')
+ result = dict(rgb=rgb, prompt_embeds=prompt, prompt_embeds_mask=mask, edit_image_pad_mask=layout)
+ if any(value.is_floating_point() and not torch.isfinite(value).all() for value in result.values()):
+ raise ValueError('nonfinite conditioning')
+ return result
+
+
+def load_conditions(path, resolution, device):
+ values = {}
+ keys = {'rgb', 'prompt_embeds', 'prompt_text_embeds', 'prompt_embeds_mask', 'edit_image_pad_mask'}
+ with safe_open(str(path), framework='pt', device=str(device)) as reader:
+ for key in keys & set(reader.keys()):
+ values[key] = reader.get_tensor(key)
+ return canonical_conditions(values, resolution)
+
+
+def input_binding(path):
+ path = Path(os.path.abspath(path)); stat = path.stat()
+ return dict(path=str(path), size=stat.st_size, mtime_ns=stat.st_mtime_ns)
+
+
+def verify_cache_input(cache, image, resolution, model_references):
+ metadata = json.loads(Path(cache).with_suffix('.json').read_text())
+ binding = metadata.get('input', metadata.get('sources', {}).get('rgb'))
+ if binding != input_binding(image) or metadata.get('resolution') != resolution:
+ raise ValueError('cache conditioning is not bound to this exact input/stat/resolution')
+ if metadata.get('model_root') not in model_references or metadata.get('prompt') != PROMPT:
+ raise ValueError('cached model reference or prompt differs from checkpoint inference contract')
+ return metadata['model_root']
+
+
+def native_conditions(square, model_root, resolution, device, low_vram=False):
+ from transformers import AutoProcessor
+ from diffsynth.models.qwen_image_21_vae import QwenImage21VAE
+ from diffsynth.models.qwen_image_21_text_encoder import QwenImage21TextEncoder
+ from diffsynth.pipelines.qwen_image_21 import QwenImage21Pipeline, QwenImage21Unit_EditImageEmbedder, QwenImage21Unit_PromptEmbedder
+ from diffsynth.utils.state_dict_converters.qwen_image_21_text_encoder import QwenImage21TextEncoderStateDictConverter
+ processor = AutoProcessor.from_pretrained(str(model_root / 'processor'), local_files_only=True)
+ processor.image_processor.size = {'shortest_edge': resolution ** 2, 'longest_edge': resolution ** 2}
+ pipe = QwenImage21Pipeline(device=device, torch_dtype=torch.bfloat16)
+ pipe.processor = processor
+ loading_device = 'cpu' if low_vram else device
+ vae = explicit_load(QwenImage21VAE, checkpoint_component(model_root, 'vae'), loading_device, torch.bfloat16)
+ pipe.vae = vae
+ with stage_on_device(vae, device, enabled=low_vram), torch.inference_mode():
+ edited = QwenImage21Unit_EditImageEmbedder().process(pipe, edit_image=[square], height=resolution, width=resolution,
+ tiled=False, tile_size=256, tile_stride=192)
+ if edited['edit_image'][0].size != square.size:
+ raise ValueError('native edit path changed dimensions')
+ rgb = edited['edit_latents'][0]
+ encoder = explicit_load(QwenImage21TextEncoder, checkpoint_component(model_root, 'text_encoder'), loading_device,
+ torch.bfloat16, QwenImage21TextEncoderStateDictConverter)
+ pipe.text_encoder = encoder
+ blocks = text_encoder_offload_blocks(encoder) if low_vram else ()
+ with sequential_cpu_offload(encoder, blocks, device, enabled=low_vram), torch.inference_mode():
+ values = QwenImage21Unit_PromptEmbedder().process(pipe, prompt=PROMPT, edit_image=[square])
+ conditions = canonical_conditions(dict(values, rgb=rgb), resolution)
+ pipe.text_encoder = None
+ del encoder, pipe, values, edited, blocks
+ gc.collect(); torch.cuda.empty_cache()
+ return conditions, vae
+
+
+def predict(model, conditions, cfg):
+ # Shape comes from observed RGB, never a GT or fabricated target tensor.
+ shape = (conditions['rgb'].shape[0], 2, *conditions['rgb'].shape[1:])
+ shape_carrier = conditions['rgb'].new_empty(shape)
+ noise = endpoint_noise(shape_carrier, cfg['noise_seed'])
+ mask = conditions['prompt_embeds_mask']
+ if cfg['omit_all_valid_mask'] and bool(mask.all()):
+ mask = None
+ with torch.inference_mode(), torch.autocast('cuda', dtype=torch.bfloat16, enabled=cfg['bf16_autocast']):
+ velocity = model(latents=noise, timestep=torch.full((noise.shape[0],), 1000., device=noise.device),
+ prompt_embeds=conditions['prompt_embeds'], prompt_embeds_mask=mask,
+ edit_image_pad_mask=conditions['edit_image_pad_mask'], edit_latents=[conditions['rgb']],
+ use_gradient_checkpointing=False, use_flex_attention=False, kv_cache=None)
+ prediction = noise.float() - velocity.float()
+ if not torch.isfinite(prediction).all():
+ raise FloatingPointError('nonfinite material latent prediction')
+ return prediction
+
+
+def predict_and_decode(model, vae, conditions, cfg, low_vram=False):
+ device = conditions['rgb'].device
+ try:
+ with sequential_cpu_offload(model, model.dit.transformer_blocks, device, enabled=low_vram):
+ latent = predict(model, conditions, cfg)
+ finally:
+ if low_vram:
+ # Qwen stores RoPE tables as an ordinary list, outside Module buffers.
+ model.dit.pos_embed.freqs = [value.cpu() for value in model.dit.pos_embed.freqs]
+ release_device_cache(device)
+ with stage_on_device(vae, device, enabled=low_vram), torch.inference_mode():
+ decoded = decode_pair(vae, latent)
+ return latent, decoded
+
+
+def output_maps(decoded, square, original, transform, restore_size):
+ if decoded.shape[0:3] != (1, 2, 4) or not torch.isfinite(decoded).all():
+ raise ValueError('invalid/nonfinite decoded material pair')
+ arrays = np.clip(np.rint(decoded[0].detach().float().cpu().permute(0, 2, 3, 1).numpy() * 255), 0, 255).astype(np.uint8)
+ diagnostics = [Image.fromarray(array[:, :, 3], 'L') for array in arrays]
+ maps = []
+ for modality, array in enumerate(arrays):
+ color = Image.fromarray(array[:, :, :3], 'RGB')
+ coverage = square.getchannel('A')
+ if restore_size:
+ x, y = transform['offset']; w, h = transform['content_size']
+ color = color.crop((x, y, x + w, y + h))
+ if color.size != original.size:
+ color = color.resize(original.size, Image.Resampling.LANCZOS)
+ coverage = original.getchannel('A') # exact original coverage, no second interpolation
+ out = np.array(color.convert('RGBA'))
+ out[:, :, 3] = np.array(coverage)
+ if modality == 1:
+ out[:, :, 2] = 255 # placeholder, not material opacity or AO prediction
+ maps.append(Image.fromarray(out, 'RGBA'))
+ return maps, diagnostics
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument('--checkpoint', required=True, type=Path,
+ help='Native adapter_model.safetensors with sibling adapter_config.json, or legacy PT with completion marker')
+ parser.add_argument('--input', required=True, type=Path)
+ parser.add_argument('--output-dir', required=True, type=Path)
+ parser.add_argument('--model-root', type=Path, help='Explicit loading location only; checkpoint model reference retained')
+ parser.add_argument('--conditions-cache', type=Path, help='Read RGB conditioning only; sibling JSON must bind input/model/resolution')
+ parser.add_argument('--compare-cache', type=Path, help='Native path plus exact conditioning/latent/decoded comparison against cache')
+ parser.add_argument('--comparison-atol', type=float, default=0.)
+ parser.add_argument('--save-conditions', action='store_true')
+ parser.add_argument('--restore-input-size', action='store_true')
+ parser.add_argument('--save-predicted-coverage', action='store_true')
+ parser.add_argument('--device', default='cuda:0')
+ parser.add_argument('--low-vram', action='store_true', default=os.environ.get('SUPRATEX_LOW_VRAM') == '1',
+ help='CPU stage/block offload; preserves weights and sampling, uses more CPU RAM and transfers (also SUPRATEX_LOW_VRAM=1)')
+ args = parser.parse_args()
+ if args.conditions_cache and args.compare_cache:
+ parser.error('--conditions-cache and --compare-cache are mutually exclusive')
+ if args.comparison_atol < 0:
+ parser.error('--comparison-atol must be nonnegative')
+ cfg, weights, step = load_checkpoint(args.checkpoint)
+ os.environ['DIFFSYNTH_ATTENTION_IMPLEMENTATION'] = cfg['attention_backend']
+ os.environ['HF_HUB_OFFLINE'] = '1'; os.environ['TRANSFORMERS_OFFLINE'] = '1'
+ if cfg['deterministic_algorithms']:
+ os.environ.setdefault('CUBLAS_WORKSPACE_CONFIG', ':4096:8')
+ torch.use_deterministic_algorithms(cfg['deterministic_algorithms'])
+ device = torch.device(args.device)
+ torch.cuda.set_device(device)
+ reference_root = cfg['model_root']
+ model_root = Path(os.path.abspath(args.model_root or reference_root))
+ # An explicit load-location override is an operator-supplied alias, never an
+ # implicit identity rewrite or a digest-based equivalence claim.
+ model_references = {reference_root, str(model_root)}
+ original, square, transform = prepare_image(args.input, cfg['resolution'])
+ if args.output_dir.exists():
+ raise FileExistsError('choose a fresh inference output directory')
+ args.output_dir.mkdir(parents=True)
+ started = time.perf_counter()
+ torch.cuda.reset_peak_memory_stats(device)
+ if args.conditions_cache:
+ verify_cache_input(args.conditions_cache, args.input, cfg['resolution'], model_references)
+ conditions = load_conditions(args.conditions_cache, cfg['resolution'], device)
+ from diffsynth.models.qwen_image_21_vae import QwenImage21VAE
+ vae = explicit_load(QwenImage21VAE, checkpoint_component(model_root, 'vae'),
+ 'cpu' if args.low_vram else device, torch.bfloat16)
+ else:
+ conditions, vae = native_conditions(square, model_root, cfg['resolution'], device, low_vram=args.low_vram)
+ load_cfg = cfg | {'model_root': str(model_root)}
+ model = build_model(load_cfg, 'cpu' if args.low_vram else device)
+ strict_load_adapter(model, weights)
+ del weights
+ latent, decoded = predict_and_decode(model, vae, conditions, cfg, low_vram=args.low_vram)
+ comparison = None
+ if args.compare_cache:
+ verify_cache_input(args.compare_cache, args.input, cfg['resolution'], model_references)
+ cached = load_conditions(args.compare_cache, cfg['resolution'], device)
+ differences = {key: float((conditions[key].float() - cached[key].float()).abs().max()) for key in conditions}
+ other_latent, other_decoded = predict_and_decode(model, vae, cached, cfg, low_vram=args.low_vram)
+ differences.update(latent=float((latent - other_latent).abs().max()), decoded=float((decoded - other_decoded).abs().max()))
+ comparison = dict(max_abs=differences, atol=args.comparison_atol, passed=all(v <= args.comparison_atol for v in differences.values()))
+ if not comparison['passed']:
+ # Discriminate native/cache disagreement from repeated-kernel noise
+ # while holding conditions, checkpoint and process constant.
+ repeat_latent, repeat_decoded = predict_and_decode(model, vae, cached, cfg, low_vram=args.low_vram)
+ comparison['cached_repeat_control_max_abs'] = dict(
+ latent=float((other_latent-repeat_latent).abs().max()),
+ decoded=float((other_decoded-repeat_decoded).abs().max()))
+ (args.output_dir / 'comparison.json').write_text(json.dumps(comparison, indent=2) + '\n')
+ if not comparison['passed']:
+ raise ValueError('native/cache comparison exceeded requested tolerance')
+ maps, diagnostics = output_maps(decoded, square, original, transform, args.restore_input_size)
+ for label, image, diag in zip(('albedo', 'mra'), maps, diagnostics):
+ image.save(args.output_dir / f'{label}.png')
+ if args.save_predicted_coverage:
+ diag.save(args.output_dir / f'{label}_predicted_coverage.png')
+ if args.save_conditions:
+ save_file({k: v.detach().cpu().contiguous() for k, v in conditions.items()}, str(args.output_dir / 'conditions.safetensors'))
+ (args.output_dir / 'conditions.json').write_text(json.dumps(dict(input=input_binding(args.input), resolution=cfg['resolution'],
+ model_root=reference_root, prompt=PROMPT, transform=transform), indent=2) + '\n')
+ report = dict(status='complete', checkpoint=os.path.abspath(args.checkpoint), checkpoint_step=step,
+ input=input_binding(args.input), loading_model_root=str(model_root), checkpoint_model_reference=reference_root,
+ model_override_is_operator_reference_not_hash_verified=bool(args.model_root), config=cfg, transform=transform,
+ accepted_model_reference_aliases=sorted(model_references),
+ low_vram=args.low_vram,
+ memory_policy='cpu_stages_and_sequential_transformer_blocks' if args.low_vram else 'resident_models',
+ restore_input_size=args.restore_input_size, restoration='crop padding; LANCZOS color interpolation if size changed; exact original coverage' if args.restore_input_size else 'training square canvas',
+ output_semantics='Albedo sRGB; MRA R=linear Metallic G=linear Roughness B=1 placeholder; A=input coverage',
+ boundary='Opaque material estimator. Input alpha retained as coverage; no segmentation, material transparency or hidden-layer recovery inferred.',
+ path='cached_conditioning_only' if args.conditions_cache else 'native_rgb_vae_and_per_image_vl', comparison=comparison,
+ peak_gpu_allocated_bytes=torch.cuda.max_memory_allocated(device),
+ peak_gpu_reserved_bytes=torch.cuda.max_memory_reserved(device),
+ elapsed_seconds=time.perf_counter()-started)
+ (args.output_dir / 'REPORT.json').write_text(json.dumps(report, indent=2) + '\n')
+ print(json.dumps(dict(status='complete', output=str(args.output_dir), checkpoint_step=step)))
+
+
+if __name__ == '__main__':
+ main()
diff --git a/deployment/runtime/qwen/examples/qwen_image_21/pbr/inference_utils.py b/deployment/runtime/qwen/examples/qwen_image_21/pbr/inference_utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..124771bddb9a681513aa230c6cf1ef5b1def24f6
--- /dev/null
+++ b/deployment/runtime/qwen/examples/qwen_image_21/pbr/inference_utils.py
@@ -0,0 +1,135 @@
+"""Model construction, local checkpoint loading, and decoding for PBR inference."""
+from __future__ import annotations
+
+import inspect
+import json
+from pathlib import Path
+import time
+
+import torch
+from safetensors import safe_open
+from safetensors.torch import load_file
+
+
+# Only settings consumed by inference; legacy checkpoint fields are not defaults.
+DEFAULTS = dict(
+ lora_rank=32, lora_alpha=32, noise_seed=42, resolution=512,
+ omit_all_valid_mask=False, bf16_autocast=False,
+ attention_backend='torch', deterministic_algorithms=False,
+ lora_output_projection=False,
+)
+
+
+PROMPT = ("Generate aligned albedo and metallic/roughness material maps for the object in the input image. "
+ "Pack MRA with red=metallic, green=roughness, and blue=constant one placeholder. "
+ "No observed ambient occlusion is available.")
+
+
+def log(message):
+ print(f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] {message}", flush=True)
+
+
+def read_json(path):
+ if not path.is_file():
+ raise FileNotFoundError(f"Required local file is absent: {path}")
+ return json.loads(path.read_text(encoding="utf-8"))
+
+
+def checkpoint_component(root, component):
+ """Resolve an explicit component and verify every indexed shard before GPU work."""
+ directory = root / component
+ config = read_json(directory / "config.json")
+ if not isinstance(config, dict):
+ raise ValueError(f"Expected an object config: {directory / 'config.json'}")
+ indexes = sorted(directory.glob("*.safetensors.index.json"))
+ if len(indexes) > 1:
+ raise ValueError(f"Ambiguous safetensors indexes under {directory}")
+ if indexes:
+ index = read_json(indexes[0])
+ weight_map = index.get("weight_map")
+ if not isinstance(weight_map, dict) or not weight_map:
+ raise ValueError(f"Invalid weight_map: {indexes[0]}")
+ names = sorted(set(weight_map.values()))
+ if any(Path(name).is_absolute() or ".." in Path(name).parts for name in names):
+ raise ValueError(f"Indexed shard path leaves component directory: {indexes[0]}")
+ files = [directory / name for name in names]
+ else:
+ pattern = "model*.safetensors" if component == "text_encoder" else "diffusion_pytorch_model*.safetensors"
+ files = sorted(directory.glob(pattern))
+ if not files:
+ raise FileNotFoundError(f"No local safetensors checkpoint found under {directory}")
+ for filename in files:
+ if not filename.is_file() or filename.stat().st_size == 0:
+ raise FileNotFoundError(f"Missing or empty checkpoint shard: {filename}")
+ keys = set()
+ for filename in files:
+ with safe_open(str(filename), framework="pt", device="cpu") as handle:
+ shard_keys = set(handle.keys())
+ duplicates = keys & shard_keys
+ if duplicates:
+ raise ValueError(f"Duplicate state keys between shards: {sorted(duplicates)[:5]}")
+ keys.update(shard_keys)
+ if indexes and keys != set(weight_map):
+ raise ValueError(f"Index and shard state keys disagree under {directory}")
+ return {"directory": directory, "config": config, "files": files, "tensor_count": len(keys)}
+
+
+def explicit_load(model_class, component, device, dtype, converter=None):
+ """Instantiate the named class and strictly assign the local tensor state."""
+ from diffsynth.core.vram.initialization import skip_model_initialization
+
+ constructor = inspect.signature(model_class.__init__).parameters
+ config = {key: value for key, value in component["config"].items() if key in constructor and key != "self"}
+ log(f"Loading {model_class.__name__} explicitly ({len(component['files'])} shards, device={device}, dtype={dtype})")
+ started = time.perf_counter()
+ # Set the default dtype to preserve generated float32 text-encoder rotary buffers;
+ # checkpoint parameters are assigned below and then cast explicitly.
+ with skip_model_initialization():
+ model = model_class(**config)
+ state = {}
+ for filename in component["files"]:
+ state.update(load_file(str(filename), device="cpu"))
+ if converter is not None:
+ state = converter(state)
+ model.load_state_dict(state, strict=True, assign=True)
+ del state
+ model = model.to(device=device, dtype=dtype).eval()
+ if any(parameter.is_meta for parameter in model.parameters()):
+ raise RuntimeError(f"{model_class.__name__}: parameters remain on meta after strict checkpoint load")
+ model.requires_grad_(False)
+ log(f"Loaded {model_class.__name__} in {time.perf_counter() - started:.2f}s")
+ return model
+
+
+def endpoint_noise(target, seed):
+ # Draw the fixed endpoint field on CPU before moving it to the model device.
+ noise = torch.randn(target.shape, generator=torch.Generator(device='cpu').manual_seed(seed), dtype=torch.float32)
+ return noise.to(device=target.device, dtype=target.dtype)
+
+
+def build_model(cfg, device):
+ from peft import LoraConfig, inject_adapter_in_model
+ from diffsynth.models.qwen_image_21_dit import QwenImage21DiT
+ from diffsynth.pipelines.qwen_image_21_pbr import QwenImage21PBRModel
+ dit = explicit_load(QwenImage21DiT, checkpoint_component(Path(cfg['model_root']), 'transformer'), device, torch.bfloat16)
+ if len(dit.transformer_blocks) != 32:
+ raise ValueError('Expected complete pretrained 32-layer backbone')
+ lora = LoraConfig(r=cfg['lora_rank'], lora_alpha=cfg['lora_alpha'], lora_dropout=0., bias='none',
+ target_modules=['attn.to_q', 'attn.to_k', 'attn.to_v', 'attn.to_out.0']
+ + (['proj_out'] if cfg['lora_output_projection'] else []))
+ dit = inject_adapter_in_model(lora, dit)
+ model = QwenImage21PBRModel(dit).to(device)
+ for parameter in model.parameters():
+ if parameter.requires_grad:
+ parameter.data = parameter.data.float()
+ return model
+
+
+def decode_pair(vae, latents, clamp_output=True):
+ """Decode the albedo and MRA latent maps in their fixed modality order."""
+ decoded = []
+ for index in range(2):
+ z = latents[:, index].to(dtype=torch.bfloat16)
+ value = vae.decode(z, tiled=False, clamp_output=clamp_output)
+ decoded.append((value.float() + 1.) / 2.)
+ return torch.stack(decoded, dim=1)
diff --git a/deployment/runtime/qwen/requirements.txt b/deployment/runtime/qwen/requirements.txt
index 2f92050dbb9e556b22a49257a8946f3edd3deea0..d00d28fd4e1be7b58965fe4904d77eb54624c9d8 100644
--- a/deployment/runtime/qwen/requirements.txt
+++ b/deployment/runtime/qwen/requirements.txt
@@ -1,7 +1,6 @@
-transformers==4.57.3
-diffusers==0.37.1
-accelerate==1.13.0
-peft==0.19.1
-ftfy==6.3.1
-pandas==2.3.3
-modelscope==1.34.0
+transformers==4.57.3
+diffusers==0.37.1
+accelerate==1.13.0
+peft==0.19.1
+ftfy==6.3.1
+modelscope==1.34.0
diff --git a/deployment/runtime/requirements-python.txt b/deployment/runtime/requirements-python.txt
index b600c2fef26c6500d82b23d7f4f9934c56573d2d..25674b7e36b0e5c4da8e7477c6e841c2c2d6c6c1 100644
--- a/deployment/runtime/requirements-python.txt
+++ b/deployment/runtime/requirements-python.txt
@@ -1,31 +1,29 @@
-# Recorded from the working Python 3.10 / PyTorch 2.6.0 CUDA 12.4 environment.
-# Install torch/torchvision and compiled extensions separately; see DEPENDENCIES.md.
-numpy==2.2.6
-Pillow==12.0.0
-scipy==1.15.3
-easydict==1.13
-tqdm==4.67.1
-einops==0.8.2
-safetensors==0.7.0
-huggingface-hub==0.36.2
-trimesh==4.10.1
-opencv-python-headless==4.13.0.92
-imageio==2.37.2
-imageio-ffmpeg==0.6.0
-numba==0.65.1
-transformers==4.57.3
-diffusers==0.37.1
-accelerate==1.13.0
-requests==2.34.2
-timm==1.0.22
-kornia==0.8.2
-plyfile==1.1.3
-zstandard==0.25.0
-ninja==1.13.0
-pandas==2.3.3
-peft==0.19.1
-ftfy==6.3.1
-lpips==0.1.4
-tensorboard==2.20.0
-av==17.1.0
-modelscope==1.34.0
+# Recorded from the working Python 3.10 / PyTorch 2.6.0 CUDA 12.4 environment.
+# Install torch/torchvision and compiled extensions separately; see DEPENDENCIES.md.
+numpy==2.2.6
+Pillow==12.0.0
+scipy==1.15.3
+easydict==1.13
+tqdm==4.67.1
+einops==0.8.2
+safetensors==0.7.0
+huggingface-hub==0.36.2
+trimesh==4.10.1
+opencv-python-headless==4.13.0.92
+imageio==2.37.2
+imageio-ffmpeg==0.6.0
+numba==0.65.1
+transformers==4.57.3
+diffusers==0.37.1
+accelerate==1.13.0
+requests==2.34.2
+timm==1.0.22
+kornia==0.8.2
+plyfile==1.1.3
+zstandard==0.25.0
+ninja==1.13.0
+pandas==2.3.3
+peft==0.19.1
+ftfy==6.3.1
+av==17.1.0
+modelscope==1.34.0
diff --git a/hf_space/ovoxel_results.py b/hf_space/ovoxel_results.py
index 00d94f87b2c53c571533cd41efd5732505946956..7209ac2daf3bd0da61e63f163a501ec352a7bb05 100644
--- a/hf_space/ovoxel_results.py
+++ b/hf_space/ovoxel_results.py
@@ -1,117 +1,117 @@
-"""Persist final o-voxels and their rendered views in the mounted data Bucket."""
-import json
-from pathlib import Path
-import re
-import shutil
-from html import escape
-from urllib.parse import quote
-
-PREVIEW_MODES = (
- ('normal', 'Normal', 'normal.png'),
- ('clay', 'Clay', 'clay.png'),
- ('base_color', 'Base color', 'basecolor.png'),
- ('shaded_studio', 'HDRI Studio', 'hdri_studio.png'),
- ('shaded_forest', 'HDRI Forest', 'hdri_forest.png'),
- ('shaded_sunset', 'HDRI Sunset', 'hdri_sunset.png'),
- ('shaded_courtyard', 'HDRI Courtyard', 'hdri_courtyard.png'),
- ('shaded', 'Studio', 'hdri_studio.png'), # Previously saved previews.
-)
-
-ROOT = Path('/data/generated/ovoxel-results-v1')
-
-
-def directory(identifier):
- if not isinstance(identifier, str) or re.fullmatch(r'[0-9a-f]{32}', identifier) is None:
- raise ValueError('Invalid generated asset identifier')
- return ROOT/identifier
-
-
-def persist(source, report):
- source = Path(source)
- identifier = source.parent.name
- destination = directory(identifier)
- destination.mkdir(parents=True, exist_ok=True)
- for name in ('asset.npz','input.png','report.json','preview.json','timings.jsonl'):
- shutil.copyfile(source.parent/name, destination/name)
- shutil.copytree(source.parent/'views', destination/'views', dirs_exist_ok=True)
- (destination/'ready.json').write_text(json.dumps({'complete':True,'identifier':identifier,
- 'project_revision':report['project_revision'],'output_kind':'final_ovoxel'}))
- return identifier
-
-
-def preview_html(identifier):
- root = directory(identifier)
- metadata = json.loads((root/'preview.json').read_text())
- default = metadata.get('default_mode', 'shaded' if 'shaded' in metadata['modes'] else 'shaded_forest')
- if default not in metadata['modes']:
- default = metadata['modes'][0]
- images = []
- for mode in metadata['modes']:
- for view in range(metadata['views']):
- url = '/gradio_api/file='+quote(str(root/'views'/f'{mode}-{view:02d}.webp'),safe='/')
- hidden = '' if mode == default and view == 0 else ' hidden'
- images.append(f'
')
- buttons = []
- for mode, label, icon in PREVIEW_MODES:
- if mode not in metadata['modes']:
- continue
- # Inline the tiny swatches; preview frames remain individually cached files.
- import base64
- icon_path = Path(__file__).resolve().parents[1]/'assets/preview-lighting'/icon
- swatch = base64.b64encode(icon_path.read_bytes()).decode('ascii')
- active = mode == default
- buttons.append(f'')
- return (f'
'
- '
'+''.join(images)+'
'
- '
'+''.join(buttons)+'
'
- f'
')
-
-
-def extract(identifier, extraction_token=None):
- from hf_space import extraction_progress as progress
- from hf_space import generation_progress
- from hf_space.timing import observe_stages
- token = extraction_token if generation_progress.valid(extraction_token) else progress.start()
- try:
- progress.stage(token, 'load')
- with observe_stages(lambda name: progress.stage(token, name)):
- output = _extract(identifier, token)
- progress.finish(token)
- return output
- except Exception:
- progress.finish(token, failed=True)
- raise
-
-
-def _extract(identifier, token):
- """Runs only inside the explicitly requested GPU extraction callback."""
- root = directory(identifier)
- if not (root/'ready.json').is_file():
- raise ValueError('The generated o-voxel has not finished saving')
- output = root/'asset.glb'
- if output.is_file():
- return str(output)
- import numpy as np
- import torch
- from supratex.representations import MeshWithVoxel
- from supratex.inference.pipeline import _export, LAYOUT, _runtime
- if not torch.cuda.is_available():
- raise RuntimeError('GLB extraction requires CUDA')
- with np.load(root/'asset.npz',allow_pickle=False) as state:
- resolution = int(state['resolution'])
- representation = MeshWithVoxel(torch.from_numpy(state['vertices']).cuda(),
- torch.from_numpy(state['faces']).cuda(),state['origin'].tolist(),float(state['voxel_size']),
- torch.from_numpy(state['coords']).cuda(),torch.from_numpy(state['attrs']).cuda(),
- torch.Size([1,6,resolution,resolution,resolution]),layout=LAYOUT)
- import time
- started = time.monotonic()
- _export(output, representation,4096,500000,'png',_runtime())
- from hf_space import extraction_progress
- extraction_progress.stage(token, 'validate')
- from hf_space.backend import validate_glb
- metadata = validate_glb(output)
- (root/'extraction.json').write_text(json.dumps({'status':'complete','device':'cuda',
- 'elapsed_seconds':time.monotonic()-started,**metadata},indent=2))
- return str(output)
+"""Persist final o-voxels and their rendered views in the mounted data Bucket."""
+import json
+from pathlib import Path
+import re
+import shutil
+from html import escape
+from urllib.parse import quote
+
+PREVIEW_MODES = (
+ ('normal', 'Normal', 'normal.png'),
+ ('clay', 'Clay', 'clay.png'),
+ ('base_color', 'Base color', 'basecolor.png'),
+ ('shaded_studio', 'HDRI Studio', 'hdri_studio.png'),
+ ('shaded_forest', 'HDRI Forest', 'hdri_forest.png'),
+ ('shaded_sunset', 'HDRI Sunset', 'hdri_sunset.png'),
+ ('shaded_courtyard', 'HDRI Courtyard', 'hdri_courtyard.png'),
+ ('shaded', 'Studio', 'hdri_studio.png'), # Previously saved previews.
+)
+
+ROOT = Path('/data/generated/ovoxel-results-v1')
+
+
+def directory(identifier):
+ if not isinstance(identifier, str) or re.fullmatch(r'[0-9a-f]{32}', identifier) is None:
+ raise ValueError('Invalid generated asset identifier')
+ return ROOT/identifier
+
+
+def persist(source, report):
+ source = Path(source)
+ identifier = source.parent.name
+ destination = directory(identifier)
+ destination.mkdir(parents=True, exist_ok=True)
+ for name in ('asset.npz','input.png','report.json','preview.json','timings.jsonl'):
+ shutil.copyfile(source.parent/name, destination/name)
+ shutil.copytree(source.parent/'views', destination/'views', dirs_exist_ok=True)
+ (destination/'ready.json').write_text(json.dumps({'complete':True,'identifier':identifier,
+ 'project_revision':report['project_revision'],'output_kind':'final_ovoxel'}))
+ return identifier
+
+
+def preview_html(identifier):
+ root = directory(identifier)
+ metadata = json.loads((root/'preview.json').read_text())
+ default = metadata.get('default_mode', 'shaded' if 'shaded' in metadata['modes'] else 'shaded_forest')
+ if default not in metadata['modes']:
+ default = metadata['modes'][0]
+ images = []
+ for mode in metadata['modes']:
+ for view in range(metadata['views']):
+ url = '/gradio_api/file='+quote(str(root/'views'/f'{mode}-{view:02d}.webp'),safe='/')
+ hidden = '' if mode == default and view == 0 else ' hidden'
+ images.append(f'
')
+ buttons = []
+ for mode, label, icon in PREVIEW_MODES:
+ if mode not in metadata['modes']:
+ continue
+ # Inline the tiny swatches; preview frames remain individually cached files.
+ import base64
+ icon_path = Path(__file__).resolve().parents[1]/'assets/preview-lighting'/icon
+ swatch = base64.b64encode(icon_path.read_bytes()).decode('ascii')
+ active = mode == default
+ buttons.append(f'')
+ return (f''
+ '
'+''.join(images)+'
'
+ '
'+''.join(buttons)+'
'
+ f'
')
+
+
+def extract(identifier, extraction_token=None):
+ from hf_space import extraction_progress as progress
+ from hf_space import generation_progress
+ from hf_space.timing import observe_stages
+ token = extraction_token if generation_progress.valid(extraction_token) else progress.start()
+ try:
+ progress.stage(token, 'load')
+ with observe_stages(lambda name: progress.stage(token, name)):
+ output = _extract(identifier, token)
+ progress.finish(token)
+ return output
+ except Exception:
+ progress.finish(token, failed=True)
+ raise
+
+
+def _extract(identifier, token):
+ """Runs only inside the explicitly requested GPU extraction callback."""
+ root = directory(identifier)
+ if not (root/'ready.json').is_file():
+ raise ValueError('The generated o-voxel has not finished saving')
+ output = root/'asset.glb'
+ if output.is_file():
+ return str(output)
+ import numpy as np
+ import torch
+ from supratex.representations import MeshWithVoxel
+ from supratex.inference.pipeline import _export, LAYOUT, _runtime
+ if not torch.cuda.is_available():
+ raise RuntimeError('GLB extraction requires CUDA')
+ with np.load(root/'asset.npz',allow_pickle=False) as state:
+ resolution = int(state['resolution'])
+ representation = MeshWithVoxel(torch.from_numpy(state['vertices']).cuda(),
+ torch.from_numpy(state['faces']).cuda(),state['origin'].tolist(),float(state['voxel_size']),
+ torch.from_numpy(state['coords']).cuda(),torch.from_numpy(state['attrs']).cuda(),
+ torch.Size([1,6,resolution,resolution,resolution]),layout=LAYOUT)
+ import time
+ started = time.monotonic()
+ _export(output, representation,4096,500000,'png',_runtime(), low_vram=True)
+ from hf_space import extraction_progress
+ extraction_progress.stage(token, 'validate')
+ from hf_space.backend import validate_glb
+ metadata = validate_glb(output)
+ (root/'extraction.json').write_text(json.dumps({'status':'complete','device':'cuda',
+ 'elapsed_seconds':time.monotonic()-started,**metadata},indent=2))
+ return str(output)
diff --git a/hf_space/release_backend.py b/hf_space/release_backend.py
new file mode 100644
index 0000000000000000000000000000000000000000..ba5869004aca578290771392b6753ccdceb644d6
--- /dev/null
+++ b/hf_space/release_backend.py
@@ -0,0 +1,176 @@
+"""Bind the pinned official single-view CLI to the existing Gradio callbacks."""
+from pathlib import Path
+import json
+import os
+import signal
+import subprocess
+import sys
+import threading
+import time
+import traceback
+import uuid
+
+from hf_space import native_build, generation_progress
+
+ROOT=Path(__file__).resolve().parents[1]
+MODEL_REVISION='1bc02eb969a8ed0f1418c2bbe1e409a8481f834c'
+CODE_REVISION='e4dfbbde6a8d76e25deef5aba5d836f03994967d'
+CHECKPOINT='denoiser_step0030000.safetensors'
+MODEL_ROOT=Path('/data/models')
+STATE=Path('/tmp/supratex-release-preparation.json')
+READY=Path('/tmp/supratex-latest-ready')
+_lock=threading.Lock()
+_worker=None
+
+def model_status():
+ return json.loads(STATE.read_text()) if STATE.exists() else {'stage':'pending'}
+
+def update(**changes):
+ value=model_status()|changes|{'updated_unix':time.time()}
+ temporary=STATE.with_suffix('.tmp')
+ temporary.write_text(json.dumps(value,indent=2))
+ temporary.replace(STATE)
+ print('[release-prepare] '+json.dumps(changes),flush=True)
+
+def prepare_models():
+ try:
+ update(stage='waiting_native_build',startup_progress={'label':'Preparing runtime','completed':0,'total':0})
+ while native_build.status().get('stage') not in ('compiled','failed'):
+ time.sleep(2)
+ if native_build.status()['stage']!='compiled':
+ raise RuntimeError('Native runtime preparation failed')
+ repository=os.environ.get('SUPRATEX_MODEL_REPO','supra3d/supra-tex')
+ revision=os.environ.get('SUPRATEX_MODEL_REVISION',MODEL_REVISION)
+ if (repository,revision)!=('supra3d/supra-tex',MODEL_REVISION):
+ raise RuntimeError('This source release requires the validated, pinned model revision')
+ from scripts import prepare_models as prep
+ from hf_space.download_models import download
+ plan=prep.build_plan(prep.parse_args(['--repo',repository,'--revision',revision,
+ '--with-rmbg','--output-dir',str(MODEL_ROOT)]),resolve_remote=True)
+ operations=sorted(plan['downloads'],key=lambda item:not item.get('gated',False))
+ for index,operation in enumerate(operations):
+ if operation['name']=='project':
+ operation=dict(operation,destination='project/'+revision)
+ update(stage='downloading_foundations',project_revision=revision,
+ startup_progress={'label':'Checking model files: '+operation['name'],
+ 'completed':index,'total':len(operations),'unit':'groups'})
+ download(operation,MODEL_ROOT)
+ from hf_space.weight_cache import mirror_models,mirror_project
+ update(stage='staging_local_weights',startup_progress={
+ 'label':'Preparing local weight files','completed':0,'total':0})
+ project_root=MODEL_ROOT/'project'/revision
+ sources=[*(MODEL_ROOT/'external').rglob('*.safetensors'),*project_root.rglob('*.safetensors')]
+ total=sum(path.stat().st_size for path in sources)
+ copied={}
+ def file_progress(source,completed,size):
+ copied[str(source)]=completed
+ update(startup_progress={'label':'Preparing local weight files','completed':sum(copied.values()),
+ 'total':total,'unit':'bytes'})
+ foundations=mirror_models(MODEL_ROOT,progress=file_progress)
+ project=mirror_project(project_root,revision,progress=file_progress)
+ arguments=['--repo',str(project),'--output-dir',str(READY)]
+ for flag,name in [('qwen-base','qwen-base'),('geometry','geometry'),('dino','dino'),
+ ('vae','vae'),('moge','moge2'),('rmbg','rmbg2')]:
+ arguments+=['--'+flag,str(foundations/name)]
+ arguments+=['--naf-weights',str(foundations/'naf/naf_release.pth')]
+ config=prep.prepare(prep.build_plan(prep.parse_args(arguments)))
+ prepared=json.loads(config.read_text())
+ if not prepared['checkpoint'].endswith(CHECKPOINT):
+ raise RuntimeError('The prepared pipeline did not select step-30000 completion weights')
+ update(stage='ready_released',config=str(config),project_repository=repository,
+ project_revision=revision,code_revision=CODE_REVISION,weight_origin='released',
+ actual_training_steps=30000,random_weights_present=False,
+ checkpoint=CHECKPOINT,model_loading='official_stage_loading',
+ startup_progress={'label':'Model files ready','completed':1,'total':1,'unit':'groups'})
+ except BaseException:
+ update(stage='failed',error=traceback.format_exc())
+ traceback.print_exc()
+
+def start_preparation():
+ global _worker
+ with _lock:
+ if model_status().get('stage')=='ready_released':return None
+ if _worker is None or not _worker.is_alive():
+ _worker=threading.Thread(target=prepare_models,daemon=True,name='latest-release-prepare')
+ _worker.start()
+ return _worker
+
+def ensure_prepared(generation_token=None):
+ worker=start_preparation()
+ while worker is not None and worker.is_alive():
+ state=model_status()
+ generation_progress.update(generation_token,status='preparing',
+ label=state.get('startup_progress',{}).get('label','Preparing model files'))
+ worker.join(timeout=1)
+ state=model_status()
+ if state.get('stage')!='ready_released':
+ raise RuntimeError('Latest-release preparation failed: '+state.get('error','unknown failure')[-2000:])
+
+def generate(image,seed,progress=None,timeout_seconds=1800,generation_token=None):
+ state=model_status()
+ if state.get('stage')!='ready_released':raise RuntimeError('Model files are not ready')
+ identifier=uuid.uuid4().hex
+ job=Path('/tmp/supratex-jobs')/identifier
+ job.mkdir(parents=True)
+ image.save(job/'input.png')
+ output=job/'asset.npz'
+ command=[sys.executable,str(ROOT/'scripts/run_example.py'),'--config',state['config'],
+ '--input',str(job/'input.png'),'--output',str(output),'--attention','sdpa','--low-vram',
+ '--seed',str(int(seed)),'--preview-only']
+ environment=os.environ.copy()
+ environment.update(OMP_NUM_THREADS='4',
+ SUPRATEX_RUNTIME_PROFILE='torch28',PYTHONUNBUFFERED='1',
+ SUPRATEX_TIMING_PATH=str(job/'timings.jsonl'),SUPRATEX_TIMING_ORIGIN_NS=str(time.monotonic_ns()))
+ started=time.monotonic()
+ generation_progress.update(generation_token,status='running',job=identifier,
+ generation_started_unix=time.time(),label='Running the latest single-view pipeline')
+ if progress:progress(.05,'Running the latest single-view pipeline')
+ process=subprocess.Popen(command,cwd=ROOT,env=environment,stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT,text=True,start_new_session=True)
+ def stop_processes():
+ if process.poll() is None:
+ try:os.killpg(process.pid,signal.SIGKILL)
+ except ProcessLookupError:pass
+ timer=threading.Timer(timeout_seconds,stop_processes)
+ timer.start()
+ try:
+ with (job/'inference.log').open('w') as log:
+ for line in process.stdout:
+ log.write(line);log.flush()
+ print(line,end='',flush=True)
+ if line.startswith('[1/2]'):
+ generation_progress.update(generation_token,label='Generating geometry and reference materials')
+ elif line.startswith('[2/2]'):
+ generation_progress.update(generation_token,label='Completing surface materials and rendering o-voxel previews')
+ code=process.wait()
+ finally:
+ timer.cancel()
+ if process.poll() is None:
+ stop_processes();process.wait()
+ receipt_path=job/'asset_work/run.json'
+ receipt=json.loads(receipt_path.read_text()) if receipt_path.exists() else {}
+ if code or receipt.get('status')!='complete' or not output.is_file():
+ raise RuntimeError('Latest single-view generation failed: '+(job/'inference.log').read_text(errors='replace')[-5000:])
+ import numpy as np
+ with np.load(output,allow_pickle=False) as asset:
+ required={'vertices','faces','coords','attrs','resolution','origin','voxel_size'}
+ if not required<=set(asset.files) or not len(asset['coords']) or not len(asset['faces']):
+ raise RuntimeError('Generated o-voxel geometry is incomplete')
+ if asset['attrs'].shape[1]!=6 or not np.isfinite(asset['attrs']).all():
+ raise RuntimeError('Generated o-voxel materials are invalid')
+ voxel_count=len(asset['coords'])
+ preview=json.loads((job/'preview.json').read_text())
+ if len(list((job/'views').glob('*.webp')))!=preview['views']*len(preview['modes']):
+ raise RuntimeError('The o-voxel preview render set is incomplete')
+ if list(job.rglob('*.glb')) or preview.get('automatic_glb_export') is not False:
+ raise RuntimeError('Generation must not export a mesh before selection')
+ report={'status':'complete','asset_id':identifier,'code_revision':CODE_REVISION,
+ 'project_revision':MODEL_REVISION,'checkpoint':CHECKPOINT,'weight_origin':'released',
+ 'elapsed_seconds':time.monotonic()-started,'output_kind':'final_ovoxel','backview':False,
+ 'refine':False,'model_loading':'official_stage_loading','run':receipt,
+ 'preview':preview,'voxel_count':voxel_count,'automatic_glb_export':False}
+ (job/'report.json').write_text(json.dumps(report,indent=2))
+ if not (job/'timings.jsonl').exists():
+ (job/'timings.jsonl').write_text(json.dumps({'name':'preview_generation','elapsed_seconds':report['elapsed_seconds']})+'\n')
+ if progress:progress(1.,'Complete')
+ return str(output),str(output),report
diff --git a/hf_space/startup_progress.py b/hf_space/startup_progress.py
index 8af897e227e6221ef4cb1fdc11422d050482c766..f8c95146bb5b0d127644362c83ae457338910ea4 100644
--- a/hf_space/startup_progress.py
+++ b/hf_space/startup_progress.py
@@ -1,23 +1,23 @@
-"""English startup progress, measured from copied bytes and loaded models."""
-import html
-
-
-def render(state):
- progress = state.get('models', {}).get('startup_progress', {})
- done = max(0, progress.get('completed', 0))
- total = max(0, progress.get('total', 0))
- label = html.escape(progress.get('label', 'Preparing runtime'))
- unit = progress.get('unit')
- if total and unit == 'bytes':
- detail = f'{done / 1024**3:.1f} / {total / 1024**3:.1f} GB'
- elif total:
- detail = f'{int(done)} / {int(total)} ' + ('models' if unit == 'models' else 'groups')
- else:
- detail = ''
- value = f' value="{min(100, done / total * 100):.2f}"' if total else ''
- return (''
- '
Loading weights
'
- '
Models load automatically in the background when the demo starts. All users share the loaded models.
'
- '
Loading progress' + detail + '
'
- '
'
- '
' + label + '
')
+"""English startup progress, measured from copied bytes and loaded models."""
+import html
+
+
+def render(state):
+ progress = state.get('models', {}).get('startup_progress', {})
+ done = max(0, progress.get('completed', 0))
+ total = max(0, progress.get('total', 0))
+ label = html.escape(progress.get('label', 'Preparing runtime'))
+ unit = progress.get('unit')
+ if total and unit == 'bytes':
+ detail = f'{done / 1024**3:.1f} / {total / 1024**3:.1f} GB'
+ elif total:
+ detail = f'{int(done)} / {int(total)} ' + ('models' if unit == 'models' else 'groups')
+ else:
+ detail = ''
+ value = f' value="{min(100, done / total * 100):.2f}"' if total else ''
+ return (''
+ '
Loading weights
'
+ '
Model files are prepared in the background and cached for all users.
'
+ '
Loading progress' + detail + '
'
+ '
'
+ '
' + label + '
')
diff --git a/infer.py b/infer.py
index 77debb69846ffb1db038d01a418f092219dc30eb..6b7e77b6a9b822719bea4f18d3dc45fbeb244ac6 100644
--- a/infer.py
+++ b/infer.py
@@ -1,227 +1,350 @@
-#!/usr/bin/env python3
-"""Generate a textured PBR mesh from one RGB/RGBA image."""
-from __future__ import annotations
-
-import argparse
-import json
-import math
-from pathlib import Path
-from hf_space.timing import span
-
-
-def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
- parser = argparse.ArgumentParser(description=__doc__)
- parser.add_argument("--input", type=Path,
- default=Path(__file__).resolve().parent / "assets/pitcher_plant.png",
- help="source RGB/RGBA image (default: assets/pitcher_plant.png)")
- parser.add_argument("--output", required=True, type=Path, help="new destination .glb file")
- parser.add_argument("--preview-only", action="store_true", help="save final o-voxel and GPU multiview snapshots; skip GLB extraction")
- parser.add_argument("--work-dir", type=Path, help="fresh intermediate directory (default: