Skip to content

[Diffusion] Fuse Joy Image Edit QKV concatenation and avoid QK copies - #40494

Merged
BBuf merged 6 commits into
sgl-project:mainfrom
BBuf:diffusion/joy-image-qkv-cat
Sep 23, 2026
Merged

BBuf merged 6 commits into
sgl-project:mainfrom
BBuf:diffusion/joy-image-qkv-cat

Conversation

@BBuf

@BBuf BBuf commented Sep 20, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Reduce data movement around Joy Image Edit's joint attention: reuse the existing out-of-place QK-Norm/RoPE operation to avoid copying packed image Q/K, then concatenate image/text Q/K/V with one pure-copy kernel.

On 2× H200, native eager 1024×1024 / 40 steps / BF16 / lossless, two fixed ABBA groups of persistent native generators using native client-side PNG saving (return_file_paths_only=False) reduce steady-state worker E2E by 3.16–3.27% and client E2E including pixel transport and PNG saving by 3.13–3.25%. All valid lossless images are byte-identical. The default worker-save path has allocator-release long tails and its repeated saved-client results did not qualify; no reliable default-path client speedup is claimed. All earlier measurements are retained below.

Modifications

  • Reuse the existing CUDA out-of-place QK-Norm/RoPE operation on large Hopper BF16 image Q/K with 32 heads ×128 and the native packed layout. This preserves the original arithmetic and contiguous outputs while removing two input copies per block. Packed Q/K/V remain intact, so even a partial-output exception can safely fall back.
  • Add a Triton row-copy kernel for image-first Q/K/V concatenation. It accepts different input row/batch strides, including packed V, and writes three disjoint contiguous outputs in one allocation. It performs no floating-point arithmetic.
  • Keep both paths behind first-signature bitwise verification. Small shapes, unsupported layouts/devices, compilation, gradients, unverified graph capture, exceptions and mismatches retain native execution. The QK path respects SGLANG_ENABLE_FUSED_QKNORM_ROPE=0; the joint-copy path is eligible for CUDA FP16/BF16 tensors of at least 32 MiB.
  • Keep two focused joint-copy tests for bit patterns, strided inputs and changed-input graph replay, and document the dispatch contract.

The existing BBuf #34616/#34617 lossless guards and packed-input work informed this approach; they are already in the baseline. The existing out-of-place operation comes from #37903 (KevinMi) and is reused without changing its CUDA math. The actual Joy request's text branch has Q/K normalization but no RoPE. Source/prior-art review.

End-to-end benchmark

Model jdopensource/JoyAI-Image-Edit-Diffusers@4b41fb25d961f37668750178ccbb380da326201c. 2× H200, CFG parallel 2, TP1/SP1, resident transformer/text encoder/VAE, manual performance mode, native SGLang backend, BF16, quality=lossless, no torch.compile. 1024×1024, one image, 40 steps, CFG4, seed42, prompt Make the cat wear a red hat; input image.

Baseline 80da4432d085ed4d6166ef643d9fd2b829dbb0c5; combined final candidate 4811f4d52aa2586412f699b3bb84ed184d76250a. Torch 2.13.0+cu130, Triton 3.7.1, CUDA 13.0, driver 595.71.05. Environment.

Same assigned GPUs and fixed A–B–B–A order. Every process uses 40-step same-shape native request warmup on both arms. The headline client-save protocol uses the existing native return_file_paths_only=False API in both revisions and adds one complete generated-and-saved warmup, then five measured saved requests per process. Two groups contain 40 measured requests and 8 saved warmups, all retained. Native generation, returned pixel transport, PNG saving and reporting remain inside the client measurement. Earlier worker-save persistent groups use the same repeat count; earlier oneshot groups each measure one saved request per fresh process. Seconds; percentages are latency reductions from arithmetic means:

Version / group Worker E2E Denoise Client E2E incl. PNG save
copy-only v2 / 1 14.2898 → 14.0491 (+1.68%) 13.9068 → 13.6601 (+1.77%) 14.9100 → 14.2250 (+4.59%)
copy-only v2 / 2 14.2731 → 14.0404 (+1.63%) 13.9001 → 13.6513 (+1.79%) 14.4500 → 14.2200 (+1.59%)
copy-only final / 1 14.2619 → 14.0580 (+1.43%) 13.8927 → 13.6549 (+1.71%) 14.4450 → 14.2500 (+1.35%)
copy-only final / 2 14.2683 → 14.0339 (+1.64%) 13.8968 → 13.6559 (+1.73%) 14.4450 → 14.2100 (+1.63%)
combined oneshot / 1 14.2650 → 13.8207 (+3.11%) 13.8929 → 13.4392 (+3.27%) 14.4500 → 14.0000 (+3.11%)
combined oneshot / 2 14.2675 → 13.8224 (+3.12%) 13.8969 → 13.4446 (+3.26%) 14.4450 → 15.0400 (-4.12%)
combined persistent / worker-save / 1 14.2230 → 13.7760 (+3.14%) 13.8775 → 13.4270 (+3.25%) 14.4440 → 14.3450 (+0.69%)
combined persistent / worker-save / 2 14.2201 → 13.7764 (+3.12%) 13.8842 → 13.4232 (+3.32%) 14.3850 → 14.7050 (-2.22%)
combined persistent / client-save / 1 14.2094 → 13.7599 (+3.16%) 13.8752 → 13.4270 (+3.23%) 14.3980 → 13.9470 (+3.13%)
combined persistent / client-save / 2 14.2323 → 13.7666 (+3.27%) 13.8892 → 13.4309 (+3.30%) 14.4230 → 13.9540 (+3.25%)

copy-only v2 is 0cff694f701987ba694954a4cf54257f7c3881e6; copy-only final is 3b00154ddb33ac436906a493c9706ed3855ec918. The latter adds backend-import guards and documentation, with the same copy kernel. Copy-only final group 1 reaches only about 1.43% worker / 1.35% client and does not qualify. This prompted the profile-driven removal of the remaining image Q/K copies. Copy-only v2 group 1 contains a 15.36 s baseline client outlier; its larger client percentage is not the headline. The earlier default one-step-warmup single-request screen improved worker time by about 1.1% and also remains non-qualifying.

Worker E2E is the native stage/perf-dump time before PNG saving. Client E2E is the native request-and-save timer, logged to 0.01s. Both exclude loading/warmup. No profiler request supplies an E2E row. Recorded lossless peak reserved memory: 45.83594, 45.87305 GiB per worker.

Combined oneshot group 2 has a 16.08 s candidate client outlier despite 13.824 s worker time, so that client's group does not qualify. Adding a complete saved-request warmup did not remove the issue: the worker-save persistent groups also fail saved-client qualification, including a 16.99 s candidate observation. Separate instrumented diagnostic worktrees localize a 1.76 s output-rank empty_cache delay in baseline, with PNG saving around 0.16 s and sub-millisecond peak collective/report writing. Candidate's twelve diagnostic requests did not reproduce its earlier long tail; the same cause for those particular earlier observations remains an inference. Startup/shutdown GC is recorded separately. The diagnostic runs are excluded from benchmark qualification.

The final protocol explicitly selects native client-side PNG saving in both arms, retaining pixel data through the worker response. The existing worker condition then does not call request-tail empty_cache; the native client saves the same PNG inside its timer. No allocator/GC function is patched, no output work is removed from the timed request, and no generation math changes. This is a supported different output configuration, with the default path's failed comparisons disclosed. Predeclared client-save protocol · Earlier persistent protocol · Default-path audit · Tail diagnosis.

The persistent driver also measures an unrounded outer wall timer around native generate plus the CLI performance dump, including input validation and the saved response. Output hashing and campaign JSON writing occur after this timer:

