Optimize ngram decode token table update (#24756)

Co-authored-by: Codex <codex@example.com>
Co-authored-by: BBuf <xiaoyu.zhang@radixark.net>
This commit is contained in:
Xiaoyu Zhang
2026-06-06 14:13:45 +08:00
committed by GitHub
co-authored by Codex BBuf
parent 9da88e32e0
commit e513c13e2e
5 changed files with 235 additions and 30 deletions
@@ -0,0 +1,76 @@
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 (
update_token_table,
update_token_table_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")
MAX_CONTEXT_LEN = 4096
BATCH_SIZE_LIST = get_benchmark_range(
full_range=[1, 2, 8, 32, 128, 512, 1024, 2048, 4096],
ci_range=[32, 1024],
)
@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 update_token_table", "decode fast path"],
styles=[("blue", "-"), ("orange", "-")],
ylabel="us",
plot_name="ngram-update-token-table",
args={},
)
)
def benchmark(batch_size: int, provider: str):
max_running_reqs = batch_size + 8
tokens = torch.arange(batch_size, dtype=torch.int32, device=DEFAULT_DEVICE)
token_table = torch.empty(
(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
)
req_lens = torch.ones(batch_size, dtype=torch.int32, device=DEFAULT_DEVICE)
if provider == "general":
def fn():
update_token_table(
tokens,
token_table,
row_indices,
column_starts,
req_lens,
None,
)
else:
def fn():
update_token_table_decode(
tokens,
token_table,
row_indices,
column_starts,
)
return run_benchmark_no_cudagraph(fn)
if __name__ == "__main__":
benchmark.run(print_data=True)
@@ -15,21 +15,22 @@ namespace device::ngram_embedding {
constexpr int kDecodeBlockSize = 256;
constexpr int kMaxComputeNGramIdsDecodeBlocks = 65535;
constexpr int kMaxUpdateTokenTableDecodeBlocks = 1024;
__global__ void ComputeNGramIdsKernel(
int batch_size,
int ne_n,
int ne_k,
int* ne_weights, // [ne_n-1,ne_k,ne_n]
int* ne_mods, // [ne_n-1,ne_k]
int* exclusive_ne_embeder_size_sums, // [(ne_n-1)*ne_k]
int* tokens, // [token_num]
int* exclusive_req_len_sums, // [batch_size+1]
int* ne_token_table, // [max_running_reqs, max_context_len]
int max_context_len, // max_context_len
long* row_indices, // [batch_size]
int* column_starts, // [batch_size]
int* n_gram_ids // [ne_n-1,ne_k,token_num]
int* ne_weights, // [ne_n-1,ne_k,ne_n]
int* ne_mods, // [ne_n-1,ne_k]
int* exclusive_ne_embeder_size_sums, // [(ne_n-1)*ne_k]
int* tokens, // [token_num]
int* exclusive_req_len_sums, // [batch_size+1]
int* ne_token_table, // [max_running_reqs, max_context_len]
int max_context_len, // max_context_len
const int64_t* __restrict__ row_indices, // [batch_size]
int* column_starts, // [batch_size]
int* n_gram_ids // [ne_n-1,ne_k,token_num]
) {
// Determine which n, k, and request this block handles.
/**
@@ -62,11 +63,11 @@ __global__ void ComputeNGramIdsKernel(
for (int i = exclusive_req_len_sums[req_id] + threadIdx.x; i < exclusive_req_len_sums[req_id + 1]; i += blockDim.x) {
uint64_t n_gram_id = 0;
// Token offset within the current request
int current_token_offset = i - exclusive_req_len_sums[req_id];
const int64_t current_token_offset = i - exclusive_req_len_sums[req_id];
// Start index of this request in the token table; tokens before this belong to other requests
int req_token_table_index = row_indices[req_id] * max_context_len;
const int64_t req_token_table_index = row_indices[req_id] * static_cast<int64_t>(max_context_len);
// Position of the current token in the token table
int current_token_table_index = req_token_table_index + column_starts[req_id] + current_token_offset;
const int64_t current_token_table_index = req_token_table_index + column_starts[req_id] + current_token_offset;
for (int j = 0; j < n + 2; j++) {
if (current_token_table_index - j < req_token_table_index) {
// Out of this request's range, stop computing n_gram_id
@@ -134,14 +135,14 @@ __global__ void ComputeNGramIdsDecodeKernel(
__global__ void UpdateTokenTableKernel(
int batch_size,
int* tokens, // [token_num]
int* ne_token_table, // [max_running_reqs, max_context_len]
int max_context_len, // max_context_len
long* row_indices, // [batch_size]
int* column_starts, // [batch_size]
int* req_lens, // [batch_size]
int ignore_token_num, // number of tokens to ignore
int* ignore_tokens // [ignore_token_num]
int* tokens, // [token_num]
int* ne_token_table, // [max_running_reqs, max_context_len]
int max_context_len, // max_context_len
const int64_t* __restrict__ row_indices, // [batch_size]
int* column_starts, // [batch_size]
int* req_lens, // [batch_size]
int ignore_token_num, // number of tokens to ignore
int* ignore_tokens // [ignore_token_num]
) {
// Each block processes one request.
const int req_id = blockIdx.x % batch_size;
@@ -154,11 +155,11 @@ __global__ void UpdateTokenTableKernel(
// stride loop
for (int i = start + threadIdx.x; i < end; i += blockDim.x) {
// Token offset within the current request
int current_token_offset = i - start;
const int64_t current_token_offset = i - start;
// Start index of this request in the token table
int req_token_table_index = row_indices[req_id] * max_context_len;
const int64_t req_token_table_index = row_indices[req_id] * static_cast<int64_t>(max_context_len);
// Position of the current token in the token table
int current_token_table_index = req_token_table_index + column_starts[req_id] + current_token_offset;
const int64_t current_token_table_index = req_token_table_index + column_starts[req_id] + current_token_offset;
ne_token_table[current_token_table_index] = tokens[i];
for (int j = 0; j < ignore_token_num; j++) {
if (ignore_tokens[j] == tokens[i]) {
@@ -169,6 +170,21 @@ __global__ void UpdateTokenTableKernel(
}
}
__global__ void UpdateTokenTableDecodeKernel(
int batch_size,
const int* __restrict__ tokens, // [batch_size]
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]
) {
for (int req_id = blockIdx.x * blockDim.x + threadIdx.x; req_id < batch_size; req_id += blockDim.x * gridDim.x) {
const int64_t token_table_offset =
row_indices[req_id] * static_cast<int64_t>(max_context_len) + column_starts[req_id];
ne_token_table[token_table_offset] = tokens[req_id];
}
}
} // namespace device::ngram_embedding
namespace {
@@ -256,7 +272,7 @@ struct NgramEmbeddingKernel {
static_cast<int*>(exclusive_req_len_sums.data_ptr()),
static_cast<int*>(ne_token_table.data_ptr()),
max_context_len,
static_cast<long*>(row_indices.data_ptr()),
static_cast<const int64_t*>(row_indices.data_ptr()),
static_cast<int*>(column_starts.data_ptr()),
static_cast<int*>(n_gram_ids.data_ptr()));
}
@@ -412,12 +428,64 @@ struct NgramEmbeddingKernel {
static_cast<int*>(tokens.data_ptr()),
static_cast<int*>(ne_token_table.data_ptr()),
max_context_len,
static_cast<long*>(row_indices.data_ptr()),
static_cast<const int64_t*>(row_indices.data_ptr()),
static_cast<int*>(column_starts.data_ptr()),
static_cast<int*>(req_lens.data_ptr()),
ignore_token_num,
ignore_tokens_typed_ptr);
}
static void update_token_table_decode(
const tvm::ffi::TensorView tokens,
const tvm::ffi::TensorView ne_token_table,
const tvm::ffi::TensorView row_indices,
const tvm::ffi::TensorView column_starts) {
using namespace host;
auto batch_size = SymbolicSize{"batch_size"};
auto device_ = SymbolicDevice{};
TensorMatcher({batch_size}) // [batch_size]
.with_dtype<int32_t>()
.with_device<kDLCUDA>(device_)
.verify(tokens);
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);
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 auto stream = LaunchKernel::resolve_device(device_.unwrap());
constexpr int kBlockSize = device::ngram_embedding::kDecodeBlockSize;
const int grid_size = std::min(
device::ngram_embedding::kMaxUpdateTokenTableDecodeBlocks, static_cast<int>(host::div_ceil(bs, kBlockSize)));
LaunchKernel(grid_size, kBlockSize, stream)(
device::ngram_embedding::UpdateTokenTableDecodeKernel,
bs,
static_cast<const int*>(tokens.data_ptr()),
static_cast<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()));
}
};
} // namespace
@@ -22,6 +22,10 @@ def _jit_ngram_embedding_module() -> Module:
"&NgramEmbeddingKernel::compute_n_gram_ids_decode",
),
("update_token_table", "&NgramEmbeddingKernel::update_token_table"),
(
"update_token_table_decode",
"&NgramEmbeddingKernel::update_token_table_decode",
),
],
)
@@ -133,3 +137,24 @@ def update_token_table(
req_lens,
ignore_tokens,
)
@debug_kernel_api
def update_token_table_decode(
tokens: torch.Tensor,
ne_token_table: torch.Tensor,
row_indices: torch.Tensor,
column_starts: torch.Tensor,
) -> None:
"""
Update one decoded token per request in the ngram embedding token table.
This is the decode-only fast path for req_lens == 1 and no ignored tokens.
"""
module = _jit_ngram_embedding_module()
module.update_token_table_decode(
tokens,
ne_token_table,
row_indices,
column_starts,
)
@@ -6,6 +6,8 @@ import torch
from sglang.jit_kernel.ngram_embedding import (
compute_n_gram_ids,
compute_n_gram_ids_decode,
update_token_table,
update_token_table_decode,
)
from sglang.test.ci.ci_register import register_cuda_ci
@@ -93,5 +95,41 @@ def test_compute_n_gram_ids_decode_matches_general(batch_size: int) -> None:
torch.testing.assert_close(n_gram_ids_decode, n_gram_ids_general, atol=0, rtol=0)
@pytest.mark.parametrize("batch_size", [1, 2, 17, 128, 1024])
def test_update_token_table_decode_matches_general(batch_size: int) -> None:
max_context_len = 4096
max_running_reqs = batch_size + 8
tokens = torch.arange(batch_size, dtype=torch.int32, device="cuda") + 100
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"
)
req_lens = torch.ones(batch_size, dtype=torch.int32, device="cuda")
token_table_general = torch.full(
(max_running_reqs, max_context_len), -1, dtype=torch.int32, device="cuda"
)
token_table_decode = token_table_general.clone()
update_token_table(
tokens=tokens,
ne_token_table=token_table_general,
row_indices=row_indices,
column_starts=column_starts,
req_lens=req_lens,
ignore_tokens=None,
)
update_token_table_decode(
tokens=tokens,
ne_token_table=token_table_decode,
row_indices=row_indices,
column_starts=column_starts,
)
torch.testing.assert_close(token_table_decode, token_table_general, atol=0, rtol=0)
if __name__ == "__main__":
sys.exit(pytest.main([__file__, "-v", "-s"]))
@@ -34,7 +34,7 @@ import torch
import torch.distributed as dist
from torch import nn
from sglang.jit_kernel.ngram_embedding import update_token_table
from sglang.jit_kernel.ngram_embedding import update_token_table_decode
from sglang.srt.configs import (
BailingHybridConfig,
FalconH1Config,
@@ -2840,13 +2840,11 @@ class ModelRunner(ModelRunnerKVCacheMixin):
forward_batch.seq_lens
)
ngram_embedding_info.out_req_lens[: forward_batch.batch_size] = 1
update_token_table(
update_token_table_decode(
ne_token_table=ngram_embedding_info.token_table,
tokens=next_token_ids.to(torch.int32),
row_indices=forward_batch.req_pool_indices,
column_starts=ngram_embedding_info.out_column_starts,
req_lens=torch.ones_like(ngram_embedding_info.out_column_starts),
ignore_tokens=None,
)
def init_device_graphs(self):