Skip to content

[KDA-Pilot] Add LTX2 QKNorm split-RoPE CUDA fast path - #29708

Merged
BBuf merged 5 commits into
sgl-project:mainfrom
BBuf:codex/kda-ltx2-qknorm-split-rope
Jul 1, 2026
Merged

[KDA-Pilot] Add LTX2 QKNorm split-RoPE CUDA fast path#29708
BBuf merged 5 commits into
sgl-project:mainfrom
BBuf:codex/kda-ltx2-qknorm-split-rope

Conversation

@BBuf

@BBuf BBuf commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator

Powered by KDA-Pilot, we propose a native CUDA JIT fast path for the LTX-2.3 Q/K RMSNorm + split-RoPE attention preprocessing pattern.

Motivation

Add a native CUDA JIT fast path for the LTX-2.3 Q/K RMSNorm + split-RoPE attention preprocessing pattern from KDA-Pilot task b200_ltx2_qknorm_split_rope__bitwise.

The hot pattern is equivalent to:

q = apply_split_rotary_emb(q_norm(q), (q_cos, q_sin)).to(torch.bfloat16)
k = apply_split_rotary_emb(k_norm(k), (k_cos, k_sin)).to(torch.bfloat16)

for BF16 contiguous [B, S, H] Q/K tensors and 4D split-RoPE cos/sin tensors with production LTX-2.3 layouts. The KDA final candidate preserves bitwise equality to this BF16 attention-input contract while avoiding the separate eager RMSNorm/RoPE materialization path.

Modifications

  • Add diffusion_ltx2_qknorm_split_rope lightweight JIT CUDA custom op.
  • Mark the CUDA csrc file with the MIT HAN Lab Kernel Design Agents provenance comment.
  • Register the op eagerly with a fake impl so direct torch.compile(fullgraph=True) sees an opaque custom op instead of tracing JIT/module loading.
  • Support the LTX-2.3 production split-RoPE rows with head_dim in {64, 128}, BF16 inputs/weights/cos/sin, real non-contiguous cos/sin strides, and independent Q/K sequence lengths.
  • Wire LTX2Attention to try this CUDA path by default for TP=1, torch.nn.RMSNorm, 4D split-RoPE inputs, then fall back to the existing RMSNorm + RoPE implementation if unsupported or if JIT load/launch fails once.
  • Add a B200 correctness test covering bitwise equality, unsupported input rejection, and torch.compile(fullgraph=True) custom-op coverage.
  • Add a standalone benchmark over the LTX-2.3 production shape set plus CI-small shapes.

Accuracy Tests

python3 -m py_compile \
  python/sglang/jit_kernel/diffusion/ltx2_qknorm_split_rope.py \
  python/sglang/multimodal_gen/runtime/models/dits/ltx_2.py \
  test/registered/jit/diffusion/test_ltx2_qknorm_split_rope.py \
  test/registered/jit/benchmark/diffusion/bench_ltx2_qknorm_split_rope.py
python3 -m ruff format --check ...
python3 -m ruff check ...
git diff --check

Result: local syntax/format/lint/diff checks passed.

B200 unit test:

CUDA_VISIBLE_DEVICES=5 \
PYTHONPATH=python:. \
FLASHINFER_DISABLE_VERSION_CHECK=1 \
TVM_FFI_CACHE_DIR=/tmp/tvm-ffi-ltx2-k22-pr \
python3 -m pytest -q test/registered/jit/diffusion/test_ltx2_qknorm_split_rope.py -s

Result: 5 passed.

Speed Tests and Profiling

KDA-Pilot task b200_ltx2_qknorm_split_rope__bitwise, final k22 run on an idle B200:

  • Correctness: all 14 production rows passed with torch.equal / zero tolerance for both Q and K outputs.
  • Production shape coverage: LTX-2.3 two-stage and HQ rows, head_dim in {64, 128}, video/audio/cross rows, sequence lengths from 126 to 32640.
  • Kernel benchmark: equal-weight geometric mean speedup 5.84x over the task-local destination-passing PyTorch baseline; min 4.22x, max 7.34x; no production row regressed.
  • The final candidate had fallback_count == 0 across the production grid.

