Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces optimized fused producers (fused_ln_adaln and fused_rmsnorm_rope) and implements exact Fixed-m support with Global Virtual K-Centering for Ulysses and Ring attention. It also adds support for Grouped Query Attention (GQA) across these attention kernels and includes comprehensive unit tests. The review feedback suggests moving an inline import of fused_rmsnorm_rope in attention_flax.py to the top of the file to avoid performance overhead in a hot path, and simplifying a double-negation conditional expression to improve code readability.
5745a10 to
5c674d1
Compare
5c674d1 to
52593cc
Compare
52593cc to
643ca72
Compare
643ca72 to
c58cdad
Compare
4ace67b to
0c191db
Compare
ea0148a to
837cebe
Compare
b1e2b13 to
e6db513
Compare
b749c64 to
3268a70
Compare
3268a70 to
66ada3d
Compare
66ada3d to
080bcca
Compare
b5ebff4 to
86ef4bd
Compare
86ef4bd to
916de47
Compare
syhuang22
left a comment
There was a problem hiding this comment.
I ran the stack against main on v7x-8 (WAN 2.2 720p/81f/40 steps, same script, only the source tree differs):
- Ring R=2 with #452's recipe (U=2, 6400/2048): 105.3s -> 118.6s (+12.6%), and per_q_block is 127.3s. The funny part is this PR hits the fixed-m accumulate branch more often than main (82% vs 70%), so it's overhead, not fallback. Taking out the V reduce only gets back 0.2%.
- Online
ulysses_custom: the md5 no longer matches main. With just the fused RMSNorm+RoPE turned off it's bit-identical again, and it isn't faster either (119.0s both ways). - Ulysses fixed-m is ~1.3% faster than main, and the relayout parts are exact. Nice!
Before this goes in I'd like to see ring R=2 back to at least main's speed on v7. Also, non-ring kernels now raise when ulysses_shards != CP, so please call that out. The dot-product / GQA fixes would be easier to review as their own PR.
| qn_dev = norm_sq.reshape(batch_size, num_q_heads, num_q_blocks, bq).max(axis=-1) | ||
| else: | ||
| qn_dev = norm_sq.max(axis=-1) | ||
| kf_centered = raw_key.astype(jnp.float32) - k_mean[:, :, None, : raw_key.shape[-1]] |
There was a problem hiding this comment.
This is where the ring regression comes from, I think. #452 computed these norms before the all-to-all behind an optimization_barrier (after the a2a measured +8%, the barrier was worth 1.46 ms/layer). Now it's post-a2a plus a centered-K subtract, pmean and all_gather. Could we move the reduction back before the a2a? The global k mean is still computable there.
There was a problem hiding this comment.
Moved the entire reduction (qn, vn, kn, v_ok, and all_fixed_global) back before the all_to_all in _ring_fixed_m_norms_pre_a2a (attention_flax.py:L1538-L1665) behind an outer optimization_barrier((query, key, value)).
Also merged the collectives into a single jax.lax.pmax((qn_head_local, vn_local, kn_local), axis_name=(ulysses_axis, ring_axis)) and eliminated the ring all_gather (which was breaking XLA fusion across the QKV projection and costing ~2.5s/video). With use_k_centering=False by default on Ring R>1, xprof shows collective-permute-done dropped from 2405 ms -> 47 ms.
| # 1. FP32 RMSNorm for stability, then cast directly to target activation dtype | ||
| q_fp32 = raw_q.astype(jnp.float32) | ||
| q_rms = jax.lax.rsqrt(jnp.mean(jnp.square(q_fp32), axis=-1, keepdims=True) + eps) | ||
| q_norm = (q_fp32 * q_rms * q_norm_scale.astype(jnp.float32)).astype(raw_q.dtype) |
There was a problem hiding this comment.
This is (x * rsqrt) * scale, but nnx.RMSNorm does x * (rsqrt * scale), so the rounding differs and the output drifts from main (md5 matches again with the fused path off). It's also not faster on v7. I'd drop it, or match Flax's order.
There was a problem hiding this comment.
Fixed in fused_producers.py:L65, L69! We updated the association order to (q_fp32 * (q_rms * q_norm_scale.astype(jnp.float32))) to match nnx.RMSNorm bit-for-bit. Added src/maxdiffusion/tests/fused_producers_test.py to assert bit-identical parity with Flax nnx.RMSNorm, and verified that md5 matches main again.
| @@ -0,0 +1,4 @@ | |||
| { | |||
| "summary": "Corrected CPU-runnable regression test for the dot-product fallback layout bug. Fixes two errors in the first draft: the function returns [B, S, H*D] so outputs are now unflattened via reshape+swapaxes before comparison, and the reviewer's counterexample is now encoded correctly as seq=3 tokens with dim_head=1 (values [1,2,3] and [10,20,30] across tokens), which yields the expected [2,20] and the buggy [8,14]. Includes a reference-comparison test on random 4-D inputs, a 3-D/4-D equivalence test, and a GQA repeat test.", | |||
There was a problem hiding this comment.
Looks like a tool artifact, please remove.
There was a problem hiding this comment.
Removed in 2ae5165d.
| key_states = jnp.reshape(key, (b, -1, heads, dim_head)) | ||
| value_states = jnp.reshape(value, (b, -1, heads, dim_head)) | ||
|
|
||
| def _to_bshd(x: Array, n_heads: int) -> Array: |
There was a problem hiding this comment.
Real bug, I reproed it on main. Could this go in its own PR though? It's unrelated to fixed-m.
There was a problem hiding this comment.
We included the fix here because our GQA / dot-product fallback layout tests in this stack (dot_fallback_layout_test.py) exercise that path directly and fail without it. If you'd like, we can cherry-pick attention_flax.py:L2069 + dot_fallback_layout_test.py into a standalone hotfix PR targeting main right away!
| # so this is bit-identical. Done after the a2a it sat between the collective | ||
| # and the kernel and XLA wrapped it in relayout copies; done before, it fuses | ||
| # into the producer of Q and its 185MB round-trip disappears. | ||
| if use_custom_kernel and use_base2_exp: |
There was a problem hiding this comment.
Nice, this one is exact (md5 matches main) and a free win.
There was a problem hiding this comment.
Thanks! Hoisting the base-2 Q rescale above the all_to_all saves 185 MB/layer of post-collective relayout traffic while keeping every bit identical.
916de47 to
3c635b1
Compare
3c635b1 to
1e2471e
Compare
1e2471e to
2ae5165
Compare
|
Thank you for running the v7x-8 benchmarks and identifying the post-a2a norm reduction and RMSNorm association order @syhuang22! I have updated the code |
5543133 to
5539440
Compare
5f233f7 to
f9256fa
Compare
f9256fa to
8589576
Compare
…ering Implements 2D Ulysses + Ring distributed attention with exact fixed-m accumulation: - Global Virtual K-Centering: Computes key mean on real tokens and reduces across ring axis with jax.lax.pmean - Cross-Ring Safety Fallback: Collective jax.lax.pmin reduction on v_ok across all ring ranks - Hoists Q * log2(e) before Ulysses All-to-All to fuse into Q producer - Slices ragged KV tail when R=1, eliminating dead sequence padding - Fused RMSNorm + RoPE producer with exact bit-identical nnx.RMSNorm associativity - Strict topology and configuration validation guards
8589576 to
e8ed02e
Compare
Summary
Stacked on top of #477 (
feat/fixed-m-kernel).Implements 2D Ulysses + Ring distributed attention with exact fixed-m accumulation and Global Virtual K-Centering:
across the ring axis with
jax.lax.pmeanwhenglobal mean. feat(attention): fixed-m splash attention kernel with dynamic bounds and safety fallbacks #477 leaves centering off by default — this PR supplies both halves of that matched pair:
the global mean, and a Cauchy-Schwarz bound measured on the centered keys. Centering shrinks the bound,
letting 76.56% of layers stay on the fast fixed-m path (
_accumulate_scan) at_ring_fixed_m_norms_pre_a2a):jax.lax.optimization_barrierand foldsthe centered K max-norm (
kn_local) directly into the single(ulysses, ring)pmax:jax.lax.pmax((qn_head_local, vn_local, kn_local), axis_name=reduce_axes).all_gather(kn_shard, ring_axis). On TPU (tpu7x-8), removingthis ring-axis
all_gathereliminates an XLA relayout barrier inside the QKV projection fusion region,recovering ~3.5s of compute fusion time (
convolution fusion−1.64s,loop fusion−0.44s,data formatting−0.36s).lax.all_gather(...).max(axis=0)withlax.pmaxon the standalone non-pregathered kernel path (speeding upring_fixed_m_test.pyby ~30s)._lse_scan):jax.lax.dynamic_index_in_dim(mk_all_sq, (my_ring_index - hop) % axis_size)with the hop-invariant ring-wide
mk_global_sq. Removing tracedlax.axis_indexfrom the kernel's scalar-prefetchchain allows the ~190 MiB K/V
ppermuteinsidelax.condto overlap kernel execution and eliminates link contentionwith the output
all-to-all(collective-permute-done: 2405.4 ms → 47.3 ms; outputall-to-allBW: 173 → 535 GiB/s).kv_pad_size = 1):actual_kv_seq_lenis 8-sublane aligned (37,800ontpu7x-8kv_pad_size = 1)and passes
orig_kv_seq_len = actual_kv_seq_len(37,800) intomake_custom_ring_attention.jnp.padHBM copies (37,800 -> 38,912) before ring attention, reducesppermuteICI payload by 1,112 tokens per shard (−2.86%),and slices the 19th KV block (
j = 18) atslice_k_len = 936 <= 1024, skipping 384,000 inner VPU/MXU loop iterations across 40 steps(saving 0.4s denoise time and 2.9s cold compile time with 100% bit-identical output).
jax.lax.pminreduction onv_ok_local. If any ring rank detectsa logit-bound violation, all ranks transition together to online-softmax accumulation across the ring cycle.
kernels/fused_producers.py): folds QK-norm and rotary application into a singleproducer with exact
x * (rsqrt * scale)associativity matchingnnx.RMSNormbit-for-bit.attention_config_guards_test.py).Verified End-to-End Benchmarks on Cloud TPU v7 (tpu7x-8,$U=2, R=2$ ) & v6e-8 ($U=4, R=1$ )
The$U=2, R=2$ ring recipe on
tpu7x-8(81 frames, 1280×720, 40 steps,vae_decode_chunk=1) has been fully re-measured and verified across two independenttpu7x-8hosts (europe-west2-aandus-central1-c):origin/main(1bc54811)5539440c)tpu7x-8(tpu7x-8(v6e-8(v6e-8(Verification
tpu7x-8&v6e-8):ring_fixed_m_test.py: 37 passed, 1 skipped in 261.62s (exercises alltest_gqa_ulysses_ring_custom_fixed_m_ragged).custom_splash_unpadded_test.py: 12 passed.attention_config_guards_test.py: 16 passed (80 subtests).fused_producers_test.py: 3 passed (bit-identical tonnx.RMSNorm).origin/main(1bc54811).pyink --pyink-indentation=2 --line-length=125andruff checkclean.