Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 117 additions & 0 deletions .github/workflows/ci-cd.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
name: CI / CD

on:
pull_request:
branches: [ "main" ]
types: [opened, synchronize, reopened, ready_for_review]
merge_group:
branches: [ "main" ]
push:
branches: [ "main" ]
workflow_dispatch:

concurrency:
group: ci-cd-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
build:
name: Build checks (Python ${{ matrix.python-version }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: [ "3.13" ]

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: pip

- name: Upgrade pip
run: python -m pip install --upgrade pip

- name: Install CI helper deps
run: python -m pip install packaging

- name: Validate dependency spec files
run: |
python - <<'PY'
import tomllib
from pathlib import Path
from packaging.requirements import Requirement

def check_req_line(path: Path):
bad = []
for i, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1):
s = line.strip()
if not s or s.startswith("#") or s.startswith("--") or s.startswith("./"):
continue
try:
Requirement(s)
except Exception as exc:
bad.append((i, s, str(exc)))
return bad

errors = []
req_errors = check_req_line(Path("requirements.txt"))
if req_errors:
for line_no, value, exc in req_errors:
errors.append(f"requirements.txt:{line_no}: {value} :: {exc}")

pyproject = tomllib.loads(Path("o-voxel/pyproject.toml").read_text(encoding="utf-8"))
for dep in pyproject["project"]["dependencies"]:
try:
Requirement(dep)
except Exception as exc:
errors.append(f"o-voxel/pyproject.toml dependency {dep!r} :: {exc}")

if errors:
raise SystemExit("\n".join(errors))
print("Dependency specs are valid.")
PY

- name: Compile Python sources
run: |
python -m compileall -q app.py app_texturing.py example.py example_texturing.py setup_windows.py train.py
python -m compileall -q trellis2
python -m compileall -q o-voxel/o_voxel

cd-artifacts:
name: CD artifact snapshot
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
needs: build
permissions:
contents: read

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Create build manifest
run: |
mkdir -p out
cat > out/build-manifest.json <<'JSON'
{
"repo": "${{ github.repository }}",
"branch": "${{ github.ref_name }}",
"commit": "${{ github.sha }}",
"workflow": "${{ github.workflow }}",
"run_id": "${{ github.run_id }}",
"run_number": "${{ github.run_number }}",
"created_utc": "${{ github.event.head_commit.timestamp }}"
}
JSON

- name: Upload build manifest
uses: actions/upload-artifact@v4
with:
name: build-manifest-${{ github.run_number }}
path: out/build-manifest.json
if-no-files-found: error
84 changes: 84 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
name: Release (tag)

on:
push:
tags:
- "v*"
workflow_dispatch:
inputs:
tag:
description: "Existing tag to release (e.g. v1.0.0)"
required: true
type: string

permissions:
contents: write

concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false

jobs:
release:
name: Create GitHub release
runs-on: ubuntu-latest

steps:
- name: Resolve release tag
id: tag
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "value=${{ inputs.tag }}" >> "$GITHUB_OUTPUT"
else
echo "value=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT"
fi

- name: Checkout repository at tag
uses: actions/checkout@v4
with:
ref: ${{ steps.tag.outputs.value }}
fetch-depth: 0
submodules: recursive

- name: Build release archives
shell: bash
run: |
set -euo pipefail
TAG="${{ steps.tag.outputs.value }}"
STAGE="release-stage"
OUT="release-out"
PKG="TRELLIS.2-${TAG}"
mkdir -p "$STAGE/$PKG" "$OUT"

rsync -a \
--exclude '.git' \
--exclude '.github' \
--exclude '__pycache__' \
--exclude '*.pyc' \
./ "$STAGE/$PKG/"

cat > "$OUT/build-manifest.json" <<JSON
{
"repo": "${{ github.repository }}",
"tag": "${TAG}",
"commit": "${{ github.sha }}",
"workflow": "${{ github.workflow }}",
"run_id": "${{ github.run_id }}",
"run_number": "${{ github.run_number }}"
}
JSON

tar -C "$STAGE" -czf "$OUT/${PKG}.tar.gz" "$PKG"
(cd "$STAGE" && zip -r "../$OUT/${PKG}.zip" "$PKG" >/dev/null)
(cd "$OUT" && sha256sum "${PKG}.tar.gz" "${PKG}.zip" > checksums.sha256)

- name: Publish GitHub release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.tag.outputs.value }}
generate_release_notes: true
files: |
release-out/*.tar.gz
release-out/*.zip
release-out/checksums.sha256
release-out/build-manifest.json
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -205,3 +205,10 @@ cython_debug/
marimo/_static/
marimo/_lsp/
__marimo__/

# app.py runtime output
tmp/

# example.py generated output
sample.mp4
sample.glb
83 changes: 82 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,74 @@ Data processing is streamlined for instant conversions that are fully **renderin
- Python version 3.8 or higher is required.

### Installation Steps

#### Windows (Python 3.13 + RTX 50-series / Blackwell)
This fork includes a pip-first Windows path for Python 3.13, CUDA Toolkit 13.0, PyTorch `2.13.0+cu130`, and NVIDIA RTX 50-series / Blackwell GPUs (`sm_120`). Conda is not required.

1. Clone the repo and initialize submodules:
```powershell
git clone -b windows-blackwell https://github.com/rwfsmith/TRELLIS.2.git --recursive
cd TRELLIS.2
git submodule update --init --recursive
```

2. Create and activate a Python 3.13 virtual environment:
```powershell
py -3.13 -m venv venv
.\venv\Scripts\Activate.ps1
python -m pip install --upgrade pip setuptools wheel
```

3. Install Visual Studio 2022 C++ build tools and CUDA Toolkit 13.0. Then install dependencies:
```powershell
powershell -ExecutionPolicy Bypass -File .\setup_windows.ps1 -Python .\venv\Scripts\python.exe
```
The setup script wraps:
```powershell
python -m pip install -r requirements.txt --no-build-isolation
```
and sets the native build environment variables needed by PyTorch CUDA extensions:
`CUDA_HOME`, `TORCH_CUDA_ARCH_LIST=12.0`, `DISTUTILS_USE_SDK=1`, and `MSSdk=1`.

If CUDA is installed somewhere other than `C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v13.0`, pass it explicitly:
```powershell
powershell -ExecutionPolicy Bypass -File .\setup_windows.ps1 -Python .\venv\Scripts\python.exe -CudaHome "D:\CUDA\v13.0"
```

If you are running from `cmd.exe` instead of PowerShell, use the bundled batch wrapper instead (it forwards all arguments to `setup_windows.ps1`):
```bat
setup_windows.bat -Python .\venv\Scripts\python.exe
```

Alternatively, a pure Python setup script is provided for those who prefer not to invoke PowerShell or a `.bat` file at all:
```
python setup_windows.py --python .\venv\Scripts\python.exe
```
It performs the same steps as `setup_windows.ps1` (locate VS2022 build tools, set `CUDA_HOME`/`TORCH_CUDA_ARCH_LIST`, then `pip install -r requirements.txt --no-build-isolation`). Use `--cuda-home` and `--torch-cuda-arch-list` to override the CUDA path or target GPU architecture.

On Windows, TRELLIS.2 does not require `flash-attn`: at startup it probes whether `flash_attn` is installed and actually runs on your GPU, and automatically falls back to PyTorch SDPA if it's missing or fails. If you install a working `flash-attn` build, it will be used automatically; you can still force a specific backend with `ATTN_BACKEND` or `SPARSE_ATTN_BACKEND` (`xformers`, `flash_attn`, `flash_attn_3`, `sdpa`, `naive`).

This fork's `requirements.txt` pins `cumesh` and `flex_gemm` to exact commits of [rwfsmith/CuMesh](https://github.com/rwfsmith/CuMesh) and [rwfsmith/FlexGEMM](https://github.com/rwfsmith/FlexGEMM). Besides the Windows/CUDA-13 build fixes, those commits carry two **cross-platform correctness fixes** to CuMesh that are not specific to this fork's platform support:

- **Out-of-bounds UDF read in the narrow-band dual contouring remesh kernel.** Caused `o_voxel.postprocess.to_glb(..., remesh=True)` (used by `extract_glb()` in `app.py`) to non-deterministically produce shredded/spiky meshes — identical code and latents could yield a clean or corrupted mesh from run to run. Upstreamed as [CuMesh#39](https://github.com/JeffreyXiang/CuMesh/pull/39).
- **Uninitialized CUB reduction identity in `fill_holes`.** `fill_holes` averages hole-cap vertices with `cub::DeviceSegmentedReduce::Sum` over `Vec3f`. CUB builds the reduction identity on the *host* as `InitT{}`, but `Vec3f` has a user-provided default constructor (so `Vec3f{}` calls it instead of zero-filling) that is marked `__device__`-only (so it isn't callable from host code). The identity was therefore raw host stack memory, folded into every segment sum. Because it depends on stack residue, the first generation in a process was usually clean and **every generation after it was corrupt**, smearing the model into a cylindrical column of sliver triangles — so it only showed up from the second image onward in the long-lived Gradio app. Upstreamed as [CuMesh#41](https://github.com/JeffreyXiang/CuMesh/pull/41).

If you see corrupted meshes, verify you are building against these pinned commits rather than upstream `main`.

This fork also fixes the DINOv3 image conditioning for Transformers 5.x. Recent releases moved the DINOv3 transformer blocks from `DINOv3ViTModel.layer` to `DINOv3ViTModel.model.layer`, so the original lookup missed them. Falling back to `last_hidden_state` is *not* equivalent: `DINOv3ViTModel.forward` applies the backbone's trained final `LayerNorm`, whose learned affine shifts the feature distribution TRELLIS.2 was conditioned on. The resulting out-of-distribution conditioning made the sparse-structure flow sampler collapse to an all-empty voxel grid on roughly 1 seed in 6, surfacing 300 lines downstream as a cryptic `RuntimeError: max(): Expected reduction dim to be specified for input.numel() == 0`. `DinoV3FeatureExtractor.extract_features` now locates the block list across Transformers versions, runs the blocks directly, and applies a parameter-free `F.layer_norm`. On a fixed 40-seed sweep this took empty-grid failures from 7/40 to 0/40 and tightened the generated voxel count from 139–2169 to 3299–4338, so it improves output fidelity on every seed, not just the ones that crashed.

4. Log in to Hugging Face before running the pretrained model. TRELLIS.2 loads gated dependencies, including `facebook/dinov3-vitl16-pretrain-lvd1689m`, so your account must have access:
```powershell
huggingface-cli login
```

5. Run the full image-to-3D example:
```powershell
python example.py
```
A successful run writes `sample.mp4` and `sample.glb`.

#### Linux
1. Clone the repo:
```sh
git clone -b main https://github.com/microsoft/TRELLIS.2.git --recursive
Expand Down Expand Up @@ -99,9 +167,22 @@ Data processing is streamlined for instant conversions that are fully **renderin
--nvdiffrec Install nvdiffrec
```

## 🔁 CI/CD (Fork)

This fork includes a GitHub Actions workflow at [.github/workflows/ci-cd.yml](.github/workflows/ci-cd.yml):

- **CI / PR build**: runs on every PR targeting `main` (opened/synchronize/reopened/ready_for_review), plus direct pushes to `main`.
- **Checks performed**: validates dependency spec syntax in `requirements.txt` and `o-voxel/pyproject.toml`, then compiles Python sources (`compileall`) for a fast build smoke test.
- **CD snapshot on `main`**: after a successful `main` push build, uploads a `build-manifest.json` artifact with commit/run metadata.

This fork also includes a tag-triggered release workflow at [.github/workflows/release.yml](.github/workflows/release.yml):

- **Release trigger**: pushes of tags matching `v*` (for example `v1.0.0`), plus optional manual `workflow_dispatch`.
- **Release artifacts**: publishes `TRELLIS.2-<tag>.tar.gz`, `TRELLIS.2-<tag>.zip`, `checksums.sha256`, and a `build-manifest.json` file to the GitHub Release.

## 📦 Pretrained Weights

The pretrained model **TRELLIS.2-4B** is available on Hugging Face. Please refer to the model card there for more details.
The pretrained model **TRELLIS.2-4B** is available on Hugging Face. Please refer to the model card there for more details. The image encoder dependency `facebook/dinov3-vitl16-pretrain-lvd1689m` is gated on Hugging Face; authenticate with an account that has access before running the example or web demo.

| Model | Parameters | Resolution | Link |
| :--- | :--- | :--- | :--- |
Expand Down
4 changes: 2 additions & 2 deletions o-voxel/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,6 @@ dependencies = [
"tqdm",
"zstandard",
"easydict",
"cumesh @ git+https://github.com/JeffreyXiang/CuMesh.git",
"flex_gemm @ git+https://github.com/JeffreyXiang/FlexGEMM.git",
"cumesh @ git+https://github.com/rwfsmith/CuMesh.git@683440c8d3959653384647e7bb7218dbd99eecca",
"flex_gemm @ git+https://github.com/rwfsmith/FlexGEMM.git@7598fe133f5b1425d5624b4c00e1f381412b96ce",
]
13 changes: 11 additions & 2 deletions o-voxel/setup.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
from setuptools import setup
from torch.utils.cpp_extension import CUDAExtension, BuildExtension, IS_HIP_EXTENSION
import os
import platform

ROOT = os.path.dirname(os.path.abspath(__file__))
BUILD_TARGET = os.environ.get("BUILD_TARGET", "auto")
IS_WINDOWS = platform.system() == "Windows"

if BUILD_TARGET == "auto":
if IS_HIP_EXTENSION:
Expand All @@ -22,6 +24,13 @@
archs = os.getenv("GPU_ARCHS", "native").split(";")
cc_flag = [f"--offload-arch={arch}" for arch in archs]

if IS_WINDOWS:
cxx_flags = ["/O2", "/std:c++17", "/EHsc", "/permissive-", "/Zc:__cplusplus"]
nvcc_flags = ["-O3", "-std=c++17", "--expt-relaxed-constexpr", "--extended-lambda", "-allow-unsupported-compiler", "-Xcompiler=/std:c++17", "-Xcompiler=/EHsc", "-Xcompiler=/permissive-", "-Xcompiler=/Zc:__cplusplus"] + cc_flag
else:
cxx_flags = ["-O3", "-std=c++17"]
nvcc_flags = ["-O3", "-std=c++17"] + cc_flag

setup(
name="o_voxel",
packages=[
Expand Down Expand Up @@ -56,8 +65,8 @@
os.path.join(ROOT, "third_party/eigen"),
],
extra_compile_args={
"cxx": ["-O3", "-std=c++17"],
"nvcc": ["-O3","-std=c++17"] + cc_flag,
"cxx": cxx_flags,
"nvcc": nvcc_flags,
}
)
],
Expand Down
5 changes: 2 additions & 3 deletions o-voxel/src/convert/flexible_dual_grid.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,7 @@ void boundry_qef(
// Calculate the QEF for the edge (boundary) defined by v0 and v1
Eigen::Vector3d dir(v1.x() - v0.x(), v1.y() - v0.y(), v1.z() - v0.z());
double segment_length = dir.norm();
if (segment_length < 1e-6d) continue; // Skip degenerate edges (zero-length)
if (segment_length < 1e-6) continue; // Skip degenerate edges (zero-length)
dir.normalize(); // unit direction vector

// Projection matrix orthogonal to the direction: I - d d^T
Expand Down Expand Up @@ -334,7 +334,7 @@ void boundry_qef(

Eigen::Vector3d tMax, tDelta;
for (int axis = 0; axis < 3; ++axis) {
if (dir[axis] == 0.0d) {
if (dir[axis] == 0.0) {
tMax[axis] = std::numeric_limits<double>::infinity();
tDelta[axis] = std::numeric_limits<double>::infinity();
} else {
Expand Down Expand Up @@ -772,4 +772,3 @@ std::tuple<torch::Tensor, torch::Tensor, torch::Tensor> mesh_to_flexible_dual_gr
torch::from_blob(intersected.data(), {int(intersected.size()), 3}, torch::kBool).clone()
);
}

4 changes: 2 additions & 2 deletions o-voxel/src/io/filter_neighbor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ torch::Tensor encode_sparse_voxel_octree_attr_neighbor_cpu(
}

// Pack the deltas into a uint8 tensor
torch::Tensor delta = torch::zeros({N, C}, torch::dtype(torch::kUInt8));
torch::Tensor delta = torch::zeros({static_cast<int64_t>(N), static_cast<int64_t>(C)}, torch::dtype(torch::kUInt8));
uint8_t* delta_data = delta.data_ptr<uint8_t>();
for (int i = 0; i < N; i++) {
int x = coord_data[i * 3 + 0];
Expand Down Expand Up @@ -163,7 +163,7 @@ torch::Tensor decode_sparse_voxel_octree_attr_neighbor_cpu(
}

// Pack the attribute into a uint8 tensor
torch::Tensor attr = torch::zeros({N, C}, torch::dtype(torch::kUInt8));
torch::Tensor attr = torch::zeros({static_cast<int64_t>(N), static_cast<int64_t>(C)}, torch::dtype(torch::kUInt8));
uint8_t* attr_data = attr.data_ptr<uint8_t>();
for (int i = 0; i < N; i++) {
int x = coord_data[i * 3 + 0];
Expand Down
Loading