3 Commits
Author SHA1 Message Date
Æmotion StudioandClaude Fable 5.1 7eb7ae3594 refactor: share the four-corner 3D simplex, a lattice hash and an FBM skeleton
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-22 01:19:47 -07:00
Æmotion StudioandClaude Opus 5 2c7047a0e3 perf: draw the channel axis in one call, not one per channel
At MiniMax H3's default latent the draw performed 37 frames x 24 channels =
888 separate renders of 4032 pixels each, and cProfile put the time in per-op
dispatch rather than arithmetic -- a 4032-element torch.floor was costing
0.3ms. Generators now offer fill_channels a batched draw and it takes it
whenever more than one extra channel is wanted.

At 1344x768/124 frames: domain_warp 5.31s -> 1.97s, temporal_coherent
6.52s -> 1.86s, curl_noise -> 2.11s.

curl_noise and temporal_coherent are byte-identical; their fixtures did not
move. domain_warp is not, and the reason is worth recording. It turns its
coordinates by an angle drawn from the seed, so unlike the others the
coordinates genuinely differ per channel and the batched draw has to
materialise them. Every downstream elementwise op then runs over N times as
many elements, and where the per-slice count does not suit the vector width
the tail is handled differently. Five fixtures moved by 1.2e-07 to 2.4e-07 --
float32 rounding, with effective channel rank identical to three decimals --
and the previous behaviour is tagged pre-batched-noise. Exact at 22x38, 48x84
and 64x64; not at 22x39, 23x38, 37x53. tests/test_simplex.py pins both sides
of that boundary so it cannot quietly get worse.

jump and stamp are unaffected by construction: fill_channels takes the
batched path only for more than one channel, and both travel modes build from
one-channel draws. 32 draws across four generators and four latent shapes
were checked byte-identical against the tag.

Also folded in, because the batching needed it:

- One simplex primitive instead of seven. The 2D hash was duplicated across
  three generators and the 3D across four, with real differences hidden
  between them -- three of the 3D copies sum a single corner rather than
  four, and one of those computed three more corner hashes and threw them
  away. shaders/simplex.py names them for what they do and keeps the
  differences rather than unifying them. temporal_coherent's four-corner
  version stays with that generator: it picks corners by the real simplex
  ordering and reads gradients from a table, so it is a different function
  and not a parameterisation of the others. I had written it as one, guessed
  wrong, and the equivalence check caught it.
- The accumulators in get_velocity_field, fbm_noise and temporal_spectral_noise
  no longer preallocate a fixed [B,H,W,C] and write into it in place, so the
  same code serves a shared coordinate grid and a per-channel one. The adds
  happen in the same order, so the values are unchanged.
- tensor_field draws its shape mask once instead of once per channel -- 127
  identical masks at LTXV's 128 channels -- and stops cloning the coordinate
  grid per channel. Byte-identical.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 18:54:02 -07:00
Æmotion StudioandClaude Opus 5 ee7c7bc31d perf: stop rendering the draw a collapse throws away
None of this changes what a seed produces. All eleven golden fixtures are
byte-identical and the suite passes untouched; that was the acceptance
criterion for every change here.

`travel_mode: jump` -- and the `jump` and `stamp` presets -- rebuilds every
channel from one-channel draws and never reads the wide draw, but the wide
draw was rendered first anyway and dropped on the next line. The guard that
discards it turns only on values known before rendering, so it moves up into
generate(). Measured on domain_warp at MiniMax H3's latent: 5.99s to 0.22s
at 1344x768/124 frames, 1.44s to 0.05s at 608x352/56 frames. `drift` is
unaffected -- it needs the wide draw to measure its rank, so it still pays.

The draw now runs under torch.inference_mode(), worth about a tenth of it,
and clones on the way out. That clone is not optional: an inference tensor
raises "Inference tensors cannot be saved for backward" the moment it
reaches a grad-recording region, and this noise is handed to whatever the
workflow does next.

domain_warp, curl_noise and temporal_coherent stopped reseeding the global
RNG. All three are coordinate hashes of their seed argument, so the calls
changed nothing -- verified by neutering torch.manual_seed and re-drawing --
and domain_warp made one per channel render, 888 times per draw at H3's
default latent, each reseeding every CUDA device as well as the CPU.
tensor_field keeps its reseed: it draws torch.randn_like inside its channel
loop and that value reaches its output.

Also here, because the batching work needs it:

- shaders/simplex.py holds one copy of the 2D primitive, which was duplicated
  across three generators, and takes a tensor of seeds as well as an int so a
  whole channel axis can be drawn in one call. domain_warp delegates to it,
  bit-identical and at performance parity. The variants are kept rather than
  unified -- domain_warp rotates its coordinates by the seed and the others do
  not -- because collapsing them would silently change three generators.
- tests/test_simplex.py pins how far batching stays exact. Without the
  rotation it is exact everywhere. With it, the coordinates differ per slice
  and have to be materialised, every downstream op then runs over N times as
  many elements, and where the per-slice count does not suit the vector width
  the tail differs by 3e-08 to 1.2e-07. So batching domain_warp will move the
  fixtures, and batching the other three will not.
- verification/benchmark_draw.py, because none of the above was reproducible:
  the only per-draw timing in the repo predated the per-channel fill and
  understated the cost roughly tenfold. It refuses to let you raise the thread
  count quietly -- 8 threads on this 4-core box is 2.5x slower than 4.
- SNK_TEST_CPU=1 pins the suite to the CPU, so it runs while a ComfyUI server
  on the same box is holding the GPU.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 18:08:46 -07:00