Skip to content

Windows/Blackwell support, plus fix DINOv3 feature extraction on Transformers 5.x - #185

Open
rwfsmith wants to merge 16 commits into
microsoft:mainfrom
rwfsmith:windows-blackwell
Open

Windows/Blackwell support, plus fix DINOv3 feature extraction on Transformers 5.x#185
rwfsmith wants to merge 16 commits into
microsoft:mainfrom
rwfsmith:windows-blackwell

Conversation

@rwfsmith

Copy link
Copy Markdown

Summary

Adds Windows + Blackwell (sm_120) support and fixes two correctness bugs found while getting the pipeline running there. Tested end-to-end on Windows 11 / Python 3.13 / CUDA 13.0 / RTX 5090, without conda.

The two correctness fixes are not Windows-specific and are the most valuable part of this PR — they are cleanly separable if you'd prefer to take them alone.


1. DINOv3 feature extraction is broken on Transformers 5.x (cross-platform)

DinoV3FeatureExtractor.extract_features iterates self.model.layer. In current Transformers, DINOv3ViTModel has no .layer — the blocks moved into the encoder submodule at DINOv3ViTModel.model.layer. Verified on transformers==5.15.1:

transformers 5.15.1
has .layer      : False
has .model.layer: True
type(m.model)   : DINOv3ViTEncoder
has .norm       : True LayerNorm
norm affine     : True

So main raises AttributeError: 'DINOv3ViTModel' object has no attribute 'layer' on any recent Transformers.

The tempting one-line fix — returning self.model(...).last_hidden_state — is wrong, and fails quietly. DINOv3ViTModel.forward applies the backbone's trained final LayerNorm (elementwise_affine=True, confirmed above), whereas this code is built on a parameter-free F.layer_norm of the pre-norm states. That learned affine shifts the conditioning distribution enough that the sparse-structure flow sampler collapses z_s to ≈0 and decodes an all-empty voxel grid on roughly 1 seed in 6. It surfaces ~300 lines downstream as:

RuntimeError: max(): Expected reduction dim to be specified for input.numel() == 0

I hit exactly this and spent a while chasing it, which is why the fix is written defensively.

This PR adds _encoder_layers(), which resolves the block list across Transformers versions, then keeps the original manual iteration and parameter-free F.layer_norm semantics unchanged.

Measured on a fixed 40-seed sparse-structure sweep:

before after
empty grids 7/40 0/40
voxel count 139–2169 3299–4338
z_s std 0.11–0.41 0.58–0.65

The whole distribution tightens, so this improves output fidelity on every seed, not just the ones that crashed.

Applied to both copies of DinoV3FeatureExtractor (inference and trainer); the two extract_features/_encoder_layers implementations are verified AST-identical.

How this was isolated: running the same 40 seeds under both flash_attn and sdpa produced the identical failing seed set, which ruled out attention-backend numerics and pointed upstream to the conditioning.

2. Invalid C++ literal suffixes in o-voxel (portability)