Integrated benchmark script CI-small check on B200:

CUDA_VISIBLE_DEVICES=5 \
PYTHONPATH=python:. \
FLASHINFER_DISABLE_VERSION_CHECK=1 \
TVM_FFI_CACHE_DIR=/tmp/tvm-ffi-ltx2-k22-pr \
SGLANG_IS_IN_CI=1 \
python3 test/registered/jit/benchmark/diffusion/bench_ltx2_qknorm_split_rope.py
Workload Torch us CUDA us Speedup
stage1_video_self_q16_k16_d4096 191.15 33.98 5.625x
stage1_audio_to_video_q16_k8_d2048 176.30 32.34 5.451x

B200 LTX-2.3 HQ end-to-end A/B with the same k22 fused path, no torch.compile:

Implementation E2E ms Avg denoise ms Median denoise ms Denoise stage ms Refinement stage ms
Baseline 17022.27 867.16 888.05 12941.22 2674.02
k22 fused 15462.38 780.02 802.26 11679.22 2367.35
Delta -9.16% -10.05% -9.66% -9.75% -11.47%

The fused run recorded hit=16384, fallback=0 for this path.

B200 Fused-vs-Unfused Accuracy

The fused CUDA path was validated on B200 against the original unfused PyTorch implementation for the exact Q/K attention-preprocessing contract it replaces:

q_ref = apply_split_rotary_emb(q_norm(q), (q_cos, q_sin)).to(torch.bfloat16)
k_ref = apply_split_rotary_emb(k_norm(k), (k_cos, k_sin)).to(torch.bfloat16)

For the supported LTX-2.3 production shapes, the CUDA output is bitwise-identical to the unfused PyTorch output before attention consumes Q/K, so this optimization does not change model accuracy relative to the original non-fused path.

B200 check Shape coverage Comparison Result
KDA production oracle 14 LTX-2.3 production rows, head_dim in {64, 128}, video/audio/cross rows, sequence lengths 126 to 32640 fused Q/K vs unfused PyTorch Q/K torch.equal for both Q and K on every row
SGLang unit test supported B200 rows, unsupported-input rejection, torch.compile(fullgraph=True) custom-op coverage fused Q/K vs unfused PyTorch Q/K 5 passed
Integrated fast-path dispatch LTX-2.3 HQ path with supported B200 shapes fast path hit count and fallback count hit=16384, fallback=0

Accuracy conclusion: on B200, the optimization produces the same Q/K tensors as the original non-fused PyTorch path for all covered production inputs; precision is unchanged.

Result Image Comparison

B200 LTX-2.3 HQ A/B output sample (seed=42, 1920x1088, 121 frames). The figure compares the original unfused path against the fused CUDA QKNorm + split-RoPE path at frames 0, 60, and 120.

pr29708_ltx2_qknorm_result_compare.png

Video-level decode comparison: SSIM All=1.000000, PSNR=inf.

Checklist

  • Default LTX2 path tries CUDA first and falls back to the existing PyTorch implementation on unsupported inputs or one-time runtime failure.
  • No environment variable is required to enable the CUDA kernel.
  • Direct custom op registration is eager and has a fake impl for torch.compile compatibility.
  • B200 bitwise unit test included.
  • Standalone benchmark script included for LTX-2.3 production shapes.
  • B200 kernel benchmark and LTX-2.3 HQ A/B evidence included.

Review and Merge Process

This is a focused KDA-Pilot kernel integration PR. The expected useful effect is the faster LTX-2.3 Q/K normalization + split-RoPE preprocessing group; whole-model speedup is visible but remains bounded because attention, MLP, and decode still dominate full video generation runtime.


CI States