Group Baseline outer saved-client s Candidate outer saved-client s Reduction
1 14.398472 13.949222 3.12%
2 14.423400 13.954875 3.25%
All 24 oneshot, 40 worker-save persistent and 40 client-save persistent observations
Version / group Arm Worker E2E Denoise Client incl. save
copy-only v2 / 1 baseline 14.2871 13.9021 14.46
copy-only v2 / 1 candidate 14.0399 13.6593 14.22
copy-only v2 / 1 candidate 14.0583 13.6610 14.23
copy-only v2 / 1 baseline 14.2925 13.9115 15.36
copy-only v2 / 2 baseline 14.2750 13.9028 14.45
copy-only v2 / 2 candidate 14.0549 13.6560 14.23
copy-only v2 / 2 candidate 14.0259 13.6467 14.21
copy-only v2 / 2 baseline 14.2711 13.8973 14.45
copy-only final / 1 baseline 14.2540 13.8826 14.44
copy-only final / 1 candidate 14.0953 13.6603 14.30
copy-only final / 1 candidate 14.0208 13.6494 14.20
copy-only final / 1 baseline 14.2698 13.9027 14.45
copy-only final / 2 baseline 14.2691 13.8904 14.45
copy-only final / 2 candidate 14.0332 13.6561 14.21
copy-only final / 2 candidate 14.0346 13.6557 14.21
copy-only final / 2 baseline 14.2675 13.9032 14.44
combined oneshot / 1 baseline 14.2411 13.8731 14.42
combined oneshot / 1 candidate 13.8319 13.4515 14.01
combined oneshot / 1 candidate 13.8096 13.4268 13.99
combined oneshot / 1 baseline 14.2889 13.9127 14.48
combined oneshot / 2 baseline 14.2798 13.9056 14.46
combined oneshot / 2 candidate 13.8205 13.4465 14.00
combined oneshot / 2 candidate 13.8242 13.4426 16.08
combined oneshot / 2 baseline 14.2551 13.8883 14.43
combined persistent / worker-save / 1 baseline 14.2498 13.9211 14.41
combined persistent / worker-save / 1 baseline 14.1939 13.8611 14.36
combined persistent / worker-save / 1 baseline 14.1871 13.8571 14.35
combined persistent / worker-save / 1 baseline 14.2303 13.9008 14.43
combined persistent / worker-save / 1 baseline 14.2615 13.8757 14.43
combined persistent / worker-save / 1 candidate 13.7635 13.4184 13.93
combined persistent / worker-save / 1 candidate 13.7418 13.4139 13.91
combined persistent / worker-save / 1 candidate 13.7830 13.4450 14.16
combined persistent / worker-save / 1 candidate 13.8757 13.4601 14.04
combined persistent / worker-save / 1 candidate 13.7707 13.4273 16.99
combined persistent / worker-save / 1 candidate 13.7276 13.3946 13.89
combined persistent / worker-save / 1 candidate 13.7441 13.4150 13.91
combined persistent / worker-save / 1 candidate 13.7740 13.4436 13.94
combined persistent / worker-save / 1 candidate 13.8247 13.4314 14.76
combined persistent / worker-save / 1 candidate 13.7550 13.4207 13.92
combined persistent / worker-save / 1 baseline 14.1735 13.8422 14.34
combined persistent / worker-save / 1 baseline 14.2804 13.8516 14.97
combined persistent / worker-save / 1 baseline 14.2319 13.9032 14.40
combined persistent / worker-save / 1 baseline 14.2121 13.8861 14.38
combined persistent / worker-save / 1 baseline 14.2093 13.8764 14.37
combined persistent / worker-save / 2 baseline 14.1824 13.8462 14.35
combined persistent / worker-save / 2 baseline 14.2394 13.9100 14.40
combined persistent / worker-save / 2 baseline 14.2263 13.8961 14.39
combined persistent / worker-save / 2 baseline 14.2410 13.8797 14.41
combined persistent / worker-save / 2 baseline 14.2037 13.8707 14.37
combined persistent / worker-save / 2 candidate 13.7669 13.4381 13.93
combined persistent / worker-save / 2 candidate 13.7380 13.4110 13.90
combined persistent / worker-save / 2 candidate 13.7954 13.4316 15.59
combined persistent / worker-save / 2 candidate 13.7880 13.4243 15.78
combined persistent / worker-save / 2 candidate 13.8009 13.4123 16.81
combined persistent / worker-save / 2 candidate 13.7678 13.4361 13.93
combined persistent / worker-save / 2 candidate 13.7351 13.4085 13.90
combined persistent / worker-save / 2 candidate 13.7653 13.4376 13.93
combined persistent / worker-save / 2 candidate 13.8705 13.4228 15.36
combined persistent / worker-save / 2 candidate 13.7363 13.4099 13.92
combined persistent / worker-save / 2 baseline 14.1924 13.8544 14.36
combined persistent / worker-save / 2 baseline 14.2522 13.9172 14.42
combined persistent / worker-save / 2 baseline 14.2378 13.9056 14.40
combined persistent / worker-save / 2 baseline 14.2153 13.8850 14.38
combined persistent / worker-save / 2 baseline 14.2107 13.8766 14.37
combined persistent / client-save / 1 baseline 14.2579 13.9222 14.45
combined persistent / client-save / 1 baseline 14.1857 13.8525 14.37
combined persistent / client-save / 1 baseline 14.1721 13.8403 14.36
combined persistent / client-save / 1 baseline 14.2145 13.8843 14.40
combined persistent / client-save / 1 baseline 14.2237 13.8928 14.41
combined persistent / client-save / 1 candidate 13.7854 13.4482 13.98
combined persistent / client-save / 1 candidate 13.7361 13.4041 13.92
combined persistent / client-save / 1 candidate 13.7336 13.4012 13.92
combined persistent / client-save / 1 candidate 13.7599 13.4319 13.94
combined persistent / client-save / 1 candidate 13.7587 13.4313 13.94
combined persistent / client-save / 1 candidate 13.7737 13.4371 13.97
combined persistent / client-save / 1 candidate 13.7803 13.4460 13.97
combined persistent / client-save / 1 candidate 13.7566 13.4229 13.94
combined persistent / client-save / 1 candidate 13.7522 13.4178 13.94
combined persistent / client-save / 1 candidate 13.7624 13.4290 13.95
combined persistent / client-save / 1 baseline 14.1951 13.8438 14.39
combined persistent / client-save / 1 baseline 14.1906 13.8530 14.38
combined persistent / client-save / 1 baseline 14.2196 13.8904 14.41
combined persistent / client-save / 1 baseline 14.2208 13.8877 14.41
combined persistent / client-save / 1 baseline 14.2140 13.8848 14.40
combined persistent / client-save / 2 baseline 14.2330 13.8935 14.43
combined persistent / client-save / 2 baseline 14.2452 13.9086 14.43
combined persistent / client-save / 2 baseline 14.2399 13.9061 14.43
combined persistent / client-save / 2 baseline 14.2148 13.8875 14.41
combined persistent / client-save / 2 baseline 14.3131 13.8829 14.50
combined persistent / client-save / 2 candidate 13.7898 13.4473 13.98
combined persistent / client-save / 2 candidate 13.7290 13.3919 13.92
combined persistent / client-save / 2 candidate 13.7745 13.4375 13.96
combined persistent / client-save / 2 candidate 13.7649 13.4295 13.95
combined persistent / client-save / 2 candidate 13.7623 13.4294 13.95
combined persistent / client-save / 2 candidate 13.8019 13.4640 13.99
combined persistent / client-save / 2 candidate 13.7594 13.4217 13.95
combined persistent / client-save / 2 candidate 13.7422 13.4088 13.93
combined persistent / client-save / 2 candidate 13.7673 13.4344 13.95
combined persistent / client-save / 2 candidate 13.7746 13.4441 13.96
combined persistent / client-save / 2 baseline 14.2378 13.9027 14.44
combined persistent / client-save / 2 baseline 14.1870 13.8542 14.37
combined persistent / client-save / 2 baseline 14.2292 13.8988 14.41
combined persistent / client-save / 2 baseline 14.2159 13.8830 14.42
combined persistent / client-save / 2 baseline 14.2068 13.8748 14.39
All 8 explicitly excluded client-save warmups (earlier warmups also retained in raw data)
Group Arm Worker s Native saved-client s Outer saved-client s
1 baseline 14.2805 14.78 14.787861
1 candidate 13.8157 14.24 14.245026
1 candidate 13.8108 14.22 14.228635
1 baseline 14.3242 14.76 14.767657
2 baseline 14.2906 18.88 18.882111
2 candidate 13.8233 14.23 14.231589
2 candidate 13.8239 14.28 14.282241
2 baseline 14.2781 14.79 14.792314

