Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:38:16 +08:00

1604 lines
56 KiB
Python

import functools
import importlib
import logging
import math
import threading
from typing import Tuple
import torch
from sglang.jit_kernel.utils import is_arch_support_pdl
from sglang.srt.environ import envs
from sglang.srt.layers.attention.dsa.utils import is_dsa_prefill_cp_round_robin_split
from sglang.srt.layers.utils.common import strict_contiguous
logger = logging.getLogger(__name__)
# This module is imported during model-registry discovery. Do not import the real
# TileLang package here: it loads native CUDA stubs. The proxy below lets
# module-level @tilelang.jit declarations parse, then imports and applies real
# TileLang only when a TileLang MHC kernel is actually called.
_real_tilelang = None
_real_T = None
_tilelang_load_lock = threading.Lock()
class _LazyTilelangAttr:
def __init__(self, path: Tuple[str, ...] = ()):
self.path = path
def __getattr__(self, name):
return _LazyTilelangAttr((*self.path, name))
def __call__(self, *_args, **_kwargs):
return _LazyTilelangAttr(self.path)
def _resolve_lazy_tilelang_value(value):
if isinstance(value, _LazyTilelangAttr):
obj = _load_tilelang()
for name in value.path:
obj = getattr(obj, name)
return obj
if isinstance(value, dict):
return {
_resolve_lazy_tilelang_value(k): _resolve_lazy_tilelang_value(v)
for k, v in value.items()
}
# Keep list/tuple support so future TileLang jit kwargs such as out_idx=[...]
# can use lazy TileLang enum values without changing the proxy.
if isinstance(value, list):
return [_resolve_lazy_tilelang_value(v) for v in value]
if isinstance(value, tuple):
return tuple(_resolve_lazy_tilelang_value(v) for v in value)
return value
def _load_tilelang():
global _real_tilelang, _real_T, tilelang, T
if _real_tilelang is None:
with _tilelang_load_lock:
if _real_tilelang is None:
try:
new_tilelang = importlib.import_module("tilelang")
new_T = importlib.import_module("tilelang.language")
except ImportError as exc:
raise RuntimeError(
"tilelang is not installed; this kernel cannot run on the current platform"
) from exc
new_tilelang.set_log_level("WARNING")
tilelang = new_tilelang
T = new_T
_real_T = new_T
_real_tilelang = new_tilelang
return _real_tilelang
class _LazyTilelang:
PassConfigKey = _LazyTilelangAttr(("PassConfigKey",))
layout = _LazyTilelangAttr(("layout",))
def jit(self, func=None, **jit_kwargs):
def decorate(fn):
compiled = None
compile_lock = threading.Lock()
@functools.wraps(fn)
def wrapper(*args, **kwargs):
nonlocal compiled
if compiled is None:
with compile_lock:
if compiled is None:
real_tilelang = _load_tilelang()
real_kwargs = _resolve_lazy_tilelang_value(jit_kwargs)
compiled = real_tilelang.jit(**real_kwargs)(fn)
return compiled(*args, **kwargs)
return wrapper
if callable(func):
return decorate(func)
return decorate
def __getattr__(self, name):
return _LazyTilelangAttr((name,))
tilelang = _LazyTilelang()
T = _LazyTilelangAttr()
pass_configs = {
tilelang.PassConfigKey.TL_DISABLE_WARP_SPECIALIZED: True,
tilelang.PassConfigKey.TL_DISABLE_TMA_LOWER: True,
}
FP8 = "float8_e4m3"
BF16 = "bfloat16"
FP32 = "float32"
INT32 = "int32"
@tilelang.jit(pass_configs=pass_configs)
def hc_split_sinkhorn_kernel(hc: int, sinkhorn_iters: int, eps: float):
n = T.symbolic("n")
mix_hc = (2 + hc) * hc
threads = 64
ENABLE_PDL = is_arch_support_pdl()
@T.prim_func
def hc_split_sinkhorn_kernel_(
mixes: T.Tensor[(n, mix_hc), FP32],
hc_scale: T.Tensor[(3,), T.float32],
hc_base: T.Tensor[(mix_hc,), T.float32],
pre: T.Tensor[(n, hc), FP32],
post: T.Tensor[(n, hc), FP32],
comb: T.Tensor[(n, hc, hc), FP32],
):
with T.Kernel(n, threads=threads) as i:
if ENABLE_PDL:
T.pdl_sync()
mixes_shared = T.alloc_shared(mix_hc, FP32)
comb_frag = T.alloc_fragment((hc, hc), FP32)
T.copy(mixes[i, :], mixes_shared)
for j in T.Parallel(hc):
pre[i, j] = T.sigmoid(mixes_shared[j] * hc_scale[0] + hc_base[j]) + eps
for j in T.Parallel(hc):
post[i, j] = 2 * T.sigmoid(
mixes_shared[j + hc] * hc_scale[1] + hc_base[j + hc]
)
for j, k in T.Parallel(hc, hc):
comb_frag[j, k] = (
mixes_shared[j * hc + k + hc * 2] * hc_scale[2]
+ hc_base[j * hc + k + hc * 2]
)
row_sum = T.alloc_fragment(hc, FP32)
col_sum = T.alloc_fragment(hc, FP32)
row_max = T.alloc_fragment(hc, FP32)
T.reduce_max(comb_frag, row_max, dim=1)
for j, k in T.Parallel(hc, hc):
comb_frag[j, k] = T.exp(comb_frag[j, k] - row_max[j])
T.reduce_sum(comb_frag, row_sum, dim=1)
for j, k in T.Parallel(hc, hc):
comb_frag[j, k] = comb_frag[j, k] / row_sum[j] + eps
T.reduce_sum(comb_frag, col_sum, dim=0)
for j, k in T.Parallel(hc, hc):
comb_frag[j, k] = comb_frag[j, k] / (col_sum[k] + eps)
for _ in T.serial(sinkhorn_iters - 1):
T.reduce_sum(comb_frag, row_sum, dim=1)
for j, k in T.Parallel(hc, hc):
comb_frag[j, k] = comb_frag[j, k] / (row_sum[j] + eps)
T.reduce_sum(comb_frag, col_sum, dim=0)
for j, k in T.Parallel(hc, hc):
comb_frag[j, k] = comb_frag[j, k] / (col_sum[k] + eps)
T.copy(comb_frag, comb[i, :, :])
if ENABLE_PDL:
T.pdl_trigger()
return hc_split_sinkhorn_kernel_
def hc_split_sinkhorn(
mixes: torch.Tensor,
hc_scale: torch.Tensor,
hc_base: torch.Tensor,
hc_mult: int = 4,
sinkhorn_iters: int = 20,
eps: float = 1e-6,
):
b, s, _ = mixes.size()
pre = mixes.new_empty(b, s, hc_mult)
post = mixes.new_empty(b, s, hc_mult)
comb = mixes.new_empty(b, s, hc_mult, hc_mult)
kernel = hc_split_sinkhorn_kernel(hc_mult, sinkhorn_iters, eps)
kernel(
mixes.view(-1, (2 + hc_mult) * hc_mult),
hc_scale,
hc_base,
pre.view(-1, hc_mult),
post.view(-1, hc_mult),
comb.view(-1, hc_mult, hc_mult),
)
return pre, post, comb
@tilelang.jit(
pass_configs={
tilelang.PassConfigKey.TL_DISABLE_WARP_SPECIALIZED: True,
tilelang.PassConfigKey.TL_DISABLE_TMA_LOWER: True,
tilelang.PassConfigKey.TL_PTXAS_REGISTER_USAGE_LEVEL: 10,
},
)
def mhc_pre_big_fuse_tilelang(
gemm_out_mul,
gemm_out_sqrsum,
hc_scale,
hc_base,
residual,
post_mix,
comb_mix,
layer_input,
hidden_size: int,
rms_eps: float,
hc_pre_eps: float,
hc_sinkhorn_eps: float,
hc_post_mult_value: float,
sinkhorn_repeat: int,
n_splits: int = 16,
hc_mult: int = 4,
gemm_last_dim: int = -1,
):
num_tokens = T.dynamic("num_tokens")
hc_mult3 = hc_mult * (2 + hc_mult)
if gemm_last_dim < 0:
gemm_last_dim = hc_mult3
hidden_block = math.gcd(512, hidden_size)
gemm_out_mul: T.Tensor[[n_splits, num_tokens, gemm_last_dim], T.float32]
gemm_out_sqrsum: T.Tensor[[n_splits, num_tokens], T.float32]
hc_scale: T.Tensor[[3], T.float32]
hc_base: T.Tensor[[hc_mult3], T.float32]
residual: T.Tensor[[num_tokens, hc_mult, hidden_size], T.bfloat16]
post_mix: T.Tensor[[num_tokens, hc_mult], T.float32]
comb_mix: T.Tensor[[num_tokens, hc_mult * hc_mult], T.float32]
layer_input: T.Tensor[[num_tokens, hidden_size], T.bfloat16]
ENABLE_PDL = is_arch_support_pdl()
with T.Kernel(num_tokens, threads=96) as i:
rms = T.alloc_fragment(1, T.float32)
mixes = T.alloc_fragment(hc_mult3, T.float32)
T.clear(mixes)
rms[0] = 0
if ENABLE_PDL:
T.pdl_sync()
for i_split in T.serial(n_splits):
rms[0] += gemm_out_sqrsum[i_split, i]
rms[0] = T.rsqrt(rms[0] / (hc_mult * hidden_size) + rms_eps)
for j in T.Parallel(hc_mult3):
mixes[j] = 0
for i_split in T.serial(n_splits):
mixes[j] += gemm_out_mul[i_split, i, j]
mixes[j] *= rms[0]
mixes_shared = T.alloc_shared(hc_mult3, T.float32)
T.copy(mixes, mixes_shared)
if T.get_thread_binding() < 32:
cm = T.alloc_fragment((hc_mult, hc_mult), T.float32)
for j in T.Parallel(hc_mult):
post_mix[i, j] = (
T.sigmoid(
mixes_shared[j + hc_mult] * hc_scale[1] + hc_base[j + hc_mult]
)
* hc_post_mult_value
)
for j, k in T.Parallel(hc_mult, hc_mult):
cm[j, k] = (
mixes_shared[j * hc_mult + k + hc_mult * 2] * hc_scale[2]
+ hc_base[j * hc_mult + k + hc_mult * 2]
)
row_sum = T.alloc_fragment(hc_mult, T.float32)
col_sum = T.alloc_fragment(hc_mult, T.float32)
row_max = T.alloc_fragment(hc_mult, T.float32)
T.reduce_max(cm, row_max, dim=1)
for j, k in T.Parallel(hc_mult, hc_mult):
cm[j, k] = T.exp(cm[j, k] - row_max[j])
T.reduce_sum(cm, row_sum, dim=1)
for j, k in T.Parallel(hc_mult, hc_mult):
cm[j, k] = cm[j, k] / row_sum[j] + hc_sinkhorn_eps
T.reduce_sum(cm, col_sum, dim=0)
for j, k in T.Parallel(hc_mult, hc_mult):
cm[j, k] = cm[j, k] / (col_sum[k] + hc_sinkhorn_eps)
for _ in T.serial(sinkhorn_repeat - 1):
T.reduce_sum(cm, row_sum, dim=1)
for j, k in T.Parallel(hc_mult, hc_mult):
cm[j, k] = cm[j, k] / (row_sum[j] + hc_sinkhorn_eps)
T.reduce_sum(cm, col_sum, dim=0)
for j, k in T.Parallel(hc_mult, hc_mult):
cm[j, k] = cm[j, k] / (col_sum[k] + hc_sinkhorn_eps)
for j, k in T.Parallel(hc_mult, hc_mult):
comb_mix[i, j * hc_mult + k] = cm[j, k]
else:
pre_mix_shared = T.alloc_shared(hc_mult, T.float32)
for j in T.Parallel(hc_mult):
pre_mix_shared[j] = (
T.sigmoid(
mixes_shared[j] * hc_scale[0] + hc_base[j],
)
+ hc_pre_eps
)
for i0_h in T.Pipelined(hidden_size // hidden_block, num_stages=2):
xs = T.alloc_shared((hc_mult, hidden_block), T.float32)
xl = T.alloc_fragment((hc_mult, hidden_block), T.float32)
T.copy(residual[i, 0, i0_h * hidden_block], xs)
T.copy(xs, xl)
ol = T.alloc_fragment(hidden_block, T.float32)
T.clear(ol)
for i_hc in T.serial(hc_mult):
pre = pre_mix_shared[i_hc]
for i1_h in T.Parallel(hidden_block):
ol[i1_h] += pre * xl[i_hc, i1_h]
T.copy(ol, layer_input[i, i0_h * hidden_block])
if ENABLE_PDL:
T.pdl_trigger()
@tilelang.jit
def mhc_pre_gemm_sqrsum_tilelang(
x,
fn,
out,
sqrsum,
hc_mult3: int,
hc_hidden_size: int,
token_block: int = 32,
hidden_block: int = 256,
):
assert hc_mult3 <= 32
num_tokens = T.dynamic("num_tokens")
assert hc_hidden_size % hidden_block == 0
x: T.Tensor((num_tokens, hc_hidden_size), T.bfloat16)
fn: T.Tensor((hc_mult3, hc_hidden_size), T.float32)
out: T.Tensor((num_tokens, hc_mult3), T.float32)
sqrsum: T.Tensor((num_tokens), T.float32)
ENABLE_PDL = is_arch_support_pdl()
with T.Kernel(T.ceildiv(num_tokens, token_block)) as px:
out_frag = T.alloc_fragment((token_block, 32), T.float32)
sqrsum_part = T.alloc_fragment((token_block, 4), T.float32)
T.clear(out_frag)
T.clear(sqrsum_part)
if ENABLE_PDL:
T.pdl_sync()
for pz in T.Pipelined(hc_hidden_size // hidden_block, num_stages=2):
x_smem_16 = T.alloc_shared((token_block, hidden_block), T.bfloat16)
fn_smem = T.alloc_shared((32, hidden_block), T.float32)
T.annotate_layout(
{x_smem_16: tilelang.layout.make_swizzled_layout(x_smem_16)}
)
T.copy(x[px * token_block, pz * hidden_block], x_smem_16)
T.copy(fn[0, pz * hidden_block], fn_smem)
x_frag_16 = T.alloc_fragment((token_block, hidden_block), T.bfloat16)
T.copy(x_smem_16, x_frag_16)
x_frag = T.alloc_fragment((token_block, hidden_block), T.float32)
T.copy(x_frag_16, x_frag)
for jj in T.serial(hidden_block // 4):
for i, j in T.Parallel(token_block, 4):
sqrsum_part[i, j] += x_frag[i, jj * 4 + j] * x_frag[i, jj * 4 + j]
T.gemm(
x_frag,
fn_smem,
out_frag,
transpose_A=False,
transpose_B=True,
clear_accum=False,
)
sqrsum_l = T.alloc_fragment(token_block, T.float32)
T.reduce_sum(sqrsum_part, sqrsum_l)
for i in T.Parallel(token_block):
sqrsum[px * token_block + i] = sqrsum_l[i]
for i, j in T.Parallel(token_block, 32):
if j < hc_mult3:
out[px * token_block + i, j] = out_frag[i, j]
if ENABLE_PDL:
T.pdl_trigger()
@functools.cache
def mhc_pre_gemm_sqrsum_splitk_kernel(
hc_mult3: int,
hc_hidden_size: int,
split_k: int,
token_block: int = 32,
hidden_block: int = 256,
threads: int = 128,
):
_load_tilelang()
assert hc_mult3 <= 32
assert hc_hidden_size % hidden_block == 0
assert hc_hidden_size % split_k == 0
split_size = hc_hidden_size // split_k
assert split_size % hidden_block == 0
num_tokens = T.dynamic("num_tokens")
ENABLE_PDL = is_arch_support_pdl()
@tilelang.jit
def mhc_pre_gemm_sqrsum_splitk_stage_0(
x: T.Tensor[(num_tokens, hc_hidden_size), T.bfloat16],
fn: T.Tensor[(hc_mult3, hc_hidden_size), T.float32],
out_partial: T.Tensor[(split_k, num_tokens, 32), T.float32],
sqrsum_partial: T.Tensor[(split_k, num_tokens), T.float32],
):
with T.Kernel(T.ceildiv(num_tokens, token_block), split_k, threads=threads) as (
px,
bz,
):
out_frag = T.alloc_fragment((token_block, 32), T.float32)
sq_part4 = T.alloc_fragment((token_block, 4), T.float32)
T.clear(out_frag)
T.clear(sq_part4)
k_base = bz * split_size
if ENABLE_PDL:
T.pdl_sync()
for pz in T.Pipelined(split_size // hidden_block, num_stages=2):
x_smem = T.alloc_shared((token_block, hidden_block), T.bfloat16)
fn_smem = T.alloc_shared((32, hidden_block), T.float32)
T.annotate_layout(
{x_smem: tilelang.layout.make_swizzled_layout(x_smem)}
)
T.copy(x[px * token_block, k_base + pz * hidden_block], x_smem)
T.copy(fn[0, k_base + pz * hidden_block], fn_smem)
x_f16 = T.alloc_fragment((token_block, hidden_block), T.bfloat16)
T.copy(x_smem, x_f16)
x_f = T.alloc_fragment((token_block, hidden_block), T.float32)
T.copy(x_f16, x_f)
for jj in T.serial(hidden_block // 4):
for i, j in T.Parallel(token_block, 4):
v = x_f[i, jj * 4 + j]
sq_part4[i, j] += v * v
T.gemm(
x_f,
fn_smem,
out_frag,
transpose_A=False,
transpose_B=True,
clear_accum=False,
)
sq_l = T.alloc_fragment((token_block,), T.float32)
T.reduce_sum(sq_part4, sq_l)
for i in T.Parallel(token_block):
t = px * token_block + i
if t < num_tokens:
sqrsum_partial[bz, t] = sq_l[i]
for i, j in T.Parallel(token_block, 32):
t = px * token_block + i
if t < num_tokens:
out_partial[bz, t, j] = out_frag[i, j]
if ENABLE_PDL:
T.pdl_trigger()
@tilelang.jit
def mhc_pre_gemm_sqrsum_splitk_stage_1(
out_partial: T.Tensor[(split_k, num_tokens, 32), T.float32],
sqrsum_partial: T.Tensor[(split_k, num_tokens), T.float32],
out: T.Tensor[(num_tokens, hc_mult3), T.float32],
sqrsum: T.Tensor[(num_tokens,), T.float32],
):
warps_per_cta = threads // 32
num_reduce = T.ceildiv(split_k, 32)
with T.Kernel(T.ceildiv(num_tokens, warps_per_cta), threads=threads) as (px,):
tx = T.get_thread_binding()
warp = tx // 32
lane = tx % 32
t = px * warps_per_cta + warp
s = T.alloc_local((1,), T.float32)
acc = T.alloc_local((1,), T.float32)
s[0] = 0
acc[0] = 0
if ENABLE_PDL:
T.pdl_sync()
if t < num_tokens:
for r in T.serial(num_reduce):
bz = r * 32 + lane
s[0] += T.if_then_else(bz < split_k, sqrsum_partial[bz, t], 0.0)
sqrsum[t] = T.warp_reduce_sum(s[0])
if lane < hc_mult3:
for bz in T.serial(split_k):
acc[0] += out_partial[bz, t, lane]
out[t, lane] = acc[0]
if ENABLE_PDL:
T.pdl_trigger()
return (
mhc_pre_gemm_sqrsum_splitk_stage_0,
mhc_pre_gemm_sqrsum_splitk_stage_1,
)
def _compute_num_split_for_mhc_pre(num_tokens: int, hc_hidden_size: int) -> int:
block_m, block_k = 64, 64
grid_size = (num_tokens + block_m - 1) // block_m
num_block_k = (hc_hidden_size + block_k - 1) // block_k
n_sms = torch.cuda.get_device_properties(0).multi_processor_count
return max(1, min(n_sms // max(grid_size, 1), num_block_k // 4))
def get_mhc_pre_token_count_representatives(
max_num_tokens: int, hc_hidden_size: int
) -> Tuple[int, ...]:
"""One representative token count per distinct mhc_pre n_splits bucket over
[1, max_num_tokens] (the kernel is specialized only by n_splits)."""
reps = {}
for grid in range(1, (max(1, max_num_tokens) + 63) // 64 + 1):
num_tokens = min(grid * 64, max_num_tokens)
reps[_compute_num_split_for_mhc_pre(num_tokens, hc_hidden_size)] = num_tokens
return tuple(sorted(reps.values()))
def prewarm_mhc_pre(
residual: torch.Tensor,
fn: torch.Tensor,
hc_scale: torch.Tensor,
hc_base: torch.Tensor,
rms_eps: float,
hc_pre_eps: float,
hc_sinkhorn_eps: float,
hc_post_mult_value: float,
sinkhorn_repeat: int,
n_splits: int,
n_splits_pre: int,
norm_weight: torch.Tensor | None,
norm_eps: float | None,
):
"""Compile the prenorm kernel for every n_splits bucket by replaying the
prenorm with the call's real weights. The compiled kernels are written to
the TileLang/DeepGEMM on-disk JIT cache, so this cost is paid only on a cold
cache; later server runs hit the cache. Driven once per process from load_weights.
"""
from sglang.srt.runtime_context import get_server_args
hc_mult, hidden_size = residual.shape[-2], residual.shape[-1]
max_num_tokens = get_server_args().chunked_prefill_size
buckets = get_mhc_pre_token_count_representatives(
max_num_tokens, hc_mult * hidden_size
)
logger.info("DeepSeek V4 MHC prenorm prewarm: %d n_splits buckets", len(buckets))
with torch.inference_mode():
for num_tokens in buckets:
mhc_pre(
residual.new_zeros(num_tokens, hc_mult, hidden_size),
fn,
hc_scale,
hc_base,
rms_eps,
hc_pre_eps,
hc_sinkhorn_eps,
hc_post_mult_value,
sinkhorn_repeat,
n_splits,
n_splits_pre,
norm_weight=norm_weight,
norm_eps=norm_eps,
)
@tilelang.jit(
pass_configs={
tilelang.PassConfigKey.TL_DISABLE_WARP_SPECIALIZED: True,
tilelang.PassConfigKey.TL_DISABLE_TMA_LOWER: True,
tilelang.PassConfigKey.TL_PTXAS_REGISTER_USAGE_LEVEL: 10,
},
)
def mhc_pre_big_fuse_with_norm_tilelang(
gemm_out_mul,
gemm_out_sqrsum,
hc_scale,
hc_base,
residual,
post_mix,
comb_mix,
layer_input,
norm_weight,
hidden_size: int,
rms_eps: float,
hc_pre_eps: float,
hc_sinkhorn_eps: float,
hc_post_mult_value: float,
sinkhorn_repeat: int,
norm_eps: float,
n_splits: int = 16,
hc_mult: int = 4,
gemm_last_dim: int = -1,
):
"""Fused mhc_pre big_fuse + RMSNorm of layer_input.
Identical to mhc_pre_big_fuse_tilelang for the (post_mix, comb_mix) path.
For the layer_input path, the weighted-sum result is stashed in shared
memory while accumulating sum_sq, then a second pipelined sweep applies
rsqrt(sum_sq/D + norm_eps) * norm_weight before writing to HBM.
"""
num_tokens = T.dynamic("num_tokens")
hc_mult3 = hc_mult * (2 + hc_mult)
if gemm_last_dim < 0:
gemm_last_dim = hc_mult3
hidden_block = math.gcd(1024, hidden_size)
gemm_out_mul: T.Tensor[[n_splits, num_tokens, gemm_last_dim], T.float32]
gemm_out_sqrsum: T.Tensor[[n_splits, num_tokens], T.float32]
hc_scale: T.Tensor[[3], T.float32]
hc_base: T.Tensor[[hc_mult3], T.float32]
residual: T.Tensor[[num_tokens, hc_mult, hidden_size], T.bfloat16]
post_mix: T.Tensor[[num_tokens, hc_mult], T.float32]
comb_mix: T.Tensor[[num_tokens, hc_mult * hc_mult], T.float32]
layer_input: T.Tensor[[num_tokens, hidden_size], T.bfloat16]
norm_weight: T.Tensor[[hidden_size], T.bfloat16]
ENABLE_PDL = is_arch_support_pdl()
with T.Kernel(num_tokens, threads=96) as i:
rms = T.alloc_fragment(1, T.float32)
mixes = T.alloc_fragment(hc_mult3, T.float32)
T.clear(mixes)
rms[0] = 0
if ENABLE_PDL:
T.pdl_sync()
for i_split in T.serial(n_splits):
rms[0] += gemm_out_sqrsum[i_split, i]
rms[0] = T.rsqrt(rms[0] / (hc_mult * hidden_size) + rms_eps)
for j in T.Parallel(hc_mult3):
mixes[j] = 0
for i_split in T.serial(n_splits):
mixes[j] += gemm_out_mul[i_split, i, j]
mixes[j] *= rms[0]
mixes_shared = T.alloc_shared(hc_mult3, T.float32)
T.copy(mixes, mixes_shared)
if T.get_thread_binding() < 32:
cm = T.alloc_fragment((hc_mult, hc_mult), T.float32)
for j in T.Parallel(hc_mult):
post_mix[i, j] = (
T.sigmoid(
mixes_shared[j + hc_mult] * hc_scale[1] + hc_base[j + hc_mult]
)
* hc_post_mult_value
)
for j, k in T.Parallel(hc_mult, hc_mult):
cm[j, k] = (
mixes_shared[j * hc_mult + k + hc_mult * 2] * hc_scale[2]
+ hc_base[j * hc_mult + k + hc_mult * 2]
)
row_sum = T.alloc_fragment(hc_mult, T.float32)
col_sum = T.alloc_fragment(hc_mult, T.float32)
row_max = T.alloc_fragment(hc_mult, T.float32)
T.reduce_max(cm, row_max, dim=1)
for j, k in T.Parallel(hc_mult, hc_mult):
cm[j, k] = T.exp(cm[j, k] - row_max[j])
T.reduce_sum(cm, row_sum, dim=1)
for j, k in T.Parallel(hc_mult, hc_mult):
cm[j, k] = cm[j, k] / row_sum[j] + hc_sinkhorn_eps
T.reduce_sum(cm, col_sum, dim=0)
for j, k in T.Parallel(hc_mult, hc_mult):
cm[j, k] = cm[j, k] / (col_sum[k] + hc_sinkhorn_eps)
for _ in T.serial(sinkhorn_repeat - 1):
T.reduce_sum(cm, row_sum, dim=1)
for j, k in T.Parallel(hc_mult, hc_mult):
cm[j, k] = cm[j, k] / (row_sum[j] + hc_sinkhorn_eps)
T.reduce_sum(cm, col_sum, dim=0)
for j, k in T.Parallel(hc_mult, hc_mult):
cm[j, k] = cm[j, k] / (col_sum[k] + hc_sinkhorn_eps)
for j, k in T.Parallel(hc_mult, hc_mult):
comb_mix[i, j * hc_mult + k] = cm[j, k]
else:
pre_mix_shared = T.alloc_shared(hc_mult, T.float32)
for j in T.Parallel(hc_mult):
pre_mix_shared[j] = (
T.sigmoid(
mixes_shared[j] * hc_scale[0] + hc_base[j],
)
+ hc_pre_eps
)
# Stash unnormalized weighted-sum output in shared memory as bf16
# (matches the rounding the reference path does when RMSNorm reads bf16).
output_shared = T.alloc_shared(hidden_size, T.bfloat16)
sumsq_per_pos = T.alloc_fragment(hidden_block, T.float32)
T.clear(sumsq_per_pos)
for i0_h in T.Pipelined(hidden_size // hidden_block, num_stages=3):
xs = T.alloc_shared((hc_mult, hidden_block), T.bfloat16)
xl = T.alloc_fragment((hc_mult, hidden_block), T.float32)
T.copy(residual[i, 0, i0_h * hidden_block], xs)
T.copy(xs, xl)
ol = T.alloc_fragment(hidden_block, T.float32)
T.clear(ol)
for i_hc in T.serial(hc_mult):
pre = pre_mix_shared[i_hc]
for i1_h in T.Parallel(hidden_block):
ol[i1_h] += pre * xl[i_hc, i1_h]
for i1_h in T.Parallel(hidden_block):
sumsq_per_pos[i1_h] += ol[i1_h] * ol[i1_h]
output_shared[i0_h * hidden_block + i1_h] = T.bfloat16(ol[i1_h])
sumsq = T.alloc_fragment(1, T.float32)
T.reduce_sum(sumsq_per_pos, sumsq, dim=0)
rsqrt_norm = T.alloc_fragment(1, T.float32)
rsqrt_norm[0] = T.rsqrt(sumsq[0] / hidden_size + norm_eps)
for i0_h in T.Pipelined(hidden_size // hidden_block, num_stages=2):
w_shared = T.alloc_shared(hidden_block, T.bfloat16)
w_local = T.alloc_fragment(hidden_block, T.float32)
T.copy(norm_weight[i0_h * hidden_block], w_shared)
T.copy(w_shared, w_local)
ol = T.alloc_fragment(hidden_block, T.float32)
for i1_h in T.Parallel(hidden_block):
ol[i1_h] = (
output_shared[i0_h * hidden_block + i1_h]
* rsqrt_norm[0]
* w_local[i1_h]
)
T.copy(ol, layer_input[i, i0_h * hidden_block])
if ENABLE_PDL:
T.pdl_trigger()
def mhc_pre(
residual: torch.Tensor,
fn: torch.Tensor,
hc_scale: torch.Tensor,
hc_base: torch.Tensor,
rms_eps: float,
hc_pre_eps: float,
hc_sinkhorn_eps: float,
hc_post_mult_value: float,
sinkhorn_repeat: int,
n_splits: int = 1,
n_splits_pre: int = 32,
*,
norm_weight: torch.Tensor | None = None,
norm_eps: float | None = None,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
assert residual.dtype == torch.bfloat16
assert fn.dtype == torch.float32
assert hc_scale.dtype == torch.float32
assert hc_base.dtype == torch.float32
hc_mult = residual.shape[-2]
hidden_size = residual.shape[-1]
hc_mult2 = hc_mult * hc_mult
hc_mult3 = hc_mult * 2 + hc_mult2
hc_hidden_size = hc_mult * hidden_size
assert fn.shape[0] == hc_mult3
assert fn.shape[1] == hc_hidden_size
assert hc_scale.shape == (3,)
assert hc_base.shape == (hc_mult3,)
outer_shape = residual.shape[:-2]
residual_flat = residual.view(-1, hc_mult, hidden_size)
num_tokens = residual_flat.shape[0]
fn_flat = fn
post_mix = torch.empty(
num_tokens, hc_mult, dtype=torch.float32, device=residual.device
)
comb_mix = torch.empty(
num_tokens, hc_mult2, dtype=torch.float32, device=residual.device
)
layer_input = torch.empty(
num_tokens, hidden_size, dtype=torch.bfloat16, device=residual.device
)
if envs.SGLANG_OPT_DEEPGEMM_HC_PRENORM.get():
n_splits = _compute_num_split_for_mhc_pre(num_tokens, hc_hidden_size)
gemm_out_mul = torch.empty(
n_splits, num_tokens, hc_mult3, dtype=torch.float32, device=residual.device
)
gemm_out_sqrsum = torch.empty(
n_splits, num_tokens, dtype=torch.float32, device=residual.device
)
from sglang.srt.layers.deep_gemm_wrapper.entrypoint import tf32_hc_prenorm_gemm
tf32_hc_prenorm_gemm(
residual_flat.view(num_tokens, hc_hidden_size),
fn_flat,
gemm_out_mul,
gemm_out_sqrsum,
n_splits,
)
gemm_last_dim = hc_mult3
big_fuse_n_splits = n_splits
else:
if num_tokens <= 2048:
assert n_splits == 1
if hc_hidden_size == 16384:
hidden_block = 256
elif hc_hidden_size == 28672:
hidden_block = 128
else:
raise NotImplementedError(
f"mhc_pre splitk kernel only supports hc_hidden_size in {{16384, 28672}}, "
f"got {hc_hidden_size}"
)
kernel_0, _ = mhc_pre_gemm_sqrsum_splitk_kernel(
hc_mult3,
hc_hidden_size,
split_k=n_splits_pre,
token_block=32,
hidden_block=hidden_block,
)
partial_out = torch.empty(
n_splits_pre,
num_tokens,
32,
dtype=torch.float32,
device=residual.device,
)
partial_sqrsum = torch.empty(
n_splits_pre, num_tokens, dtype=torch.float32, device=residual.device
)
kernel_0(
residual_flat.view(num_tokens, hc_hidden_size),
fn_flat,
partial_out,
partial_sqrsum,
)
# Stage_1 reduction is folded into big_fuse below; skip launching it.
gemm_out_mul = partial_out
gemm_out_sqrsum = partial_sqrsum
gemm_last_dim = 32
big_fuse_n_splits = n_splits_pre
else:
gemm_out_mul = torch.empty(
n_splits,
num_tokens,
hc_mult3,
dtype=torch.float32,
device=residual.device,
)
gemm_out_sqrsum = torch.empty(
n_splits, num_tokens, dtype=torch.float32, device=residual.device
)
assert (
n_splits == 1
), "The simple TileLang version gemm_sqrsum doesn't support split-k"
mhc_pre_gemm_sqrsum_tilelang(
residual_flat.view(num_tokens, hc_mult * hidden_size),
fn_flat,
gemm_out_mul.squeeze(0),
gemm_out_sqrsum.squeeze(0),
hc_mult3,
hc_mult * hidden_size,
)
gemm_last_dim = hc_mult3
big_fuse_n_splits = n_splits
if norm_weight is not None:
assert norm_eps is not None, "norm_eps required when norm_weight is provided"
assert norm_weight.shape == (
hidden_size,
), f"norm_weight shape {tuple(norm_weight.shape)} != (hidden_size={hidden_size},)"
norm_weight_bf = (
norm_weight.bfloat16()
if norm_weight.dtype != torch.bfloat16
else norm_weight
)
if not norm_weight_bf.is_contiguous():
norm_weight_bf = norm_weight_bf.contiguous()
mhc_pre_big_fuse_with_norm_tilelang(
gemm_out_mul,
gemm_out_sqrsum,
hc_scale,
hc_base,
residual_flat,
post_mix,
comb_mix,
layer_input,
norm_weight_bf,
hidden_size,
rms_eps,
hc_pre_eps,
hc_sinkhorn_eps,
hc_post_mult_value,
sinkhorn_repeat,
norm_eps,
big_fuse_n_splits,
hc_mult,
gemm_last_dim,
)
else:
mhc_pre_big_fuse_tilelang(
gemm_out_mul,
gemm_out_sqrsum,
hc_scale,
hc_base,
residual_flat,
post_mix,
comb_mix,
layer_input,
hidden_size,
rms_eps,
hc_pre_eps,
hc_sinkhorn_eps,
hc_post_mult_value,
sinkhorn_repeat,
big_fuse_n_splits,
hc_mult,
gemm_last_dim,
)
post_mix = post_mix.view(*outer_shape, hc_mult, 1)
comb_mix = comb_mix.view(*outer_shape, hc_mult, hc_mult)
layer_input = layer_input.view(*outer_shape, hidden_size)
return post_mix, comb_mix, layer_input
@tilelang.jit(
pass_configs={
tilelang.PassConfigKey.TL_DISABLE_WARP_SPECIALIZED: True,
tilelang.PassConfigKey.TL_DISABLE_TMA_LOWER: True,
tilelang.PassConfigKey.TL_PTXAS_REGISTER_USAGE_LEVEL: 10,
},
)
def mhc_post_tilelang(
a, b, c, d, x, hc: int, hidden: int, n_thr: int = 128, h_blk: int = 1024
):
n = T.dynamic("num_tokens")
h = hidden
h_blk = math.gcd(hidden, h_blk)
a: T.Tensor((n, hc, hc), T.float32)
b: T.Tensor((n, hc, h), T.bfloat16)
c: T.Tensor((n, hc), T.float32)
d: T.Tensor((n, h), T.bfloat16)
x: T.Tensor((n, hc, h), T.bfloat16)
ENABLE_PDL = is_arch_support_pdl()
with T.Kernel(n, threads=n_thr) as i_n:
if ENABLE_PDL:
T.pdl_sync()
x_shared = T.alloc_shared((hc, h_blk), T.bfloat16)
b_shared = T.alloc_shared((hc, h_blk), T.bfloat16)
d_shared = T.alloc_shared(h_blk, T.bfloat16)
x_local = T.alloc_fragment((hc, h_blk), T.float32)
b_local = T.alloc_fragment((hc, h_blk), T.float32)
d_local = T.alloc_fragment(h_blk, T.float32)
a_local = T.alloc_fragment((hc, hc), T.float32)
c_local = T.alloc_fragment(hc, T.float32)
T.copy(a[i_n, 0, 0], a_local)
T.copy(c[i_n, 0], c_local)
for i0_h in T.Pipelined(T.ceildiv(h, h_blk), num_stages=2):
T.copy(b[i_n, 0, i0_h * h_blk], b_shared)
T.copy(d[i_n, i0_h * h_blk], d_shared)
T.copy(b_shared, b_local)
T.copy(d_shared, d_local)
for i_hco, i1_h in T.Parallel(hc, h_blk):
x_local[i_hco, i1_h] = c_local[i_hco] * d_local[i1_h]
for i_hci in T.serial(hc):
x_local[i_hco, i1_h] += a_local[i_hci, i_hco] * b_local[i_hci, i1_h]
T.copy(x_local, x_shared)
T.copy(x_shared, x[i_n, 0, i0_h * h_blk])
if ENABLE_PDL:
T.pdl_trigger()
def mhc_post(
x: torch.Tensor,
residual: torch.Tensor,
post_layer_mix: torch.Tensor,
comb_res_mix: torch.Tensor,
) -> torch.Tensor:
if is_dsa_prefill_cp_round_robin_split():
x = strict_contiguous(x)
residual = strict_contiguous(residual)
post_layer_mix = strict_contiguous(post_layer_mix)
comb_res_mix = strict_contiguous(comb_res_mix)
out = torch.empty_like(residual)
mhc_post_tilelang(
comb_res_mix,
residual,
post_layer_mix.squeeze(-1),
x,
out,
residual.shape[-2],
residual.shape[-1],
)
return out
@tilelang.jit(
pass_configs={
tilelang.PassConfigKey.TL_DISABLE_WARP_SPECIALIZED: True,
tilelang.PassConfigKey.TL_DISABLE_TMA_LOWER: True,
tilelang.PassConfigKey.TL_PTXAS_REGISTER_USAGE_LEVEL: 10,
},
)
def mhc_fused_post_pre_fma_tilelang(
prev_comb_mix,
prev_residual,
prev_post_mix,
hidden_in,
pre_fn,
mixes_partial_out,
sqrsum_partial_out,
cur_residual_out,
hc: int,
hidden_size: int,
num_mix_outputs: int,
n_thr: int = 256,
tile_mix_outputs: int = 1,
split_k: int = 1,
):
num_tokens = T.dynamic("num_tokens")
split_k = T.dynamic("split_k")
hidden_per_split = (hidden_size + split_k - 1) // split_k
num_mix_output_tiles = (num_mix_outputs + tile_mix_outputs - 1) // tile_mix_outputs
prev_comb_mix: T.Tensor((num_tokens, hc, hc), T.float32)
prev_residual: T.Tensor((num_tokens, hc, hidden_size), T.bfloat16)
prev_post_mix: T.Tensor((num_tokens, hc), T.float32)
hidden_in: T.Tensor((num_tokens, hidden_size), T.bfloat16)
pre_fn: T.Tensor((num_mix_outputs, hc, hidden_size), T.float32)
mixes_partial_out: T.Tensor((split_k, num_tokens, num_mix_outputs), T.float32)
sqrsum_partial_out: T.Tensor((split_k, num_tokens), T.float32)
cur_residual_out: T.Tensor((num_tokens, hc, hidden_size), T.bfloat16)
hidden_iters_per_thread = (hidden_per_split + n_thr - 1) // n_thr
num_warps = n_thr // 32
ENABLE_PDL = is_arch_support_pdl()
# CTA assignment:
# token_idx : this CTA handles one token.
# mix_output_tile_idx : this CTA handles a small tile of mix output columns.
# For HC=4, num_mix_outputs = 24:
# [0:4] -> pre logits
# [4:8] -> post logits
# [8:24] -> comb logits
# hidden_split_idx : this CTA handles one split of the hidden dimension.
#
# Thread assignment inside one CTA:
# Each thread owns several hidden positions in this hidden split:
# hidden_idx = hidden_split_start + hidden_iter * n_thr + thread_idx
#
# For each owned hidden_idx, the thread computes:
# 1. post result: cur_residual[token, :, hidden_idx]
# 2. sqrsum partial for pre RMS
# 3. GEMM partial for several mix output columns
with T.Kernel(
num_tokens,
num_mix_output_tiles,
split_k,
threads=n_thr,
) as (token_idx, mix_output_tile_idx, hidden_split_idx):
thread_idx = T.get_thread_binding()
warp_idx = T.get_warp_idx()
lane_idx = T.get_lane_idx()
warp_partials = T.alloc_shared((num_warps, tile_mix_outputs + 1), T.float32)
post_mix_smem = T.alloc_shared((hc,), T.float32)
comb_mix_smem = T.alloc_shared((hc, hc), T.float32)
post_mix_for_token = T.alloc_local((hc,), T.float32)
comb_mix_for_token = T.alloc_local((hc, hc), T.float32)
mix_acc = T.alloc_local((tile_mix_outputs,), T.float32)
sqrsum_acc = T.alloc_local((1,), T.float32)
cur_residual_values = T.alloc_local((hc,), T.float32)
T.clear(mix_acc)
T.clear(sqrsum_acc)
hidden_split_start = hidden_split_idx * hidden_per_split
if ENABLE_PDL:
T.pdl_sync()
# Load post/comb coefficients for this token.
#
# PyTorch equivalent:
# post = prev_post_mix[token_idx] # [HC]
# comb = prev_comb_mix[token_idx] # [HC, HC]
T.copy(prev_post_mix[token_idx, 0], post_mix_smem)
T.copy(prev_comb_mix[token_idx, 0, 0], comb_mix_smem)
for route_idx in T.unroll(hc):
post_mix_for_token[route_idx] = post_mix_smem[route_idx]
for old_route_idx in T.unroll(hc):
for new_route_idx in T.unroll(hc):
comb_mix_for_token[old_route_idx, new_route_idx] = comb_mix_smem[
old_route_idx, new_route_idx
]
for hidden_iter in T.serial(hidden_iters_per_thread):
hidden_idx = hidden_split_start + hidden_iter * n_thr + thread_idx
if hidden_idx < hidden_size:
# Step A: fused post.
#
# PyTorch equivalent:
# cur_residual =
# post.unsqueeze(-1) * hidden_in.unsqueeze(1)
# + (
# comb.unsqueeze(-1)
# * prev_residual.unsqueeze(2)
# ).sum(dim=1)
#
# Scalar form for this token and this hidden position:
# cur_residual[j, h]
# = post[j] * hidden_in[h]
# + sum_k comb[k, j] * prev_residual[k, h]
for new_route_idx in T.unroll(hc):
cur_residual_values[new_route_idx] = (
post_mix_for_token[new_route_idx]
* hidden_in[token_idx, hidden_idx]
)
for old_route_idx in T.unroll(hc):
cur_residual_values[new_route_idx] += (
comb_mix_for_token[old_route_idx, new_route_idx]
* prev_residual[token_idx, old_route_idx, hidden_idx]
)
# Match the unfused path:
# mhc_post writes bf16 residual,
# then mhc_pre reads bf16 residual.
for route_idx in T.unroll(hc):
cur_residual_values[route_idx] = T.bfloat16(
cur_residual_values[route_idx]
)
# Step B1: pre sqrsum partial.
#
# PyTorch equivalent:
# x_flat = cur_residual.reshape(T, HC * H).float()
# sqrsum = (x_flat * x_flat).sum(dim=-1)
#
# Only mix_output_tile_idx == 0 writes cur_residual and sqrsum,
# otherwise different output-column CTAs would duplicate this work.
if mix_output_tile_idx == 0:
for route_idx in T.unroll(hc):
cur_residual_out[token_idx, route_idx, hidden_idx] = (
cur_residual_values[route_idx]
)
sqrsum_acc[0] += (
cur_residual_values[route_idx]
* cur_residual_values[route_idx]
)
# Step B2: pre GEMM partial.
#
# PyTorch equivalent:
# mixes = F.linear(x_flat, fn)
#
# Scalar form:
# mixes[token, o] +=
# pre_fn[o, route, hidden] * cur_residual[route, hidden]
#
# This CTA computes only tile_mix_outputs columns of mixes.
for tile_col_idx in T.unroll(tile_mix_outputs):
mix_output_idx = (
mix_output_tile_idx * tile_mix_outputs + tile_col_idx
)
if mix_output_idx < num_mix_outputs:
for route_idx in T.unroll(hc):
mix_acc[tile_col_idx] += (
pre_fn[mix_output_idx, route_idx, hidden_idx]
* cur_residual_values[route_idx]
)
# Reduce thread partials inside each warp.
for tile_col_idx in T.unroll(tile_mix_outputs):
mix_acc[tile_col_idx] = T.warp_reduce_sum(mix_acc[tile_col_idx])
if mix_output_tile_idx == 0:
sqrsum_acc[0] = T.warp_reduce_sum(sqrsum_acc[0])
# One lane per warp writes warp-level partials to shared memory.
if lane_idx == 0:
for tile_col_idx in T.unroll(tile_mix_outputs):
warp_partials[warp_idx, tile_col_idx] = mix_acc[tile_col_idx]
if mix_output_tile_idx == 0:
warp_partials[warp_idx, tile_mix_outputs] = sqrsum_acc[0]
T.sync_threads()
# Reduce across warps and write split partials.
#
# The full PyTorch result would be:
# mixes = F.linear(cur_residual.reshape(T, HC * H), fn)
# sqrsum = (cur_residual.float() ** 2).sum(dim=(1, 2))
#
# This kernel is split along hidden, so each CTA writes only:
# mixes_partial_out[hidden_split_idx, token, o]
# sqrsum_partial_out[hidden_split_idx, token]
#
# Later mhc_pre_big_fuse does:
# mixes = mixes_partial_out.sum(dim=0)
# sqrsum = sqrsum_partial_out.sum(dim=0)
# rms = rsqrt(sqrsum / (HC * H) + eps)
# mixes *= rms
# mixes -> pre/post/comb
# layer_input = sum_j pre[j] * cur_residual[j]
if warp_idx == 0:
for tile_col_idx in T.unroll(tile_mix_outputs):
mix_output_idx = mix_output_tile_idx * tile_mix_outputs + tile_col_idx
if mix_output_idx < num_mix_outputs and lane_idx == tile_col_idx:
mix_output_partial = T.alloc_var(T.float32, init=0.0)
for reduce_warp_idx in T.unroll(num_warps):
mix_output_partial += warp_partials[
reduce_warp_idx, tile_col_idx
]
mixes_partial_out[hidden_split_idx, token_idx, mix_output_idx] = (
mix_output_partial
)
if mix_output_tile_idx == 0 and lane_idx == 0:
sqrsum_partial = T.alloc_var(T.float32, init=0.0)
for reduce_warp_idx in T.unroll(num_warps):
sqrsum_partial += warp_partials[reduce_warp_idx, tile_mix_outputs]
sqrsum_partial_out[hidden_split_idx, token_idx] = sqrsum_partial
if ENABLE_PDL:
T.pdl_trigger()
def mhc_fused_post_pre(
x: torch.Tensor,
residual: torch.Tensor,
post_layer_mix: torch.Tensor,
comb_res_mix: torch.Tensor,
fn: torch.Tensor,
hc_scale: torch.Tensor,
hc_base: torch.Tensor,
rms_eps: float,
hc_pre_eps: float,
hc_sinkhorn_eps: float,
hc_post_mult_value: float,
sinkhorn_repeat: int,
n_splits: int = 1,
tile_n: int = 1,
*,
norm_weight: torch.Tensor | None = None,
norm_eps: float | None = None,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
"""Fuse the boundary between one mHC post step and the next mHC pre step.
The unfused sequence is ``mhc_post -> pre-norm GEMM -> mhc_pre big_fuse``.
This wrapper keeps the numerically sensitive ``mhc_pre_big_fuse`` stage,
including optional RMSNorm, but removes the separate post/pre boundary.
Small token batches use the FMA kernel above to combine ``mhc_post`` and the
pre-norm GEMM in one launch; larger batches keep DeepGEMM for throughput and
only fuse the Python/model-level scheduling boundary.
Returns:
residual_cur: post-mapped residual, shape (..., hc_mult, hidden_size)
post_mix_cur: shape (..., hc_mult, 1)
comb_mix_cur: shape (..., hc_mult, hc_mult)
layer_input_cur: shape (..., hidden_size)
"""
assert residual.dtype == torch.bfloat16
assert x.dtype == torch.bfloat16
assert post_layer_mix.dtype == torch.float32
assert comb_res_mix.dtype == torch.float32
assert fn.dtype == torch.float32
assert hc_scale.dtype == torch.float32
assert hc_base.dtype == torch.float32
hc_mult = residual.shape[-2]
hidden_size = residual.shape[-1]
hc_mult2 = hc_mult * hc_mult
hc_mult3 = hc_mult * 2 + hc_mult2
hc_hidden_size = hc_mult * hidden_size
outer_shape = residual.shape[:-2]
assert x.shape == (*outer_shape, hidden_size)
assert post_layer_mix.shape in (
(*outer_shape, hc_mult, 1),
(*outer_shape, hc_mult),
)
assert comb_res_mix.shape == (*outer_shape, hc_mult, hc_mult)
assert fn.shape == (hc_mult3, hc_hidden_size)
assert hc_scale.shape == (3,)
assert hc_base.shape == (hc_mult3,)
residual_flat = residual.view(-1, hc_mult, hidden_size)
num_tokens = residual_flat.shape[0]
if num_tokens == 0:
# Some DP/EP ranks can receive no tokens; return correctly typed empty
# tensors so later fused layers keep the same contracts as mhc_pre/hc_post.
return (
torch.empty_like(residual),
torch.empty(
(*outer_shape, hc_mult, 1), dtype=torch.float32, device=residual.device
),
torch.empty(
(*outer_shape, hc_mult, hc_mult),
dtype=torch.float32,
device=residual.device,
),
torch.empty(
(*outer_shape, hidden_size),
dtype=torch.bfloat16,
device=residual.device,
),
)
x_flat = x.view(num_tokens, hidden_size)
# The scalar-FMA kernel wins only for small batches where launch
# overhead dominates; beyond the threshold DeepGEMM's tensor-core path wins.
fma_token_threshold = 32
if num_tokens <= fma_token_threshold:
tile_n = 2 if num_tokens < 8 else 3
n_splits = 8 if (num_tokens < 8 and hidden_size <= 4096) else 4
else:
n_splits = _compute_num_split_for_mhc_pre(num_tokens, hc_hidden_size)
gemm_out_mul = torch.empty(
n_splits,
num_tokens,
hc_mult3,
dtype=torch.float32,
device=residual.device,
)
gemm_out_sqrsum = torch.empty(
n_splits,
num_tokens,
dtype=torch.float32,
device=residual.device,
)
residual_cur = torch.empty_like(residual_flat)
if num_tokens <= fma_token_threshold:
# Small-batch path: one TileLang launch computes hc_post, the bf16
# residual write, GEMM partials, and the RMS square-sum partials.
mhc_fused_post_pre_fma_tilelang(
comb_res_mix.view(num_tokens, hc_mult, hc_mult),
residual_flat,
post_layer_mix.view(num_tokens, hc_mult),
x_flat,
fn.view(hc_mult3, hc_mult, hidden_size),
gemm_out_mul,
gemm_out_sqrsum,
residual_cur,
hc_mult,
hidden_size,
hc_mult3,
tile_mix_outputs=tile_n,
split_k=n_splits,
)
else:
# Large-batch path: keep the existing high-throughput TileLang hc_post +
# DeepGEMM pre-norm GEMM decomposition instead of replacing tensor cores.
mhc_post_tilelang(
comb_res_mix.view(num_tokens, hc_mult, hc_mult),
residual_flat,
post_layer_mix.view(num_tokens, hc_mult),
x_flat,
residual_cur,
hc_mult,
hidden_size,
)
if envs.SGLANG_OPT_DEEPGEMM_HC_PRENORM.get():
import deep_gemm
deep_gemm.tf32_hc_prenorm_gemm(
residual_cur.view(num_tokens, hc_hidden_size),
fn,
gemm_out_mul,
gemm_out_sqrsum,
num_splits=n_splits,
)
else:
# Fallback mirrors mhc_pre when DeepGEMM prenorm is disabled.
n_splits = 1
gemm_out_mul_2d = torch.empty(
num_tokens, hc_mult3, dtype=torch.float32, device=residual.device
)
gemm_out_sqrsum_1d = torch.empty(
num_tokens, dtype=torch.float32, device=residual.device
)
mhc_pre_gemm_sqrsum_tilelang(
residual_cur.view(num_tokens, hc_hidden_size),
fn,
gemm_out_mul_2d,
gemm_out_sqrsum_1d,
hc_mult3,
hc_hidden_size,
)
gemm_out_mul = gemm_out_mul_2d.unsqueeze(0)
gemm_out_sqrsum = gemm_out_sqrsum_1d.unsqueeze(0)
post_mix_cur = torch.empty(
num_tokens,
hc_mult,
dtype=torch.float32,
device=residual.device,
)
comb_mix_cur = torch.empty(
num_tokens,
hc_mult2,
dtype=torch.float32,
device=residual.device,
)
layer_input_cur = torch.empty(
num_tokens,
hidden_size,
dtype=torch.bfloat16,
device=residual.device,
)
if norm_weight is not None:
# Final mhc_pre stage: convert GEMM partials into post/comb/layer_input
# and fuse the following RMSNorm when the model passed a norm weight.
assert norm_eps is not None
assert norm_weight.shape == (hidden_size,)
norm_weight_bf = (
norm_weight.bfloat16()
if norm_weight.dtype != torch.bfloat16
else norm_weight
)
if not norm_weight_bf.is_contiguous():
norm_weight_bf = norm_weight_bf.contiguous()
mhc_pre_big_fuse_with_norm_tilelang(
gemm_out_mul,
gemm_out_sqrsum,
hc_scale,
hc_base,
residual_cur,
post_mix_cur,
comb_mix_cur,
layer_input_cur,
norm_weight_bf,
hidden_size,
rms_eps,
hc_pre_eps,
hc_sinkhorn_eps,
hc_post_mult_value,
sinkhorn_repeat,
norm_eps,
n_splits,
hc_mult,
hc_mult3,
)
else:
# Same mhc_pre finalization without the model-layer RMSNorm.
mhc_pre_big_fuse_tilelang(
gemm_out_mul,
gemm_out_sqrsum,
hc_scale,
hc_base,
residual_cur,
post_mix_cur,
comb_mix_cur,
layer_input_cur,
hidden_size,
rms_eps,
hc_pre_eps,
hc_sinkhorn_eps,
hc_post_mult_value,
sinkhorn_repeat,
n_splits,
hc_mult,
hc_mult3,
)
return (
residual_cur.view(*outer_shape, hc_mult, hidden_size),
post_mix_cur.view(*outer_shape, hc_mult, 1),
comb_mix_cur.view(*outer_shape, hc_mult, hc_mult),
layer_input_cur.view(*outer_shape, hidden_size),
)
def npu_hc_pre(
x: torch.Tensor,
hc_fn: torch.Tensor,
hc_scale: torch.Tensor,
hc_base: torch.Tensor,
hc_mult: int,
hc_sinkhorn_iters: int,
rms_norm_eps: float,
hc_eps: float,
forward_batch=None,
) -> tuple:
"""NPU-accelerated hc_pre via the custom_ops kernel.
Returns (y, post, comb, norm_fused). norm_fused is always False
because npu_hc_pre does not fold input_layernorm — the caller must
apply it separately.
"""
shape, dtype = x.size(), x.dtype
# IDLE / empty short-circuit, mirroring the dsv4-flash source.
# The kernel emits post/comb in fp32 (sinkhorn iterates in fp32),
# so the dummies must too — otherwise downstream comb/post-aware
# ops see a silent fp32 ↔ bf16 split between idle and non-idle
# batches.
is_idle = forward_batch is not None and forward_batch.forward_mode.is_idle()
if is_idle or x.shape[0] == 0:
bs = x.shape[0]
y = torch.empty((bs, shape[-1]), dtype=dtype, device=x.device)
post = torch.empty((bs, hc_mult), dtype=torch.float32, device=x.device)
comb = torch.empty(
(bs, hc_mult, hc_mult),
dtype=torch.float32,
device=x.device,
)
return y, post, comb, False
# Note the return order: (y, post, comb) — y is the (T, hidden)
# mixed activation, post / comb are the hc_post inputs. The
# fused kernel emits y in fp32 (sinkhorn iterates in fp32), so
# cast back to the input dtype before the downstream
# aclnnRmsNorm (which has no x=fp32 / gamma=bf16 overload).
y, post, comb = torch.ops.custom.npu_hc_pre(
x,
hc_fn,
hc_scale,
hc_base,
hc_mult=hc_mult,
hc_sinkhorn_iters=hc_sinkhorn_iters,
norm_eps=rms_norm_eps,
hc_eps=hc_eps,
)
# npu_hc_pre uses norm_eps for sinkhorn's internal RMS only; it does
# not fold input_layernorm. Return norm_fused=False so the caller
# applies the layernorm itself, matching the deepgemm/torch paths.
return y.to(dtype), post, comb, False