Latest PR Test (Base): ⏳ Run #28491339822
Latest PR Test (Extra): ⏳ Run #28491339740

@github-actions github-actions Bot added diffusion SGLang Diffusion jit-kernel labels Jun 30, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a CUDA fast path for LTX2 Q/K RMSNorm + split RoPE, including its Python FFI bindings, integration into the LTX2 model forward pass, benchmarks, and unit tests. The review feedback suggests several improvements to enhance robustness and code cleanliness: using int64_t for tensor dimensions in the CUDA kernel to prevent potential integer overflows, removing redundant explicit type casts in both CUDA and Python wrappers, simplifying parameter checks for RMSNorm, and aligning the reference implementation in the unit tests with the benchmark code.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +34 to +45
__device__ inline float compute_rstd(
const __nv_bfloat16* __restrict__ xrow,
int hidden_size,
float eps,
int tid,
int lane,
int warp_id,
float* warp_sum,
float* s_rstd) {
float local = 0.f;
const int n_vec = hidden_size >> 2;
for (int i = tid; i < n_vec; i += kThreads) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

To prevent potential integer overflows with large tensors, it's safer to use int64_t for dimensions passed from Python, as PyTorch tensor dimensions are 64-bit. hidden_size and related variables in this function should be int64_t.

__device__ inline float compute_rstd(
    const __nv_bfloat16* __restrict__ xrow,
    int64_t hidden_size,
    float eps,
    int tid,
    int lane,
    int warp_id,
    float* warp_sum,
    float* s_rstd) {
  float local = 0.f;
  const int64_t n_vec = hidden_size >> 2;
  for (int64_t i = tid; i < n_vec; i += kThreads) {

Comment on lines +92 to +117
int seq_len,
int num_heads,
int head_dim,
int64_t stride_cos_b,
int64_t stride_cos_h,
int64_t stride_cos_t,
int64_t stride_sin_b,
int64_t stride_sin_h,
int64_t stride_sin_t) {
const int row = blockIdx.x;
const int batch = row / seq_len;
const int token = row - batch * seq_len;
const int hidden_size = num_heads * head_dim;
const int half_dim = head_dim >> 1;
const auto* __restrict__ xrow = x + static_cast<int64_t>(row) * hidden_size;
auto* __restrict__ outrow = out + static_cast<int64_t>(row) * hidden_size;
const int tid = threadIdx.x + threadIdx.y * 32;
const int lane = threadIdx.x;
const int warp_id = threadIdx.y;

__shared__ float warp_sum[4];
__shared__ float s_rstd;
const float rstd = compute_rstd(xrow, hidden_size, eps, tid, lane, warp_id, warp_sum, &s_rstd);

const int num_pairs = num_heads * half_dim;
for (int pair = tid; pair < num_pairs; pair += kThreads) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

To prevent potential integer overflows, dimensions passed from Python should be handled as int64_t. This includes the kernel parameters, derived dimension variables, and loop counters.

    int64_t seq_len,
    int64_t num_heads,
    int64_t head_dim,
    int64_t stride_cos_b,
    int64_t stride_cos_h,
    int64_t stride_cos_t,
    int64_t stride_sin_b,
    int64_t stride_sin_h,
    int64_t stride_sin_t) {
  const int row = blockIdx.x;
  const int batch = row / seq_len;
  const int token = row - batch * seq_len;
  const int64_t hidden_size = static_cast<int64_t>(num_heads) * head_dim;
  const int64_t half_dim = head_dim >> 1;
  const auto* __restrict__ xrow = x + static_cast<int64_t>(row) * hidden_size;
  auto* __restrict__ outrow = out + static_cast<int64_t>(row) * hidden_size;
  const int tid = threadIdx.x + threadIdx.y * 32;
  const int lane = threadIdx.x;
  const int warp_id = threadIdx.y;

  __shared__ float warp_sum[4];
  __shared__ float s_rstd;
  const float rstd = compute_rstd(xrow, hidden_size, eps, tid, lane, warp_id, warp_sum, &s_rstd);

  const int64_t num_pairs = num_heads * half_dim;
  for (int64_t pair = tid; pair < num_pairs; pair += kThreads) {

Comment on lines +162 to +164
static_cast<int>(seq_len),
static_cast<int>(num_heads),
static_cast<int>(head_dim),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

With the kernel arguments updated to int64_t, these static_cast<int> are no longer necessary and should be removed to pass the full 64-bit values.

      seq_len,
      num_heads,
      head_dim,

Comment on lines +80 to +85
int(q.shape[0] * q.shape[1]),
int(q.shape[1]),
int(k.shape[0] * k.shape[1]),
int(k.shape[1]),
int(num_heads),
int(head_dim),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

Corresponding to the change to int64_t in the CUDA kernel, these explicit casts to int should be removed. The TVM FFI will handle passing Python integers as int64_t to the C++ backend.

Suggested change
int(q.shape[0] * q.shape[1]),
int(q.shape[1]),
int(k.shape[0] * k.shape[1]),
int(k.shape[1]),
int(num_heads),
int(head_dim),
q.shape[0] * q.shape[1],
q.shape[1],
k.shape[0] * k.shape[1],
k.shape[1],
num_heads,
head_dim,

Comment on lines +103 to +110
or not isinstance(q_norm, nn.RMSNorm)
or not isinstance(k_norm, nn.RMSNorm)
or q_norm.weight is None
or k_norm.weight is None
or q_norm.eps is None
or k_norm.eps is None
or float(q_norm.eps) != float(eps)
or float(k_norm.eps) != float(eps)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The checks for weight and eps being None appear to be redundant. torch.nn.RMSNorm (used when tp_size==1) is created with elementwise_affine=True (the default), so weight will be a Parameter. eps is also a required float argument. The custom LTX2TPRMSNormAcrossHeads also ensures these attributes are not None. Removing these unnecessary checks would improve code clarity.

Suggested change
or not isinstance(q_norm, nn.RMSNorm)
or not isinstance(k_norm, nn.RMSNorm)
or q_norm.weight is None
or k_norm.weight is None
or q_norm.eps is None
or k_norm.eps is None
or float(q_norm.eps) != float(eps)
or float(k_norm.eps) != float(eps)
or not isinstance(q_norm, nn.RMSNorm)
or not isinstance(k_norm, nn.RMSNorm)
or float(q_norm.eps) != float(eps)
or float(k_norm.eps) != float(eps)

Comment on lines +57 to +62
out = split_x * cos_u
first_out = out[..., :1, :]
second_out = out[..., 1:, :]
first_out.addcmul_(-sin_u, second_x)
second_out.addcmul_(sin_u, first_x)
out = out.reshape(*out.shape[:-2], last)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

This part of the reference implementation can be simplified by applying addcmul_ directly on slices of the out tensor, which would make it more concise and consistent with the implementation in the benchmark file.

Suggested change
out = split_x * cos_u
first_out = out[..., :1, :]
second_out = out[..., 1:, :]
first_out.addcmul_(-sin_u, second_x)
second_out.addcmul_(sin_u, first_x)
out = out.reshape(*out.shape[:-2], last)
out = split_x * cos_u
out[..., :1, :].addcmul_(-sin_u, second_x)
out[..., 1:, :].addcmul_(sin_u, first_x)
out = out.reshape(*out.shape[:-2], last)

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@BBuf
BBuf force-pushed the codex/kda-ltx2-qknorm-split-rope branch from 900251f to 8b5ee20 Compare June 30, 2026 04:55
@BBuf
BBuf force-pushed the codex/kda-ltx2-qknorm-split-rope branch from 7399c56 to 06a3d4c Compare June 30, 2026 09:36
@BBuf

BBuf commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator Author
图片

@BBuf
BBuf merged commit fcb9f22 into sgl-project:main Jul 1, 2026
108 of 136 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant