Windows/Blackwell support, plus fix DINOv3 feature extraction on Transformers 5.x - #185
Windows/Blackwell support, plus fix DINOv3 feature extraction on Transformers 5.x#185rwfsmith wants to merge 16 commits into
Conversation
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>
|
Following up on the "Needs discussion before merge" section — I've now filed the upstream PRs so the fork pins in CuMesh
FlexGEMM
Once those land, I'll update Happy to also split the two correctness fixes in this PR (the DINOv3 Transformers 5.x fix and the |
|
@microsoft-github-policy-service agree |
|
Update: while testing further I found and fixed a third correctness bug, this one also cross-platform, in 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: The corrupted coordinates decoded to exactly Validation (N generations in one process, same input):
0/5 clean before, 5/5 after, confirmed visually as well. No change to this PR's diff is needed — |
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>
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_featuresiteratesself.model.layer. In current Transformers,DINOv3ViTModelhas no.layer— the blocks moved into the encoder submodule atDINOv3ViTModel.model.layer. Verified ontransformers==5.15.1:So
mainraisesAttributeError: '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.forwardapplies the backbone's trained finalLayerNorm(elementwise_affine=True, confirmed above), whereas this code is built on a parameter-freeF.layer_normof the pre-norm states. That learned affine shifts the conditioning distribution enough that the sparse-structure flow sampler collapsesz_sto ≈0 and decodes an all-empty voxel grid on roughly 1 seed in 6. It surfaces ~300 lines downstream as: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-freeF.layer_normsemantics unchanged.Measured on a fixed 40-seed sparse-structure sweep:
z_sstdThe 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 twoextract_features/_encoder_layersimplementations are verified AST-identical.How this was isolated: running the same 40 seeds under both
flash_attnandsdpaproduced 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.cppuses1e-6dand0.0d.dis not a valid floating-point suffix in C++ (it's Java/D); in C++11 it parses as a user-defined literaloperator""d, which isn't defined here. MSVC rejects it. Changed to1e-6/0.0— these were alreadydoublecontexts, so no behavior change.Also
torch::from_blob(..., {svo.size()}, ...):size()returnssize_t, and narrowing inside a braced-init-list is ill-formed. Added an explicitstatic_cast<int64_t>.3. Windows support (additive)
requirements.txt— pip-only install; no conda requiredsetup_windows.ps1/.bat/.py— three entry points (PowerShell, cmd.exe, pure Python); locate VS2022 build tools, setCUDA_HOME/TORCH_CUDA_ARCH_LIST, then pip installflash_attnactually runs on the GPU and fall back tosdpaif not. Linux behavior is unchanged —_default_backend()returns'flash_attn'unconditionally off Windows.'sdpa'to theSPARSE_ATTN_BACKENDenv allowlist and toset_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 crypticmax()failure 300 lines later..gitignoreentries fortmp/andexample.pyoutput.requirements.txtpins two dependencies to my forks: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.py→sample.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 extentsapp.pyend-to-end through the Gradio UI, including GLB extractionassets/example_image/T.png, a combination that previously crashed, now produces a clean meshScope / limitations
platform.system() == 'Windows', and the DINOv3 and C++ changes preserve existing semantics, but a Linux CI run would be worth doing.