flexible_dual_grid.cpp uses 1e-6d and 0.0d. d is not a valid floating-point suffix in C++ (it's Java/D); in C++11 it parses as a user-defined literal operator""d, which isn't defined here. MSVC rejects it. Changed to 1e-6 / 0.0 — these were already double contexts, so no behavior change.

Also torch::from_blob(..., {svo.size()}, ...): size() returns size_t, and narrowing inside a braced-init-list is ill-formed. Added an explicit static_cast<int64_t>.


3. Windows support (additive)

  • requirements.txt — pip-only install; no conda required
  • setup_windows.ps1 / .bat / .py — three entry points (PowerShell, cmd.exe, pure Python); locate VS2022 build tools, set CUDA_HOME / TORCH_CUDA_ARCH_LIST, then pip install
  • Attention backend defaults: on Windows, probe whether flash_attn actually runs on the GPU and fall back to sdpa if not. Linux behavior is unchanged_default_backend() returns 'flash_attn' unconditionally off Windows.
  • Added missing 'sdpa' to the SPARSE_ATTN_BACKEND env allowlist and to set_attn_backend's type hint; the sdpa path existed but was unreachable via env var.
  • sample_sparse_structure() now raises an actionable error on an empty grid instead of the cryptic max() failure 300 lines later.
  • README section for the Windows path; .gitignore entries for tmp/ and example.py output.

⚠️ Needs discussion before merge

requirements.txt pins two dependencies to my forks:

cumesh     @ git+https://github.com/rwfsmith/CuMesh.git@windows-py313-cuda13
flex_gemm  @ git+https://github.com/rwfsmith/FlexGEMM.git@windows-py313-cuda13

These carry Windows/CUDA-13 build fixes, and the CuMesh branch also fixes an out-of-bounds GPU read in the narrow-band dual contouring remesh kernel that made to_glb(..., remesh=True) non-deterministically produce shredded meshes — same inputs, different result run to run. That one is a real correctness bug worth upstreaming to CuMesh independently.

I don't expect you'd want to ship pins to a personal fork. Happy to split this PR so the two correctness fixes land on their own, or to rework the packaging however you prefer.

Verification

  • example.pysample.mp4 (4.0 MB) + sample.glb (44.1 MB); mesh checks out at 808k verts / 973k faces, 0 NaNs, 6 degenerate faces of 973k, sane extents
  • app.py end-to-end through the Gradio UI, including GLB extraction
  • Seed 1 + assets/example_image/T.png, a combination that previously crashed, now produces a clean mesh

Scope / limitations

  • Validated on a single configuration (Windows 11, Python 3.13, CUDA 13.0, RTX 5090). Not tested on other Windows GPUs or CUDA versions.
  • I did not regression-test on Linux. The Windows-specific changes are guarded by platform.system() == 'Windows', and the DINOv3 and C++ changes preserve existing semantics, but a Linux CI run would be worth doing.
  • The DINOv3 fix is validated by distribution behavior and by restoring the original intent of the code, not by a bit-for-bit comparison against a known-good older-Transformers reference.

rwfsmith and others added 9 commits August 20, 2026 10:51
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Probe whether flash_attn is installed and actually runs on the current
GPU at import time (shared cached check in modules/_attn_probe.py),
and default to it automatically when usable. Falls back to sdpa
otherwise. ATTN_BACKEND/SPARSE_ATTN_BACKEND env vars still take
precedence and force a specific backend.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Explains the non-deterministic shredded-mesh bug in o_voxel's
remesh=True path (extract_glb() in app.py) and points to the
rwfsmith/CuMesh fix that resolves it.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Transformers 5.x moved the DINOv3 transformer blocks from
DINOv3ViTModel.layer to DINOv3ViTModel.model.layer, so the previous
lookup never found them and always took the last_hidden_state fallback.

That fallback is not equivalent. DINOv3ViTModel.forward applies the
backbone's trained final LayerNorm (learned weight/bias), whereas
TRELLIS.2 was conditioned on a parameter-free F.layer_norm of the
pre-norm states. The learned affine shifts the feature distribution far
enough out of distribution that the sparse-structure flow sampler
collapses z_s to ~0 and decodes an all-empty voxel grid on roughly one
seed in six. The failure surfaced ~300 lines downstream as a cryptic
"RuntimeError: max(): Expected reduction dim to be specified for
input.numel() == 0" in sparse/basic.py.

extract_features now resolves the block list across Transformers
versions via a new _encoder_layers() helper, runs the blocks directly,
and applies the parameter-free F.layer_norm the models expect.

Verified with a fixed 40-seed sparse-structure sweep:
  - empty grids: 7/40 -> 0/40
  - voxel count: 139-2169 -> 3299-4338
  - z_s std:     0.11-0.41 -> 0.58-0.65
The same 7 seeds failed identically under both flash_attn and sdpa,
which ruled out attention-backend numerics and pointed upstream to the
conditioning. Tightening the whole distribution means this improves
output fidelity on every seed, not just the ones that crashed.

Also raise an actionable RuntimeError in sample_sparse_structure() when
the grid is genuinely empty, and gitignore app.py's tmp/ output dir.

End-to-end validated through the Gradio app on Windows 11 / Python 3.13
/ CUDA 13.0 / RTX 5090: seed 1 with assets/example_image/T.png (a
previously crashing combination) now generates a clean mesh and exports
a 22.4 MB GLB (451k verts, 491k faces, 0 NaNs).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
README step 5 documents that a successful run writes sample.mp4 and
sample.glb. Those are ~48 MB of generated binaries that should not be
committed, and leaving them untracked makes `git status` noisy after
every validation run.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
trellis2/trainers/flow_matching/mixins/image_conditioned.py carries a
second copy of DinoV3FeatureExtractor for the training path. It still
had the naive `hasattr(self.model, 'layer')` guard, so on Transformers
5.x it always fell through to `last_hidden_state` and applied the
backbone's trained final LayerNorm instead of the parameter-free
F.layer_norm the models expect -- the same conditioning bug already
fixed on the inference path in 3f4faad.

Port the same _encoder_layers() resolution so both copies behave
identically. Verified the two extract_features and _encoder_layers
implementations are now AST-identical.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@rwfsmith

Copy link
Copy Markdown
Author

Following up on the "Needs discussion before merge" section — I've now filed the upstream PRs so the fork pins in requirements.txt can be removed rather than merged:

CuMesh

  • JeffreyXiang/CuMesh#39 — out-of-bounds UDF read in the dual contouring kernel. This one is not Windows-specific: get_vertex_val() uses a hashmap lookup result as an array index without checking the miss sentinel, so it reads ~16 GB past the base pointer on a miss. It's UB on every platform and causes silent, intermittent mesh corruption; it just happened to be reproducible on my setup. Worth taking regardless of anything in this PR.
  • JeffreyXiang/CuMesh#40 — C++20 for the existing Windows MSVC flags (PyTorch header requirement).

FlexGEMM

  • JeffreyXiang/FlexGEMM#31template disambiguator on four dependent data_ptr<T>() calls that MSVC rejects, plus the same C++20 flag bump.

Once those land, I'll update requirements.txt here to point back at the official JeffreyXiang/* repos, which removes the only part of this PR that pins to a personal fork.

Happy to also split the two correctness fixes in this PR (the DINOv3 Transformers 5.x fix and the o-voxel invalid C++ literal suffixes) into a separate, smaller PR if you'd prefer to evaluate those independently of the Windows support work — the DINOv3 one in particular fixes a hard AttributeError crash on current Transformers that affects Linux users too.

@rwfsmith

Copy link
Copy Markdown
Author

@microsoft-github-policy-service agree

@rwfsmith

Copy link
Copy Markdown
Author

Update: while testing further I found and fixed a third correctness bug, this one also cross-platform, in CuMesh::fill_holes. It's now in the pinned rwfsmith/CuMesh@windows-py313-cuda13 branch and upstreamed separately as JeffreyXiang/CuMesh#41.

Symptom: every generation after the first one in a process produced a mesh smeared into a cylindrical column of sliver triangles. Because the first run in a fresh process was almost always fine, single-shot repros never showed it — but the Gradio app is long-lived, so in practice the 2nd image onward was corrupt.

Cause: fill_holes averages hole-cap vertices with cub::DeviceSegmentedReduce::Sum over Vec3f. CUB builds the reduction identity on the host as InitT{}. Vec3f has a user-provided default constructor, so Vec3f{} calls it rather than zero-filling — and that constructor is __device__-only, so it isn't callable from host code. The identity was therefore raw host stack memory, folded into every segment sum.

The corrupted coordinates decoded to exactly G / n for small integers n (G ≈ 9.52e9), which is only possible if a single constant is added to each segment before dividing by segment size — that pinned it to the identity. Fix is 6 lines: make Vec3f's constructors __host__ __device__.

Validation (N generations in one process, same input):

before after
run 0 ok ok
runs 1–4 broken — max edge 4.76e9, 3910 long edges, 52.3% mask coverage ok — max edge 0.005, 0 long edges, 30.0% coverage

0/5 clean before, 5/5 after, confirmed visually as well.

No change to this PR's diff is needed — requirements.txt already pins that branch. Flagging it here since it's the third cross-platform correctness fix uncovered by this work, alongside CuMesh#39, CuMesh#40, and FlexGEMM#31.

rwfsmith and others added 7 commits August 21, 2026 16:37
The forked dependencies were pinned to a mutable branch, so a rebuild could
silently pick up different code than the build that was validated. Pin both to
commit SHAs instead, matching how utils3d is already pinned, and keep
o-voxel/pyproject.toml in sync with requirements.txt.

Also documents the second cross-platform CuMesh correctness fix: an
uninitialized CUB reduction identity in fill_holes. CUB builds the identity on
the host as InitT{}, but Vec3f has a user-provided default constructor that is
__device__-only, so the identity was raw host stack memory folded into every
segment sum. The first generation in a process was usually clean and every one
after it was corrupt, which is why it only appeared from the second image
onward in the Gradio app.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add GitHub Actions workflow triggered on pull requests targeting main,
  pushes to main, and manual dispatch.
- Add PR/main build checks: validate dependency spec syntax and compile
  Python sources as a fast smoke build.
- Add push-to-main CD snapshot job that uploads a build manifest artifact.
- Document the fork workflow in README.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add .github/workflows/release.yml for v* tag releases and manual dispatch.
- Package tagged source into .tar.gz and .zip artifacts.
- Publish release artifacts with SHA256 checksums and build manifest.
- Document release workflow in README CI/CD section.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Pin the validated CuMesh and FlexGEMM C++17 build commits.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Use the validated C++17 CuMesh and FlexGEMM revisions consistently.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Use the C++17 standard required by the PyTorch extension toolchain.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant