diff --git a/IF_Trellis.py b/IF_Trellis.py index 2eb6c22..722eb77 100644 --- a/IF_Trellis.py +++ b/IF_Trellis.py @@ -193,7 +193,7 @@ class IF_TrellisImageTo3D: video_path = os.path.join(out_dir, f"{project_name}_preview.mp4") imageio.mimsave(video_path, video, fps=fps) full_video_path = os.path.abspath(video_path) - video_path = get_subpath_after_dir(full_video_path, "output") + video_path = os.path.abspath(video_path) logger.info(f"Full video path: {full_video_path}, Processed video path: {video_path}") if save_glb: @@ -308,7 +308,7 @@ class IF_TrellisImageTo3D: pipeline_params = self.get_pipeline_params( seed, ss_sampling_steps, ss_guidance_strength, - slat_sampling_steps, slat_guidance_strength + slat_sampling_steps, slat_guidance_strength, ) # Handle single vs multi mode differently @@ -316,7 +316,8 @@ class IF_TrellisImageTo3D: # Take just the first image regardless of how many were input images = images[0:1] pil_imgs = self.torch_to_pil_batch(images, masks) - outputs = model.run(pil_imgs[0], **pipeline_params) + outputs = model.run(pil_imgs[0], + **pipeline_params) else: # In multi mode, treat the whole list as a batch pil_imgs = self.torch_to_pil_batch(images, masks) diff --git a/IF_TrellisCheckpointLoader.py b/IF_TrellisCheckpointLoader.py index 27ec9bd..f69fcaa 100644 --- a/IF_TrellisCheckpointLoader.py +++ b/IF_TrellisCheckpointLoader.py @@ -1,261 +1,161 @@ # IF_TrellisCheckpointLoader.py import os -import sys -import importlib -import torch import logging +import torch import folder_paths -from huggingface_hub import hf_hub_download, snapshot_download -from pathlib import Path -import json from trellis_model_manager import TrellisModelManager from trellis.pipelines.trellis_image_to_3d import TrellisImageTo3DPipeline -from trellis.modules import set_attention_backend +from trellis.backend_config import ( + set_attention_backend, + set_sparse_backend, + get_available_backends, + get_available_sparse_backends +) from typing import Literal -from trellis.modules.attention_utils import enable_sage_attention, disable_sage_attention +from torchvision import transforms logger = logging.getLogger("IF_Trellis") -def set_backend(backend: Literal['spconv', 'torchsparse']): - # Example helper if you wish to call the underlying global set_backend from trellis.modules.sparse: - from trellis.modules.sparse import set_backend as _set_sparse_backend - # Also handle spconv algo if desired, e.g. os.environ['SPCONV_ALGO'] = ... - _set_sparse_backend(backend) - -class TrellisConfig: - """Global configuration for Trellis""" - def __init__(self): - self.logger = logger - self.attention_backend = "sage" - self.spconv_algo = "implicit_gemm" - self.smooth_k = True - self.device = "cuda" - self.use_fp16 = True - # Added new configuration dictionary - self._config = { - "dinov2_size": "large", # Default model size - "dinov2_model": "dinov2_vitg14" # Default model name - } - - # Added new methods - def get(self, key, default=None): - """Get configuration value with fallback""" - return self._config.get(key, default) - - def set(self, key, value): - """Set configuration value""" - self._config[key] = value - - def setup_environment(self): - """Set up all environment variables and backends""" - import os - from trellis.modules import set_attention_backend - from trellis.modules.sparse import set_backend - - # Set attention backend - set_attention_backend(self.attention_backend) - - # Set smooth k for sage attention - os.environ['SAGEATTN_SMOOTH_K'] = '1' if self.smooth_k else '0' - - # Set spconv algorithm - os.environ['SPCONV_ALGO'] = self.spconv_algo - - # Always use spconv as backend for now - set_backend('spconv') - - logger.info(f"Environment configured - Backend: spconv, " - f"Attention: {self.attention_backend}, " - f"Smooth K: {self.smooth_k}, " - f"SpConv Algo: {self.spconv_algo}") - -# Global config instance -TRELLIS_CONFIG = TrellisConfig() - class IF_TrellisCheckpointLoader: """ - Node to manage the loading of the TRELLIS model. - Follows ComfyUI conventions for model management. + Node to manage the loading of the TRELLIS model with lazy backend selection. """ def __init__(self): self.logger = logger self.model_manager = None - # Check for available devices - self.device = self._get_device() - - def _get_device(self): - """Determine the best available device.""" - if torch.cuda.is_available(): - return "cuda" - elif hasattr(torch.backends, 'mps') and torch.backends.mps.is_available(): - return "mps" - return "cpu" - + self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + + # We might call these to figure out what's actually installed, + # if we want to populate UI dropdowns: + self.attn_backends = get_available_backends() # e.g. { 'xformers': True, 'flash_attn': False, ... } + self.sparse_backends = get_available_sparse_backends()# e.g. { 'spconv': True, 'torchsparse': True } + @classmethod def INPUT_TYPES(cls): """Define input types with device-specific options.""" - device_options = [] - if torch.cuda.is_available(): - device_options.append("cuda") - if hasattr(torch.backends, 'mps') and torch.backends.mps.is_available(): - device_options.append("mps") - device_options.append("cpu") + # Filter only available backends + attn_backends = get_available_backends() + sparse_backends = get_available_sparse_backends() + + # e.g. create a list of names that are True: + available_attn = [k for k, v in attn_backends.items() if v] + if not available_attn: + available_attn = ['sdpa'] # fallback + + available_sparse = [k for k, v in sparse_backends.items() if v] + if not available_sparse: + available_sparse = ['spconv'] # fallback return { "required": { "model_name": (["TRELLIS-image-large"],), - "dinov2_model": (["dinov2_vitl14_reg", "dinov2_vitg14_reg"], {"default": "dinov2_vitl14_reg", "tooltip": "Select the Dinov2 model to use for the image to 3D conversion. Smaller models work but better results with larger models."}), + "dinov2_model": (["dinov2_vitl14_reg"], + {"default": "dinov2_vitl14_reg", + "tooltip": "Select which Dinov2 model to use."}), "use_fp16": ("BOOLEAN", {"default": True}), - "attn_backend": (["sage", "xformers", "flash_attn", "sdpa", "naive"], {"default": "sage", "tooltip": "Select the attention backend to use for the image to 3D conversion. Sage is experimental but faster"}), - "smooth_k": ("BOOLEAN", {"default": True, "tooltip": "Smooth k for sage attention. This is a hyperparameter that controls the smoothness of the attention distribution. It is a boolean value that determines whether to use smooth k or not. Smooth k is a hyperparameter that controls the smoothness of the attention distribution. It is a boolean value that determines whether to use smooth k or not."}), - "spconv_algo": (["implicit_gemm", "native"], {"default": "implicit_gemm", "tooltip": "Select the spconv algorithm to use for the image to 3D conversion. Implicit gemm is the best but slower. Native is the fastest but less accurate."}), - "main_device": (device_options, {"default": device_options[0]}), + # + # The user picks from the actually installed backends + # + "attn_backend": (available_attn, + {"default": "sdpa" if "sdpa" in available_attn else available_attn[0], + "tooltip": "Select attention backend."}), + "sparse_backend": (available_sparse, + {"default": "spconv" if "spconv" in available_sparse else available_sparse[0], + "tooltip": "Select sparse backend."}), + "spconv_algo": (["implicit_gemm", "native", "auto"], + {"default": "implicit_gemm", + "tooltip": "Spconv algorithm. 'implicit_gemm' is slower but more robust."}), + "smooth_k": ("BOOLEAN", + {"default": True, + "tooltip": "Smooth-k for SageAttention. Only relevant if attn_backend=sage."}), }, } - + RETURN_TYPES = ("TRELLIS_MODEL",) RETURN_NAMES = ("model",) FUNCTION = "load_model" CATEGORY = "ImpactFrames๐Ÿ’ฅ๐ŸŽž๏ธ/Trellis" - @classmethod - def _check_backend_availability(cls, backend: str) -> bool: - """Check if a specific attention backend is available""" - try: - if backend == 'sage': - import sageattention - elif backend == 'xformers': - import xformers.ops - elif backend == 'flash_attn': - import flash_attn - elif backend in ['sdpa', 'naive']: - # These are always available in PyTorch - pass - else: - return False - return True - except ImportError: - return False - - @classmethod - def _initialize_backend(cls, requested_backend: str = None) -> str: - """Initialize attention backend with fallback logic""" - # Priority order for backends - backend_priority = ['sage', 'flash_attn', 'xformers', 'sdpa'] - - # If a specific backend is requested, try it first - if requested_backend: - if cls._check_backend_availability(requested_backend): - logger.info(f"Using requested attention backend: {requested_backend}") - return requested_backend - else: - logger.warning(f"Requested backend '{requested_backend}' not available, falling back") - - # Try backends in priority order - for backend in backend_priority: - if cls._check_backend_availability(backend): - logger.info(f"Using attention backend: {backend}") - return backend - - # Final fallback to SDPA - logger.info("All optimized attention backends unavailable, using PyTorch SDPA") - return 'sdpa' - - def _setup_environment(self): + def _setup_environment(self, attn_backend: str, sparse_backend: str, spconv_algo: str, smooth_k: bool): """ - Set up environment variables based on the global TRELLIS_CONFIG. + Set up environment variables and backends lazily. + This is the main difference: we call our new lazy set_*_backend funcs. """ - import os - from trellis.modules import set_attention_backend - from trellis.modules.sparse import set_backend - from trellis.modules.sparse.conv import SPCONV_ALGO + # Try attention + success = set_attention_backend(attn_backend) + if not success: + self.logger.warning(f"Failed to set {attn_backend} or not installed, fallback to sdpa.") - # Set attention backend - os.environ['ATTN_BACKEND'] = TRELLIS_CONFIG.attention_backend - set_attention_backend(TRELLIS_CONFIG.attention_backend) + # Try sparse + success2 = set_sparse_backend(sparse_backend, spconv_algo) + if not success2: + self.logger.warning(f"Failed to set {sparse_backend} or not installed, fallback to default.") - # Set smooth k for sage attention - os.environ['SAGEATTN_SMOOTH_K'] = '1' if TRELLIS_CONFIG.smooth_k else '0' + # If user wants SageAttn smooth_k, we set environment var (if they'd want that): + os.environ['SAGEATTN_SMOOTH_K'] = '1' if smooth_k else '0' - # Set spconv algorithm - os.environ['SPCONV_ALGO'] = TRELLIS_CONFIG.spconv_algo - - # Always use spconv as backend for now - set_backend('spconv') + def _initialize_transforms(self): + """Initialize image transforms if needed.""" + return transforms.Compose([ + transforms.Normalize( + mean=[0.485, 0.456, 0.406], + std=[0.229, 0.224, 0.225] + ) + ]) - logger.info(f"Environment configured - Backend: spconv, " - f"Attention: {TRELLIS_CONFIG.attention_backend}, " - f"Smooth K: {TRELLIS_CONFIG.smooth_k}, " - f"SpConv Algo: {TRELLIS_CONFIG.spconv_algo}") - - def optimize_pipeline(self, pipeline, use_fp16=True, attn_backend='sage'): - """Apply optimizations to the pipeline if available""" - if self.device == "cuda": + def _optimize_pipeline(self, pipeline, use_fp16: bool = True): + """ + Apply typical optimizations, half-precision, etc. + """ + if self.device.type == "cuda": try: if hasattr(pipeline, 'cuda'): pipeline.cuda() - + if use_fp16: if hasattr(pipeline, 'enable_attention_slicing'): - pipeline.enable_attention_slicing() + pipeline.enable_attention_slicing(slice_size="auto") if hasattr(pipeline, 'half'): pipeline.half() - - # Only enable xformers if using xformers backend - if attn_backend == 'xformers' and hasattr(pipeline, 'enable_xformers_memory_efficient_attention'): - pipeline.enable_xformers_memory_efficient_attention() - except Exception as e: - logger.warning(f"Some optimizations failed: {str(e)}") - + logger.warning(f"Some pipeline optimizations failed: {str(e)}") + return pipeline - def load_model(self, model_name, dinov2_model="dinov2_vitg14", attn_backend="sage", use_fp16=True, - smooth_k=True, spconv_algo="implicit_gemm", main_device="cuda"): - """Load and configure the TRELLIS model.""" + def load_model( + self, + model_name: str, + dinov2_model: str = "dinov2_vitl14_reg", + attn_backend: str = "sdpa", + sparse_backend: str = "spconv", + spconv_algo: str = "implicit_gemm", + use_fp16: bool = True, + smooth_k: bool = True, + ) -> tuple: + """ + Load and configure the TRELLIS pipeline. + This is typically the main function invoked by ComfyUI at node execution time. + """ try: - # Update global config - TRELLIS_CONFIG.attention_backend = attn_backend - TRELLIS_CONFIG.spconv_algo = spconv_algo - TRELLIS_CONFIG.smooth_k = smooth_k - TRELLIS_CONFIG.device = main_device - TRELLIS_CONFIG.use_fp16 = use_fp16 - TRELLIS_CONFIG.set("dinov2_model", dinov2_model) + # 1) Setup environment + backends + self._setup_environment(attn_backend, sparse_backend, spconv_algo, smooth_k) - # Set up environment - self._setup_environment() - - # Configure attention backend - set_attention_backend(attn_backend) - if attn_backend == 'sage': - enable_sage_attention() - else: - disable_sage_attention() - - # Get model path + # 2) Get model path model_path = folder_paths.get_full_path("checkpoints", model_name) if model_path is None: model_path = os.path.join(folder_paths.models_dir, "checkpoints", model_name) + if not os.path.exists(model_path): + raise FileNotFoundError(f"Model not found: {model_path}") - # Create pipeline with specified dinov2 model - pipeline = TrellisImageTo3DPipeline.from_pretrained(model_path, dinov2_model=dinov2_model) - - # Configure pipeline after loading - pipeline._device = torch.device(main_device) - pipeline.attention_backend = attn_backend - - # Store configuration in pipeline - pipeline.config = { - 'device': main_device, - 'use_fp16': use_fp16, - 'attention_backend': attn_backend, - 'dinov2_model': dinov2_model, - 'spconv_algo': spconv_algo, - 'smooth_k': smooth_k - } + # 3) Create pipeline with the config + pipeline = TrellisImageTo3DPipeline.from_pretrained( + model_path, + dinov2_model=dinov2_model + ) + pipeline._device = self.device # ensure pipeline uses our same device - # Apply optimizations - pipeline = self.optimize_pipeline(pipeline, use_fp16, attn_backend) + # 4) Apply optimizations + pipeline = self._optimize_pipeline(pipeline, use_fp16) return (pipeline,) diff --git a/README.md b/README.md index 97aa9e5..cd44ea9 100644 --- a/README.md +++ b/README.md @@ -1,117 +1,117 @@ -

ComfyUI-IF_Trellis

-

arXiv -Project Page - -

