Files
wehub-resource-sync 770d92cb1f
Lint / lint (push) Has been cancelled
Build Docs / Deploy Docs (push) Has been cancelled
Windows CI / Windows (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:23:58 +08:00

391 lines
16 KiB
Python

"""The pass that attaches GPU sampler functions to the IRModule."""
from typing import Dict # noqa: UP035
import tvm
from tvm import IRModule, relax, te, tirx
from tvm.relax.frontend import nn
from tvm.script import tirx as T
from mlc_llm.op.batch_spec_verify import batch_spec_verify
from mlc_llm.op.top_p_pivot import top_p_pivot, top_p_renorm
@tvm.transform.module_pass(opt_level=0, name="AttachGPUSamplingFunc")
class AttachGPUSamplingFunc:
"""Attach GPU sampling functions to IRModule."""
def __init__(self, target: tvm.target.Target, variable_bounds: Dict[str, int]): # noqa: UP006
# Specifically for RWKV workloads, which contains -1 max_seq_len
max_batch_size = variable_bounds["batch_size"]
self.variable_bounds = {
"batch_size": max_batch_size,
"num_samples": max_batch_size,
"num_positions": 6 * max_batch_size,
}
self.non_negative_var = ["vocab_size"]
self.target = target
def transform_module(self, mod: IRModule, _ctx: tvm.transform.PassContext) -> IRModule:
"""Entrypoint"""
target_kind = self.target.kind.name
if target_kind not in ["cuda", "vulkan", "metal", "webgpu"]:
# Only enable GPU sampling for CUDA, Vulkan, Metal, and WebGPU.
return mod
bb = relax.BlockBuilder(mod)
if target_kind == "webgpu":
# Only attach functions that do not contain i8s for WebGPU
gv_names = [
gv.name_hint
for gv in [
_attach_argsort_func(bb),
_attach_sample_with_top_p(bb),
]
]
else:
gv_names = [
gv.name_hint
for gv in [
_attach_multinomial_sampling_func(bb),
_attach_argsort_func(bb),
_attach_sample_with_top_p(bb),
_attach_take_probs_func(bb),
_attach_batch_verifier(bb),
_attach_renormalize_by_top_p(bb, self.target),
]
]
mod = bb.finalize()
for gv_name in gv_names:
mod[gv_name] = (
mod[gv_name]
.with_attr("tir_var_upper_bound", self.variable_bounds)
.with_attr("tir_non_negative_var", self.non_negative_var)
)
return mod
def _attach_multinomial_sampling_func(bb: relax.BlockBuilder):
batch_size = tirx.Var("batch_size", "int64")
num_samples = tirx.Var("num_samples", "int64")
vocab_size = tirx.Var("vocab_size", "int64")
probs = relax.Var("probs", relax.TensorType((batch_size, vocab_size), "float32"))
uniform_samples = relax.Var("uniform_samples", relax.TensorType((num_samples,), "float32"))
sample_indices = relax.Var("sample_indices", relax.TensorType((num_samples,), "int32"))
with bb.function("multinomial_from_uniform", [probs, uniform_samples, sample_indices]):
with bb.dataflow():
sample_shape = relax.ShapeExpr([num_samples, 1])
probs_tensor = nn.wrap_nested(probs, name="probs")
uniform_samples_tensor = nn.wrap_nested(
relax.call_pure_packed(
"vm.builtin.reshape",
uniform_samples,
sample_shape,
ty_args=relax.TensorType(sample_shape, "float32"),
),
name="uniform_samples",
)
sample_indices_tensor = nn.wrap_nested(
relax.call_pure_packed(
"vm.builtin.reshape",
sample_indices,
sample_shape,
ty_args=relax.TensorType(sample_shape, "int32"),
),
name="sample_indices",
)
result_tensor = nn.multinomial_from_uniform(
probs_tensor,
uniform_samples_tensor,
sample_indices_tensor,
"int32",
name="nn_multinomial_from_uniform",
)
result = bb.emit(
relax.call_pure_packed(
"vm.builtin.reshape",
result_tensor._expr,
sample_indices.ty.shape,
ty_args=sample_indices.ty,
)
)
output = bb.emit_output(result)
gv = bb.emit_func_output(output)
return gv
def _attach_argsort_func(bb: relax.BlockBuilder):
batch_size = tirx.Var("batch_size", "int64")
vocab_size = tirx.Var("vocab_size", "int64")
probs = relax.Var("probs", relax.TensorType((batch_size, vocab_size), "float32"))
with bb.function("argsort_probs", [probs]):
with bb.dataflow():
sorted_indices = bb.emit(relax.op.argsort(probs, descending=True, dtype="int32"))
sorted_values = bb.emit_te(
lambda unsorted_probs, sorted_indices: te.compute(
(batch_size, vocab_size),
lambda i, j: unsorted_probs[i, sorted_indices[i, j]],
name="take_sorted_probs",
),
probs,
sorted_indices,
primfunc_name_hint="take_sorted_probs",
)
output = bb.emit_output((sorted_values, sorted_indices))
gv = bb.emit_func_output(output)
return gv
@T.prim_func(s_tir=True)
def full(var_result: T.handle, value: T.int32):
"""The filling function for top k."""
batch_size = T.int32()
result = T.match_buffer(var_result, (batch_size, 1), "int32")
for i in T.serial(batch_size):
with T.sblock("block"):
vi = T.axis.spatial(batch_size, i)
result[vi, 0] = value
def _attach_sample_with_top_p(bb: relax.BlockBuilder):
batch_size = tirx.Var("batch_size", "int64")
num_samples = tirx.Var("num_samples", "int64")
vocab_size = tirx.Var("vocab_size", "int64")
sorted_probs = relax.Var("sorted_probs", relax.TensorType((batch_size, vocab_size), "float32"))
sorted_indices = relax.Var(
"sorted_indices", relax.TensorType((batch_size, vocab_size), "int32")
)
uniform_samples = relax.Var("uniform_samples", relax.TensorType((num_samples,), "float32"))
sample_indices = relax.Var("sample_indices", relax.TensorType((num_samples,), "int32"))
top_p = relax.Var("top_p", relax.TensorType((batch_size,), "float32"))
with bb.function(
"sample_with_top_p",
[sorted_probs, sorted_indices, uniform_samples, sample_indices, top_p],
):
with bb.dataflow():
sample_shape = relax.ShapeExpr([num_samples, 1])
top_p_shape = relax.ShapeExpr([batch_size, 1])
sorted_probs_tensor = nn.wrap_nested(sorted_probs, name="sorted_probs")
sorted_indices_tensor = nn.wrap_nested(sorted_indices, name="sorted_indices")
uniform_samples_tensor = nn.wrap_nested(
relax.call_pure_packed(
"vm.builtin.reshape",
uniform_samples,
sample_shape,
ty_args=relax.TensorType(sample_shape, "float32"),
),
name="uniform_samples",
)
sample_indices_tensor = nn.wrap_nested(
relax.call_pure_packed(
"vm.builtin.reshape",
sample_indices,
sample_shape,
ty_args=relax.TensorType(sample_shape, "int32"),
),
name="sample_indices",
)
top_p_tensor = nn.wrap_nested(
relax.call_pure_packed(
"vm.builtin.reshape",
top_p,
top_p_shape,
ty_args=relax.TensorType(top_p_shape, "float32"),
),
name="sample_indices",
)
top_k_tensor = nn.tensor_ir_op(
full,
name_hint="full",
args=[vocab_size],
out=nn.Tensor.placeholder(
[batch_size, 1],
"int32",
),
)
result_tensor = nn.sample_top_p_top_k_from_sorted_prob(
sorted_probs_tensor,
sorted_indices_tensor,
top_p_tensor,
top_k_tensor,
uniform_samples_tensor,
sample_indices_tensor,
)
result = bb.emit_output(
relax.call_pure_packed(
"vm.builtin.reshape",
result_tensor._expr,
sample_indices.ty.shape,
ty_args=sample_indices.ty,
)
)
gv = bb.emit_func_output(result)
return gv
def _attach_renormalize_by_top_p(bb: relax.BlockBuilder, target: tvm.target.Target):
batch_size = tirx.Var("batch_size", "int64")
vocab_size = tirx.Var("vocab_size", "int64")
num_pivots = 3
probs = relax.Var("probs", relax.TensorType((batch_size, vocab_size), "float32"))
top_p = relax.Var("top_p", relax.TensorType((batch_size,), "float32"))
init_pivots = relax.Var("init_pivots", relax.TensorType((batch_size, num_pivots), "float32"))
with bb.function("renormalize_by_top_p", [probs, top_p, init_pivots]):
with bb.dataflow():
cutoff_output = bb.emit(
relax.call_tir(
bb.add_func(top_p_pivot(num_pivots, target), "top_p_pivot_cutoff"),
args=[probs, top_p, init_pivots],
out_ty=[top_p.ty, top_p.ty],
)
)
final_pivot = cutoff_output[0]
renorm_sum = cutoff_output[1]
renormalized_probs = bb.emit_output(
relax.call_tir(
bb.add_func(top_p_renorm(target), "top_p_renorm_after_cutoff"),
args=[probs, final_pivot, renorm_sum],
out_ty=probs.ty,
)
)
gv = bb.emit_func_output(renormalized_probs)
return gv
def _attach_take_probs_func(bb: relax.BlockBuilder):
batch_size = tirx.Var("batch_size", "int64")
num_samples = tirx.Var("num_samples", "int64")
num_positions = tirx.Var("num_positions", "int64")
vocab_size = tirx.Var("vocab_size", "int64")
unsorted_probs = relax.Var(
"unsorted_probs", relax.TensorType((batch_size, vocab_size), "float32")
)
sorted_indices = relax.Var(
"sorted_indices", relax.TensorType((batch_size, vocab_size), "int32")
)
sample_indices = relax.Var("sample_indices", relax.TensorType((num_samples,), "int32"))
sampling_results = relax.Var("sampling_result", relax.TensorType((num_samples,), "int32"))
top_prob_offsets = relax.Var("lobprob_offsets", relax.TensorType((num_positions,), "int32"))
@T.prim_func(s_tir=True)
def sampler_take_probs_tir(
var_unsorted_probs: T.handle,
var_sorted_indices: T.handle,
var_sample_indices: T.handle,
var_sampling_results: T.handle,
var_top_prob_offsets: T.handle,
var_sampled_values: T.handle,
var_top_prob_probs: T.handle,
var_top_prob_indices: T.handle,
):
batch_size = T.int32()
num_samples = T.int32()
num_positions = T.int32()
vocab_size = T.int32()
unsorted_probs = T.match_buffer(var_unsorted_probs, (batch_size, vocab_size), "float32")
sorted_indices = T.match_buffer(var_sorted_indices, (batch_size, vocab_size), "int32")
sample_indices = T.match_buffer(var_sample_indices, (num_samples,), "int32")
sampling_results = T.match_buffer(var_sampling_results, (num_samples,), "int32")
top_prob_offsets = T.match_buffer(var_top_prob_offsets, (num_positions,), "int32")
sampled_values = T.match_buffer(var_sampled_values, (num_samples,), "float32")
top_prob_probs = T.match_buffer(var_top_prob_probs, (num_positions,), "float32")
top_prob_indices = T.match_buffer(var_top_prob_indices, (num_positions,), "int32")
for i in T.serial(num_positions):
with T.sblock("top_prob"):
vi = T.axis.spatial(num_positions, i)
# Reads are data-dependent gathers; declare full-buffer read
# regions explicitly so tirx does not infer data-dependent regions.
T.reads(
top_prob_offsets[vi],
sorted_indices[0:batch_size, 0:vocab_size],
unsorted_probs[0:batch_size, 0:vocab_size],
)
T.writes(top_prob_indices[vi], top_prob_probs[vi])
row = T.floordiv(top_prob_offsets[vi], vocab_size)
col = T.floormod(top_prob_offsets[vi], vocab_size)
top_prob_indices[vi] = sorted_indices[row, col]
top_prob_probs[vi] = unsorted_probs[row, sorted_indices[row, col]]
for i in T.serial(num_samples):
with T.sblock("sample"):
vj = T.axis.spatial(num_samples, i)
T.reads(
sample_indices[vj],
sampling_results[vj],
unsorted_probs[0:batch_size, 0:vocab_size],
)
T.writes(sampled_values[vj])
sampled_values[vj] = unsorted_probs[sample_indices[vj], sampling_results[vj]]
args = [
unsorted_probs,
sorted_indices,
sample_indices,
sampling_results,
top_prob_offsets,
]
with bb.function("sampler_take_probs", args):
with bb.dataflow():
taken_probs_indices = bb.emit_output(
relax.call_tir(
bb.add_func(sampler_take_probs_tir, "sampler_take_probs_tir"),
args,
out_ty=[
relax.TensorType((num_samples,), "float32"),
relax.TensorType((num_positions,), "float32"),
relax.TensorType((num_positions,), "int32"),
],
)
)
gv = bb.emit_func_output(taken_probs_indices)
return gv
def _attach_batch_verifier(bb: relax.BlockBuilder):
num_nodes = tirx.Var("num_nodes", "int64")
nbatch = tirx.Var("nbatch", "int64")
vocab_size = tirx.Var("vocab_size", "int64")
draft_probs = relax.Var("draft_probs", relax.TensorType((num_nodes, vocab_size), "float32"))
draft_tokens = relax.Var("draft_tokens", relax.TensorType((num_nodes,), "int32"))
model_probs = relax.Var("model_probs", relax.TensorType((num_nodes, vocab_size), "float32"))
token_tree_first_child = relax.Var(
"token_tree_first_child", relax.TensorType((num_nodes,), "int32")
)
token_tree_next_sibling = relax.Var(
"token_tree_next_sibling", relax.TensorType((num_nodes,), "int32")
)
uniform_samples = relax.Var("uniform_samples", relax.TensorType((num_nodes,), "float32"))
token_tree_parent_ptr = relax.Var("token_tree_parent_ptr", relax.TensorType((nbatch,), "int32"))
args = [
draft_probs,
draft_tokens,
model_probs,
token_tree_first_child,
token_tree_next_sibling,
uniform_samples,
token_tree_parent_ptr,
]
with bb.function("sampler_verify_draft_tokens", args):
with bb.dataflow():
res = bb.emit_output(
relax.call_tir_inplace(
bb.add_func(
batch_spec_verify(vocab_size),
"batch_verify_on_gpu_single_kernel",
),
args,
inplace_indices=[
args.index(model_probs),
args.index(token_tree_parent_ptr),
],
out_ty=[
model_probs.ty,
token_tree_parent_ptr.ty,
],
)
)
gv = bb.emit_func_output(res)
return gv