[Kernel][HY V4] Add Triton iHC pre/post fallback (#55059)

Signed-off-by: linitra24 <renshuang.zhou@daocloud.io>
Co-authored-by: Jee Jee Li <pandaleefree@gmail.com>
This commit is contained in:
linitra24
2026-09-05 23:12:50 +08:00
committed by GitHub
co-authored by Jee Jee Li
parent 52bc900d93
commit 7985444339
3 changed files with 531 additions and 3 deletions
+199
View File
@@ -0,0 +1,199 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Benchmark the HY V4 Triton iHC pre/post kernels against eager PyTorch."""
import os
import subprocess
from functools import partial
from importlib.metadata import version
from statistics import median
import torch
import torch.nn.functional as F
import vllm
from vllm.models.hy_v4.nvidia.triton_ihc import (
triton_ihc_post,
triton_ihc_pre,
)
from vllm.triton_utils import triton
from vllm.utils.argparse_utils import FlexibleArgumentParser
def eager_pre(
x: torch.Tensor,
weight: torch.Tensor,
scale: torch.Tensor,
base: torch.Tensor,
magnitude: float,
hc_eps: float,
norm_eps: float,
) -> tuple[torch.Tensor, torch.Tensor]:
num_tokens, hc_mult, hidden_size = x.shape
x_flat = x.flatten(1).float()
reciprocal_rms = torch.rsqrt(x_flat.square().mean(-1, keepdim=True) + norm_eps)
mixes = F.linear(x_flat, weight) * reciprocal_rms
pre = torch.sigmoid(mixes[:, :hc_mult] * scale[0] + base[:hc_mult])
post = torch.sigmoid(mixes[:, hc_mult:] * scale[1] + base[hc_mult:])
pre = pre + hc_eps
post = magnitude * post + hc_eps
output = torch.sum(pre.unsqueeze(-1) * x.float(), dim=1)
return output.to(x.dtype).reshape(num_tokens, hidden_size), post
def eager_post(
x: torch.Tensor,
residual: torch.Tensor,
post: torch.Tensor,
) -> torch.Tensor:
return (post.float().unsqueeze(-1) * x.float().unsqueeze(-2) + residual.float()).to(
x.dtype
)
def _timer(method: str):
if method == "cupti":
from flashinfer.testing import bench_gpu_time_with_cupti
return partial(
bench_gpu_time_with_cupti,
use_cuda_graph=True,
cold_l2_cache=True,
)
if method == "cudagraph":
from flashinfer.testing import bench_gpu_time_with_cudagraph
return partial(bench_gpu_time_with_cudagraph, cold_l2_cache=True)
raise ValueError(f"unknown timing method: {method}")
@torch.inference_mode()
def run_benchmark(
token_counts: list[int],
hidden_size: int,
dtype: torch.dtype,
method: str,
) -> None:
hc_mult = 4
device = torch.device("cuda")
torch.manual_seed(0)
weight = torch.randn(
2 * hc_mult,
hc_mult * hidden_size,
device=device,
dtype=torch.float32,
)
scale = torch.randn(2, device=device, dtype=torch.float32) * 0.01
base = torch.randn(2 * hc_mult, device=device, dtype=torch.float32)
timer = _timer(method)
properties = torch.cuda.get_device_properties(device)
git_branch = subprocess.run(
["git", "branch", "--show-current"],
check=False,
capture_output=True,
text=True,
).stdout.strip()
git_commit = subprocess.run(
["git", "rev-parse", "--short", "HEAD"],
check=False,
capture_output=True,
text=True,
).stdout.strip()
print(f"device: {properties.name}")
print(f"branch: {git_branch}; commit: {git_commit}")
print(
f"vllm: {vllm.__version__}; torch: {torch.__version__}; "
f"CUDA: {torch.version.cuda}"
)
print(
f"triton: {triton.__version__}; flashinfer: {version('flashinfer-python')}; "
f"dtype: {dtype}; method: {method}; cache: cold L2"
)
print(
"env: VLLM_ENABLE_HPC_OPS="
f"{os.getenv('VLLM_ENABLE_HPC_OPS', '<unset>')}; "
"VLLM_BATCH_INVARIANT="
f"{os.getenv('VLLM_BATCH_INVARIANT', '<unset>')}"
)
print(f"hidden_size: {hidden_size}; hc_mult: {hc_mult}")
print(
f"{'tokens':>8} {'op':>6} {'eager (us)':>12} "
f"{'triton (us)':>12} {'speedup':>9} {'GiB':>8} "
f"{'eager GB/s':>12} {'triton GB/s':>13}"
)
for num_tokens in token_counts:
x = torch.randn(
num_tokens,
hc_mult,
hidden_size,
device=device,
dtype=dtype,
)
block_output = torch.randn(num_tokens, hidden_size, device=device, dtype=dtype)
residual = torch.randn_like(x)
eager_output, post = eager_pre(x, weight, scale, base, 2.0, 1e-6, 1e-5)
triton_output, triton_post = triton_ihc_pre(
x, weight, scale, base, 2.0, 1e-6, 1e-5
)
torch.testing.assert_close(triton_output, eager_output, atol=2e-2, rtol=1e-2)
torch.testing.assert_close(triton_post, post, atol=2e-5, rtol=1e-5)
torch.testing.assert_close(
triton_ihc_post(block_output, residual, post),
eager_post(block_output, residual, post),
atol=0,
rtol=0,
)
benchmarks = (
(
"pre",
partial(eager_pre, x, weight, scale, base, 2.0, 1e-6, 1e-5),
partial(triton_ihc_pre, x, weight, scale, base, 2.0, 1e-6, 1e-5),
x.nbytes
+ weight.nbytes
+ scale.nbytes
+ base.nbytes
+ eager_output.nbytes
+ post.nbytes,
),
(
"post",
partial(eager_post, block_output, residual, post),
partial(triton_ihc_post, block_output, residual, post),
block_output.nbytes + residual.nbytes + post.nbytes + residual.nbytes,
),
)
for op_name, eager_fn, triton_fn, logical_bytes in benchmarks:
eager_us = median(timer(eager_fn)) * 1e3
triton_us = median(timer(triton_fn)) * 1e3
speedup = eager_us / triton_us
logical_gib = logical_bytes / 2**30
eager_gbps = logical_bytes / eager_us / 1e3
triton_gbps = logical_bytes / triton_us / 1e3
print(
f"{num_tokens:>8} {op_name:>6} {eager_us:>12.1f} "
f"{triton_us:>12.1f} {speedup:>8.2f}x {logical_gib:>8.3f} "
f"{eager_gbps:>12.1f} {triton_gbps:>13.1f}"
)
if __name__ == "__main__":
parser = FlexibleArgumentParser(description=__doc__)
parser.add_argument(
"--token-counts",
type=int,
nargs="+",
default=[1, 2, 4, 8, 16, 64, 256, 1024, 4096, 8192],
)
parser.add_argument("--hidden-size", type=int, choices=[4096, 6144], default=6144)
parser.add_argument("--dtype", choices=["float16", "bfloat16"], default="bfloat16")
parser.add_argument("--method", choices=["cupti", "cudagraph"], default="cupti")
args = parser.parse_args()
run_benchmark(
args.token_counts,
args.hidden_size,
getattr(torch, args.dtype),
args.method,
)
+19 -3
View File
@@ -9,9 +9,8 @@ scatters the result back over the channels (``HYV4HCPostLayer``). The final
``HYV4HCHeadLayer`` merges the channels before the model's output norm.
NOTE: Each of the three steps has an optional single-kernel HPC replacement
(``HpcIHCPre`` / ``HpcIHCPost`` / ``HpcIHCHead``). They are only constructed
when the hpc package is installed, ``VLLM_ENABLE_HPC_OPS=1`` and the shape /
device constraints hold; otherwise the eager path below runs unchanged.
(``HpcIHCPre`` / ``HpcIHCPost`` / ``HpcIHCHead``). Pre and post fall back to
in-tree Triton kernels on CUDA when HPC is unavailable, then to the eager path.
TODO: port the cross-layer post+pre fusion (``HpcIHCPostPre``) as well; it
requires restructuring the decoder-layer forward scheduling.
"""
@@ -22,6 +21,11 @@ from transformers import PretrainedConfig
from vllm.model_executor.layers.hpc import HpcIHCHead, HpcIHCPost, HpcIHCPre
from vllm.model_executor.layers.linear import ReplicatedLinear
from vllm.models.hy_v4.nvidia.triton_ihc import (
triton_ihc_post,
triton_ihc_pre,
triton_ihc_supported,
)
class HYV4HCPreLayer(nn.Module):
@@ -106,6 +110,16 @@ class HYV4HCPreLayer(nn.Module):
"""
if self.hpc_op is not None:
return self.hpc_op(x)
if triton_ihc_supported(x):
return triton_ihc_pre(
x,
self.hc_fn.weight,
self.hc_scale,
self.hc_base,
self.magnitude,
self.hc_eps,
self.layernorm_epsilon,
)
shape = x.size() # [num_tokens, hc, d]
hc = self.hc_mult
@@ -175,6 +189,8 @@ class HYV4HCPostLayer(nn.Module):
"""
if self.hpc_op is not None:
return self.hpc_op(x, residual, post)
if triton_ihc_supported(x):
return triton_ihc_post(x, residual, post)
dtype = x.dtype
x = x.float()
+313
View File
@@ -0,0 +1,313 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Triton iHC pre/post kernels for HY V4.
Adapted from the SGLang HY V4 implementation:
https://github.com/sgl-project/sglang/pull/36805
"""
import torch
from vllm import envs
from vllm.platforms import current_platform
from vllm.triton_utils import HAS_TRITON, tl, triton
_BLOCK_K = 1024
_BLOCK_D = 1024
def triton_ihc_supported(x: torch.Tensor) -> bool:
"""Return whether the in-tree Triton path can run for this input."""
return (
HAS_TRITON
and current_platform.is_cuda()
and x.is_cuda
and x.dtype in (torch.float16, torch.bfloat16)
and not envs.VLLM_BATCH_INVARIANT
)
@triton.jit
def _ihc_pre_stage1(
x_ptr,
weight_ptr,
partial_ptr,
K_TOTAL: tl.constexpr,
HC_MULT: tl.constexpr,
HC_POW2: tl.constexpr,
NUM_SPLITS: tl.constexpr,
BLOCK_K: tl.constexpr,
PARTIAL_STRIDE: tl.constexpr,
):
token_idx = tl.program_id(0).to(tl.int64)
split_idx = tl.program_id(1)
hc_idx = tl.arange(0, HC_POW2)
hc_mask = hc_idx < HC_MULT
k_offsets = split_idx * BLOCK_K + tl.arange(0, BLOCK_K)
k_mask = k_offsets < K_TOTAL
x = tl.load(
x_ptr + token_idx * K_TOTAL + k_offsets,
mask=k_mask,
other=0.0,
).to(tl.float32)
sum_squares = tl.sum(x * x, axis=0)
weight_offsets = hc_idx[:, None] * K_TOTAL + k_offsets[None, :]
weight_mask = hc_mask[:, None] & k_mask[None, :]
pre_mix = tl.sum(
tl.load(weight_ptr + weight_offsets, mask=weight_mask, other=0.0) * x[None, :],
axis=1,
)
post_mix = tl.sum(
tl.load(
weight_ptr + HC_MULT * K_TOTAL + weight_offsets,
mask=weight_mask,
other=0.0,
)
* x[None, :],
axis=1,
)
partial = partial_ptr + (token_idx * NUM_SPLITS + split_idx) * PARTIAL_STRIDE
tl.store(partial, sum_squares)
tl.store(partial + 1 + hc_idx, pre_mix, mask=hc_mask)
tl.store(partial + 1 + HC_POW2 + hc_idx, post_mix, mask=hc_mask)
@triton.jit
def _ihc_pre_stage2(
x_ptr,
partial_ptr,
scale_ptr,
base_ptr,
output_ptr,
post_ptr,
HIDDEN_SIZE: tl.constexpr,
K_TOTAL: tl.constexpr,
HC_MULT: tl.constexpr,
HC_POW2: tl.constexpr,
NUM_SPLITS: tl.constexpr,
PARTIAL_STRIDE: tl.constexpr,
BLOCK_D: tl.constexpr,
MAGNITUDE: tl.constexpr,
NORM_EPS: tl.constexpr,
HC_EPS: tl.constexpr,
):
token_idx = tl.program_id(0).to(tl.int64)
hidden_block_idx = tl.program_id(1)
hc_idx = tl.arange(0, HC_POW2)
hc_mask = hc_idx < HC_MULT
partial_row = partial_ptr + token_idx * NUM_SPLITS * PARTIAL_STRIDE
sum_squares = tl.zeros((), dtype=tl.float32)
pre_mix = tl.zeros((HC_POW2,), dtype=tl.float32)
post_mix = tl.zeros((HC_POW2,), dtype=tl.float32)
for split_idx in tl.static_range(NUM_SPLITS):
partial = partial_row + split_idx * PARTIAL_STRIDE
sum_squares += tl.load(partial)
pre_mix += tl.load(partial + 1 + hc_idx, mask=hc_mask, other=0.0)
post_mix += tl.load(
partial + 1 + HC_POW2 + hc_idx,
mask=hc_mask,
other=0.0,
)
reciprocal_rms = tl.rsqrt(sum_squares / K_TOTAL + NORM_EPS)
pre_scale = tl.load(scale_ptr)
post_scale = tl.load(scale_ptr + 1)
pre_base = tl.load(base_ptr + hc_idx, mask=hc_mask, other=0.0)
post_base = tl.load(base_ptr + HC_MULT + hc_idx, mask=hc_mask, other=0.0)
pre = tl.sigmoid(pre_mix * reciprocal_rms * pre_scale + pre_base) + HC_EPS
if hidden_block_idx == 0:
post = (
MAGNITUDE * tl.sigmoid(post_mix * reciprocal_rms * post_scale + post_base)
+ HC_EPS
)
tl.store(
post_ptr + token_idx * HC_MULT + hc_idx,
post,
mask=hc_mask,
)
hidden_offsets = hidden_block_idx * BLOCK_D + tl.arange(0, BLOCK_D)
hidden_mask = hidden_offsets < HIDDEN_SIZE
x_row = x_ptr + token_idx * K_TOTAL
output = tl.zeros((BLOCK_D,), dtype=tl.float32)
for channel_idx in tl.static_range(HC_MULT):
channel = tl.load(
x_row + channel_idx * HIDDEN_SIZE + hidden_offsets,
mask=hidden_mask,
other=0.0,
).to(tl.float32)
channel_gate = tl.sum(tl.where(hc_idx == channel_idx, pre, 0.0), axis=0)
output += channel_gate * channel
tl.store(
output_ptr + token_idx * HIDDEN_SIZE + hidden_offsets,
output.to(output_ptr.dtype.element_ty),
mask=hidden_mask,
)
@triton.jit
def _ihc_post_kernel(
x_ptr,
residual_ptr,
post_ptr,
output_ptr,
HIDDEN_SIZE: tl.constexpr,
HC_MULT: tl.constexpr,
HC_POW2: tl.constexpr,
BLOCK_D: tl.constexpr,
):
token_idx = tl.program_id(0).to(tl.int64)
hidden_block_idx = tl.program_id(1)
hc_idx = tl.arange(0, HC_POW2)
hc_mask = hc_idx < HC_MULT
post = tl.load(
post_ptr + token_idx * HC_MULT + hc_idx,
mask=hc_mask,
other=0.0,
)
hidden_offsets = hidden_block_idx * BLOCK_D + tl.arange(0, BLOCK_D)
hidden_mask = hidden_offsets < HIDDEN_SIZE
x = tl.load(
x_ptr + token_idx * HIDDEN_SIZE + hidden_offsets,
mask=hidden_mask,
other=0.0,
).to(tl.float32)
residual_row = residual_ptr + token_idx * HC_MULT * HIDDEN_SIZE
output_row = output_ptr + token_idx * HC_MULT * HIDDEN_SIZE
for channel_idx in tl.static_range(HC_MULT):
residual = tl.load(
residual_row + channel_idx * HIDDEN_SIZE + hidden_offsets,
mask=hidden_mask,
other=0.0,
).to(tl.float32)
channel_gate = tl.sum(tl.where(hc_idx == channel_idx, post, 0.0), axis=0)
output = channel_gate * x + residual
tl.store(
output_row + channel_idx * HIDDEN_SIZE + hidden_offsets,
output.to(output_ptr.dtype.element_ty),
mask=hidden_mask,
)
def triton_ihc_pre(
x: torch.Tensor,
weight: torch.Tensor,
scale: torch.Tensor,
base: torch.Tensor,
magnitude: float,
hc_eps: float,
norm_eps: float,
) -> tuple[torch.Tensor, torch.Tensor]:
"""Reduce iHC channels and produce the post gates."""
assert x.dim() == 3
assert x.is_cuda and weight.is_cuda and scale.is_cuda and base.is_cuda
assert weight.dtype == torch.float32
assert scale.dtype == torch.float32 and base.dtype == torch.float32
x = x.contiguous()
assert weight.is_contiguous()
scale = scale.contiguous()
base = base.contiguous()
num_tokens, hc_mult, hidden_size = x.shape
k_total = hc_mult * hidden_size
assert weight.shape == (2 * hc_mult, k_total)
assert scale.shape == (2,)
assert base.shape == (2 * hc_mult,)
if num_tokens == 0:
return (
torch.empty((0, hidden_size), dtype=x.dtype, device=x.device),
torch.empty((0, hc_mult), dtype=torch.float32, device=x.device),
)
output = torch.empty((num_tokens, hidden_size), dtype=x.dtype, device=x.device)
post = torch.empty((num_tokens, hc_mult), dtype=torch.float32, device=x.device)
hc_pow2 = triton.next_power_of_2(hc_mult)
num_splits = triton.cdiv(k_total, _BLOCK_K)
partial_stride = 1 + 2 * hc_pow2
partial = torch.empty(
(num_tokens, num_splits, partial_stride),
dtype=torch.float32,
device=x.device,
)
_ihc_pre_stage1[(num_tokens, num_splits)](
x,
weight,
partial,
K_TOTAL=k_total,
HC_MULT=hc_mult,
HC_POW2=hc_pow2,
NUM_SPLITS=num_splits,
BLOCK_K=_BLOCK_K,
PARTIAL_STRIDE=partial_stride,
num_warps=8,
enable_fp_fusion=False,
)
_ihc_pre_stage2[(num_tokens, triton.cdiv(hidden_size, _BLOCK_D))](
x,
partial,
scale,
base,
output,
post,
HIDDEN_SIZE=hidden_size,
K_TOTAL=k_total,
HC_MULT=hc_mult,
HC_POW2=hc_pow2,
NUM_SPLITS=num_splits,
PARTIAL_STRIDE=partial_stride,
BLOCK_D=_BLOCK_D,
MAGNITUDE=magnitude,
NORM_EPS=norm_eps,
HC_EPS=hc_eps,
num_warps=4,
enable_fp_fusion=False,
)
return output, post
def triton_ihc_post(
x: torch.Tensor,
residual: torch.Tensor,
post: torch.Tensor,
) -> torch.Tensor:
"""Scatter a sub-block output back over the iHC residual channels."""
assert x.dim() == 2
assert x.is_cuda and residual.is_cuda and post.is_cuda
assert post.dtype == torch.float32
x = x.contiguous()
residual = residual.contiguous()
post = post.contiguous()
num_tokens, hidden_size = x.shape
hc_mult = post.shape[-1]
assert residual.shape == (num_tokens, hc_mult, hidden_size)
assert post.shape == (num_tokens, hc_mult)
if num_tokens == 0:
return torch.empty((0, hc_mult, hidden_size), dtype=x.dtype, device=x.device)
output = torch.empty(
(num_tokens, hc_mult, hidden_size), dtype=x.dtype, device=x.device
)
_ihc_post_kernel[(num_tokens, triton.cdiv(hidden_size, _BLOCK_D))](
x,
residual,
post,
output,
HIDDEN_SIZE=hidden_size,
HC_MULT=hc_mult,
HC_POW2=triton.next_power_of_2(hc_mult),
BLOCK_D=_BLOCK_D,
num_warps=4,
enable_fp_fusion=False,
)
return output