Raw native logs, presets, source SHAs and perf dumps · Complete audit.

Profile and kernel benchmark

Three-step native profiler requests select the third complete Joy model forward using CUDA launch correlation. These traces use the original worker-save configuration; the generation parameters and measured source revisions match the client-save benchmark, and the forward slice precedes either output transport/save path. Copy scopes are identified by their actual image/text input shapes. The optimized-chain subtotal includes image Q/K copies, image QK-Norm/RoPE and Q/K/V concatenations; unchanged position arange is excluded.

Arm Whole-forward kernels Image Q/K copy kernels Joint concat kernels Optimized-chain kernels Chain GPU ms
baseline 1268 80 120 240 24.047472
candidate 1108 0 40 80 8.776787

The 80 text Q/K copies and two unrelated FP32 concatenations remain. Attention, GEMMs, MLP and modulation math are unchanged. FA3 is attention; the generic analyzer's GEMM/MoE classifications do not apply to every named kernel in this dense BF16 model. Full/sliced traces and analysis.

Production helper versus the original image QK copy/norm/RoPE + three-cat chain, BF16, 32 heads ×128. Median of 40 CUDA-event samples after 10 warmups, normal output allocations, first-signature verification completed before timing:

Batch / image tokens / text tokens Mode Native µs Candidate µs Speedup
1 / 8048 / 1004 eager 541.360 271.440 1.994×
1 / 8048 / 1004 standalone_graph 531.408 213.648 2.487×
1 / 4096 / 512 eager 288.880 190.192 1.519×
1 / 4096 / 512 standalone_graph 278.688 112.912 2.468×
1 / 2048 / 256 eager 154.416 157.264 0.982×
1 / 2048 / 256 standalone_graph 145.312 145.376 1.000×
2 / 257 / 13 eager 110.880 118.432 0.936×
2 / 257 / 13 standalone_graph 46.656 46.768 0.998×

Smaller shapes retain native operations; their eager rows expose the wrapper/guard overhead and are not claimed faster. Standalone graph is a kernel diagnostic, not native model BCG. Samples · Harness.

Focused NCU on the production shape:

Metric Original chain Combined candidate
Kernels 7 3
GPU duration 678.560 µs 229.568 µs
DRAM read + write 855,088,640 B 671,013,888 B
QK-Norm/RoPE duration 108.512 µs 118.368 µs
QK-Norm/RoPE registers/thread 32 32
Joint concat duration 398.720 µs, three kernels 109.152 µs, one kernel

The out-of-place norm itself is slightly slower; avoiding the two input copies (169.120 µs combined) compensates. Joint-copy L2 throughput is 88.94%, versus 12.18% for the original generic V cat. No measured local-memory spills. Source stalls map to the new Triton loads/stores; the reused CUDA JIT norm lacks usable source-line mapping in this report. PM raw instances are preserved, with no unsupported tail-balance claim. NCU reports, six-part analysis and limitations · Earlier copy-only NCU.

Output comparison

All 127 valid lossless PNGs are byte- and pixel-identical, including profiles and all measured revisions: SSIM 1, PSNR ∞. SHA256 76cfd40eb83feacb1b0292e171b9beb2a8cd7eb7fadc606cb1632e886d960ce6.

All 5 valid high-mode PNGs are also byte-identical to lossless and to each other for this request; they are correctness checks, excluded from the lossless timing table.

Native baseline/candidate lossless and high images

Original baseline PNG · Original candidate PNG · Baseline high · Candidate high · Absolute pixel difference.

All 9 native BCG probes report [Diffusion BCG] disabled and no capture. Their saved eager fallback images are independently hashed and excluded from performance claims. This PR improves eager execution and does not add Joy model BCG support.

Validation and reproduction

