Optimize ngram decode id computation (#24757)

Co-authored-by: Codex <codex@example.com>
Co-authored-by: BBuf <xiaoyu.zhang@radixark.net>
This commit is contained in:
Xiaoyu Zhang
2026-06-02 17:37:34 +08:00
committed by GitHub
co-authored by Codex BBuf
parent f651b48764
commit 84e1108312
5 changed files with 417 additions and 22 deletions
@@ -0,0 +1,121 @@
import torch
import triton
import triton.testing
from sglang.jit_kernel.benchmark.utils import (
DEFAULT_DEVICE,
get_benchmark_range,
run_benchmark_no_cudagraph,
)
from sglang.jit_kernel.ngram_embedding import (
compute_n_gram_ids,
compute_n_gram_ids_decode,
)
from sglang.test.ci.ci_register import register_cuda_ci
register_cuda_ci(est_time=15, suite="base-b-kernel-benchmark-1-gpu-large")
NE_N = 8
NE_K = 2
VOCAB_SIZE = 32000
MAX_CONTEXT_LEN = 1024
BATCH_SIZE_LIST = get_benchmark_range(
full_range=[1, 2, 8, 32, 128, 512, 1024, 2048, 4096],
ci_range=[32, 1024],
)
def _make_ngram_params():
ne_weights = torch.zeros([NE_N - 1, NE_K, NE_N], dtype=torch.int32)
ne_mods = torch.zeros([NE_N - 1, NE_K], dtype=torch.int32)
exclusive_sums = torch.zeros([(NE_N - 1) * NE_K + 1], dtype=torch.int32)
for n in range(2, NE_N + 1):
for k in range(NE_K):
config_id = (n - 2) * NE_K + k
mod = 65537 + 2 * config_id
ne_mods[n - 2][k] = mod
exclusive_sums[config_id + 1] = exclusive_sums[config_id] + mod
for delta in range(NE_N):
ne_weights[n - 2][k][delta] = pow(VOCAB_SIZE, delta, mod)
return (
ne_weights.to(DEFAULT_DEVICE),
ne_mods.to(DEFAULT_DEVICE),
exclusive_sums.to(DEFAULT_DEVICE),
)
@triton.testing.perf_report(
triton.testing.Benchmark(
x_names=["batch_size"],
x_vals=BATCH_SIZE_LIST,
line_arg="provider",
line_vals=["general", "decode"],
line_names=["general compute_n_gram_ids", "decode fast path"],
styles=[("blue", "-"), ("orange", "-")],
ylabel="us",
plot_name="ngram-compute-decode",
args={},
)
)
def benchmark(batch_size: int, provider: str):
num_configs = (NE_N - 1) * NE_K
max_running_reqs = batch_size + 8
ne_weights, ne_mods, exclusive_sums = _make_ngram_params()
ne_token_table = torch.randint(
0,
VOCAB_SIZE,
(max_running_reqs, MAX_CONTEXT_LEN),
dtype=torch.int32,
device=DEFAULT_DEVICE,
)
row_indices = torch.arange(batch_size, dtype=torch.int64, device=DEFAULT_DEVICE)
column_starts = torch.randint(
0, MAX_CONTEXT_LEN, (batch_size,), dtype=torch.int32, device=DEFAULT_DEVICE
)
n_gram_ids = torch.empty(
(batch_size, num_configs), dtype=torch.int32, device=DEFAULT_DEVICE
)
if provider == "general":
tokens = torch.empty(batch_size, dtype=torch.int32, device=DEFAULT_DEVICE)
exclusive_req_len_sums = torch.arange(
batch_size + 1, dtype=torch.int32, device=DEFAULT_DEVICE
)
def fn():
compute_n_gram_ids(
NE_N,
NE_K,
ne_weights,
ne_mods,
exclusive_sums,
tokens,
exclusive_req_len_sums,
ne_token_table,
row_indices,
column_starts,
n_gram_ids,
)
else:
def fn():
compute_n_gram_ids_decode(
NE_N,
NE_K,
ne_weights,
ne_mods,
exclusive_sums,
ne_token_table,
row_indices,
column_starts,
n_gram_ids,
)
return run_benchmark_no_cudagraph(fn)
if __name__ == "__main__":
benchmark.run(print_data=True)
@@ -13,6 +13,9 @@
namespace device::ngram_embedding {
constexpr int kDecodeBlockSize = 256;
constexpr int kMaxComputeNGramIdsDecodeBlocks = 65535;
__global__ void ComputeNGramIdsKernel(
int batch_size,
int ne_n,
@@ -84,6 +87,51 @@ __global__ void ComputeNGramIdsKernel(
}
}
__global__ void ComputeNGramIdsDecodeKernel(
int batch_size,
int ne_n,
int ne_k,
const int* __restrict__ ne_weights, // [ne_n-1,ne_k,ne_n]
const int* __restrict__ ne_mods, // [ne_n-1,ne_k]
const int* __restrict__ exclusive_ne_embeder_size_sums, // [(ne_n-1)*ne_k]
const int* __restrict__ ne_token_table, // [max_running_reqs, max_context_len]
int max_context_len, // max_context_len
const int64_t* __restrict__ row_indices, // [batch_size]
const int* __restrict__ column_starts, // [batch_size]
int* __restrict__ n_gram_ids // [batch_size, (ne_n-1)*ne_k]
) {
const int num_configs = (ne_n - 1) * ne_k;
const int total_outputs = batch_size * num_configs;
for (int output_idx = blockIdx.x * blockDim.x + threadIdx.x; output_idx < total_outputs;
output_idx += blockDim.x * gridDim.x) {
const int req_id = output_idx / num_configs;
const int config_idx = output_idx - req_id * num_configs;
const int k_idx = config_idx % ne_k;
const int n_idx = config_idx / ne_k;
const int weight_offset = n_idx * ne_k * ne_n + k_idx * ne_n;
const int ne_mod = ne_mods[n_idx * ne_k + k_idx];
uint64_t n_gram_id = 0;
const int64_t req_token_table_offset = row_indices[req_id] * static_cast<int64_t>(max_context_len);
const int64_t current_token_table_offset = req_token_table_offset + column_starts[req_id];
for (int j = 0; j < n_idx + 2; j++) {
if (current_token_table_offset - j < req_token_table_offset) {
break;
}
const int token = ne_token_table[current_token_table_offset - j];
if (token < 0) {
break;
}
const uint64_t term = static_cast<uint64_t>(token) * static_cast<uint64_t>(ne_weights[weight_offset + j]);
n_gram_id += term % ne_mod;
}
n_gram_id %= ne_mod;
n_gram_id += exclusive_ne_embeder_size_sums[n_idx * ne_k + k_idx];
n_gram_ids[output_idx] = static_cast<int>(n_gram_id);
}
}
__global__ void UpdateTokenTableKernel(
int batch_size,
int* tokens, // [token_num]
@@ -213,6 +261,86 @@ struct NgramEmbeddingKernel {
static_cast<int*>(n_gram_ids.data_ptr()));
}
static void compute_n_gram_ids_decode(
const int64_t ne_n,
const int64_t ne_k,
const tvm::ffi::TensorView ne_weights,
const tvm::ffi::TensorView ne_mods,
const tvm::ffi::TensorView exclusive_ne_embeder_size_sums,
const tvm::ffi::TensorView ne_token_table,
const tvm::ffi::TensorView row_indices,
const tvm::ffi::TensorView column_starts,
const tvm::ffi::TensorView n_gram_ids) {
using namespace host;
auto device_ = SymbolicDevice{};
auto batch_size = SymbolicSize{"batch_size"};
TensorMatcher({-1, -1, -1}) // [ne_n-1, ne_k, ne_n]
.with_dtype<int32_t>()
.with_device<kDLCUDA>(device_)
.verify(ne_weights);
TensorMatcher({-1, -1}) // [ne_n-1, ne_k]
.with_dtype<int32_t>()
.with_device<kDLCUDA>()
.verify(ne_mods);
TensorMatcher({-1}) // [(ne_n-1)*ne_k + 1]
.with_dtype<int32_t>()
.with_device<kDLCUDA>()
.verify(exclusive_ne_embeder_size_sums);
TensorMatcher({-1, -1}) // [max_running_reqs, max_context_len]
.with_dtype<int32_t>()
.with_device<kDLCUDA>()
.verify(ne_token_table);
TensorMatcher({batch_size}) // [batch_size]
.with_dtype<int64_t>()
.with_device<kDLCUDA>()
.verify(row_indices);
TensorMatcher({batch_size}) // [batch_size]
.with_dtype<int32_t>()
.with_device<kDLCUDA>()
.verify(column_starts);
TensorMatcher({batch_size, -1}) // [batch_size, (ne_n-1)*ne_k]
.with_dtype<int32_t>()
.with_device<kDLCUDA>()
.verify(n_gram_ids);
const int bs = static_cast<int>(batch_size.unwrap());
if (bs <= 0) {
return;
}
const int max_context_len = static_cast<int>(ne_token_table.size(1));
const int num_configs = (static_cast<int>(ne_n) - 1) * static_cast<int>(ne_k);
const int total_outputs = bs * num_configs;
const auto stream = LaunchKernel::resolve_device(device_.unwrap());
constexpr int kBlockSize = device::ngram_embedding::kDecodeBlockSize;
const int grid_size = std::min(
device::ngram_embedding::kMaxComputeNGramIdsDecodeBlocks,
static_cast<int>(div_ceil(total_outputs, kBlockSize)));
LaunchKernel(grid_size, kBlockSize, stream)(
device::ngram_embedding::ComputeNGramIdsDecodeKernel,
bs,
static_cast<int>(ne_n),
static_cast<int>(ne_k),
static_cast<const int*>(ne_weights.data_ptr()),
static_cast<const int*>(ne_mods.data_ptr()),
static_cast<const int*>(exclusive_ne_embeder_size_sums.data_ptr()),
static_cast<const int*>(ne_token_table.data_ptr()),
max_context_len,
static_cast<const int64_t*>(row_indices.data_ptr()),
static_cast<const int*>(column_starts.data_ptr()),
static_cast<int*>(n_gram_ids.data_ptr()));
}
static void update_token_table(
const tvm::ffi::TensorView tokens,
const tvm::ffi::TensorView ne_token_table,
@@ -17,6 +17,10 @@ def _jit_ngram_embedding_module() -> Module:
cuda_files=["ngram_embedding.cuh"],
cuda_wrappers=[
("compute_n_gram_ids", "&NgramEmbeddingKernel::compute_n_gram_ids"),
(
"compute_n_gram_ids_decode",
"&NgramEmbeddingKernel::compute_n_gram_ids_decode",
),
("update_token_table", "&NgramEmbeddingKernel::update_token_table"),
],
)
@@ -68,6 +72,35 @@ def compute_n_gram_ids(
)
@debug_kernel_api
def compute_n_gram_ids_decode(
ne_n: int,
ne_k: int,
ne_weights: torch.Tensor,
ne_mods: torch.Tensor,
exclusive_ne_embedder_size_sums: torch.Tensor,
ne_token_table: torch.Tensor,
row_indices: torch.Tensor,
column_starts: torch.Tensor,
n_gram_ids: torch.Tensor,
) -> None:
"""
Compute n-gram IDs for decode, where each request contributes one token.
"""
module = _jit_ngram_embedding_module()
module.compute_n_gram_ids_decode(
ne_n,
ne_k,
ne_weights,
ne_mods,
exclusive_ne_embedder_size_sums,
ne_token_table,
row_indices,
column_starts,
n_gram_ids,
)
@debug_kernel_api
def update_token_table(
tokens: torch.Tensor,
@@ -0,0 +1,97 @@
import sys
import pytest
import torch
from sglang.jit_kernel.ngram_embedding import (
compute_n_gram_ids,
compute_n_gram_ids_decode,
)
from sglang.test.ci.ci_register import register_cuda_ci
register_cuda_ci(est_time=30, suite="base-b-kernel-unit-1-gpu-large")
def _make_ngram_params(ne_n: int, ne_k: int, vocab_size: int):
ne_weights = torch.zeros([ne_n - 1, ne_k, ne_n], dtype=torch.int32)
ne_mods = torch.zeros([ne_n - 1, ne_k], dtype=torch.int32)
exclusive_sums = torch.zeros([(ne_n - 1) * ne_k + 1], dtype=torch.int32)
for n in range(2, ne_n + 1):
for k in range(ne_k):
config_id = (n - 2) * ne_k + k
mod = 65537 + 2 * config_id
ne_mods[n - 2][k] = mod
exclusive_sums[config_id + 1] = exclusive_sums[config_id] + mod
for delta in range(ne_n):
ne_weights[n - 2][k][delta] = pow(vocab_size, delta, mod)
return (
ne_weights.cuda(),
ne_mods.cuda(),
exclusive_sums.cuda(),
)
@pytest.mark.parametrize("batch_size", [1, 2, 17, 128, 1024])
def test_compute_n_gram_ids_decode_matches_general(batch_size: int) -> None:
ne_n = 8
ne_k = 2
vocab_size = 32000
max_context_len = 1024
max_running_reqs = batch_size + 8
num_configs = (ne_n - 1) * ne_k
ne_weights, ne_mods, exclusive_sums = _make_ngram_params(ne_n, ne_k, vocab_size)
ne_token_table = torch.randint(
0,
vocab_size,
(max_running_reqs, max_context_len),
dtype=torch.int32,
device="cuda",
)
row_indices = torch.randperm(max_running_reqs, device="cuda")[:batch_size].to(
torch.int64
)
column_starts = torch.randint(
0, max_context_len, (batch_size,), dtype=torch.int32, device="cuda"
)
tokens = torch.empty(batch_size, dtype=torch.int32, device="cuda")
exclusive_req_len_sums = torch.arange(
batch_size + 1, dtype=torch.int32, device="cuda"
)
n_gram_ids_general = torch.empty(
(batch_size, num_configs), dtype=torch.int32, device="cuda"
)
n_gram_ids_decode = torch.empty_like(n_gram_ids_general)
compute_n_gram_ids(
ne_n=ne_n,
ne_k=ne_k,
ne_weights=ne_weights,
ne_mods=ne_mods,
exclusive_ne_embedder_size_sums=exclusive_sums,
tokens=tokens,
exclusive_req_len_sums=exclusive_req_len_sums,
ne_token_table=ne_token_table,
row_indices=row_indices,
column_starts=column_starts,
n_gram_ids=n_gram_ids_general,
)
compute_n_gram_ids_decode(
ne_n=ne_n,
ne_k=ne_k,
ne_weights=ne_weights,
ne_mods=ne_mods,
exclusive_ne_embedder_size_sums=exclusive_sums,
ne_token_table=ne_token_table,
row_indices=row_indices,
column_starts=column_starts,
n_gram_ids=n_gram_ids_decode,
)
torch.testing.assert_close(n_gram_ids_decode, n_gram_ids_general, atol=0, rtol=0)
if __name__ == "__main__":
sys.exit(pytest.main([__file__, "-v", "-s"]))
+38 -22
View File
@@ -2,7 +2,10 @@ import torch
from torch import nn
from torch.nn import Parameter
from sglang.jit_kernel.ngram_embedding import compute_n_gram_ids
from sglang.jit_kernel.ngram_embedding import (
compute_n_gram_ids,
compute_n_gram_ids_decode,
)
from sglang.srt.layers.dp_attention import is_dp_attention_enabled
from sglang.srt.layers.vocab_parallel_embedding import VocabParallelEmbedding
from sglang.srt.model_executor.forward_batch_info import ForwardBatch
@@ -135,27 +138,40 @@ class NgramEmbedding(torch.nn.Module):
or forward_batch.forward_mode.is_decode()
):
ngram_embedding_info = forward_batch.ngram_embedding_info
torch.cumsum(
ngram_embedding_info.req_lens,
dim=0,
dtype=torch.int32,
out=self.exclusive_req_len_sums[1 : 1 + forward_batch.batch_size],
)
compute_n_gram_ids(
ne_n=self.over_embedding_n,
ne_k=self.over_embedding_k,
ne_weights=self.oe_weights,
ne_mods=self.oe_mods,
tokens=input_ids.to(torch.int32),
exclusive_ne_embedder_size_sums=self.exclusive_oe_embedder_size_sums,
exclusive_req_len_sums=self.exclusive_req_len_sums[
: forward_batch.batch_size + 1
],
ne_token_table=ngram_embedding_info.token_table,
row_indices=forward_batch.req_pool_indices,
column_starts=ngram_embedding_info.column_starts,
n_gram_ids=self.oe_n_gram_ids[: len(input_ids)],
)
if forward_batch.forward_mode.is_decode():
compute_n_gram_ids_decode(
ne_n=self.over_embedding_n,
ne_k=self.over_embedding_k,
ne_weights=self.oe_weights,
ne_mods=self.oe_mods,
exclusive_ne_embedder_size_sums=self.exclusive_oe_embedder_size_sums,
ne_token_table=ngram_embedding_info.token_table,
row_indices=forward_batch.req_pool_indices,
column_starts=ngram_embedding_info.column_starts,
n_gram_ids=self.oe_n_gram_ids[: len(input_ids)],
)
else:
torch.cumsum(
ngram_embedding_info.req_lens,
dim=0,
dtype=torch.int32,
out=self.exclusive_req_len_sums[1 : 1 + forward_batch.batch_size],
)
compute_n_gram_ids(
ne_n=self.over_embedding_n,
ne_k=self.over_embedding_k,
ne_weights=self.oe_weights,
ne_mods=self.oe_mods,
tokens=input_ids.to(torch.int32),
exclusive_ne_embedder_size_sums=self.exclusive_oe_embedder_size_sums,
exclusive_req_len_sums=self.exclusive_req_len_sums[
: forward_batch.batch_size + 1
],
ne_token_table=ngram_embedding_info.token_table,
row_indices=forward_batch.req_pool_indices,
column_starts=ngram_embedding_info.column_starts,
n_gram_ids=self.oe_n_gram_ids[: len(input_ids)],
)
# [13, seq_len, hidden_dim]
all_hidden_states = torch.empty(