94057c3d3e
PR Test (NPU) / check-changes (push) Has been cancelled
PR Test (NPU) / pr-gate (push) Has been cancelled
PR Test (NPU) / set-image-config (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-4-npu-a3 (push) Has been cancelled
PR Test (NPU) / stage-b-test-16-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-1-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-2-npu-a3 (push) Has been cancelled
PR Test (Arm64) / pr-gate (push) Has been cancelled
PR Test (Arm64) / check-changes (push) Has been cancelled
PR Test (Arm64) / build-test (push) Has been cancelled
PR Test (sgl-router) / gate (push) Has been cancelled
PR Test (sgl-router) / tier-1 — lint (push) Has been cancelled
PR Test (sgl-router) / tier-2 — build + test (push) Has been cancelled
PR Test (sgl-router) / tier-3 — docker (placeholder) (push) Has been cancelled
PR Test (sgl-router) / tier-3 — k8s integration (push) Has been cancelled
PR Test (sgl-router) / tier-3 — e2e (push) Has been cancelled
PR Test (sgl-router) / finish (push) Has been cancelled
PR Test (NPU) / single-node-poc (map[name:qwen3_6_27b_w8a8_1p_in64k_out1k_50ms runner:linux-aarch64-a3-2 test_case:test/registered/ascend/performance/qwen3_6_27b/test_npu_qwen3_6_27b_w8a8_1p_in64k_out1k_50ms.py test_type:perf]) (push) Has been cancelled
PR Test (NPU) / pr-test-npu-finish (push) Has been cancelled
PR Test (Xeon) / pr-gate (push) Has been cancelled
PR Test (Xeon) / check-changes (push) Has been cancelled
PR Test (Xeon) / build-test (, xeon-gnr, base-b-test-cpu) (push) Has been cancelled
PR Test (XPU) / check-changes (push) Has been cancelled
PR Test (XPU) / pr-gate (push) Has been cancelled
PR Test (XPU) / stage-a-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / wait-for-stage-a (push) Has been cancelled
PR Test (XPU) / stage-b-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / finish (push) Has been cancelled
CI Model Inventory / build-inventory (push) Has been cancelled
Lint / lint (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Compilation Check (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Manual Policy (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Request Processing (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Summary (push) Has been cancelled
PR Test (SMG) / build-wheel (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on windows (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (x86_64 - auto) (push) Has been cancelled
PR Test (SMG) / python-unit-tests (push) Has been cancelled
PR Test (SMG) / unit-tests (push) Has been cancelled
PR Test (SMG) / benchmarks (push) Has been cancelled
PR Test (SMG) / chat-completions (push) Has been cancelled
PR Test (SMG) / chat-completions-4gpu (push) Has been cancelled
PR Test (SMG) / e2e (push) Has been cancelled
PR Test (SMG) / docker-build-test (push) Has been cancelled
PR Test (SMG) / k8s-integration (push) Has been cancelled
PR Test (SMG) / finish (push) Has been cancelled
PR Test (SMG) / summarize-benchmarks (push) Has been cancelled
Release SGLang Model Gateway Docker Image / publish (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Build SDist (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Upload to PyPI (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (aarch64, 12.9, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (x86_64, 12.9, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu129 (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (aarch64, 13.0, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (x86_64, 13.0, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu130 (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 700) (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 720) (push) Has been cancelled
Release SGLang Kernels / release-rocm700 (push) Has been cancelled
Release SGLang Kernels / release-rocm720 (push) Has been cancelled
Release SGLang Kernels / build-musa43 (43, 3.10) (push) Has been cancelled
Release SGLang Kernels / release-musa43 (push) Has been cancelled
541 lines
16 KiB
Python
541 lines
16 KiB
Python
"""Fused triton kernels for Gemma4 decoder layer operations.
|
|
|
|
Fuses standard RMSNorm + residual-add (+ optional scalar multiply) into
|
|
a single kernel pass to reduce kernel launch overhead.
|
|
"""
|
|
|
|
from typing import Optional
|
|
|
|
import torch
|
|
import triton
|
|
import triton.language as tl
|
|
|
|
|
|
@triton.jit
|
|
def _gemma_rmsnorm_residual_kernel(
|
|
X_ptr,
|
|
W_ptr,
|
|
Residual_ptr,
|
|
Scalar_ptr,
|
|
Out_ptr,
|
|
stride_x,
|
|
stride_r,
|
|
stride_o,
|
|
N,
|
|
eps,
|
|
HAS_SCALAR: tl.constexpr,
|
|
BLOCK_SIZE: tl.constexpr,
|
|
):
|
|
"""Fused kernel: out = rmsnorm(x, w) + residual [* scalar]
|
|
|
|
When HAS_SCALAR is True, also multiplies by a scalar loaded from Scalar_ptr.
|
|
"""
|
|
row = tl.program_id(0)
|
|
cols = tl.arange(0, BLOCK_SIZE)
|
|
mask = cols < N
|
|
|
|
x = tl.load(X_ptr + row * stride_x + cols, mask=mask, other=0.0).to(tl.float32)
|
|
w = tl.load(W_ptr + cols, mask=mask, other=0.0).to(tl.float32)
|
|
r = tl.load(Residual_ptr + row * stride_r + cols, mask=mask, other=0.0).to(
|
|
tl.float32
|
|
)
|
|
|
|
var = tl.sum(x * x, axis=0) / N
|
|
rrms = tl.rsqrt(var + eps)
|
|
out = x * rrms * w + r
|
|
|
|
if HAS_SCALAR:
|
|
scalar = tl.load(Scalar_ptr).to(tl.float32)
|
|
out = out * scalar
|
|
|
|
tl.store(Out_ptr + row * stride_o + cols, out.to(x.dtype), mask=mask)
|
|
|
|
|
|
def gemma_rmsnorm_residual_scalar(
|
|
x: torch.Tensor,
|
|
weight: torch.Tensor,
|
|
residual: torch.Tensor,
|
|
scalar: torch.Tensor,
|
|
eps: float = 1e-6,
|
|
) -> torch.Tensor:
|
|
"""Fused (rmsnorm(x) + residual) * scalar."""
|
|
assert x.dim() == 2 and x.stride(-1) == 1, "Expected contiguous 2D input"
|
|
M, N = x.shape
|
|
BLOCK_SIZE = triton.next_power_of_2(N)
|
|
out = torch.empty_like(x)
|
|
|
|
_gemma_rmsnorm_residual_kernel[(M,)](
|
|
x,
|
|
weight,
|
|
residual,
|
|
scalar,
|
|
out,
|
|
x.stride(0),
|
|
residual.stride(0),
|
|
out.stride(0),
|
|
N,
|
|
eps,
|
|
HAS_SCALAR=True,
|
|
BLOCK_SIZE=BLOCK_SIZE,
|
|
)
|
|
return out
|
|
|
|
|
|
@triton.jit
|
|
def _gemma_dual_rmsnorm_residual_kernel(
|
|
X1_ptr,
|
|
W1_ptr,
|
|
X2_ptr,
|
|
W2_ptr,
|
|
W3_ptr,
|
|
Residual_ptr,
|
|
Scalar_ptr,
|
|
Out_ptr,
|
|
stride_x1,
|
|
stride_x2,
|
|
stride_r,
|
|
stride_o,
|
|
N,
|
|
eps1,
|
|
eps2,
|
|
eps3,
|
|
BLOCK_SIZE: tl.constexpr,
|
|
):
|
|
"""Fused: out = (rmsnorm(rmsnorm(x1,w1) + rmsnorm(x2,w2), w3) + residual) * scalar"""
|
|
row = tl.program_id(0)
|
|
cols = tl.arange(0, BLOCK_SIZE)
|
|
mask = cols < N
|
|
|
|
x1 = tl.load(X1_ptr + row * stride_x1 + cols, mask=mask, other=0.0).to(tl.float32)
|
|
w1 = tl.load(W1_ptr + cols, mask=mask, other=0.0).to(tl.float32)
|
|
x2 = tl.load(X2_ptr + row * stride_x2 + cols, mask=mask, other=0.0).to(tl.float32)
|
|
w2 = tl.load(W2_ptr + cols, mask=mask, other=0.0).to(tl.float32)
|
|
w3 = tl.load(W3_ptr + cols, mask=mask, other=0.0).to(tl.float32)
|
|
r = tl.load(Residual_ptr + row * stride_r + cols, mask=mask, other=0.0).to(
|
|
tl.float32
|
|
)
|
|
|
|
var1 = tl.sum(x1 * x1, axis=0) / N
|
|
norm1 = x1 * tl.rsqrt(var1 + eps1) * w1
|
|
|
|
var2 = tl.sum(x2 * x2, axis=0) / N
|
|
norm2 = x2 * tl.rsqrt(var2 + eps2) * w2
|
|
|
|
combined = norm1 + norm2
|
|
|
|
var3 = tl.sum(combined * combined, axis=0) / N
|
|
norm3 = combined * tl.rsqrt(var3 + eps3) * w3
|
|
|
|
scalar = tl.load(Scalar_ptr).to(tl.float32)
|
|
out = (norm3 + r) * scalar
|
|
|
|
tl.store(Out_ptr + row * stride_o + cols, out.to(x1.dtype), mask=mask)
|
|
|
|
|
|
@triton.jit
|
|
def _gemma_qkv_rmsnorm_store(
|
|
X_ptr,
|
|
W_ptr,
|
|
stride_m,
|
|
m,
|
|
h,
|
|
cols,
|
|
mask,
|
|
HEAD_DIM: tl.constexpr,
|
|
eps,
|
|
HAS_WEIGHT: tl.constexpr,
|
|
):
|
|
off = m * stride_m + h * HEAD_DIM + cols
|
|
x = tl.load(X_ptr + off, mask=mask, other=0.0).to(tl.float32)
|
|
rrms = tl.rsqrt(tl.sum(x * x, axis=0) / HEAD_DIM + eps)
|
|
out = x * rrms
|
|
if HAS_WEIGHT:
|
|
w = tl.load(W_ptr + cols, mask=mask, other=0.0).to(tl.float32)
|
|
out = out * w
|
|
tl.store(X_ptr + off, out.to(X_ptr.dtype.element_ty), mask=mask)
|
|
|
|
|
|
@triton.jit
|
|
def _gemma_qkv_rmsnorm_kernel(
|
|
Q_ptr,
|
|
K_ptr,
|
|
V_ptr,
|
|
Q_w_ptr,
|
|
K_w_ptr,
|
|
stride_q_m,
|
|
stride_k_m,
|
|
stride_v_m,
|
|
NUM_Q_HEADS: tl.constexpr,
|
|
NUM_KV_HEADS: tl.constexpr,
|
|
HEAD_DIM: tl.constexpr,
|
|
eps,
|
|
HAS_KV: tl.constexpr,
|
|
BY_HEAD: tl.constexpr,
|
|
BLOCK: tl.constexpr,
|
|
):
|
|
"""Fused per-head RMSNorm for Q, K, V.
|
|
|
|
The same kernel supports two launch shapes:
|
|
- BY_HEAD=True: grid is (M, total_heads), one program per token/head.
|
|
- BY_HEAD=False: grid is (M,), one program per token looping over heads.
|
|
|
|
Layout assumption: each tensor's last dim packs (num_heads, head_dim)
|
|
contiguously so per-head offset is `h * HEAD_DIM`. The token (M) stride is
|
|
taken from stride_*_m so the kernel works on strided views (e.g. slices of a
|
|
larger qkv buffer produced by `qkv.split`) without requiring `.contiguous()`
|
|
copies. V uses `weight=ones` semantics so the multiply-by-weight is omitted.
|
|
"""
|
|
m = tl.program_id(0)
|
|
cols = tl.arange(0, BLOCK)
|
|
mask = cols < HEAD_DIM
|
|
|
|
if BY_HEAD:
|
|
h_all = tl.program_id(1)
|
|
if h_all < NUM_Q_HEADS:
|
|
_gemma_qkv_rmsnorm_store(
|
|
Q_ptr, Q_w_ptr, stride_q_m, m, h_all, cols, mask, HEAD_DIM, eps, True
|
|
)
|
|
elif HAS_KV and h_all < NUM_Q_HEADS + NUM_KV_HEADS:
|
|
h = h_all - NUM_Q_HEADS
|
|
_gemma_qkv_rmsnorm_store(
|
|
K_ptr, K_w_ptr, stride_k_m, m, h, cols, mask, HEAD_DIM, eps, True
|
|
)
|
|
elif HAS_KV:
|
|
h = h_all - NUM_Q_HEADS - NUM_KV_HEADS
|
|
_gemma_qkv_rmsnorm_store(
|
|
V_ptr, Q_w_ptr, stride_v_m, m, h, cols, mask, HEAD_DIM, eps, False
|
|
)
|
|
else:
|
|
for h in tl.static_range(NUM_Q_HEADS):
|
|
_gemma_qkv_rmsnorm_store(
|
|
Q_ptr, Q_w_ptr, stride_q_m, m, h, cols, mask, HEAD_DIM, eps, True
|
|
)
|
|
|
|
if HAS_KV:
|
|
for h in tl.static_range(NUM_KV_HEADS):
|
|
_gemma_qkv_rmsnorm_store(
|
|
K_ptr,
|
|
K_w_ptr,
|
|
stride_k_m,
|
|
m,
|
|
h,
|
|
cols,
|
|
mask,
|
|
HEAD_DIM,
|
|
eps,
|
|
True,
|
|
)
|
|
|
|
for h in tl.static_range(NUM_KV_HEADS):
|
|
_gemma_qkv_rmsnorm_store(
|
|
V_ptr,
|
|
Q_w_ptr,
|
|
stride_v_m,
|
|
m,
|
|
h,
|
|
cols,
|
|
mask,
|
|
HEAD_DIM,
|
|
eps,
|
|
False,
|
|
)
|
|
|
|
|
|
def gemma_qkv_rmsnorm(
|
|
q: torch.Tensor,
|
|
k: Optional[torch.Tensor],
|
|
v: Optional[torch.Tensor],
|
|
q_weight: torch.Tensor,
|
|
k_weight: Optional[torch.Tensor],
|
|
num_q_heads: int,
|
|
num_kv_heads: int,
|
|
head_dim: int,
|
|
eps: float = 1e-6,
|
|
) -> None:
|
|
"""In-place fused RMSNorm on Q, K, V for Gemma4 attention.
|
|
|
|
All three norms compute `x * rsqrt(mean(x^2) + eps)` independently per head.
|
|
Q is scaled by `q_weight`, K by `k_weight`, V by 1 (Gemma4's V-norm has
|
|
`with_scale=False`).
|
|
|
|
Inputs may be 2D `(M, num_heads * head_dim)` or strided views of a larger
|
|
buffer (such as q/k/v slices from `qkv.split`). The kernel uses the actual
|
|
`stride(0)` so no `.contiguous()` copy is required. Within a token, the
|
|
last dim must be contiguous so heads pack as `h * head_dim` offsets.
|
|
|
|
If k and v are both None (KV-shared layer), only Q is normalized.
|
|
"""
|
|
assert q.is_cuda or q.is_xpu
|
|
assert q.stride(-1) == 1, "Q's last dim must be contiguous"
|
|
assert q_weight.shape[-1] == head_dim
|
|
M = q.shape[0] if q.dim() >= 2 else 1
|
|
BLOCK = triton.next_power_of_2(head_dim)
|
|
|
|
has_kv = k is not None and v is not None
|
|
if has_kv:
|
|
assert (k.is_cuda and v.is_cuda) or (k.is_xpu and v.is_xpu)
|
|
assert k.stride(-1) == 1 and v.stride(-1) == 1
|
|
assert k_weight is not None and k_weight.shape[-1] == head_dim
|
|
|
|
if M <= 256:
|
|
total_heads = num_q_heads + (2 * num_kv_heads if has_kv else 0)
|
|
_gemma_qkv_rmsnorm_kernel[(M, total_heads)](
|
|
q,
|
|
k if has_kv else q,
|
|
v if has_kv else q,
|
|
q_weight,
|
|
k_weight if has_kv else q_weight,
|
|
q.stride(0),
|
|
k.stride(0) if has_kv else 0,
|
|
v.stride(0) if has_kv else 0,
|
|
NUM_Q_HEADS=num_q_heads,
|
|
NUM_KV_HEADS=num_kv_heads if has_kv else 0,
|
|
HEAD_DIM=head_dim,
|
|
eps=eps,
|
|
HAS_KV=has_kv,
|
|
BY_HEAD=True,
|
|
BLOCK=BLOCK,
|
|
)
|
|
return
|
|
|
|
_gemma_qkv_rmsnorm_kernel[(M,)](
|
|
q,
|
|
k if has_kv else q,
|
|
v if has_kv else q,
|
|
q_weight,
|
|
k_weight if has_kv else q_weight,
|
|
q.stride(0),
|
|
k.stride(0) if has_kv else 0,
|
|
v.stride(0) if has_kv else 0,
|
|
NUM_Q_HEADS=num_q_heads,
|
|
NUM_KV_HEADS=num_kv_heads if has_kv else 0,
|
|
HEAD_DIM=head_dim,
|
|
eps=eps,
|
|
HAS_KV=has_kv,
|
|
BY_HEAD=False,
|
|
BLOCK=BLOCK,
|
|
)
|
|
|
|
|
|
@triton.jit
|
|
def _gemma_routing_post_topk_kernel(
|
|
Logits_ptr,
|
|
Ids_ptr,
|
|
Scale_ptr,
|
|
Out_weights_ptr,
|
|
Out_ids_ptr,
|
|
stride_l,
|
|
stride_ow,
|
|
stride_oi,
|
|
K: tl.constexpr,
|
|
BLOCK_K: tl.constexpr,
|
|
):
|
|
"""Fused: softmax(topk_logits) * per_expert_scale[topk_ids] → float32 weights, int32 ids.
|
|
|
|
One program per token. K is the number of top-k experts (e.g. 8).
|
|
"""
|
|
row = tl.program_id(0)
|
|
cols = tl.arange(0, BLOCK_K)
|
|
mask = cols < K
|
|
|
|
logits = tl.load(
|
|
Logits_ptr + row * stride_l + cols, mask=mask, other=float("-inf")
|
|
).to(tl.float32)
|
|
ids_i64 = tl.load(Ids_ptr + row * stride_l + cols, mask=mask, other=0)
|
|
|
|
# Stable softmax
|
|
max_val = tl.max(logits, axis=0)
|
|
exp_val = tl.exp(logits - max_val)
|
|
sum_exp = tl.sum(exp_val, axis=0)
|
|
weights = exp_val / sum_exp
|
|
|
|
# Gather per_expert_scale and multiply
|
|
scale = tl.load(Scale_ptr + ids_i64, mask=mask, other=1.0).to(tl.float32)
|
|
weights = weights * scale
|
|
|
|
tl.store(Out_weights_ptr + row * stride_ow + cols, weights, mask=mask)
|
|
tl.store(Out_ids_ptr + row * stride_oi + cols, ids_i64.to(tl.int32), mask=mask)
|
|
|
|
|
|
def gemma_routing_post_topk(
|
|
topk_logits: torch.Tensor,
|
|
topk_ids: torch.Tensor,
|
|
per_expert_scale: torch.Tensor,
|
|
) -> tuple[torch.Tensor, torch.Tensor]:
|
|
"""Fused softmax + scale-gather + casts for Gemma4 routing.
|
|
|
|
Replaces: softmax(topk_logits) * per_expert_scale[topk_ids] → (f32, i32).
|
|
"""
|
|
B, K = topk_logits.shape
|
|
BLOCK_K = triton.next_power_of_2(K)
|
|
out_weights = torch.empty((B, K), dtype=torch.float32, device=topk_logits.device)
|
|
out_ids = torch.empty((B, K), dtype=torch.int32, device=topk_logits.device)
|
|
|
|
_gemma_routing_post_topk_kernel[(B,)](
|
|
topk_logits,
|
|
topk_ids,
|
|
per_expert_scale,
|
|
out_weights,
|
|
out_ids,
|
|
topk_logits.stride(0),
|
|
out_weights.stride(0),
|
|
out_ids.stride(0),
|
|
K=K,
|
|
BLOCK_K=BLOCK_K,
|
|
)
|
|
return out_weights, out_ids
|
|
|
|
|
|
def gemma_dual_rmsnorm_residual_scalar(
|
|
x1: torch.Tensor,
|
|
weight1: torch.Tensor,
|
|
x2: torch.Tensor,
|
|
weight2: torch.Tensor,
|
|
weight3: torch.Tensor,
|
|
residual: torch.Tensor,
|
|
scalar: torch.Tensor,
|
|
eps1: float = 1e-6,
|
|
eps2: float = 1e-6,
|
|
eps3: float = 1e-6,
|
|
) -> torch.Tensor:
|
|
"""Fused (rmsnorm(rmsnorm(x1,w1) + rmsnorm(x2,w2), w3) + residual) * scalar."""
|
|
assert x1.dim() == 2 and x1.stride(-1) == 1
|
|
M, N = x1.shape
|
|
BLOCK_SIZE = triton.next_power_of_2(N)
|
|
out = torch.empty_like(x1)
|
|
|
|
_gemma_dual_rmsnorm_residual_kernel[(M,)](
|
|
x1,
|
|
weight1,
|
|
x2,
|
|
weight2,
|
|
weight3,
|
|
residual,
|
|
scalar,
|
|
out,
|
|
x1.stride(0),
|
|
x2.stride(0),
|
|
residual.stride(0),
|
|
out.stride(0),
|
|
N,
|
|
eps1,
|
|
eps2,
|
|
eps3,
|
|
BLOCK_SIZE=BLOCK_SIZE,
|
|
)
|
|
return out
|
|
|
|
|
|
@triton.jit
|
|
def _gemma4_routing_kernel(
|
|
gating_ptr, # [T, E] router logits, any float dtype
|
|
per_expert_scale_ptr, # [E] per-expert scale (any float dtype)
|
|
topk_weights_ptr, # [T, K] fp32 out
|
|
topk_ids_ptr, # [T, K] int32 out
|
|
stride_g_t, # stride of gating in the token dim
|
|
E: tl.constexpr,
|
|
K: tl.constexpr,
|
|
BLOCK_E: tl.constexpr,
|
|
):
|
|
pid = tl.program_id(0)
|
|
offs_e = tl.arange(0, BLOCK_E)
|
|
valid = offs_e < E
|
|
|
|
logits = tl.load(
|
|
gating_ptr + pid * stride_g_t + offs_e,
|
|
mask=valid,
|
|
other=-float("inf"),
|
|
).to(tl.float32)
|
|
|
|
# Pack (sort_key, expert_id) into one int64 so a single signed-ascending
|
|
# tl.sort yields logits in descending float order. The key bijection is
|
|
# anti-monotone on the float value, and the <<32 shift moves its high bit
|
|
# into the int64 sign bit. Ties break by expert id ascending. Invalid
|
|
# lanes use a max key so they sort last.
|
|
MIN32 = -2147483648
|
|
logit_bits = logits.to(tl.int32, bitcast=True)
|
|
sign = logit_bits >> 31
|
|
key = tl.where(sign == 0, logit_bits ^ -1, logit_bits ^ MIN32)
|
|
key = tl.where(valid, key, 0x7FFFFFFF)
|
|
sk64 = key.to(tl.int64) & 0x00000000FFFFFFFF
|
|
packed = (sk64 << 32) | offs_e.to(tl.int64)
|
|
|
|
sorted_p = tl.sort(packed, descending=False)
|
|
all_keys = ((sorted_p >> 32) & 0x00000000FFFFFFFF).to(tl.int32)
|
|
all_ids = (sorted_p & 0x00000000FFFFFFFF).to(tl.int32)
|
|
|
|
# Invert the key bijection to recover the original logit value.
|
|
sign_k = all_keys >> 31
|
|
all_bits = tl.where(sign_k < 0, all_keys ^ -1, all_keys ^ MIN32)
|
|
all_logits = all_bits.to(tl.float32, bitcast=True)
|
|
|
|
# softmax over the top-K logits; max sits at index 0 (sorted descending).
|
|
top_mask = offs_e < K
|
|
max_l = tl.max(tl.where(top_mask, all_logits, -float("inf")), axis=0)
|
|
raw_exp = tl.where(top_mask, tl.exp(all_logits - max_l), 0.0)
|
|
|
|
denom = tl.sum(raw_exp, axis=0)
|
|
denom = tl.where(denom > 0.0, denom, 1.0)
|
|
weights = raw_exp / denom
|
|
|
|
scales = tl.load(
|
|
per_expert_scale_ptr + all_ids.to(tl.int64),
|
|
mask=top_mask,
|
|
other=1.0,
|
|
).to(tl.float32)
|
|
weights = weights * scales
|
|
|
|
base_off = pid * K + offs_e
|
|
tl.store(topk_weights_ptr + base_off, weights, mask=top_mask)
|
|
tl.store(topk_ids_ptr + base_off, all_ids, mask=top_mask)
|
|
|
|
|
|
def gemma4_fused_routing(
|
|
gating_output: torch.Tensor,
|
|
per_expert_scale: torch.Tensor,
|
|
topk: int,
|
|
) -> tuple[torch.Tensor, torch.Tensor]:
|
|
"""One-launch Gemma4 router.
|
|
|
|
Args:
|
|
gating_output: [T, E] router logits in any floating dtype; will be
|
|
cast to fp32 inside the kernel.
|
|
per_expert_scale: [E] per-expert scale, any floating dtype.
|
|
topk: number of experts to keep per token.
|
|
|
|
Returns:
|
|
topk_weights: [T, topk] fp32 (matches SGLang TopK contract).
|
|
topk_ids: [T, topk] int32 (matches SGLang TopK contract).
|
|
"""
|
|
assert gating_output.dim() == 2, "expected [T, E] router logits"
|
|
assert per_expert_scale.dim() == 1
|
|
assert per_expert_scale.shape[0] == gating_output.shape[1]
|
|
T, E = gating_output.shape
|
|
assert topk <= E, f"topk ({topk}) must be <= E ({E})"
|
|
assert E <= 1024, f"gemma4_fused_routing only supports E<=1024, got E={E}"
|
|
|
|
gating_output = gating_output.contiguous()
|
|
per_expert_scale = per_expert_scale.contiguous()
|
|
|
|
BLOCK_E = triton.next_power_of_2(E)
|
|
topk_weights = torch.empty(
|
|
(T, topk), dtype=torch.float32, device=gating_output.device
|
|
)
|
|
topk_ids = torch.empty((T, topk), dtype=torch.int32, device=gating_output.device)
|
|
|
|
if T == 0:
|
|
return topk_weights, topk_ids
|
|
|
|
_gemma4_routing_kernel[(T,)](
|
|
gating_output,
|
|
per_expert_scale,
|
|
topk_weights,
|
|
topk_ids,
|
|
gating_output.stride(0),
|
|
E,
|
|
topk,
|
|
BLOCK_E,
|
|
num_warps=1,
|
|
)
|
|
return topk_weights, topk_ids
|