-ComfyUI TRELLIS is a large 3D asset generation in various formats, such as Radiance Fields, 3D Gaussians, and meshes. The cornerstone of TRELLIS is a unified Structured LATent (SLAT) representation that allows decoding to different output formats and Rectified Flow Transformers tailored for SLAT as the powerful backbones. - -![teaser (1)](https://github.com/user-attachments/assets/6eee56bd-0936-44a5-b843-be4e87be649f) - -### Prerequisites -- **System**: The original code is currently tested only on **Linux**. For windows setup, you may refer to [#3](https://github.com/microsoft/TRELLIS/issues/3) -- (This comfyui node is following this suggeted installation steps It works but you need to follow the steps as described in part one and two of the guide). -- Windows users need to use the Win_requirements.txt. -- Linux Users (Not tested Yet) use the linux_requirements.txt but I am still testing if it works with comfy in Linux I just added the original repo requirements. - -- **Hardware**: This repo improve memmory mangement with the PR made by Amorano you will need NVIDIA GPU with at least 8GB. -- **Software**: - - The [CUDA Toolkit](https://developer.nvidia.com/cuda-toolkit-archive) is needed to compile certain submodules. The code has been tested with CUDA versions 11.8 and 12.2. This repo use **CUDA 12.4**. - - [Conda](https://docs.anaconda.com/miniconda/install/#quick-command-line-install) is recommended for managing dependencies. - - Python version 3.8 or higher is required. - - Give unrestricted script access to powershell so venv can work: - -- Open an administrator powershell window -- Type `Set-ExecutionPolicy Unrestricted` and answer A -- Close admin powershell window - -### Installation Steps -1. Clone the repo: - ``` - cd ComfyUI/Custom_nodes - git clone --recurse-submodules https://github.com/if-ai/ComfyUI-IF_Trellis.git - ``` -## MUST HAVE `--recurse-submodules` - -ONLY tested on windows but it should work easier in Linux without any issues or needing such specific stuffs. -***NOT tested or compatible with PORTABLE comfy embeded python env*** -watch quick overview of setting the env if needed -[![Watch the video](https://img.youtube.com/vi/-vEpuYL9I3g/hqdefault.jpg)](https://www.youtube.com/watch?v=-vEpuYL9I3g) - -You need to set up the environment first -follow this guide for the first part - -Set the VSCode Cpp Envirronment as in the guide - -[Installing Triton and Sage Attention Flash Attention](https://ko-fi.com/post/Installing-Triton-and-Sage-Attention-Flash-Attenti-P5P8175434) - - -Setting up ComfyUI with the Xformers, flash attention, Sage-attention(Optional Recommended for Hunyuan and other Video models) - -*** You can Also Try This Other Guide *** - -[![Watch this guide if you need extra help](https://img.youtube.com/vi/FjNfDsX-jR0/hqdefault.jpg)](https://www.youtube.com/watch?v=FjNfDsX-jR0) - -[How to Run Micromamba in ComfyUI with Triton, SAGE Attention, Flash Attention, and xFormers for ComfyUI 3D](https://comfyuiblog.com/how-to-run-micromamba-in-comfyui-with-triton-sage-attention-flash-attention-and-x-formers-for-comfyui-3d/) - - - - -## ๐Ÿ“ฆ Second part of the installation - -Activate youur comfy environment -``` -(gen) PS D:\ComfyUI\custom_nodes\ComfyUI-IF_Trellis> micromamba activate gen -``` -If you haven't set your vars or for some reason it can't compile some of this specially `nvdiffrast` -it doesn't hurt if you do it again now. -``` -cmd.exe /c "C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\VC\Auxiliary\Build\vcvarsall.bat" x64 "&&" powershell -``` -You will see some message like this: - -********************************************************************** -** Visual Studio 2019 Developer Command Prompt v16.11.41 -** Copyright (c) 2021 Microsoft Corporation -********************************************************************** -[vcvarsall.bat] Environment initialized for: 'x64' -Windows PowerShell -Copyright (C) Microsoft Corporation. All rights reserved. - -``` -pip install -r win_requirements.txt -``` - -```bash -pip install git+https://github.com/EasternJournalist/utils3d.git@9a4eb15e4021b67b12c460c7057d642626897ec8 -New-Item -ItemType Directory -Force -Path C:\tmp\extensions -git clone --recurse-submodules https://github.com/JeffreyXiang/diffoctreerast.git C:\tmp\extensions\diffoctreerast -pip install C:\tmp\extensions\diffoctreerast -git clone https://github.com/autonomousvision/mip-splatting.git C:\tmp\extensions\mip-splatting -pip install C:\tmp\extensions\mip-splatting\submodules\diff-gaussian-rasterization\ -pip install kaolin -f https://nvidia-kaolin.s3.us-east-2.amazonaws.com/torch-2.4.0_cu121.html -git clone https://github.com/NVlabs/nvdiffrast.git C:\tmp\extensions\nvdiffrast -pip install C:\tmp\extensions\nvdiffrast -``` - - -![blender_oWFuSjA6yq](https://github.com/user-attachments/assets/7d5fb61a-f2f6-4000-ab32-8555d5c6b7da) - -![thorium_jtbGswydSr](https://github.com/user-attachments/assets/15f1d538-faa1-4c79-86f9-05f9b74ae794) - -## ๐ŸŒŸ Features -- **High Quality**: It produces diverse 3D assets at high quality with intricate shape and texture details. -- **Versatility**: It takes text or image prompts and can generate various final 3D representations including but not limited to *Radiance Fields*, *3D Gaussians*, and *meshes*, accommodating diverse downstream requirements. -- **Flexible Editing**: It allows for easy editings of generated 3D assets, such as generating variants of the same object or local editing of the 3D asset. - - -## ๐Ÿšง TODO List -- [x] Release comfyUI-IF_Trellis -- [x] 3D Viewport -- [x] OPT mode -- [x] Multiviews -- [x] Sage attn -- [ ] Installation -- [ ] colab notebook -- [ ] MarchingCubes mode -- [ ] Exposing Texture paranmeters +

ComfyUI-IF_Trellis

+

arXiv +Project Page + +

+ComfyUI TRELLIS is a large 3D asset generation in various formats, such as Radiance Fields, 3D Gaussians, and meshes. The cornerstone of TRELLIS is a unified Structured LATent (SLAT) representation that allows decoding to different output formats and Rectified Flow Transformers tailored for SLAT as the powerful backbones. + +![teaser (1)](https://github.com/user-attachments/assets/6eee56bd-0936-44a5-b843-be4e87be649f) + +### Prerequisites +- **System**: The original code is currently tested only on **Linux**. For windows setup, you may refer to [#3](https://github.com/microsoft/TRELLIS/issues/3) +- (This comfyui node is following this suggeted installation steps It works but you need to follow the steps as described in part one and two of the guide). +- Windows users need to use the Win_requirements.txt. +- Linux Users (Not tested Yet) use the linux_requirements.txt but I am still testing if it works with comfy in Linux I just added the original repo requirements. + +- **Hardware**: This repo improve memmory mangement with the PR made by Amorano you will need NVIDIA GPU with at least 8GB. +- **Software**: + - The [CUDA Toolkit](https://developer.nvidia.com/cuda-toolkit-archive) is needed to compile certain submodules. The code has been tested with CUDA versions 11.8 and 12.2. This repo use **CUDA 12.4**. + - [Conda](https://docs.anaconda.com/miniconda/install/#quick-command-line-install) is recommended for managing dependencies. + - Python version 3.8 or higher is required. + + Give unrestricted script access to powershell so venv can work: + +- Open an administrator powershell window +- Type `Set-ExecutionPolicy Unrestricted` and answer A +- Close admin powershell window + +### Installation Steps +1. Clone the repo: + ``` + cd ComfyUI/Custom_nodes + git clone --recurse-submodules https://github.com/if-ai/ComfyUI-IF_Trellis.git + ``` +## MUST HAVE `--recurse-submodules` + +ONLY tested on windows but it should work easier in Linux without any issues or needing such specific stuffs. +***NOT tested or compatible with PORTABLE comfy embeded python env*** +watch quick overview of setting the env if needed +[![Watch the video](https://img.youtube.com/vi/-vEpuYL9I3g/hqdefault.jpg)](https://www.youtube.com/watch?v=-vEpuYL9I3g) + +You need to set up the environment first +follow this guide for the first part + +Set the VSCode Cpp Envirronment as in the guide + +[Installing Triton and Sage Attention Flash Attention](https://ko-fi.com/post/Installing-Triton-and-Sage-Attention-Flash-Attenti-P5P8175434) + + +Setting up ComfyUI with the Xformers, flash attention, Sage-attention(Optional Recommended for Hunyuan and other Video models) + +*** You can Also Try This Other Guide *** + +[![Watch this guide if you need extra help](https://img.youtube.com/vi/FjNfDsX-jR0/hqdefault.jpg)](https://www.youtube.com/watch?v=FjNfDsX-jR0) + +[How to Run Micromamba in ComfyUI with Triton, SAGE Attention, Flash Attention, and xFormers for ComfyUI 3D](https://comfyuiblog.com/how-to-run-micromamba-in-comfyui-with-triton-sage-attention-flash-attention-and-x-formers-for-comfyui-3d/) + + + + +## ๐Ÿ“ฆ Second part of the installation + +Activate youur comfy environment +``` +(gen) PS D:\ComfyUI\custom_nodes\ComfyUI-IF_Trellis> micromamba activate gen +``` +If you haven't set your vars or for some reason it can't compile some of this specially `nvdiffrast` +it doesn't hurt if you do it again now. +``` +cmd.exe /c "C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\VC\Auxiliary\Build\vcvarsall.bat" x64 "&&" powershell +``` +You will see some message like this: + +********************************************************************** +** Visual Studio 2019 Developer Command Prompt v16.11.41 +** Copyright (c) 2021 Microsoft Corporation +********************************************************************** +[vcvarsall.bat] Environment initialized for: 'x64' +Windows PowerShell +Copyright (C) Microsoft Corporation. All rights reserved. + +``` +pip install -r win_requirements.txt +``` + +```bash +pip install git+https://github.com/EasternJournalist/utils3d.git@9a4eb15e4021b67b12c460c7057d642626897ec8 +New-Item -ItemType Directory -Force -Path C:\tmp\extensions +git clone --recurse-submodules https://github.com/JeffreyXiang/diffoctreerast.git C:\tmp\extensions\diffoctreerast +pip install C:\tmp\extensions\diffoctreerast +git clone https://github.com/autonomousvision/mip-splatting.git C:\tmp\extensions\mip-splatting +pip install C:\tmp\extensions\mip-splatting\submodules\diff-gaussian-rasterization\ +pip install kaolin -f https://nvidia-kaolin.s3.us-east-2.amazonaws.com/torch-2.4.0_cu121.html +git clone https://github.com/NVlabs/nvdiffrast.git C:\tmp\extensions\nvdiffrast +pip install C:\tmp\extensions\nvdiffrast +``` + + +![blender_oWFuSjA6yq](https://github.com/user-attachments/assets/7d5fb61a-f2f6-4000-ab32-8555d5c6b7da) + +![thorium_jtbGswydSr](https://github.com/user-attachments/assets/15f1d538-faa1-4c79-86f9-05f9b74ae794) + +## ๐ŸŒŸ Features +- **High Quality**: It produces diverse 3D assets at high quality with intricate shape and texture details. +- **Versatility**: It takes text or image prompts and can generate various final 3D representations including but not limited to *Radiance Fields*, *3D Gaussians*, and *meshes*, accommodating diverse downstream requirements. +- **Flexible Editing**: It allows for easy editings of generated 3D assets, such as generating variants of the same object or local editing of the 3D asset. + + +## ๐Ÿšง TODO List +- [x] Release comfyUI-IF_Trellis +- [x] 3D Viewport +- [x] OPT mode +- [x] Multiviews +- [x] Sage attn +- [ ] Installation +- [ ] colab notebook +- [ ] MarchingCubes mode +- [ ] Exposing Texture paranmeters diff --git a/trellis/backend_config.py b/trellis/backend_config.py new file mode 100644 index 0000000..53575ee --- /dev/null +++ b/trellis/backend_config.py @@ -0,0 +1,225 @@ +# trellis/backend_config.py +from typing import * +import os +import logging +import importlib + +# Global variables +BACKEND = 'spconv' # Default sparse backend +DEBUG = False # Debug mode flag +ATTN = 'sdpa' # Default attention backend +SPCONV_ALGO = 'implicit_gemm' # Default algorithm + +def get_spconv_algo() -> str: + """Get current spconv algorithm.""" + global SPCONV_ALGO + return SPCONV_ALGO + +def set_spconv_algo(algo: Literal['implicit_gemm', 'native', 'auto']) -> bool: + """Set spconv algorithm with validation.""" + global SPCONV_ALGO + + if algo not in ['implicit_gemm', 'native', 'auto']: + logger.warning(f"Invalid spconv algorithm: {algo}. Must be 'implicit_gemm', 'native', or 'auto'") + return False + + SPCONV_ALGO = algo + os.environ['SPCONV_ALGO'] = algo + logger.info(f"Set spconv algorithm to: {algo}") + return True + +logger = logging.getLogger(__name__) + +def _try_import_xformers() -> bool: + try: + import xformers.ops + return True + except ImportError: + return False + +def _try_import_flash_attn() -> bool: + try: + import flash_attn + return True + except ImportError: + return False + +def _try_import_sageattention() -> bool: + try: + import torch.nn.functional as F + from sageattention import sageattn + F.scaled_dot_product_attention = sageattn + #import sageattention + return True + except ImportError: + return False + +def _try_import_spconv() -> bool: + try: + import spconv + return True + except ImportError: + return False + +def _try_import_torchsparse() -> bool: + try: + import torchsparse + return True + except ImportError: + return False + +def get_available_backends() -> Dict[str, bool]: + """Return dict of available attention backends and their status""" + return { + 'xformers': _try_import_xformers(), + 'flash_attn': _try_import_flash_attn(), + 'sage': _try_import_sageattention(), + 'naive': True, + 'sdpa': True # Always available with PyTorch >= 2.0 + } + +def get_available_sparse_backends() -> Dict[str, bool]: + """Return dict of available sparse backends and their status""" + return { + 'spconv': _try_import_spconv(), + 'torchsparse': _try_import_torchsparse() + } + +def get_attention_backend() -> str: + """Get current attention backend""" + global ATTN + return ATTN + +def get_sparse_backend() -> str: + """Get current sparse backend""" + global BACKEND + return BACKEND + +def get_debug_mode() -> bool: + """Get current debug mode status""" + global DEBUG + return DEBUG + +def __from_env(): + """Initialize settings from environment variables""" + global BACKEND + global DEBUG + global ATTN + + env_sparse_backend = os.environ.get('SPARSE_BACKEND') + env_sparse_debug = os.environ.get('SPARSE_DEBUG') + env_sparse_attn = os.environ.get('SPARSE_ATTN_BACKEND') + + if env_sparse_backend is not None and env_sparse_backend in ['spconv', 'torchsparse']: + BACKEND = env_sparse_backend + if env_sparse_debug is not None: + DEBUG = env_sparse_debug == '1' + if env_sparse_attn is not None and env_sparse_attn in ['xformers', 'flash_attn', 'sage', 'sdpa', 'naive']: + ATTN = env_sparse_attn + os.environ['SPARSE_ATTN_BACKEND'] = env_sparse_attn + os.environ['ATTN_BACKEND'] = env_sparse_attn + + logger.info(f"[SPARSE] Backend: {BACKEND}, Attention: {ATTN}") + +def set_backend(backend: Literal['spconv', 'torchsparse']) -> bool: + """Set sparse backend with validation""" + global BACKEND + + backend = backend.lower().strip() + logger.info(f"Setting sparse backend to: {backend}") + + if backend == 'spconv': + try: + import spconv + BACKEND = 'spconv' + os.environ['SPARSE_BACKEND'] = 'spconv' + return True + except ImportError: + logger.warning("spconv not available") + return False + + elif backend == 'torchsparse': + try: + import torchsparse + BACKEND = 'torchsparse' + os.environ['SPARSE_BACKEND'] = 'torchsparse' + return True + except ImportError: + logger.warning("torchsparse not available") + return False + + return False + +def set_sparse_backend(backend: Literal['spconv', 'torchsparse'], algo: str = None) -> bool: + """Alias for set_backend for backwards compatibility + + Parameters: + backend: The sparse backend to use + algo: The algorithm to use (only relevant for spconv backend) + """ + # Call set_backend first + result = set_backend(backend) + + # If algorithm is provided and backend was set successfully + if algo is not None and result: + set_spconv_algo(algo) + + return result + +def set_debug(debug: bool): + """Set debug mode""" + global DEBUG + DEBUG = debug + if debug: + os.environ['SPARSE_DEBUG'] = '1' + else: + os.environ['SPARSE_DEBUG'] = '0' + +def set_attn(attn: Literal['xformers', 'flash_attn', 'sage', 'sdpa', 'naive']) -> bool: + """Set attention backend with validation""" + global ATTN + + attn = attn.lower().strip() + logger.info(f"Setting attention backend to: {attn}") + + if attn == 'xformers' and _try_import_xformers(): + ATTN = 'xformers' + os.environ['SPARSE_ATTN_BACKEND'] = 'xformers' + os.environ['ATTN_BACKEND'] = 'xformers' + return True + + elif attn == 'flash_attn' and _try_import_flash_attn(): + ATTN = 'flash_attn' + os.environ['SPARSE_ATTN_BACKEND'] = 'flash_attn' + os.environ['ATTN_BACKEND'] = 'flash_attn' + return True + + elif attn == 'sage' and _try_import_sageattention(): + ATTN = 'sage' + os.environ['SPARSE_ATTN_BACKEND'] = 'sage' + os.environ['ATTN_BACKEND'] = 'sage' + return True + + elif attn == 'sdpa': + ATTN = 'sdpa' + os.environ['SPARSE_ATTN_BACKEND'] = 'sdpa' + os.environ['ATTN_BACKEND'] = 'sdpa' + return True + + elif attn == 'naive': + ATTN = 'naive' + os.environ['SPARSE_ATTN_BACKEND'] = 'naive' + os.environ['ATTN_BACKEND'] = 'naive' + return True + + + logger.warning(f"Attention backend {attn} not available") + return False + +# Add alias for backwards compatibility +def set_attention_backend(backend: Literal['xformers', 'flash_attn', 'sage', 'sdpa']) -> bool: + """Alias for set_attn for backwards compatibility""" + return set_attn(backend) + +# Initialize from environment variables on module import +__from_env() diff --git a/trellis/modules/__init__.py b/trellis/modules/__init__.py index 5c2cefc..2a16f24 100644 --- a/trellis/modules/__init__.py +++ b/trellis/modules/__init__.py @@ -1,18 +1,16 @@ -from .attention_utils import enable_sage_attention, disable_sage_attention +#from .attention_utils import enable_sage_attention, disable_sage_attention from .attention import ( - set_attention_backend, - get_attention_op, - ATTN_BACKEND, + scaled_dot_product_attention, + BACKEND, + DEBUG, MultiHeadAttention, RotaryPositionEmbedder ) __all__ = [ - 'enable_sage_attention', - 'disable_sage_attention', - 'set_attention_backend', - 'get_attention_op', - 'ATTN_BACKEND', + 'scaled_dot_product_attention', + 'BACKEND', + 'DEBUG', 'MultiHeadAttention', 'RotaryPositionEmbedder' ] \ No newline at end of file diff --git a/trellis/modules/attention/__init__.py b/trellis/modules/attention/__init__.py index b3cd0b7..27f8c0b 100644 --- a/trellis/modules/attention/__init__.py +++ b/trellis/modules/attention/__init__.py @@ -1,65 +1,38 @@ import os import logging from typing import Literal +from trellis.backend_config import ( + get_attention_backend, + get_debug_mode, +) +import logging logger = logging.getLogger(__name__) -# Global settings -ATTN_BACKEND = 'flash_attn' # Default backend -DEBUG = False # Add DEBUG flag here -BACKEND = 'flash_attn' # Change default to a valid backend +#ATTN = get_attention_backend() +BACKEND = get_attention_backend() +DEBUG = get_debug_mode() -def set_attention_backend(backend: Literal['xformers', 'flash_attn', 'sdpa', 'sage', 'naive']): - """Set the global attention backend""" - global ATTN_BACKEND, BACKEND # Also update BACKEND when setting attention backend - if backend not in ['xformers', 'flash_attn', 'sdpa', 'sage', 'naive']: - raise ValueError(f"Unsupported attention backend: {backend}") - ATTN_BACKEND = backend - BACKEND = backend # Keep BACKEND in sync with ATTN_BACKEND - os.environ['ATTN_BACKEND'] = backend - logger.info(f"[ATTENTION] Set backend to: {backend}") - -def get_attention_op(): - """Get the appropriate attention implementation""" - if ATTN_BACKEND == 'xformers': - try: - import xformers.ops - return xformers.ops.memory_efficient_attention - except ImportError: - logger.warning("xformers not available, falling back to naive attention") - return None - elif ATTN_BACKEND == 'flash_attn': - try: - from flash_attn import flash_attn_func - return flash_attn_func - except ImportError: - logger.warning("flash_attn not available, falling back to naive attention") - return None - elif ATTN_BACKEND == 'sage': - try: - from sageattention import sageattn - return sageattn - except ImportError: - logger.warning("sageattention not available, falling back to naive attention") - return None - elif ATTN_BACKEND == 'sdpa': - import torch - if hasattr(torch.nn.functional, 'scaled_dot_product_attention'): - return torch.nn.functional.scaled_dot_product_attention +def __from_env(): + """Read current backend configuration""" + #global ATTN + global BACKEND + global DEBUG - # Fallback to naive implementation - logger.warning("Using naive attention implementation") - return None + # Get current settings from central config + #ATTN = + BACKEND = get_attention_backend() + DEBUG = get_debug_mode() + + print(f"[ATTENTION] Using backend: {BACKEND}") -# Import attention modules after defining globals from .modules import MultiHeadAttention, RotaryPositionEmbedder +from .full_attn import scaled_dot_product_attention __all__ = [ - 'set_attention_backend', - 'get_attention_op', - 'ATTN_BACKEND', - 'DEBUG', + 'scaled_dot_product_attention', 'BACKEND', + 'DEBUG', 'MultiHeadAttention', 'RotaryPositionEmbedder' ] diff --git a/trellis/modules/attention/full_attn.py b/trellis/modules/attention/full_attn.py index 68303dc..c34c91e 100644 --- a/trellis/modules/attention/full_attn.py +++ b/trellis/modules/attention/full_attn.py @@ -1,18 +1,36 @@ from typing import * import torch import math -from . import DEBUG, BACKEND +from trellis.backend_config import ( + get_attention_backend, + get_debug_mode, + get_available_backends +) +import logging -if BACKEND == 'xformers': +logger = logging.getLogger(__name__) + +# Get configuration from central config +BACKEND = get_attention_backend() +DEBUG = get_debug_mode() + +# Get available backends and import if active +available_backends = get_available_backends() + +if BACKEND == "xformers" and available_backends['xformers']: import xformers.ops as xops -elif BACKEND == 'flash_attn': +elif BACKEND == "flash_attn" and available_backends['flash_attn']: import flash_attn -elif BACKEND == 'sdpa': +elif BACKEND == "sage" and available_backends['sage']: + import torch.nn.functional as F + from sageattention import sageattn + F.scaled_dot_product_attention = sageattn +elif BACKEND == "sdpa": from torch.nn.functional import scaled_dot_product_attention as sdpa -elif BACKEND == 'naive': - pass +elif BACKEND == "naive": + from torch.nn.functional import scaled_dot_product_attention as naive else: - raise ValueError(f"Unknown attention backend: {BACKEND}") + raise ValueError(f"Unknown attention module: {BACKEND}") __all__ = [ @@ -137,4 +155,4 @@ def scaled_dot_product_attention(*args, **kwargs): else: raise ValueError(f"Unknown attention module: {BACKEND}") - return out + return out \ No newline at end of file diff --git a/trellis/modules/repomix-output.txt b/trellis/modules/repomix-output.txt new file mode 100644 index 0000000..40795da --- /dev/null +++ b/trellis/modules/repomix-output.txt @@ -0,0 +1,2461 @@ +This file is a merged representation of the entire codebase, combining all repository files into a single document. +Generated by Repomix on: 2025-01-12T00:27:20.764Z + +================================================================ +File Summary +================================================================ + +Purpose: +-------- +This file contains a packed representation of the entire repository's contents. +It is designed to be easily consumable by AI systems for analysis, code review, +or other automated processes. + +File Format: +------------ +The content is organized as follows: +1. This summary section +2. Repository information +3. Repository structure +4. Multiple file entries, each consisting of: + a. A separator line (================) + b. The file path (File: path/to/file) + c. Another separator line + d. The full contents of the file + e. A blank line + +Usage Guidelines: +----------------- +- This file should be treated as read-only. Any changes should be made to the + original repository files, not this packed version. +- When processing this file, use the file path to distinguish + between different files in the repository. +- Be aware that this file may contain sensitive information. Handle it with + the same level of security as you would the original repository. + +Notes: +------ +- Some files may have been excluded based on .gitignore rules and Repomix's + configuration. +- Binary files are not included in this packed representation. Please refer to + the Repository Structure section for a complete list of file paths, including + binary files. + +Additional Info: +---------------- + +For more information about Repomix, visit: https://github.com/yamadashy/repomix + +================================================================ +Repository Structure +================================================================ +__init__.py +attention_utils.py +attention/__init__.py +attention/full_attn.py +attention/modules.py +norm.py +sparse/__init__.py +sparse/attention/__init__.py +sparse/attention/full_attn.py +sparse/attention/modules.py +sparse/attention/serialized_attn.py +sparse/attention/windowed_attn.py +sparse/basic.py +sparse/conv/__init__.py +sparse/conv/conv_spconv.py +sparse/conv/conv_torchsparse.py +sparse/linear.py +sparse/nonlinearity.py +sparse/norm.py +sparse/spatial.py +spatial.py +utils.py + +================================================================ +Repository Files +================================================================ + +================ +File: __init__.py +================ +import logging +from trellis.backend_config import ( + get_attention_backend, + get_available_backends +) +logger = logging.getLogger(__name__) + +# Import modules +from .attention import MultiHeadAttention, RotaryPositionEmbedder + + +__all__ = [ + 'get_attention_backend', + 'MultiHeadAttention', + 'RotaryPositionEmbedder', + 'get_available_backends' +] + +================ +File: attention_utils.py +================ +#sage_attn.py +import os +from typing import Optional +import torch +import torch.nn.functional as F +from sageattention import sageattn +import math + +__all__ = ['SageAttention', 'sage_attention'] + + +def enable_sage_attention(): + """ + Enable SageAttention by replacing PyTorch's scaled_dot_product_attention + with sageattn from the SageAttention library. + """ + F.scaled_dot_product_attention = sageattn + return True + +def disable_sage_attention(): + """ + Restore PyTorch's original scaled_dot_product_attention function. + """ + F.scaled_dot_product_attention = torch.nn.functional.scaled_dot_product_attention + return True + +================ +File: attention/__init__.py +================ +import os +import logging +from typing import Literal +from trellis.backend_config import ( + get_attention_backend, + get_debug_mode, +) +import logging + +logger = logging.getLogger(__name__) + +BACKEND = get_attention_backend() +DEBUG = get_debug_mode() + +def __from_env(): + """Read current backend configuration""" + global BACKEND + global DEBUG + + # Get current settings from central config + BACKEND = get_attention_backend() + DEBUG = get_debug_mode() + + print(f"[ATTENTION] Using backend: {BACKEND}") + +from .modules import MultiHeadAttention, RotaryPositionEmbedder +from .full_attn import scaled_dot_product_attention + +__all__ = [ + 'MultiHeadAttention', + 'RotaryPositionEmbedder', + 'scaled_dot_product_attention' +] + +================ +File: attention/full_attn.py +================ +from typing import * +import torch +import math +import os +import logging +from typing import * +import torch +import math +import logging +from trellis.backend_config import get_attention_backend + +logger = logging.getLogger(__name__) + +# Get the backend from central config +ATTN_BACKEND = get_attention_backend() + +# Import available backends based on central configuration +if ATTN_BACKEND == "xformers": + import xformers.ops as xops +elif ATTN_BACKEND == "flash_attn": + import flash_attn +elif ATTN_BACKEND == "sage": + import sageattention + sageattn = sageattention.sageattn + from sageattention.core import sageattn_varlen +elif ATTN_BACKEND == "sdpa": + from torch.nn.functional import scaled_dot_product_attention as sdpa + +# Log the active backend +logger.info(f"Using attention backend: {ATTN_BACKEND}") + +__all__ = [ + 'scaled_dot_product_attention', +] + + +def _naive_sdpa(q, k, v): + """ + Naive implementation of scaled dot product attention. + """ + q = q.permute(0, 2, 1, 3) # [N, H, L, C] + k = k.permute(0, 2, 1, 3) # [N, H, L, C] + v = v.permute(0, 2, 1, 3) # [N, H, L, C] + scale_factor = 1 / math.sqrt(q.size(-1)) + attn_weight = q @ k.transpose(-2, -1) * scale_factor + attn_weight = torch.softmax(attn_weight, dim=-1) + out = attn_weight @ v + out = out.permute(0, 2, 1, 3) # [N, L, H, C] + return out + + +@overload +def scaled_dot_product_attention(qkv: torch.Tensor) -> torch.Tensor: + """ + Apply scaled dot product attention. + + Args: + qkv (torch.Tensor): A [N, L, 3, H, C] tensor containing Qs, Ks, and Vs. + """ + ... + +@overload +def scaled_dot_product_attention(q: torch.Tensor, kv: torch.Tensor) -> torch.Tensor: + """ + Apply scaled dot product attention. + + Args: + q (torch.Tensor): A [N, L, H, C] tensor containing Qs. + kv (torch.Tensor): A [N, L, 2, H, C] tensor containing Ks and Vs. + """ + ... + +@overload +def scaled_dot_product_attention(q: torch.Tensor, k: torch.Tensor, v: torch.Tensor) -> torch.Tensor: + """ + Apply scaled dot product attention. + + Args: + q (torch.Tensor): A [N, L, H, Ci] tensor containing Qs. + k (torch.Tensor): A [N, L, H, Ci] tensor containing Ks. + v (torch.Tensor): A [N, L, H, Co] tensor containing Vs. + + Note: + k and v are assumed to have the same coordinate map. + """ + ... + +def scaled_dot_product_attention(*args, **kwargs): + arg_names_dict = { + 1: ['qkv'], + 2: ['q', 'kv'], + 3: ['q', 'k', 'v'] + } + num_all_args = len(args) + len(kwargs) + assert num_all_args in arg_names_dict, f"Invalid number of arguments, got {num_all_args}, expected 1, 2, or 3" + for key in arg_names_dict[num_all_args][len(args):]: + assert key in kwargs, f"Missing argument {key}" + + if num_all_args == 1: + qkv = args[0] if len(args) > 0 else kwargs['qkv'] + assert len(qkv.shape) == 5 and qkv.shape[2] == 3, f"Invalid shape for qkv, got {qkv.shape}, expected [N, L, 3, H, C]" + device = qkv.device + + elif num_all_args == 2: + q = args[0] if len(args) > 0 else kwargs['q'] + kv = args[1] if len(args) > 1 else kwargs['kv'] + assert q.shape[0] == kv.shape[0], f"Batch size mismatch, got {q.shape[0]} and {kv.shape[0]}" + assert len(q.shape) == 4, f"Invalid shape for q, got {q.shape}, expected [N, L, H, C]" + assert len(kv.shape) == 5, f"Invalid shape for kv, got {kv.shape}, expected [N, L, 2, H, C]" + device = q.device + + elif num_all_args == 3: + q = args[0] if len(args) > 0 else kwargs['q'] + k = args[1] if len(args) > 1 else kwargs['k'] + v = args[2] if len(args) > 2 else kwargs['v'] + assert q.shape[0] == k.shape[0] == v.shape[0], f"Batch size mismatch, got {q.shape[0]}, {k.shape[0]}, and {v.shape[0]}" + assert len(q.shape) == 4, f"Invalid shape for q, got {q.shape}, expected [N, L, H, Ci]" + assert len(k.shape) == 4, f"Invalid shape for k, got {k.shape}, expected [N, L, H, Ci]" + assert len(v.shape) == 4, f"Invalid shape for v, got {v.shape}, expected [N, L, H, Co]" + device = q.device + + # Implementation based on backend + if ATTN_BACKEND == "xformers": + if num_all_args == 1: + q, k, v = qkv.unbind(dim=2) + elif num_all_args == 2: + k, v = kv.unbind(dim=2) + out = xops.memory_efficient_attention(q, k, v) + elif ATTN_BACKEND == "flash_attn": + if num_all_args == 1: + out = flash_attn.flash_attn_qkvpacked_func(qkv) + elif num_all_args == 2: + out = flash_attn.flash_attn_kvpacked_func(q, kv) + elif num_all_args == 3: + out = flash_attn.flash_attn_func(q, k, v) + elif ATTN_BACKEND == "sdpa": + if num_all_args == 1: + q, k, v = qkv.unbind(dim=2) + elif num_all_args == 2: + k, v = kv.unbind(dim=2) + q = q.permute(0, 2, 1, 3) # [N, H, L, C] + k = k.permute(0, 2, 1, 3) # [N, H, L, C] + v = v.permute(0, 2, 1, 3) # [N, H, L, C] + if "attn_mask" in kwargs: + out = sdpa(q, k, v, attn_mask=kwargs["attn_mask"]) + else: + out = sdpa(q, k, v) + out = out.permute(0, 2, 1, 3) # [N, L, H, C] + elif ATTN_BACKEND == 'sage': + q = q.permute(0, 2, 1, 3).contiguous() # [N, H, L, C] + k = k.permute(0, 2, 1, 3).contiguous() # [N, H, L, C] + v = v.permute(0, 2, 1, 3).contiguous() # [N, H, L, C] + out = sageattn(q, k, v, tensor_layout="HND") + out = out.permute(0, 2, 1, 3) # [N, L, H, C] + elif ATTN_BACKEND == "naive": + if num_all_args == 1: + q, k, v = qkv.unbind(dim=2) + elif num_all_args == 2: + k, v = kv.unbind(dim=2) + out = _naive_sdpa(q, k, v) + else: + raise ValueError(f"Unknown attention module: {ATTN_BACKEND}") + + return out + +================ +File: attention/modules.py +================ +from typing import * +import torch +import torch.nn as nn +import torch.nn.functional as F +from .full_attn import scaled_dot_product_attention + + +class MultiHeadRMSNorm(nn.Module): + def __init__(self, dim: int, heads: int): + super().__init__() + self.scale = dim ** 0.5 + self.gamma = nn.Parameter(torch.ones(heads, dim)) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + return (F.normalize(x.float(), dim = -1) * self.gamma * self.scale).to(x.dtype) + + +class RotaryPositionEmbedder(nn.Module): + def __init__(self, hidden_size: int, in_channels: int = 3): + super().__init__() + assert hidden_size % 2 == 0, "Hidden size must be divisible by 2" + self.hidden_size = hidden_size + self.in_channels = in_channels + self.freq_dim = hidden_size // in_channels // 2 + self.freqs = torch.arange(self.freq_dim, dtype=torch.float32) / self.freq_dim + self.freqs = 1.0 / (10000 ** self.freqs) + + def _get_phases(self, indices: torch.Tensor) -> torch.Tensor: + self.freqs = self.freqs.to(indices.device) + phases = torch.outer(indices, self.freqs) + phases = torch.polar(torch.ones_like(phases), phases) + return phases + + def _rotary_embedding(self, x: torch.Tensor, phases: torch.Tensor) -> torch.Tensor: + x_complex = torch.view_as_complex(x.float().reshape(*x.shape[:-1], -1, 2)) + x_rotated = x_complex * phases + x_embed = torch.view_as_real(x_rotated).reshape(*x_rotated.shape[:-1], -1).to(x.dtype) + return x_embed + + def forward(self, q: torch.Tensor, k: torch.Tensor, indices: Optional[torch.Tensor] = None) -> Tuple[torch.Tensor, torch.Tensor]: + """ + Args: + q (sp.SparseTensor): [..., N, D] tensor of queries + k (sp.SparseTensor): [..., N, D] tensor of keys + indices (torch.Tensor): [..., N, C] tensor of spatial positions + """ + if indices is None: + indices = torch.arange(q.shape[-2], device=q.device) + if len(q.shape) > 2: + indices = indices.unsqueeze(0).expand(q.shape[:-2] + (-1,)) + + phases = self._get_phases(indices.reshape(-1)).reshape(*indices.shape[:-1], -1) + if phases.shape[1] < self.hidden_size // 2: + phases = torch.cat([phases, torch.polar( + torch.ones(*phases.shape[:-1], self.hidden_size // 2 - phases.shape[1], device=phases.device), + torch.zeros(*phases.shape[:-1], self.hidden_size // 2 - phases.shape[1], device=phases.device) + )], dim=-1) + q_embed = self._rotary_embedding(q, phases) + k_embed = self._rotary_embedding(k, phases) + return q_embed, k_embed + + +class MultiHeadAttention(nn.Module): + def __init__( + self, + channels: int, + num_heads: int, + ctx_channels: Optional[int]=None, + type: Literal["self", "cross"] = "self", + attn_mode: Literal["full", "windowed"] = "full", + window_size: Optional[int] = None, + shift_window: Optional[Tuple[int, int, int]] = None, + qkv_bias: bool = True, + use_rope: bool = False, + qk_rms_norm: bool = False, + ): + super().__init__() + assert channels % num_heads == 0 + assert type in ["self", "cross"], f"Invalid attention type: {type}" + assert attn_mode in ["full", "windowed"], f"Invalid attention mode: {attn_mode}" + assert type == "self" or attn_mode == "full", "Cross-attention only supports full attention" + + if attn_mode == "windowed": + raise NotImplementedError("Windowed attention is not yet implemented") + + self.channels = channels + self.head_dim = channels // num_heads + self.ctx_channels = ctx_channels if ctx_channels is not None else channels + self.num_heads = num_heads + self._type = type + self.attn_mode = attn_mode + self.window_size = window_size + self.shift_window = shift_window + self.use_rope = use_rope + self.qk_rms_norm = qk_rms_norm + + if self._type == "self": + self.to_qkv = nn.Linear(channels, channels * 3, bias=qkv_bias) + else: + self.to_q = nn.Linear(channels, channels, bias=qkv_bias) + self.to_kv = nn.Linear(self.ctx_channels, channels * 2, bias=qkv_bias) + + if self.qk_rms_norm: + self.q_rms_norm = MultiHeadRMSNorm(self.head_dim, num_heads) + self.k_rms_norm = MultiHeadRMSNorm(self.head_dim, num_heads) + + self.to_out = nn.Linear(channels, channels) + + if use_rope: + self.rope = RotaryPositionEmbedder(channels) + + def forward(self, x: torch.Tensor, context: Optional[torch.Tensor] = None, indices: Optional[torch.Tensor] = None) -> torch.Tensor: + B, L, C = x.shape + if self._type == "self": + qkv = self.to_qkv(x) + qkv = qkv.reshape(B, L, 3, self.num_heads, -1) + if self.use_rope: + q, k, v = qkv.unbind(dim=2) + q, k = self.rope(q, k, indices) + qkv = torch.stack([q, k, v], dim=2) + if self.attn_mode == "full": + if self.qk_rms_norm: + q, k, v = qkv.unbind(dim=2) + q = self.q_rms_norm(q) + k = self.k_rms_norm(k) + h = scaled_dot_product_attention(q, k, v) + else: + h = scaled_dot_product_attention(qkv) + elif self.attn_mode == "windowed": + raise NotImplementedError("Windowed attention is not yet implemented") + else: + Lkv = context.shape[1] + q = self.to_q(x) + kv = self.to_kv(context) + q = q.reshape(B, L, self.num_heads, -1) + kv = kv.reshape(B, Lkv, 2, self.num_heads, -1) + if self.qk_rms_norm: + q = self.q_rms_norm(q) + k, v = kv.unbind(dim=2) + k = self.k_rms_norm(k) + h = scaled_dot_product_attention(q, k, v) + else: + h = scaled_dot_product_attention(q, kv) + h = h.reshape(B, L, -1) + h = self.to_out(h) + return h + +================ +File: norm.py +================ +import torch +import torch.nn as nn + + +class LayerNorm32(nn.LayerNorm): + def forward(self, x: torch.Tensor) -> torch.Tensor: + return super().forward(x.float()).type(x.dtype) + + +class GroupNorm32(nn.GroupNorm): + """ + A GroupNorm layer that converts to float32 before the forward pass. + """ + def forward(self, x: torch.Tensor) -> torch.Tensor: + return super().forward(x.float()).type(x.dtype) + + +class ChannelLayerNorm32(LayerNorm32): + def forward(self, x: torch.Tensor) -> torch.Tensor: + DIM = x.dim() + x = x.permute(0, *range(2, DIM), 1).contiguous() + x = super().forward(x) + x = x.permute(0, DIM-1, *range(1, DIM-1)).contiguous() + return x + +================ +File: sparse/__init__.py +================ +from typing import * +import importlib +import logging +from trellis.backend_config import ( + get_sparse_backend, + get_debug_mode, +) + +# Get configuration from central backend config +BACKEND = get_sparse_backend() +DEBUG = get_debug_mode() + +def __from_env(): + """Read current backend configuration""" + global BACKEND + global DEBUG + + # Get current settings from central config + BACKEND = get_sparse_backend() + DEBUG = get_debug_mode() + + print(f"[SPARSE] Using backend: {BACKEND}") + +logger = logging.getLogger(__name__) + +# Define module attributes and their corresponding files +__attributes = { + 'SparseTensor': 'basic', + 'sparse_batch_broadcast': 'basic', + 'sparse_batch_op': 'basic', + 'sparse_cat': 'basic', + 'sparse_unbind': 'basic', + 'SparseGroupNorm': 'norm', + 'SparseLayerNorm': 'norm', + 'SparseGroupNorm32': 'norm', + 'SparseLayerNorm32': 'norm', + 'SparseReLU': 'nonlinearity', + 'SparseSiLU': 'nonlinearity', + 'SparseGELU': 'nonlinearity', + 'SparseActivation': 'nonlinearity', + 'SparseLinear': 'linear', + 'sparse_scaled_dot_product_attention': 'attention.full_attn', + 'SerializeMode': 'attention.serialized_attn', + 'sparse_serialized_scaled_dot_product_self_attention': 'attention.serialized_attn', + 'sparse_windowed_scaled_dot_product_self_attention': 'attention.windowed_attn', + 'SparseMultiHeadAttention': 'attention.modules', + 'SparseConv3d': 'conv', + 'SparseInverseConv3d': 'conv', + 'SparseDownsample': 'spatial', + 'SparseUpsample': 'spatial', + 'SparseSubdivide': 'spatial', +} + +__submodules = [ + 'transformer', # for blocks.py, modulated.py, etc. +] + +__all__ = list(__attributes.keys()) + __submodules + +def __getattr__(name: str): + """Lazy import logic for submodules""" + if name in __attributes: + module_name = __attributes[name] + submod = importlib.import_module(f".{module_name}", __name__) + return getattr(submod, name) + elif name in __submodules: + return importlib.import_module(f".{name}", __name__) + else: + raise AttributeError(f"Module '{__name__}' has no attribute '{name}'.") + +logger.debug(f"Initialized trellis.modules.sparse package in lazy mode using backend: {BACKEND}") + +# Initialize settings from environment +__from_env() + +# For Pylance/type checking +if __name__ == '__main__': + from .basic import * + from .norm import * + from .nonlinearity import * + from .linear import * + from .attention import * + from .conv import * + from .spatial import * + import transformer + +================ +File: sparse/attention/__init__.py +================ +from .full_attn import * +from .serialized_attn import * +from .windowed_attn import * +from .modules import * + +# Re-export SerializeMode from serialized_attn +from .serialized_attn import SerializeMode + +__all__ = [ + 'sparse_scaled_dot_product_attention', + 'sparse_serialized_scaled_dot_product_self_attention', + 'sparse_windowed_scaled_dot_product_self_attention', + 'SparseMultiHeadAttention', + 'SerializeMode' +] + +================ +File: sparse/attention/full_attn.py +================ +from typing import * +import torch +import math +from .. import SparseTensor +from trellis.backend_config import ( + get_attention_backend, + get_debug_mode, + get_available_backends +) +import logging + +logger = logging.getLogger(__name__) + +# Get configuration from central config +ATTN_BACKEND = get_attention_backend() +DEBUG = get_debug_mode() + +# Get available backends and import if active +available_backends = get_available_backends() + +if ATTN_BACKEND == "xformers" and available_backends['xformers']: + import xformers.ops as xops +elif ATTN_BACKEND == "flash_attn" and available_backends['flash_attn']: + import flash_attn +elif ATTN_BACKEND == "sage" and available_backends['sage']: + import sageattention + sageattn = sageattention.sageattn + from sageattention.core import sageattn_varlen +elif ATTN_BACKEND == "sdpa": + from torch.nn.functional import scaled_dot_product_attention as sdpa + +# Log the active backend +logger.info(f"Using attention backend: {ATTN_BACKEND}") + +__all__ = ['sparse_scaled_dot_product_attention'] + +@overload +def sparse_scaled_dot_product_attention(qkv: SparseTensor) -> SparseTensor: + """ + Apply scaled dot product attention to a sparse tensor. + + Args: + qkv (SparseTensor): A [N, *, 3, H, C] sparse tensor containing Qs, Ks, and Vs. + """ + ... + +@overload +def sparse_scaled_dot_product_attention(q: SparseTensor, kv: Union[SparseTensor, torch.Tensor]) -> SparseTensor: + """ + Apply scaled dot product attention to a sparse tensor. + + Args: + q (SparseTensor): A [N, *, H, C] sparse tensor containing Qs. + kv (SparseTensor or torch.Tensor): A [N, *, 2, H, C] sparse tensor or a [N, L, 2, H, C] dense tensor containing Ks and Vs. + """ + ... + +@overload +def sparse_scaled_dot_product_attention(q: torch.Tensor, kv: SparseTensor) -> torch.Tensor: + """ + Apply scaled dot product attention to a sparse tensor. + + Args: + q (SparseTensor): A [N, L, H, C] dense tensor containing Qs. + kv (SparseTensor or torch.Tensor): A [N, *, 2, H, C] sparse tensor containing Ks and Vs. + """ + ... + +@overload +def sparse_scaled_dot_product_attention(q: SparseTensor, k: SparseTensor, v: SparseTensor) -> SparseTensor: + """ + Apply scaled dot product attention to a sparse tensor. + + Args: + q (SparseTensor): A [N, *, H, Ci] sparse tensor containing Qs. + k (SparseTensor): A [N, *, H, Ci] sparse tensor containing Ks. + v (SparseTensor): A [N, *, H, Co] sparse tensor containing Vs. + + Note: + k and v are assumed to have the same coordinate map. + """ + ... + +@overload +def sparse_scaled_dot_product_attention(q: SparseTensor, k: torch.Tensor, v: torch.Tensor) -> SparseTensor: + """ + Apply scaled dot product attention to a sparse tensor. + + Args: + q (SparseTensor): A [N, *, H, Ci] sparse tensor containing Qs. + k (torch.Tensor): A [N, L, H, Ci] dense tensor containing Ks. + v (torch.Tensor): A [N, L, H, Co] dense tensor containing Vs. + """ + ... + +@overload +def sparse_scaled_dot_product_attention(q: torch.Tensor, k: SparseTensor, v: SparseTensor) -> torch.Tensor: + """ + Apply scaled dot product attention to a sparse tensor. + + Args: + q (torch.Tensor): A [N, L, H, Ci] dense tensor containing Qs. + k (SparseTensor): A [N, *, H, Ci] sparse tensor containing Ks. + v (SparseTensor): A [N, *, H, Co] sparse tensor containing Vs. + """ + ... + +def sparse_scaled_dot_product_attention(*args, **kwargs): + arg_names_dict = { + 1: ['qkv'], + 2: ['q', 'kv'], + 3: ['q', 'k', 'v'] + } + num_all_args = len(args) + len(kwargs) + assert num_all_args in arg_names_dict, f"Invalid number of arguments, got {num_all_args}, expected 1, 2, or 3" + for key in arg_names_dict[num_all_args][len(args):]: + assert key in kwargs, f"Missing argument {key}" + + if num_all_args == 1: + qkv = args[0] if len(args) > 0 else kwargs['qkv'] + assert isinstance(qkv, SparseTensor), f"qkv must be a SparseTensor, got {type(qkv)}" + assert len(qkv.shape) == 4 and qkv.shape[1] == 3, f"Invalid shape for qkv, got {qkv.shape}, expected [N, *, 3, H, C]" + device = qkv.device + + s = qkv + q_seqlen = [qkv.layout[i].stop - qkv.layout[i].start for i in range(qkv.shape[0])] + kv_seqlen = q_seqlen + qkv = qkv.feats # [T, 3, H, C] + + elif num_all_args == 2: + q = args[0] if len(args) > 0 else kwargs['q'] + kv = args[1] if len(args) > 1 else kwargs['kv'] + assert isinstance(q, SparseTensor) and isinstance(kv, (SparseTensor, torch.Tensor)) or \ + isinstance(q, torch.Tensor) and isinstance(kv, SparseTensor), \ + f"Invalid types, got {type(q)} and {type(kv)}" + assert q.shape[0] == kv.shape[0], f"Batch size mismatch, got {q.shape[0]} and {kv.shape[0]}" + device = q.device + + if isinstance(q, SparseTensor): + assert len(q.shape) == 3, f"Invalid shape for q, got {q.shape}, expected [N, *, H, C]" + s = q + q_seqlen = [q.layout[i].stop - q.layout[i].start for i in range(q.shape[0])] + q = q.feats # [T_Q, H, C] + else: + assert len(q.shape) == 4, f"Invalid shape for q, got {q.shape}, expected [N, L, H, C]" + s = None + N, L, H, C = q.shape + q_seqlen = [L] * N + q = q.reshape(N * L, H, C) # [T_Q, H, C] + + if isinstance(kv, SparseTensor): + assert len(kv.shape) == 4 and kv.shape[1] == 2, f"Invalid shape for kv, got {kv.shape}, expected [N, *, 2, H, C]" + kv_seqlen = [kv.layout[i].stop - kv.layout[i].start for i in range(kv.shape[0])] + kv = kv.feats # [T_KV, 2, H, C] + else: + assert len(kv.shape) == 5, f"Invalid shape for kv, got {kv.shape}, expected [N, L, 2, H, C]" + N, L, _, H, C = kv.shape + kv_seqlen = [L] * N + kv = kv.reshape(N * L, 2, H, C) # [T_KV, 2, H, C] + + elif num_all_args == 3: + q = args[0] if len(args) > 0 else kwargs['q'] + k = args[1] if len(args) > 1 else kwargs['k'] + v = args[2] if len(args) > 2 else kwargs['v'] + assert isinstance(q, SparseTensor) and isinstance(k, (SparseTensor, torch.Tensor)) and type(k) == type(v) or \ + isinstance(q, torch.Tensor) and isinstance(k, SparseTensor) and isinstance(v, SparseTensor), \ + f"Invalid types, got {type(q)}, {type(k)}, and {type(v)}" + assert q.shape[0] == k.shape[0] == v.shape[0], f"Batch size mismatch, got {q.shape[0]}, {k.shape[0]}, and {v.shape[0]}" + device = q.device + + if isinstance(q, SparseTensor): + assert len(q.shape) == 3, f"Invalid shape for q, got {q.shape}, expected [N, *, H, Ci]" + s = q + q_seqlen = [q.layout[i].stop - q.layout[i].start for i in range(q.shape[0])] + q = q.feats # [T_Q, H, Ci] + else: + assert len(q.shape) == 4, f"Invalid shape for q, got {q.shape}, expected [N, L, H, Ci]" + s = None + N, L, H, CI = q.shape + q_seqlen = [L] * N + q = q.reshape(N * L, H, CI) # [T_Q, H, Ci] + + if isinstance(k, SparseTensor): + assert len(k.shape) == 3, f"Invalid shape for k, got {k.shape}, expected [N, *, H, Ci]" + assert len(v.shape) == 3, f"Invalid shape for v, got {v.shape}, expected [N, *, H, Co]" + kv_seqlen = [k.layout[i].stop - k.layout[i].start for i in range(k.shape[0])] + k = k.feats # [T_KV, H, Ci] + v = v.feats # [T_KV, H, Co] + else: + assert len(k.shape) == 4, f"Invalid shape for k, got {k.shape}, expected [N, L, H, Ci]" + assert len(v.shape) == 4, f"Invalid shape for v, got {v.shape}, expected [N, L, H, Co]" + N, L, H, CI, CO = *k.shape, v.shape[-1] + kv_seqlen = [L] * N + k = k.reshape(N * L, H, CI) # [T_KV, H, Ci] + v = v.reshape(N * L, H, CO) # [T_KV, H, Co] + + if DEBUG: + if s is not None: + for i in range(s.shape[0]): + assert (s.coords[s.layout[i]] == i).all(), f"SparseScaledDotProductSelfAttention: batch index mismatch" + if num_all_args in [2, 3]: + assert q.shape[:2] == [1, sum(q_seqlen)], f"SparseScaledDotProductSelfAttention: q shape mismatch" + if num_all_args == 3: + assert k.shape[:2] == [1, sum(kv_seqlen)], f"SparseScaledDotProductSelfAttention: k shape mismatch" + assert v.shape[:2] == [1, sum(kv_seqlen)], f"SparseScaledDotProductSelfAttention: v shape mismatch" + + if ATTN_BACKEND == 'xformers': + if num_all_args == 1: + q, k, v = qkv.unbind(dim=1) + elif num_all_args == 2: + k, v = kv.unbind(dim=1) + q = q.unsqueeze(0) + k = k.unsqueeze(0) + v = v.unsqueeze(0) + mask = xops.fmha.BlockDiagonalMask.from_seqlens(q_seqlen, kv_seqlen) + out = xops.memory_efficient_attention(q, k, v, mask)[0] + elif ATTN_BACKEND == 'flash_attn': + cu_seqlens_q = torch.cat([torch.tensor([0]), torch.cumsum(torch.tensor(q_seqlen), dim=0)]).int().to(device) + if num_all_args in [2, 3]: + cu_seqlens_kv = torch.cat([torch.tensor([0]), torch.cumsum(torch.tensor(kv_seqlen), dim=0)]).int().to(device) + if num_all_args == 1: + out = flash_attn.flash_attn_varlen_qkvpacked_func(qkv, cu_seqlens_q, max(q_seqlen)) + elif num_all_args == 2: + out = flash_attn.flash_attn_varlen_kvpacked_func(q, kv, cu_seqlens_q, cu_seqlens_kv, max(q_seqlen), max(kv_seqlen)) + elif num_all_args == 3: + out = flash_attn.flash_attn_varlen_func(q, k, v, cu_seqlens_q, cu_seqlens_kv, max(q_seqlen), max(kv_seqlen)) + elif ATTN_BACKEND == 'sage': + # SAGE branch + # Usually you have a [N, L, H, C] or [T, H, C], etc. shape in a sparse representation. + # You must reshape or reorder so it's [N, H, L, C]. + # Then call sageattn(q, k, v, is_causal=..., tensor_layout="HND") + # or perhaps sageattn_varlen(...) if your batch sequences differ in length. + + # Example if your 'q' is shaped [N, L, H, C]: + q = q.permute(0, 2, 1, 3).contiguous() + k = k.permute(0, 2, 1, 3).contiguous() + v = v.permute(0, 2, 1, 3).contiguous() + out = sageattn(q, k, v, tensor_layout="HND") + out = out.permute(0, 2, 1, 3) # back to [N, L, H, C] + + # Replace feats in SparseTensor if needed + # e.g. if 's' is your SparseTensor handle: + # return s.replace(out) + else: + raise ValueError(f"Unknown attention module: {ATTN_BACKEND}") + + if s is not None: + return s.replace(out) + else: + return out.reshape(N, L, H, -1) + +================ +File: sparse/attention/modules.py +================ +from typing import * +import torch +import torch.nn as nn +import torch.nn.functional as F +from .. import SparseTensor +from .full_attn import sparse_scaled_dot_product_attention +from .serialized_attn import SerializeMode, sparse_serialized_scaled_dot_product_self_attention +from .windowed_attn import sparse_windowed_scaled_dot_product_self_attention +from ...attention import RotaryPositionEmbedder + + +class SparseMultiHeadRMSNorm(nn.Module): + def __init__(self, dim: int, heads: int): + super().__init__() + self.scale = dim ** 0.5 + self.gamma = nn.Parameter(torch.ones(heads, dim)) + + def forward(self, x: Union[SparseTensor, torch.Tensor]) -> Union[SparseTensor, torch.Tensor]: + x_type = x.dtype + x = x.float() + if isinstance(x, SparseTensor): + x = x.replace(F.normalize(x.feats, dim=-1)) + else: + x = F.normalize(x, dim=-1) + return (x * self.gamma * self.scale).to(x_type) + + +class SparseMultiHeadAttention(nn.Module): + def __init__( + self, + channels: int, + num_heads: int, + ctx_channels: Optional[int] = None, + type: Literal["self", "cross"] = "self", + attn_mode: Literal["full", "serialized", "windowed"] = "full", + window_size: Optional[int] = None, + shift_sequence: Optional[int] = None, + shift_window: Optional[Tuple[int, int, int]] = None, + serialize_mode: Optional[SerializeMode] = None, + qkv_bias: bool = True, + use_rope: bool = False, + qk_rms_norm: bool = False, + ): + super().__init__() + assert channels % num_heads == 0 + assert type in ["self", "cross"], f"Invalid attention type: {type}" + assert attn_mode in ["full", "serialized", "windowed"], f"Invalid attention mode: {attn_mode}" + assert type == "self" or attn_mode == "full", "Cross-attention only supports full attention" + assert type == "self" or use_rope is False, "Rotary position embeddings only supported for self-attention" + self.channels = channels + self.ctx_channels = ctx_channels if ctx_channels is not None else channels + self.num_heads = num_heads + self._type = type + self.attn_mode = attn_mode + self.window_size = window_size + self.shift_sequence = shift_sequence + self.shift_window = shift_window + self.serialize_mode = serialize_mode + self.use_rope = use_rope + self.qk_rms_norm = qk_rms_norm + + if self._type == "self": + self.to_qkv = nn.Linear(channels, channels * 3, bias=qkv_bias) + else: + self.to_q = nn.Linear(channels, channels, bias=qkv_bias) + self.to_kv = nn.Linear(self.ctx_channels, channels * 2, bias=qkv_bias) + + if self.qk_rms_norm: + self.q_rms_norm = SparseMultiHeadRMSNorm(channels // num_heads, num_heads) + self.k_rms_norm = SparseMultiHeadRMSNorm(channels // num_heads, num_heads) + + self.to_out = nn.Linear(channels, channels) + + if use_rope: + self.rope = RotaryPositionEmbedder(channels) + + @staticmethod + def _linear(module: nn.Linear, x: Union[SparseTensor, torch.Tensor]) -> Union[SparseTensor, torch.Tensor]: + if isinstance(x, SparseTensor): + # Reshape features to match expected dimensions + feats = x.feats + if feats.shape[-1] != module.in_features: + feats = feats.view(-1, module.in_features) + return x.replace(module(feats)) + else: + # Reshape tensor to match expected dimensions + if x.shape[-1] != module.in_features: + x = x.view(-1, module.in_features) + return module(x) + + @staticmethod + def _reshape_chs(x: Union[SparseTensor, torch.Tensor], shape: Tuple[int, ...]) -> Union[SparseTensor, torch.Tensor]: + if isinstance(x, SparseTensor): + return x.reshape(*shape) + else: + return x.reshape(*x.shape[:2], *shape) + + def _fused_pre(self, x: Union[SparseTensor, torch.Tensor], num_fused: int) -> Union[SparseTensor, torch.Tensor]: + if isinstance(x, SparseTensor): + x_feats = x.feats.unsqueeze(0) + else: + x_feats = x + x_feats = x_feats.reshape(*x_feats.shape[:2], num_fused, self.num_heads, -1) + return x.replace(x_feats.squeeze(0)) if isinstance(x, SparseTensor) else x_feats + + def _rope(self, qkv: SparseTensor) -> SparseTensor: + q, k, v = qkv.feats.unbind(dim=1) # [T, H, C] + q, k = self.rope(q, k, qkv.coords[:, 1:]) + qkv = qkv.replace(torch.stack([q, k, v], dim=1)) + return qkv + + def forward(self, x: Union[SparseTensor, torch.Tensor], context: Optional[Union[SparseTensor, torch.Tensor]] = None) -> Union[SparseTensor, torch.Tensor]: + if self._type == "self": + qkv = self._linear(self.to_qkv, x) + qkv = self._fused_pre(qkv, num_fused=3) + if self.use_rope: + qkv = self._rope(qkv) + if self.qk_rms_norm: + q, k, v = qkv.unbind(dim=1) + q = self.q_rms_norm(q) + k = self.k_rms_norm(k) + qkv = qkv.replace(torch.stack([q.feats, k.feats, v.feats], dim=1)) + if self.attn_mode == "full": + h = sparse_scaled_dot_product_attention(qkv) + elif self.attn_mode == "serialized": + h = sparse_serialized_scaled_dot_product_self_attention( + qkv, self.window_size, serialize_mode=self.serialize_mode, shift_sequence=self.shift_sequence, shift_window=self.shift_window + ) + elif self.attn_mode == "windowed": + h = sparse_windowed_scaled_dot_product_self_attention( + qkv, self.window_size, shift_window=self.shift_window, qk_rms_norm=self.qk_rms_norm + ) + else: + q = self._linear(self.to_q, x) + q = self._reshape_chs(q, (self.num_heads, -1)) + kv = self._linear(self.to_kv, context) + kv = self._fused_pre(kv, num_fused=2) + if self.qk_rms_norm: + q = self.q_rms_norm(q) + k, v = kv.unbind(dim=1) + k = self.k_rms_norm(k) + kv = kv.replace(torch.stack([k.feats, v.feats], dim=1)) + h = sparse_scaled_dot_product_attention(q, kv) + h = self._reshape_chs(h, (-1,)) + h = self._linear(self.to_out, h) + return h + +================ +File: sparse/attention/serialized_attn.py +================ +from typing import * +from enum import Enum +import torch +import math +from .. import SparseTensor +from trellis.backend_config import ( + get_attention_backend, + get_debug_mode, + get_available_backends +) +import logging + +logger = logging.getLogger(__name__) + +# Get configuration from central config +ATTN_BACKEND = get_attention_backend() +DEBUG = get_debug_mode() + +# Get available backends and import if active +available_backends = get_available_backends() + +if ATTN_BACKEND == "xformers" and available_backends['xformers']: + import xformers.ops as xops +elif ATTN_BACKEND == "flash_attn" and available_backends['flash_attn']: + import flash_attn +elif ATTN_BACKEND == "sage" and available_backends['sage']: + import sageattention + sageattn = sageattention.sageattn + from sageattention.core import sageattn_varlen +elif ATTN_BACKEND == "sdpa": + from torch.nn.functional import scaled_dot_product_attention as sdpa + +# Log the active backend +logger.info(f"Using attention backend: {ATTN_BACKEND}") + +__all__ = [ + 'SerializeMode', + 'sparse_serialized_scaled_dot_product_self_attention', +] + +class SerializeMode(Enum): + Z_ORDER = 0 + Z_ORDER_TRANSPOSED = 1 + HILBERT = 2 + HILBERT_TRANSPOSED = 3 + +SerializeModes = [ + SerializeMode.Z_ORDER, + SerializeMode.Z_ORDER_TRANSPOSED, + SerializeMode.HILBERT, + SerializeMode.HILBERT_TRANSPOSED +] + +def calc_serialization( + tensor: SparseTensor, + window_size: int, + serialize_mode: SerializeMode = SerializeMode.Z_ORDER, + shift_sequence: int = 0, + shift_window: Tuple[int, int, int] = (0, 0, 0) +) -> Tuple[torch.Tensor, torch.Tensor, List[int]]: + """ + Calculate serialization and partitioning for a set of coordinates. + + Args: + tensor (SparseTensor): The input tensor. + window_size (int): The window size to use. + serialize_mode (SerializeMode): The serialization mode to use. + shift_sequence (int): The shift of serialized sequence. + shift_window (Tuple[int, int, int]): The shift of serialized coordinates. + + Returns: + (torch.Tensor, torch.Tensor): Forwards and backwards indices. + """ + fwd_indices = [] + bwd_indices = [] + seq_lens = [] + seq_batch_indices = [] + offsets = [0] + + if 'vox2seq' not in globals(): + import vox2seq + + # Serialize the input + serialize_coords = tensor.coords[:, 1:].clone() + serialize_coords += torch.tensor(shift_window, dtype=torch.int32, device=tensor.device).reshape(1, 3) + if serialize_mode == SerializeMode.Z_ORDER: + code = vox2seq.encode(serialize_coords, mode='z_order', permute=[0, 1, 2]) + elif serialize_mode == SerializeMode.Z_ORDER_TRANSPOSED: + code = vox2seq.encode(serialize_coords, mode='z_order', permute=[1, 0, 2]) + elif serialize_mode == SerializeMode.HILBERT: + code = vox2seq.encode(serialize_coords, mode='hilbert', permute=[0, 1, 2]) + elif serialize_mode == SerializeMode.HILBERT_TRANSPOSED: + code = vox2seq.encode(serialize_coords, mode='hilbert', permute=[1, 0, 2]) + else: + raise ValueError(f"Unknown serialize mode: {serialize_mode}") + + for bi, s in enumerate(tensor.layout): + num_points = s.stop - s.start + num_windows = (num_points + window_size - 1) // window_size + valid_window_size = num_points / num_windows + to_ordered = torch.argsort(code[s.start:s.stop]) + if num_windows == 1: + fwd_indices.append(to_ordered) + bwd_indices.append(torch.zeros_like(to_ordered).scatter_(0, to_ordered, torch.arange(num_points, device=tensor.device))) + fwd_indices[-1] += s.start + bwd_indices[-1] += offsets[-1] + seq_lens.append(num_points) + seq_batch_indices.append(bi) + offsets.append(offsets[-1] + seq_lens[-1]) + else: + # Partition the input + offset = 0 + mids = [(i + 0.5) * valid_window_size + shift_sequence for i in range(num_windows)] + split = [math.floor(i * valid_window_size + shift_sequence) for i in range(num_windows + 1)] + bwd_index = torch.zeros((num_points,), dtype=torch.int64, device=tensor.device) + for i in range(num_windows): + mid = mids[i] + valid_start = split[i] + valid_end = split[i + 1] + padded_start = math.floor(mid - 0.5 * window_size) + padded_end = padded_start + window_size + fwd_indices.append(to_ordered[torch.arange(padded_start, padded_end, device=tensor.device) % num_points]) + offset += valid_start - padded_start + bwd_index.scatter_(0, fwd_indices[-1][valid_start-padded_start:valid_end-padded_start], torch.arange(offset, offset + valid_end - valid_start, device=tensor.device)) + offset += padded_end - valid_start + fwd_indices[-1] += s.start + seq_lens.extend([window_size] * num_windows) + seq_batch_indices.extend([bi] * num_windows) + bwd_indices.append(bwd_index + offsets[-1]) + offsets.append(offsets[-1] + num_windows * window_size) + + fwd_indices = torch.cat(fwd_indices) + bwd_indices = torch.cat(bwd_indices) + + return fwd_indices, bwd_indices, seq_lens, seq_batch_indices + + + '''fwd_indices = torch.arange(tensor.feats.shape[0], device=tensor.device) + bwd_indices = torch.arange(tensor.feats.shape[0], device=tensor.device) + seq_lens = [tensor.feats.shape[0]] + seq_batch_indices = [0] + return fwd_indices, bwd_indices, seq_lens, seq_batch_indices''' + + +def sparse_serialized_scaled_dot_product_self_attention( + qkv: SparseTensor, + window_size: int, + serialize_mode: SerializeMode = SerializeMode.Z_ORDER, + shift_sequence: int = 0, + shift_window: Tuple[int, int, int] = (0, 0, 0) +) -> SparseTensor: + """ + Apply serialized scaled dot product self attention to a sparse tensor. + + Args: + qkv (SparseTensor): shape [N, *, 3, H, C] + window_size (int) + serialize_mode (SerializeMode) + shift_sequence (int) + shift_window (Tuple[int,int,int]) + + Returns: + SparseTensor with updated .feats + """ + assert len(qkv.shape) == 4 and qkv.shape[1] == 3, ( + f"Invalid shape for qkv, got {qkv.shape}, expected [N, *, 3, H, C]" + ) + + # Retrieve or compute serialization indices + serialization_cache_name = f'serialization_{serialize_mode}_{window_size}_{shift_sequence}_{shift_window}' + serialization_spatial_cache = qkv.get_spatial_cache(serialization_cache_name) + if serialization_spatial_cache is None: + fwd_indices, bwd_indices, seq_lens, seq_batch_indices = calc_serialization( + qkv, window_size, serialize_mode, shift_sequence, shift_window + ) + qkv.register_spatial_cache(serialization_cache_name, + (fwd_indices, bwd_indices, seq_lens, seq_batch_indices)) + else: + fwd_indices, bwd_indices, seq_lens, seq_batch_indices = serialization_spatial_cache + + # Gather QKV feats in new order + qkv_feats = qkv.feats[fwd_indices] # [M, 3, H, C] + M = qkv_feats.shape[0] + H = qkv_feats.shape[2] + C = qkv_feats.shape[3] + + # Handle uniform vs. variable chunk sizes + if all(s == window_size for s in seq_lens): + # Uniform chunk size + B = len(seq_lens) + N = window_size # chunk length + assert B * N == M, "Mismatched total tokens (B*N != M)" + + qkv_feats = qkv_feats.reshape(B, N, 3, H, C) # [B, N, 3, H, C] + + if ATTN_BACKEND == 'xformers': + q, k, v = qkv_feats.unbind(dim=2) # [B, N, H, C] + out = xops.memory_efficient_attention(q, k, v) # [B, N, H, C] + out = out.reshape(M, H, C) # [M, H, C] + elif ATTN_BACKEND == 'flash_attn': + out = flash_attn.flash_attn_qkvpacked_func(qkv_feats) # [B, N, H, C] + out = out.reshape(M, H, C) + elif ATTN_BACKEND == 'sage': + # SageAttention path + # Re-permute to [B, 3, H, N, C] + qkv_feats = qkv_feats.permute(0, 2, 3, 1, 4).contiguous() # [B, 3, H, N, C] + q, k, v = qkv_feats.unbind(dim=1) # each => [B, H, N, C] + + out = sageattn(q, k, v, tensor_layout="HND") # => [B, H, N, C] + # Flatten back + out = out.permute(0, 2, 1, 3).contiguous() # => [B, N, H, C] + out = out.reshape(M, H, C) + else: + raise ValueError(f"Unknown attention module: {ATTN_BACKEND}") + + else: + # Variable-length chunks + if ATTN_BACKEND == 'xformers': + q, k, v = qkv_feats.unbind(dim=1) # [M, H, C] + q = q.unsqueeze(0) + k = k.unsqueeze(0) + v = v.unsqueeze(0) + mask = xops.fmha.BlockDiagonalMask.from_seqlens(seq_lens) + out = xops.memory_efficient_attention(q, k, v, mask=mask)[0] # [M, H, C] + elif ATTN_BACKEND == 'flash_attn': + cu_seqlens = torch.zeros(len(seq_lens)+1, + dtype=torch.int32, + device=qkv.device) + cu_seqlens[1:] = torch.cumsum(torch.tensor(seq_lens, + device=qkv.device), dim=0) + out = flash_attn.flash_attn_varlen_qkvpacked_func(qkv_feats, cu_seqlens, max(seq_lens)) # => [M, H, C] + elif ATTN_BACKEND == 'sage': + # SageAttention varlen path + # If you want to do "varlen," you can do something like: + cu_seqlens = torch.zeros(len(seq_lens)+1, dtype=torch.int32, device=qkv.device) + cu_seqlens[1:] = torch.cumsum(torch.tensor(seq_lens, device=qkv.device), dim=0) + + # qkv_feats => [M, 3, H, C], reorder to [3, M, H, C] + qkv_feats = qkv_feats.permute(1, 0, 2, 3).contiguous() # [3, M, H, C] + q, k, v = qkv_feats[0], qkv_feats[1], qkv_feats[2] # each => [M, H, C] + + # SageAttention varlen typically expects q, k, v => [M, H, D], etc. + # For is_causal or not, pass as needed + #from sageattention import sageattn_varlen # or define at top + max_len = max(seq_lens) + out = sageattn_varlen( + q, k, v, + cu_seqlens, cu_seqlens, + max_len, max_len, + is_causal=False + ) # => [M, H, C] + else: + raise ValueError(f"Unknown attention module: {ATTN_BACKEND}") + + # Re-map via bwd_indices => original order + out = out[bwd_indices] # [T, H, C] + return qkv.replace(out) + +================ +File: sparse/attention/windowed_attn.py +================ +from typing import * +import torch +import math +import os +import logging +import torch +from .. import SparseTensor +from trellis.backend_config import ( + get_attention_backend, + get_debug_mode, + get_available_backends +) +import logging + +logger = logging.getLogger(__name__) + +# Get configuration from central config +ATTN_BACKEND = get_attention_backend() +DEBUG = get_debug_mode() + +# Get available backends and import if active +available_backends = get_available_backends() + +if ATTN_BACKEND == "xformers" and available_backends['xformers']: + import xformers.ops as xops +elif ATTN_BACKEND == "flash_attn" and available_backends['flash_attn']: + import flash_attn +elif ATTN_BACKEND == "sage" and available_backends['sage']: + import sageattention + sageattn = sageattention.sageattn + from sageattention.core import sageattn_varlen +elif ATTN_BACKEND == "sdpa": + from torch.nn.functional import scaled_dot_product_attention as sdpa + +# Log the active backend +logger.info(f"Using attention backend: {ATTN_BACKEND}") + +__all__ = [ + "WindowedMode", + 'sparse_windowed_scaled_dot_product_self_attention', +] + +def calc_window_partition( + tensor: SparseTensor, + window_size: Union[int, Tuple[int, ...]], + shift_window: Union[int, Tuple[int, ...]] = 0 +) -> Tuple[torch.Tensor, torch.Tensor, List[int], List[int]]: + """ + Calculate serialization and partitioning for a set of coordinates. + + Args: + tensor (SparseTensor): The input tensor. + window_size (int): The window size to use. + shift_window (Tuple[int, ...]): The shift of serialized coordinates. + + Returns: + (torch.Tensor): Forwards indices. + (torch.Tensor): Backwards indices. + (List[int]): Sequence lengths. + (List[int]): Sequence batch indices. + """ + DIM = tensor.coords.shape[1] - 1 + shift_window = (shift_window,) * DIM if isinstance(shift_window, int) else shift_window + window_size = (window_size,) * DIM if isinstance(window_size, int) else window_size + shifted_coords = tensor.coords.clone().detach() + shifted_coords[:, 1:] += torch.tensor(shift_window, device=tensor.device, dtype=torch.int32).unsqueeze(0) + + MAX_COORDS = shifted_coords[:, 1:].max(dim=0).values.tolist() + NUM_WINDOWS = [math.ceil((mc + 1) / ws) for mc, ws in zip(MAX_COORDS, window_size)] + OFFSET = torch.cumprod(torch.tensor([1] + NUM_WINDOWS[::-1]), dim=0).tolist()[::-1] + + shifted_coords[:, 1:] //= torch.tensor(window_size, device=tensor.device, dtype=torch.int32).unsqueeze(0) + shifted_indices = (shifted_coords * torch.tensor(OFFSET, device=tensor.device, dtype=torch.int32).unsqueeze(0)).sum(dim=1) + fwd_indices = torch.argsort(shifted_indices) + bwd_indices = torch.empty_like(fwd_indices) + bwd_indices[fwd_indices] = torch.arange(fwd_indices.shape[0], device=tensor.device) + seq_lens = torch.bincount(shifted_indices) + seq_batch_indices = torch.arange(seq_lens.shape[0], device=tensor.device, dtype=torch.int32) // OFFSET[0] + mask = seq_lens != 0 + seq_lens = seq_lens[mask].tolist() + seq_batch_indices = seq_batch_indices[mask].tolist() + + return fwd_indices, bwd_indices, seq_lens, seq_batch_indices + + + '''fwd_indices = torch.arange(tensor.feats.shape[0], device=tensor.device) + bwd_indices = torch.arange(tensor.feats.shape[0], device=tensor.device) + seq_lens = [tensor.feats.shape[0]] + seq_batch_indices = [0] + return fwd_indices, bwd_indices, seq_lens, seq_batch_indices''' + + +def sparse_windowed_scaled_dot_product_self_attention( + qkv: SparseTensor, + window_size: int, + shift_window: Tuple[int, int, int] = (0, 0, 0) +) -> SparseTensor: + """ + Apply windowed scaled dot product self attention to a sparse tensor. + + Args: + qkv (SparseTensor): shape [N, *, 3, H, C] + window_size (int) + shift_window (Tuple[int,int,int]) + + Returns: + SparseTensor with updated .feats + """ + assert len(qkv.shape) == 4 and qkv.shape[1] == 3, ( + f"Invalid shape for qkv, got {qkv.shape}, expected [N, *, 3, H, C]" + ) + + cache_name = f'window_partition_{window_size}_{shift_window}' + window_spatial_cache = qkv.get_spatial_cache(cache_name) + if window_spatial_cache is None: + fwd_indices, bwd_indices, seq_lens, seq_batch_indices = calc_window_partition( + qkv, window_size, shift_window + ) + qkv.register_spatial_cache(cache_name, (fwd_indices, bwd_indices, seq_lens, seq_batch_indices)) + else: + fwd_indices, bwd_indices, seq_lens, seq_batch_indices = window_spatial_cache + + M = fwd_indices.shape[0] + qkv_feats = qkv.feats[fwd_indices] # [M, 3, H, C] + H = qkv_feats.shape[2] + C = qkv_feats.shape[3] + + if all(s == window_size for s in seq_lens): + # Uniform chunk size + B = len(seq_lens) + N = window_size + assert B * N == M, "Mismatched total tokens" + + qkv_feats = qkv_feats.reshape(B, N, 3, H, C) + + if ATTN_BACKEND == 'xformers': + q, k, v = qkv_feats.unbind(dim=2) # [B, N, H, C] + out = xops.memory_efficient_attention(q, k, v) # [B, N, H, C] + out = out.reshape(M, H, C) + elif ATTN_BACKEND == 'flash_attn': + out = flash_attn.flash_attn_qkvpacked_func(qkv_feats) # [B, N, H, C] + out = out.reshape(M, H, C) + elif ATTN_BACKEND == 'sage': + # SageAttention uniform path + # permute => [B, 3, H, N, C] + qkv_feats = qkv_feats.permute(0, 2, 3, 1, 4).contiguous() + q, k, v = qkv_feats.unbind(dim=1) # => [B, H, N, C] + + out = sageattn(q, k, v, tensor_layout="HND") + # => [B, H, N, C] + out = out.permute(0, 2, 1, 3).contiguous() # => [B, N, H, C] + out = out.reshape(M, H, C) + else: + raise ValueError(f"Unknown attention module: {ATTN_BACKEND}") + + else: + # Varlen branch + if ATTN_BACKEND == 'xformers': + q, k, v = qkv_feats.unbind(dim=1) # [M, H, C] + q = q.unsqueeze(0) + k = k.unsqueeze(0) + v = v.unsqueeze(0) + mask = xops.fmha.BlockDiagonalMask.from_seqlens(seq_lens) + out = xops.memory_efficient_attention(q, k, v, mask=mask)[0] # [M, H, C] + elif ATTN_BACKEND == 'flash_attn': + cu_seqlens = torch.zeros(len(seq_lens)+1, dtype=torch.int32, device=qkv.device) + cu_seqlens[1:] = torch.cumsum(torch.tensor(seq_lens, device=qkv.device), dim=0) + out = flash_attn.flash_attn_varlen_qkvpacked_func(qkv_feats, cu_seqlens, max(seq_lens)) # => [M, H, C] + elif ATTN_BACKEND == 'sage': + # Varlen SageAttention approach + #from sageattention import sageattn_varlen # or whichever kernel + + cu_seqlens = torch.zeros(len(seq_lens)+1, dtype=torch.int32, device=qkv.device) + cu_seqlens[1:] = torch.cumsum(torch.tensor(seq_lens, device=qkv.device), dim=0) + + # reorder => [3, M, H, C] + qkv_feats = qkv_feats.permute(1, 0, 2, 3).contiguous() + q, k, v = qkv_feats[0], qkv_feats[1], qkv_feats[2] # each => [M, H, C] + max_len = max(seq_lens) + + out = sageattn_varlen( + q, k, v, + cu_seqlens, cu_seqlens, + max_len, max_len, + is_causal=False + ) # => [M, H, C] + else: + raise ValueError(f"Unknown attention module: {ATTN_BACKEND}") + + out = out[bwd_indices] # [T, H, C] + return qkv.replace(out) + +================ +File: sparse/basic.py +================ +from typing import * +import torch +import torch.nn as nn +from trellis.backend_config import get_sparse_backend, get_debug_mode + +# Get configuration from central config +BACKEND = get_sparse_backend() +DEBUG = get_debug_mode() + +SparseTensorData = None # Lazy import + +__all__ = [ + 'SparseTensor', + 'sparse_batch_broadcast', + 'sparse_batch_op', + 'sparse_cat', + 'sparse_unbind', +] + +class SparseTensor: + """ + Sparse tensor with support for both torchsparse and spconv backends. + + Parameters: + - feats (torch.Tensor): Features of the sparse tensor. + - coords (torch.Tensor): Coordinates of the sparse tensor. + - shape (torch.Size): Shape of the sparse tensor. + - layout (List[slice]): Layout of the sparse tensor for each batch + - data (SparseTensorData): Sparse tensor data used for convolusion + + NOTE: + - Data corresponding to a same batch should be contiguous. + - Coords should be in [0, 1023] + """ + @overload + def __init__(self, feats: torch.Tensor, coords: torch.Tensor, shape: Optional[torch.Size] = None, layout: Optional[List[slice]] = None, **kwargs): ... + + @overload + def __init__(self, data, shape: Optional[torch.Size] = None, layout: Optional[List[slice]] = None, **kwargs): ... + + def __init__(self, *args, **kwargs): + # Lazy import of sparse tensor backend + global SparseTensorData + if SparseTensorData is None: + import importlib + backend = get_sparse_backend() # Get current backend from central config + if backend == 'torchsparse': + SparseTensorData = importlib.import_module('torchsparse').SparseTensor + elif backend == 'spconv': + SparseTensorData = importlib.import_module('spconv.pytorch').SparseConvTensor + + method_id = 0 + if len(args) != 0: + method_id = 0 if isinstance(args[0], torch.Tensor) else 1 + else: + method_id = 1 if 'data' in kwargs else 0 + + if method_id == 0: + feats, coords, shape, layout = args + (None,) * (4 - len(args)) + if 'feats' in kwargs: + feats = kwargs['feats'] + del kwargs['feats'] + if 'coords' in kwargs: + coords = kwargs['coords'] + del kwargs['coords'] + if 'shape' in kwargs: + shape = kwargs['shape'] + del kwargs['shape'] + if 'layout' in kwargs: + layout = kwargs['layout'] + del kwargs['layout'] + + if shape is None: + shape = self.__cal_shape(feats, coords) + if layout is None: + layout = self.__cal_layout(coords, shape[0]) + if BACKEND == 'torchsparse': + self.data = SparseTensorData(feats, coords, **kwargs) + elif BACKEND == 'spconv': + spatial_shape = list(coords.max(0)[0] + 1)[1:] + self.data = SparseTensorData(feats.reshape(feats.shape[0], -1), coords, spatial_shape, shape[0], **kwargs) + self.data._features = feats + elif method_id == 1: + data, shape, layout = args + (None,) * (3 - len(args)) + if 'data' in kwargs: + data = kwargs['data'] + del kwargs['data'] + if 'shape' in kwargs: + shape = kwargs['shape'] + del kwargs['shape'] + if 'layout' in kwargs: + layout = kwargs['layout'] + del kwargs['layout'] + + self.data = data + if shape is None: + shape = self.__cal_shape(self.feats, self.coords) + if layout is None: + layout = self.__cal_layout(self.coords, shape[0]) + + self._shape = shape + self._layout = layout + self._scale = kwargs.get('scale', (1, 1, 1)) + self._spatial_cache = kwargs.get('spatial_cache', {}) + + if DEBUG: + try: + assert self.feats.shape[0] == self.coords.shape[0], f"Invalid feats shape: {self.feats.shape}, coords shape: {self.coords.shape}" + assert self.shape == self.__cal_shape(self.feats, self.coords), f"Invalid shape: {self.shape}" + assert self.layout == self.__cal_layout(self.coords, self.shape[0]), f"Invalid layout: {self.layout}" + for i in range(self.shape[0]): + assert torch.all(self.coords[self.layout[i], 0] == i), f"The data of batch {i} is not contiguous" + except Exception as e: + print('Debugging information:') + print(f"- Shape: {self.shape}") + print(f"- Layout: {self.layout}") + print(f"- Scale: {self._scale}") + print(f"- Coords: {self.coords}") + raise e + + def __cal_shape(self, feats, coords): + shape = [] + shape.append(coords[:, 0].max().item() + 1) + shape.extend([*feats.shape[1:]]) + return torch.Size(shape) + + def __cal_layout(self, coords, batch_size): + seq_len = torch.bincount(coords[:, 0], minlength=batch_size) + offset = torch.cumsum(seq_len, dim=0) + layout = [slice((offset[i] - seq_len[i]).item(), offset[i].item()) for i in range(batch_size)] + return layout + + @property + def shape(self) -> torch.Size: + return self._shape + + def dim(self) -> int: + return len(self.shape) + + @property + def layout(self) -> List[slice]: + return self._layout + + @property + def feats(self) -> torch.Tensor: + if BACKEND == 'torchsparse': + return self.data.F + elif BACKEND == 'spconv': + return self.data.features + + @feats.setter + def feats(self, value: torch.Tensor): + if BACKEND == 'torchsparse': + self.data.F = value + elif BACKEND == 'spconv': + self.data.features = value + + @property + def coords(self) -> torch.Tensor: + if BACKEND == 'torchsparse': + return self.data.C + elif BACKEND == 'spconv': + return self.data.indices + + @coords.setter + def coords(self, value: torch.Tensor): + if BACKEND == 'torchsparse': + self.data.C = value + elif BACKEND == 'spconv': + self.data.indices = value + + @property + def dtype(self): + return self.feats.dtype + + @property + def device(self): + return self.feats.device + + @overload + def to(self, dtype: torch.dtype) -> 'SparseTensor': ... + + @overload + def to(self, device: Optional[Union[str, torch.device]] = None, dtype: Optional[torch.dtype] = None) -> 'SparseTensor': ... + + def to(self, *args, **kwargs) -> 'SparseTensor': + device = None + dtype = None + if len(args) == 2: + device, dtype = args + elif len(args) == 1: + if isinstance(args[0], torch.dtype): + dtype = args[0] + else: + device = args[0] + if 'dtype' in kwargs: + assert dtype is None, "to() received multiple values for argument 'dtype'" + dtype = kwargs['dtype'] + if 'device' in kwargs: + assert device is None, "to() received multiple values for argument 'device'" + device = kwargs['device'] + + new_feats = self.feats.to(device=device, dtype=dtype) + new_coords = self.coords.to(device=device) + return self.replace(new_feats, new_coords) + + def type(self, dtype): + new_feats = self.feats.type(dtype) + return self.replace(new_feats) + + def cpu(self) -> 'SparseTensor': + new_feats = self.feats.cpu() + new_coords = self.coords.cpu() + return self.replace(new_feats, new_coords) + + def cuda(self) -> 'SparseTensor': + new_feats = self.feats.cuda() + new_coords = self.coords.cuda() + return self.replace(new_feats, new_coords) + + def half(self) -> 'SparseTensor': + new_feats = self.feats.half() + return self.replace(new_feats) + + def float(self) -> 'SparseTensor': + new_feats = self.feats.float() + return self.replace(new_feats) + + def detach(self) -> 'SparseTensor': + new_coords = self.coords.detach() + new_feats = self.feats.detach() + return self.replace(new_feats, new_coords) + + def dense(self) -> torch.Tensor: + if BACKEND == 'torchsparse': + return self.data.dense() + elif BACKEND == 'spconv': + return self.data.dense() + + def reshape(self, *shape) -> 'SparseTensor': + new_feats = self.feats.reshape(self.feats.shape[0], *shape) + return self.replace(new_feats) + + def unbind(self, dim: int) -> List['SparseTensor']: + return sparse_unbind(self, dim) + + def replace(self, feats: torch.Tensor, coords: Optional[torch.Tensor] = None) -> 'SparseTensor': + new_shape = [self.shape[0]] + new_shape.extend(feats.shape[1:]) + if BACKEND == 'torchsparse': + new_data = SparseTensorData( + feats=feats, + coords=self.data.coords if coords is None else coords, + stride=self.data.stride, + spatial_range=self.data.spatial_range, + ) + new_data._caches = self.data._caches + elif BACKEND == 'spconv': + new_data = SparseTensorData( + self.data.features.reshape(self.data.features.shape[0], -1), + self.data.indices, + self.data.spatial_shape, + self.data.batch_size, + self.data.grid, + self.data.voxel_num, + self.data.indice_dict + ) + new_data._features = feats + new_data.benchmark = self.data.benchmark + new_data.benchmark_record = self.data.benchmark_record + new_data.thrust_allocator = self.data.thrust_allocator + new_data._timer = self.data._timer + new_data.force_algo = self.data.force_algo + new_data.int8_scale = self.data.int8_scale + if coords is not None: + new_data.indices = coords + new_tensor = SparseTensor(new_data, shape=torch.Size(new_shape), layout=self.layout, scale=self._scale, spatial_cache=self._spatial_cache) + return new_tensor + + @staticmethod + def full(aabb, dim, value, dtype=torch.float32, device=None) -> 'SparseTensor': + N, C = dim + x = torch.arange(aabb[0], aabb[3] + 1) + y = torch.arange(aabb[1], aabb[4] + 1) + z = torch.arange(aabb[2], aabb[5] + 1) + coords = torch.stack(torch.meshgrid(x, y, z, indexing='ij'), dim=-1).reshape(-1, 3) + coords = torch.cat([ + torch.arange(N).view(-1, 1).repeat(1, coords.shape[0]).view(-1, 1), + coords.repeat(N, 1), + ], dim=1).to(dtype=torch.int32, device=device) + feats = torch.full((coords.shape[0], C), value, dtype=dtype, device=device) + return SparseTensor(feats=feats, coords=coords) + + def __merge_sparse_cache(self, other: 'SparseTensor') -> dict: + new_cache = {} + for k in set(list(self._spatial_cache.keys()) + list(other._spatial_cache.keys())): + if k in self._spatial_cache: + new_cache[k] = self._spatial_cache[k] + if k in other._spatial_cache: + if k not in new_cache: + new_cache[k] = other._spatial_cache[k] + else: + new_cache[k].update(other._spatial_cache[k]) + return new_cache + + def __neg__(self) -> 'SparseTensor': + return self.replace(-self.feats) + + def __elemwise__(self, other: Union[torch.Tensor, 'SparseTensor'], op: callable) -> 'SparseTensor': + if isinstance(other, torch.Tensor): + try: + other = torch.broadcast_to(other, self.shape) + other = sparse_batch_broadcast(self, other) + except: + pass + if isinstance(other, SparseTensor): + other = other.feats + new_feats = op(self.feats, other) + new_tensor = self.replace(new_feats) + if isinstance(other, SparseTensor): + new_tensor._spatial_cache = self.__merge_sparse_cache(other) + return new_tensor + + def __add__(self, other: Union[torch.Tensor, 'SparseTensor', float]) -> 'SparseTensor': + return self.__elemwise__(other, torch.add) + + def __radd__(self, other: Union[torch.Tensor, 'SparseTensor', float]) -> 'SparseTensor': + return self.__elemwise__(other, torch.add) + + def __sub__(self, other: Union[torch.Tensor, 'SparseTensor', float]) -> 'SparseTensor': + return self.__elemwise__(other, torch.sub) + + def __rsub__(self, other: Union[torch.Tensor, 'SparseTensor', float]) -> 'SparseTensor': + return self.__elemwise__(other, lambda x, y: torch.sub(y, x)) + + def __mul__(self, other: Union[torch.Tensor, 'SparseTensor', float]) -> 'SparseTensor': + return self.__elemwise__(other, torch.mul) + + def __rmul__(self, other: Union[torch.Tensor, 'SparseTensor', float]) -> 'SparseTensor': + return self.__elemwise__(other, torch.mul) + + def __truediv__(self, other: Union[torch.Tensor, 'SparseTensor', float]) -> 'SparseTensor': + return self.__elemwise__(other, torch.div) + + def __rtruediv__(self, other: Union[torch.Tensor, 'SparseTensor', float]) -> 'SparseTensor': + return self.__elemwise__(other, lambda x, y: torch.div(y, x)) + + def __getitem__(self, idx): + if isinstance(idx, int): + idx = [idx] + elif isinstance(idx, slice): + idx = range(*idx.indices(self.shape[0])) + elif isinstance(idx, torch.Tensor): + if idx.dtype == torch.bool: + assert idx.shape == (self.shape[0],), f"Invalid index shape: {idx.shape}" + idx = idx.nonzero().squeeze(1) + elif idx.dtype in [torch.int32, torch.int64]: + assert len(idx.shape) == 1, f"Invalid index shape: {idx.shape}" + else: + raise ValueError(f"Unknown index type: {idx.dtype}") + else: + raise ValueError(f"Unknown index type: {type(idx)}") + + coords = [] + feats = [] + for new_idx, old_idx in enumerate(idx): + coords.append(self.coords[self.layout[old_idx]].clone()) + coords[-1][:, 0] = new_idx + feats.append(self.feats[self.layout[old_idx]]) + coords = torch.cat(coords, dim=0).contiguous() + feats = torch.cat(feats, dim=0).contiguous() + return SparseTensor(feats=feats, coords=coords) + + def register_spatial_cache(self, key, value) -> None: + """ + Register a spatial cache. + The spatial cache can be any thing you want to cache. + The registery and retrieval of the cache is based on current scale. + """ + scale_key = str(self._scale) + if scale_key not in self._spatial_cache: + self._spatial_cache[scale_key] = {} + self._spatial_cache[scale_key][key] = value + + def get_spatial_cache(self, key=None): + """ + Get a spatial cache. + """ + scale_key = str(self._scale) + cur_scale_cache = self._spatial_cache.get(scale_key, {}) + if key is None: + return cur_scale_cache + return cur_scale_cache.get(key, None) + + +def sparse_batch_broadcast(input: SparseTensor, other: torch.Tensor) -> torch.Tensor: + """ + Broadcast a 1D tensor to a sparse tensor along the batch dimension then perform an operation. + + Args: + input (torch.Tensor): 1D tensor to broadcast. + target (SparseTensor): Sparse tensor to broadcast to. + op (callable): Operation to perform after broadcasting. Defaults to torch.add. + """ + coords, feats = input.coords, input.feats + broadcasted = torch.zeros_like(feats) + for k in range(input.shape[0]): + broadcasted[input.layout[k]] = other[k] + return broadcasted + + +def sparse_batch_op(input: SparseTensor, other: torch.Tensor, op: callable = torch.add) -> SparseTensor: + """ + Broadcast a 1D tensor to a sparse tensor along the batch dimension then perform an operation. + + Args: + input (torch.Tensor): 1D tensor to broadcast. + target (SparseTensor): Sparse tensor to broadcast to. + op (callable): Operation to perform after broadcasting. Defaults to torch.add. + """ + return input.replace(op(input.feats, sparse_batch_broadcast(input, other))) + + +def sparse_cat(inputs: List[SparseTensor], dim: int = 0) -> SparseTensor: + """ + Concatenate a list of sparse tensors. + + Args: + inputs (List[SparseTensor]): List of sparse tensors to concatenate. + """ + if dim == 0: + start = 0 + coords = [] + for input in inputs: + coords.append(input.coords.clone()) + coords[-1][:, 0] += start + start += input.shape[0] + coords = torch.cat(coords, dim=0) + feats = torch.cat([input.feats for input in inputs], dim=0) + output = SparseTensor( + coords=coords, + feats=feats, + ) + else: + feats = torch.cat([input.feats for input in inputs], dim=dim) + output = inputs[0].replace(feats) + + return output + + +def sparse_unbind(input: SparseTensor, dim: int) -> List[SparseTensor]: + """ + Unbind a sparse tensor along a dimension. + + Args: + input (SparseTensor): Sparse tensor to unbind. + dim (int): Dimension to unbind. + """ + if dim == 0: + return [input[i] for i in range(input.shape[0])] + else: + feats = input.feats.unbind(dim) + return [input.replace(f) for f in feats] + +================ +File: sparse/conv/__init__.py +================ +import os +import logging +from trellis.backend_config import get_sparse_backend, get_spconv_algo + +logger = logging.getLogger(__name__) + # 'auto', 'implicit_gemm', 'native' +def get_conv_impl(): + """ + Called once at import time (or the first time someone references these objects). + Decides which conv module to load based on the user's chosen backend. + """ + backend = get_sparse_backend() # e.g. 'spconv' or 'torchsparse' + logger.debug(f"[Sparse Conv] Detected user-chosen backend: {backend}") + + if backend == 'spconv': + # We also set or fetch the spconv algorithm from backend_config + algo = get_spconv_algo() + os.environ['SPCONV_ALGO'] = algo # Let the submodule read it if needed + # Now import the actual spconv-based conv + from .conv_spconv import SparseConv3d, SparseInverseConv3d + return SparseConv3d, SparseInverseConv3d + + elif backend == 'torchsparse': + from .conv_torchsparse import SparseConv3d, SparseInverseConv3d + return SparseConv3d, SparseInverseConv3d + + else: + # If user didn't set a valid backend, or there's no available backend + raise ImportError(f"Unknown or unsupported sparse backend '{backend}'.") + +SparseConv3d, SparseInverseConv3d = get_conv_impl() + +__all__ = [ + "SparseConv3d", + "SparseInverseConv3d", +] + +================ +File: sparse/conv/conv_spconv.py +================ +import torch +import torch.nn as nn +from .. import SparseTensor +import torch +import torch.nn as nn +from .. import SparseTensor +from trellis.backend_config import get_debug_mode, get_spconv_algo + +# Get configuration from central config +DEBUG = get_debug_mode() +SPCONV_ALGO = get_spconv_algo() + +class SparseConv3d(nn.Module): + def __init__(self, in_channels, out_channels, kernel_size, stride=1, dilation=1, padding=None, bias=True, indice_key=None): + super(SparseConv3d, self).__init__() + if 'spconv' not in globals(): + import spconv.pytorch as spconv + + # Get algorithm from central config + algo = None + current_algo = get_spconv_algo() + if current_algo == 'native': + algo = spconv.ConvAlgo.Native + elif current_algo == 'implicit_gemm': + algo = spconv.ConvAlgo.MaskImplicitGemm + if stride == 1 and (padding is None): + self.conv = spconv.SubMConv3d(in_channels, out_channels, kernel_size, dilation=dilation, bias=bias, indice_key=indice_key, algo=algo) + else: + self.conv = spconv.SparseConv3d(in_channels, out_channels, kernel_size, stride=stride, dilation=dilation, padding=padding, bias=bias, indice_key=indice_key, algo=algo) + self.stride = tuple(stride) if isinstance(stride, (list, tuple)) else (stride, stride, stride) + self.padding = padding + + def forward(self, x: SparseTensor) -> SparseTensor: + spatial_changed = any(s != 1 for s in self.stride) or (self.padding is not None) + new_data = self.conv(x.data) + new_shape = [x.shape[0], self.conv.out_channels] + new_layout = None if spatial_changed else x.layout + + if spatial_changed and (x.shape[0] != 1): + # spconv was non-1 stride will break the contiguous of the output tensor, sort by the coords + fwd = new_data.indices[:, 0].argsort() + bwd = torch.zeros_like(fwd).scatter_(0, fwd, torch.arange(fwd.shape[0], device=fwd.device)) + sorted_feats = new_data.features[fwd] + sorted_coords = new_data.indices[fwd] + unsorted_data = new_data + new_data = spconv.SparseConvTensor(sorted_feats, sorted_coords, unsorted_data.spatial_shape, unsorted_data.batch_size) # type: ignore + + out = SparseTensor( + new_data, shape=torch.Size(new_shape), layout=new_layout, + scale=tuple([s * stride for s, stride in zip(x._scale, self.stride)]), + spatial_cache=x._spatial_cache, + ) + + if spatial_changed and (x.shape[0] != 1): + out.register_spatial_cache(f'conv_{self.stride}_unsorted_data', unsorted_data) + out.register_spatial_cache(f'conv_{self.stride}_sort_bwd', bwd) + + return out + + +class SparseInverseConv3d(nn.Module): + def __init__(self, in_channels, out_channels, kernel_size, stride=1, dilation=1, bias=True, indice_key=None): + super(SparseInverseConv3d, self).__init__() + if 'spconv' not in globals(): + import spconv.pytorch as spconv + self.conv = spconv.SparseInverseConv3d(in_channels, out_channels, kernel_size, bias=bias, indice_key=indice_key) + self.stride = tuple(stride) if isinstance(stride, (list, tuple)) else (stride, stride, stride) + + def forward(self, x: SparseTensor) -> SparseTensor: + spatial_changed = any(s != 1 for s in self.stride) + if spatial_changed: + # recover the original spconv order + data = x.get_spatial_cache(f'conv_{self.stride}_unsorted_data') + bwd = x.get_spatial_cache(f'conv_{self.stride}_sort_bwd') + data = data.replace_feature(x.feats[bwd]) + if DEBUG: + assert torch.equal(data.indices, x.coords[bwd]), 'Recover the original order failed' + else: + data = x.data + + new_data = self.conv(data) + new_shape = [x.shape[0], self.conv.out_channels] + new_layout = None if spatial_changed else x.layout + out = SparseTensor( + new_data, shape=torch.Size(new_shape), layout=new_layout, + scale=tuple([s // stride for s, stride in zip(x._scale, self.stride)]), + spatial_cache=x._spatial_cache, + ) + return out + +================ +File: sparse/conv/conv_torchsparse.py +================ +import torch +import torch.nn as nn +from .. import SparseTensor + + +class SparseConv3d(nn.Module): + def __init__(self, in_channels, out_channels, kernel_size, stride=1, dilation=1, bias=True, indice_key=None): + super(SparseConv3d, self).__init__() + if 'torchsparse' not in globals(): + import torchsparse + self.conv = torchsparse.nn.Conv3d(in_channels, out_channels, kernel_size, stride, 0, dilation, bias) + + def forward(self, x: SparseTensor) -> SparseTensor: + out = self.conv(x.data) + new_shape = [x.shape[0], self.conv.out_channels] + out = SparseTensor(out, shape=torch.Size(new_shape), layout=x.layout if all(s == 1 for s in self.conv.stride) else None) + out._spatial_cache = x._spatial_cache + out._scale = tuple([s * stride for s, stride in zip(x._scale, self.conv.stride)]) + return out + + +class SparseInverseConv3d(nn.Module): + def __init__(self, in_channels, out_channels, kernel_size, stride=1, dilation=1, bias=True, indice_key=None): + super(SparseInverseConv3d, self).__init__() + if 'torchsparse' not in globals(): + import torchsparse + self.conv = torchsparse.nn.Conv3d(in_channels, out_channels, kernel_size, stride, 0, dilation, bias, transposed=True) + + def forward(self, x: SparseTensor) -> SparseTensor: + out = self.conv(x.data) + new_shape = [x.shape[0], self.conv.out_channels] + out = SparseTensor(out, shape=torch.Size(new_shape), layout=x.layout if all(s == 1 for s in self.conv.stride) else None) + out._spatial_cache = x._spatial_cache + out._scale = tuple([s // stride for s, stride in zip(x._scale, self.conv.stride)]) + return out + +================ +File: sparse/linear.py +================ +import torch +import torch.nn as nn +from . import SparseTensor + +__all__ = [ + 'SparseLinear' +] + + +class SparseLinear(nn.Linear): + def __init__(self, in_features, out_features, bias=True): + super(SparseLinear, self).__init__(in_features, out_features, bias) + + def forward(self, input: SparseTensor) -> SparseTensor: + return input.replace(super().forward(input.feats)) + +================ +File: sparse/nonlinearity.py +================ +import torch +import torch.nn as nn +from . import SparseTensor + +__all__ = [ + 'SparseReLU', + 'SparseSiLU', + 'SparseGELU', + 'SparseActivation' +] + + +class SparseReLU(nn.ReLU): + def forward(self, input: SparseTensor) -> SparseTensor: + return input.replace(super().forward(input.feats)) + + +class SparseSiLU(nn.SiLU): + def forward(self, input: SparseTensor) -> SparseTensor: + return input.replace(super().forward(input.feats)) + + +class SparseGELU(nn.GELU): + def forward(self, input: SparseTensor) -> SparseTensor: + return input.replace(super().forward(input.feats)) + + +class SparseActivation(nn.Module): + def __init__(self, activation: nn.Module): + super().__init__() + self.activation = activation + + def forward(self, input: SparseTensor) -> SparseTensor: + return input.replace(self.activation(input.feats)) + +================ +File: sparse/norm.py +================ +import torch +import torch.nn as nn +from . import SparseTensor +from trellis.backend_config import get_debug_mode + +DEBUG = get_debug_mode() + +__all__ = [ + 'SparseGroupNorm', + 'SparseLayerNorm', + 'SparseGroupNorm32', + 'SparseLayerNorm32', +] + + +class SparseGroupNorm(nn.GroupNorm): + def __init__(self, num_groups, num_channels, eps=1e-5, affine=True): + super(SparseGroupNorm, self).__init__(num_groups, num_channels, eps, affine) + + def forward(self, input: SparseTensor) -> SparseTensor: + nfeats = torch.zeros_like(input.feats) + for k in range(input.shape[0]): + if DEBUG: + assert (input.coords[input.layout[k], 0] == k).all(), f"SparseGroupNorm: batch index mismatch" + bfeats = input.feats[input.layout[k]] + bfeats = bfeats.permute(1, 0).reshape(1, input.shape[1], -1) + bfeats = super().forward(bfeats) + bfeats = bfeats.reshape(input.shape[1], -1).permute(1, 0) + nfeats[input.layout[k]] = bfeats + return input.replace(nfeats) + + +class SparseLayerNorm(nn.LayerNorm): + def __init__(self, normalized_shape, eps=1e-5, elementwise_affine=True): + super(SparseLayerNorm, self).__init__(normalized_shape, eps, elementwise_affine) + + def forward(self, input: SparseTensor) -> SparseTensor: + nfeats = torch.zeros_like(input.feats) + for k in range(input.shape[0]): + bfeats = input.feats[input.layout[k]] + bfeats = bfeats.permute(1, 0).reshape(1, input.shape[1], -1) + bfeats = super().forward(bfeats) + bfeats = bfeats.reshape(input.shape[1], -1).permute(1, 0) + nfeats[input.layout[k]] = bfeats + return input.replace(nfeats) + + +class SparseGroupNorm32(SparseGroupNorm): + """ + A GroupNorm layer that converts to float32 before the forward pass. + """ + def forward(self, x: SparseTensor) -> SparseTensor: + return super().forward(x.float()).type(x.dtype) + +class SparseLayerNorm32(SparseLayerNorm): + """ + A LayerNorm layer that converts to float32 before the forward pass. + """ + def forward(self, x: SparseTensor) -> SparseTensor: + return super().forward(x.float()).type(x.dtype) + +================ +File: sparse/spatial.py +================ +from typing import * +import torch +import torch.nn as nn +from . import SparseTensor + +__all__ = [ + 'SparseDownsample', + 'SparseUpsample', + 'SparseSubdivide' +] + + +class SparseDownsample(nn.Module): + """ + Downsample a sparse tensor by a factor of `factor`. + Implemented as average pooling. + """ + def __init__(self, factor: Union[int, Tuple[int, ...], List[int]]): + super(SparseDownsample, self).__init__() + self.factor = tuple(factor) if isinstance(factor, (list, tuple)) else factor + + def forward(self, input: SparseTensor) -> SparseTensor: + DIM = input.coords.shape[-1] - 1 + factor = self.factor if isinstance(self.factor, tuple) else (self.factor,) * DIM + assert DIM == len(factor), 'Input coordinates must have the same dimension as the downsample factor.' + + coord = list(input.coords.unbind(dim=-1)) + for i, f in enumerate(factor): + coord[i+1] = coord[i+1] // f + + MAX = [coord[i+1].max().item() + 1 for i in range(DIM)] + OFFSET = torch.cumprod(torch.tensor(MAX[::-1]), 0).tolist()[::-1] + [1] + code = sum([c * o for c, o in zip(coord, OFFSET)]) + code, idx = code.unique(return_inverse=True) + + new_feats = torch.scatter_reduce( + torch.zeros(code.shape[0], input.feats.shape[1], device=input.feats.device, dtype=input.feats.dtype), + dim=0, + index=idx.unsqueeze(1).expand(-1, input.feats.shape[1]), + src=input.feats, + reduce='mean' + ) + new_coords = torch.stack( + [code // OFFSET[0]] + + [(code // OFFSET[i+1]) % MAX[i] for i in range(DIM)], + dim=-1 + ) + out = SparseTensor(new_feats, new_coords, input.shape,) + out._scale = tuple([s // f for s, f in zip(input._scale, factor)]) + out._spatial_cache = input._spatial_cache + + out.register_spatial_cache(f'upsample_{factor}_coords', input.coords) + out.register_spatial_cache(f'upsample_{factor}_layout', input.layout) + out.register_spatial_cache(f'upsample_{factor}_idx', idx) + + return out + + +class SparseUpsample(nn.Module): + """ + Upsample a sparse tensor by a factor of `factor`. + Implemented as nearest neighbor interpolation. + """ + def __init__(self, factor: Union[int, Tuple[int, int, int], List[int]]): + super(SparseUpsample, self).__init__() + self.factor = tuple(factor) if isinstance(factor, (list, tuple)) else factor + + def forward(self, input: SparseTensor) -> SparseTensor: + DIM = input.coords.shape[-1] - 1 + factor = self.factor if isinstance(self.factor, tuple) else (self.factor,) * DIM + assert DIM == len(factor), 'Input coordinates must have the same dimension as the upsample factor.' + + new_coords = input.get_spatial_cache(f'upsample_{factor}_coords') + new_layout = input.get_spatial_cache(f'upsample_{factor}_layout') + idx = input.get_spatial_cache(f'upsample_{factor}_idx') + if any([x is None for x in [new_coords, new_layout, idx]]): + raise ValueError('Upsample cache not found. SparseUpsample must be paired with SparseDownsample.') + new_feats = input.feats[idx] + out = SparseTensor(new_feats, new_coords, input.shape, new_layout) + out._scale = tuple([s * f for s, f in zip(input._scale, factor)]) + out._spatial_cache = input._spatial_cache + return out + +class SparseSubdivide(nn.Module): + """ + Upsample a sparse tensor by a factor of `factor`. + Implemented as nearest neighbor interpolation. + """ + def __init__(self): + super(SparseSubdivide, self).__init__() + + def forward(self, input: SparseTensor) -> SparseTensor: + DIM = input.coords.shape[-1] - 1 + # upsample scale=2^DIM + n_cube = torch.ones([2] * DIM, device=input.device, dtype=torch.int) + n_coords = torch.nonzero(n_cube) + n_coords = torch.cat([torch.zeros_like(n_coords[:, :1]), n_coords], dim=-1) + factor = n_coords.shape[0] + assert factor == 2 ** DIM + # print(n_coords.shape) + new_coords = input.coords.clone() + new_coords[:, 1:] *= 2 + new_coords = new_coords.unsqueeze(1) + n_coords.unsqueeze(0).to(new_coords.dtype) + + new_feats = input.feats.unsqueeze(1).expand(input.feats.shape[0], factor, *input.feats.shape[1:]) + out = SparseTensor(new_feats.flatten(0, 1), new_coords.flatten(0, 1), input.shape) + out._scale = input._scale * 2 + out._spatial_cache = input._spatial_cache + return out + +================ +File: spatial.py +================ +import torch + + +def pixel_shuffle_3d(x: torch.Tensor, scale_factor: int) -> torch.Tensor: + """ + 3D pixel shuffle. + """ + B, C, H, W, D = x.shape + C_ = C // scale_factor**3 + x = x.reshape(B, C_, scale_factor, scale_factor, scale_factor, H, W, D) + x = x.permute(0, 1, 5, 2, 6, 3, 7, 4) + x = x.reshape(B, C_, H*scale_factor, W*scale_factor, D*scale_factor) + return x + + +def patchify(x: torch.Tensor, patch_size: int): + """ + Patchify a tensor. + + Args: + x (torch.Tensor): (N, C, *spatial) tensor + patch_size (int): Patch size + """ + DIM = x.dim() - 2 + for d in range(2, DIM + 2): + assert x.shape[d] % patch_size == 0, f"Dimension {d} of input tensor must be divisible by patch size, got {x.shape[d]} and {patch_size}" + + x = x.reshape(*x.shape[:2], *sum([[x.shape[d] // patch_size, patch_size] for d in range(2, DIM + 2)], [])) + x = x.permute(0, 1, *([2 * i + 3 for i in range(DIM)] + [2 * i + 2 for i in range(DIM)])) + x = x.reshape(x.shape[0], x.shape[1] * (patch_size ** DIM), *(x.shape[-DIM:])) + return x + + +def unpatchify(x: torch.Tensor, patch_size: int): + """ + Unpatchify a tensor. + + Args: + x (torch.Tensor): (N, C, *spatial) tensor + patch_size (int): Patch size + """ + DIM = x.dim() - 2 + assert x.shape[1] % (patch_size ** DIM) == 0, f"Second dimension of input tensor must be divisible by patch size to unpatchify, got {x.shape[1]} and {patch_size ** DIM}" + + x = x.reshape(x.shape[0], x.shape[1] // (patch_size ** DIM), *([patch_size] * DIM), *(x.shape[-DIM:])) + x = x.permute(0, 1, *(sum([[2 + DIM + i, 2 + i] for i in range(DIM)], []))) + x = x.reshape(x.shape[0], x.shape[1], *[x.shape[2 + 2 * i] * patch_size for i in range(DIM)]) + return x + +================ +File: utils.py +================ +import torch.nn as nn +from ..modules import sparse as sp + +FP16_MODULES = ( + nn.Conv1d, + nn.Conv2d, + nn.Conv3d, + nn.ConvTranspose1d, + nn.ConvTranspose2d, + nn.ConvTranspose3d, + nn.Linear, + sp.SparseConv3d, + sp.SparseInverseConv3d, + sp.SparseLinear, +) + +def convert_module_to_f16(l): + """ + Convert primitive modules to float16. + """ + if isinstance(l, FP16_MODULES): + for p in l.parameters(): + p.data = p.data.half() + + +def convert_module_to_f32(l): + """ + Convert primitive modules to float32, undoing convert_module_to_f16(). + """ + if isinstance(l, FP16_MODULES): + for p in l.parameters(): + p.data = p.data.float() + + +def zero_module(module): + """ + Zero out the parameters of a module and return it. + """ + for p in module.parameters(): + p.detach().zero_() + return module + + +def scale_module(module, scale): + """ + Scale the parameters of a module and return it. + """ + for p in module.parameters(): + p.detach().mul_(scale) + return module + + +def modulate(x, shift, scale): + return x * (1 + scale.unsqueeze(1)) + shift.unsqueeze(1) diff --git a/trellis/modules/sparse/attention/__init__.py b/trellis/modules/sparse/attention/__init__.py index 400de9a..fade63b 100644 --- a/trellis/modules/sparse/attention/__init__.py +++ b/trellis/modules/sparse/attention/__init__.py @@ -2,3 +2,30 @@ from .full_attn import * from .serialized_attn import * from .windowed_attn import * from .modules import * +import os +import logging +from typing import Literal +from trellis.backend_config import ( + get_attention_backend, + get_debug_mode, +) +import logging + +logger = logging.getLogger(__name__) + +#ATTN = get_attention_backend() +ATTN = get_attention_backend() +DEBUG = get_debug_mode() + +def __from_env(): + """Read current backend configuration""" + #global ATTN + global ATTN + global DEBUG + + # Get current settings from central config + #ATTN = + ATTN = get_attention_backend() + DEBUG = get_debug_mode() + + print(f"[ATTENTION] sparse backend: {ATTN}") \ No newline at end of file diff --git a/trellis/modules/sparse/attention/full_attn.py b/trellis/modules/sparse/attention/full_attn.py index c724327..ea8ed61 100644 --- a/trellis/modules/sparse/attention/full_attn.py +++ b/trellis/modules/sparse/attention/full_attn.py @@ -1,15 +1,41 @@ +#trellis\modules\sparse\attention\full_attn.py from typing import * import torch +import math from .. import SparseTensor -from .. import DEBUG, ATTN +from trellis.backend_config import ( + get_attention_backend, + get_debug_mode, + get_available_backends +) +import logging -if ATTN == 'xformers': +logger = logging.getLogger(__name__) + +# Get configuration from central config +ATTN = get_attention_backend() +DEBUG = get_debug_mode() + +# Get available backends and import if active +available_backends = get_available_backends() + +if ATTN == "xformers" and available_backends['xformers']: import xformers.ops as xops -elif ATTN == 'flash_attn': +elif ATTN == "flash_attn" and available_backends['flash_attn']: import flash_attn +elif ATTN == "sage" and available_backends['sage']: + import torch.nn.functional as F + from sageattention import sageattn + F.scaled_dot_product_attention = sageattn +elif ATTN == "sdpa": + from torch.nn.functional import scaled_dot_product_attention as sdpa +elif ATTN == "naive": + from torch.nn.functional import scaled_dot_product_attention as naive else: raise ValueError(f"Unknown attention module: {ATTN}") +# Log the active backend +logger.info(f"Using attention backend: {ATTN}") __all__ = [ 'sparse_scaled_dot_product_attention', @@ -212,4 +238,4 @@ def sparse_scaled_dot_product_attention(*args, **kwargs): if s is not None: return s.replace(out) else: - return out.reshape(N, L, H, -1) + return out.reshape(N, L, H, -1) \ No newline at end of file diff --git a/trellis/modules/sparse/attention/serialized_attn.py b/trellis/modules/sparse/attention/serialized_attn.py index b3da276..f86ef77 100644 --- a/trellis/modules/sparse/attention/serialized_attn.py +++ b/trellis/modules/sparse/attention/serialized_attn.py @@ -3,15 +3,39 @@ from enum import Enum import torch import math from .. import SparseTensor -from .. import DEBUG, ATTN +from trellis.backend_config import ( + get_attention_backend, + get_debug_mode, + get_available_backends +) +import logging -if ATTN == 'xformers': +logger = logging.getLogger(__name__) + +# Get configuration from central config +ATTN = get_attention_backend() +DEBUG = get_debug_mode() + +# Get available backends and import if active +available_backends = get_available_backends() + +if ATTN == "xformers" and available_backends['xformers']: import xformers.ops as xops -elif ATTN == 'flash_attn': +elif ATTN == "flash_attn" and available_backends['flash_attn']: import flash_attn +elif ATTN == "sage" and available_backends['sage']: + import torch.nn.functional as F + from sageattention import sageattn + F.scaled_dot_product_attention = sageattn +elif ATTN == "sdpa": + from torch.nn.functional import scaled_dot_product_attention as sdpa +elif ATTN == "naive": + from torch.nn.functional import scaled_dot_product_attention as naive else: raise ValueError(f"Unknown attention module: {ATTN}") +# Log the active backend +logger.info(f"Using attention backend: {ATTN}") __all__ = [ 'sparse_serialized_scaled_dot_product_self_attention', @@ -190,4 +214,4 @@ def sparse_serialized_scaled_dot_product_self_attention( qkv_coords = qkv_coords[bwd_indices] assert torch.equal(qkv_coords, qkv.coords), "SparseWindowedScaledDotProductSelfAttention: coordinate mismatch" - return qkv.replace(out) + return qkv.replace(out) \ No newline at end of file diff --git a/trellis/modules/sparse/attention/windowed_attn.py b/trellis/modules/sparse/attention/windowed_attn.py index 11eebf8..7f1f3ba 100644 --- a/trellis/modules/sparse/attention/windowed_attn.py +++ b/trellis/modules/sparse/attention/windowed_attn.py @@ -1,16 +1,43 @@ from typing import * -import torch +from enum import Enum import math +import os +import logging +import torch from .. import SparseTensor -from .. import DEBUG, ATTN +from trellis.backend_config import ( + get_attention_backend, + get_debug_mode, + get_available_backends +) +import logging -if ATTN == 'xformers': +logger = logging.getLogger(__name__) + +# Get configuration from central config +ATTN = get_attention_backend() +DEBUG = get_debug_mode() + +# Get available backends and import if active +available_backends = get_available_backends() + +if ATTN == "xformers" and available_backends['xformers']: import xformers.ops as xops -elif ATTN == 'flash_attn': +elif ATTN == "flash_attn" and available_backends['flash_attn']: import flash_attn +elif ATTN == "sage" and available_backends['sage']: + import torch.nn.functional as F + from sageattention import sageattn + F.scaled_dot_product_attention = sageattn +elif ATTN == "sdpa": + from torch.nn.functional import scaled_dot_product_attention as sdpa +elif ATTN == "naive": + from torch.nn.functional import scaled_dot_product_attention as naive else: raise ValueError(f"Unknown attention module: {ATTN}") +# Log the active backend +logger.info(f"Using attention backend: {ATTN}") __all__ = [ 'sparse_windowed_scaled_dot_product_self_attention', diff --git a/trellis/modules/sparse/basic.py b/trellis/modules/sparse/basic.py index 0fc6851..24a8220 100644 --- a/trellis/modules/sparse/basic.py +++ b/trellis/modules/sparse/basic.py @@ -1,10 +1,17 @@ from typing import * import torch import torch.nn as nn -from . import BACKEND, DEBUG + +from trellis.backend_config import get_debug_mode, get_spconv_algo, get_sparse_backend + + SparseTensorData = None # Lazy import +DEBUG = get_debug_mode() +SPCONV_ALGO = get_spconv_algo() + +BACKEND = get_sparse_backend() __all__ = [ 'SparseTensor', 'sparse_batch_broadcast', diff --git a/trellis/modules/sparse/conv/__init__.py b/trellis/modules/sparse/conv/__init__.py index 8ae5437..e3f0b95 100644 --- a/trellis/modules/sparse/conv/__init__.py +++ b/trellis/modules/sparse/conv/__init__.py @@ -1,7 +1,9 @@ -from .. import BACKEND +import os +import logging +from trellis.backend_config import get_sparse_backend, get_spconv_algo - -SPCONV_ALGO = 'auto' # 'auto', 'implicit_gemm', 'native' +BACKEND = get_sparse_backend() +SPCONV_ALGO = get_spconv_algo() def __from_env(): import os @@ -19,4 +21,9 @@ if BACKEND == 'torchsparse': from .conv_torchsparse import * elif BACKEND == 'spconv': from .conv_spconv import * + +__all__ = [ + "SparseConv3d", + "SparseInverseConv3d", +] diff --git a/trellis/modules/sparse/conv/conv_spconv.py b/trellis/modules/sparse/conv/conv_spconv.py index 856405d..2d8c9da 100644 --- a/trellis/modules/sparse/conv/conv_spconv.py +++ b/trellis/modules/sparse/conv/conv_spconv.py @@ -2,12 +2,17 @@ import torch import torch.nn as nn from .. import SparseTensor from .. import DEBUG -from . import SPCONV_ALGO +from trellis.backend_config import get_debug_mode, get_spconv_algo, get_sparse_backend + +# Get configuration from central config +DEBUG = get_debug_mode() +SPCONV_ALGO = get_spconv_algo() +BACKEND = get_sparse_backend() class SparseConv3d(nn.Module): def __init__(self, in_channels, out_channels, kernel_size, stride=1, dilation=1, padding=None, bias=True, indice_key=None): super(SparseConv3d, self).__init__() - if 'spconv' not in globals(): + if BACKEND == 'spconv': import spconv.pytorch as spconv algo = None if SPCONV_ALGO == 'native': @@ -52,7 +57,7 @@ class SparseConv3d(nn.Module): class SparseInverseConv3d(nn.Module): def __init__(self, in_channels, out_channels, kernel_size, stride=1, dilation=1, bias=True, indice_key=None): super(SparseInverseConv3d, self).__init__() - if 'spconv' not in globals(): + if BACKEND == 'spconv': import spconv.pytorch as spconv self.conv = spconv.SparseInverseConv3d(in_channels, out_channels, kernel_size, bias=bias, indice_key=indice_key) self.stride = tuple(stride) if isinstance(stride, (list, tuple)) else (stride, stride, stride) diff --git a/trellis/modules/sparse/conv/conv_torchsparse.py b/trellis/modules/sparse/conv/conv_torchsparse.py index a10bd91..feec227 100644 --- a/trellis/modules/sparse/conv/conv_torchsparse.py +++ b/trellis/modules/sparse/conv/conv_torchsparse.py @@ -1,12 +1,17 @@ import torch import torch.nn as nn from .. import SparseTensor +from trellis.backend_config import get_debug_mode, get_spconv_algo, get_sparse_backend +# Get configuration from central config +DEBUG = get_debug_mode() +SPCONV_ALGO = get_spconv_algo() +BACKEND = get_sparse_backend() class SparseConv3d(nn.Module): def __init__(self, in_channels, out_channels, kernel_size, stride=1, dilation=1, bias=True, indice_key=None): super(SparseConv3d, self).__init__() - if 'torchsparse' not in globals(): + if BACKEND == 'torchsparse': import torchsparse self.conv = torchsparse.nn.Conv3d(in_channels, out_channels, kernel_size, stride, 0, dilation, bias) @@ -22,7 +27,7 @@ class SparseConv3d(nn.Module): class SparseInverseConv3d(nn.Module): def __init__(self, in_channels, out_channels, kernel_size, stride=1, dilation=1, bias=True, indice_key=None): super(SparseInverseConv3d, self).__init__() - if 'torchsparse' not in globals(): + if BACKEND == 'torchsparse': import torchsparse self.conv = torchsparse.nn.Conv3d(in_channels, out_channels, kernel_size, stride, 0, dilation, bias, transposed=True) diff --git a/trellis_model_manager.py b/trellis_model_manager.py index 6458c19..9ae8935 100644 --- a/trellis_model_manager.py +++ b/trellis_model_manager.py @@ -172,12 +172,17 @@ class TrellisModelManager: return models def load_dinov2(self, model_name: str): - """Load DINOv2 model with device and precision management""" + """Load DINOv2 model with device, precision, and attention backend management""" try: - # Get use_fp16 from config dict or object + # Get configuration values use_fp16 = (self.config.get('use_fp16', True) if isinstance(self.config, dict) else getattr(self.config, 'use_fp16', True)) + + # Get attention backend from config + attention_backend = (self.config.get('attention_backend', 'default') + if isinstance(self.config, dict) + else getattr(self.config, 'attention_backend', 'default')) # Try to load from local path first model_path = folder_paths.get_full_path("classifiers", f"{model_name}.pth") @@ -185,8 +190,11 @@ class TrellisModelManager: if model_path is None: print(f"Downloading {model_name} from torch hub...") try: - # Load model architecture - model = torch.hub.load('facebookresearch/dinov2', model_name, pretrained=True) + # Load model architecture with specified attention backend + model = torch.hub.load('facebookresearch/dinov2', model_name, + pretrained=True, + force_reload=False, + trust_repo=True) # Save model for future use save_dir = os.path.join(folder_paths.models_dir, "classifiers") @@ -203,7 +211,10 @@ class TrellisModelManager: else: # Load from local path print(f"Loading DINOv2 model from {model_path}") - model = torch.hub.load('facebookresearch/dinov2', model_name, pretrained=False) + model = torch.hub.load('facebookresearch/dinov2', model_name, + pretrained=False, + force_reload=False, + trust_repo=True) model.load_state_dict(torch.load(model_path)) # Move model to specified device and apply precision settings @@ -211,6 +222,10 @@ class TrellisModelManager: if use_fp16: model = model.half() + # Set attention backend if specified in config + if hasattr(model, 'set_attention_backend') and attention_backend != 'default': + model.set_attention_backend(attention_backend) + model.eval() return model