Validation scope: The test counts, logs and GPU measurements below are historical results from the recorded benchmark revisions, before test cleanup. The current diff retains two joint-copy correctness/replay tests. This cleanup does not change runtime code or claim a new GPU measurement.

  • H200: 8 tests + 8 subtests passed, including production/batch-two shapes, bit patterns, unchanged packed inputs, non-overlapping outputs, changed-input/weight/cache graph replay, native fallback, explicit disable, forced mismatch and partial-output exception. After cleanup, the retained joint-copy tests use the single-GPU CUDA CI lane; the duplicate B200 registration and separate QK test file were removed.
  • Changed-file pre-commit, Mintlify build and broken-link checks passed. Validation logs, pre-commit, build, links.
  • The initial native command used an unsupported CFG-parallel CLI flag; it was corrected before admission. The first profile auditor mistakenly included two unrelated FP32 concatenations in the QKV count; the retry distinguishes dtype and preserves those kernels. Failed logs and the superseded audit waiter's termination are retained. No failed/disabled run is counted as performance.
  • The first persistent harness omitted native CLI sys.argv bookkeeping and failed before model initialization. Retry1 supplies the same explicit CLI arguments and preserves the original failed log.
  • Campaign-owned model weights were deleted after completed output/visual audit: zero remaining files, weights or bytes. Cleanup.

The following native CLI reproduces the workload and the earlier worker-save oneshot measurement. For the headline client-save protocol, use run-joy-client-save.py and validate-joy-client-save.py; they construct the same native CLI arguments and retain a local DiffGenerator, calling generate(sampling_params_kwargs={..., "return_file_paths_only": False, "save_output": True}) for the saved warmup and five measured requests. Both revisions use the same pinned checkpoint and input. To select this output mode in a one-request CLI, pass a JSON config containing {"return_file_paths_only": false} with --config:

CUDA_VISIBLE_DEVICES=0,1 FLASHINFER_DISABLE_VERSION_CHECK=1 \
SGLANG_DIFFUSION_SYNC_STAGE_PROFILING=1 \
sglang generate --backend sglang \
  --model-path jdopensource/JoyAI-Image-Edit-Diffusers --revision 4b41fb25d961f37668750178ccbb380da326201c \
  --prompt 'Make the cat wear a red hat' --image-path ./input.png \
  --width 1024 --height 1024 --num-frames 1 --num-inference-steps 40 \
  --guidance-scale 4 --seed 42 --num-gpus 2 --enable-cfg-parallel --cfg-parallel-size 2 \
  --tp-size 1 --ulysses-degree 1 --performance-mode manual --quality lossless \
  --enable-torch-compile false --dit-cpu-offload false --dit-layerwise-offload false \
  --text-encoder-cpu-offload false --vae-cpu-offload false \
  --warmup-mode request --warmup-resolutions 1024x1024 --warmup-num-frames 1 \
  --warmup-steps 40 --save-output --output-path ./joy-output --perf-dump-path ./joy-perf.json

Exact driver scripts · SHA256 manifest.


CI States

Latest PR Test (Base): ⏳ Run #35871593583
Latest PR Test (Extra): ❌ Run #35871592959
Latest PR Test (AMD ROCm 10): ⏳ Run #35871593623

@BBuf BBuf added documentation Improvements or additions to documentation run-ci CI: run the baseline test suite on this PR labels Sep 20, 2026
@BBuf
BBuf requested a review from HaiShaw as a code owner September 20, 2026 22:07
@BBuf BBuf added diffusion SGLang Diffusion jit-kernel labels Sep 20, 2026

@mickqian mickqian left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exact-head CI follow-up for 4811f4d:

The root failure is qwen_image_t2i_cache_dit_enabled, Denoise Step 49, in single-GPU partition 2. Initial execution plus all six testcase retries exceeded the unchanged 65.1100 ms limit (65.7389–66.9358 ms; final 66.4692 ms). This is persistent within this job, not a startup failure. The modified Joy Image paths do not establish a cause for this Qwen failure, but the log alone also does not prove a runner defect.

The downstream Base-B failures inspected are health-gate cascades from this job, not additional model-test failures. I have not changed thresholds or requested another blind retry. Please investigate the exact Qwen configuration on a comparable runner before changing the assertion. I cannot push to the head repository.

@BBuf
BBuf requested a review from kevin-mii as a code owner September 23, 2026 14:05
@BBuf
BBuf merged commit 4e60d70 into sgl-project:main Sep 23, 2026
88 of 128 checks passed
@BBuf
BBuf deleted the diffusion/joy-image-qkv-cat branch September 23, 2026 14:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

diffusion SGLang Diffusion documentation Improvements or additions to documentation jit-kernel run-ci CI: run the baseline test suite on this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants