Skip to content

test(3d): Paint schedule, UniPC, Euler-ancestral and 2.1 guidance against diffusers - #793

Merged
kalebbroo merged 3 commits into
mainfrom
feat/paint-sampler-reference
Nov 12, 2026
Merged

kalebbroo merged 3 commits into
mainfrom
feat/paint-sampler-reference

Conversation

@kalebbroo

Copy link
Copy Markdown
Contributor

What

Parity of Paint's scheduler and guidance against diffusers and upstream. Oracle tests/python-reference/dump_paint_sampler.py builds UniPCMultistepScheduler (Paint 2.1, 15 steps, trailing) and EulerAncestralDiscreteScheduler (Paint 2.0, 30 steps) from the released scheduler_config.json, runs both from a fixed random sample with a fixed synthetic model (noise injected for the ancestral one), and transcribes upstream's guidance (cam_mapping view scale, the three-way combination). HunyuanPaintSamplerReferenceTests replays it in C# (env HARTSY_PAINT_SAMPLER_REF, no weights needed). HunyuanPaintPipeline.Guide gets a static overload that takes the per-view scales so it can be tested.

Result (CPU, diffusers 0.42.1)

Check Result
UniPC trajectory, 15 / 30 steps relRMS 2.3e-7 / 4.6e-7
Euler ancestral trajectory, 15 / 30 steps 8.7e-7 / 1.1e-6 (model input 1e-6, initial sigma 4096 both)
Trailing timesteps, sigmas timesteps equal, sigmas 8e-7
alphas_cumprod within 3.8e-5 relative, largest at timestep 998 (alpha 2e-7, diffusers' float32 table)
View scale (azimuths 0 to 345), guidance (2 materials x 8 views) scales equal, relRMS 8e-8

A synthetic model drives both sides, so this compares the scheduler arithmetic, not a network.

Not verified

The bake, the DINO encoder, a full run, the VAE on the GPU. Paint 2.0's UNet has no parity run. Both this PR and #788 edit the Paint 2.1 row of PARITY_VERIFICATION.md; the second to merge resolves the conflict.

…inst diffusers and upstream (oracle script, reference tests)
@kalebbroo

Copy link
Copy Markdown
Contributor Author

@claude review PR

@claude

claude Bot commented Nov 12, 2026 •

Copy link
Copy Markdown

Claude finished @kalebbroo's task in 1m 21s —— View job


Review: PR #793 (Paint sampler and guidance parity)

  • Gather context
  • Read diff (oracle script, C# test, Guide overload, parity doc)
  • Cross-check against HunyuanPaintSchedule, HunyuanPaintEulerAncestralSampler, HunyuanPaintUniPcSampler
  • Post review

Verdict: looks good to merge. I found no correctness bugs. The production change is a behaviour-preserving refactor. The oracle and the C# test model the same thing. The items below are tightening and doc nits. I did not run the tests: the fixture needs diffusers and the released scheduler_config.json, and neither is available here.

What I checked

  • Guide refactor (HunyuanPaintPipeline.cs:303-322): the instance overload now builds scales[v] = UsePoseRope ? ViewScale(az) : 1f and calls the static one with s = guidance * viewScales[v]. That is the same arithmetic as before for both 2.0 and 2.1. internal is reachable from the tests through the existing InternalsVisibleTo.
  • Guidance branch layout: branch.chunk(3) gives uncond, ref and full, and view_scale.repeat(n_pbr,1).view(-1) puts materials first. The test slices uncond at m*per and full at 2*materials*per + m*per, which is correct. u + gs(ref-u) + gs(full-ref) = u + gs(full-u) holds, so dropping the ref branch on the C# side is valid.
  • Euler ancestral initial sigma: for trailing, diffusers returns sigmas.max(), not sqrt(max²+1). InitialNoiseScale => _sigmas.Max() matches that, so the 4096 check is meaningful.
  • Euler step: sigma_up, sigma_down, the v-prediction c_out/c_skip, and adding noise * sigma_up after the Euler update all match diffusers.
  • Trailing timesteps: MidpointRounding.ToEven matches numpy's round().
  • Noise injection: the oracle patches the module-level randn_tensor and restores it in finally, so it records exactly the noise diffusers consumed. The generator draw order is deterministic.
  • Synthetic model: both sides do the multiplication in the same float32 order. The 0.1*sin term differs by about 1 ULP (double vs float product), which is far below the tolerances.

Suggestions (non-blocking)

  1. Euler timesteps and sigmas are dumped but never asserted. The oracle writes euler.{N}.timesteps and euler.{N}.sigmas, but Schedule_MatchesDiffusers only checks the unipc.* keys. The trajectory covers them indirectly. Still, a direct Assert.Equal on the Euler timesteps, plus the sigma loop, would show a schedule regression directly instead of a later trajectory drift. Otherwise, drop the keys from the oracle. Fix this →
  2. No length check in the sigma loop (Schedule_MatchesDiffusers). The loop runs over mine.Length and indexes sigmas[i]. If the fixture's length differs, the test fails with IndexOutOfRange or quietly skips the tail. An Assert.Equal(sigmas.Length, mine.Length) before the loop would fix that.
  3. Class doc mentions DDIMScheduler. The <summary> says the comparison is against DDIMScheduler, but the oracle never uses it; its own comment says Paint doesn't use DDIM, and alphas_cumprod comes from UniPCMultistepScheduler. Remove it from the summary.
  4. Guidance_MatchesUpstream only checks the first 8 scales. The fixture's view_scale has materials × views entries and the loop checks the first views. That is fine because they repeat, but a comment saying so would help the next reader.
  5. Duplicated Read and Compare helpers. The same unsafe Read(fixture, key) plus relRMS Compare pattern now appears in 8 ThreeD test files, including this one. This PR follows an existing pattern, but per AGENTS.md "Reuse before writing", a shared ReferenceFixture helper in the test project would be worth a follow-up.
  6. PARITY_VERIFICATION row title. The row is still titled "Hunyuan3D Paint 2.1 (UNet, VAE)", but it now also records sampler and guidance evidence, including Paint 2.0's Euler ancestral. Something like "(UNet, VAE, sampler, guidance)" would make it easier to find. You already noted the conflict with test(3d): Paint 2.1 UNet reference lane on CUDA, with the GPU numbers #788 in the PR body.

Accuracy of the claims

The PR body's "Not verified" list matches what the code exercises: a synthetic model, so no network, bake, DINO or GPU coverage. The parity doc entry stays at 🔧, which fits that.


TL;DR: The math checks out against diffusers' trailing-schedule semantics, and the Guide refactor changes no behaviour. The suggestions are small test-tightening and doc nits.

@kalebbroo
kalebbroo merged commit 2ac4792 into main Nov 12, 2026
4 checks passed
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