[AMD] Optimize Qwen3-VL decode - fuse QK-norm + 3D mRoPE + KV cache write (#21458)

Co-authored-by: Bingxu Chen <bingxche@amd.com>
Co-authored-by: HaiShaw <hixiao@gmail.com>
This commit is contained in:
YC Yen-Ching Tseng
2026-03-31 23:34:07 -07:00
committed by GitHub
co-authored by Bingxu Chen HaiShaw
parent 71baa025be
commit a188208e9a
+101 -3
View File
@@ -19,6 +19,7 @@ from sglang.srt.layers.pooler import Pooler, PoolingType
from sglang.srt.layers.quantization.base_config import QuantizationConfig
from sglang.srt.layers.radix_attention import RadixAttention
from sglang.srt.layers.rotary_embedding import get_rope
from sglang.srt.layers.rotary_embedding.mrope import MRotaryEmbedding
from sglang.srt.layers.utils import PPMissingLayer, get_layer_id
from sglang.srt.layers.vocab_parallel_embedding import ParallelLMHead
from sglang.srt.model_executor.forward_batch_info import ForwardBatch, PPProxyTensors
@@ -30,13 +31,25 @@ from sglang.srt.models.qwen2 import Qwen2MLP as Qwen3MLP
from sglang.srt.models.qwen2 import Qwen2Model
from sglang.srt.models.utils import apply_qk_norm
from sglang.srt.server_args import get_global_server_args
from sglang.srt.utils import add_prefix, is_cuda, is_npu
from sglang.srt.utils import add_prefix, get_bool_env_var, is_cuda, is_hip, is_npu
Qwen3Config = None
logger = logging.getLogger(__name__)
_is_cuda = is_cuda()
_is_hip = is_hip()
_is_npu = is_npu()
_use_aiter = get_bool_env_var("SGLANG_USE_AITER") and _is_hip
_has_fused_qk_norm_mrope = False
if _use_aiter:
try:
from aiter import fused_qk_norm_mrope_3d_cache_pts_quant_shuffle
_has_fused_qk_norm_mrope = True
logger.info("aiter fused_qk_norm_mrope_3d kernel available")
except ImportError:
pass
if _is_npu:
from sgl_kernel_npu.norm.split_qkv_rmsnorm_rope import split_qkv_rmsnorm_rope
@@ -138,6 +151,19 @@ class Qwen3Attention(nn.Module):
)
self.alt_stream = alt_stream
self.use_fused_qk_norm_mrope = (
_has_fused_qk_norm_mrope
and isinstance(self.rotary_emb, MRotaryEmbedding)
and getattr(self.rotary_emb, "mrope_section", None) is not None
)
if self.use_fused_qk_norm_mrope:
# Scale tensors MUST stay on CPU: the C++ kernel uses .item<float>()
# which triggers hipMemcpy D2H + sync on CUDA tensors, breaking graph capture.
# Explicit device='cpu' is required because SGLang constructs models inside
# a `with torch.device('cuda'):` context that changes the default device.
self._fused_k_scale = torch.tensor(1.0, dtype=torch.float32, device="cpu")
self._fused_v_scale = torch.tensor(1.0, dtype=torch.float32, device="cpu")
def forward_prepare_native(self, positions, hidden_states):
qkv, _ = self.qkv_proj(hidden_states)
q, k, v = qkv.split([self.q_size, self.kv_size, self.kv_size], dim=-1)
@@ -172,6 +198,66 @@ class Qwen3Attention(nn.Module):
)
return q, k, v
def forward_prepare_aiter_fused_mrope(self, positions, hidden_states, forward_batch):
"""Fused QK-norm + 3D mRoPE + KV cache write for decode (ROCm/aiter).
The fused HIP kernel replaces split → QK norm → mRoPE → cache write,
so KV is already in the paged cache when this returns.
Returns (q, None, None); caller must pass save_kv_cache=False to attn.
"""
qkv, _ = self.qkv_proj(hidden_states)
num_tokens = qkv.shape[0]
qkv_3d = qkv.view(num_tokens, -1, self.head_dim)
token_to_kv_pool = forward_batch.token_to_kv_pool
k_cache, v_cache = token_to_kv_pool.get_kv_buffer(self.attn.layer_id)
slot_mapping = forward_batch.out_cache_loc
cos_sin = self.rotary_emb.cos_sin_cache
if cos_sin.dtype != qkv.dtype:
cos_sin = cos_sin.to(dtype=qkv.dtype)
q_out = torch.empty(
num_tokens,
self.num_heads,
self.head_dim,
dtype=qkv.dtype,
device=qkv.device,
)
fused_qk_norm_mrope_3d_cache_pts_quant_shuffle(
qkv_3d,
self.q_norm.weight,
self.k_norm.weight,
cos_sin,
positions,
num_tokens,
self.num_heads,
self.num_kv_heads,
self.num_kv_heads,
self.head_dim,
self.rotary_emb.is_neox_style,
self.rotary_emb.mrope_section,
self.rotary_emb.mrope_interleaved,
self.q_norm.variance_epsilon,
q_out,
k_cache,
v_cache,
slot_mapping,
self._fused_k_scale,
self._fused_v_scale,
None,
None,
False,
False,
0,
0,
)
q = q_out.reshape(num_tokens, -1)
return q, None, None
def forward(
self,
positions: torch.Tensor,
@@ -181,7 +267,19 @@ class Qwen3Attention(nn.Module):
if get_global_server_args().rl_on_policy_target is not None:
hidden_states = hidden_states.bfloat16()
if (
save_kv_cache = True
use_aiter_fused = (
self.use_fused_qk_norm_mrope
and forward_batch.forward_mode.is_decode()
and get_global_server_args().rl_on_policy_target is None
)
if use_aiter_fused:
q, k, v = self.forward_prepare_aiter_fused_mrope(
positions, hidden_states, forward_batch
)
save_kv_cache = False
elif (
not _is_npu
or forward_batch.forward_mode.is_extend_or_draft_extend_or_mixed()
):
@@ -200,7 +298,7 @@ class Qwen3Attention(nn.Module):
q = q.to(torch.bfloat16)
k = k.to(torch.bfloat16)
attn_output = self.attn(q, k, v, forward_batch)
attn_output = self.attn(q, k, v, forward_batch, save_kv_cache=save_kv_cache)
output, _ = self.o_proj(attn_output)
return output