Skip to content

[diffusion] Replay small Triton wrappers' launches per operand signature - #43859

Merged
mickqian merged 2 commits into
mainfrom
mick/diffusion-triton-fast-launch
Oct 14, 2026
Merged

mickqian merged 2 commits into
mainfrom
mick/diffusion-triton-fast-launch

Conversation

@mickqian

@mickqian mickqian commented Oct 12, 2026 •

Copy link
Copy Markdown
Collaborator

Motivation

Many of sglang's small Triton kernels cost more host time per call than GPU time. Before each launch, the Python wrapper runs operand checks, stride math, the device guard and Triton's JIT dispatch. For rmsnorm_scale at a Z-Image 512² shape that is ~28 µs on H200, against a ~9 µs launch. None of that work depends on tensor values, only on the operands' signature.

A py-spy profile of the denoising loop (H200, 2 GPUs, eager, 256² so that the host bounds the step) shows how much of each step this is. The share below is the host time spent inside sglang's diffusion fused-kernel wrappers and their predicates:

model share of host time largest wrappers
FLUX.1 TP2 ≈32% fused_layernorm_modulate_raw 14.6%, residual_gate_add 8.5%
FLUX.2 TP2 ≈38% fused_layernorm_modulate_raw 12.3%, residual_gate_add 8.2%, flux2_strided_qknorm_rope 6.2%, fused_packed_silu_mul_bitexact 6.1%
Qwen-Image-2.1 Ulysses2 ≈28% fused_layernorm_modulate_raw 10.6%, residual_gate_add 7.4%, qknorm_complex_rope_cuda 4.5%
Ideogram-4 TP2 ≈26% fused_rope_rotate_half_bitexact 8.1%, residual_gate_add 7.5%, modulate_scale_shift 4.9%

Whether this reaches end-to-end latency depends on whether the step is host-bound. That is the case on Grace hosts (GB200/GB300), with several TP ranks, and at low resolution.

#39878 removes the overhead for three Z-Image kernels by replaying recorded launches from a Rust extension. This PR does the same in pure Python, and does it generically, so that covering another kernel takes one decorator line and no change to the kernel or its wrapper.

Modifications

sglang/kernels/ops/diffusion/common/fast_launch.py adds @replay_launches:

  • First call with a given signature. The signature is each tensor's shape, stride, dtype, device and data_ptr() % 16, plus the non-tensor arguments. On this call the wrapper runs normally while its Triton launches are recorded: compiled kernel, grid and arguments, with each tensor argument mapped by pointer to an input or a returned output.
  • Later calls with that signature. They allocate the outputs with empty_strided and call the recorded compiled launchers directly. The kernels, grids and arguments are the same, so outputs are bit-identical.
  • What a signature must satisfy to be replayed. The wrapper's GPU work is Triton launches into outputs it allocates with torch.empty* and returns. Recording enforces what it can see of that and otherwise leaves the signature on the normal path:
    • aten ops other than allocation and views (checked with a dispatch mode; a view is any op that mutates nothing and returns only aliases of its inputs, so reshape on inference tensors counts);
    • reads of tensor values (.item());
    • launches on tensors that are neither inputs nor outputs, or on inputs that share a pointer.
  • Graph capture, compile and Triton versions.
    • Recording does not sync, so it also works inside CUDA graph capture.
    • Under torch.compile the decorator calls the wrapper itself, so it stays traceable.
    • A Triton whose launcher does not take every parameter (before 3.3) stays on the normal path.
    • SGLANG_DIFFUSION_TRITON_FAST_LAUNCH=0 turns it off.

The decorator is applied, one line each, to the hot Triton wrappers above:

  • rmsnorm_scale, rmsnorm_tanh_residual, zimage_qk_rmsnorm_native
  • fused_layernorm_modulate_raw
  • fused_silu_mul_bitexact, fused_packed_silu_mul_bitexact
  • flux2_strided_qknorm_rope, fused_rope_rotate_half_bitexact

Kernels and wrapper bodies are unchanged: existing files change by +18/−1 lines in total (one import and one decorator per wrapper, plus the package README and common/__init__ docstrings). The mechanism is 281 lines in the new module, and the tests are 343 lines. JIT CUDA (tvm_ffi) wrappers such as residual_gate_add are not covered, because their calls have no common hook to record.

Accuracy Tests

Replay calls the same compiled kernels with the same arguments, so outputs are bit-identical. Output image md5 matched main in every end-to-end configuration below.

test/registered/kernels/ops/diffusion/test_fast_launch.py:

  • The decorated wrappers. For each of the 8, a replayed call equals the wrapper's own result bit for bit, and Triton's JIT is never called. This runs under both no_grad and inference_mode.
  • The recording rules, using a toy Triton kernel:
    • a 16-byte-misaligned operand records its own launch;
    • a function that does aten compute, launches on a temporary, reads a tensor value or takes a list argument stays on the normal path;
    • an in-place launch, with or without a returned result, replays;
    • nested decorated functions replay as one;
    • scalars are keyed by type;
    • views replay under inference mode.
  • Graph capture, compile and the off switch. A recorded launch replays inside CUDA graph capture, and a new signature records there without a sync. torch.compile(fullgraph=True) traces the wrapper. The off switch runs the wrapper.

Passing on H200 (sm90) and GB300 (sm103), together with every existing suite that calls a decorated wrapper:

  • test_norm, test_model_fast_paths, test_activation, test_flux2_fp8_norm_quant, test_flux2_strided_qknorm_rope;
  • test_layernorm_modulate, test_sites, test_shape_specialization, test_h3_vae_fused_norm, test_longcat_image_norm_modulate, test_fused_qk_rmsnorm_rope_gate;
  • test_zimage_qknorm_fusion, test_qwen_image21_cuda, test_minimax_h3_vae_parallel_modes.

Speed Tests and Profiling

Base 8afe107, torch 2.13, triton 3.7.1.

Host time per call (µs, 256²-class shapes):

wrapper H200 main H200 this PR GB300 main GB300 this PR
rmsnorm_scale 27.95 13.55 30.79 14.71
rmsnorm_tanh_residual 32.02 14.97 34.03 15.60
zimage_qk_rmsnorm_native 24.11 12.46 25.67 13.68
fused_layernorm_modulate_raw 31.32 14.40 33.16 15.82
fused_silu_mul_bitexact 19.76 14.28 29.32 21.74
fused_packed_silu_mul_bitexact 20.99 11.24 23.22 12.99
flux2_strided_qknorm_rope 26.70 18.29 29.60 19.94
fused_rope_rotate_half_bitexact 22.05 15.73 31.29 22.46

With SGLANG_DIFFUSION_TRITON_FAST_LAUNCH=0 the numbers match main.

End to end (client wall clock, median, eager):

GB300, one GPU. Servers of main and this PR are resident on the same GPU, requests are rotated between them, and processes are pinned to the GPU's NUMA node.

main this PR this PR faster in
Z-Image-Turbo 512² 435.6 / 416.2 ms 374.7 / 371.0 ms −14.0% / −10.9% 15 and 20 rotations
Z-Image-Turbo 1024² 447.9 ms 404.3 ms −9.7%
FLUX.1-dev 512² 3219 ms 3098 ms −3.8% 10/10
FLUX.1-dev 1024² 3211 ms 3092 ms −3.7% 10/10
Ideogram-4 512² 1712 ms 1671 ms −2.4% 10/10
Ideogram-4 1024² 2661 ms 2663 ms no change 5/10

The previous revision of this PR applied the cache by hand inside each of the three Z-Image wrappers. It was measured in the same rotation: Z-Image 512² 357.5 / 355.1 ms, 1024² 405.2 ms. The decorator keeps most of that gain without touching the wrappers. Its per-call cost measured the same as the hand-written path, and every call replayed (counted in a live server).

GB300 ×2, Qwen-Image-2.1 TP2. main, this PR and a later JIT-replay experiment were resident together, with requests rotated over 8 rounds. The JIT-replay experiment was dropped: replay costs more than the thin tvm-ffi wrappers spend.

main this PR
512² 1546.7 ms 1354.5 ms −12.4%
1024² 1625.5 ms 1461.8 ms −10.1%

B200 ×2, TP2 / Ulysses2 (x86 host, AMD EPYC 9575F). Servers were swapped each round, 2 rounds.

  • FLUX.1-dev 512²: main 1335–1357 ms, this PR 1240–1297 ms (−5 to −8%). 1024² is bimodal on both, and this PR is at or below main.
  • Ideogram-4, Qwen-Image-2.1 and FLUX.2-dev: within noise at both sizes. These cells are GPU-bound on this host.

H200, Z-Image one GPU: no change (512² 236.5 vs 234.9 ms; 1024² 862.7 vs 862.4 ms). The step is GPU-bound there.

The gain shows up where the step is host-bound: on Grace hosts, and at low resolution with several ranks. The JIT CUDA wrappers that remain (residual_gate_add, qknorm_complex_rope_cuda, modulate_scale_shift) and custom_all_reduce are the next host-time items for FLUX/Qwen/Ideogram.

Checklist

🤖 Generated with Claude Code


CI States

Latest PR Test (Base): 🚫 Run #38355101182
Latest PR Test (Extra): ❌ Run #38355100702
Latest PR Test (AMD ROCm 10): ❌ Run #38355101222

A small Triton kernel's wrapper costs more host time than its GPU time:
operand checks, stride math, the device guard and the JIT dispatch take
~28 us per rmsnorm_scale call at a Z-Image 512^2 shape on H200, all of it
fixed by the operands' signature. SignatureLaunch keeps Triton's compiled
launcher and the derived scalars per signature and relaunches with the same
kernel, grid and arguments, so outputs are bit-identical. The signature
includes 16-byte pointer alignment, which Triton specializes on.

The launcher's calling convention is checked once per kernel against the
JIT result (never inside graph capture); a convention change turns the
fast path off. torch.compile traces the unchanged JIT path.
SGLANG_DIFFUSION_TRITON_FAST_LAUNCH=0 disables it.

Applied to rmsnorm_scale, rmsnorm_tanh_residual and
zimage_qk_rmsnorm_native, the call sites #39878 replays
from Rust; a Python replay of the compiled launcher costs the same.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added documentation Improvements or additions to documentation jit-kernel labels Oct 12, 2026
@mickqian

Copy link
Copy Markdown
Collaborator Author

/tag-and-rerun-ci

@github-actions github-actions Bot added the run-ci CI: run the baseline test suite on this PR label Oct 12, 2026
Replace the per-wrapper signature/record code with @replay_launches: the
first call per operand signature runs the wrapper while recording its
Triton launches (JITFunction.run patched for the call; a dispatch mode
allowing only allocation and views); later calls allocate the outputs
and call the recorded compiled launchers. Kernels and wrapper bodies are
untouched; each covered wrapper gains one decorator line.

Views are recognised by aliasing rather than by op name: inference
tensors skip the autograd decomposition, so a reshape reaches the guard
as aten.reshape and a name list rejected rmsnorm_scale in a live server.

Cover the hot wrappers of FLUX.1/2, Qwen-Image and Ideogram besides
Z-Image's three: fused_layernorm_modulate_raw, fused_silu_mul_bitexact,
fused_packed_silu_mul_bitexact, flux2_strided_qknorm_rope and
fused_rope_rotate_half_bitexact.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@mickqian mickqian changed the title [diffusion] Launch small Triton norm kernels from a per-signature cache [diffusion] Replay small Triton wrappers' launches per operand signature Oct 13, 2026
@mickqian
mickqian merged commit 1617627 into main Oct 14, 2026
191 of 211 checks passed
@mickqian
mickqian deleted the mick/diffusion-triton-fast-launch branch October 14, 2026 03:18
detain added a commit to detain/sglang that referenced this pull request Oct 15, 2026
…ject#4

Conflict resolutions (35 files), preserving stack behavior:

srt/utils/common.py: make_layers — folded our per-layer empty_cache body
  INTO upstream's stage-binding build() (layer_stack refactor sgl-project#43453-43460).
  Upstream removed is_building_neighbour_layer/building_neighbour_layer
  entirely (our earlier sgl-project#42481 pick); only user was qwen4_exp.
srt/models/qwen4_exp.py: import unions (cached_property/partial/TYPE_CHECKING,
  linear_attn_parallel_group); dropped dead is_building_neighbour_layer import;
  prefetch_stream keeps our _graph_prefetch_buffer line with upstream's
  ple_offload_embedding conditional; our PLE host-staging design kept
  (_offloads_ple guard moot under new stage facts).
srt/managers/cache_controller.py: upstream sgl-project#43550 absorbed our sgl-project#39830
  prefetch-degrade fixes; took upstream restructure, deleted auto-merge's
  stale duplicate tails (double-ack/deadlock hazard), kept our rationale
  comments. Fenced start_writing + load_fence_stream survive.
srt/mem_cache/hybrid_cache/hybrid_cache_controller.py: same pattern;
  sidecar degrade comment updated ('upstream adopted unconditionally').
model_executor/pool_configurator.py: UNION draft-cell sizing — our FP4
  _compute_eagle_fp4_draft_cell_size branch + upstream HiSparse
  draft_loc_space_scale else-branch. b05a7c1 None layer_info guard intact.
srt/layers/attention/qsa/sparse_attn.py: kept OUR FP8-descale chunk loop
  (superset; upstream sgl-project#41933 only re-commented); took upstream
  _pack_qsa_prefill_kv + pack_qsa_prefill_kv; our ck interface keeps
  k_scale/v_scale/max_q; __all__ union.
qsa/mqa.py: import functools + min_scored_len kwarg into our triton-first
  _resolve_mqa_backend dispatch; upstream ctas_per_row rework kept.
qsa/qsa_indexer.py: batch-invariance gate moved INTO upstream's shared
  use_fast_topk predicate.
qwen_sparse_attn_backend.py: imports union; gather path UNION — upstream
  pack_qsa_prefill_kv used when scales are unit (QSA stores unscaled K/V),
  our index_select fallback when checkpoint k/v descales != 1.0.
managers/schedule_policy.py: add_chunked_req UNION — upstream slot-reserve
  gate first, then our prefill_chunk_alignment truncation (monotone-safe);
  all 3 alignment sites (1177/1340/1608) intact.
managers/tokenizer_manager.py: import unions (PositionalEmbedsTransport +
  RequestPreprocessor + watermarking fns).
entrypoints/openai/serving_base.py: kept OUR request_preprocessor.run path.
model_executor/runner/eager_runner.py: took upstream TCPCG eager-fallback
  removal (sgl-project#42287); kept our shared cp_extend_forward call shape (our
  runner_utils/cp_extend.py is byte-equivalent to upstream's
  _execute_extend_cp — single source of truth).
runner/flashinfer_autotune.py: import union (can_run_dummy_draft_extend +
  cdiv/ceil_align + upstream get_eager_max_batch_size sgl-project#43789).
arg_groups/model_hook.py: Gemma4 accepted_backends union fa4 + flashinfer
  (NVFP4-KV enablement).
multimodal_gen/runtime/layers/lora/linear.py: KEPT OUR sgl-project#37460 pipeline
  structure; upstream runtime_lora_scale==0 early-return ordering adopted;
  _add_lora_delta/_add_lora_output_offset wired into pipeline; kept
  UPSTREAM's quality-tiered _scale_lora_delta (deleted our simple variant).
  DEBT: upstream RowParallel bias_in_gemm lossless opt not ported (perf-only).
multimodal_gen/runtime/layers/attention/layer.py: _gather_sharded_sequence
  took upstream sgl-project#43793 .contiguous() fix (subsumes our 55aaa02 guard);
  our _forward_with_kv_gather k/v guards kept (separate path, not covered).
configs/pipeline_configs/base.py + configs/sample/minimax_h3.py + envs.py:
  unions (supports_continuous_batching / teacache_params+fields /
  SGLANG_TEACACHE_CALIBRATE).
models/dits/ltx_2.py mask-fill: took upstream (identical to our fix).
ltx_2/denoising.py: took upstream pass_specs (sgl-project#43667) — subsumes our
  legacy one-stage branch; our _ltx2_model_forward_context helpers already
  upstreamed (entry-142 sgl-project#42928 CONFIRMED SUPERSEDED).
ltx_2/decoding_av.py: KEPT OUR split-stage AV decode (independent pool
  scaling) over upstream single-class restructure; adopted upstream
  chunked pinned-memory _postprocess_video; TODO(merge-4): upstream
  EncodeWhileDecoding streaming + rank-0 retain gating not ported.
kernels/ops/diffusion/activation/silu_mul_bitexact.py: our out= column-slice
  support + XPU guard UNION with upstream @replay_launches (sgl-project#43859).
kernels/ops/diffusion/README.md + docs performance-optimization.mdx: unions.
rust/sglang-renderer: ACCEPTED upstream crate removal (sampling.rs git rm);
  our sgl-project#42779 top_k vocab-bound port moves to sglang-server message/sampling.rs
  (follow-up).
Tests: anthropic test_serving — UNION fake-class exception param + our
  request_preprocessor stubs, took upstream's _FakeNonStreamingOpenAI removal
  (68 pass). test_server_args — union both new tests (pre-existing GPU-only
  collection ENV fail unchanged). test_lora_inference_mode — took upstream
  sgl-project#43028 runtime_scale+quality-tier test (supersedes our adapted variant;
  our kept quality-tiered _scale_lora_delta satisfies it).
  test_fused_moe_triton_config — kept our int4-filename + RTX-PRO-6000 JSON
  tests despite upstream sgl-project#43503 test-drop. test_hicache_file_lru_unit —
  union our corrupt-page-degrade class + upstream sgl-project#42077 uncached-batch test
  (40 pass). test_hicache_host_register — upstream alias host_common.
  test_cuda_graph_setup — took upstream rewrite of our override-decode-runner
  test into draft-local-capture-sizes test (sgl-project#43303 API).

Verified auto-merged stack markers survived: flashinfer_backend KVLocPlan/
prefill_wrappers_ragged/NVFP4-KV forwarding, memory_pool set_kv_buffer_prefix_valid,
fp8 grouped_moe kwarg, fp8_utils reshape fix 3642490, SGLANG_QWEN4_PLE_FILE_REUSE,
schedule_batch freed_rows fix, prefill_chunk_alignment sites, sgl-project#42736 autotune
default-on, PCIe-IPC vocab gathers, flashinfer 0.7.1 pin taken (NOT installed).

Co-Authored-By: Claude Opus 5.5 (1M context)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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.

1 participant